Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/sanity-dataset-resolve/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ function isProductionHost(env: SanityDatasetResolveInput): boolean {
* Whether this deploy is the production site: an explicit `SANITY_STUDIO_DEPLOYMENT_TARGET`
* decides (`production` or not, any case), otherwise the host's production context. A staging
* site on a production host context sets the target to stay non-production. The web's crawl
* switch (`robots.ts`) and canonical origin (`siteUrl.ts`) follow it.
* switch (`robots.ts`) follows it, and so does its canonical origin (`siteUrl.ts`) except on
* Netlify's production context, which serves the primary domain either way.
*/
export function isProductionDeployment(
env: SanityDatasetResolveInput,
Expand Down
6 changes: 6 additions & 0 deletions web/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { resolveStudioDatasetAsync } from "@repo/sanity-dataset-resolve";
import type { NextConfig } from "next";

import { SITE_BASE_URL } from "./src/utils/siteUrl";

const isDev = process.env.NODE_ENV === "development";

function originOf(raw: string | undefined): string | null {
Expand Down Expand Up @@ -228,6 +230,10 @@ export default async function config(): Promise<NextConfig> {
return {
...nextConfig,
env: {
/* Resolved here, where Netlify's `URL` / `DEPLOY_PRIME_URL` exist, and
* inlined: the function runtime lacks them, so ISR regenerations and
* route handlers fell back to `http://localhost:3000`. */
NEXT_PUBLIC_SITE_URL: SITE_BASE_URL,
SANITY_STUDIO_PROJECT_ID: process.env.SANITY_STUDIO_PROJECT_ID ?? "",
SANITY_STUDIO_DATASET: await resolveStudioDatasetAsync(process.env),
},
Expand Down
32 changes: 19 additions & 13 deletions web/src/utils/siteUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { isProductionDeployment } from "@repo/sanity-dataset-resolve";
/**
* The site's canonical origin, resolved once at module load.
*
* Server-only. Everything below `NEXT_PUBLIC_SITE_URL` reads host variables
* that Next never inlines into the client bundle, so import this from
* metadata, `robots.ts` and `sitemap.ts` — not from a client component.
* Resolved at build: `next.config.ts` imports it while the host's build
* variables exist and inlines the result as `NEXT_PUBLIC_SITE_URL`. Netlify's
* function runtime, which serves ISR regenerations and route handlers, has no
* `URL` / `DEPLOY_PRIME_URL`, so it reads that inlined value. Server-only all
* the same: import this from metadata, `robots.ts` and `sitemap.ts` — not
* from a client component.
*
* Canonicals, `hreflang`, `og:url`, `metadataBase` and the sitemap reference
* in `robots.txt` all build on it. An explicit value wins, the deploy host's
Expand All @@ -27,15 +30,17 @@ function normalizeOrigin(value: string | undefined): string | null {
const isProduction = isProductionDeployment(process.env);

/**
* Netlify's own view of where this build is served from. Previews, branch
* deploys and non-production targets resolve to their own origin
* (`DEPLOY_PRIME_URL`) rather than the production domain, so they never claim
* to be canonical for the live site. The `NETLIFY` guard keeps a stray local
* `URL` variable out.
* Netlify's own view of where this build is served from. Previews and branch
* deploys resolve to their own origin (`DEPLOY_PRIME_URL`) rather than the
* production domain, so they never claim to be canonical for the live site.
* The `NETLIFY` guard keeps a stray local `URL` variable out.
*/
function netlifyOrigin(): string | null {
if (process.env.NETLIFY !== "true") return null;
if (isProduction) {
/* The production context serves the primary domain even when the deploy
* target keeps it non-production (a staging site on the development
* dataset): `DEPLOY_PRIME_URL` would be the `<branch>--` host there. */
if (isProduction || process.env.CONTEXT === "production") {
return normalizeOrigin(process.env.URL);
}
return (
Expand All @@ -54,10 +59,11 @@ function vercelOrigin(): string | null {
}

function resolveSiteBaseUrl(): string {
/* Explicit wins, and it is the only candidate that is inlined at build
* time — so the prerender and every later ISR regeneration agree on it.
* Set it (netlify.toml `[context.production.environment]`) on a site with
* its own domain; don't rely on the rest. */
/* Explicit wins. Inside the app it is always set: `next.config.ts` inlines
* this resolver's build-time result under that name, so the prerender and
* every later ISR regeneration agree on it. Set it yourself (netlify.toml
* `[context.production.environment]`) on a site with its own domain, so the
* canonical never depends on which domain the host considers primary. */
const explicit = normalizeOrigin(process.env.NEXT_PUBLIC_SITE_URL);
if (explicit) return explicit;

Expand Down
Loading