Skip to content

Commit a20c390

Browse files
fix(docs): separate SEO and display titles
1 parent f930b22 commit a20c390

4 files changed

Lines changed: 72 additions & 3 deletions

File tree

docs/app/docs/[[...slug]]/page.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import {
77
} from 'fumadocs-ui/page';
88
import { notFound } from 'next/navigation';
99
import { createRelativeLink } from 'fumadocs-ui/mdx';
10+
import defaultMdxComponents from 'fumadocs-ui/mdx';
1011
import { getMDXComponents } from '@/mdx-components';
1112
import type { Metadata } from 'next';
1213
import { buildSeoProfile } from '@/lib/seo';
14+
import { getDisplayTitle, getDisplayTitleNode } from '@/lib/title';
1315

1416
const siteUrl = (process.env.NEXT_PUBLIC_SITE_URL ?? 'https://ml.kanaries.net').replace(/\/$/, '');
1517

@@ -24,6 +26,17 @@ export default async function Page(props: {
2426
const slug = params.slug ?? [];
2527
const path = slug.length ? `/docs/${slug.join('/')}` : '/docs';
2628
const canonicalUrl = new URL(path, siteUrl).toString();
29+
const displayTitle = getDisplayTitle(page.data.title);
30+
const displayToc = page.data.toc
31+
.map((item) => {
32+
const title = getDisplayTitleNode(item.title);
33+
34+
return {
35+
...item,
36+
title,
37+
};
38+
})
39+
.filter((item) => item.title !== displayTitle);
2740
const seoProfile = buildSeoProfile({
2841
title: page.data.title,
2942
description: page.data.description,
@@ -66,14 +79,24 @@ export default async function Page(props: {
6679
<>
6780
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(articleLd) }} />
6881
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(faqLd) }} />
69-
<DocsPage toc={page.data.toc} full={page.data.full}>
70-
<DocsTitle>{page.data.title}</DocsTitle>
82+
<DocsPage toc={displayToc} full={page.data.full}>
83+
<DocsTitle>{displayTitle}</DocsTitle>
7184
<DocsDescription>{page.data.description}</DocsDescription>
7285
<DocsBody>
7386
<MDXContent
7487
components={getMDXComponents({
7588
// this allows you to link to other pages with relative file paths
7689
a: createRelativeLink(source, page),
90+
h1: (props) => {
91+
const children = getDisplayTitleNode(props.children);
92+
93+
if (children === displayTitle) return null;
94+
95+
return defaultMdxComponents.h1({
96+
...props,
97+
children,
98+
});
99+
},
77100
})}
78101
/>
79102
<section className="mt-12 rounded-lg border border-fd-border bg-fd-muted/40 p-6">

docs/lib/seo.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { getDisplayTitle } from '@/lib/title';
2+
13
type SeoInput = {
24
title: string;
35
description?: string;
@@ -60,7 +62,7 @@ function unique(values: string[]): string[] {
6062
}
6163

6264
function titleToTopic(title: string): string {
63-
return title.replace(/\s+[-]\s+@kanaries\/ml.*$/i, '').replace(/\s{2,}/g, ' ').trim();
65+
return getDisplayTitle(title.replace(/\s+[-]\s+@kanaries\/ml.*$/i, '')).replace(/\s{2,}/g, ' ').trim();
6466
}
6567

6668
function slugToWords(value: string): string {

docs/lib/source.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
import { docs } from '@/.source';
22
import { loader } from 'fumadocs-core/source';
3+
import { getDisplayTitle } from '@/lib/title';
34

45
// See https://fumadocs.vercel.app/docs/headless/source-api for more info
56
export const source = loader({
67
// it assigns a URL to your pages
78
baseUrl: '/docs',
89
source: docs.toFumadocsSource(),
10+
pageTree: {
11+
attachFile(node, file) {
12+
if (!file) return node;
13+
14+
return {
15+
...node,
16+
name: getDisplayTitle(file.data.data.title) || node.name,
17+
};
18+
},
19+
},
920
});

docs/lib/title.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export function getDisplayTitle(title: string | undefined): string {
2+
if (!title) return '';
3+
4+
return title
5+
.replace(/\s+with\s+@kanaries\/ml\s*$/i, '')
6+
.replace(/\s+in\s+(?:JavaScript|TypeScript)(?:\s+(?:and|or)\s+(?:JavaScript|TypeScript))*\s*$/i, '')
7+
.replace(/\s+JavaScript\s+implementation\s*$/i, '')
8+
.replace(/\s{2,}/g, ' ')
9+
.trim();
10+
}
11+
12+
export function getTextFromNode(value: unknown): string | undefined {
13+
if (typeof value === 'string') return value;
14+
if (typeof value === 'number') return String(value);
15+
16+
if (Array.isArray(value)) {
17+
const text = value.map((item) => getTextFromNode(item)).filter(Boolean).join('');
18+
return text || undefined;
19+
}
20+
21+
if (value && typeof value === 'object' && 'props' in value) {
22+
return getTextFromNode((value as { props?: { children?: unknown } }).props?.children);
23+
}
24+
25+
return undefined;
26+
}
27+
28+
export function getDisplayTitleNode<T>(value: T): T | string {
29+
const text = getTextFromNode(value);
30+
if (!text) return value;
31+
32+
return getDisplayTitle(text) || text;
33+
}

0 commit comments

Comments
 (0)