seo and geo optimizations

This commit is contained in:
miloschwartz
2026-09-25 19:52:51 -04:00
parent 175208ee49
commit c8b73e591a
6 changed files with 211 additions and 59 deletions
+19 -15
View File
@@ -15,6 +15,7 @@ import { appName, docsDir, getPageMarkdownUrl, gitConfig, siteUrl } from '@/lib/
import { AskAIAboutPage } from '@/components/ai/ask-page'; import { AskAIAboutPage } from '@/components/ai/ask-page';
import { SiteFooter } from '@/components/site-footer'; import { SiteFooter } from '@/components/site-footer';
import { AskBar } from '@/components/ai/ask-bar'; import { AskBar } from '@/components/ai/ask-bar';
import { pageJsonLd } from '@/lib/json-ld';
export default async function Page(props: PageProps<'/[[...slug]]'>) { export default async function Page(props: PageProps<'/[[...slug]]'>) {
const params = await props.params; const params = await props.params;
@@ -25,20 +26,22 @@ export default async function Page(props: PageProps<'/[[...slug]]'>) {
const markdownUrl = getPageMarkdownUrl(page).url; const markdownUrl = getPageMarkdownUrl(page).url;
return ( return (
<DocsPage <>
toc={page.data.toc} <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: pageJsonLd(page) }} />
full={page.data.full} <DocsPage
tableOfContent={{ style: 'clerk' }} toc={page.data.toc}
// rendered after the previous / next page links full={page.data.full}
footer={{ tableOfContent={{ style: 'clerk' }}
children: ( // rendered after the previous / next page links
<> footer={{
<SiteFooter /> children: (
<AskBar /> <>
</> <SiteFooter />
), <AskBar />
}} </>
> ),
}}
>
<DocsTitle>{page.data.title}</DocsTitle> <DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription className="mb-0">{page.data.description}</DocsDescription> <DocsDescription className="mb-0">{page.data.description}</DocsDescription>
<div className="flex flex-row flex-wrap gap-2 items-center border-b pb-6"> <div className="flex flex-row flex-wrap gap-2 items-center border-b pb-6">
@@ -58,7 +61,8 @@ export default async function Page(props: PageProps<'/[[...slug]]'>) {
})} })}
/> />
</DocsBody> </DocsBody>
</DocsPage> </DocsPage>
</>
); );
} }
+1
View File
@@ -14,6 +14,7 @@ export async function GET() {
`> ${siteDescription}`, `> ${siteDescription}`,
'', '',
`The full documentation as a single file is available at ${siteUrl}/llms-full.txt. Append \`.md\` to any page URL to get its Markdown source.`, `The full documentation as a single file is available at ${siteUrl}/llms-full.txt. Append \`.md\` to any page URL to get its Markdown source.`,
`An MCP server for searching and reading this documentation is available at ${siteUrl}/mcp.`,
'', '',
]; ];
const listed = new Set<string>(); const listed = new Set<string>();
+6 -43
View File
@@ -1,55 +1,18 @@
import { execFileSync } from 'node:child_process';
import { statSync } from 'node:fs';
import path from 'node:path';
import type { MetadataRoute } from 'next'; import type { MetadataRoute } from 'next';
import { pageLastModified } from '@/lib/last-modified';
import { source } from '@/lib/source'; import { source } from '@/lib/source';
import { docsDir, siteUrl } from '@/lib/shared'; import { siteUrl } from '@/lib/shared';
/** /**
* Same shape as a Mintlify docs sitemap (`/docs/sitemap.xml` on Infisical): * Same shape as a Mintlify docs sitemap (`/docs/sitemap.xml` on Infisical):
* one `<loc>` and `<lastmod>` per published page. * one `<loc>` and `<lastmod>` per published page.
*/ */
export default function sitemap(): MetadataRoute.Sitemap { export default function sitemap(): MetadataRoute.Sitemap {
const dates = lastCommitDates();
return source return source
.getPages() .getPages()
.map((page) => { .map((page) => ({
const file = `${docsDir}/${page.path}`; url: `${siteUrl}${page.url}`,
return { lastModified: pageLastModified(page),
url: `${siteUrl}${page.url}`, }))
lastModified: dates.get(file) ?? mtime(page.absolutePath ?? path.join(process.cwd(), file)),
};
})
.sort((a, b) => a.url.localeCompare(b.url)); .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();
}
}
+136
View File
@@ -0,0 +1,136 @@
import navigation from '@/lib/navigation.json';
import { pageLastModified } from '@/lib/last-modified';
import type { Page } from '@/lib/source';
import { appName, siteUrl } from '@/lib/shared';
type NavItem = string | { group: string; pages: NavItem[] };
type Crumb = { name: string; url: string };
type IdRef = { '@id': string };
/**
* Mintlify's per-page graph: Organization, WebSite, WebPage, BreadcrumbList,
* and TechArticle. `dateModified` is the same git date the sitemap uses.
*/
export function pageJsonLd(page: Page): string {
const url = absoluteUrl(page.url);
const dateModified = pageLastModified(page).toISOString();
const description = page.data.description || undefined;
const orgId = `${siteUrl}/#organization`;
const siteId = `${siteUrl}/#website`;
const pageId = `${url}#webpage`;
const crumbs = breadcrumb(page.slugs.length === 0 ? 'index' : page.slugs.join('/'), url, page.data.title);
const graph: Record<string, unknown>[] = [
{
'@type': 'Organization',
'@id': orgId,
name: appName,
url: siteUrl,
logo: { '@type': 'ImageObject', url: `${siteUrl}/logo/light.png` },
},
{
'@type': 'WebSite',
'@id': siteId,
name: appName,
url: siteUrl,
publisher: { '@id': orgId } satisfies IdRef,
},
{
'@type': 'WebPage',
'@id': pageId,
url,
name: page.data.title,
description,
dateModified,
isPartOf: { '@id': siteId } satisfies IdRef,
breadcrumb: crumbs ? { '@id': `${url}#breadcrumb` } : undefined,
},
];
if (crumbs) {
graph.push({
'@type': 'BreadcrumbList',
'@id': `${url}#breadcrumb`,
itemListElement: crumbs.map((crumb, index) => ({
'@type': 'ListItem',
position: index + 1,
name: crumb.name,
item: crumb.url,
})),
});
}
graph.push({
'@type': ['Article', 'TechArticle'],
'@id': `${url}#article`,
headline: page.data.title,
name: page.data.title,
description,
url,
mainEntityOfPage: { '@id': pageId } satisfies IdRef,
dateModified,
publisher: { '@id': orgId } satisfies IdRef,
isPartOf: { '@id': siteId } satisfies IdRef,
});
return JSON.stringify({ '@context': 'https://schema.org', '@graph': graph }).replace(/</g, '\\u003c');
}
function absoluteUrl(url: string) {
return `${siteUrl}${url}`;
}
/**
* Two crumbs, same as Mintlify: the nearest nav group whose first page is not
* this one, then the page. The home page has no trail.
*/
function breadcrumb(navPath: string, url: string, title: string): Crumb[] | undefined {
const ancestors = findAncestors(navigation.groups as NavItem[], navPath) ?? [];
for (let i = ancestors.length - 1; i >= 0; i--) {
const group = ancestors[i];
if (!group) continue;
const leaf = firstLeaf(group.pages);
const parentUrl = leaf ? urlForNavPath(leaf) : undefined;
if (parentUrl && parentUrl !== url) {
return [
{ name: group.group, url: parentUrl },
{ name: title, url },
];
}
}
if (url === absoluteUrl('/')) return undefined;
return [
{ name: appName, url: absoluteUrl('/') },
{ name: title, url },
];
}
function urlForNavPath(navPath: string) {
const slugs = navPath === 'index' ? [] : navPath.split('/');
return absoluteUrl(slugs.length === 0 ? '/' : `/${slugs.join('/')}`);
}
function firstLeaf(items: NavItem[]): string | undefined {
for (const item of items) {
if (typeof item === 'string') return item;
const found = firstLeaf(item.pages);
if (found) return found;
}
}
function findAncestors(
items: NavItem[],
target: string,
trail: Extract<NavItem, { group: string }>[] = [],
): Extract<NavItem, { group: string }>[] | undefined {
for (const item of items) {
if (typeof item === 'string') {
if (item === target) return trail;
continue;
}
const found = findAncestors(item.pages, target, [...trail, item]);
if (found) return found;
}
}
+48
View File
@@ -0,0 +1,48 @@
import { execFileSync } from 'node:child_process';
import { statSync } from 'node:fs';
import path from 'node:path';
import { docsDir } from './shared';
type ContentFile = { path: string; absolutePath?: string };
let cached: Map<string, Date> | undefined;
/** Newest commit that touched each file under content/docs. Empty when git is unavailable. */
function commitDates(): Map<string, Date> {
if (cached) return cached;
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.
}
cached = dates;
return dates;
}
function mtime(file: string): Date {
try {
return statSync(file).mtime;
} catch {
return new Date();
}
}
/** Last time this page's MDX changed: newest commit, then file mtime. */
export function pageLastModified(page: ContentFile): Date {
const file = `${docsDir}/${page.path}`;
return commitDates().get(file) ?? mtime(page.absolutePath ?? path.join(process.cwd(), file));
}
+1 -1
View File
@@ -42,6 +42,6 @@ export default function proxy(request: NextRequest) {
export const config = { export const config = {
// skip Next internals, API routes, generated files and static assets // skip Next internals, API routes, generated files and static assets
matcher: [ matcher: [
'/((?!_next/|api/|og/|llms|images/|logo/|favicon|icon|apple-icon|manifest|web-app-manifest).*)', '/((?!_next/|api/|og/|llms|images/|logo/|favicon|icon|apple-icon|manifest|web-app-manifest|sitemap\\.xml|robots\\.txt).*)',
], ],
}; };