A bilingual Next.js 16 landing-page starter for service businesses, consultancies, and agencies — production-grade headers, accessible by default, and configured from a single file.
Ships with a neutral default and vertical-specific presets in examples/ so you can be productive on day one.
Coming soon.
pnpm install
cp .env.example .env.local
pnpm devOpen http://localhost:3000. The root redirects to /pt by default; edit i18n/routing.ts to change.
Then:
- Open
site.config.tsand updatebrand,contact,social, andnav. - Open
messages/pt.jsonandmessages/en.jsonand replace the placeholder copy. - Drop replacement images into
public/placeholders/keeping the same filenames, or repoint paths insite.config.ts.
A single typed config controls everything the layout reads at build time:
brand.name,brand.domain,brand.logo.{dark,light,mark}contact.emailandcontact.resend.fromEmailnavitems and their anchor targetssections.portfolio.enabledand the image listsections.trust.enabledsocial.{instagram,linkedin,github}— empty strings hide the iconseo.ogImage,hero.image,about.portrait
Every visible string on the page is keyed in both locales. To change the headline, edit hero.headline in both files. To add another locale, see the i18n section below.
The starter ships with 12 SVG placeholders so every image slot is visible on first render. Replace them with your own files (PNG, JPG, WEBP, or SVG) using the same filenames, or update the paths in site.config.ts.
| Slot | Placeholder |
|---|---|
| Header / Footer logo (light bg) | logo-wordmark-dark.svg |
| Hero logo / Footer logo (dark bg) | logo-wordmark-light.svg |
| Favicon / monogram | logo-mark.svg, favicon.svg |
| Hero photo | hero.svg |
| Open Graph card | og-image.svg |
| Portfolio grid (×6) | portfolio-0[1..6].svg |
When you switch to raster files, you can also swap the plain <img> tags inside components/Hero.tsx, components/About.tsx, components/Header.tsx, components/Footer.tsx, and components/PortfolioLightbox.tsx for next/image to get automatic optimization.
The examples/ folder contains preset configurations that adapt the kit to specific verticals. Each preset is a drop-in replacement for the root site.config.ts and messages/{en,pt}.json — no code changes required.
| Preset | Use case |
|---|---|
examples/engineering/ |
Engineering consultancies, technical services firms, B2B automation shops |
Apply a preset from the repo root:
cp examples/engineering/site.config.ts site.config.ts
cp examples/engineering/messages/en.json messages/en.json
cp examples/engineering/messages/pt.json messages/pt.json
pnpm devSee examples/README.md for details and how to contribute a preset.
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript 5 (strict) |
| Styles | Tailwind CSS 3 |
| i18n | next-intl 4 (PT default, EN secondary) |
| Resend, with mailto fallback when keys are absent | |
| Motion | Framer Motion (respects prefers-reduced-motion) |
Production-grade security headers are defined in vercel.json and applied on every response:
Content-Security-Policy— locked to self for scripts, styles, fonts, images; allowsapi.resend.comfor contact form submissionsStrict-Transport-Security— two-year max-age with preloadX-Frame-Options: SAMEORIGIN,X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-originPermissions-Policy— camera, microphone, geolocation disabled
Adjust the CSP allowlist when you add fonts, analytics, or CDN-hosted images.
- Skip-to-content link as the first focusable element
- Visible
:focus-visiblering on every interactive element aria-liveregions for form success and error states- Lightbox traps focus, closes on Escape, navigates with arrow keys
- Framer Motion calls respect
prefers-reduced-motion - All images carry meaningful
alttext sourced from the locale files
i18n/routing.ts declares the supported locales and the default. To add a third locale, e.g. Spanish:
// i18n/routing.ts
export const routing = defineRouting({
locales: ['pt', 'en', 'es'],
defaultLocale: 'pt',
localePrefix: 'always',
});Then add messages/es.json (start from a copy of en.json and translate every value), and update siteConfig.i18n.locales in site.config.ts.
The form posts to app/actions/contact.ts, a Next.js Server Action that:
- Drops submissions where the honeypot
websitefield is non-empty (silent success) - Validates with Zod using locale-aware error messages
- Forwards via Resend if
RESEND_API_KEYandCONTACT_FORM_TO_EMAILare set - Returns a structured error the form surfaces inline
If Resend env vars are not set, swap the section for a plain mailto: link by deleting <ContactForm /> from app/[locale]/page.tsx and rendering an <a href={mailto:${siteConfig.contact.email}}> instead.
| Command | Purpose |
|---|---|
pnpm dev |
Local dev server (port 3000) |
pnpm build |
Production build |
pnpm start |
Serve production build |
pnpm lint |
ESLint via the Next config |
pnpm typecheck |
TypeScript (no emit) |
| Key | Purpose |
|---|---|
RESEND_API_KEY |
Resend API key for contact form delivery |
CONTACT_FORM_TO_EMAIL |
Recipient address for inbound inquiries |
CONTACT_FORM_FROM_EMAIL |
Sender address; must match a verified Resend domain |
NEXT_PUBLIC_SITE_URL |
Canonical origin (used for metadata + sitemap) |
MIT — see LICENSE.
Architecture patterns extracted from real production sites and rebuilt as a reusable starter.