Landing page for Catopia de Chen Antúnez — a software and AI solutions firm based in Paraguay. Built with Next.js 16 and deployed to Cloudflare Workers via the OpenNext adapter.
- Next.js 16 (App Router, Turbopack) —
force-staticpages - Cloudflare Workers via OpenNext
- Tailwind CSS v4
- next-intl — i18n with
en(en-US),es(es-PY), andpt(pt-BR) locales - Zod — runtime schema validation for all API routes
- Resend — transactional email for the contact form
- Bun as the package manager and runtime
bun install # install dependencies
bun dev # Next.js dev server at localhost:3000 (Node.js runtime)
bun preview # build + preview on the actual Cloudflare Workers runtimebun run lint # prettier --check + eslint
bun run format # prettier --write (auto-fix formatting)bun run deploy # build with OpenNext and deploy to Cloudflaresrc/
app/
not-found.tsx # Root 404 page (self-contained with <html>/<body>)
page.tsx # Root redirect: / → /en
sitemap.ts # Auto-generated /sitemap.xml (routes via APP_ROUTES env)
[locale]/ # App Router pages (home, services, about, contact)
layout.tsx # Locale layout — guards invalid locales with notFound()
components/ # Nav, Footer, ThemeToggle, LocaleSwitch, FontSizeControl,
# ThemeScript, FontSizeScript, ThemeRestorer
i18n/ # next-intl routing, request config, navigation helpers
messages/
en.json # English (en-US) translations
es.json # Spanish (es-PY) translations
pt.json # Brazilian Portuguese (pt-BR) translations
public/
robots.txt # Static robots file (must be static — see i18n note below)
next.config.ts # Scans src/app/[locale] at build time; injects APP_ROUTES env
wrangler.jsonc # Cloudflare Worker config
open-next.config.ts # OpenNext/Cloudflare adapter config
- Supported locales:
en(en-US),es(es-PY),pt(pt-BR) - URL-based locale prefix:
/en/...,/es/...,/pt/... - No middleware — Next.js 16's
proxy.tsis forced to the Node.js runtime, which OpenNext Cloudflare does not support. Locale detection is handled entirely viasetRequestLocale(locale)in each page. - Root
/redirects to/enviasrc/app/page.tsx - Client components use
useTranslations(), server components usegetTranslations() - Always use
usePathnameanduseRouterfrom@/i18n/navigation(notnext/navigation) in client components robots.txtmust be a static file inpublic/. Without it,/robots.txtmatches the[locale]dynamic segment (treating"robots.txt"as a locale) and serves the app instead.
- Dark/light toggle via
ThemeScriptin<head>(readslocalStorage, falls back toprefers-color-scheme) andThemeTogglecomponent usinguseSyncExternalStore - Font size control (S / M / L → 16px / 18px / 20px root) via
FontSizeScript+FontSizeControl; scales allrem-based Tailwind utilities automatically - Both preferences persist in
localStorageand are restored before hydration (no flash) and on every route navigation viaThemeRestorer
The /contact page posts to src/app/api/contact/route.ts. The handler validates the body with Zod, then sends an email via Resend with the client's address as Reply-To.
Secrets are read from getCloudflareContext().env — never from the client bundle.
Local development — add to .dev.vars (loaded by both bun dev and bun preview):
RESEND_API_KEY=re_...
RESEND_FROM=Catopia <noreply@catopia.chenantunez.com>
RESEND_TO=catopia@chenantunez.com
RESEND_SUBJECT_PREFIX=[CLIENT INQUIRY]
Scripts:
bun run test-email # send a test email using .dev.vars values
bun run set-secrets # push all RESEND_* entries from .dev.vars to the Cloudflare Workerset-secrets reads .dev.vars and pipes each RESEND_* value to wrangler secret put, so secrets never appear in the process list.
/robots.txt— served frompublic/robots.txt; allows all crawlers, disallows/_next/, references the sitemap/sitemap.xml— generated bysrc/app/sitemap.ts; covers all locale × route combinations; routes are auto-discovered at build time vianext.config.ts(no manual list needed)