feat: content routes, SEO footer & verify-content script#1785
feat: content routes, SEO footer & verify-content script#1785
Conversation
…nt verification Six content types had published content but no matching Next.js route — they silently fell through to the catch-all recipient page. The old validate-links script only checked links against content directories, not actual routes, so CI passed while production served the wrong page. New routes: - /[locale]/use-cases/[slug] (5 published use cases) - /[locale]/withdraw/[slug] (9 withdraw guides) - /[locale]/stories/[slug] + index (ready for when stories go live) - /[locale]/pricing (singleton) - /[locale]/supported-networks (singleton) Content infrastructure: - readSingletonContent/Localized in content.ts for pages without slug subdirs - ContentSEOFooter server component reading footer-manifest.json - SEO footer added to all marketing content pages via ContentPage locale prop - 5 new route slugs registered for hreflang alternate tags verify-content replaces validate-links with 5 verification passes: 1. Internal link validation (2013 links across 258 published files) 2. Published content → route existence (no catch-all fallback) 3. Footer manifest URL validation 4. Frontmatter consistency (title, description) 5. Cross-locale coverage warnings
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughReplace legacy link-validator with a new multi-pass content verifier, add singleton content support and multiple localized marketing pages, thread a locale prop into ContentPage, add a localized SEO footer, update route slugs, and bump the content submodule. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (7)
src/components/Marketing/ContentSEOFooter.tsx (2)
13-13: UnusedsendMoney.fromarray in manifest interface.The
FooterManifestinterface includesfrom: ManifestEntry[]at line 13, but onlysendMoney.tois rendered (lines 74-82). Iffromentries should appear in the footer, they need to be added to the JSX. If intentionally unused, consider removing it from the interface to avoid confusion.Also applies to: 74-82
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Marketing/ContentSEOFooter.tsx` at line 13, The FooterManifest interface declares sendMoney: { to: ManifestEntry[]; from: ManifestEntry[] } but only sendMoney.to is rendered; either remove the unused from: ManifestEntry[] from the FooterManifest type (if not needed) or add rendering for the sendMoney.from entries in the ContentSEOFooter component (e.g., map over sendMoney.from similar to how sendMoney.to is mapped) and update any related JSX/props to include appropriate labels/accessibility; locate the declaration FooterManifest and the usage in ContentSEOFooter/JSX where sendMoney.to is rendered to apply the change.
29-32: Consider documenting the/en/prefix assumption inlocalizeHref.The function only handles paths starting with
/en/. If manifest entries ever use a different locale prefix or no prefix, localization would fail silently. This seems intentional given the manifest generation, but a brief comment would clarify the contract.+/** Rewrites hrefs from /en/... to /{locale}/... — manifest entries must use /en/ prefix */ function localizeHref(href: string, locale: string): string { if (href.startsWith('/en/')) return `/${locale}/${href.slice(4)}` return href }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Marketing/ContentSEOFooter.tsx` around lines 29 - 32, The localizeHref function only rewrites paths that start with '/en/' which can silently skip other prefixes; add a short comment above localizeHref explaining the contract: manifest entries are expected to be normalized with an '/en/' prefix and only those will be localized, and note that if manifests change to use different or no locale prefixes the function must be updated to handle those cases (reference function name localizeHref and its behavior of checking href.startsWith('/en/') and returning `/${locale}/${href.slice(4)}`).src/app/[locale]/(marketing)/stories/page.tsx (2)
26-27: Hardcoded English strings bypass i18n.Several strings are hardcoded in English while the page supports multiple locales:
- Line 26-27:
'User Stories | Peanut'and'Real stories from Peanut users around the world.'- Line 61:
'Stories'in breadcrumb (vsi18n.homeon line 60)- Line 64: Hero title/subtitle
Consider adding these to the i18n translations for consistency with other marketing pages, or document if this is intentional for SEO purposes.
Also applies to: 61-61, 64-64
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/(marketing)/stories/page.tsx around lines 26 - 27, Replace hardcoded English strings with i18n lookups: move the metadata.title and metadata.description values into your translations and use the i18n/t function (same pattern as i18n.home) instead of the literals; update the breadcrumb label currently set to 'Stories' to use the i18n key (e.g., t('stories.breadcrumb') or similar), and change the Hero component title/subtitle props to use i18n lookups (e.g., t('stories.hero.title') and t('stories.hero.subtitle')) so all text (metadata.title, metadata.description, breadcrumb label, and Hero title/subtitle) is served from the locale translations.
42-54: Minor: Redundant published check.
listPublishedSlugs('stories')at line 42 already filters forpublished !== false(persrc/lib/content.ts:212-217). The check at line 47 is defensive but redundant. Not a problem, just noting it's unnecessary.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/(marketing)/stories/page.tsx around lines 42 - 54, Remove the redundant published check inside the stories mapping: since listPublishedSlugs('stories') already returns only published slugs, update the mapping in the stories variable to call readPageContentLocalized<ContentFrontmatter>('stories', slug, locale), return null only when content is missing, and otherwise return the object with slug, title and description; remove any reference to content.frontmatter.published so the map relies on listPublishedSlugs, keeping the trailing .filter(Boolean) cast as-is.scripts/verify-content.ts (3)
436-460: Missing content types in locale coverage check.
checkLocaleCoveragechecks['countries', 'help', 'compare', 'pay-with', 'use-cases', 'deposit']but omits'stories'and'withdraw'which are included in Pass 2'scontentTypes. If locale coverage warnings are desired for these content types, they should be added here.♻️ Proposed fix to add missing content types
function checkLocaleCoverage() { - const contentTypes = ['countries', 'help', 'compare', 'pay-with', 'use-cases', 'deposit'] + const contentTypes = ['countries', 'help', 'compare', 'pay-with', 'use-cases', 'deposit', 'stories', 'withdraw'] let warnings = 0🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/verify-content.ts` around lines 436 - 460, The checkLocaleCoverage function currently uses contentTypes = ['countries', 'help', 'compare', 'pay-with', 'use-cases', 'deposit'] but omits 'stories' and 'withdraw', causing inconsistent coverage with Pass 2; update the contentTypes array inside checkLocaleCoverage to include 'stories' and 'withdraw' so the locale coverage checks mirror the set used in Pass 2 (ensure the symbol checkLocaleCoverage and the contentTypes array are the only places changed).
109-112:isPublishedlogic differs fromsrc/lib/content.tsbut matches route behavior.The library's
isPublishedreturnstrueonly whenpublished === true, but this script returnstruewhenpublished !== false. This matches how the route pages checkfrontmatter.published === falseto callnotFound().This is correct for verification, but the inconsistency between this script and the library function could confuse future maintainers. Consider adding a comment explaining the intentional difference.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/verify-content.ts` around lines 109 - 112, The isPublished function in scripts/verify-content.ts currently interprets frontmatter as published when fm.published !== false (using parseFrontmatter), which intentionally differs from the stricter isPublished in src/lib/content.ts that returns true only when published === true; add a concise comment above the isPublished function explaining this intentional divergence (reference parseFrontmatter and the library's isPublished in src/lib/content.ts) so future maintainers understand that the script mirrors route behavior (frontmatter.published === false -> notFound()) rather than the library's stricter semantics.
23-24: Hardcoded locale arrays may drift from source of truth.
SUPPORTED_LOCALESandPRIMARY_LOCALESare duplicated here instead of being imported fromsrc/i18n/config.ts. If locales are added or removed in the config, this script won't reflect those changes automatically.Consider importing from the config (requires adjusting the import path for a script context) or documenting that these must be kept in sync.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/verify-content.ts` around lines 23 - 24, The arrays SUPPORTED_LOCALES and PRIMARY_LOCALES in scripts/verify-content.ts are hardcoded and can drift from the canonical source; replace the duplicated definitions by importing the exported locale lists from src/i18n/config.ts (or require() it if the script runs outside TS compilation), updating the import path to work in the script context, and remove the local constants so the script always uses the single source of truth (or if import isn’t possible, add a clear comment noting they must be kept in sync with src/i18n/config.ts).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/`[locale]/(marketing)/stories/[slug]/page.tsx:
- Around line 56-60: The breadcrumbs array in the page component uses a
hardcoded 'Stories' label; replace that entry to use a localized key (e.g.,
i18n.stories) so it matches i18n.home usage — update the breadcrumbs initializer
where breadcrumbs=[{ name: i18n.home, ... }, { name: 'Stories', ... }, { name:
mdxSource.frontmatter.title, href: url }] to use i18n.stories, and add the
corresponding 'stories' translation key to your translation files so
i18n.stories resolves correctly.
---
Nitpick comments:
In `@scripts/verify-content.ts`:
- Around line 436-460: The checkLocaleCoverage function currently uses
contentTypes = ['countries', 'help', 'compare', 'pay-with', 'use-cases',
'deposit'] but omits 'stories' and 'withdraw', causing inconsistent coverage
with Pass 2; update the contentTypes array inside checkLocaleCoverage to include
'stories' and 'withdraw' so the locale coverage checks mirror the set used in
Pass 2 (ensure the symbol checkLocaleCoverage and the contentTypes array are the
only places changed).
- Around line 109-112: The isPublished function in scripts/verify-content.ts
currently interprets frontmatter as published when fm.published !== false (using
parseFrontmatter), which intentionally differs from the stricter isPublished in
src/lib/content.ts that returns true only when published === true; add a concise
comment above the isPublished function explaining this intentional divergence
(reference parseFrontmatter and the library's isPublished in src/lib/content.ts)
so future maintainers understand that the script mirrors route behavior
(frontmatter.published === false -> notFound()) rather than the library's
stricter semantics.
- Around line 23-24: The arrays SUPPORTED_LOCALES and PRIMARY_LOCALES in
scripts/verify-content.ts are hardcoded and can drift from the canonical source;
replace the duplicated definitions by importing the exported locale lists from
src/i18n/config.ts (or require() it if the script runs outside TS compilation),
updating the import path to work in the script context, and remove the local
constants so the script always uses the single source of truth (or if import
isn’t possible, add a clear comment noting they must be kept in sync with
src/i18n/config.ts).
In `@src/app/`[locale]/(marketing)/stories/page.tsx:
- Around line 26-27: Replace hardcoded English strings with i18n lookups: move
the metadata.title and metadata.description values into your translations and
use the i18n/t function (same pattern as i18n.home) instead of the literals;
update the breadcrumb label currently set to 'Stories' to use the i18n key
(e.g., t('stories.breadcrumb') or similar), and change the Hero component
title/subtitle props to use i18n lookups (e.g., t('stories.hero.title') and
t('stories.hero.subtitle')) so all text (metadata.title, metadata.description,
breadcrumb label, and Hero title/subtitle) is served from the locale
translations.
- Around line 42-54: Remove the redundant published check inside the stories
mapping: since listPublishedSlugs('stories') already returns only published
slugs, update the mapping in the stories variable to call
readPageContentLocalized<ContentFrontmatter>('stories', slug, locale), return
null only when content is missing, and otherwise return the object with slug,
title and description; remove any reference to content.frontmatter.published so
the map relies on listPublishedSlugs, keeping the trailing .filter(Boolean) cast
as-is.
In `@src/components/Marketing/ContentSEOFooter.tsx`:
- Line 13: The FooterManifest interface declares sendMoney: { to:
ManifestEntry[]; from: ManifestEntry[] } but only sendMoney.to is rendered;
either remove the unused from: ManifestEntry[] from the FooterManifest type (if
not needed) or add rendering for the sendMoney.from entries in the
ContentSEOFooter component (e.g., map over sendMoney.from similar to how
sendMoney.to is mapped) and update any related JSX/props to include appropriate
labels/accessibility; locate the declaration FooterManifest and the usage in
ContentSEOFooter/JSX where sendMoney.to is rendered to apply the change.
- Around line 29-32: The localizeHref function only rewrites paths that start
with '/en/' which can silently skip other prefixes; add a short comment above
localizeHref explaining the contract: manifest entries are expected to be
normalized with an '/en/' prefix and only those will be localized, and note that
if manifests change to use different or no locale prefixes the function must be
updated to handle those cases (reference function name localizeHref and its
behavior of checking href.startsWith('/en/') and returning
`/${locale}/${href.slice(4)}`).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 04fc6003-cb74-4a97-912a-a5a0e9259550
📒 Files selected for processing (22)
package.jsonscripts/validate-links.tsscripts/verify-content.tssrc/app/[locale]/(marketing)/[country]/page.tsxsrc/app/[locale]/(marketing)/compare/[slug]/page.tsxsrc/app/[locale]/(marketing)/deposit/[exchange]/page.tsxsrc/app/[locale]/(marketing)/help/[slug]/page.tsxsrc/app/[locale]/(marketing)/help/page.tsxsrc/app/[locale]/(marketing)/pay-with/[method]/page.tsxsrc/app/[locale]/(marketing)/pricing/page.tsxsrc/app/[locale]/(marketing)/receive-money-from/[country]/page.tsxsrc/app/[locale]/(marketing)/send-money-from/[from]/to/[to]/page.tsxsrc/app/[locale]/(marketing)/send-money-to/[country]/page.tsxsrc/app/[locale]/(marketing)/stories/[slug]/page.tsxsrc/app/[locale]/(marketing)/stories/page.tsxsrc/app/[locale]/(marketing)/supported-networks/page.tsxsrc/app/[locale]/(marketing)/use-cases/[slug]/page.tsxsrc/app/[locale]/(marketing)/withdraw/[slug]/page.tsxsrc/components/Marketing/ContentPage.tsxsrc/components/Marketing/ContentSEOFooter.tsxsrc/i18n/config.tssrc/lib/content.ts
💤 Files with no reviewable changes (1)
- scripts/validate-links.ts
| breadcrumbs={[ | ||
| { name: i18n.home, href: `/${locale}` }, | ||
| { name: 'Stories', href: `/${locale}/stories` }, | ||
| { name: mdxSource.frontmatter.title, href: url }, | ||
| ]} |
There was a problem hiding this comment.
Hardcoded 'Stories' breadcrumb breaks localization.
Line 58 uses a hardcoded 'Stories' string instead of an i18n key. Other breadcrumbs use i18n.home for localization. For consistency, this should use a localized string (e.g., i18n.stories if available).
🌐 Proposed fix for localized breadcrumb
breadcrumbs={[
{ name: i18n.home, href: `/${locale}` },
- { name: 'Stories', href: `/${locale}/stories` },
+ { name: i18n.stories, href: `/${locale}/stories` },
{ name: mdxSource.frontmatter.title, href: url },
]}Note: This requires adding a stories key to the translations in src/i18n.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`[locale]/(marketing)/stories/[slug]/page.tsx around lines 56 - 60,
The breadcrumbs array in the page component uses a hardcoded 'Stories' label;
replace that entry to use a localized key (e.g., i18n.stories) so it matches
i18n.home usage — update the breadcrumbs initializer where breadcrumbs=[{ name:
i18n.home, ... }, { name: 'Stories', ... }, { name: mdxSource.frontmatter.title,
href: url }] to use i18n.stories, and add the corresponding 'stories'
translation key to your translation files so i18n.stories resolves correctly.
ContentSEOFooter was rendering inside ContentPage (above the main footer). Moved it to the marketing layout, after Footer — matching the landing page pattern where SEOFooter renders below the main footer.
Pass 6 warns about published pages that use plain markdown without any MDX components (Hero, Callout, FAQ, etc.). Currently 33 pages flagged. This helps catch content generated from older templates that didn't use the MDX component system.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
scripts/verify-content.ts (2)
23-24: Consider importing locales from the canonical source to avoid drift.These arrays duplicate
SUPPORTED_LOCALESfromsrc/i18n/config.ts. If locales are added/removed in the app, this script won't reflect those changes until manually updated.Since this is a Node script, you may need to adjust the import path or re-export the constants from a shared location that works in both contexts.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/verify-content.ts` around lines 23 - 24, The SUPPORTED_LOCALES and PRIMARY_LOCALES arrays in scripts/verify-content.ts are duplicating the canonical lists in src/i18n/config.ts; replace the local constants by importing (or re-exporting) the arrays from the single source of truth so the script always reflects app changes—update scripts/verify-content.ts to import SUPPORTED_LOCALES and PRIMARY_LOCALES from src/i18n/config (or create a small shared export module that both the app and Node scripts can import) and remove the hard-coded arrays, adjusting the import path or export surface as needed for Node execution.
119-140: Hardcoded route lists may drift over time.These static and app routes are manually maintained. Consider adding a comment noting when they should be updated, or extracting them to a shared config if the list grows.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/verify-content.ts` around lines 119 - 140, The hardcoded route arrays used in the two for-loops that call routes.add (the static pages list and the app routes list) can drift over time; to fix this, extract these arrays into a single exported constant (e.g., ROUTES or APP_ROUTES) in a shared configuration module and import it into scripts/verify-content.ts, or at minimum add a clear TODO comment above the for-loops noting where the canonical source of truth lives and when to update them; update the loops to iterate over the imported constant(s) instead of inline literals and ensure any new route additions are documented in that shared config.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@scripts/verify-content.ts`:
- Around line 109-112: The isPublished function in scripts/verify-content.ts
currently treats undefined as published; update it to match the canonical logic
in src/lib/content.ts by parsing frontmatter with parseFrontmatter and returning
fm.published === true (so only an explicit true is considered published). Locate
the function isPublished and change its return condition accordingly to ensure
the validator's semantics align with the app's rendering behavior.
- Around line 56-63: In listEntitySlugs add a filter to exclude README.md so
README files aren't treated as content slugs: update the pipeline in the
listEntitySlugs function to filter out entries equal to 'README.md' (apply
before the .map that strips '.md'), ensuring only actual entity markdown files
become slugs.
---
Nitpick comments:
In `@scripts/verify-content.ts`:
- Around line 23-24: The SUPPORTED_LOCALES and PRIMARY_LOCALES arrays in
scripts/verify-content.ts are duplicating the canonical lists in
src/i18n/config.ts; replace the local constants by importing (or re-exporting)
the arrays from the single source of truth so the script always reflects app
changes—update scripts/verify-content.ts to import SUPPORTED_LOCALES and
PRIMARY_LOCALES from src/i18n/config (or create a small shared export module
that both the app and Node scripts can import) and remove the hard-coded arrays,
adjusting the import path or export surface as needed for Node execution.
- Around line 119-140: The hardcoded route arrays used in the two for-loops that
call routes.add (the static pages list and the app routes list) can drift over
time; to fix this, extract these arrays into a single exported constant (e.g.,
ROUTES or APP_ROUTES) in a shared configuration module and import it into
scripts/verify-content.ts, or at minimum add a clear TODO comment above the
for-loops noting where the canonical source of truth lives and when to update
them; update the loops to iterate over the imported constant(s) instead of
inline literals and ensure any new route additions are documented in that shared
config.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: da712f40-5eb0-442f-91ca-aad08d5f7287
📒 Files selected for processing (3)
scripts/verify-content.tssrc/app/[locale]/(marketing)/layout.tsxsrc/components/Marketing/ContentPage.tsx
| function listEntitySlugs(category: string): string[] { | ||
| const dir = path.join(ROOT, 'input/data', category) | ||
| if (!fs.existsSync(dir)) return [] | ||
| return fs | ||
| .readdirSync(dir) | ||
| .filter((f) => f.endsWith('.md')) | ||
| .map((f) => f.replace('.md', '')) | ||
| } |
There was a problem hiding this comment.
Missing README.md filter present in canonical implementation.
The listEntitySlugs in src/lib/content.ts (lines 146-147) filters out README.md files. Without this filter, any README.md in entity directories would be treated as a content slug.
🔧 Proposed fix
function listEntitySlugs(category: string): string[] {
const dir = path.join(ROOT, 'input/data', category)
if (!fs.existsSync(dir)) return []
return fs
.readdirSync(dir)
- .filter((f) => f.endsWith('.md'))
+ .filter((f) => f.endsWith('.md') && f !== 'README.md')
.map((f) => f.replace('.md', ''))
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function listEntitySlugs(category: string): string[] { | |
| const dir = path.join(ROOT, 'input/data', category) | |
| if (!fs.existsSync(dir)) return [] | |
| return fs | |
| .readdirSync(dir) | |
| .filter((f) => f.endsWith('.md')) | |
| .map((f) => f.replace('.md', '')) | |
| } | |
| function listEntitySlugs(category: string): string[] { | |
| const dir = path.join(ROOT, 'input/data', category) | |
| if (!fs.existsSync(dir)) return [] | |
| return fs | |
| .readdirSync(dir) | |
| .filter((f) => f.endsWith('.md') && f !== 'README.md') | |
| .map((f) => f.replace('.md', '')) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@scripts/verify-content.ts` around lines 56 - 63, In listEntitySlugs add a
filter to exclude README.md so README files aren't treated as content slugs:
update the pipeline in the listEntitySlugs function to filter out entries equal
to 'README.md' (apply before the .map that strips '.md'), ensuring only actual
entity markdown files become slugs.
| function isPublished(content: string): boolean { | ||
| const fm = parseFrontmatter(content) | ||
| return fm.published !== false | ||
| } |
There was a problem hiding this comment.
Semantic mismatch with canonical isPublished implementation.
This returns true when published is undefined, but src/lib/content.ts (lines 206-209) uses published === true, meaning undefined content is treated as unpublished in the actual app.
This inconsistency could cause the validator to check content the app won't render (false positives) or skip content the app does render (false negatives).
🔧 Proposed fix to align semantics
function isPublished(content: string): boolean {
const fm = parseFrontmatter(content)
- return fm.published !== false
+ return fm.published === true
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function isPublished(content: string): boolean { | |
| const fm = parseFrontmatter(content) | |
| return fm.published !== false | |
| } | |
| function isPublished(content: string): boolean { | |
| const fm = parseFrontmatter(content) | |
| return fm.published === true | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@scripts/verify-content.ts` around lines 109 - 112, The isPublished function
in scripts/verify-content.ts currently treats undefined as published; update it
to match the canonical logic in src/lib/content.ts by parsing frontmatter with
parseFrontmatter and returning fm.published === true (so only an explicit true
is considered published). Locate the function isPublished and change its return
condition accordingly to ensure the validator's semantics align with the app's
rendering behavior.
Bumps peanut-content to 8788afa (includes PR #18 republish 72 locales, PR #19 publish+MDX upgrade, PR #20 broken link fix). New verify-content checks: - Pass 7: all files must have explicit published field (catches the ambiguity that caused withdraw pages to silently render without being in generateStaticParams) - Pass 8: published consistency — flags files where page would render but generateStaticParams would exclude them (the permissive vs strict mismatch we hit) - Pass 9: submodule freshness — warns when content submodule is behind origin/main
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
scripts/verify-content.ts (1)
23-24: Consider importingSUPPORTED_LOCALESfrom the canonical source.These locale lists are duplicated from
src/i18n/config.ts. If the app's supported locales change, this script will silently drift out of sync.If importing from app code is intentional to keep the script standalone, add a comment documenting this and referencing the canonical source.
♻️ Option: Import from config
-const SUPPORTED_LOCALES = ['en', 'es-419', 'es-ar', 'es-es', 'pt-br'] -const PRIMARY_LOCALES = ['en', 'es-419', 'pt-br'] +import { SUPPORTED_LOCALES } from '../src/i18n/config' + +const PRIMARY_LOCALES = ['en', 'es-419', 'pt-br']🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/verify-content.ts` around lines 23 - 24, The SUPPORTED_LOCALES and PRIMARY_LOCALES arrays in scripts/verify-content.ts are duplicated from the canonical source; replace the local constants with an import from the source of truth (import { SUPPORTED_LOCALES, PRIMARY_LOCALES } from 'src/i18n/config') so they stay in sync, or if keeping the script standalone is intentional, add a concise comment above the SUPPORTED_LOCALES and PRIMARY_LOCALES declarations that states they are intentionally duplicated and reference the canonical source path ('src/i18n/config.ts') so future maintainers know where to update them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@scripts/verify-content.ts`:
- Around line 426-428: The code emits both a warning and later an error for the
same missing frontmatter field; remove the early warn call so the missing
fm.published condition is handled only by the later error path. Specifically,
delete the warn('frontmatter', 'No explicit published field (defaults to true)',
rel(file)) invocation that checks fm.published, and ensure the later
isPublished/error check remains authoritative; if you prefer to keep any message
here instead, make it accurate to the isPublished logic (only say "defaults to
true" when the code treats missing as permissive, e.g., uses !== false).
- Around line 574-578: The shell command builds a string with ROOT which can
break for paths with spaces; update the git call to use
child_process.execFileSync (or spawnSync) with argument array form instead of
string concatenation: import or require execFileSync, call execFileSync('git',
['-C', ROOT, 'rev-list', '--count', 'HEAD..origin/main'], { encoding: 'utf-8' })
and assign the trimmed result back to behindCount (same variable), keeping the
try/catch and options intact so paths with spaces or special characters are
handled safely.
- Around line 515-527: The loop over singletonDirs (variable singletonDirs and
the for-loop that builds dirPath and iterates files) is dead code and should be
removed; delete the entire block that iterates singletonDirs (the for (const dir
of singletonDirs) { ... } section) since getAllMdFiles already handles those
files and the loop performs no actions besides checks and continues, leaving no
runtime effect.
---
Nitpick comments:
In `@scripts/verify-content.ts`:
- Around line 23-24: The SUPPORTED_LOCALES and PRIMARY_LOCALES arrays in
scripts/verify-content.ts are duplicated from the canonical source; replace the
local constants with an import from the source of truth (import {
SUPPORTED_LOCALES, PRIMARY_LOCALES } from 'src/i18n/config') so they stay in
sync, or if keeping the script standalone is intentional, add a concise comment
above the SUPPORTED_LOCALES and PRIMARY_LOCALES declarations that states they
are intentionally duplicated and reference the canonical source path
('src/i18n/config.ts') so future maintainers know where to update them.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 26cc7f8c-dccf-4510-985c-7e47f8b6b488
📒 Files selected for processing (2)
scripts/verify-content.tssrc/content
✅ Files skipped from review due to trivial changes (1)
- src/content
| if (fm.published === undefined) { | ||
| warn('frontmatter', 'No explicit published field (defaults to true)', rel(file)) | ||
| } |
There was a problem hiding this comment.
Conflicting diagnostics: Pass 4 warns but Pass 7 errors for the same condition.
Pass 4 emits a warning for missing published field (line 427), while Pass 7 (lines 505-512) emits an error for the exact same condition. This is contradictory—the same issue is both a warning and an error.
Additionally, the message "defaults to true" is only accurate if isPublished uses permissive logic (!== false), but the canonical implementation uses strict logic (=== true).
🔧 Proposed fix: Remove the warning from Pass 4 since Pass 7 handles it as an error
if (!fm.description || (typeof fm.description === 'string' && fm.description.trim() === '')) {
error('frontmatter', 'Published file missing description', rel(file))
issues++
}
- if (fm.published === undefined) {
- warn('frontmatter', 'No explicit published field (defaults to true)', rel(file))
- }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (fm.published === undefined) { | |
| warn('frontmatter', 'No explicit published field (defaults to true)', rel(file)) | |
| } | |
| if (!fm.description || (typeof fm.description === 'string' && fm.description.trim() === '')) { | |
| error('frontmatter', 'Published file missing description', rel(file)) | |
| issues++ | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@scripts/verify-content.ts` around lines 426 - 428, The code emits both a
warning and later an error for the same missing frontmatter field; remove the
early warn call so the missing fm.published condition is handled only by the
later error path. Specifically, delete the warn('frontmatter', 'No explicit
published field (defaults to true)', rel(file)) invocation that checks
fm.published, and ensure the later isPublished/error check remains
authoritative; if you prefer to keep any message here instead, make it accurate
to the isPublished logic (only say "defaults to true" when the code treats
missing as permissive, e.g., uses !== false).
| // Also check singleton content (files directly in content/{type}/ not in a subdir) | ||
| const singletonDirs = ['pricing', 'supported-networks'] | ||
| for (const dir of singletonDirs) { | ||
| const dirPath = path.join(CONTENT_DIR, dir) | ||
| if (!fs.existsSync(dirPath)) continue | ||
| for (const f of fs.readdirSync(dirPath)) { | ||
| if (!f.endsWith('.md')) continue | ||
| const filePath = path.join(dirPath, f) | ||
| const stat = fs.statSync(filePath) | ||
| if (stat.isDirectory()) continue | ||
| // Already checked above in getAllMdFiles, skip duplicate | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Dead code: singleton directory loop does nothing.
This loop iterates over singleton directories but performs no action—it reads files and checks if they're directories, then continues. The comment says duplicates are skipped because getAllMdFiles already handles them, but if that's the case, this entire block should be removed.
🧹 Proposed fix: Remove dead code
}
}
- // Also check singleton content (files directly in content/{type}/ not in a subdir)
- const singletonDirs = ['pricing', 'supported-networks']
- for (const dir of singletonDirs) {
- const dirPath = path.join(CONTENT_DIR, dir)
- if (!fs.existsSync(dirPath)) continue
- for (const f of fs.readdirSync(dirPath)) {
- if (!f.endsWith('.md')) continue
- const filePath = path.join(dirPath, f)
- const stat = fs.statSync(filePath)
- if (stat.isDirectory()) continue
- // Already checked above in getAllMdFiles, skip duplicate
- }
- }
-
console.log(📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Also check singleton content (files directly in content/{type}/ not in a subdir) | |
| const singletonDirs = ['pricing', 'supported-networks'] | |
| for (const dir of singletonDirs) { | |
| const dirPath = path.join(CONTENT_DIR, dir) | |
| if (!fs.existsSync(dirPath)) continue | |
| for (const f of fs.readdirSync(dirPath)) { | |
| if (!f.endsWith('.md')) continue | |
| const filePath = path.join(dirPath, f) | |
| const stat = fs.statSync(filePath) | |
| if (stat.isDirectory()) continue | |
| // Already checked above in getAllMdFiles, skip duplicate | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@scripts/verify-content.ts` around lines 515 - 527, The loop over
singletonDirs (variable singletonDirs and the for-loop that builds dirPath and
iterates files) is dead code and should be removed; delete the entire block that
iterates singletonDirs (the for (const dir of singletonDirs) { ... } section)
since getAllMdFiles already handles those files and the loop performs no actions
besides checks and continues, leaving no runtime effect.
| try { | ||
| const { execSync } = require('child_process') | ||
| const behindCount = execSync('git -C ' + ROOT + ' rev-list --count HEAD..origin/main 2>/dev/null', { | ||
| encoding: 'utf-8', | ||
| }).trim() |
There was a problem hiding this comment.
Shell command construction could fail with paths containing spaces or special characters.
The ROOT path is concatenated directly into the shell command string. If the working directory contains spaces or shell metacharacters, the command will fail or behave unexpectedly.
🔧 Proposed fix: Use execFileSync with array arguments
try {
- const { execSync } = require('child_process')
- const behindCount = execSync('git -C ' + ROOT + ' rev-list --count HEAD..origin/main 2>/dev/null', {
+ const { execFileSync } = require('child_process')
+ const behindCount = execFileSync('git', ['-C', ROOT, 'rev-list', '--count', 'HEAD..origin/main'], {
encoding: 'utf-8',
+ stdio: ['pipe', 'pipe', 'ignore'],
}).trim()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@scripts/verify-content.ts` around lines 574 - 578, The shell command builds a
string with ROOT which can break for paths with spaces; update the git call to
use child_process.execFileSync (or spawnSync) with argument array form instead
of string concatenation: import or require execFileSync, call
execFileSync('git', ['-C', ROOT, 'rev-list', '--count', 'HEAD..origin/main'], {
encoding: 'utf-8' }) and assign the trimmed result back to behindCount (same
variable), keeping the try/catch and options intact so paths with spaces or
special characters are handled safely.
Pass 6 now: - ERROR if published page has 0 MDX components (was warning) - WARNING if only 1 MDX component type (should have 2+) - skip_polish_check: true in frontmatter overrides this check Also adds mb-10 to Steps component for spacing after full-bleed section.
|
Closing — reopening as PR against main directly to avoid pulling unrelated dev commits. |
Summary
Six content types in peanut-content had published content + internal links but no matching Next.js route — they silently fell through to the
[...recipient]catch-all, serving the wrong page with a 200 OK. Thevalidate-linksCI script didn't catch this because it validated against content directory structure, not actual app routes.This PR fixes the root cause and adds guardrails to prevent it from recurring.
What changed
6 new routes (all follow existing
ContentPage+readPageContentLocalizedpattern):/{locale}/use-cases/{slug}— 5 published use cases (digital-nomads, remote-workers, families, tourists, cross-border-travel)/{locale}/withdraw/{slug}— 9 withdraw guides (ach, arbitrum, base, ethereum, polygon, solana, to-bank, tron, avalanche)/{locale}/stories/{slug}+ index — ready for when stories go live (all currentlypublished: false)/{locale}/pricing— singleton page, 3 locales/{locale}/supported-networks— singleton page, 3 localesContent infrastructure:
readSingletonContent/readSingletonContentLocalizedinsrc/lib/content.tsfor pages like pricing that don't use slug subdirectoriesContentSEOFooterserver component that readsfooter-manifest.jsonand renders locale-aware internal linkslocaleprop onContentPageROUTE_SLUGSfor hreflang alternate tagsverify-contentreplacesvalidate-linkswith 5 verification passes:page.tsx(the key fix — validates against actual routes, not just content dirs)footer-manifest.jsonresolvesFiles
Context
This fixes the issues discussed in Chip's content sync thread — broken
/en/pricing,/en/supported-networks,/en/use-cases/*,/en/withdraw/*all now render correctly instead of hitting the catch-all.The
validate-linksalias in package.json still works (points to verify-content.ts) so the peanut-content CI workflow doesn't need an immediate update.Test plan
pnpm verify-content— 0 errors (184 locale-coverage warnings expected)npm test— all 565 tests passnpm run typecheck— no new errors/en/pricing,/en/use-cases/digital-nomads,/en/withdraw/ach,/en/supported-networks/en/argentina,/en/help/verification)/en/storiesreturns 404 (no published stories yet — expected)/en/argentina,/en/help/verification,/en/compare/peanut-vs-wise@coderabbitai review