@brandlab-digital/site-core 3.1.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/business.d.ts +40 -0
- package/dist/business.d.ts.map +1 -1
- package/dist/business.js +175 -79
- package/dist/business.js.map +1 -1
- package/dist/geography.d.ts +2 -0
- package/dist/geography.d.ts.map +1 -1
- package/dist/geography.js +0 -18
- package/dist/geography.js.map +1 -1
- package/dist/site.d.ts +11 -1
- package/dist/site.d.ts.map +1 -1
- package/dist/site.js +24 -15
- package/dist/site.js.map +1 -1
- package/package.json +1 -1
package/dist/site.d.ts
CHANGED
|
@@ -38,8 +38,12 @@ export interface BrandlabSiteDefinition {
|
|
|
38
38
|
/**
|
|
39
39
|
* Hosts that permanently redirect every path to the canonical origin, such as
|
|
40
40
|
* the `www` host, the Vercel project host or a retired domain.
|
|
41
|
+
*
|
|
42
|
+
* Optional from 3.2.1: declare them in next.config with
|
|
43
|
+
* `defineBrandlabNextConfig({ aliasHosts })` instead, so they stay out of the
|
|
44
|
+
* browser bundles of every client component that imports the site identity.
|
|
41
45
|
*/
|
|
42
|
-
aliasHosts
|
|
46
|
+
aliasHosts?: readonly string[];
|
|
43
47
|
}
|
|
44
48
|
export interface BrandlabSite {
|
|
45
49
|
readonly name: string;
|
|
@@ -57,4 +61,10 @@ export declare function isBrandlabHostname(value: unknown): value is string;
|
|
|
57
61
|
* feeds metadata, robots, sitemaps, redirects and next.config.
|
|
58
62
|
*/
|
|
59
63
|
export declare function defineBrandlabSite(definition: BrandlabSiteDefinition): BrandlabSite;
|
|
64
|
+
/**
|
|
65
|
+
* Checks a list of alias hosts: lowercase hostnames without a scheme, port or
|
|
66
|
+
* path, never the canonical host, each listed once. `defineBrandlabSite` and
|
|
67
|
+
* `defineBrandlabNextConfig({ aliasHosts })` share it.
|
|
68
|
+
*/
|
|
69
|
+
export declare function assertBrandlabAliasHosts(hosts: unknown, canonicalHost: string, field?: string): asserts hosts is readonly string[];
|
|
60
70
|
//# sourceMappingURL=site.d.ts.map
|
package/dist/site.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"site.d.ts","sourceRoot":"","sources":["../src/site.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,eAAe,CAAA;AAEtB;;;GAGG;AACH,MAAM,MAAM,6BAA6B,GAAG,YAAY,GAAG,SAAS,GAAG,aAAa,CAAA;AAEpF,MAAM,WAAW,kBAAkB;IACjC,2GAA2G;IAC3G,QAAQ,CAAC,WAAW,EAAE,6BAA6B,CAAA;IACnD;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAA;CAClC;AAED,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;AAE9E;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,mBAAmB,GAAG,kBAAkB,CAe9F;AAED,MAAM,WAAW,sBAAsB;IACrC,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAA;IACZ,4FAA4F;IAC5F,YAAY,EAAE,MAAM,CAAA;IACpB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAA;IACd,sFAAsF;IACtF,aAAa,EAAE,MAAM,CAAA;IACrB,kBAAkB,EAAE,mBAAmB,CAAA;IACvC
|
|
1
|
+
{"version":3,"file":"site.d.ts","sourceRoot":"","sources":["../src/site.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,eAAe,CAAA;AAEtB;;;GAGG;AACH,MAAM,MAAM,6BAA6B,GAAG,YAAY,GAAG,SAAS,GAAG,aAAa,CAAA;AAEpF,MAAM,WAAW,kBAAkB;IACjC,2GAA2G;IAC3G,QAAQ,CAAC,WAAW,EAAE,6BAA6B,CAAA;IACnD;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAA;CAClC;AAED,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;AAE9E;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,mBAAmB,GAAG,kBAAkB,CAe9F;AAED,MAAM,WAAW,sBAAsB;IACrC,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAA;IACZ,4FAA4F;IAC5F,YAAY,EAAE,MAAM,CAAA;IACpB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAA;IACd,sFAAsF;IACtF,aAAa,EAAE,MAAM,CAAA;IACrB,kBAAkB,EAAE,mBAAmB,CAAA;IACvC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,uFAAuF;IACvF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAA;IAC1D,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;CACvC;AAyBD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAElE;AA0BD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,GAAG,YAAY,CA+BnF;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,aAAa,EAAE,MAAM,EACrB,KAAK,SAAe,GACnB,OAAO,CAAC,KAAK,IAAI,SAAS,MAAM,EAAE,CAcpC"}
|
package/dist/site.js
CHANGED
|
@@ -89,20 +89,8 @@ export function defineBrandlabSite(definition) {
|
|
|
89
89
|
throw new Error('titleTemplate must contain exactly one %s placeholder.');
|
|
90
90
|
}
|
|
91
91
|
assertBrandlabSocialImage(definition.defaultSocialImage, 'defaultSocialImage');
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
definition.aliasHosts.forEach((host, index) => {
|
|
96
|
-
if (!isBrandlabHostname(host)) {
|
|
97
|
-
throw new Error(`aliasHosts[${index}] must be a lowercase hostname without a scheme, port or path.`);
|
|
98
|
-
}
|
|
99
|
-
if (host === origin.hostname) {
|
|
100
|
-
throw new Error(`aliasHosts[${index}] cannot be the canonical host.`);
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
const duplicate = definition.aliasHosts.find((host, index) => definition.aliasHosts.indexOf(host) !== index);
|
|
104
|
-
if (duplicate)
|
|
105
|
-
throw new Error(`aliasHosts contains duplicate host "${duplicate}".`);
|
|
92
|
+
const aliasHosts = definition.aliasHosts ?? [];
|
|
93
|
+
assertBrandlabAliasHosts(aliasHosts, origin.hostname);
|
|
106
94
|
return Object.freeze({
|
|
107
95
|
name: definition.name,
|
|
108
96
|
canonicalUrl: origin.origin,
|
|
@@ -110,7 +98,28 @@ export function defineBrandlabSite(definition) {
|
|
|
110
98
|
locale: definition.locale,
|
|
111
99
|
titleTemplate: definition.titleTemplate,
|
|
112
100
|
defaultSocialImage: Object.freeze({ ...definition.defaultSocialImage }),
|
|
113
|
-
aliasHosts: Object.freeze([...
|
|
101
|
+
aliasHosts: Object.freeze([...aliasHosts]),
|
|
114
102
|
});
|
|
115
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Checks a list of alias hosts: lowercase hostnames without a scheme, port or
|
|
106
|
+
* path, never the canonical host, each listed once. `defineBrandlabSite` and
|
|
107
|
+
* `defineBrandlabNextConfig({ aliasHosts })` share it.
|
|
108
|
+
*/
|
|
109
|
+
export function assertBrandlabAliasHosts(hosts, canonicalHost, field = 'aliasHosts') {
|
|
110
|
+
if (!Array.isArray(hosts)) {
|
|
111
|
+
throw new Error(`${field} must list every host that redirects to the canonical origin.`);
|
|
112
|
+
}
|
|
113
|
+
hosts.forEach((host, index) => {
|
|
114
|
+
if (!isBrandlabHostname(host)) {
|
|
115
|
+
throw new Error(`${field}[${index}] must be a lowercase hostname without a scheme, port or path.`);
|
|
116
|
+
}
|
|
117
|
+
if (host === canonicalHost) {
|
|
118
|
+
throw new Error(`${field}[${index}] cannot be the canonical host.`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
const duplicate = hosts.find((host, index) => hosts.indexOf(host) !== index);
|
|
122
|
+
if (duplicate)
|
|
123
|
+
throw new Error(`${field} contains duplicate host "${duplicate}".`);
|
|
124
|
+
}
|
|
116
125
|
//# sourceMappingURL=site.js.map
|
package/dist/site.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"site.js","sourceRoot":"","sources":["../src/site.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,GAE1B,MAAM,eAAe,CAAA;AAqBtB;;;;;;;;GAQG;AACH,MAAM,UAAU,yBAAyB,CAAC,WAAgC;IACxE,IAAI,CAAC,WAAW,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;QACpD,MAAM,IAAI,SAAS,CAAC,2DAA2D,CAAC,CAAA;IAClF,CAAC;IACD,MAAM,QAAQ,GAAG,CAAC,WAAW,CAAC,UAAU,IAAI,WAAW,CAAC,sBAAsB,CAAC,EAAE,IAAI,EAAE,CAAA;IACvF,MAAM,qBAAqB,GACzB,CAAC,QAAQ,IAAI,QAAQ,KAAK,aAAa;QACrC,CAAC,CAAC,aAAa;QACf,CAAC,CAAC,QAAQ,KAAK,YAAY;YACzB,CAAC,CAAC,YAAY;YACd,CAAC,CAAC,SAAS,CAAA;IACjB,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,WAAW,EAAE,qBAAqB;QAClC,eAAe,EAAE,WAAW,CAAC,QAAQ,KAAK,YAAY;KACvD,CAAC,CAAA;AACJ,CAAC;AA8BD,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,MAAM;IACN,cAAc;IACd,QAAQ;IACR,eAAe;IACf,oBAAoB;IACpB,YAAY;CACb,CAAC,CAAA;AACF,MAAM,gBAAgB,GACpB,wFAAwF,CAAA;AAE1F,SAAS,cAAc,CAAC,KAAc,EAAE,KAAa,EAAE,OAAe;IACpE,IACE,OAAO,KAAK,KAAK,QAAQ;QACzB,CAAC,KAAK,CAAC,IAAI,EAAE;QACb,KAAK,CAAC,IAAI,EAAE,KAAK,KAAK;QACtB,KAAK,CAAC,MAAM,GAAG,OAAO;QACtB,uBAAuB,CAAC,IAAI,CAAC,KAAK,CAAC,EACnC,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,8CAA8C,CAAC,CAAA;IACzE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AAClE,CAAC;AAED,SAAS,eAAe,CAAC,KAAc;IACrC,cAAc,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,CAAC,CAAA;IAC5C,IAAI,GAAQ,CAAA;IACZ,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAA;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,mDAAmD,KAAK,IAAI,CAAC,CAAA;IAC/E,CAAC;IACD,IACE,GAAG,CAAC,QAAQ,KAAK,QAAQ;QACzB,GAAG,CAAC,QAAQ;QACZ,GAAG,CAAC,QAAQ;QACZ,GAAG,CAAC,IAAI;QACR,GAAG,CAAC,MAAM;QACV,GAAG,CAAC,IAAI;QACR,CAAC,GAAG,CAAC,QAAQ,KAAK,GAAG,IAAI,GAAG,CAAC,QAAQ,KAAK,EAAE,CAAC;QAC7C,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,MAAM;QACvC,CAAC,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,EACjC,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,mDAAmD,KAAK,IAAI,CAAC,CAAA;IAC/E,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAAkC;IACnE,IAAI,CAAC,UAAU,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/E,MAAM,IAAI,SAAS,CAAC,gDAAgD,CAAC,CAAA;IACvE,CAAC;IACD,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAA;IAChF,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,8CAA8C,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAC1F,CAAC;IACD,cAAc,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,CAAA;IAC5C,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,YAAY,CAAC,CAAA;IACvD,cAAc,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;IAC/C,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACxD,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAA;IAC9E,CAAC;IACD,cAAc,CAAC,UAAU,CAAC,aAAa,EAAE,eAAe,EAAE,GAAG,CAAC,CAAA;IAC9D,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/D,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAA;IAC3E,CAAC;IACD,yBAAyB,CAAC,UAAU,CAAC,kBAAkB,EAAE,oBAAoB,CAAC,CAAA;IAC9E,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,yEAAyE,CAAC,CAAA;IAC5F,CAAC;IACD,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAC5C,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,cAAc,KAAK,gEAAgE,CAAC,CAAA;QACtG,CAAC;QACD,IAAI,IAAI,KAAK,MAAM,CAAC,QAAQ,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,cAAc,KAAK,iCAAiC,CAAC,CAAA;QACvE,CAAC;IACH,CAAC,CAAC,CAAA;IACF,MAAM,SAAS,GAAG,UAAU,CAAC,UAAU,CAAC,IAAI,CAC1C,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,KAAK,CAC/D,CAAA;IACD,IAAI,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,SAAS,IAAI,CAAC,CAAA;IAEpF,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,IAAI,EAAE,UAAU,CAAC,IAAI;QACrB,YAAY,EAAE,MAAM,CAAC,MAAM;QAC3B,aAAa,EAAE,MAAM,CAAC,QAAQ;QAC9B,MAAM,EAAE,UAAU,CAAC,MAAM;QACzB,aAAa,EAAE,UAAU,CAAC,aAAa;QACvC,kBAAkB,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,UAAU,CAAC,kBAAkB,EAAE,CAAC;QACvE,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;KACtD,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import {\n assertBrandlabSocialImage,\n type BrandlabSocialImage,\n} from './metadata.js'\n\n/**\n * `production` and `preview` are real Vercel deployments. Everything else —\n * `next dev`, a local `next start`, CI and tests — is `development`.\n */\nexport type BrandlabDeploymentEnvironment = 'production' | 'preview' | 'development'\n\nexport interface BrandlabDeployment {\n /** The single gate for production-only behaviour: analytics, contact delivery and Preview restrictions. */\n readonly environment: BrandlabDeploymentEnvironment\n /**\n * An optimised build (`NODE_ENV=production`), on Vercel or locally. Security\n * headers and crawler access follow the build so a local `next start` serves\n * exactly the production headers.\n */\n readonly productionBuild: boolean\n}\n\nexport type BrandlabEnvironment = Readonly<Record<string, string | undefined>>\n\n/**\n * Resolves the deployment once for every consumer (next.config headers,\n * robots, analytics and contact delivery). Server code passes `process.env`;\n * browser code passes `{ NEXT_PUBLIC_VERCEL_ENV: process.env.NEXT_PUBLIC_VERCEL_ENV }`,\n * which `defineBrandlabNextConfig` inlines at build time.\n *\n * `VERCEL_ENV` wins over `NEXT_PUBLIC_VERCEL_ENV`. An unrecognised declared\n * environment fails closed to `preview`: never analytics, never indexable.\n */\nexport function resolveBrandlabDeployment(environment: BrandlabEnvironment): BrandlabDeployment {\n if (!environment || typeof environment !== 'object') {\n throw new TypeError('resolveBrandlabDeployment requires an environment record.')\n }\n const declared = (environment.VERCEL_ENV ?? environment.NEXT_PUBLIC_VERCEL_ENV)?.trim()\n const deploymentEnvironment: BrandlabDeploymentEnvironment =\n !declared || declared === 'development'\n ? 'development'\n : declared === 'production'\n ? 'production'\n : 'preview'\n return Object.freeze({\n environment: deploymentEnvironment,\n productionBuild: environment.NODE_ENV === 'production',\n })\n}\n\nexport interface BrandlabSiteDefinition {\n /** Public site name: Open Graph `siteName` and the brand guard for templated titles. */\n name: string\n /** Canonical HTTPS origin written as a source constant, never read from the environment. */\n canonicalUrl: string\n /** Open Graph locale, for example `en_AU`. */\n locale: string\n /** Page title template with exactly one `%s`, for example `%s | Example Services`. */\n titleTemplate: string\n defaultSocialImage: BrandlabSocialImage\n /**\n * Hosts that permanently redirect every path to the canonical origin, such as\n * the `www` host, the Vercel project host or a retired domain.\n */\n aliasHosts: readonly string[]\n}\n\nexport interface BrandlabSite {\n readonly name: string\n /** Canonical origin without a trailing slash, for example `https://example.com.au`. */\n readonly canonicalUrl: string\n readonly canonicalHost: string\n readonly locale: string\n readonly titleTemplate: string\n readonly defaultSocialImage: Readonly<BrandlabSocialImage>\n readonly aliasHosts: readonly string[]\n}\n\nconst SITE_KEYS = new Set([\n 'name',\n 'canonicalUrl',\n 'locale',\n 'titleTemplate',\n 'defaultSocialImage',\n 'aliasHosts',\n])\nconst HOSTNAME_PATTERN =\n /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$/\n\nfunction assertSiteText(value: unknown, field: string, maximum: number): asserts value is string {\n if (\n typeof value !== 'string' ||\n !value.trim() ||\n value.trim() !== value ||\n value.length > maximum ||\n /[\\u0000-\\u001f\\u007f]/.test(value)\n ) {\n throw new Error(`${field} must be bounded, trimmed, single-line text.`)\n }\n}\n\nexport function isBrandlabHostname(value: unknown): value is string {\n return typeof value === 'string' && HOSTNAME_PATTERN.test(value)\n}\n\nfunction canonicalOrigin(value: unknown) {\n assertSiteText(value, 'canonicalUrl', 2_048)\n let url: URL\n try {\n url = new URL(value)\n } catch {\n throw new Error(`canonicalUrl must be an absolute HTTPS origin: \"${value}\".`)\n }\n if (\n url.protocol !== 'https:' ||\n url.username ||\n url.password ||\n url.port ||\n url.search ||\n url.hash ||\n (url.pathname !== '/' && url.pathname !== '') ||\n value.replace(/\\/$/, '') !== url.origin ||\n !isBrandlabHostname(url.hostname)\n ) {\n throw new Error(`canonicalUrl must be an absolute HTTPS origin: \"${value}\".`)\n }\n return url\n}\n\n/**\n * Declares the site-wide SEO identity once (`src/config/site.ts`). The result\n * feeds metadata, robots, sitemaps, redirects and next.config.\n */\nexport function defineBrandlabSite(definition: BrandlabSiteDefinition): BrandlabSite {\n if (!definition || typeof definition !== 'object' || Array.isArray(definition)) {\n throw new TypeError('defineBrandlabSite requires a site definition.')\n }\n const unsupported = Object.keys(definition).filter((key) => !SITE_KEYS.has(key))\n if (unsupported.length > 0) {\n throw new Error(`Site definition contains unsupported keys: ${unsupported.join(', ')}.`)\n }\n assertSiteText(definition.name, 'name', 160)\n const origin = canonicalOrigin(definition.canonicalUrl)\n assertSiteText(definition.locale, 'locale', 20)\n if (!/^[a-z]{2}(?:_[A-Z]{2})?$/.test(definition.locale)) {\n throw new Error('locale must use a language or language_REGION identifier.')\n }\n assertSiteText(definition.titleTemplate, 'titleTemplate', 240)\n if ((definition.titleTemplate.match(/%s/g) ?? []).length !== 1) {\n throw new Error('titleTemplate must contain exactly one %s placeholder.')\n }\n assertBrandlabSocialImage(definition.defaultSocialImage, 'defaultSocialImage')\n if (!Array.isArray(definition.aliasHosts)) {\n throw new Error('aliasHosts must list every host that redirects to the canonical origin.')\n }\n definition.aliasHosts.forEach((host, index) => {\n if (!isBrandlabHostname(host)) {\n throw new Error(`aliasHosts[${index}] must be a lowercase hostname without a scheme, port or path.`)\n }\n if (host === origin.hostname) {\n throw new Error(`aliasHosts[${index}] cannot be the canonical host.`)\n }\n })\n const duplicate = definition.aliasHosts.find(\n (host, index) => definition.aliasHosts.indexOf(host) !== index,\n )\n if (duplicate) throw new Error(`aliasHosts contains duplicate host \"${duplicate}\".`)\n\n return Object.freeze({\n name: definition.name,\n canonicalUrl: origin.origin,\n canonicalHost: origin.hostname,\n locale: definition.locale,\n titleTemplate: definition.titleTemplate,\n defaultSocialImage: Object.freeze({ ...definition.defaultSocialImage }),\n aliasHosts: Object.freeze([...definition.aliasHosts]),\n })\n}\n"]}
|
|
1
|
+
{"version":3,"file":"site.js","sourceRoot":"","sources":["../src/site.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n assertBrandlabSocialImage,\n type BrandlabSocialImage,\n} from './metadata.js'\n\n/**\n * `production` and `preview` are real Vercel deployments. Everything else —\n * `next dev`, a local `next start`, CI and tests — is `development`.\n */\nexport type BrandlabDeploymentEnvironment = 'production' | 'preview' | 'development'\n\nexport interface BrandlabDeployment {\n /** The single gate for production-only behaviour: analytics, contact delivery and Preview restrictions. */\n readonly environment: BrandlabDeploymentEnvironment\n /**\n * An optimised build (`NODE_ENV=production`), on Vercel or locally. Security\n * headers and crawler access follow the build so a local `next start` serves\n * exactly the production headers.\n */\n readonly productionBuild: boolean\n}\n\nexport type BrandlabEnvironment = Readonly<Record<string, string | undefined>>\n\n/**\n * Resolves the deployment once for every consumer (next.config headers,\n * robots, analytics and contact delivery). Server code passes `process.env`;\n * browser code passes `{ NEXT_PUBLIC_VERCEL_ENV: process.env.NEXT_PUBLIC_VERCEL_ENV }`,\n * which `defineBrandlabNextConfig` inlines at build time.\n *\n * `VERCEL_ENV` wins over `NEXT_PUBLIC_VERCEL_ENV`. An unrecognised declared\n * environment fails closed to `preview`: never analytics, never indexable.\n */\nexport function resolveBrandlabDeployment(environment: BrandlabEnvironment): BrandlabDeployment {\n if (!environment || typeof environment !== 'object') {\n throw new TypeError('resolveBrandlabDeployment requires an environment record.')\n }\n const declared = (environment.VERCEL_ENV ?? environment.NEXT_PUBLIC_VERCEL_ENV)?.trim()\n const deploymentEnvironment: BrandlabDeploymentEnvironment =\n !declared || declared === 'development'\n ? 'development'\n : declared === 'production'\n ? 'production'\n : 'preview'\n return Object.freeze({\n environment: deploymentEnvironment,\n productionBuild: environment.NODE_ENV === 'production',\n })\n}\n\nexport interface BrandlabSiteDefinition {\n /** Public site name: Open Graph `siteName` and the brand guard for templated titles. */\n name: string\n /** Canonical HTTPS origin written as a source constant, never read from the environment. */\n canonicalUrl: string\n /** Open Graph locale, for example `en_AU`. */\n locale: string\n /** Page title template with exactly one `%s`, for example `%s | Example Services`. */\n titleTemplate: string\n defaultSocialImage: BrandlabSocialImage\n /**\n * Hosts that permanently redirect every path to the canonical origin, such as\n * the `www` host, the Vercel project host or a retired domain.\n *\n * Optional from 3.2.1: declare them in next.config with\n * `defineBrandlabNextConfig({ aliasHosts })` instead, so they stay out of the\n * browser bundles of every client component that imports the site identity.\n */\n aliasHosts?: readonly string[]\n}\n\nexport interface BrandlabSite {\n readonly name: string\n /** Canonical origin without a trailing slash, for example `https://example.com.au`. */\n readonly canonicalUrl: string\n readonly canonicalHost: string\n readonly locale: string\n readonly titleTemplate: string\n readonly defaultSocialImage: Readonly<BrandlabSocialImage>\n readonly aliasHosts: readonly string[]\n}\n\nconst SITE_KEYS = new Set([\n 'name',\n 'canonicalUrl',\n 'locale',\n 'titleTemplate',\n 'defaultSocialImage',\n 'aliasHosts',\n])\nconst HOSTNAME_PATTERN =\n /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$/\n\nfunction assertSiteText(value: unknown, field: string, maximum: number): asserts value is string {\n if (\n typeof value !== 'string' ||\n !value.trim() ||\n value.trim() !== value ||\n value.length > maximum ||\n /[\\u0000-\\u001f\\u007f]/.test(value)\n ) {\n throw new Error(`${field} must be bounded, trimmed, single-line text.`)\n }\n}\n\nexport function isBrandlabHostname(value: unknown): value is string {\n return typeof value === 'string' && HOSTNAME_PATTERN.test(value)\n}\n\nfunction canonicalOrigin(value: unknown) {\n assertSiteText(value, 'canonicalUrl', 2_048)\n let url: URL\n try {\n url = new URL(value)\n } catch {\n throw new Error(`canonicalUrl must be an absolute HTTPS origin: \"${value}\".`)\n }\n if (\n url.protocol !== 'https:' ||\n url.username ||\n url.password ||\n url.port ||\n url.search ||\n url.hash ||\n (url.pathname !== '/' && url.pathname !== '') ||\n value.replace(/\\/$/, '') !== url.origin ||\n !isBrandlabHostname(url.hostname)\n ) {\n throw new Error(`canonicalUrl must be an absolute HTTPS origin: \"${value}\".`)\n }\n return url\n}\n\n/**\n * Declares the site-wide SEO identity once (`src/config/site.ts`). The result\n * feeds metadata, robots, sitemaps, redirects and next.config.\n */\nexport function defineBrandlabSite(definition: BrandlabSiteDefinition): BrandlabSite {\n if (!definition || typeof definition !== 'object' || Array.isArray(definition)) {\n throw new TypeError('defineBrandlabSite requires a site definition.')\n }\n const unsupported = Object.keys(definition).filter((key) => !SITE_KEYS.has(key))\n if (unsupported.length > 0) {\n throw new Error(`Site definition contains unsupported keys: ${unsupported.join(', ')}.`)\n }\n assertSiteText(definition.name, 'name', 160)\n const origin = canonicalOrigin(definition.canonicalUrl)\n assertSiteText(definition.locale, 'locale', 20)\n if (!/^[a-z]{2}(?:_[A-Z]{2})?$/.test(definition.locale)) {\n throw new Error('locale must use a language or language_REGION identifier.')\n }\n assertSiteText(definition.titleTemplate, 'titleTemplate', 240)\n if ((definition.titleTemplate.match(/%s/g) ?? []).length !== 1) {\n throw new Error('titleTemplate must contain exactly one %s placeholder.')\n }\n assertBrandlabSocialImage(definition.defaultSocialImage, 'defaultSocialImage')\n const aliasHosts = definition.aliasHosts ?? []\n assertBrandlabAliasHosts(aliasHosts, origin.hostname)\n\n return Object.freeze({\n name: definition.name,\n canonicalUrl: origin.origin,\n canonicalHost: origin.hostname,\n locale: definition.locale,\n titleTemplate: definition.titleTemplate,\n defaultSocialImage: Object.freeze({ ...definition.defaultSocialImage }),\n aliasHosts: Object.freeze([...aliasHosts]),\n })\n}\n\n/**\n * Checks a list of alias hosts: lowercase hostnames without a scheme, port or\n * path, never the canonical host, each listed once. `defineBrandlabSite` and\n * `defineBrandlabNextConfig({ aliasHosts })` share it.\n */\nexport function assertBrandlabAliasHosts(\n hosts: unknown,\n canonicalHost: string,\n field = 'aliasHosts',\n): asserts hosts is readonly string[] {\n if (!Array.isArray(hosts)) {\n throw new Error(`${field} must list every host that redirects to the canonical origin.`)\n }\n hosts.forEach((host, index) => {\n if (!isBrandlabHostname(host)) {\n throw new Error(`${field}[${index}] must be a lowercase hostname without a scheme, port or path.`)\n }\n if (host === canonicalHost) {\n throw new Error(`${field}[${index}] cannot be the canonical host.`)\n }\n })\n const duplicate = hosts.find((host, index) => hosts.indexOf(host) !== index)\n if (duplicate) throw new Error(`${field} contains duplicate host \"${duplicate}\".`)\n}\n"]}
|