diff --git a/app/api/og/blog/route.tsx b/app/api/og/blog/route.tsx new file mode 100644 index 0000000..2cf5b20 --- /dev/null +++ b/app/api/og/blog/route.tsx @@ -0,0 +1,268 @@ +/* eslint-disable @next/next/no-img-element */ +import { ImageResponse } from "next/og"; + +async function loadGoogleFont(font: string, text: string) { + const url = `https://fonts.googleapis.com/css2?family=${font}:wght@600&text=${encodeURIComponent(text)}`; + const css = await (await fetch(url)).text(); + const resource = css.match( + /src: url\((.+)\) format\('(opentype|truetype)'\)/, + ); + + if (resource) { + const response = await fetch(resource[1]); + if (response.status == 200) { + return await response.arrayBuffer(); + } + } + + throw new Error("failed to load font data"); +} + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + + const title = searchParams.get("title"); + + return new ImageResponse( + ( +
+
+
+
+
+ +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ {title} +
+
+ ), + { + width: 1200, + height: 630, + fonts: [ + { + name: "Geist", + weight: 600, + data: await loadGoogleFont("Geist", title || ""), + style: "normal", + }, + ], + }, + ); + } catch (error: unknown) { + return new Response("Failed to generate OG image.", { status: 500 }); + } +} diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx index 2db39ac..3d2668c 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[slug]/page.tsx @@ -2,12 +2,12 @@ import React from "react"; // @components import Link from "next/link"; -import Image from "next/image"; -import { Code } from "bright"; import { MDXRemote } from "next-mdx-remote/rsc"; import { CodeBlock } from "@/components/code-block"; import { Typography } from "@/components/typography"; import { BlogPostCard } from "@/components/blog-post-card"; +import { BlogCopyLink } from "@/components/blog-copy-link"; +import { BrowserWindow } from "@/components/browser-window"; // @icons import { RiArrowLeftLine } from "@remixicon/react"; @@ -18,12 +18,55 @@ import matter from "gray-matter"; import remarkGfm from "remark-gfm"; import { formatDate } from "@/lib/utils"; import { notFound } from "next/navigation"; -import { BlogCopyLink } from "@/components/blog-copy-link"; +import rehypePrettyCode from "rehype-pretty-code"; +import { generateMetadata as generateMetadataFn } from "@/lib/utils"; + +// @types +import type { Metadata } from "next"; + +const isProd = process.env.NODE_ENV === "production"; -Code.theme = { - dark: "github-dark", - light: "github-light", -}; +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }>; +}): Promise { + const { slug } = await params; + const { frontMatter } = await readMdxFile(slug); + + const domain = new URL( + isProd + ? process.env.NEXT_PUBLIC_PROD_URL! + : process.env.NEXT_PUBLIC_DEV_URL!, + ); + + const imageUrl = `${domain}/api/og/blog?title=${encodeURIComponent(frontMatter.title)}`; + + return generateMetadataFn({ + title: frontMatter.title, + description: frontMatter.description, + keywords: frontMatter.keywords, + alternates: { + canonical: `${domain}/${slug}`, + }, + openGraph: { + type: "article", + title: frontMatter.title, + description: frontMatter.description, + images: [ + { + url: imageUrl, + }, + ], + }, + twitter: { + card: "summary_large_image", + title: frontMatter.title, + description: frontMatter.description, + images: imageUrl, + }, + }); +} export default async function Post({ params, @@ -46,7 +89,7 @@ export default async function Post({ return (
-
+
-
+ @@ -103,7 +162,7 @@ export default async function Post({ <>

Continue your journey with these related posts

diff --git a/app/globals.css b/app/globals.css index 3391134..ffb06a1 100644 --- a/app/globals.css +++ b/app/globals.css @@ -39,6 +39,12 @@ scrollbar-width: none; } -.code-block pre *::selection { - @apply !bg-primary !text-white; +code[data-theme], +code[data-theme] span { + color: var(--shiki-light); +} + +.dark code[data-theme], +.dark code[data-theme] span { + color: var(--shiki-dark); } diff --git a/package.json b/package.json index 755ab64..8a3a57f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "@radix-ui/react-tooltip": "1.1.6", "@remixicon/react": "4.6.0", "@tailwindcss/postcss": "4.0.1", - "bright": "1.0.0", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "gray-matter": "4.0.3", @@ -34,8 +33,10 @@ "react-dom": "19.0.0", "react-fast-marquee": "1.6.5", "react-hook-form": "7.54.1", + "rehype-pretty-code": "0.14.0", "remark-gfm": "4.0.0", "resend": "4.0.1", + "shiki": "2.3.2", "tailwind-merge": "2.5.5", "usehooks-ts": "3.1.0", "zod": "3.24.1"