add 404 page and sitemap

This commit is contained in:
miloschwartz
2026-09-25 19:46:47 -04:00
parent e5e87f9019
commit 175208ee49
9 changed files with 315 additions and 3 deletions
+5 -2
View File
@@ -5,8 +5,8 @@ import {
DocsPage,
DocsTitle,
MarkdownCopyButton,
ViewOptionsPopover,
} from 'fumadocs-ui/layouts/notebook/page';
import { ViewOptionsPopover } from '@/components/view-options';
import { notFound } from 'next/navigation';
import { getMDXComponents } from '@/components/mdx';
import type { Metadata } from 'next';
@@ -77,7 +77,10 @@ export async function generateMetadata(props: PageProps<'/[[...slug]]'>): Promis
description: page.data.description,
alternates: {
canonical: page.url,
types: { 'text/markdown': getPageMarkdownUrl(page).url },
types: {
'text/markdown': getPageMarkdownUrl(page).url,
'application/xml': '/sitemap.xml',
},
},
openGraph: {
images: '/images/home-social-graph.png',
+30
View File
@@ -0,0 +1,30 @@
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
import type * as PageTree from 'fumadocs-core/page-tree';
import { NotFoundPage, type NotFoundLink } from '@/components/not-found';
import { getNavigationTree } from '@/lib/navigation';
export const metadata: Metadata = {
title: 'Page Not Found',
};
function label(name: ReactNode) {
return typeof name === 'string' ? name : null;
}
function flatten(nodes: PageTree.Node[]): NotFoundLink[] {
const links: NotFoundLink[] = [];
for (const node of nodes) {
if (node.type === 'page') {
const title = label(node.name);
if (title && node.url) links.push({ title, url: node.url });
continue;
}
if (node.type === 'folder') links.push(...flatten(node.children));
}
return links;
}
export default function NotFound() {
return <NotFoundPage pages={flatten(getNavigationTree().children)} />;
}
+1 -1
View File
@@ -323,7 +323,7 @@ body {
.pg-ask-ai-btn svg {
width: 1rem;
height: 1rem;
color: var(--color-pg-orange);
color: currentColor;
}
/* logo: fixed height, width from the image's aspect ratio (never stretched) */
+1
View File
@@ -15,6 +15,7 @@ export const metadata: Metadata = {
title: { template: `%s | ${appName}`, default: appName },
description: siteDescription,
appleWebApp: { title: appName },
alternates: { types: { 'application/xml': '/sitemap.xml' } },
openGraph: { siteName: appName, images: '/images/home-social-graph.png' },
twitter: { card: 'summary_large_image' },
};
+21
View File
@@ -0,0 +1,21 @@
import type { MetadataRoute } from 'next';
import { siteUrl } from '@/lib/shared';
/**
* Mintlify's generated robots.txt: AI crawlers may train, search, and answer
* from the docs, while Next.js build assets stay out of the index.
* https://www.mintlify.com/docs/optimize/seo
*/
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/_next/image',
disallow: ['/cdn-cgi/', '/_next/'],
other: {
'Content-Signal': 'ai-train=yes, search=yes, ai-input=yes',
},
},
sitemap: `${siteUrl}/sitemap.xml`,
};
}
+55
View File
@@ -0,0 +1,55 @@
import { execFileSync } from 'node:child_process';
import { statSync } from 'node:fs';
import path from 'node:path';
import type { MetadataRoute } from 'next';
import { source } from '@/lib/source';
import { docsDir, siteUrl } from '@/lib/shared';
/**
* Same shape as a Mintlify docs sitemap (`/docs/sitemap.xml` on Infisical):
* one `<loc>` and `<lastmod>` per published page.
*/
export default function sitemap(): MetadataRoute.Sitemap {
const dates = lastCommitDates();
return source
.getPages()
.map((page) => {
const file = `${docsDir}/${page.path}`;
return {
url: `${siteUrl}${page.url}`,
lastModified: dates.get(file) ?? mtime(page.absolutePath ?? path.join(process.cwd(), file)),
};
})
.sort((a, b) => a.url.localeCompare(b.url));
}
/** Newest commit that touched each file under content/docs. Empty when git is unavailable. */
function lastCommitDates(): Map<string, Date> {
const dates = new Map<string, Date>();
try {
const out = execFileSync('git', ['log', '--format=COMMIT %cI', '--name-only', '--', docsDir], {
encoding: 'utf8',
});
let current: Date | undefined;
for (const line of out.split('\n')) {
if (line.startsWith('COMMIT ')) {
const date = new Date(line.slice('COMMIT '.length));
current = Number.isNaN(date.getTime()) ? undefined : date;
} else if (line && current && !dates.has(line)) {
dates.set(line, current);
}
}
} catch {
// Docker builds without git fall back to file mtime.
}
return dates;
}
function mtime(file: string): Date {
try {
return statSync(file).mtime;
} catch {
return new Date();
}
}