Skip to content

Commit eb0fcc3

Browse files
author
Carla Goncalves
committed
Update SEO breadcrumb casing
1 parent 0b7278c commit eb0fcc3

3 files changed

Lines changed: 98 additions & 14 deletions

File tree

β€Žapps/docs/src/app/api/search/route.tsβ€Ž

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createMixedbreadSearchAPI } from "fumadocs-core/search/mixedbread";
22
import Mixedbread from "@mixedbread/sdk";
33
import { SortedResult } from "fumadocs-core/search";
4+
import { formatSlugDisplayName } from "@/lib/breadcrumb-utils";
45

56
export const dynamic = "force-dynamic";
67

@@ -13,13 +14,9 @@ function getBreadcrumbsFromUrl(url: string): string[] {
1314
const normalized = segments[0] === "v6" ? segments.slice(1) : segments;
1415
if (normalized.length === 0) return [];
1516
// Ancestors only (exclude last = current page), or full path for section roots
16-
const breadcrumbSegments = normalized.length > 1 ? normalized.slice(0, -1) : normalized;
17-
return breadcrumbSegments.map((s) =>
18-
s
19-
.split(/[-_]/)
20-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
21-
.join(" "),
22-
);
17+
const breadcrumbSegments =
18+
normalized.length > 1 ? normalized.slice(0, -1) : normalized;
19+
return breadcrumbSegments.map((s) => formatSlugDisplayName(s));
2320
}
2421

2522
function slugger(value: string): string {
@@ -35,7 +32,10 @@ function removeMd(md: string): string {
3532
if (typeof md !== "string") return "";
3633
try {
3734
return md
38-
.replace(/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/gm, "")
35+
.replace(
36+
/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/gm,
37+
"",
38+
)
3939
.replace(/^([\s\t]*)([*\-+]|\d+\.)\s+/gm, "$1")
4040
.replace(/\n={2,}/g, "\n")
4141
.replace(/^[=-]{2,}\s*$/gm, "")
@@ -49,7 +49,10 @@ function removeMd(md: string): string {
4949
.replace(/!\[(.*?)\][[(].*?[\])]/g, "")
5050
.replace(/\[([\s\S]*?)\]\s*[([].*?[)\]]/g, "$1")
5151
.replace(/^(\n)?\s{0,3}>\s?/gm, "$1")
52-
.replace(/^(\n)?\s{0,}#{1,6}\s*( (.+))? +#+$|^(\n)?\s{0,}#{1,6}\s*( (.+))?$/gm, "$1$3$4$6")
52+
.replace(
53+
/^(\n)?\s{0,}#{1,6}\s*( (.+))? +#+$|^(\n)?\s{0,}#{1,6}\s*( (.+))?$/gm,
54+
"$1$3$4$6",
55+
)
5356
.replace(/([*]+)(\S)(.*?\S)??\1/g, "$2$3")
5457
.replace(/(^|\W)([_]+)(\S)(.*?\S)??\2($|\W)/g, "$1$3$4$5")
5558
.replace(/(`{3,})(.*?)\1/gm, "$2")
@@ -88,7 +91,8 @@ export const { GET } = createMixedbreadSearchAPI({
8891
breadcrumbs,
8992
},
9093
];
91-
const heading = item.type === "text" ? extractHeadingTitle(item.text) : "";
94+
const heading =
95+
item.type === "text" ? extractHeadingTitle(item.text) : "";
9296
if (heading)
9397
chunkResults.push({
9498
id: `${base}-heading`,

β€Žapps/docs/src/components/structured-data.tsxβ€Ž

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getBaseUrl, withDocsBasePath } from "@/lib/urls";
2+
import { formatSlugDisplayName } from "@/lib/breadcrumb-utils";
23
import type { InferPageType } from "fumadocs-core/source";
34
import type { source, sourceV6 } from "@/lib/source";
45
import { JsonLd } from "@prisma-docs/ui/components/json-ld";
@@ -42,7 +43,9 @@ function getSectionTitle(page: DocsPage, slugs: string[]) {
4243

4344
for (const candidatePath of candidatePaths) {
4445
try {
45-
const meta = JSON.parse(readFileSync(candidatePath, "utf8")) as { title?: string };
46+
const meta = JSON.parse(readFileSync(candidatePath, "utf8")) as {
47+
title?: string;
48+
};
4649
if (typeof meta.title === "string" && meta.title.trim().length > 0) {
4750
sectionTitleCache.set(cacheKey, meta.title);
4851
return meta.title;
@@ -61,14 +64,16 @@ function getBreadcrumbName(page: DocsPage, slugs: string[], index: number) {
6164

6265
return (
6366
getSectionTitle(page, slugs.slice(0, index + 1)) ??
64-
slugs[index].charAt(0).toUpperCase() + slugs[index].slice(1).replace(/-/g, " ")
67+
formatSlugDisplayName(slugs[index])
6568
);
6669
}
6770

6871
export function TechArticleSchema({ page }: StructuredDataProps) {
6972
const baseUrl = getBaseUrl();
70-
const lastModified = (page.data as { lastModified?: Date | string }).lastModified;
71-
const datePublished = (page.data as { datePublished?: Date | string }).datePublished;
73+
const lastModified = (page.data as { lastModified?: Date | string })
74+
.lastModified;
75+
const datePublished = (page.data as { datePublished?: Date | string })
76+
.datePublished;
7277

7378
const schema = {
7479
"@context": "https://schema.org",
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Maps individual slug words (after splitting on hyphens/underscores) to their
3+
* correct display form. Handles acronyms, initialisms, and proper names that
4+
* simple title-casing gets wrong.
5+
*
6+
* Keys MUST be lowercase.
7+
*/
8+
const WORD_DISPLAY_MAP: Record<string, string> = {
9+
// Initialisms / acronyms
10+
ai: 'AI',
11+
api: 'API',
12+
cli: 'CLI',
13+
crud: 'CRUD',
14+
css: 'CSS',
15+
csv: 'CSV',
16+
db: 'DB',
17+
ddl: 'DDL',
18+
dns: 'DNS',
19+
html: 'HTML',
20+
http: 'HTTP',
21+
https: 'HTTPS',
22+
iac: 'IaC',
23+
id: 'ID',
24+
ids: 'IDs',
25+
json: 'JSON',
26+
jwt: 'JWT',
27+
mdx: 'MDX',
28+
orm: 'ORM',
29+
pgx: 'PGX',
30+
pql: 'PQL',
31+
sdk: 'SDK',
32+
sql: 'SQL',
33+
ssl: 'SSL',
34+
tcp: 'TCP',
35+
tls: 'TLS',
36+
url: 'URL',
37+
urls: 'URLs',
38+
xml: 'XML',
39+
// Proper names / compound words that don't split on hyphens
40+
authjs: 'Auth.js',
41+
nextauth: 'NextAuth',
42+
postgresql: 'PostgreSQL',
43+
mysql: 'MySQL',
44+
mongodb: 'MongoDB',
45+
cockroachdb: 'CockroachDB',
46+
planetscale: 'PlanetScale',
47+
};
48+
49+
/**
50+
* Converts a raw URL slug segment into a human-readable display name, correctly
51+
* casing acronyms, initialisms, and known proper names.
52+
*
53+
* The slug is split on hyphens and underscores; each token is looked up in
54+
* {@link WORD_DISPLAY_MAP} (case-insensitively) and, if not found, is given a
55+
* simple initial-capital. Tokens are then joined with a single space.
56+
*
57+
* @example
58+
* formatSlugDisplayName('orm') // β†’ 'ORM'
59+
* formatSlugDisplayName('prisma-client') // β†’ 'Prisma Client'
60+
* formatSlugDisplayName('management-api') // β†’ 'Management API'
61+
* formatSlugDisplayName('using-raw-sql') // β†’ 'Using Raw SQL'
62+
* formatSlugDisplayName('platform-cli') // β†’ 'Platform CLI'
63+
* formatSlugDisplayName('api-reference') // β†’ 'API Reference'
64+
* formatSlugDisplayName('authjs') // β†’ 'Auth.js'
65+
*/
66+
export function formatSlugDisplayName(slug: string): string {
67+
return slug
68+
.split(/[-_]+/)
69+
.filter(Boolean)
70+
.map((word) => {
71+
const key = word.toLowerCase();
72+
return WORD_DISPLAY_MAP[key] ?? word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
73+
})
74+
.join(' ');
75+
}

0 commit comments

Comments
Β (0)