Production
The search, sharing and security defaults prfct ships with: what each one does, and how to keep them as you build on top.
An interface people can't find, or can't trust, isn't finished. This site ships with the defaults below, and so does any app you start from its repository. None of them needs a database or a third-party service: each one lives in a file you can read, change or copy. In an existing project, the security baseline installs from the registry (see In another project below); the metadata helpers are short files to copy from the repository.
Search engines
Metadata
Every page describes itself: a title, a description, its canonical URL, its translations (hreflang, with x-default), and the Open Graph and Twitter cards that link previews read. pageMetadata() writes all of it from four values.
import type { Metadata } from "next"
import { pageMetadata } from "@/lib/i18n/metadata"
import { getI18n } from "@/lib/i18n/server"
export async function generateMetadata(): Promise<Metadata> {
const { locale, t } = await getI18n()
return pageMetadata({
path: "/pricing",
locale,
title: t.pricing.title,
description: t.pricing.description,
})
}Sitemap and robots
app/sitemap.ts lists every page in every language, each with links to its translations. app/robots.ts points crawlers to the sitemap and keeps data endpoints, such as the search index, out of results. Documentation pages and example apps are listed automatically. When you add a route of your own, add its path to the list in that file.
Social cards
Each page has its own 1200 × 630 card, rendered at build time from its title and description by lib/og.tsx. There is no image service to run and nothing to design by hand.
pageMetadata() points each page at /og followed by its own path, and the cards for the documentation, the examples and the theme builder already exist. A route you add needs a card route of its own: copy app/[locale]/og/themes/route.tsx to the matching folder, such as app/[locale]/og/pricing/route.tsx. Without it, the page's link preview has no image.
Structured data
<JsonLd> writes schema.org data that search engines use for rich results. The home page describes the site and its source code; documentation pages describe themselves as a TechArticle with a BreadcrumbList. The component escapes <, so no string in the data can close its script element.
Security headers
Every response carries a security baseline, set once in lib/security.ts and applied in next.config.ts.
In another project
The baseline is a registry item. Install it into any Next.js project, then apply it in next.config.ts:
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/security.jsonimport type { NextConfig } from "next"
import { securityHeaders } from "./lib/security"
const nextConfig: NextConfig = {
poweredByHeader: false,
async headers() {
return [{ source: "/:path*", headers: securityHeaders }]
},
}
export default nextConfigCheck Strict-Transport-Security before you deploy: it covers every subdomain for two years and carries preload, so every subdomain must already serve HTTPS.
Allowing a service
The policy is data: one entry per directive. To use a service from another origin, add that origin to the directives it needs. An analytics script, for example, loads a script and sends requests:
export const contentSecurityPolicy: Record<string, string[]> = {
// …
"script-src": [
"'self'",
"'unsafe-inline'",
...(isDev ? ["'unsafe-eval'"] : []),
"https://plausible.io",
],
// …
"connect-src": ["'self'", ...(isDev ? ["ws:"] : []), "https://plausible.io"],
}Keep the isDev entries: they apply only in next dev, where React needs eval to rebuild error stacks and hot reload needs its socket.
When something doesn't load, open the browser console: every blocked request is reported there, with the directive that blocked it.
Stricter scripts
Static pages are built once, so they can't carry a fresh nonce for every request. The policy therefore allows inline scripts, which Next.js uses to start the page; scripts from other origins stay blocked. If your app renders every request anyway, behind a sign-in for example, switch to a nonce-based policy in proxy.ts, as the Next.js guide describes.
Reporting vulnerabilities
/.well-known/security.txt (RFC 9116) tells security researchers where to report a problem. Its expiry date renews with every deploy. Point its Contact at your own address in app/.well-known/security.txt/route.ts.
Before you ship
- Set
NEXT_PUBLIC_SITE_URLto your domain, so canonical URLs, the sitemap, social cards and install commands point to it. - Give every new route a
generateMetadatathat callspageMetadata(), a card route underapp/[locale]/og/, and an entry inapp/sitemap.ts. - Keep secrets in environment variables without the
NEXT_PUBLIC_prefix, and read them only in server code. Anything with the prefix is shipped to every visitor. - Validate every input on the server, even when the form already validated it in the browser.
- Render what people type as text.
dangerouslySetInnerHTMLis for markup your own code produced, never for user input. - Check the deployed site: securityheaders.com grades the headers, and Google's Rich Results Test reads the structured data.