Skip to content

Commit 9f6dec7

Browse files
os-zhuangclaude
andcommitted
feat(docs): add sitemap, robots, and metadataBase for SEO
Search Console had nothing to crawl: the site served no /sitemap.xml or /robots.txt and metadata used relative URLs. - app/sitemap.ts: enumerate all Fumadocs pages across the 7 locales with hreflang + x-default alternates (default-locale prefix hidden) - app/robots.ts: allow all, declare the sitemap, disallow /api and the generated llms*.txt text dumps - layout.tsx: set metadataBase so canonical/OG URLs are absolute Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f072cbb commit 9f6dec7

3 files changed

Lines changed: 79 additions & 0 deletions

File tree

apps/docs/app/layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ReactNode } from 'react';
33
import type { Metadata } from 'next';
44

55
export const metadata: Metadata = {
6+
metadataBase: new URL('https://docs.objectos.ai'),
67
title: {
78
template: '%s | ObjectOS',
89
default: 'ObjectOS',

apps/docs/app/robots.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { MetadataRoute } from 'next';
2+
3+
const BASE = 'https://docs.objectos.ai';
4+
5+
export const revalidate = false;
6+
7+
export default function robots(): MetadataRoute.Robots {
8+
return {
9+
rules: {
10+
userAgent: '*',
11+
allow: '/',
12+
// Generated text endpoints duplicate page content; keep them out of the index.
13+
disallow: ['/api/', '/llms.txt', '/llms-full.txt'],
14+
},
15+
sitemap: `${BASE}/sitemap.xml`,
16+
host: BASE,
17+
};
18+
}

apps/docs/app/sitemap.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { MetadataRoute } from 'next';
2+
import { source } from '@/lib/source';
3+
import { i18n } from '@/lib/i18n';
4+
5+
// Canonical production host. Keep in sync with middleware's domain redirect.
6+
const BASE = 'https://docs.objectos.ai';
7+
8+
// Build an absolute URL for a given locale, hiding the prefix for the
9+
// default language (matches i18n.hideLocale = 'default-locale').
10+
function localize(lang: string, path: string): string {
11+
const suffix = path ? `/${path}` : '';
12+
return lang === i18n.defaultLanguage
13+
? `${BASE}${suffix}`
14+
: `${BASE}/${lang}${suffix}`;
15+
}
16+
17+
// hreflang alternates map for a logical path, linking every locale version
18+
// plus an x-default that points at the English (canonical) URL.
19+
function alternates(path: string): Record<string, string> {
20+
const languages: Record<string, string> = {};
21+
for (const lang of i18n.languages) {
22+
languages[lang] = localize(lang, path);
23+
}
24+
languages['x-default'] = localize(i18n.defaultLanguage, path);
25+
return languages;
26+
}
27+
28+
export const revalidate = false;
29+
30+
export default function sitemap(): MetadataRoute.Sitemap {
31+
const entries: MetadataRoute.Sitemap = [];
32+
33+
// Top-level static pages (paths are locale-independent slugs).
34+
const staticPaths: Array<{ path: string; priority: number }> = [
35+
{ path: '', priority: 1 },
36+
{ path: 'download', priority: 0.6 },
37+
{ path: 'privacy', priority: 0.3 },
38+
{ path: 'terms', priority: 0.3 },
39+
];
40+
41+
// Documentation pages, enumerated from the Fumadocs source. Slugs are the
42+
// same across locales, so we reconstruct each locale URL from page.slugs.
43+
const docPaths = source
44+
.getPages()
45+
.map((page) => ({ path: ['docs', ...page.slugs].join('/'), priority: 0.8 }));
46+
47+
for (const { path, priority } of [...staticPaths, ...docPaths]) {
48+
const langAlternates = alternates(path);
49+
for (const lang of i18n.languages) {
50+
entries.push({
51+
url: localize(lang, path),
52+
changeFrequency: 'weekly',
53+
priority,
54+
alternates: { languages: langAlternates },
55+
});
56+
}
57+
}
58+
59+
return entries;
60+
}

0 commit comments

Comments
 (0)