mirror of
https://github.com/fosrl/docs-v2.git
synced 2026-10-01 02:09:12 +02:00
add 404 page and sitemap
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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
@@ -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) */
|
||||
|
||||
@@ -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' },
|
||||
};
|
||||
|
||||
@@ -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`,
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user