The boilerplate includes comprehensive SEO tools including dynamic sitemap generation, robots.txt configuration, structured data (JSON-LD), and metadata utilities for optimal search engine visibility.
Dynamic Sitemap
Auto-generated sitemap with static and CMS pages.
Structured Data
JSON-LD schemas for Organization, Product, FAQ, and more.
robots.txt
Dynamic robots.txt with protected routes excluded. Fully blocked when NEXT_PUBLIC_INDEXABLE is not true.
OpenGraph & Twitter
Social media metadata for rich link previews.
Viewport & Theme
Mobile-optimized viewport with light/dark theme colors.
PWA Manifest
Web app manifest for installability and app icons.
Resource Preloading
DNS prefetch and preconnect for faster page loads.
Locale Proxy
Auto locale detection with SEO-friendly redirects (Next.js 16+).
Sitemap Generation
The sitemap is automatically generated at /sitemap.xml and includes both static pages
and dynamic CMS pages. It's built using Next.js Metadata API.
| Page Type | Priority | Change Frequency |
|---|---|---|
Homepage (/) |
1.0 | Daily |
Pricing (/pricing) |
0.8 | Weekly |
Blog index (/blog) |
0.8 | Daily |
Contact (/contact) |
0.7 | Monthly |
| CMS pages (dynamic, excluding legal/blog) | 0.7 | Weekly |
Blog posts (dynamic, blog/*) |
0.7 | Weekly |
Changelog (/changelog) |
0.6 | Weekly |
Affiliates (/affiliates, only when AFFILIATES_ENABLED=true) |
0.6 | Monthly |
Legal pages (/terms, /privacy, /privacy-choices, /legal) |
0.5 | Monthly |
Pages for non-default locales are emitted with priority multiplied by 0.9 (e.g. an English-default site emits French URLs at 0.9, 0.72, 0.63, 0.54…). Configured in app/sitemap.ts.
The sitemap reads the database, which Next.js does not treat as a dynamic API — so it is revalidated on an hourly ISR window (export const revalidate = 3600). Without that it would be rendered once at build time and frozen, and content published after the last deploy would never appear.
Multi-locale sitemap & hreflang
Dynamic CMS and blog entries use shared Prisma with five explicit fields
and one read-only snapshot. The existing publication, noindex and canonical
rules apply before the output quota of 1,000 CMS pages and 1,000 articles.
Reads advance by unique slug in batches of 200, with a scan budget of
10,000 rows per kind and at most one final probe. These limits live in
config/cms.ts. Unreachable paths and reserved routes, including
the noindex blog/browse route and auth/account/dashboard paths, are excluded.
A database failure, malformed result or exhausted scan budget throws a centrally logged error. It does not produce a partial or static-only sitemap that could be cached as complete. The hourly ISR and existing static/documentation entries remain unchanged. Indexing disabled means an empty sitemap before any database or documentation read.
Every sitemap entry carries the full hreflang cluster for its page — one <url> per
locale, each linking to all the others plus itself and an x-default, following Google’s
localized-versions guidance. The same cluster is emitted in each page’s <head> via
alternates.languages.
For CMS pages and blog posts, a locale is included only when all three hold:
- Published in that locale — per-locale, resolved without cross-language fallback. A page published only in French is absent from the English sitemap and is not listed as an English alternate.
- Not flagged
noindexfor that locale. - Not canonicalized elsewhere — if you set a custom canonical pointing at a different URL, that locale is dropped rather than advertised in a sitemap that contradicts its own canonical tag.
Slugs are shared across locales (/fr-FR/about and /en-US/about are the same row), so an
entry differs only by its locale prefix. Slugs owned by a dedicated route — terms,
privacy, privacy-choices, legal, pricing, contact,
blog, changelog, affiliates — are excluded from the CMS portion of the
sitemap: a static route always wins the match, so a CMS page using one of those slugs is unreachable.
Publication is per-locale with no cross-language fallback, so existing CMS rows do not automatically become published in a newly added locale — they stay out of that locale’s sitemap until you publish them. This is intentional: it stops a page going live in a language it was never translated into. See Internationalization → Adding a New Locale.
When NEXT_PUBLIC_INDEXABLE is not true, the sitemap is served empty.
robots.txt already blocks every crawler in that case, but an empty sitemap also avoids handing a
full URL inventory to anything that ignores robots.txt.
robots.txt Configuration
The /robots.txt file is dynamically generated based on the NEXT_PUBLIC_INDEXABLE environment variable (configured as appConfig.isIndexable). When indexing is disabled (the default), all crawlers are blocked with Disallow: /. When enabled (NEXT_PUBLIC_INDEXABLE=true), the following private routes are excluded from crawling:
/api/*- All API routes/private-dashboard/*- User dashboard/admin-dashboard/*- Admin panel/my-account/*- Account settings/login,/register,/callback- Auth pages (/registerhas no implementation today — auth is magic-link only; this entry is defensive)/workspace/*- Workspace pages
Leave NEXT_PUBLIC_INDEXABLE unset or set to false on staging and development environments to prevent accidental indexing. The defaultMetadata robots directive and robots.txt both respect this flag.
Structured Data (JSON-LD)
The boilerplate provides helper functions to generate structured data schemas that help search engines
understand your content. Available in lib/seo/index.ts:
| Function | Schema Type | Use Case |
|---|---|---|
generateOrganizationSchema() |
Organization | Company info, logo, social links |
generateWebsiteSchema() |
WebSite | Site name, search action |
generateProductSchema() |
Product | Pricing plans with offers |
generateFAQSchema() |
FAQPage | FAQ sections for rich snippets |
generateSoftwareApplicationSchema() |
SoftwareApplication | SaaS product with ratings |
generateBreadcrumbSchema() |
BreadcrumbList | Navigation breadcrumbs |
generateWebPageSchema() |
WebPage | General page metadata |
generateContactPageSchema() |
ContactPage | Contact page with organization info |
Default Metadata
The defaultMetadata object provides base SEO configuration that can be extended per-page:
- Title Template - Uses
%s | Your Apppattern for consistent titles - Description - Default description from
config/app.ts - OpenGraph - Site name, type, locale with alternate locale
- Twitter Card - Summary with large image
- Robots - Index and follow controlled by
NEXT_PUBLIC_INDEXABLEenv variable, with GoogleBot optimizations
SEO Utility Functions
| Function | Description |
|---|---|
getCanonicalUrl(path) |
Generates full canonical URL for a given path |
getAlternateLanguages(path, availableLocales?) |
Returns hreflang alternate URLs plus x-default. Omit the second argument for routes that exist in every locale; pass the available locales for content that does not (CMS pages, blog posts). Returns an empty map when no locale qualifies, so callers can omit alternates.languages entirely. |
getPublishedLocales(page) (lib/cms/seo.ts) |
Locales a CMS row is published in. Use for UI — language switcher, cross-locale links. |
getIndexableLocales(page, options) (lib/cms/seo.ts) |
Published and not noindex and not canonicalized elsewhere. Use for crawler surfaces — hreflang and the sitemap. |
jsonLdScriptProps(schema) |
Returns props for embedding JSON-LD in a script tag |
Viewport & Mobile Optimization
The root layout includes a comprehensive viewport configuration for optimal mobile experience:
- Device Width - Responsive width matching device screen
- Initial Scale - Set to 1 for proper initial zoom
- Maximum Scale - Set to 5 for accessibility (allows zooming)
- Theme Color - Automatic light/dark mode browser UI colors
PWA Manifest
The dynamic manifest at app/manifest.ts (Next.js Metadata API) is the canonical source — it reads appConfig so name, theme color, and icons stay in sync with the rest of the project. There is no static manifest fallback in this repository. PWA features it enables:
- Installability - Users can install the app on their devices
- Standalone Mode - App runs without browser UI
- App Icons - SVG and PNG icons for various contexts
- Theme Colors - Consistent branding across platforms
Resource Preloading
The frontend layout includes DNS prefetch and preconnect hints for critical resources:
- Google Fonts - DNS prefetch and preconnect for faster font loading
- Supabase - Conditional preconnect when configured
- Google Tag Manager - Conditional preconnect when GTM is enabled
Locale Proxy
The proxy.ts handles automatic locale detection and SEO-friendly redirects (Next.js 16+ convention):
- Accept-Language Detection - Automatically detects user's preferred language
- Cookie Persistence - Remembers user's locale preference for 1 year
- 308 Permanent Redirect - SEO-friendly redirects for non-localized URLs
- Excluded Paths - Static files and API routes bypass locale routing
Base SEO settings are configured in config/app.ts under the seo object. This includes default title, title template, and OpenGraph/Twitter settings.
You need to create the following image files in public/: favicon.ico (32x32), apple-touch-icon.png (180x180), and og-image.png (1200x630). SVG favicons are already provided.