Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,6 @@ export NEXT_PUBLIC_ONESIGNAL_APP_ID=
export NEXT_PUBLIC_SAFARI_WEB_ID=
export NEXT_PUBLIC_ONESIGNAL_WEBHOOK=

# rewards-screen ragdoll easter egg (triple-click "Rewards" title).
# PeanutRagdoll mascot on the 404, maintenance and waitlist-jail screens.
# On by default; set to `false` to disable + tree-shake the chunk out.
export NEXT_PUBLIC_RAGDOLL_ENABLED=
2 changes: 1 addition & 1 deletion public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Tourists, digital nomads, expats, remote workers, freelancers, and families movi
## Company

- Website: https://peanut.me
- Twitter/X: https://twitter.com/PeanutProtocol
- Twitter/X: https://x.com/joinpeanut
- GitHub: https://github.com/peanutprotocol
- LinkedIn: https://www.linkedin.com/company/peanut-trade/
- Careers: https://peanut.me/careers
Expand Down
15 changes: 15 additions & 0 deletions redirects.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@
"destination": "/shhhhh",
"permanent": true
},
{
"source": "/exchange",
"destination": "/",
"permanent": true
},
{
"source": "/sh",
"destination": "/card",
Expand Down Expand Up @@ -211,6 +216,16 @@
"destination": "/:locale/deposit/via-spei",
"permanent": true
},
{
"source": "/:locale/blog",
"destination": "/:locale/content",
"permanent": true
},
{
"source": "/:locale/team",
"destination": "/:locale/press",
"permanent": true
},
{
"source": "/:locale(en|es-419|es-ar|pt-br)/:path+/",
"destination": "/:locale/:path+",
Expand Down
2 changes: 1 addition & 1 deletion scripts/generate-valid-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function main() {
// Static pages
lines.push('## Static Pages')
lines.push('')
for (const p of ['/', '/careers', '/exchange', '/privacy', '/terms', '/shhhhh']) {
for (const p of ['/', '/careers', '/privacy', '/terms', '/shhhhh']) {
lines.push(`- \`${p}\``)
}
lines.push('')
Expand Down
2 changes: 1 addition & 1 deletion scripts/ping-indexnow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async function getAllPaths(): Promise<string[]> {
const { SUPPORTED_LOCALES } = await import('../src/i18n/types')
const { listContentSlugs } = await import('../src/lib/content')

const paths: string[] = ['/', '/shhhhh', '/careers', '/exchange', '/privacy', '/terms']
const paths: string[] = ['/', '/shhhhh', '/careers', '/privacy', '/terms']

for (const locale of SUPPORTED_LOCALES) {
for (const country of Object.keys(COUNTRIES_SEO)) {
Expand Down
4 changes: 2 additions & 2 deletions scripts/verify-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ function discoverRoutes(): Set<string> {
const routes = new Set<string>()

// Static pages
for (const p of ['/', '/careers', '/exchange', '/privacy', '/terms', '/shhhhh']) {
for (const p of ['/', '/careers', '/privacy', '/terms', '/shhhhh']) {
routes.add(p)
}

Expand Down Expand Up @@ -732,7 +732,7 @@ function expectedSitemapUrls(): string[] {
urls.push(`/${locale}/supported-networks`)
urls.push(`/${locale}/pricing`)
urls.push(`/${locale}/content`)
urls.push(`/${locale}/blog`)
// No /{locale}/blog index — it is a 308 to /{locale}/content. Posts stay.
for (const slug of blogSlugs) urls.push(`/${locale}/blog/${slug}`)
}

Expand Down
4 changes: 2 additions & 2 deletions src/app/[...recipient]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ export async function generateMetadata({ params, searchParams }: PageProps) {
},
twitter: {
card: 'summary_large_image',
site: '@PeanutProtocol',
creator: '@PeanutProtocol',
site: '@joinpeanut',
creator: '@joinpeanut',
title,
description,
images: [
Expand Down
10 changes: 7 additions & 3 deletions src/app/[locale]/(marketing)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,13 @@ export default async function BlogPostPageLocalized({ params }: PageProps) {
}
: null

// The standalone /{locale}/blog index is gone (308 to the content hub), so the
// parent crumb is the hub filtered to blog — same shape as compare/ and use-cases/.
const hubHref = `/${locale}/content?type=blog`

const breadcrumbs = [
{ name: i18n.home, href: `/${locale}` },
{ name: i18n.blog, href: `/${locale}/blog` },
{ name: i18n.filterBlog, href: hubHref },
{ name: post.frontmatter.title, href: `/${locale}/blog/${slug}` },
]

Expand All @@ -116,8 +120,8 @@ export default async function BlogPostPageLocalized({ params }: PageProps) {
{faqSchema && <JsonLd data={faqSchema} />}
<MarketingShell className="max-w-2xl">
<ArticleBackNav
parentLabel={i18n.blog}
parentHref={`/${locale}/blog`}
parentLabel={i18n.filterBlog}
parentHref={hubHref}
backToTemplate={i18n.backTo}
currentLocale={locale as Locale}
localizedHrefs={localizedHrefs}
Expand Down
93 changes: 0 additions & 93 deletions src/app/[locale]/(marketing)/blog/page.tsx

This file was deleted.

60 changes: 26 additions & 34 deletions src/app/[locale]/(marketing)/content/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { generateMetadata as metadataHelper } from '@/app/metadata'
import { SUPPORTED_LOCALES, getAlternates, isValidLocale } from '@/i18n/config'
import { getTranslations } from '@/i18n'
import type { Locale } from '@/i18n/types'
import { listAllContent } from '@/lib/content'
import { listAllContent, type ContentItem } from '@/lib/content'
import { ContentPage } from '@/components/Marketing/ContentPage'
import { Hero } from '@/components/Marketing/mdx/Hero'
import ContentLanding from '@/components/Marketing/ContentLanding'
import ContentLanding, { ContentLinkList, type ContentLandingStrings } from '@/components/Marketing/ContentLanding'

interface PageProps {
params: Promise<{ locale: string }>
Expand Down Expand Up @@ -39,26 +39,20 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
}
}

function LandingSkeleton() {
/**
* Suspense fallback — and the crawlable version of this page. ContentLanding reads the URL
* through nuqs, so on a statically generated route Next prerenders this fallback in its place;
* whatever renders here is what search engines get. Serve the real link list, and skeleton only
* the search box that genuinely needs the client.
*/
function LandingFallback({ items, strings }: { items: ContentItem[]; strings: ContentLandingStrings }) {
return (
<div className="mx-auto mb-8 mt-10 max-w-[720px] px-6 md:mt-12 md:px-4">
<div className="h-12 w-full animate-pulse rounded-sm border border-n-1 bg-gray-200" />
<div className="mt-10 flex flex-col gap-10">
{[1, 2, 3].map((i) => (
<div key={i}>
<div className="mb-4 h-3 w-32 animate-pulse rounded bg-gray-200" />
<div className="flex flex-col gap-px overflow-hidden rounded-sm border border-n-1">
{[1, 2, 3].map((j) => (
<div key={j} className="flex flex-col gap-1.5 bg-white px-5 py-4">
<div className="h-4 w-3/4 animate-pulse rounded bg-gray-200" />
<div className="h-3 w-1/2 animate-pulse rounded bg-gray-200" />
</div>
))}
</div>
</div>
))}
<>
<div className="mx-auto mb-6 mt-10 max-w-[720px] px-6 md:mt-12 md:px-4">
<div className="h-12 w-full animate-pulse rounded-sm border border-n-1 bg-gray-200" />
</div>
</div>
<ContentLinkList items={items} strings={strings} grouped />
</>
)
}

Expand All @@ -70,6 +64,16 @@ export default async function ContentHubPage({ params }: PageProps) {
const i18n = getTranslations(typedLocale)
const items = listAllContent(typedLocale)

const strings: ContentLandingStrings = {
searchPlaceholder: i18n.contentSearchPlaceholder,
noResults: i18n.noContentResults,
filterAll: i18n.filterAll,
filterBlog: i18n.filterBlog,
filterStories: i18n.filterStories,
filterUseCases: i18n.filterUseCases,
filterCompare: i18n.filterCompare,
}

return (
<ContentPage
locale={locale}
Expand All @@ -79,20 +83,8 @@ export default async function ContentHubPage({ params }: PageProps) {
]}
>
<Hero title={i18n.contentHubTitle} subtitle={i18n.contentHubSubtitle} />
<Suspense fallback={<LandingSkeleton />}>
<ContentLanding
items={items}
locale={typedLocale}
strings={{
searchPlaceholder: i18n.contentSearchPlaceholder,
noResults: i18n.noContentResults,
filterAll: i18n.filterAll,
filterBlog: i18n.filterBlog,
filterStories: i18n.filterStories,
filterUseCases: i18n.filterUseCases,
filterCompare: i18n.filterCompare,
}}
/>
<Suspense fallback={<LandingFallback items={items} strings={strings} />}>
<ContentLanding items={items} locale={typedLocale} strings={strings} />
</Suspense>
</ContentPage>
)
Expand Down
24 changes: 17 additions & 7 deletions src/app/[locale]/(marketing)/press/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {

// Press kit data lives in mono at content/press/{lang}.md frontmatter — singleton
// content authored by marketing/leadership, shipped via the mirror. Team member
// data is reused as-is from content/team/{lang}.md (one fact, one place).
// data comes from content/team/{lang}.md (one fact, one place); this page is the
// only surface for it since /team was removed.
//
// brand_assets/team_photos hrefs are author-supplied frontmatter that can be
// pushed straight to mono main without code review — only emit http(s) URLs
Expand Down Expand Up @@ -114,6 +115,15 @@ export default async function PressPage({ params }: PageProps) {
name: 'Peanut',
url: 'https://peanut.me',
description: fm.boilerplate?.press,
member: members.map((member) => {
const linkedin = safeHttpUrl(member.social?.linkedin)
return {
'@type': 'Person',
name: member.name,
jobTitle: member.role,
...(linkedin ? { sameAs: [linkedin] } : {}),
}
}),
}

return (
Expand All @@ -126,7 +136,7 @@ export default async function PressPage({ params }: PageProps) {
<div className="flex flex-col gap-10">
{fm.boilerplate && (
<section className="flex flex-col gap-4">
<h2 className="text-xl font-bold">Company description</h2>
<h2 className="text-xl font-bold">{i18n.pressCompanyDescription}</h2>
<div className="grid gap-4 md:grid-cols-3">
{fm.boilerplate.short && (
<Card className="gap-2 p-6">
Expand All @@ -152,7 +162,7 @@ export default async function PressPage({ params }: PageProps) {

{fm.tagline && (
<section className="flex flex-col gap-4">
<h2 className="text-xl font-bold">Tagline &amp; headlines</h2>
<h2 className="text-xl font-bold">{i18n.pressTaglineHeadlines}</h2>
<Card className="gap-1 p-6">
<p className="text-lg font-bold text-n-1">{fm.tagline}</p>
{fm.secondary_line && <p className="text-sm text-grey-1">{fm.secondary_line}</p>}
Expand All @@ -173,7 +183,7 @@ export default async function PressPage({ params }: PageProps) {

{fm.brand_assets && fm.brand_assets.length > 0 && (
<section className="flex flex-col gap-4">
<h2 className="text-xl font-bold">Brand assets</h2>
<h2 className="text-xl font-bold">{i18n.pressBrandAssets}</h2>
<div className="grid gap-4 md:grid-cols-2">
{fm.brand_assets.map((group) => (
<Card key={group.label} className="gap-3 p-6">
Expand Down Expand Up @@ -205,7 +215,7 @@ export default async function PressPage({ params }: PageProps) {

{members.length > 0 && (
<section className="flex flex-col gap-4">
<h2 className="text-xl font-bold">Team</h2>
<h2 className="text-xl font-bold">{i18n.pressTeam}</h2>
<div className="grid gap-6 md:grid-cols-2">
{members.map((member) => (
<Card key={member.slug} className="gap-3 p-6">
Expand Down Expand Up @@ -242,7 +252,7 @@ export default async function PressPage({ params }: PageProps) {

{fm.company_facts && fm.company_facts.length > 0 && (
<section className="flex flex-col gap-2">
<h2 className="text-xl font-bold">Company</h2>
<h2 className="text-xl font-bold">{i18n.pressCompany}</h2>
{fm.company_facts.map((fact) => (
<p key={fact} className="text-sm text-grey-1">
{fact}
Expand All @@ -253,7 +263,7 @@ export default async function PressPage({ params }: PageProps) {

{fm.media_contact && (
<section className="flex flex-col gap-2">
<h2 className="text-xl font-bold">Media contact</h2>
<h2 className="text-xl font-bold">{i18n.pressMediaContact}</h2>
<a href={`mailto:${fm.media_contact}`} className="w-fit text-sm text-black underline">
{fm.media_contact}
</a>
Expand Down
3 changes: 2 additions & 1 deletion src/app/[locale]/(marketing)/stories/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ interface PageProps {
params: Promise<{ locale: string; slug: string }>
}

const STORY_SLUGS = listPublishedSlugs('stories')
// 'index' is the legacy stories/index/ directory — the hub at /stories owns that URL space.
const STORY_SLUGS = listPublishedSlugs('stories').filter((slug) => slug !== 'index')

export async function generateStaticParams() {
return SUPPORTED_LOCALES.flatMap((locale) => STORY_SLUGS.map((slug) => ({ locale, slug })))
Expand Down
Loading
Loading