feat(web): add structured SEO data

This commit is contained in:
jinhojang6
2026-08-20 23:10:38 +09:00
committed by Jinho Jang
parent 130a8e029b
commit 5fcb7c5fa5
14 changed files with 426 additions and 55 deletions
@@ -5,16 +5,21 @@ import {
ContentNotFoundError,
getAllIdeas,
getAllRfps,
getBuilderHubListingSettings,
getIdeaBySlug,
getPageCopy,
} from '@repo/content/loaders'
import { BuildersHubDetailLayout } from '@/components/sections/builders-hub/builders-hub-detail-layout'
import { RelatedLinksList } from '@/components/sections/builders-hub/related-links-list'
import { JsonLd } from '@/components/seo/json-ld'
import { ROUTES } from '@/constants/routes'
import siteConfig from '@/constants/site-config'
import { formatDateLong } from '@/lib/dates'
import { createDefaultMetadata } from '@/lib/metadata'
import { resolveLocale, type LocaleSlugParams } from '@/lib/route-params'
import { formatReward } from '@/lib/reward'
import { createBreadcrumbListJsonLd } from '@/lib/structured-data'
export async function generateStaticParams() {
const params: Array<{ locale: string; slug: string }> = []
@@ -60,6 +65,11 @@ export default async function IdeaDetailPage({ params }: LocaleSlugParams) {
}
if (idea.status !== 'published') notFound()
const [buildersHub, listingSettings] = await Promise.all([
getPageCopy(ROUTES.buildersHub, locale),
getBuilderHubListingSettings({ page: 'ideas', locale }),
])
const submitter = idea.submitter.name
? `${idea.submitter.name} (@${idea.submitter.handle})`
: `@${idea.submitter.handle}`
@@ -90,30 +100,46 @@ export default async function IdeaDetailPage({ params }: LocaleSlugParams) {
}
return (
<BuildersHubDetailLayout
backHref={ROUTES.ideas}
backLabel="All ideas"
eyebrow={`Idea · ${idea.status}`}
title={idea.title}
tagline={idea.tagline}
description={idea.description}
primaryCta={
idea.discussionUrl
? {
label: idea.ctaLabel ?? 'Discuss',
href: idea.discussionUrl,
external: true,
}
: undefined
}
meta={meta}
footer={
<RelatedLinksList
heading="Related RFPs"
hrefBase={ROUTES.rfps}
items={related}
/>
}
/>
<>
<JsonLd
data={createBreadcrumbListJsonLd(
[
{ name: siteConfig.name, path: ROUTES.home },
{
name: buildersHub.heading ?? buildersHub.title,
path: ROUTES.buildersHub,
},
{ name: listingSettings.breadcrumbLabel, path: ROUTES.ideas },
{ name: idea.title, path: `${ROUTES.ideas}/${slug}` },
],
locale
)}
/>
<BuildersHubDetailLayout
backHref={ROUTES.ideas}
backLabel="All ideas"
eyebrow={`Idea · ${idea.status}`}
title={idea.title}
tagline={idea.tagline}
description={idea.description}
primaryCta={
idea.discussionUrl
? {
label: idea.ctaLabel ?? 'Discuss',
href: idea.discussionUrl,
external: true,
}
: undefined
}
meta={meta}
footer={
<RelatedLinksList
heading="Related RFPs"
hrefBase={ROUTES.rfps}
items={related}
/>
}
/>
</>
)
}
@@ -1,12 +1,16 @@
import {
getAllIdeas,
getBuilderHubListingSettings,
getPageCopy,
} from '@repo/content/loaders'
import { isActiveLocale } from '@repo/content/locales'
import { BuildersHubListingClient } from '@/components/sections/builders-hub/builders-hub-listing-client'
import { JsonLd } from '@/components/seo/json-ld'
import { ROUTES } from '@/constants/routes'
import siteConfig from '@/constants/site-config'
import { createDefaultMetadata } from '@/lib/metadata'
import { createBreadcrumbListJsonLd } from '@/lib/structured-data'
const ROUTE = ROUTES.ideas
@@ -38,16 +42,32 @@ export default async function IdeasPage({
throw new Error(`IdeasPage received non-active locale "${locale}"`)
}
const [settings, allIdeas] = await Promise.all([
const [settings, allIdeas, buildersHub] = await Promise.all([
getBuilderHubListingSettings({ page: 'ideas', locale }),
getAllIdeas({ locale, status: 'published' }),
getPageCopy(ROUTES.buildersHub, locale),
])
return (
<BuildersHubListingClient
kind="ideas"
settings={settings}
items={allIdeas}
/>
<>
<JsonLd
data={createBreadcrumbListJsonLd(
[
{ name: siteConfig.name, path: ROUTES.home },
{
name: buildersHub.heading ?? buildersHub.title,
path: ROUTES.buildersHub,
},
{ name: settings.breadcrumbLabel, path: ROUTE },
],
locale
)}
/>
<BuildersHubListingClient
kind="ideas"
settings={settings}
items={allIdeas}
/>
</>
)
}
@@ -1,11 +1,17 @@
import { notFound } from 'next/navigation'
import { getActiveLocales } from '@repo/content/locales'
import {
getBuilderHubListingSettings,
getPageCopy,
} from '@repo/content/loaders'
import { BuildersHubDetailLayout } from '@/components/sections/builders-hub/builders-hub-detail-layout'
import { JsonLd } from '@/components/seo/json-ld'
import { LegalMarkdown } from '@/components/sections/shared/legal-markdown'
import { Button } from '@/components/ui'
import { ROUTES } from '@/constants/routes'
import siteConfig from '@/constants/site-config'
import { createDefaultMetadata } from '@/lib/metadata'
import { resolveLocale, type LocaleSlugParams } from '@/lib/route-params'
import {
@@ -14,6 +20,7 @@ import {
fetchGithubRfps,
stripLeadingHeading,
} from '@/lib/rfps-github'
import { createBreadcrumbListJsonLd } from '@/lib/structured-data'
export async function generateStaticParams() {
const rfps = await fetchGithubRfps()
@@ -45,12 +52,17 @@ export async function generateMetadata({ params }: LocaleSlugParams) {
}
export default async function RfpDetailPage({ params }: LocaleSlugParams) {
await resolveLocale(params, 'RfpDetailPage')
const locale = await resolveLocale(params, 'RfpDetailPage')
const { slug } = await params
const rfp = await fetchGithubRfpBySlug(slug)
if (!rfp) notFound()
const [buildersHub, listingSettings] = await Promise.all([
getPageCopy(ROUTES.buildersHub, locale),
getBuilderHubListingSettings({ page: 'rfps', locale }),
])
const meta = [
{ label: 'Status', value: rfp.status },
rfp.category ? { label: 'Category', value: rfp.category } : null,
@@ -58,24 +70,40 @@ export default async function RfpDetailPage({ params }: LocaleSlugParams) {
].filter((x): x is { label: string; value: string } => Boolean(x))
return (
<BuildersHubDetailLayout
backHref={ROUTES.rfps}
backLabel="All RFPs"
eyebrow={`${rfp.number} · ${rfp.status}`}
title={rfp.title}
tagline={rfp.summary}
body={<LegalMarkdown body={stripLeadingHeading(rfp.rawMarkdown)} />}
primaryCta={{
label: 'Apply',
href: RFP_APPLY_URL,
external: true,
}}
meta={meta}
footer={
<Button href={rfp.githubUrl} variant="secondary">
View on GitHub
</Button>
}
/>
<>
<JsonLd
data={createBreadcrumbListJsonLd(
[
{ name: siteConfig.name, path: ROUTES.home },
{
name: buildersHub.heading ?? buildersHub.title,
path: ROUTES.buildersHub,
},
{ name: listingSettings.breadcrumbLabel, path: ROUTES.rfps },
{ name: rfp.title, path: `${ROUTES.rfps}/${slug}` },
],
locale
)}
/>
<BuildersHubDetailLayout
backHref={ROUTES.rfps}
backLabel="All RFPs"
eyebrow={`${rfp.number} · ${rfp.status}`}
title={rfp.title}
tagline={rfp.summary}
body={<LegalMarkdown body={stripLeadingHeading(rfp.rawMarkdown)} />}
primaryCta={{
label: 'Apply',
href: RFP_APPLY_URL,
external: true,
}}
meta={meta}
footer={
<Button href={rfp.githubUrl} variant="secondary">
View on GitHub
</Button>
}
/>
</>
)
}
@@ -1,10 +1,16 @@
import { getBuilderHubListingSettings } from '@repo/content/loaders'
import {
getBuilderHubListingSettings,
getPageCopy,
} from '@repo/content/loaders'
import { isActiveLocale } from '@repo/content/locales'
import { BuildersHubListingClient } from '@/components/sections/builders-hub/builders-hub-listing-client'
import { JsonLd } from '@/components/seo/json-ld'
import { ROUTES } from '@/constants/routes'
import siteConfig from '@/constants/site-config'
import { createDefaultMetadata } from '@/lib/metadata'
import { fetchGithubRfps } from '@/lib/rfps-github'
import { createBreadcrumbListJsonLd } from '@/lib/structured-data'
const ROUTE = ROUTES.rfps
@@ -36,12 +42,32 @@ export default async function RfpsPage({
throw new Error(`RfpsPage received non-active locale "${locale}"`)
}
const [settings, allRfps] = await Promise.all([
const [settings, allRfps, buildersHub] = await Promise.all([
getBuilderHubListingSettings({ page: 'rfps', locale }),
fetchGithubRfps(),
getPageCopy(ROUTES.buildersHub, locale),
])
return (
<BuildersHubListingClient kind="rfps" settings={settings} items={allRfps} />
<>
<JsonLd
data={createBreadcrumbListJsonLd(
[
{ name: siteConfig.name, path: ROUTES.home },
{
name: buildersHub.heading ?? buildersHub.title,
path: ROUTES.buildersHub,
},
{ name: settings.breadcrumbLabel, path: ROUTE },
],
locale
)}
/>
<BuildersHubListingClient
kind="rfps"
settings={settings}
items={allRfps}
/>
</>
)
}
@@ -9,9 +9,12 @@ import {
import { isActiveLocale } from '@repo/content/locales'
import { FieldGuidePageView } from '@/components/sections/field-guide'
import { JsonLd } from '@/components/seo/json-ld'
import { ROUTES } from '@/constants/routes'
import siteConfig from '@/constants/site-config'
import { routing } from '@/i18n/routing'
import { createDefaultMetadata } from '@/lib/metadata'
import { createBreadcrumbListJsonLd } from '@/lib/structured-data'
const INDEX_SLUG = 'index'
@@ -73,7 +76,26 @@ export default async function FieldGuideChapterPage({
getFieldGuideManifest(locale),
getFieldGuideChapter(locale, slug),
])
return <FieldGuidePageView manifest={manifest} slug={slug} body={body} />
const item = flattenFieldGuideItems(manifest).find(
(chapter) => chapter.slug === slug
)
if (!item) notFound()
return (
<>
<JsonLd
data={createBreadcrumbListJsonLd(
[
{ name: siteConfig.name, path: ROUTES.home },
{ name: manifest.title, path: ROUTES.fieldGuide },
{ name: item.title, path: ROUTES.fieldGuideChapter(slug) },
],
locale
)}
/>
<FieldGuidePageView manifest={manifest} slug={slug} body={body} />
</>
)
} catch (error) {
if (error instanceof ContentNotFoundError) notFound()
throw error
+3
View File
@@ -16,6 +16,7 @@ import type {
import AboutSection from '@/components/sections/home/about-section'
import BuilderPortalSection from '@/components/sections/home/builder-portal-section'
import DecideSection from '@/components/sections/home/decide-section'
import { JsonLd } from '@/components/seo/json-ld'
import FeatureCardsSection from '@/components/sections/shared/feature-cards-section'
import HeroSectionView from '@/components/sections/shared/hero-section'
import BlogSection from '@/components/sections/home/blog-section'
@@ -28,6 +29,7 @@ import { EXTERNAL_URLS, ROUTES } from '@/constants/routes'
import { createPageMetadata } from '@/lib/page-metadata'
import { createSectionFinder } from '@/lib/page-sections'
import { getLatestBlogArticles } from '@/lib/blog-engine'
import { createOrganizationJsonLd } from '@/lib/structured-data'
import { getSocialProofStats } from '@/lib/social-proof-stats'
import { getWinnableIssuesCount } from '@/lib/winnable-issues'
@@ -112,6 +114,7 @@ export default async function HomePage({
return (
<>
<JsonLd data={createOrganizationJsonLd()} />
<HeroSectionView data={hero} />
<SocialProofSection
data={socialProof}
@@ -10,6 +10,7 @@ import type {
import BlockchainCryptarchia from '@/components/sections/blockchain/blockchain-cryptarchia'
import BlockchainHero from '@/components/sections/blockchain/blockchain-hero'
import BlockchainPrivacy from '@/components/sections/blockchain/blockchain-privacy'
import { JsonLd } from '@/components/seo/json-ld'
import TechStackBuilderCta from '@/components/sections/shared/tech-stack-builder-cta'
import {
TechStackDetailPage,
@@ -18,10 +19,12 @@ import {
import TechStackExplorer from '@/components/sections/shared/tech-stack-explorer'
import TechStackRelatedArticles from '@/components/sections/shared/tech-stack-related-articles'
import { ROUTES } from '@/constants/routes'
import siteConfig from '@/constants/site-config'
import { createPageMetadata } from '@/lib/page-metadata'
import { createSectionFinder } from '@/lib/page-sections'
import { getLatestBlogArticles } from '@/lib/blog-engine'
import { TECH_STACK_RELATED_ARTICLE_TAGS } from '@/lib/tech-stack-related-articles'
import { createBreadcrumbListJsonLd } from '@/lib/structured-data'
const ROUTE = ROUTES.blockchain
@@ -73,6 +76,16 @@ export default async function BlockchainPage({
return (
<TechStackDetailPage>
<JsonLd
data={createBreadcrumbListJsonLd(
[
{ name: siteConfig.name, path: ROUTES.home },
{ name: hero.eyebrow ?? page.title, path: ROUTES.technologyStack },
{ name: page.heading ?? page.title, path: ROUTE },
],
locale
)}
/>
<BlockchainHero data={hero} backHref={ROUTES.technologyStack} />
<TechStackDetailSection className="mt-0 border-t border-brand-dark-green/10 md:border-t-0">
<BlockchainPrivacy data={privacy} />
@@ -11,6 +11,7 @@ import MessagingCaseStudies from '@/components/sections/messaging/messaging-case
import MessagingHero from '@/components/sections/messaging/messaging-hero'
import MessagingIntro from '@/components/sections/messaging/messaging-intro'
import MessagingTechStack from '@/components/sections/messaging/messaging-tech-stack'
import { JsonLd } from '@/components/seo/json-ld'
import TechStackBuilderCta from '@/components/sections/shared/tech-stack-builder-cta'
import {
TechStackDetailPage,
@@ -18,10 +19,12 @@ import {
} from '@/components/sections/shared/tech-stack-detail-layout'
import TechStackRelatedArticles from '@/components/sections/shared/tech-stack-related-articles'
import { ROUTES } from '@/constants/routes'
import siteConfig from '@/constants/site-config'
import { createPageMetadata } from '@/lib/page-metadata'
import { createSectionFinder } from '@/lib/page-sections'
import { getLatestBlogArticles } from '@/lib/blog-engine'
import { TECH_STACK_RELATED_ARTICLE_TAGS } from '@/lib/tech-stack-related-articles'
import { createBreadcrumbListJsonLd } from '@/lib/structured-data'
const ROUTE = ROUTES.messaging
@@ -91,6 +94,16 @@ export default async function MessagingPage({
return (
<TechStackDetailPage>
<JsonLd
data={createBreadcrumbListJsonLd(
[
{ name: siteConfig.name, path: ROUTES.home },
{ name: hero.eyebrow ?? page.title, path: ROUTES.technologyStack },
{ name: page.heading ?? page.title, path: ROUTE },
],
locale
)}
/>
<MessagingHero data={hero} backHref={ROUTES.technologyStack} />
<TechStackDetailSection>
<MessagingIntro privacy={privacy} lmn={lmn} censorship={censorship} />
@@ -10,6 +10,7 @@ import type {
import NetworkingFeatures from '@/components/sections/networking/networking-features'
import NetworkingHero from '@/components/sections/networking/networking-hero'
import NetworkingIntro from '@/components/sections/networking/networking-intro'
import { JsonLd } from '@/components/seo/json-ld'
import TechStackBuilderCta from '@/components/sections/shared/tech-stack-builder-cta'
import {
TechStackDetailPage,
@@ -19,10 +20,12 @@ import TechStackRelatedArticles from '@/components/sections/shared/tech-stack-re
import NetworkingTechStack from '@/components/sections/networking/networking-tech-stack'
import { ROUTES } from '@/constants/routes'
import siteConfig from '@/constants/site-config'
import { createPageMetadata } from '@/lib/page-metadata'
import { createSectionFinder } from '@/lib/page-sections'
import { getLatestBlogArticles } from '@/lib/blog-engine'
import { TECH_STACK_RELATED_ARTICLE_TAGS } from '@/lib/tech-stack-related-articles'
import { createBreadcrumbListJsonLd } from '@/lib/structured-data'
const ROUTE = ROUTES.networking
@@ -74,6 +77,16 @@ export default async function NetworkingPage({
return (
<TechStackDetailPage>
<JsonLd
data={createBreadcrumbListJsonLd(
[
{ name: siteConfig.name, path: ROUTES.home },
{ name: hero.eyebrow ?? page.title, path: ROUTES.technologyStack },
{ name: page.heading ?? page.title, path: ROUTE },
],
locale
)}
/>
<NetworkingHero data={hero} backHref={ROUTES.technologyStack} />
<TechStackDetailSection>
<NetworkingIntro data={intro} />
@@ -11,6 +11,7 @@ import StorageHero from '@/components/sections/storage/storage-hero'
import StorageAccess from '@/components/sections/storage/storage-access'
import StorageMain from '@/components/sections/storage/storage-main'
import StorageTechStack from '@/components/sections/storage/storage-tech-stack'
import { JsonLd } from '@/components/seo/json-ld'
// Temporarily hidden — may be reused later. Do not delete.
// import StorageUseCases from '@/components/sections/storage/storage-use-cases'
import TechStackBuilderCta from '@/components/sections/shared/tech-stack-builder-cta'
@@ -21,10 +22,12 @@ import {
import TechStackRelatedArticles from '@/components/sections/shared/tech-stack-related-articles'
import { ROUTES } from '@/constants/routes'
import siteConfig from '@/constants/site-config'
import { createPageMetadata } from '@/lib/page-metadata'
import { createSectionFinder } from '@/lib/page-sections'
import { getLatestBlogArticles } from '@/lib/blog-engine'
import { TECH_STACK_RELATED_ARTICLE_TAGS } from '@/lib/tech-stack-related-articles'
import { createBreadcrumbListJsonLd } from '@/lib/structured-data'
const ROUTE = ROUTES.storage
@@ -89,6 +92,16 @@ export default async function StoragePage({
return (
<TechStackDetailPage>
<JsonLd
data={createBreadcrumbListJsonLd(
[
{ name: siteConfig.name, path: ROUTES.home },
{ name: hero.eyebrow ?? page.title, path: ROUTES.technologyStack },
{ name: page.heading ?? page.title, path: ROUTE },
],
locale
)}
/>
<StorageHero data={hero} backHref={ROUTES.technologyStack} />
<TechStackDetailSection>
<StorageMain data={main} />
+16
View File
@@ -0,0 +1,16 @@
import type { JsonLdObject } from '@/lib/structured-data'
type Props = {
data: JsonLdObject
}
export function JsonLd({ data }: Props) {
const json = JSON.stringify(data).replace(/</g, '\\u003c')
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: json }}
/>
)
}
@@ -0,0 +1,73 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it } from 'vitest'
import { JsonLd } from '@/components/seo/json-ld'
import {
createBreadcrumbListJsonLd,
createOrganizationJsonLd,
} from '@/lib/structured-data'
const BASE_URL = 'https://logos.co'
describe('structured data', () => {
it('builds the canonical Logos organisation entity', () => {
expect(createOrganizationJsonLd()).toMatchObject({
'@context': 'https://schema.org',
'@type': 'Organization',
'@id': `${BASE_URL}/#organization`,
name: 'Logos',
url: BASE_URL,
logo: `${BASE_URL}/apple-touch-icon.png`,
})
})
it('builds ordered canonical breadcrumb URLs', () => {
expect(
createBreadcrumbListJsonLd(
[
{ name: 'Logos', path: '/' },
{ name: 'Technology Stack', path: '/technology-stack' },
{ name: 'Storage', path: '/technology-stack/storage' },
],
'en'
)
).toMatchObject({
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Logos',
item: `${BASE_URL}/`,
},
{
'@type': 'ListItem',
position: 2,
name: 'Technology Stack',
item: `${BASE_URL}/technology-stack`,
},
{
'@type': 'ListItem',
position: 3,
name: 'Storage',
item: `${BASE_URL}/technology-stack/storage`,
},
],
})
})
it('escapes markup that could terminate the JSON-LD script', () => {
const html = renderToStaticMarkup(
<JsonLd
data={{
'@context': 'https://schema.org',
'@type': 'Thing',
name: '</script><script>alert(1)</script>',
}}
/>
)
expect(html).not.toContain('</script><script>alert(1)</script>')
expect(html).toContain('\\u003c/script>')
})
})
+54
View File
@@ -0,0 +1,54 @@
import siteConfig from '@/constants/site-config'
import { absoluteUrl } from '@/lib/metadata'
import siteSettings from '../../../content/site/en/settings.json'
type JsonLdPrimitive = boolean | null | number | string
type JsonLdValue =
| JsonLdPrimitive
| JsonLdObject
| ReadonlyArray<JsonLdValue>
export interface JsonLdObject {
readonly [key: string]: JsonLdValue | undefined
}
export interface BreadcrumbItem {
name: string
path: string
}
export function createOrganizationJsonLd(): JsonLdObject {
const sameAs = [
siteSettings.social.twitter,
siteSettings.social.youtube,
siteSettings.social.github,
].filter((url): url is string => Boolean(url))
return {
'@context': 'https://schema.org',
'@type': 'Organization',
'@id': `${siteConfig.url}/#organization`,
name: siteConfig.name,
url: siteConfig.url,
logo: absoluteUrl('/apple-touch-icon.png'),
description: siteConfig.description,
sameAs,
}
}
export function createBreadcrumbListJsonLd(
items: ReadonlyArray<BreadcrumbItem>,
locale: string
): JsonLdObject {
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: absoluteUrl(item.path, locale),
})),
}
}
+51
View File
@@ -121,6 +121,56 @@ const isLocalAssetHref = (href: string): boolean => {
)
}
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null && !Array.isArray(value)
const breadcrumbTechRoutes = new Set<string>([
ROUTES.blockchain,
ROUTES.messaging,
ROUTES.networking,
ROUTES.storage,
])
const assertStructuredData = (route: string, html: string): string[] => {
const failures: string[] = []
const types = new Set<string>()
const scripts = html.matchAll(
/<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi
)
for (const [, rawJson] of scripts) {
try {
const parsed: unknown = JSON.parse(rawJson ?? '')
const entries = Array.isArray(parsed) ? parsed : [parsed]
for (const entry of entries) {
if (isRecord(entry) && typeof entry['@type'] === 'string') {
types.add(entry['@type'])
}
}
} catch {
failures.push(`${route} contains invalid JSON-LD`)
}
}
if (route === ROUTES.home && !types.has('Organization')) {
failures.push(`${route} is missing Organization JSON-LD`)
}
const expectsBreadcrumb =
route === ROUTES.ideas ||
route === ROUTES.rfps ||
route.startsWith(`${ROUTES.ideas}/`) ||
route.startsWith(`${ROUTES.rfps}/`) ||
route.startsWith(`${ROUTES.fieldGuide}/`) ||
breadcrumbTechRoutes.has(route)
if (expectsBreadcrumb && !types.has('BreadcrumbList')) {
failures.push(`${route} is missing BreadcrumbList JSON-LD`)
}
return failures
}
const assertHtmlPage = (route: string, filePath: string): string[] => {
const html = readFileSync(filePath, 'utf8')
const failures: string[] = []
@@ -133,6 +183,7 @@ const assertHtmlPage = (route: string, filePath: string): string[] => {
if (html.includes(`href="/${locale}/`) || html.includes(`src="/${locale}/`)) {
failures.push(`${route} still contains default-locale-prefixed asset paths`)
}
failures.push(...assertStructuredData(route, html))
const refs = html.matchAll(/\b(?:href|src)=["']([^"']+)["']/g)
for (const [, rawHref] of refs) {