Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d0945db
fix: guard createAppKit with typeof window to restore SSR
Hugo0 Mar 27, 2026
2cca942
fix: move createAppKit into component render to survive production SSR
Hugo0 Mar 27, 2026
99822ec
fix: wrap NoFees in Suspense to prevent landing page SSR bailout
Hugo0 Mar 27, 2026
de26e6d
refactor: consolidate ensureAppKit into initializeAppKit
Hugo0 Mar 27, 2026
879ed3a
Merge pull request #1818 from peanutprotocol/fix/ssr-landing-page
Hugo0 Mar 27, 2026
7cb38fb
fix: remove sanctioned countries from country list
chip-peanut-bot[bot] Mar 27, 2026
479f4bd
Merge pull request #1829 from peanutprotocol/chip/remove-sanctioned-c…
Hugo0 Mar 27, 2026
015b429
fix(verify): make published field optional (default to true)
chip-peanut-bot[bot] Mar 27, 2026
6eb97dd
Merge pull request #1838 from peanutprotocol/chip/fix-published-optio…
Hugo0 Mar 27, 2026
943ecf5
Update content submodule to latest main (27 commits)
chip-peanut-bot[bot] Mar 27, 2026
e447def
Merge pull request #1839 from peanutprotocol/auto/update-content-2026…
Hugo0 Mar 27, 2026
46134ab
fix: blog routes 404 — update content paths and enable static generation
Hugo0 Mar 27, 2026
8ab3cf2
Merge remote-tracking branch 'origin/main' into fix/blog-routes-404
Hugo0 Mar 27, 2026
2df9e27
fix: use resolved content locale in blog post schema
Hugo0 Mar 27, 2026
9a4f53e
fix: coerce frontmatter date to string and align category locale
Hugo0 Mar 27, 2026
9bb527b
Merge pull request #1840 from peanutprotocol/fix/blog-routes-404
Hugo0 Mar 27, 2026
8d4aa96
hotfix: switch blog rendering from marked to MDX pipeline
Hugo0 Mar 27, 2026
c26d61f
Merge pull request #1843 from peanutprotocol/hotfix/blog-mdx-rendering
Hugo0 Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 14 additions & 57 deletions scripts/verify-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,9 +437,7 @@ function checkFrontmatter() {
error('frontmatter', 'Published file missing description', rel(file))
issues++
}
if (fm.published === undefined) {
warn('frontmatter', 'No explicit published field (defaults to true)', rel(file))
}
// published field is now optional — missing defaults to published
}

console.log(` Pass 4 — Frontmatter: ${issues === 0 ? 'all published files valid' : `${issues} issues`}`)
Expand Down Expand Up @@ -527,76 +525,35 @@ function checkContentPolish() {
)
}

// --- Pass 7: Published field must be explicit ---
// --- Pass 7: Check for published: false files (drafts) ---
// With the flipped default (missing = published), only published: false is meaningful.
// This pass just reports how many drafts exist for visibility.

function checkExplicitPublished() {
const files = getAllMdFiles(CONTENT_DIR)
let issues = 0
let drafts = 0

for (const file of files) {
const content = fs.readFileSync(file, 'utf-8')
const fm = parseFrontmatter(content)

if (fm.published === undefined) {
error(
'no-published-field',
'File has no explicit published field — add published: true or published: false',
rel(file)
)
issues++
}
}

// 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
if (fm.published === false) {
warn('draft-content', 'File is explicitly unpublished (draft)', rel(file))
drafts++
}
}

console.log(
` Pass 7 — Explicit published: ${issues === 0 ? 'all files have published field' : `${issues} files missing published field`}`
` Pass 7 — Drafts: ${drafts === 0 ? 'no draft files' : `${drafts} files marked as drafts (published: false)`}`
)
}

// --- Pass 8: isPublished consistency ---
// The page-level check uses `published === false` (permissive: undefined = published)
// The lib isPublished uses `published === true` (strict: undefined = unpublished)
// Flag files where these disagree — they'll render on the page but won't appear in generateStaticParams
// Both page-level and lib now agree: missing/true = published, false = unpublished.
// This pass is kept as a no-op placeholder for numbering stability.

function checkPublishedConsistency() {
const files = getAllMdFiles(CONTENT_DIR)
let issues = 0

for (const file of files) {
const content = fs.readFileSync(file, 'utf-8')
const fm = parseFrontmatter(content)

// Permissive: page renders (published !== false)
const pageWouldRender = fm.published !== false
// Strict: generateStaticParams includes it (published === true)
const buildWouldInclude = fm.published === true

if (pageWouldRender && !buildWouldInclude) {
error(
'published-mismatch',
`published=${String(fm.published)} — page would render but generateStaticParams excludes it. Set published: true or published: false explicitly.`,
rel(file)
)
issues++
}
}

console.log(
` Pass 8 — Published consistency: ${issues === 0 ? 'no mismatches' : `${issues} files with ambiguous published state`}`
)
console.log(' Pass 8 — Published consistency: unified (both default to published)')
}

// --- Pass 9: Submodule freshness ---
Expand All @@ -607,8 +564,8 @@ function checkSubmoduleFreshness() {
if (!fs.existsSync(contentGitDir)) return

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',
}).trim()
const behind = parseInt(behindCount, 10)
Expand Down
24 changes: 10 additions & 14 deletions src/app/[locale]/(marketing)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,15 @@ interface PageProps {
}

export async function generateStaticParams() {
if (process.env.NODE_ENV === 'production') return []
// Generate params for locales that have blog content (fall back to en slugs)
return SUPPORTED_LOCALES.flatMap((locale) => {
let posts = getAllPosts(locale as Locale)
if (posts.length === 0) posts = getAllPosts('en')
return posts.map((post) => ({ locale, slug: post.slug }))
})
}
// TODO: when blog content is added to src/content/blog/, either remove the
// production guard in generateStaticParams above, or set dynamicParams = true.
// Currently no blog posts exist so this has no effect, but with content present
// the combination of returning [] in prod + dynamicParams = false would 404 all
// blog pages.
export const dynamicParams = false

// Allow dynamic rendering for slugs not in static params (e.g. newly added content)
export const dynamicParams = true

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { locale, slug } = await params
Expand All @@ -54,8 +49,10 @@ export default async function BlogPostPageLocalized({ params }: PageProps) {
const { locale, slug } = await params
if (!isValidLocale(locale)) notFound()

const post = (await getPostBySlug(slug, locale as Locale)) ?? (await getPostBySlug(slug, 'en'))
const localizedPost = await getPostBySlug(slug, locale as Locale)
const post = localizedPost ?? (await getPostBySlug(slug, 'en'))
if (!post) notFound()
const contentLocale: Locale = localizedPost ? (locale as Locale) : 'en'

const i18n = getTranslations(locale)

Expand All @@ -65,7 +62,7 @@ export default async function BlogPostPageLocalized({ params }: PageProps) {
headline: post.frontmatter.title,
description: post.frontmatter.description,
datePublished: post.frontmatter.date,
inLanguage: locale,
inLanguage: contentLocale,
author: { '@type': 'Organization', name: post.frontmatter.author ?? 'Peanut' },
publisher: { '@type': 'Organization', name: 'Peanut', url: 'https://peanut.me' },
mainEntityOfPage: `https://peanut.me/${locale}/blog/${slug}`,
Expand Down Expand Up @@ -137,10 +134,9 @@ export default async function BlogPostPageLocalized({ params }: PageProps) {
<p className="mt-2 text-gray-600">{post.frontmatter.description}</p>
<time className="mt-3 block text-sm text-gray-400">{post.frontmatter.date}</time>
</header>
<article
className="prose prose-lg prose-headings:font-bold prose-a:text-black prose-a:underline prose-pre:border prose-pre:border-n-1 prose-pre:bg-white max-w-none"
dangerouslySetInnerHTML={{ __html: post.html }}
/>
<article className="prose prose-lg prose-headings:font-bold prose-a:text-black prose-a:underline prose-pre:border prose-pre:border-n-1 prose-pre:bg-white max-w-none">
{post.content}
</article>
</MarketingShell>
</>
)
Expand Down
8 changes: 4 additions & 4 deletions src/app/[locale]/(marketing)/blog/category/[cat]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@ interface PageProps {
}

export async function generateStaticParams() {
if (process.env.NODE_ENV === 'production') return []
return SUPPORTED_LOCALES.flatMap((locale) => {
// Use English categories as fallback
const cats = getAllCategories(locale as Locale)
const fallbackCats = cats.length > 0 ? cats : getAllCategories('en')
return fallbackCats.map((cat) => ({ locale, cat }))
})
}
export const dynamicParams = false

export const dynamicParams = true

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { locale, cat } = await params
Expand Down Expand Up @@ -52,11 +51,12 @@ export default async function BlogCategoryPageLocalized({ params }: PageProps) {
const i18n = getTranslations(typedLocale)

let posts = getPostsByCategory(cat, typedLocale)
const resolvedLocale: Locale = posts.length > 0 ? typedLocale : 'en'
if (posts.length === 0) posts = getPostsByCategory(cat, 'en')
if (posts.length === 0) notFound()

const label = cat.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
const categories = getAllCategories(typedLocale).length > 0 ? getAllCategories(typedLocale) : getAllCategories('en')
const categories = getAllCategories(resolvedLocale)

return (
<>
Expand Down
1 change: 0 additions & 1 deletion src/app/[locale]/(marketing)/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ interface PageProps {
}

export async function generateStaticParams() {
if (process.env.NODE_ENV === 'production') return []
return SUPPORTED_LOCALES.map((locale) => ({ locale }))
}

Expand Down
69 changes: 7 additions & 62 deletions src/components/AddMoney/consts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -724,16 +724,7 @@ export const countryData: CountryData[] = [
iso3: 'CRI',
region: 'latam',
},
{
id: 'CU',
type: 'country',
title: 'Cuba',
currency: 'CUP',
path: 'cuba',
iso2: 'CU',
iso3: 'CUB',
region: 'north-america',
},

{
id: 'CV',
type: 'country',
Expand Down Expand Up @@ -1294,16 +1285,7 @@ export const countryData: CountryData[] = [
iso3: 'IRQ',
region: 'rest-of-the-world',
},
{
id: 'IR',
type: 'country',
title: 'Iran',
currency: 'IRR',
path: 'iran',
iso2: 'IR',
iso3: 'IRN',
region: 'rest-of-the-world',
},

{
id: 'ISL',
type: 'country',
Expand Down Expand Up @@ -1424,16 +1406,7 @@ export const countryData: CountryData[] = [
iso3: 'KNA',
region: 'latam',
},
{
id: 'KP',
type: 'country',
title: 'North Korea',
currency: 'KPW',
path: 'north-korea',
iso2: 'KP',
iso3: 'PRK',
region: 'rest-of-the-world',
},

{
id: 'KR',
type: 'country',
Expand Down Expand Up @@ -1657,7 +1630,7 @@ export const countryData: CountryData[] = [
{
id: 'MK',
type: 'country',
title: 'Macedonia',
title: 'North Macedonia',
currency: 'MKD',
path: 'macedonia',
iso2: 'MK',
Expand All @@ -1674,16 +1647,7 @@ export const countryData: CountryData[] = [
iso3: 'MLI',
region: 'rest-of-the-world',
},
{
id: 'MM',
type: 'country',
title: 'Myanmar',
currency: 'MMK',
path: 'myanmar',
iso2: 'MM',
iso3: 'MMR',
region: 'rest-of-the-world',
},

{
id: 'MN',
type: 'country',
Expand Down Expand Up @@ -2124,16 +2088,7 @@ export const countryData: CountryData[] = [
iso3: 'SRB',
region: 'europe',
},
{
id: 'RU',
type: 'country',
title: 'Russia',
currency: 'RUB',
path: 'russia',
iso2: 'RU',
iso3: 'RUS',
region: 'europe',
},

{
id: 'RW',
type: 'country',
Expand Down Expand Up @@ -2334,16 +2289,7 @@ export const countryData: CountryData[] = [
iso3: 'SXM',
region: 'north-america',
},
{
id: 'SY',
type: 'country',
title: 'Syria',
currency: 'SYP',
path: 'syria',
iso2: 'SY',
iso3: 'SYR',
region: 'rest-of-the-world',
},

{
id: 'SZ',
type: 'country',
Expand Down Expand Up @@ -2736,7 +2682,6 @@ const LATAM_COUNTRY_CODES = [
'CL',
'CO',
'CR',
'CU',
'DO',
'EC',
'SV',
Expand Down
9 changes: 7 additions & 2 deletions src/components/LandingPage/LandingPageClient.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { useFooterVisibility } from '@/context/footerVisibility'
import { useEffect, useState, useRef, useCallback, type ReactNode } from 'react'
import { Suspense, useEffect, useState, useRef, useCallback, type ReactNode } from 'react'
import { DropLink, FAQs, Hero, Marquee, NoFees, CardPioneers } from '@/components/LandingPage'
import TweetCarousel from '@/components/LandingPage/TweetCarousel'
import { StickyMobileCTA } from '@/components/LandingPage/StickyMobileCTA'
Expand Down Expand Up @@ -208,7 +208,12 @@ export function LandingPageClient({
<Marquee {...marqueeProps} />
<div ref={sendInSecondsRef}>{sendInSecondsSlot}</div>
<Marquee {...marqueeProps} />
<NoFees />
{/* Suspense needed: NoFees renders ExchangeRateWidget which uses useSearchParams().
Without this boundary, the entire LandingPageClient suspends during SSR,
sending an empty HTML shell to crawlers and killing SEO. */}
<Suspense>
<NoFees />
</Suspense>
<Marquee {...marqueeProps} />
<FAQs heading={faqData.heading} questions={faqData.questions} marquee={faqData.marquee} />
<Marquee {...marqueeProps} />
Expand Down
Loading
Loading