feat(docs): add comprehensive SEO optimization for static docs site

Add per-page metadata, Open Graph / Twitter tags, canonical URLs,
hreflang, sitemap generation, robots.txt, and JSON-LD structured data
for the bilingual (en/cn) documentation site.
This commit is contained in:
t8y2
2026-05-28 00:31:34 +08:00
parent bd78436ab1
commit 4b9f770534
10 changed files with 391 additions and 7 deletions
+19
View File
@@ -1,6 +1,8 @@
import { LandingNav } from '@/components/landing/LandingNav';
import { ChangelogRuntime } from '@/components/landing/ChangelogRuntime';
import { fetchChangelog } from '@/lib/changelog';
import { buildMetadata } from '@/lib/metadata';
import type { Metadata } from 'next';
const i18n = {
en: {
@@ -13,6 +15,23 @@ const i18n = {
},
};
export async function generateMetadata({
params,
}: {
params: Promise<{ lang: string }>;
}): Promise<Metadata> {
const { lang } = await params;
const l = lang === 'cn' ? 'cn' : 'en';
const t = i18n[l];
return buildMetadata({
title: t.title,
description: t.desc,
path: `/${l}/changelog`,
lang: l,
});
}
export default async function ChangelogPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const l = lang === 'cn' ? 'cn' : 'en';
+19
View File
@@ -1,4 +1,6 @@
import { LandingNav } from '@/components/landing/LandingNav';
import { buildMetadata } from '@/lib/metadata';
import type { Metadata } from 'next';
const channels = [
{
@@ -58,6 +60,23 @@ const i18n = {
},
};
export async function generateMetadata({
params,
}: {
params: Promise<{ lang: string }>;
}): Promise<Metadata> {
const { lang } = await params;
const l = lang === 'cn' ? 'cn' : 'en';
const t = i18n[l];
return buildMetadata({
title: t.title,
description: t.desc,
path: `/${l}/community`,
lang: l,
});
}
export default async function CommunityPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const l = lang === 'cn' ? 'cn' : 'en';
+66 -2
View File
@@ -1,5 +1,6 @@
import { source } from '@/lib/source';
import { notFound } from 'next/navigation';
import type { Metadata } from 'next';
import {
DocsPage,
DocsBody,
@@ -15,6 +16,28 @@ import { ImageZoom } from 'fumadocs-ui/components/image-zoom';
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
import type { MDXContent } from 'mdx/types';
import type { TOCItemType } from 'fumadocs-core/toc';
import { buildMetadata, SITE_URL } from '@/lib/metadata';
export async function generateMetadata({
params,
}: {
params: Promise<{ lang: string; slug?: string[] }>;
}): Promise<Metadata> {
const { lang, slug } = await params;
const page = source.getPage(slug, lang);
if (!page) return {};
const title = page.data.title as string;
const description = (page.data.description as string) || undefined;
return buildMetadata({
title,
description: description ?? '',
path: slug ? `/${lang}/docs/${slug.join('/')}` : `/${lang}/docs`,
lang,
ogType: 'article',
});
}
const mdxComponents = {
...defaultMdxComponents,
@@ -44,10 +67,51 @@ export default async function Page({
toc: TOCItemType[];
};
const title = page.data.title as string;
const description = (page.data.description as string) || '';
const docPath = slug ? `/${lang}/docs/${slug.join('/')}` : `/${lang}/docs`;
const breadcrumbJsonLd = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: SITE_URL },
{ '@type': 'ListItem', position: 2, name: 'Docs', item: `${SITE_URL}/${lang}/docs` },
...(slug
? slug.map((segment, i) => ({
'@type': 'ListItem' as const,
position: i + 3,
name: segment.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
item: `${SITE_URL}/${lang}/docs/${slug.slice(0, i + 1).join('/')}`,
}))
: []),
],
};
const articleJsonLd = {
'@context': 'https://schema.org',
'@type': 'TechArticle',
headline: title,
description,
inLanguage: lang === 'cn' ? 'zh-CN' : 'en',
isPartOf: {
'@type': 'WebSite',
url: SITE_URL,
},
};
return (
<DocsPage toc={toc}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
/>
<DocsTitle>{title}</DocsTitle>
<DocsDescription>{description}</DocsDescription>
<DocsBody>
<MDX components={mdxComponents} />
</DocsBody>
+46
View File
@@ -1,6 +1,52 @@
import type { ReactNode } from 'react';
import type { Metadata } from 'next';
import { RootProvider } from 'fumadocs-ui/provider/next';
import { i18nUI } from '@/lib/i18n';
import { SITE_URL, SITE_NAME, DEFAULT_DESCRIPTION } from '@/lib/metadata';
const LOCALE_MAP: Record<string, { locale: string; title: string; description: string }> = {
en: {
locale: 'en_US',
title: 'DBX - 15 MB to manage 35+ databases',
description: DEFAULT_DESCRIPTION,
},
cn: {
locale: 'zh_CN',
title: 'DBX - 15MB,管理35+种数据库',
description: '25+ 种数据库,仅 15 MB。支持桌面与 Docker 自托管,内置 AI 助手。',
},
};
export async function generateMetadata({
params,
}: {
params: Promise<{ lang: string }>;
}): Promise<Metadata> {
const { lang } = await params;
const l = lang === 'cn' ? 'cn' : 'en';
const meta = LOCALE_MAP[l];
return {
title: {
default: meta.title,
template: `%s | ${SITE_NAME}`,
},
description: meta.description,
openGraph: {
locale: meta.locale,
siteName: SITE_NAME,
url: `${SITE_URL}/${l}`,
},
alternates: {
canonical: `${SITE_URL}/${l}`,
languages: {
en: `${SITE_URL}/en`,
zh: `${SITE_URL}/cn`,
'x-default': `${SITE_URL}/en`,
},
},
};
}
export default async function LangLayout({
params,
+34
View File
@@ -1,4 +1,5 @@
import Link from 'next/link';
import type { Metadata } from 'next';
import type { CSSProperties } from 'react';
import { HeroProductStage } from '@/components/aceternity/HeroProductStage';
import { InfiniteMovingCards } from '@/components/aceternity/InfiniteMovingCards';
@@ -366,6 +367,39 @@ const i18nText = {
},
};
import { buildMetadata } from '@/lib/metadata';
const landingMeta = {
en: {
title: 'DBX - 15 MB to manage 35+ databases!',
description:
'DBX brings connections, SQL editing, data grids, schema tools, AI assistance, and self-hosted access into one lightweight product.',
},
cn: {
title: 'DBX - 15MB,管理35+种数据库!',
description:
'DBX 将连接管理、SQL 编辑、数据表格、结构工具、AI 助手和自托管访问放进一个轻量产品里。',
},
};
export async function generateMetadata({
params,
}: {
params: Promise<{ lang: string }>;
}): Promise<Metadata> {
const { lang } = await params;
const l = lang === 'cn' ? 'cn' : 'en';
const meta = landingMeta[l];
return buildMetadata({
title: meta.title,
description: meta.description,
path: `/${l}`,
lang: l,
ogType: 'website',
});
}
export default async function LandingPage({
params,
}: {
+37 -5
View File
@@ -1,22 +1,54 @@
import './global.css';
import type { ReactNode } from 'react';
import type { Metadata } from 'next';
import { SITE_URL, SITE_NAME, DEFAULT_DESCRIPTION, DEFAULT_OG_IMAGE } from '@/lib/metadata';
export const metadata = {
export const metadata: Metadata = {
title: {
default: 'DBX',
template: '%s | DBX',
default: SITE_NAME,
template: `%s | ${SITE_NAME}`,
},
description: '25+ databases in 15 MB. Desktop & Docker self-hosting, with built-in AI assistant.',
description: DEFAULT_DESCRIPTION,
metadataBase: new URL(SITE_URL),
icons: {
icon: '/favicon.png',
shortcut: '/favicon.png',
apple: '/logo.png',
},
openGraph: {
siteName: SITE_NAME,
images: [{ url: DEFAULT_OG_IMAGE }],
},
twitter: {
card: 'summary_large_image',
},
};
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" className="dark" suppressHydrationWarning>
<html className="dark" suppressHydrationWarning>
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebSite',
name: SITE_NAME,
url: SITE_URL,
description: DEFAULT_DESCRIPTION,
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${SITE_URL}/search?q={search_term_string}`,
},
'query-input': 'required name=search_term_string',
},
}),
}}
/>
</head>
<body className="flex min-h-screen flex-col">{children}</body>
</html>
);