Skip to content

Commit b7f589d

Browse files
committed
extract json-ld component and add frontmatter placeholders
This GEO optimization change restructures JSON-LD rendering so the custom head override stays small and composition-focused. What changed: - moved JSON-LD graph construction into `src/components/JsonLD.astro` - simplified `src/components/Head.astro` to render `<Default />` + `<JsonLD />` - extended docs frontmatter schema in `src/content.config.ts` with optional `jsonLd` nodes while requiring non-empty `description` - added glossary page-level `DefinedTermSet` metadata through frontmatter - added placeholder substitution support for frontmatter JSON-LD values (`$site`, `$pageUrl`, `$bookId`, `$organizationId`, `$lang`, `$title`, `$description`) so pages can define self-contained schema nodes Why: - keep head override maintainable as we add GEO/schema features - let content authors attach route-specific structured data without repeating global wiring in component code Manual testing: - ran `bun lint` - ran `bun run build` - verified generated pages include expected JSON-LD graph entries for: - default docs pages (`TechArticle`) - glossary (`DefinedTermSet` from frontmatter) - 404 route (global graph only) Special considerations: - no `@id` normalization is applied to extra frontmatter nodes anymore; placeholders now determine final IDs directly.
1 parent a3fa8b1 commit b7f589d

File tree

4 files changed

+140
-54
lines changed

4 files changed

+140
-54
lines changed

src/components/Head.astro

Lines changed: 2 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,7 @@
11
---
22
import Default from "@astrojs/starlight/components/Head.astro";
3-
4-
const { lang, siteTitle } = Astro.locals.starlightRoute;
5-
const organizationId = "https://swmansion.com/#organization";
6-
const websiteId = new URL("#website", Astro.site);
7-
const bookId = new URL("#book", Astro.site);
8-
const defaultOgImage = new URL("og-default.png", Astro.site);
9-
10-
const globalJsonLd = {
11-
"@context": "https://schema.org",
12-
"@graph": [
13-
{
14-
"@type": "Organization",
15-
"@id": organizationId,
16-
name: "Software Mansion",
17-
url: "https://swmansion.com/",
18-
logo: {
19-
"@type": "ImageObject",
20-
url: new URL("swm-logo.png", Astro.site),
21-
},
22-
},
23-
{
24-
"@type": "WebSite",
25-
"@id": websiteId,
26-
url: Astro.site,
27-
name: siteTitle,
28-
inLanguage: lang,
29-
publisher: {
30-
"@id": organizationId,
31-
},
32-
},
33-
{
34-
"@type": "Book",
35-
"@id": bookId,
36-
name: siteTitle,
37-
description:
38-
"Practical guidance for setting up and scaling agentic engineering workflows in real software projects.",
39-
url: Astro.site,
40-
inLanguage: lang,
41-
image: defaultOgImage,
42-
publisher: { "@id": organizationId },
43-
author: [
44-
{ "@type": "Person", name: "Marek Kaput" },
45-
{ "@type": "Person", name: "Jakub Kosmydel" },
46-
{ "@type": "Person", name: "Adam Grzybowski" },
47-
],
48-
},
49-
],
50-
};
3+
import JsonLD from "./JsonLD.astro";
514
---
525

536
<Default />
54-
<script
55-
type="application/ld+json"
56-
is:inline
57-
set:html={JSON.stringify(globalJsonLd)}
58-
/>
7+
<JsonLD />

src/components/JsonLD.astro

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
---
2+
const {
3+
lang,
4+
siteTitle,
5+
lastUpdated,
6+
entry: { id: entryId, data },
7+
} = Astro.locals.starlightRoute;
8+
9+
const isNotFoundPage = entryId === "404";
10+
11+
const organizationId = "https://swmansion.com/#organization";
12+
const websiteId = new URL("#website", Astro.site);
13+
const bookId = new URL("#book", Astro.site);
14+
const defaultOgImage = new URL("og-default.png", Astro.site);
15+
16+
const jsonLdPlaceholders = {
17+
$site: `${Astro.site}`,
18+
$pageUrl: `${Astro.url}`,
19+
$bookId: `${bookId}`,
20+
$organizationId: organizationId,
21+
$lang: lang,
22+
$title: data.title,
23+
$description: data.description,
24+
};
25+
26+
const replaceJsonLdPlaceholders = (value: string) =>
27+
Object.entries(jsonLdPlaceholders).reduce(
28+
(output, [placeholder, replacement]) =>
29+
output.replaceAll(placeholder, replacement),
30+
value,
31+
);
32+
33+
const applyJsonLdPlaceholders = (value: unknown): unknown =>
34+
JSON.parse(
35+
JSON.stringify(value, (_, nestedValue) =>
36+
typeof nestedValue === "string"
37+
? replaceJsonLdPlaceholders(nestedValue)
38+
: nestedValue,
39+
),
40+
);
41+
42+
const basePageNode = isNotFoundPage
43+
? null
44+
: {
45+
"@type": "TechArticle",
46+
"@id": new URL("#tech-article", Astro.url),
47+
name: data.title,
48+
headline: data.title,
49+
description: data.description,
50+
url: Astro.url,
51+
inLanguage: lang,
52+
image: defaultOgImage,
53+
mainEntityOfPage: Astro.url,
54+
isPartOf: { "@id": bookId },
55+
publisher: { "@id": organizationId },
56+
...(lastUpdated && { dateModified: lastUpdated.toISOString() }),
57+
};
58+
59+
const extraPageNodes = (data.jsonLd ?? []).map((node) =>
60+
applyJsonLdPlaceholders(node),
61+
);
62+
63+
const jsonLd = {
64+
"@context": "https://schema.org",
65+
"@graph": [
66+
{
67+
"@type": "Organization",
68+
"@id": organizationId,
69+
name: "Software Mansion",
70+
url: "https://swmansion.com/",
71+
logo: {
72+
"@type": "ImageObject",
73+
url: new URL("swm-logo.png", Astro.site),
74+
},
75+
},
76+
{
77+
"@type": "WebSite",
78+
"@id": websiteId,
79+
url: Astro.site,
80+
name: siteTitle,
81+
inLanguage: lang,
82+
publisher: {
83+
"@id": organizationId,
84+
},
85+
},
86+
{
87+
"@type": "Book",
88+
"@id": bookId,
89+
name: siteTitle,
90+
description:
91+
"Practical guidance for setting up and scaling agentic engineering workflows in real software projects.",
92+
url: Astro.site,
93+
inLanguage: lang,
94+
image: defaultOgImage,
95+
publisher: { "@id": organizationId },
96+
author: [
97+
{ "@type": "Person", name: "Marek Kaput" },
98+
{ "@type": "Person", name: "Jakub Kosmydel" },
99+
{ "@type": "Person", name: "Adam Grzybowski" },
100+
],
101+
},
102+
...(basePageNode ? [basePageNode] : []),
103+
...extraPageNodes,
104+
],
105+
};
106+
---
107+
108+
<script
109+
type="application/ld+json"
110+
is:inline
111+
set:html={JSON.stringify(jsonLd)}
112+
/>

src/content.config.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
import { defineCollection } from "astro:content";
2+
import { z } from "astro/zod";
23
import { docsLoader } from "@astrojs/starlight/loaders";
34
import { docsSchema } from "@astrojs/starlight/schema";
45

6+
const jsonLdNodeSchema = z
7+
.object({
8+
"@type": z.string().min(1),
9+
})
10+
.passthrough();
11+
512
export const collections = {
6-
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
13+
docs: defineCollection({
14+
loader: docsLoader(),
15+
schema: docsSchema({
16+
extend: z.object({
17+
/** All pages all require to have a meaningful description. */
18+
description: z.string().min(1),
19+
jsonLd: z.array(jsonLdNodeSchema).optional(),
20+
}),
21+
}),
22+
}),
723
};

src/content/docs/getting-started/glossary.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
---
22
title: Glossary
33
description: Definitions of core terms used throughout this guide.
4+
jsonLd:
5+
- "@type": "DefinedTermSet"
6+
"@id": "$pageUrl#defined-term-set"
7+
name: "$title"
8+
description: "$description"
9+
url: "$pageUrl"
10+
inLanguage: "$lang"
11+
isPartOf: { "@id": "$bookId" }
12+
publisher: { "@id": "$organizationId" }
413
---
514

615
Before we start, let us introduce a few terms that we will use a lot:

0 commit comments

Comments
 (0)