diff --git a/scripts/validate-links.ts b/scripts/validate-links.ts index 430868946..7c38bb557 100644 --- a/scripts/validate-links.ts +++ b/scripts/validate-links.ts @@ -42,6 +42,24 @@ function buildValidPaths(): Set { paths.add(p) } + // App routes (behind auth / mobile-ui) — content may link to these + for (const p of [ + '/profile', + '/profile/backup', + '/profile/edit', + '/profile/exchange-rate', + '/profile/identity-verification', + '/home', + '/send', + '/request', + '/settings', + '/history', + '/points', + '/recover-funds', + ]) { + paths.add(p) + } + const countrySlugs = listDirs(path.join(CONTENT_DIR, 'countries')) const competitorSlugs = listDirs(path.join(CONTENT_DIR, 'compare')) const payWithSlugs = listDirs(path.join(CONTENT_DIR, 'pay-with')) @@ -187,6 +205,30 @@ function isInternalLink(url: string): boolean { return true } +// --- Frontmatter parsing --- + +function parseFrontmatter(content: string): Record { + const match = content.match(/^---\n([\s\S]*?)\n---/) + if (!match) return {} + const frontmatter: Record = {} + for (const line of match[1].split('\n')) { + const colonIdx = line.indexOf(':') + if (colonIdx === -1) continue + const key = line.slice(0, colonIdx).trim() + const value = line.slice(colonIdx + 1).trim() + if (value === 'true') frontmatter[key] = true + else if (value === 'false') frontmatter[key] = false + else frontmatter[key] = value + } + return frontmatter +} + +function isPublished(content: string): boolean { + const fm = parseFrontmatter(content) + // If published is explicitly false, skip the file + return fm.published !== false +} + // --- Scan content files --- function getAllMdFiles(dir: string): string[] { @@ -225,8 +267,17 @@ function main() { const broken: BrokenLink[] = [] let totalLinks = 0 + let skippedUnpublished = 0 + for (const file of files) { const content = fs.readFileSync(file, 'utf-8') + + // Skip unpublished/draft content — links to not-yet-built routes are expected + if (!isPublished(content)) { + skippedUnpublished++ + continue + } + const links = extractLinks(content) totalLinks += links.length @@ -246,7 +297,10 @@ function main() { } // --- Report --- - console.log(`Checked ${totalLinks} internal links across ${files.length} files\n`) + if (skippedUnpublished > 0) { + console.log(` Skipped ${skippedUnpublished} unpublished files\n`) + } + console.log(`Checked ${totalLinks} internal links across ${files.length - skippedUnpublished} published files\n`) if (broken.length === 0) { console.log('✓ No broken internal links found!') diff --git a/src/components/LandingPage/SEOFooter.tsx b/src/components/LandingPage/SEOFooter.tsx index c4c3869eb..3a84f8ebc 100644 --- a/src/components/LandingPage/SEOFooter.tsx +++ b/src/components/LandingPage/SEOFooter.tsx @@ -1,112 +1,84 @@ import Link from 'next/link' +import footerManifest from '@/content/generated/footer-manifest.json' -// Curated "seed list" for Google crawl discovery. Renders below the main footer -// on non-marketing pages (homepage, /exchange, /lp, etc.). Marketing pages don't -// need this — they already have RelatedPages + CountryGrid linking to sibling content. +// SEO footer driven by peanut-content's generated/footer-manifest.json. +// To update: add `featured: true` to content frontmatter, run +// `node scripts/generate-footer-manifest.js` in peanut-content, and deploy. // -// Data is inlined (not imported from @/data/seo) because Footer.tsx is bundled -// by webpack for client-routed pages (e.g. /exchange) — importing fs-dependent -// modules would break the build. -// -// IMPORTANT: Only list slugs that have published content in peanut-content. -// The validate-links CI in peanut-content catches broken internal links, but -// this file lives in peanut-ui — verify manually when editing. +// JSON import is webpack-safe for client bundles (no fs dependency). + +interface FooterLink { + slug: string + name: string + href: string + external?: boolean +} + +const linkClass = 'text-xs text-white underline hover:text-white/70' + +function FooterColumn({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

{title}

+
    {children}
+
+ ) +} -const TOP_COUNTRIES: Array<{ slug: string; name: string }> = [ - { slug: 'argentina', name: 'Argentina' }, - { slug: 'brazil', name: 'Brazil' }, - { slug: 'mexico', name: 'Mexico' }, - { slug: 'colombia', name: 'Colombia' }, - { slug: 'philippines', name: 'Philippines' }, - { slug: 'nigeria', name: 'Nigeria' }, - { slug: 'india', name: 'India' }, - { slug: 'chile', name: 'Chile' }, -] +function FooterLink({ link, prefix }: { link: FooterLink; prefix?: string }) { + const label = prefix ? `${prefix} ${link.name}` : link.name -const COMPETITORS: Array<{ slug: string; name: string }> = [ - { slug: 'wise', name: 'Wise' }, - { slug: 'western-union', name: 'Western Union' }, - { slug: 'paypal', name: 'PayPal' }, - { slug: 'revolut', name: 'Revolut' }, - { slug: 'binance-p2p', name: 'Binance P2P' }, -] + if (link.external) { + return ( +
  • + + {label} + +
  • + ) + } -const EXCHANGES: Array<{ slug: string; name: string }> = [ - { slug: 'binance', name: 'Binance' }, - { slug: 'coinbase', name: 'Coinbase' }, - { slug: 'bybit', name: 'Bybit' }, - { slug: 'kraken', name: 'Kraken' }, -] + return ( +
  • + + {label} + +
  • + ) +} export function SEOFooter() { + const { sendMoney, compare, articles, resources } = footerManifest + return ( ) diff --git a/src/content b/src/content index ffc4bdd8a..35cd834a4 160000 --- a/src/content +++ b/src/content @@ -1 +1 @@ -Subproject commit ffc4bdd8ac3925b9f80d77bb5bb6e5f85a3d45b4 +Subproject commit 35cd834a4494e11136099b739fadff1aea4c06ce