diff --git a/apps/blog/app/(en)/essays/[slug]/opengraph-image.tsx b/apps/blog/app/(en)/essays/[slug]/opengraph-image.tsx new file mode 100644 index 0000000..8c64e87 --- /dev/null +++ b/apps/blog/app/(en)/essays/[slug]/opengraph-image.tsx @@ -0,0 +1,52 @@ +import { ImageResponse } from 'next/og'; +import { getEssayBySlug, getEssaySlugs } from '@/lib/essays'; +import { OgCard, OG_WIDTH, OG_HEIGHT } from '@/components/seo/og-card'; +import { ogFontsFor } from '@/lib/og-fonts'; +import { TOPIC_LABELS } from '@/lib/constants'; + +export const dynamic = 'force-static'; + +export const alt = 'Algo Mind essay'; +export const size = { width: OG_WIDTH, height: OG_HEIGHT }; +export const contentType = 'image/png'; + +export function generateStaticParams() { + return getEssaySlugs().map((slug) => ({ slug })); +} + +interface RouteProps { + params: { slug: string }; +} + +export default async function OgImage({ params }: RouteProps) { + const fonts = ogFontsFor('en'); + const essay = getEssayBySlug(params.slug); + if (!essay || essay.lang !== 'en') { + return new ImageResponse( + ( + + ), + { ...size, fonts }, + ); + } + + const kicker = essay.topics[0] ? TOPIC_LABELS[essay.topics[0]] : 'Essay'; + + return new ImageResponse( + ( + + ), + { ...size, fonts }, + ); +} diff --git a/apps/blog/app/(en)/essays/[slug]/page.tsx b/apps/blog/app/(en)/essays/[slug]/page.tsx index e05b5a0..6e221cf 100644 --- a/apps/blog/app/(en)/essays/[slug]/page.tsx +++ b/apps/blog/app/(en)/essays/[slug]/page.tsx @@ -5,6 +5,8 @@ import { getEssayBySlug, getEssaySlugs, getTranslation } from '@/lib/essays'; import { getMDXComponents } from '@/components/mdx/MDXComponents'; import { mdxOptions } from '@/lib/mdx-options'; import { EssayLayout, EssayHeader } from '@/components/essay'; +import { JsonLd } from '@/components/seo'; +import { breadcrumbSchema, essayPostingSchema } from '@/lib/jsonld'; interface EssayPageProps { params: Promise<{ slug: string }>; @@ -53,6 +55,13 @@ export async function generateMetadata({ alternates.languages!['zh'] = `/zh/essays/${zhTranslation.slug}`; } + // If the essay declares a hand-picked image, override the autogenerated + // opengraph-image. Next will then use this URL for both og:image and + // twitter:image. Otherwise the file-convention opengraph-image takes over. + const ogOverride = essay.image + ? { openGraph: { images: [essay.image] }, twitter: { images: [essay.image] } } + : {}; + return { title: essay.title, description: essay.description, @@ -62,6 +71,11 @@ export async function generateMetadata({ type: 'article', publishedTime: essay.date, tags: essay.topics, + ...ogOverride.openGraph, + }, + twitter: { + card: 'summary_large_image', + ...ogOverride.twitter, }, alternates, }; @@ -150,22 +164,33 @@ export default async function EssayPage({ params }: EssayPageProps) { const { title, description, date, type, topics, readingTime, content, toc } = essay; + const urlPath = `/essays/${slug}`; return ( - - } - > - - + <> + + + + } + > + + + ); } diff --git a/apps/blog/app/(en)/layout.tsx b/apps/blog/app/(en)/layout.tsx index 08a2d6d..6c5512f 100644 --- a/apps/blog/app/(en)/layout.tsx +++ b/apps/blog/app/(en)/layout.tsx @@ -11,6 +11,8 @@ import { } from '@/components/layout'; import { LanguageProvider } from '@/lib/i18n'; import { SITE_AUTHOR, SITE_URL } from '@/lib/constants'; +import { JsonLd } from '@/components/seo'; +import { siteGraph } from '@/lib/jsonld'; export const metadata: Metadata = { metadataBase: new URL(SITE_URL), @@ -58,6 +60,13 @@ export default function EnRootLayout({ children }: EnRootLayoutProps) { return ( + +