@@ -22,20 +22,72 @@ import {
2222 PlayIcon ,
2323} from 'lucide-react'
2424import Link from 'next/link'
25+ import { Metadata } from 'next/types'
2526import React from 'react'
2627
2728import { useNonce } from '@/components/context/nonce-context'
2829import { Flex } from '@/components/flex'
2930import { Page , PageHeading , PageSection } from '@/components/page'
31+ import config from '@/data/config.json'
32+ import snippets from '@/data/snippets.json'
3033import { getSnippetBySlug , Snippet } from '@/lib/snippets'
3134
3235const 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}
0 commit comments