Foundation
The base layer everything else assumes. One origin, one host, one trailing-slash policy, real status codes. Get these wrong and nothing built on top of them can work.
6 min read · updated 2026-07-26· last reviewed 2026-07-26
The site has exactly one origin
The principle
Every URL your site emits, in canonicals, in structured data, in the sitemap, in Open Graph tags, in emails, has to agree on what the site's origin is. The failure mode is not subtle and it is extremely common: a staging hostname leaks into a production canonical, and the affected pages quietly de-index because they are telling Google their real address is somewhere that returns a 404 to the public.
The fix is structural rather than diligent. Define the origin in one constant. Make every other file import it. Then add a test that fails the build if any other file contains the domain as a literal, because diligence does not survive the twentieth file.
The implementation
// kit/seo/constants.ts
export function resolveSiteUrl(fallback: string): string {
const fromEnv =
process.env.NEXT_PUBLIC_SITE_URL ??
(process.env.VERCEL_PROJECT_PRODUCTION_URL
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
: undefined);
return stripTrailingSlashes(fromEnv && fromEnv.length > 0 ? fromEnv : fallback);
}The test is the part people skip, and it is the part that makes the rule hold:
it("only lib/brand.ts hardcodes the production host", () => {
const offenders = walk(REPO_ROOT)
.filter((file) => readFileSync(file, "utf8").includes(HOST))
.filter((file) => !ALLOWED.has(relative(REPO_ROOT, file)));
expect(offenders).toEqual([]);
});One canonical host, and a permanent redirect from the other
The principle
example.com and www.example.com are different origins. If both return 200 for the same content, you have doubled your URL count, halved the link equity per URL, and doubled the crawl budget spent covering the same pages.
Pick one. It does not matter much which, as long as it is consistent and permanent. Redirect the other with a 301, not a 302: a temporary redirect tells the crawler to keep checking the old host indefinitely.
The implementation
// next.config.ts
async redirects() {
return [
{
source: "/:path*",
has: [{ type: "host", value: APEX_HOST }],
destination: `https://${CANONICAL_HOST}/:path*`,
permanent: true,
},
];
}Verify both directions. A redirect rule with a matching condition that is too loose produces an infinite loop, and the symptom is a page that works in your browser because of a cached redirect and fails for everyone else.
curl -sI https://example.com | grep -i '^location'
curl -s -o /dev/null -w '%{http_code}\n' https://www.example.comHTTPS everywhere, with HSTS
The principle
Mixed content is a hard failure in modern browsers rather than a warning. One http:// script tag on an https:// page is a blocked resource, and if it is your analytics or your checkout, you will not notice until the numbers look wrong.
HSTS removes the redirect hop on every repeat visit. Without it, the first request of each session goes out over plain http and is redirectable by anything on the network path between the visitor and you.
The implementation
// kit/config/headers.ts
{
key: "Strict-Transport-Security",
value: `max-age=63072000; includeSubDomains; preload`,
}preload is a commitment. Once the domain is on the browsers' preload list, removal takes months. Do not add it until you are certain every subdomain, including ones you forgot about, serves HTTPS.
One trailing-slash policy
The principle
/about and /about/ are different URLs to a crawler. If both return 200, every page on the site has a duplicate. If one redirects to the other, you have one canonical form and a cheap, permanent rule.
The choice between them is arbitrary. The consistency is not.
The implementation
Normalisation belongs inside the canonical URL function, so it applies everywhere by construction rather than by remembering:
export function canonicalUrl(path: string | undefined, siteUrl: string): string {
const origin = stripTrailingSlashes(siteUrl);
if (!path) return origin;
if (/^https?:\/\//i.test(path)) return stripTrailingSlashes(path) || path;
const queryIndex = path.search(/[?#]/);
const rawPath = queryIndex === -1 ? path : path.slice(0, queryIndex);
const suffix = queryIndex === -1 ? "" : path.slice(queryIndex);
let normalised = rawPath.replace(/\/{2,}/g, "/");
if (!normalised.startsWith("/")) normalised = `/${normalised}`;
normalised = normalised.replace(/\/+$/, "");
if (normalised === "" || normalised === "/") {
return suffix ? `${origin}/${suffix}` : origin;
}
return `${origin}${normalised}${suffix}`;
}The edge cases that matter in production are "", "/", "/a/", "//a" and a path with a query string attached. All five appear in real code, usually because a path was built by string concatenation somewhere upstream.
A missing page returns 404, not 200
The principle
A soft 404 is a page that says "not found" in words while returning a 200 status. Crawlers read the status, not the words. The result is an indexed page with no useful content, and enough of them will affect how the whole site is assessed.
The most common source is a dynamic route that renders a template with empty data instead of calling the framework's not-found handler.
The implementation
// app/glossary/[term]/page.tsx
import { notFound } from "next/navigation";
import { GLOSSARY_TERMS } from "@/data/glossary-terms";
export default async function TermPage({ params }: { params: Promise<{ term: string }> }) {
const { term } = await params;
const entry = GLOSSARY_TERMS.find((candidate) => candidate.slug === term);
if (!entry) notFound();
return <TermTemplate entry={entry} />;
}This is the same mechanism as entity gating, covered in its own chapter. The difference in framing: entity gating is about not generating junk pages, and this is about correctly refusing the ones you never generated.
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/this-does-not-existThe 404 page itself should carry real recovery links, not an apology. Search, the main hub pages, and the highest-traffic entry points. A 404 that offers a way forward converts; one that offers a sad face does not.
Checks this chapter covers
Each one has a command you can run against your own site.
- →One constant holds the site origin, and nothing else hardcodes itfound-single-site-url
- →One canonical host, with a permanent redirect from the otherfound-canonical-host
- →HTTPS everywhere, with HSTS and no mixed contentfound-https-everywhere
- →One trailing-slash policy, enforced by redirectfound-trailing-slash-policy
- →A missing page returns a real 404 status, not a 200 with an error messagefound-404-returns-404