Skip to content

Commit f7965e8

Browse files
Centralize SEO metadata and structured data
Introduces shared metadata builders for canonical URLs, Open Graph, and Twitter cards across pages, and adds reusable JSON-LD helpers with WebSite/Organization/SoftwareApplication and docs article/breadcrumb schemas. The docs route now includes last-modified metadata and schema output, with breadcrumb handling updated for non-linkable entries. It also removes standalone FAQ and roadmap pages in favor of docs routes, updates sitemap entries, adds legacy-to-docs redirects, and adds default OG and LLM discovery files in `public`.
1 parent 2dad1a1 commit f7965e8

22 files changed

Lines changed: 475 additions & 155 deletions

File tree

app/changelog/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ import Link from "next/link";
33
import { MarkdownPage } from "@/components/markdown-page";
44
import { readRootMarkdown } from "@/lib/markdown";
55

6-
export const metadata: Metadata = {
6+
import { buildPageMetadata } from "@/lib/page-metadata";
7+
8+
export const metadata: Metadata = buildPageMetadata({
9+
path: "/changelog",
710
title: "Changelog",
811
description: "DevStackBox release history and notable changes.",
9-
};
12+
});
1013

1114
export default function ChangelogPage() {
1215
const markdown = readRootMarkdown("CHANGELOG.md");

app/community/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import { MarkdownPageSimple } from "@/components/markdown-page";
44
import { siteConfig } from "@/content/homepage";
55
import { readRootMarkdown } from "@/lib/markdown";
66

7-
export const metadata: Metadata = {
7+
import { buildPageMetadata } from "@/lib/page-metadata";
8+
9+
export const metadata: Metadata = buildPageMetadata({
10+
path: "/community",
811
title: "Community",
912
description: "Join the DevStackBox open-source community.",
10-
};
13+
});
1114

1215
export default function CommunityPage() {
1316
const coc = readRootMarkdown("CODE_OF_CONDUCT.md");

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

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import type { Metadata } from "next";
22
import Link from "next/link";
33
import { notFound } from "next/navigation";
44
import { DocPageActions } from "@/components/doc-page-actions";
5+
import { JsonLd } from "@/components/json-ld";
56
import {
67
DocsSidebar,
78
getBreadcrumbs,
89
getPrevNext,
910
} from "@/components/docs-sidebar";
10-
import { getLastUpdated } from "@/lib/git-meta";
11+
import { getLastModifiedIso, getLastUpdated } from "@/lib/git-meta";
12+
import { docPageMetadata } from "@/lib/page-metadata";
1113
import { source } from "@/lib/source";
14+
import { docPageSchemas } from "@/lib/structured-data";
1215
import { getMDXComponents } from "@/mdx-components";
1316

1417
type PageProps = {
@@ -28,20 +31,34 @@ export default async function DocPage(props: PageProps) {
2831
const { prev, next } = getPrevNext(activeSlug);
2932
const crumbs = getBreadcrumbs(activeSlug);
3033
const mdxPath = activeSlug ? `docs/${activeSlug}.mdx` : "docs/index.mdx";
34+
const modifiedIso = getLastModifiedIso(mdxPath);
3135

3236
return (
3337
<div className="grid gap-8 lg:grid-cols-[240px_1fr]">
38+
<JsonLd
39+
data={docPageSchemas({
40+
slug: activeSlug,
41+
title: page.data.title,
42+
description: page.data.description,
43+
crumbs,
44+
modifiedTime: modifiedIso,
45+
})}
46+
/>
3447
<aside className="hidden lg:block">
3548
<DocsSidebar activeSlug={activeSlug} />
3649
</aside>
3750
<article className="min-w-0 max-w-3xl">
3851
<nav className="mb-4 flex flex-wrap items-center gap-1 text-sm text-muted-foreground">
3952
{crumbs.map((c, i) => (
40-
<span key={c.href} className="flex items-center gap-1">
53+
<span key={`${c.label}-${i}`} className="flex items-center gap-1">
4154
{i > 0 ? <span>/</span> : null}
42-
<Link href={c.href} className="hover:text-foreground">
43-
{c.label}
44-
</Link>
55+
{c.href ? (
56+
<Link href={c.href} className="hover:text-foreground">
57+
{c.label}
58+
</Link>
59+
) : (
60+
<span>{c.label}</span>
61+
)}
4562
</span>
4663
))}
4764
</nav>
@@ -78,13 +95,12 @@ export async function generateMetadata(props: PageProps): Promise<Metadata> {
7895
source.getPage(slug) ??
7996
(slug.length === 0 ? source.getPage(["index"]) : undefined);
8097
if (!page) return {};
81-
return {
98+
const activeSlug = slug.join("/");
99+
const mdxPath = activeSlug ? `docs/${activeSlug}.mdx` : "docs/index.mdx";
100+
return docPageMetadata({
101+
slug: activeSlug,
82102
title: page.data.title,
83103
description: page.data.description,
84-
openGraph: {
85-
title: `${page.data.title} · DevStackBox`,
86-
description: page.data.description,
87-
type: "article",
88-
},
89-
};
104+
modifiedTime: getLastModifiedIso(mdxPath),
105+
});
90106
}

app/download/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ import {
88
getLatestRelease,
99
} from "@/lib/github";
1010

11-
export const metadata: Metadata = {
11+
import { buildPageMetadata } from "@/lib/page-metadata";
12+
13+
export const metadata: Metadata = buildPageMetadata({
14+
path: "/download",
1215
title: "Download",
1316
description: "Download the latest DevStackBox installer for Windows.",
14-
};
17+
});
1518

1619
export default async function DownloadPage() {
1720
const release = await getLatestRelease();

app/faq/page.tsx

Lines changed: 0 additions & 42 deletions
This file was deleted.

app/layout.tsx

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,29 @@
11
import type { Metadata } from "next";
22
import { RootProvider } from "fumadocs-ui/provider/next";
3+
import { JsonLd } from "@/components/json-ld";
34
import { SiteFooter } from "@/components/site-footer";
45
import { SiteHeader } from "@/components/site-header";
56
import { siteConfig } from "@/content/homepage";
7+
import { buildPageMetadata, DEFAULT_OG_IMAGE } from "@/lib/page-metadata";
8+
import { websiteSchemas } from "@/lib/structured-data";
69
import "./globals.css";
710

811
export const metadata: Metadata = {
9-
metadataBase: new URL(siteConfig.url),
12+
...buildPageMetadata({
13+
path: "/",
14+
description:
15+
"Apache, PHP, MySQL and phpMyAdmin in one free, open-source desktop application for Windows. No telemetry. No cloud dependency.",
16+
}),
1017
title: {
1118
default: "DevStackBox - Modern local PHP development for Windows",
1219
template: "%s · DevStackBox",
1320
},
14-
description:
15-
"Apache, PHP, MySQL and phpMyAdmin in one free, open-source desktop application for Windows. No telemetry. No cloud dependency.",
21+
metadataBase: new URL(siteConfig.url),
1622
openGraph: {
1723
type: "website",
1824
locale: "en_US",
19-
url: siteConfig.url,
2025
siteName: siteConfig.name,
21-
description:
22-
"Apache, PHP, MySQL and phpMyAdmin in one free, open-source desktop application for Windows.",
23-
},
24-
twitter: {
25-
card: "summary_large_image",
26-
title: siteConfig.name,
27-
description:
28-
"Apache, PHP, MySQL and phpMyAdmin in one free, open-source desktop application for Windows.",
26+
images: [{ url: DEFAULT_OG_IMAGE, width: 1200, height: 630, alt: siteConfig.name }],
2927
},
3028
};
3129

@@ -37,26 +35,12 @@ export default function RootLayout({
3735
return (
3836
<html lang="en" suppressHydrationWarning>
3937
<body className="flex min-h-screen flex-col bg-background font-sans text-foreground antialiased">
38+
<JsonLd data={websiteSchemas()} />
4039
<RootProvider>
4140
<SiteHeader />
4241
<main className="flex-1">{children}</main>
4342
<SiteFooter />
4443
</RootProvider>
45-
<script
46-
type="application/ld+json"
47-
dangerouslySetInnerHTML={{
48-
__html: JSON.stringify({
49-
"@context": "https://schema.org",
50-
"@type": "SoftwareApplication",
51-
name: "DevStackBox",
52-
applicationCategory: "DeveloperApplication",
53-
operatingSystem: "Windows 10, Windows 11",
54-
offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
55-
url: siteConfig.url,
56-
downloadUrl: `${siteConfig.url}/download`,
57-
}),
58-
}}
59-
/>
6044
</body>
6145
</html>
6246
);

app/license/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import type { Metadata } from "next";
22
import { MarkdownPage } from "@/components/markdown-page";
33
import { readRootMarkdown } from "@/lib/markdown";
44

5-
export const metadata: Metadata = {
5+
import { buildPageMetadata } from "@/lib/page-metadata";
6+
7+
export const metadata: Metadata = buildPageMetadata({
8+
path: "/license",
69
title: "License",
710
description: "DevStackBox is released under the MIT License.",
8-
};
11+
});
912

1013
export default function LicensePage() {
1114
return (

app/privacy/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import type { Metadata } from "next";
22
import { MarkdownPage } from "@/components/markdown-page";
33
import { readRootMarkdown } from "@/lib/markdown";
44

5-
export const metadata: Metadata = {
5+
import { buildPageMetadata } from "@/lib/page-metadata";
6+
7+
export const metadata: Metadata = buildPageMetadata({
8+
path: "/privacy",
69
title: "Privacy",
710
description: "DevStackBox privacy policy - no telemetry, local-first.",
8-
};
11+
});
912

1013
export default function PrivacyPage() {
1114
return (

app/releases/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import type { Metadata } from "next";
22
import Link from "next/link";
33
import { formatReleaseDate, getReleases } from "@/lib/github";
44

5-
export const metadata: Metadata = {
5+
import { buildPageMetadata } from "@/lib/page-metadata";
6+
7+
export const metadata: Metadata = buildPageMetadata({
8+
path: "/releases",
69
title: "Releases",
710
description: "DevStackBox release history and downloads.",
8-
};
11+
});
912

1013
export default async function ReleasesPage() {
1114
const releases = await getReleases(30);

app/roadmap/page.tsx

Lines changed: 0 additions & 45 deletions
This file was deleted.

0 commit comments

Comments
 (0)