Skip to content

Commit 4f53137

Browse files
committed
test: close SEO verifier review gaps
1 parent 41e332c commit 4f53137

6 files changed

Lines changed: 86 additions & 55 deletions

next.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ let nextConfig = {
306306
//
307307
// The SEO problem it leaves behind — /en/help/ and /en/help both returning
308308
// 200 — is solved narrowly instead: redirects.json ends with a
309-
// `/:locale(en|es-419|es-ar|pt-br)/:path*/` -> slashless permanent (308)
309+
// `/:locale(en|es-419|es-ar|pt-br)/:path+/` -> slashless permanent (308)
310310
// redirect, which only covers the locale-prefixed marketing tree and cannot
311311
// touch /relay, /monitoring, /passkeys or the recipient catch-all. Keep that
312312
// locale list in sync with SUPPORTED_LOCALES (src/i18n/types.ts).
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/** @jest-environment node */
2+
3+
import { isPublishedContent, parseContentFrontmatter } from '../verify-content-frontmatter'
4+
5+
describe('content verifier frontmatter', () => {
6+
it.each(['published: false', 'published: False', 'published: false # draft'])(
7+
'treats YAML boolean %s as unpublished',
8+
(publishedLine) => {
9+
expect(isPublishedContent(`---\n${publishedLine}\n---\nDraft`)).toBe(false)
10+
}
11+
)
12+
13+
it('defaults missing publication state to published', () => {
14+
expect(isPublishedContent('---\ntitle: Published\n---\nBody')).toBe(true)
15+
})
16+
17+
it('uses YAML parsing for the remaining frontmatter fields', () => {
18+
expect(parseContentFrontmatter('---\nskip_polish_check: true # reviewed\n---\nBody')).toMatchObject({
19+
skip_polish_check: true,
20+
})
21+
})
22+
})
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/** @jest-environment node */
2+
3+
import { isKnownRouteOrLocaleRedirect } from '../verify-content-routes'
4+
5+
describe('content route aliases', () => {
6+
const validPaths = new Set(['/en/pricing', '/es-419/help/delete-account'])
7+
8+
it('accepts direct routes and retired-locale aliases with real destinations', () => {
9+
expect(isKnownRouteOrLocaleRedirect('/en/pricing', validPaths)).toBe(true)
10+
expect(isKnownRouteOrLocaleRedirect('/es-es', validPaths)).toBe(true)
11+
expect(isKnownRouteOrLocaleRedirect('/es-es/help/delete-account', validPaths)).toBe(true)
12+
})
13+
14+
it('rejects retired-locale aliases whose destinations do not exist', () => {
15+
expect(isKnownRouteOrLocaleRedirect('/es-es/definitely-not-a-route', validPaths)).toBe(false)
16+
})
17+
})
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import matter from 'gray-matter'
2+
3+
export function parseContentFrontmatter(content: string): Record<string, unknown> {
4+
return matter(content).data as Record<string, unknown>
5+
}
6+
7+
/** Match the application's publication contract: only YAML boolean false is a draft. */
8+
export function isPublishedContent(content: string): boolean {
9+
return parseContentFrontmatter(content).published !== false
10+
}

scripts/verify-content-routes.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const LOCALE_REDIRECTS = {
2+
'es-es': 'es-419',
3+
} as const
4+
5+
/**
6+
* A retired-locale URL is valid only when its redirect destination is a real
7+
* route. Accepting the alias prefix by itself would hide broken links such as
8+
* /es-es/definitely-not-a-route.
9+
*/
10+
export function isKnownRouteOrLocaleRedirect(url: string, validPaths: ReadonlySet<string>): boolean {
11+
if (validPaths.has(url)) return true
12+
13+
for (const [sourceLocale, destinationLocale] of Object.entries(LOCALE_REDIRECTS)) {
14+
const sourceRoot = `/${sourceLocale}`
15+
if (url !== sourceRoot && !url.startsWith(`${sourceRoot}/`)) continue
16+
if (url === sourceRoot) return true
17+
18+
const destination = `/${destinationLocale}${url.slice(sourceRoot.length)}`
19+
return validPaths.has(destination)
20+
}
21+
22+
return false
23+
}

scripts/verify-content.ts

Lines changed: 13 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import fs from 'fs'
2828
import path from 'path'
2929
import { RAIL_SLUGS } from '../src/data/seo/deposit-rails'
3030
import { SUPPORTED_LOCALES } from '../src/i18n/types'
31+
import { isPublishedContent, parseContentFrontmatter } from './verify-content-frontmatter'
32+
import { isKnownRouteOrLocaleRedirect } from './verify-content-routes'
3133

3234
const ROOT = path.join(process.cwd(), 'src/content')
3335
const CONTENT_DIR = path.join(ROOT, 'content')
@@ -37,15 +39,6 @@ const PRIMARY_LOCALES = ['en', 'es-419', 'pt-br']
3739
const LOCALE_PATH_PREFIX = new RegExp(
3840
`^/(${SUPPORTED_LOCALES.map((locale) => locale.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})/`
3941
)
40-
// These locales no longer render pages, but their old URLs remain valid link
41-
// destinations because redirects.json forwards both the bare root and every
42-
// subpath. Keep this list separate from SUPPORTED_LOCALES: adding it to the
43-
// route/sitemap loops would recreate phantom static pages.
44-
const REDIRECTED_LOCALE_ALIASES = ['es-es']
45-
const REDIRECTED_LOCALE_PATH_PREFIX = new RegExp(
46-
`^/(${REDIRECTED_LOCALE_ALIASES.map((locale) => locale.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})(?:/|$)`
47-
)
48-
4942
// `content/deposit/` mixes two URL families on the same dynamic route:
5043
// exchanges → /{locale}/deposit/from-{slug}
5144
// rails → /{locale}/deposit/via-{slug}
@@ -96,7 +89,7 @@ function gateReceiveSources(): string[] {
9689
.filter((slug) => slug !== 'index')
9790
.filter((slug) => {
9891
const en = path.join(CONTENT_DIR, 'receive-from', slug, 'en.md')
99-
return fs.existsSync(en) && isPublished(fs.readFileSync(en, 'utf-8'))
92+
return fs.existsSync(en) && isPublishedContent(fs.readFileSync(en, 'utf-8'))
10093
})
10194
}
10295

@@ -132,36 +125,6 @@ function isContentPage(filePath: string): boolean {
132125
return true
133126
}
134127

135-
// --- Frontmatter parsing ---
136-
137-
function parseFrontmatter(content: string): Record<string, unknown> {
138-
const match = content.match(/^---\n([\s\S]*?)\n---/)
139-
if (!match) return {}
140-
const frontmatter: Record<string, unknown> = {}
141-
for (const line of match[1].split('\n')) {
142-
const colonIdx = line.indexOf(':')
143-
if (colonIdx === -1) continue
144-
const key = line.slice(0, colonIdx).trim()
145-
let value: string | boolean = line.slice(colonIdx + 1).trim()
146-
if (value === 'true') value = true
147-
else if (value === 'false') value = false
148-
// Strip quotes
149-
if (
150-
typeof value === 'string' &&
151-
((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
152-
) {
153-
value = value.slice(1, -1)
154-
}
155-
frontmatter[key] = value
156-
}
157-
return frontmatter
158-
}
159-
160-
function isPublished(content: string): boolean {
161-
const fm = parseFrontmatter(content)
162-
return fm.published !== false
163-
}
164-
165128
// --- Build valid paths from actual routes ---
166129

167130
function discoverRoutes(): Set<string> {
@@ -350,10 +313,6 @@ function cleanUrl(url: string): string {
350313
return url.split('?')[0].split('#')[0].replace(/\/$/, '')
351314
}
352315

353-
function isKnownRouteOrLocaleRedirect(url: string, validPaths: Set<string>): boolean {
354-
return validPaths.has(url) || REDIRECTED_LOCALE_PATH_PREFIX.test(url)
355-
}
356-
357316
// --- Pass 1: Internal link validation ---
358317

359318
function checkLinks(validPaths: Set<string>) {
@@ -364,7 +323,7 @@ function checkLinks(validPaths: Set<string>) {
364323
for (const file of files) {
365324
if (!isContentPage(file)) continue
366325
const content = fs.readFileSync(file, 'utf-8')
367-
if (!isPublished(content)) {
326+
if (!isPublishedContent(content)) {
368327
skippedUnpublished++
369328
continue
370329
}
@@ -417,7 +376,7 @@ function checkPublishedHasRoute(validPaths: Set<string>) {
417376
const enFile = path.join(CONTENT_DIR, ct.dir, slug, 'en.md')
418377
if (!fs.existsSync(enFile)) continue
419378
const content = fs.readFileSync(enFile, 'utf-8')
420-
if (!isPublished(content)) continue
379+
if (!isPublishedContent(content)) continue
421380

422381
const url = ct.urlPattern('en', slug)
423382
if (!validPaths.has(url)) {
@@ -432,7 +391,7 @@ function checkPublishedHasRoute(validPaths: Set<string>) {
432391
const enFile = path.join(CONTENT_DIR, intent, 'en.md')
433392
if (!fs.existsSync(enFile)) continue
434393
const content = fs.readFileSync(enFile, 'utf-8')
435-
if (!isPublished(content)) continue
394+
if (!isPublishedContent(content)) continue
436395

437396
const url = `/en/${intent}`
438397
if (!validPaths.has(url)) {
@@ -489,9 +448,9 @@ function checkFrontmatter() {
489448
for (const file of files) {
490449
if (!isContentPage(file)) continue
491450
const content = fs.readFileSync(file, 'utf-8')
492-
const fm = parseFrontmatter(content)
451+
const fm = parseContentFrontmatter(content)
493452

494-
if (!isPublished(content)) continue
453+
if (!isPublishedContent(content)) continue
495454

496455
if (!fm.title || (typeof fm.title === 'string' && fm.title.trim() === '')) {
497456
error('frontmatter', 'Published file missing title', rel(file))
@@ -520,7 +479,7 @@ function checkLocaleCoverage() {
520479
const enFile = path.join(slugDir, 'en.md')
521480
if (!fs.existsSync(enFile)) continue
522481
const content = fs.readFileSync(enFile, 'utf-8')
523-
if (!isPublished(content)) continue
482+
if (!isPublishedContent(content)) continue
524483

525484
for (const locale of PRIMARY_LOCALES) {
526485
if (locale === 'en') continue
@@ -559,9 +518,9 @@ function checkContentPolish() {
559518
for (const file of files) {
560519
if (!isContentPage(file)) continue
561520
const content = fs.readFileSync(file, 'utf-8')
562-
if (!isPublished(content)) continue
521+
if (!isPublishedContent(content)) continue
563522

564-
const fm = parseFrontmatter(content)
523+
const fm = parseContentFrontmatter(content)
565524

566525
// Frontmatter override: skip_polish_check: true bypasses this check
567526
if (fm.skip_polish_check === true) continue
@@ -604,7 +563,7 @@ function checkExplicitPublished() {
604563

605564
for (const file of files) {
606565
const content = fs.readFileSync(file, 'utf-8')
607-
const fm = parseFrontmatter(content)
566+
const fm = parseContentFrontmatter(content)
608567

609568
if (fm.published === false) {
610569
warn('draft-content', 'File is explicitly unpublished (draft)', rel(file))
@@ -659,7 +618,7 @@ function checkPageCountRegression() {
659618
const files = getAllMdFiles(CONTENT_DIR)
660619
const publishedCount = files.filter((f) => {
661620
const content = fs.readFileSync(f, 'utf-8')
662-
return isPublished(content)
621+
return isPublishedContent(content)
663622
}).length
664623

665624
let baseline = 0

0 commit comments

Comments
 (0)