Skip to content

Commit a9b17b5

Browse files
committed
chore(blog): improved seo metadata
1 parent 9ce7670 commit a9b17b5

3 files changed

Lines changed: 194 additions & 92 deletions

File tree

apps/blog/src/app/(public)/blog/[collection]/[slug]/page.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,12 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
8787

8888
export default async function BlogArticle({ params }: Props) {
8989
const { collection, slug } = await params
90-
const { category, date, description, title, href, updatedAt } =
90+
const { category, date, description, tags, title, href, updatedAt } =
9191
getArticleBySlug(slug)
9292

9393
const MdxContent = await getContent(collection, slug)
94+
const fullUrl = config.baseUrl.concat(href)
95+
const ogImageUrl = `${config.baseUrl}/og?title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}`
9496

9597
return (
9698
<React.Fragment>
@@ -101,15 +103,32 @@ export default async function BlogArticle({ params }: Props) {
101103
__html: JSON.stringify({
102104
'@context': 'https://schema.org',
103105
'@type': 'BlogPosting',
106+
mainEntityOfPage: {
107+
'@type': 'WebPage',
108+
'@id': fullUrl,
109+
},
104110
headline: title,
105-
datePublished: date,
106-
dateModified: updatedAt,
107111
description: description,
108-
url: config.baseUrl.concat(href),
112+
image: [ogImageUrl],
109113
author: {
110114
'@type': 'Person',
111115
name: config.author,
116+
url: config.baseUrl,
112117
},
118+
publisher: {
119+
'@type': 'Organization',
120+
name: config.author,
121+
logo: {
122+
'@type': 'ImageObject',
123+
url: `${config.baseUrl}/favicon/android-chrome-512x512.png`,
124+
},
125+
},
126+
datePublished: date,
127+
dateModified: updatedAt ?? date,
128+
url: fullUrl,
129+
keywords: [collection, category, ...tags].join(', '),
130+
articleSection: category,
131+
inLanguage: 'en',
113132
}),
114133
}}
115134
/>

apps/blog/src/app/(public)/snippets/[slug]/page.tsx

Lines changed: 170 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,72 @@ import {
2222
PlayIcon,
2323
} from 'lucide-react'
2424
import Link from 'next/link'
25+
import { Metadata } from 'next/types'
2526
import React from 'react'
2627

2728
import { useNonce } from '@/components/context/nonce-context'
2829
import { Flex } from '@/components/flex'
2930
import { Page, PageHeading, PageSection } from '@/components/page'
31+
import config from '@/data/config.json'
32+
import snippets from '@/data/snippets.json'
3033
import { getSnippetBySlug, Snippet } from '@/lib/snippets'
3134

3235
const NONCE_HEADER = String('nonce')
3336

34-
export default function SnippetPage({
37+
type Props = {
38+
params: Promise<{ slug: string }>
39+
}
40+
41+
export async function generateStaticParams() {
42+
return snippets.map((file) => ({
43+
slug: file.slug,
44+
}))
45+
}
46+
47+
export async function generateMetadata({
3548
params,
3649
}: {
37-
params: Promise<{ slug: string }>
38-
}) {
50+
params: { slug: string }
51+
}): Promise<Metadata> {
52+
const snippet = getSnippetBySlug(params.slug)
53+
54+
const title = `${snippet.title} | Snippet by ${config.author}`
55+
const description = snippet.description
56+
const url = `${process.env.NEXT_PUBLIC_SITE_URL}/snippets/${snippet.slug}`
57+
58+
const ogImageUrl = `${process.env.NEXT_PUBLIC_SITE_URL}/og?title=${encodeURIComponent(snippet.title)}&description=${encodeURIComponent(snippet.description)}`
59+
60+
return {
61+
title,
62+
description,
63+
keywords: [snippet.language, 'code snippet', snippet.title.toLowerCase()],
64+
authors: [{ name: config.author, url: config.baseUrl }],
65+
alternates: {
66+
canonical: url,
67+
},
68+
openGraph: {
69+
title,
70+
description,
71+
url,
72+
type: 'article',
73+
locale: 'en_US',
74+
images: [{ url: ogImageUrl }],
75+
},
76+
twitter: {
77+
card: 'summary_large_image',
78+
title,
79+
description,
80+
images: [{ url: ogImageUrl }],
81+
creator: '@moatorres',
82+
},
83+
robots: {
84+
index: true,
85+
follow: true,
86+
},
87+
}
88+
}
89+
90+
export default function SnippetPage({ params }: Props) {
3991
const [snippet, setSnippet] = React.useState<Snippet | null>(null)
4092
const [loading, setLoading] = React.useState(true)
4193
const [copied, setCopied] = React.useState(false)
@@ -127,92 +179,122 @@ export default function SnippetPage({
127179
}
128180

129181
return (
130-
<Page>
131-
<PageSection>
132-
<PageHeading>{snippet.title}</PageHeading>
133-
134-
<Tabs defaultValue="code">
135-
{snippet && (
136-
<div className="px-0 space-y-4">
137-
{snippet.description && (
138-
<p className="text-muted-foreground">{snippet.description}</p>
139-
)}
140-
<div className="flex items-center gap-2">
141-
<Badge>{snippet.language}</Badge>
142-
<span className="text-sm text-muted-foreground">
143-
Created{' '}
144-
{formatDistanceToNow(new Date(snippet.createdAt), {
145-
addSuffix: true,
146-
})}
147-
</span>
148-
</div>
149-
<Flex className="flex-row justify-between">
150-
<TabsList>
151-
<TabsTrigger value="code">Code</TabsTrigger>
152-
<TabsTrigger value="execute">Execute</TabsTrigger>
153-
</TabsList>
154-
<Flex className="flex-row gap-2 align-middle">
155-
<Button size="sm" variant="outline" onClick={handleCopyCode}>
156-
{copied ? (
157-
<CheckIcon strokeWidth={1.625} />
158-
) : (
159-
<CopyIcon strokeWidth={1.625} />
160-
)}
161-
{copied ? 'Copied' : 'Copy'}
162-
</Button>
182+
<>
183+
{snippet && (
184+
<script
185+
type="application/ld+json"
186+
suppressHydrationWarning
187+
dangerouslySetInnerHTML={{
188+
__html: JSON.stringify({
189+
'@context': 'https://schema.org',
190+
'@type': 'SoftwareSourceCode',
191+
name: snippet.title,
192+
description: snippet.description,
193+
programmingLanguage: snippet.language,
194+
codeSampleType: 'full (compiled)',
195+
url: `${config.baseUrl}/snippets/${snippet.slug}`,
196+
author: {
197+
'@type': 'Person',
198+
name: 'Moa Torres',
199+
url: config.baseUrl,
200+
},
201+
dateCreated: new Date(snippet.createdAt).toISOString(),
202+
}),
203+
}}
204+
/>
205+
)}
206+
207+
<Page>
208+
<PageSection>
209+
<PageHeading>{snippet.title}</PageHeading>
210+
211+
<Tabs defaultValue="code">
212+
{snippet && (
213+
<div className="px-0 space-y-4">
214+
{snippet.description && (
215+
<p className="text-muted-foreground">{snippet.description}</p>
216+
)}
217+
<div className="flex items-center gap-2">
218+
<Badge>{snippet.language}</Badge>
219+
<span className="text-sm text-muted-foreground">
220+
Created{' '}
221+
{formatDistanceToNow(new Date(snippet.createdAt), {
222+
addSuffix: true,
223+
})}
224+
</span>
225+
</div>
226+
<Flex className="flex-row justify-between">
227+
<TabsList>
228+
<TabsTrigger value="code">Code</TabsTrigger>
229+
<TabsTrigger value="execute">Execute</TabsTrigger>
230+
</TabsList>
231+
<Flex className="flex-row gap-2 align-middle">
232+
<Button
233+
size="sm"
234+
variant="outline"
235+
onClick={handleCopyCode}
236+
>
237+
{copied ? (
238+
<CheckIcon strokeWidth={1.625} />
239+
) : (
240+
<CopyIcon strokeWidth={1.625} />
241+
)}
242+
{copied ? 'Copied' : 'Copy'}
243+
</Button>
244+
</Flex>
163245
</Flex>
164-
</Flex>
165-
166-
<TabsContent value="code">
167-
<ScrollArea className="h-[64vh] sm:h-[70vh] rounded-md no-scrollbar">
168-
<Code
169-
controls
170-
title={snippet.title}
171-
className="text-xs bg-(--color-zinc-100)/80 dark:bg-(--color-zinc-950) w-[90ch] md:w-fit"
172-
>
173-
{snippet.code}
174-
</Code>
175-
<ScrollBar orientation="horizontal" />
176-
</ScrollArea>
177-
</TabsContent>
178-
179-
<TabsContent value="execute">
180-
<div className="space-y-4">
181-
<Button
182-
onClick={handleExecute}
183-
size="sm"
184-
disabled={isRunning}
185-
>
186-
{isRunning ? (
187-
<Loader2Icon
188-
strokeWidth={1.625}
189-
className="animate-spin"
190-
/>
191-
) : (
192-
<PlayIcon strokeWidth={1.625} />
193-
)}
194-
{isRunning ? 'Running' : 'Run Snippet'}
195-
</Button>
196-
197-
<ScrollArea className="h-[64vh] rounded-md bg-muted p-4 text-sm font-mono whitespace-pre-wrap">
198-
<Code>{output.map((line) => line).join('\n')}</Code>
199-
<ScrollBar orientation="vertical" />
246+
247+
<TabsContent value="code">
248+
<ScrollArea className="h-[64vh] sm:h-[70vh] rounded-md no-scrollbar">
249+
<Code
250+
controls
251+
title={snippet.title}
252+
className="text-xs bg-(--color-zinc-100)/80 dark:bg-(--color-zinc-950) w-[90ch] md:w-fit"
253+
>
254+
{snippet.code}
255+
</Code>
256+
<ScrollBar orientation="horizontal" />
200257
</ScrollArea>
201-
</div>
202-
</TabsContent>
203-
</div>
204-
)}
205-
</Tabs>
206-
207-
<div className="mt-4">
208-
<Link href="/snippets">
209-
<Button variant="ghost" size="sm">
210-
<ArrowLeft className="h-4 w-4" />
211-
Back to snippets
212-
</Button>
213-
</Link>
214-
</div>
215-
</PageSection>
216-
</Page>
258+
</TabsContent>
259+
260+
<TabsContent value="execute">
261+
<div className="space-y-4">
262+
<Button
263+
onClick={handleExecute}
264+
size="sm"
265+
disabled={isRunning}
266+
>
267+
{isRunning ? (
268+
<Loader2Icon
269+
strokeWidth={1.625}
270+
className="animate-spin"
271+
/>
272+
) : (
273+
<PlayIcon strokeWidth={1.625} />
274+
)}
275+
{isRunning ? 'Running' : 'Run Snippet'}
276+
</Button>
277+
278+
<ScrollArea className="h-[64vh] rounded-md bg-muted p-4 text-sm font-mono whitespace-pre-wrap">
279+
<Code>{output.map((line) => line).join('\n')}</Code>
280+
<ScrollBar orientation="vertical" />
281+
</ScrollArea>
282+
</div>
283+
</TabsContent>
284+
</div>
285+
)}
286+
</Tabs>
287+
288+
<div className="mt-4">
289+
<Link href="/snippets">
290+
<Button variant="ghost" size="sm">
291+
<ArrowLeft className="h-4 w-4" />
292+
Back to snippets
293+
</Button>
294+
</Link>
295+
</div>
296+
</PageSection>
297+
</Page>
298+
</>
217299
)
218300
}

apps/blog/src/utils/bootstrap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export async function extractArticleMetadata(
4949
author: config.author,
5050
category: content.metadata.category,
5151
date: content.metadata.date,
52+
tags: content.metadata.tags,
5253
title: content.metadata.title,
5354
description: content.metadata.description,
5455
fileName,

0 commit comments

Comments
 (0)