Skip to content

Commit 639981c

Browse files
committed
feat: enhance article update flow and add canonical URL support in ArticleEditor
1 parent cef9822 commit 639981c

File tree

5 files changed

+53
-27
lines changed

5 files changed

+53
-27
lines changed

src/app/tags/[tag_id]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,4 @@ export async function generateMetadata({ params }: TagPageProps) {
3838
description: `Browse all articles with this tag on Tech Diary`,
3939
},
4040
};
41-
}
41+
}

src/backend/services/article.actions.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,12 @@ export async function updateMyArticle(
179179
});
180180
}
181181

182-
return article?.rows?.[0];
182+
return {
183+
success: true as const,
184+
data: article?.rows?.[0],
185+
};
183186
} catch (error) {
184-
handleActionException(error);
187+
return handleActionException(error);
185188
}
186189
}
187190

src/backend/services/inputs/article.input.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,11 @@ export const ArticleRepositoryInput = {
7676
title: z.string().optional(),
7777
description: z.string().optional(),
7878
keywords: z.array(z.string()).optional(),
79-
// canonical_url: z.string().url().nullable().optional(),
80-
// canonical_url: z
81-
// .union([z.string().url(), z.literal(""), z.null()])
82-
// .optional()
83-
// .nullable()
84-
// .transform((val) => (val === "" ? null : val)),
79+
canonical_url: z
80+
.union([z.string().url(), z.literal(""), z.null()])
81+
.optional()
82+
.nullable()
83+
.transform((val) => (val === "" ? null : val)),
8584
})
8685
.nullable()
8786
.optional(),

src/components/Editor/ArticleEditor.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { ArticleRepositoryInput } from "@/backend/services/inputs/article.input"
1919
import { useAutosizeTextArea } from "@/hooks/use-auto-resize-textarea";
2020
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
2121
import { useToggle } from "@/hooks/use-toggle";
22-
import { formattedTime } from "@/lib/utils";
22+
import { actionPromisify, formattedTime } from "@/lib/utils";
2323
import { markdocParser } from "@/utils/markdoc-parser";
2424
import { useMutation } from "@tanstack/react-query";
2525
import clsx from "clsx";
@@ -62,7 +62,7 @@ const ArticleEditor: React.FC<ArticleEditorProps> = ({ article, uuid }) => {
6262
const updateMyArticleMutation = useMutation({
6363
mutationFn: (
6464
input: z.infer<typeof ArticleRepositoryInput.updateMyArticleInput>
65-
) => articleActions.updateMyArticle(input),
65+
) => actionPromisify(articleActions.updateMyArticle(input)),
6666
onSuccess: () => router.refresh(),
6767
onError: (err) => alert(err.message),
6868
});

src/components/Editor/ArticleEditorDrawer.tsx

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
import { Input } from "../ui/input";
3030
import { Sheet, SheetContent } from "../ui/sheet";
3131
import { Textarea } from "../ui/textarea";
32+
import { actionPromisify } from "@/lib/utils";
3233

3334
interface Props {
3435
article: Article;
@@ -43,9 +44,10 @@ const ArticleEditorDrawer: React.FC<Props> = ({ article, open, onClose }) => {
4344
const updateMyArticleMutation = useMutation({
4445
mutationFn: (
4546
input: z.infer<typeof ArticleRepositoryInput.updateMyArticleInput>
46-
) => {
47-
return articleActions.updateMyArticle(input);
48-
},
47+
) =>
48+
actionPromisify(articleActions.updateMyArticle(input), {
49+
enableToast: true,
50+
}),
4951
onSuccess: () => {
5052
router.refresh();
5153
},
@@ -79,6 +81,7 @@ const ArticleEditorDrawer: React.FC<Props> = ({ article, open, onClose }) => {
7981
title: article?.metadata?.seo?.title ?? "",
8082
description: article?.metadata?.seo?.description ?? "",
8183
keywords: article?.metadata?.seo?.keywords ?? [],
84+
canonical_url: article?.metadata?.seo?.canonical_url ?? "",
8285
},
8386
},
8487
},
@@ -98,6 +101,7 @@ const ArticleEditorDrawer: React.FC<Props> = ({ article, open, onClose }) => {
98101
title: payload.metadata?.seo?.title ?? "",
99102
description: payload.metadata?.seo?.description ?? "",
100103
keywords: payload.metadata?.seo?.keywords ?? [],
104+
canonical_url: payload.metadata?.seo?.canonical_url ?? "",
101105
},
102106
},
103107
});
@@ -112,10 +116,10 @@ const ArticleEditorDrawer: React.FC<Props> = ({ article, open, onClose }) => {
112116
onSubmit={form.handleSubmit(handleOnSubmit)}
113117
className="flex flex-col gap-2"
114118
>
115-
{JSON.stringify({
119+
{/* {JSON.stringify({
116120
errors: form.formState.errors,
117121
values: form.watch("tag_ids"),
118-
})}
122+
})} */}
119123
{/* <pre>{JSON.stringify(article, null, 2)}</pre> */}
120124
<div className="flex flex-col gap-6">
121125
<FormField
@@ -247,11 +251,9 @@ const ArticleEditorDrawer: React.FC<Props> = ({ article, open, onClose }) => {
247251
<FormItem>
248252
<FormLabel>{_t("SEO Title")}</FormLabel>
249253
<FormDescription className="text-xs">
250-
The &quot;SEO title&quot; will be shown in place of your
251-
Title on search engine results pages, such as a Google
252-
search. SEO titles between 40 and 50 characters with
253-
commonly searched words have the best
254-
click-through-rates.
254+
{_t(
255+
"Override your article title for Google search results and social media cards. Keep it 40-50 characters for best performance."
256+
)}
255257
</FormDescription>
256258
<FormControl>
257259
<Input {...field} placeholder={form.watch("title")} />
@@ -261,7 +263,7 @@ const ArticleEditorDrawer: React.FC<Props> = ({ article, open, onClose }) => {
261263
)}
262264
/>
263265

264-
<FormField
266+
{/* <FormField
265267
control={form.control}
266268
name="metadata.seo.keywords"
267269
render={({ field }) => (
@@ -291,7 +293,7 @@ const ArticleEditorDrawer: React.FC<Props> = ({ article, open, onClose }) => {
291293
<FormMessage />
292294
</FormItem>
293295
)}
294-
/>
296+
/> */}
295297

296298
<FormField
297299
control={form.control}
@@ -300,10 +302,9 @@ const ArticleEditorDrawer: React.FC<Props> = ({ article, open, onClose }) => {
300302
<FormItem>
301303
<FormLabel>{_t("SEO Description")}</FormLabel>
302304
<FormDescription className="text-xs">
303-
The &quot;SEO description&quot; will be used in place of
304-
your Subtitle on search engine results pages. Good SEO
305-
descriptions utilize keywords, summarize the article and
306-
are between 140-156 characters long.
305+
Override your article description for Google search
306+
results and social media cards. Keep it 140-156
307+
characters and include relevant keywords.
307308
</FormDescription>
308309
<FormControl>
309310
<Textarea {...field} />
@@ -312,6 +313,29 @@ const ArticleEditorDrawer: React.FC<Props> = ({ article, open, onClose }) => {
312313
</FormItem>
313314
)}
314315
/>
316+
317+
<FormField
318+
control={form.control}
319+
name="metadata.seo.canonical_url"
320+
render={({ field }) => (
321+
<FormItem>
322+
<FormLabel>{_t("Canonical URL")}</FormLabel>
323+
<FormDescription className="text-xs">
324+
{_t(
325+
"Specify the preferred URL for this content to prevent duplicate content issues. Leave empty to use the default URL."
326+
)}
327+
</FormDescription>
328+
<FormControl>
329+
<Input
330+
{...field}
331+
value={field.value ?? ""}
332+
placeholder="https://example.com/original-post"
333+
/>
334+
</FormControl>
335+
<FormMessage />
336+
</FormItem>
337+
)}
338+
/>
315339
</div>
316340

317341
<Button

0 commit comments

Comments
 (0)