@brandlab-digital/site-core 2.0.0 → 3.0.0
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/analytics.d.ts +0 -11
- package/dist/analytics.d.ts.map +1 -1
- package/dist/analytics.js +16 -8
- package/dist/analytics.js.map +1 -1
- package/dist/blog.d.ts +11 -0
- package/dist/blog.d.ts.map +1 -1
- package/dist/blog.js +27 -0
- package/dist/blog.js.map +1 -1
- package/dist/business.d.ts +13 -0
- package/dist/business.d.ts.map +1 -1
- package/dist/business.js +19 -8
- package/dist/business.js.map +1 -1
- package/dist/locations.d.ts +89 -2
- package/dist/locations.d.ts.map +1 -1
- package/dist/locations.js +290 -2
- package/dist/locations.js.map +1 -1
- package/dist/metadata.d.ts +18 -5
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +36 -15
- package/dist/metadata.js.map +1 -1
- package/dist/project.d.ts +0 -8
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +21 -28
- package/dist/project.js.map +1 -1
- package/dist/redirects.d.ts +40 -0
- package/dist/redirects.d.ts.map +1 -0
- package/dist/redirects.js +286 -0
- package/dist/redirects.js.map +1 -0
- package/dist/schema.js +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/seo-page.d.ts +3 -2
- package/dist/seo-page.d.ts.map +1 -1
- package/dist/seo-page.js +3 -9
- package/dist/seo-page.js.map +1 -1
- package/dist/site.d.ts +60 -0
- package/dist/site.d.ts.map +1 -0
- package/dist/site.js +116 -0
- package/dist/site.js.map +1 -0
- package/dist/sitemaps.d.ts +5 -6
- package/dist/sitemaps.d.ts.map +1 -1
- package/dist/sitemaps.js +38 -48
- package/dist/sitemaps.js.map +1 -1
- package/package.json +11 -1
package/dist/site.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type BrandlabSocialImage } from './metadata.js';
|
|
2
|
+
/**
|
|
3
|
+
* `production` and `preview` are real Vercel deployments. Everything else —
|
|
4
|
+
* `next dev`, a local `next start`, CI and tests — is `development`.
|
|
5
|
+
*/
|
|
6
|
+
export type BrandlabDeploymentEnvironment = 'production' | 'preview' | 'development';
|
|
7
|
+
export interface BrandlabDeployment {
|
|
8
|
+
/** The single gate for production-only behaviour: analytics, contact delivery and Preview restrictions. */
|
|
9
|
+
readonly environment: BrandlabDeploymentEnvironment;
|
|
10
|
+
/**
|
|
11
|
+
* An optimised build (`NODE_ENV=production`), on Vercel or locally. Security
|
|
12
|
+
* headers and crawler access follow the build so a local `next start` serves
|
|
13
|
+
* exactly the production headers.
|
|
14
|
+
*/
|
|
15
|
+
readonly productionBuild: boolean;
|
|
16
|
+
}
|
|
17
|
+
export type BrandlabEnvironment = Readonly<Record<string, string | undefined>>;
|
|
18
|
+
/**
|
|
19
|
+
* Resolves the deployment once for every consumer (next.config headers,
|
|
20
|
+
* robots, analytics and contact delivery). Server code passes `process.env`;
|
|
21
|
+
* browser code passes `{ NEXT_PUBLIC_VERCEL_ENV: process.env.NEXT_PUBLIC_VERCEL_ENV }`,
|
|
22
|
+
* which `defineBrandlabNextConfig` inlines at build time.
|
|
23
|
+
*
|
|
24
|
+
* `VERCEL_ENV` wins over `NEXT_PUBLIC_VERCEL_ENV`. An unrecognised declared
|
|
25
|
+
* environment fails closed to `preview`: never analytics, never indexable.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveBrandlabDeployment(environment: BrandlabEnvironment): BrandlabDeployment;
|
|
28
|
+
export interface BrandlabSiteDefinition {
|
|
29
|
+
/** Public site name: Open Graph `siteName` and the brand guard for templated titles. */
|
|
30
|
+
name: string;
|
|
31
|
+
/** Canonical HTTPS origin written as a source constant, never read from the environment. */
|
|
32
|
+
canonicalUrl: string;
|
|
33
|
+
/** Open Graph locale, for example `en_AU`. */
|
|
34
|
+
locale: string;
|
|
35
|
+
/** Page title template with exactly one `%s`, for example `%s | Example Services`. */
|
|
36
|
+
titleTemplate: string;
|
|
37
|
+
defaultSocialImage: BrandlabSocialImage;
|
|
38
|
+
/**
|
|
39
|
+
* Hosts that permanently redirect every path to the canonical origin, such as
|
|
40
|
+
* the `www` host, the Vercel project host or a retired domain.
|
|
41
|
+
*/
|
|
42
|
+
aliasHosts: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
export interface BrandlabSite {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
/** Canonical origin without a trailing slash, for example `https://example.com.au`. */
|
|
47
|
+
readonly canonicalUrl: string;
|
|
48
|
+
readonly canonicalHost: string;
|
|
49
|
+
readonly locale: string;
|
|
50
|
+
readonly titleTemplate: string;
|
|
51
|
+
readonly defaultSocialImage: Readonly<BrandlabSocialImage>;
|
|
52
|
+
readonly aliasHosts: readonly string[];
|
|
53
|
+
}
|
|
54
|
+
export declare function isBrandlabHostname(value: unknown): value is string;
|
|
55
|
+
/**
|
|
56
|
+
* Declares the site-wide SEO identity once (`src/config/site.ts`). The result
|
|
57
|
+
* feeds metadata, robots, sitemaps, redirects and next.config.
|
|
58
|
+
*/
|
|
59
|
+
export declare function defineBrandlabSite(definition: BrandlabSiteDefinition): BrandlabSite;
|
|
60
|
+
//# sourceMappingURL=site.d.ts.map
|
|
@@ -0,0 +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;;;OAGG;IACH,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;CAC9B;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,CA4CnF"}
|
package/dist/site.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { assertBrandlabSocialImage, } from './metadata.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the deployment once for every consumer (next.config headers,
|
|
4
|
+
* robots, analytics and contact delivery). Server code passes `process.env`;
|
|
5
|
+
* browser code passes `{ NEXT_PUBLIC_VERCEL_ENV: process.env.NEXT_PUBLIC_VERCEL_ENV }`,
|
|
6
|
+
* which `defineBrandlabNextConfig` inlines at build time.
|
|
7
|
+
*
|
|
8
|
+
* `VERCEL_ENV` wins over `NEXT_PUBLIC_VERCEL_ENV`. An unrecognised declared
|
|
9
|
+
* environment fails closed to `preview`: never analytics, never indexable.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveBrandlabDeployment(environment) {
|
|
12
|
+
if (!environment || typeof environment !== 'object') {
|
|
13
|
+
throw new TypeError('resolveBrandlabDeployment requires an environment record.');
|
|
14
|
+
}
|
|
15
|
+
const declared = (environment.VERCEL_ENV ?? environment.NEXT_PUBLIC_VERCEL_ENV)?.trim();
|
|
16
|
+
const deploymentEnvironment = !declared || declared === 'development'
|
|
17
|
+
? 'development'
|
|
18
|
+
: declared === 'production'
|
|
19
|
+
? 'production'
|
|
20
|
+
: 'preview';
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
environment: deploymentEnvironment,
|
|
23
|
+
productionBuild: environment.NODE_ENV === 'production',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const SITE_KEYS = new Set([
|
|
27
|
+
'name',
|
|
28
|
+
'canonicalUrl',
|
|
29
|
+
'locale',
|
|
30
|
+
'titleTemplate',
|
|
31
|
+
'defaultSocialImage',
|
|
32
|
+
'aliasHosts',
|
|
33
|
+
]);
|
|
34
|
+
const HOSTNAME_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$/;
|
|
35
|
+
function assertSiteText(value, field, maximum) {
|
|
36
|
+
if (typeof value !== 'string' ||
|
|
37
|
+
!value.trim() ||
|
|
38
|
+
value.trim() !== value ||
|
|
39
|
+
value.length > maximum ||
|
|
40
|
+
/[\u0000-\u001f\u007f]/.test(value)) {
|
|
41
|
+
throw new Error(`${field} must be bounded, trimmed, single-line text.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function isBrandlabHostname(value) {
|
|
45
|
+
return typeof value === 'string' && HOSTNAME_PATTERN.test(value);
|
|
46
|
+
}
|
|
47
|
+
function canonicalOrigin(value) {
|
|
48
|
+
assertSiteText(value, 'canonicalUrl', 2_048);
|
|
49
|
+
let url;
|
|
50
|
+
try {
|
|
51
|
+
url = new URL(value);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error(`canonicalUrl must be an absolute HTTPS origin: "${value}".`);
|
|
55
|
+
}
|
|
56
|
+
if (url.protocol !== 'https:' ||
|
|
57
|
+
url.username ||
|
|
58
|
+
url.password ||
|
|
59
|
+
url.port ||
|
|
60
|
+
url.search ||
|
|
61
|
+
url.hash ||
|
|
62
|
+
(url.pathname !== '/' && url.pathname !== '') ||
|
|
63
|
+
value.replace(/\/$/, '') !== url.origin ||
|
|
64
|
+
!isBrandlabHostname(url.hostname)) {
|
|
65
|
+
throw new Error(`canonicalUrl must be an absolute HTTPS origin: "${value}".`);
|
|
66
|
+
}
|
|
67
|
+
return url;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Declares the site-wide SEO identity once (`src/config/site.ts`). The result
|
|
71
|
+
* feeds metadata, robots, sitemaps, redirects and next.config.
|
|
72
|
+
*/
|
|
73
|
+
export function defineBrandlabSite(definition) {
|
|
74
|
+
if (!definition || typeof definition !== 'object' || Array.isArray(definition)) {
|
|
75
|
+
throw new TypeError('defineBrandlabSite requires a site definition.');
|
|
76
|
+
}
|
|
77
|
+
const unsupported = Object.keys(definition).filter((key) => !SITE_KEYS.has(key));
|
|
78
|
+
if (unsupported.length > 0) {
|
|
79
|
+
throw new Error(`Site definition contains unsupported keys: ${unsupported.join(', ')}.`);
|
|
80
|
+
}
|
|
81
|
+
assertSiteText(definition.name, 'name', 160);
|
|
82
|
+
const origin = canonicalOrigin(definition.canonicalUrl);
|
|
83
|
+
assertSiteText(definition.locale, 'locale', 20);
|
|
84
|
+
if (!/^[a-z]{2}(?:_[A-Z]{2})?$/.test(definition.locale)) {
|
|
85
|
+
throw new Error('locale must use a language or language_REGION identifier.');
|
|
86
|
+
}
|
|
87
|
+
assertSiteText(definition.titleTemplate, 'titleTemplate', 240);
|
|
88
|
+
if ((definition.titleTemplate.match(/%s/g) ?? []).length !== 1) {
|
|
89
|
+
throw new Error('titleTemplate must contain exactly one %s placeholder.');
|
|
90
|
+
}
|
|
91
|
+
assertBrandlabSocialImage(definition.defaultSocialImage, 'defaultSocialImage');
|
|
92
|
+
if (!Array.isArray(definition.aliasHosts)) {
|
|
93
|
+
throw new Error('aliasHosts must list every host that redirects to the canonical origin.');
|
|
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}".`);
|
|
106
|
+
return Object.freeze({
|
|
107
|
+
name: definition.name,
|
|
108
|
+
canonicalUrl: origin.origin,
|
|
109
|
+
canonicalHost: origin.hostname,
|
|
110
|
+
locale: definition.locale,
|
|
111
|
+
titleTemplate: definition.titleTemplate,
|
|
112
|
+
defaultSocialImage: Object.freeze({ ...definition.defaultSocialImage }),
|
|
113
|
+
aliasHosts: Object.freeze([...definition.aliasHosts]),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=site.js.map
|
package/dist/site.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
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 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"]}
|
package/dist/sitemaps.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BrandlabRouteCatalog } from './routes.js';
|
|
1
2
|
export interface SitemapEntry {
|
|
2
3
|
url: string;
|
|
3
4
|
lastModified?: string | Date;
|
|
@@ -7,6 +8,10 @@ export interface SitemapChunk {
|
|
|
7
8
|
entries: readonly SitemapEntry[];
|
|
8
9
|
}
|
|
9
10
|
export declare function escapeXml(value: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Exactly the headers Next 16's metadata `sitemap.ts` route sends, so a route
|
|
13
|
+
* served by the shared sitemap helper is indistinguishable from it.
|
|
14
|
+
*/
|
|
10
15
|
export declare function getSitemapXmlHeaders(): {
|
|
11
16
|
'content-type': string;
|
|
12
17
|
'cache-control': string;
|
|
@@ -24,11 +29,6 @@ export declare function filterSitemapIndexPaths(groups: readonly {
|
|
|
24
29
|
entryCount: number;
|
|
25
30
|
includeWhenEmpty?: boolean;
|
|
26
31
|
}[]): string[];
|
|
27
|
-
export declare function shouldIndexSite(options: {
|
|
28
|
-
nodeEnv?: string;
|
|
29
|
-
vercelEnv?: string;
|
|
30
|
-
allowIndexing?: string;
|
|
31
|
-
}): boolean;
|
|
32
32
|
/**
|
|
33
33
|
* Builds canonical sitemap entries from the route catalog. It deliberately
|
|
34
34
|
* omits priority and changeFrequency because search engines do not use those
|
|
@@ -37,5 +37,4 @@ export declare function shouldIndexSite(options: {
|
|
|
37
37
|
export declare function buildSitemapEntries(routes: BrandlabRouteCatalog, options: {
|
|
38
38
|
siteUrl: string;
|
|
39
39
|
}): SitemapEntry[];
|
|
40
|
-
import type { BrandlabRouteCatalog } from './routes.js';
|
|
41
40
|
//# sourceMappingURL=sitemaps.d.ts.map
|
package/dist/sitemaps.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sitemaps.d.ts","sourceRoot":"","sources":["../src/sitemaps.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sitemaps.d.ts","sourceRoot":"","sources":["../src/sitemaps.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAGvD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;CACjC;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO/C;AA+BD;;;GAGG;AACH,wBAAgB,oBAAoB;;;EAKnC;AAKD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CAezE;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,OAAO,EAAE;IAAE,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAAE,GACjD,MAAM,CAQR;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,OAAO,EAAE;IACP,UAAU,EAAE,MAAM,CAAA;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,GACA,YAAY,EAAE,CAqBhB;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAE,EAAE,YAKpF;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAC3B,YAAY,EAAE,CAmChB"}
|
package/dist/sitemaps.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getAbsoluteUrl } from './metadata.js';
|
|
2
|
+
import { normalizeBrandlabRoutePath, resolveBrandlabRouteIndexing } from './routes.js';
|
|
1
3
|
export function escapeXml(value) {
|
|
2
4
|
return value
|
|
3
5
|
.replace(/&/g, '&')
|
|
@@ -6,13 +8,23 @@ export function escapeXml(value) {
|
|
|
6
8
|
.replace(/"/g, '"')
|
|
7
9
|
.replace(/'/g, ''');
|
|
8
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* A `lastmod` exactly as Next's metadata `sitemap.ts` writes it: a string is
|
|
13
|
+
* kept verbatim (a `YYYY-MM-DD` date stays date-only) and only a `Date` becomes
|
|
14
|
+
* an ISO timestamp.
|
|
15
|
+
*/
|
|
9
16
|
function formatLastModified(value) {
|
|
10
17
|
if (!value)
|
|
11
18
|
return null;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
if (value instanceof Date) {
|
|
20
|
+
if (Number.isNaN(value.getTime()))
|
|
21
|
+
throw new Error('Sitemap lastModified must be a valid date.');
|
|
22
|
+
return value.toISOString();
|
|
23
|
+
}
|
|
24
|
+
if (typeof value !== 'string' || value.trim() !== value || Number.isNaN(Date.parse(value))) {
|
|
25
|
+
throw new Error(`Sitemap lastModified must be a valid date: "${String(value)}".`);
|
|
26
|
+
}
|
|
27
|
+
return escapeXml(value);
|
|
16
28
|
}
|
|
17
29
|
function assertSitemapUrl(value, field) {
|
|
18
30
|
let url;
|
|
@@ -26,55 +38,42 @@ function assertSitemapUrl(value, field) {
|
|
|
26
38
|
throw new Error(`${field} must be an absolute HTTPS URL without credentials or a hash.`);
|
|
27
39
|
}
|
|
28
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Exactly the headers Next 16's metadata `sitemap.ts` route sends, so a route
|
|
43
|
+
* served by the shared sitemap helper is indistinguishable from it.
|
|
44
|
+
*/
|
|
29
45
|
export function getSitemapXmlHeaders() {
|
|
30
46
|
return {
|
|
31
|
-
'content-type': 'application/xml
|
|
32
|
-
'cache-control': 'public, max-age=
|
|
47
|
+
'content-type': 'application/xml',
|
|
48
|
+
'cache-control': 'public, max-age=0, must-revalidate',
|
|
33
49
|
};
|
|
34
50
|
}
|
|
51
|
+
const XML_DECLARATION = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
52
|
+
const SITEMAP_NAMESPACE = 'http://www.sitemaps.org/schemas/sitemap/0.9';
|
|
35
53
|
export function serializeSitemap(entries) {
|
|
36
54
|
entries.forEach((entry, index) => assertSitemapUrl(entry.url, `entries[${index}].url`));
|
|
37
55
|
const duplicate = entries.find((entry, index) => entries.slice(0, index).some((candidate) => candidate.url === entry.url));
|
|
38
56
|
if (duplicate)
|
|
39
57
|
throw new Error(`Sitemap contains duplicate URL "${duplicate.url}".`);
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
// Byte-for-byte the layout of Next's metadata sitemap (resolveSitemap).
|
|
59
|
+
let xml = `${XML_DECLARATION}<urlset xmlns="${SITEMAP_NAMESPACE}">\n`;
|
|
60
|
+
for (const entry of entries) {
|
|
42
61
|
const lastModified = formatLastModified(entry.lastModified);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.filter(Boolean)
|
|
50
|
-
.join('\n');
|
|
51
|
-
})
|
|
52
|
-
.join('\n');
|
|
53
|
-
return [
|
|
54
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
55
|
-
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
56
|
-
urls,
|
|
57
|
-
'</urlset>',
|
|
58
|
-
].join('\n');
|
|
62
|
+
xml += `<url>\n<loc>${escapeXml(entry.url)}</loc>\n`;
|
|
63
|
+
if (lastModified)
|
|
64
|
+
xml += `<lastmod>${lastModified}</lastmod>\n`;
|
|
65
|
+
xml += '</url>\n';
|
|
66
|
+
}
|
|
67
|
+
return `${xml}</urlset>\n`;
|
|
59
68
|
}
|
|
60
69
|
export function serializeSitemapIndex(paths, options) {
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
let xml = `${XML_DECLARATION}<sitemapindex xmlns="${SITEMAP_NAMESPACE}">\n`;
|
|
71
|
+
paths.forEach((path, index) => {
|
|
63
72
|
const url = options.absoluteUrl(path);
|
|
64
73
|
assertSitemapUrl(url, `paths[${index}]`);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
' </sitemap>',
|
|
69
|
-
].join('\n');
|
|
70
|
-
})
|
|
71
|
-
.join('\n');
|
|
72
|
-
return [
|
|
73
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
74
|
-
'<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
75
|
-
entries,
|
|
76
|
-
'</sitemapindex>',
|
|
77
|
-
].join('\n');
|
|
74
|
+
xml += `<sitemap>\n<loc>${escapeXml(url)}</loc>\n</sitemap>\n`;
|
|
75
|
+
});
|
|
76
|
+
return `${xml}</sitemapindex>\n`;
|
|
78
77
|
}
|
|
79
78
|
export function createSitemapChunks(entries, options) {
|
|
80
79
|
const maxUrlsPerSitemap = options.maxUrlsPerSitemap ?? 500;
|
|
@@ -99,13 +98,6 @@ export function filterSitemapIndexPaths(groups) {
|
|
|
99
98
|
.filter((group) => group.includeWhenEmpty || group.entryCount > 0)
|
|
100
99
|
.map((group) => group.path);
|
|
101
100
|
}
|
|
102
|
-
export function shouldIndexSite(options) {
|
|
103
|
-
// A Vercel environment declaration is authoritative: preview and
|
|
104
|
-
// development deployments cannot be made indexable by a drifting override.
|
|
105
|
-
if (options.vercelEnv)
|
|
106
|
-
return options.vercelEnv === 'production';
|
|
107
|
-
return options.nodeEnv === 'production' || options.allowIndexing === '1';
|
|
108
|
-
}
|
|
109
101
|
/**
|
|
110
102
|
* Builds canonical sitemap entries from the route catalog. It deliberately
|
|
111
103
|
* omits priority and changeFrequency because search engines do not use those
|
|
@@ -139,6 +131,4 @@ export function buildSitemapEntries(routes, options) {
|
|
|
139
131
|
throw new Error(`Sitemap contains duplicate canonical URL "${duplicate.url}".`);
|
|
140
132
|
return entries;
|
|
141
133
|
}
|
|
142
|
-
import { getAbsoluteUrl } from './metadata.js';
|
|
143
|
-
import { normalizeBrandlabRoutePath, resolveBrandlabRouteIndexing } from './routes.js';
|
|
144
134
|
//# sourceMappingURL=sitemaps.js.map
|
package/dist/sitemaps.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sitemaps.js","sourceRoot":"","sources":["../src/sitemaps.ts"],"names":[],"mappings":"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","sourcesContent":["export interface SitemapEntry {\n url: string\n lastModified?: string | Date\n}\n\nexport interface SitemapChunk {\n path: string\n entries: readonly SitemapEntry[]\n}\n\nexport function escapeXml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\nfunction formatLastModified(value: SitemapEntry['lastModified']) {\n if (!value) return null\n const parsed = value instanceof Date ? value : new Date(value)\n if (Number.isNaN(parsed.getTime())) throw new Error('Sitemap lastModified must be a valid date.')\n return parsed.toISOString()\n}\n\nfunction assertSitemapUrl(value: string, field: string) {\n let url: URL\n try {\n url = new URL(value)\n } catch {\n throw new Error(`${field} must be an absolute HTTPS URL.`)\n }\n if (url.protocol !== 'https:' || url.username || url.password || url.hash) {\n throw new Error(`${field} must be an absolute HTTPS URL without credentials or a hash.`)\n }\n}\n\nexport function getSitemapXmlHeaders() {\n return {\n 'content-type': 'application/xml; charset=utf-8',\n 'cache-control': 'public, max-age=3600, s-maxage=3600',\n }\n}\n\nexport function serializeSitemap(entries: readonly SitemapEntry[]): string {\n entries.forEach((entry, index) => assertSitemapUrl(entry.url, `entries[${index}].url`))\n const duplicate = entries.find((entry, index) =>\n entries.slice(0, index).some((candidate) => candidate.url === entry.url),\n )\n if (duplicate) throw new Error(`Sitemap contains duplicate URL \"${duplicate.url}\".`)\n const urls = entries\n .map((entry) => {\n const lastModified = formatLastModified(entry.lastModified)\n\n return [\n ' <url>',\n ` <loc>${escapeXml(entry.url)}</loc>`,\n lastModified ? ` <lastmod>${lastModified}</lastmod>` : null,\n ' </url>',\n ]\n .filter(Boolean)\n .join('\\n')\n })\n .join('\\n')\n\n return [\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n '<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">',\n urls,\n '</urlset>',\n ].join('\\n')\n}\n\nexport function serializeSitemapIndex(\n paths: readonly string[],\n options: { absoluteUrl: (path: string) => string },\n): string {\n const entries = paths\n .map((path, index) => {\n const url = options.absoluteUrl(path)\n assertSitemapUrl(url, `paths[${index}]`)\n return [\n ' <sitemap>',\n ` <loc>${escapeXml(url)}</loc>`,\n ' </sitemap>',\n ].join('\\n')\n })\n .join('\\n')\n\n return [\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n '<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">',\n entries,\n '</sitemapindex>',\n ].join('\\n')\n}\n\nexport function createSitemapChunks(\n entries: readonly SitemapEntry[],\n options: {\n pathPrefix: string\n maxUrlsPerSitemap?: number\n },\n): SitemapChunk[] {\n const maxUrlsPerSitemap = options.maxUrlsPerSitemap ?? 500\n\n if (!Number.isInteger(maxUrlsPerSitemap) || maxUrlsPerSitemap < 1 || maxUrlsPerSitemap > 50_000) {\n throw new Error('maxUrlsPerSitemap must be an integer between 1 and 50000.')\n }\n\n const pathPrefix = options.pathPrefix.startsWith('/')\n ? options.pathPrefix\n : `/${options.pathPrefix}`\n const normalizedPrefix = pathPrefix.replace(/\\.xml$/, '')\n const chunks: SitemapChunk[] = []\n\n for (let index = 0; index < entries.length; index += maxUrlsPerSitemap) {\n chunks.push({\n path: `${normalizedPrefix}-${chunks.length + 1}.xml`,\n entries: entries.slice(index, index + maxUrlsPerSitemap),\n })\n }\n\n return chunks\n}\n\nexport function filterSitemapIndexPaths(\n groups: readonly { path: string; entryCount: number; includeWhenEmpty?: boolean }[],\n) {\n return groups\n .filter((group) => group.includeWhenEmpty || group.entryCount > 0)\n .map((group) => group.path)\n}\n\nexport function shouldIndexSite(options: {\n nodeEnv?: string\n vercelEnv?: string\n allowIndexing?: string\n}) {\n // A Vercel environment declaration is authoritative: preview and\n // development deployments cannot be made indexable by a drifting override.\n if (options.vercelEnv) return options.vercelEnv === 'production'\n return options.nodeEnv === 'production' || options.allowIndexing === '1'\n}\n\n/**\n * Builds canonical sitemap entries from the route catalog. It deliberately\n * omits priority and changeFrequency because search engines do not use those\n * values as reliable crawl signals.\n */\nexport function buildSitemapEntries(\n routes: BrandlabRouteCatalog,\n options: { siteUrl: string },\n): SitemapEntry[] {\n const siteUrl = new URL(options.siteUrl)\n if (\n siteUrl.protocol !== 'https:' ||\n siteUrl.username ||\n siteUrl.password ||\n siteUrl.pathname !== '/' ||\n siteUrl.search ||\n siteUrl.hash\n ) {\n throw new Error('siteUrl must be an absolute HTTPS origin.')\n }\n\n const entries = routes\n .filter((route) => resolveBrandlabRouteIndexing(route) === 'index')\n .map((route) => {\n const path = normalizeBrandlabRoutePath(route.canonicalPath ?? route.path)\n const lastModified = route.lastSignificantChange\n\n if (lastModified && Number.isNaN(Date.parse(lastModified))) {\n throw new Error(`Route \"${route.id}\" has an invalid lastSignificantChange date.`)\n }\n\n return {\n url: getAbsoluteUrl(siteUrl.origin, path),\n ...(lastModified ? { lastModified } : {}),\n }\n })\n\n const duplicate = entries.find((entry, index) =>\n entries.slice(0, index).some((candidate) => candidate.url === entry.url),\n )\n if (duplicate) throw new Error(`Sitemap contains duplicate canonical URL \"${duplicate.url}\".`)\n\n return entries\n}\nimport { getAbsoluteUrl } from './metadata.js'\nimport type { BrandlabRouteCatalog } from './routes.js'\nimport { normalizeBrandlabRoutePath, resolveBrandlabRouteIndexing } from './routes.js'\n"]}
|
|
1
|
+
{"version":3,"file":"sitemaps.js","sourceRoot":"","sources":["../src/sitemaps.ts"],"names":[],"mappings":"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","sourcesContent":["import { getAbsoluteUrl } from './metadata.js'\nimport type { BrandlabRouteCatalog } from './routes.js'\nimport { normalizeBrandlabRoutePath, resolveBrandlabRouteIndexing } from './routes.js'\n\nexport interface SitemapEntry {\n url: string\n lastModified?: string | Date\n}\n\nexport interface SitemapChunk {\n path: string\n entries: readonly SitemapEntry[]\n}\n\nexport function escapeXml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\n/**\n * A `lastmod` exactly as Next's metadata `sitemap.ts` writes it: a string is\n * kept verbatim (a `YYYY-MM-DD` date stays date-only) and only a `Date` becomes\n * an ISO timestamp.\n */\nfunction formatLastModified(value: SitemapEntry['lastModified']) {\n if (!value) return null\n if (value instanceof Date) {\n if (Number.isNaN(value.getTime())) throw new Error('Sitemap lastModified must be a valid date.')\n return value.toISOString()\n }\n if (typeof value !== 'string' || value.trim() !== value || Number.isNaN(Date.parse(value))) {\n throw new Error(`Sitemap lastModified must be a valid date: \"${String(value)}\".`)\n }\n return escapeXml(value)\n}\n\nfunction assertSitemapUrl(value: string, field: string) {\n let url: URL\n try {\n url = new URL(value)\n } catch {\n throw new Error(`${field} must be an absolute HTTPS URL.`)\n }\n if (url.protocol !== 'https:' || url.username || url.password || url.hash) {\n throw new Error(`${field} must be an absolute HTTPS URL without credentials or a hash.`)\n }\n}\n\n/**\n * Exactly the headers Next 16's metadata `sitemap.ts` route sends, so a route\n * served by the shared sitemap helper is indistinguishable from it.\n */\nexport function getSitemapXmlHeaders() {\n return {\n 'content-type': 'application/xml',\n 'cache-control': 'public, max-age=0, must-revalidate',\n }\n}\n\nconst XML_DECLARATION = '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n'\nconst SITEMAP_NAMESPACE = 'http://www.sitemaps.org/schemas/sitemap/0.9'\n\nexport function serializeSitemap(entries: readonly SitemapEntry[]): string {\n entries.forEach((entry, index) => assertSitemapUrl(entry.url, `entries[${index}].url`))\n const duplicate = entries.find((entry, index) =>\n entries.slice(0, index).some((candidate) => candidate.url === entry.url),\n )\n if (duplicate) throw new Error(`Sitemap contains duplicate URL \"${duplicate.url}\".`)\n // Byte-for-byte the layout of Next's metadata sitemap (resolveSitemap).\n let xml = `${XML_DECLARATION}<urlset xmlns=\"${SITEMAP_NAMESPACE}\">\\n`\n for (const entry of entries) {\n const lastModified = formatLastModified(entry.lastModified)\n xml += `<url>\\n<loc>${escapeXml(entry.url)}</loc>\\n`\n if (lastModified) xml += `<lastmod>${lastModified}</lastmod>\\n`\n xml += '</url>\\n'\n }\n return `${xml}</urlset>\\n`\n}\n\nexport function serializeSitemapIndex(\n paths: readonly string[],\n options: { absoluteUrl: (path: string) => string },\n): string {\n let xml = `${XML_DECLARATION}<sitemapindex xmlns=\"${SITEMAP_NAMESPACE}\">\\n`\n paths.forEach((path, index) => {\n const url = options.absoluteUrl(path)\n assertSitemapUrl(url, `paths[${index}]`)\n xml += `<sitemap>\\n<loc>${escapeXml(url)}</loc>\\n</sitemap>\\n`\n })\n return `${xml}</sitemapindex>\\n`\n}\n\nexport function createSitemapChunks(\n entries: readonly SitemapEntry[],\n options: {\n pathPrefix: string\n maxUrlsPerSitemap?: number\n },\n): SitemapChunk[] {\n const maxUrlsPerSitemap = options.maxUrlsPerSitemap ?? 500\n\n if (!Number.isInteger(maxUrlsPerSitemap) || maxUrlsPerSitemap < 1 || maxUrlsPerSitemap > 50_000) {\n throw new Error('maxUrlsPerSitemap must be an integer between 1 and 50000.')\n }\n\n const pathPrefix = options.pathPrefix.startsWith('/')\n ? options.pathPrefix\n : `/${options.pathPrefix}`\n const normalizedPrefix = pathPrefix.replace(/\\.xml$/, '')\n const chunks: SitemapChunk[] = []\n\n for (let index = 0; index < entries.length; index += maxUrlsPerSitemap) {\n chunks.push({\n path: `${normalizedPrefix}-${chunks.length + 1}.xml`,\n entries: entries.slice(index, index + maxUrlsPerSitemap),\n })\n }\n\n return chunks\n}\n\nexport function filterSitemapIndexPaths(\n groups: readonly { path: string; entryCount: number; includeWhenEmpty?: boolean }[],\n) {\n return groups\n .filter((group) => group.includeWhenEmpty || group.entryCount > 0)\n .map((group) => group.path)\n}\n\n/**\n * Builds canonical sitemap entries from the route catalog. It deliberately\n * omits priority and changeFrequency because search engines do not use those\n * values as reliable crawl signals.\n */\nexport function buildSitemapEntries(\n routes: BrandlabRouteCatalog,\n options: { siteUrl: string },\n): SitemapEntry[] {\n const siteUrl = new URL(options.siteUrl)\n if (\n siteUrl.protocol !== 'https:' ||\n siteUrl.username ||\n siteUrl.password ||\n siteUrl.pathname !== '/' ||\n siteUrl.search ||\n siteUrl.hash\n ) {\n throw new Error('siteUrl must be an absolute HTTPS origin.')\n }\n\n const entries = routes\n .filter((route) => resolveBrandlabRouteIndexing(route) === 'index')\n .map((route) => {\n const path = normalizeBrandlabRoutePath(route.canonicalPath ?? route.path)\n const lastModified = route.lastSignificantChange\n\n if (lastModified && Number.isNaN(Date.parse(lastModified))) {\n throw new Error(`Route \"${route.id}\" has an invalid lastSignificantChange date.`)\n }\n\n return {\n url: getAbsoluteUrl(siteUrl.origin, path),\n ...(lastModified ? { lastModified } : {}),\n }\n })\n\n const duplicate = entries.find((entry, index) =>\n entries.slice(0, index).some((candidate) => candidate.url === entry.url),\n )\n if (duplicate) throw new Error(`Sitemap contains duplicate canonical URL \"${duplicate.url}\".`)\n\n return entries\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brandlab-digital/site-core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/RomieDandan/brandlab.git",
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
"types": "./dist/project.d.ts",
|
|
27
27
|
"import": "./dist/project.js"
|
|
28
28
|
},
|
|
29
|
+
"./site": {
|
|
30
|
+
"types": "./dist/site.d.ts",
|
|
31
|
+
"import": "./dist/site.js",
|
|
32
|
+
"require": "./dist/site.js"
|
|
33
|
+
},
|
|
29
34
|
"./business": {
|
|
30
35
|
"types": "./dist/business.d.ts",
|
|
31
36
|
"import": "./dist/business.js"
|
|
@@ -34,6 +39,11 @@
|
|
|
34
39
|
"types": "./dist/routes.d.ts",
|
|
35
40
|
"import": "./dist/routes.js"
|
|
36
41
|
},
|
|
42
|
+
"./redirects": {
|
|
43
|
+
"types": "./dist/redirects.d.ts",
|
|
44
|
+
"import": "./dist/redirects.js",
|
|
45
|
+
"require": "./dist/redirects.js"
|
|
46
|
+
},
|
|
37
47
|
"./seo-page": {
|
|
38
48
|
"types": "./dist/seo-page.d.ts",
|
|
39
49
|
"import": "./dist/seo-page.js"
|