mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 12:41:39 +00:00
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:
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user