@brandlab-digital/site-core 2.1.0 → 3.1.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/metadata.js
CHANGED
|
@@ -13,7 +13,7 @@ function assertMetadataText(value, field, maximum) {
|
|
|
13
13
|
throw new Error(`${field} must be bounded, trimmed, single-line text.`);
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
function
|
|
16
|
+
export function assertBrandlabSocialImage(image, field) {
|
|
17
17
|
if (!image || typeof image !== 'object' || Array.isArray(image)) {
|
|
18
18
|
throw new Error(`${field} must be a social image record.`);
|
|
19
19
|
}
|
|
@@ -36,6 +36,32 @@ function assertSocialImage(image, field) {
|
|
|
36
36
|
if (image.alt !== undefined)
|
|
37
37
|
assertMetadataText(image.alt, `${field}.alt`, 300);
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Validate keywords in either form without changing them. A string is one trimmed,
|
|
41
|
+
* single-line value whose comma-separated entries follow the list rules: at most 20
|
|
42
|
+
* distinct (case-insensitive) values of up to 100 characters.
|
|
43
|
+
*/
|
|
44
|
+
export function assertBrandlabKeywords(keywords, field = 'keywords') {
|
|
45
|
+
let values;
|
|
46
|
+
if (typeof keywords === 'string') {
|
|
47
|
+
assertMetadataText(keywords, field, 2_100);
|
|
48
|
+
values = keywords.split(',').map((keyword) => keyword.trim());
|
|
49
|
+
}
|
|
50
|
+
else if (Array.isArray(keywords)) {
|
|
51
|
+
values = keywords;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
throw new Error(`${field} must be a comma-separated string or a list of reviewed values.`);
|
|
55
|
+
}
|
|
56
|
+
if (values.length > 20) {
|
|
57
|
+
throw new Error(`${field} must contain no more than 20 reviewed values.`);
|
|
58
|
+
}
|
|
59
|
+
values.forEach((keyword, index) => assertMetadataText(keyword, `${field}[${index}]`, 100));
|
|
60
|
+
const normalized = values.map((keyword) => String(keyword).toLowerCase());
|
|
61
|
+
if (new Set(normalized).size !== normalized.length) {
|
|
62
|
+
throw new Error(`${field} must not contain duplicate values.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
39
65
|
function assertMetadataDate(value, field) {
|
|
40
66
|
if (!/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z)?$/.test(value) ||
|
|
41
67
|
Number.isNaN(Date.parse(value))) {
|
|
@@ -60,19 +86,11 @@ function assertBrandlabMetadataInput(input) {
|
|
|
60
86
|
if (input.pathname.includes('?') || input.pathname.includes('#')) {
|
|
61
87
|
throw new Error('pathname must not contain a query string or hash.');
|
|
62
88
|
}
|
|
63
|
-
|
|
89
|
+
assertBrandlabSocialImage(input.defaultSocialImage, 'defaultSocialImage');
|
|
64
90
|
if (input.socialImage)
|
|
65
|
-
|
|
66
|
-
if (input.keywords)
|
|
67
|
-
|
|
68
|
-
throw new Error('keywords must contain no more than 20 reviewed values.');
|
|
69
|
-
}
|
|
70
|
-
input.keywords.forEach((keyword, index) => assertMetadataText(keyword, `keywords[${index}]`, 100));
|
|
71
|
-
const normalized = input.keywords.map((keyword) => keyword.toLowerCase());
|
|
72
|
-
if (new Set(normalized).size !== normalized.length) {
|
|
73
|
-
throw new Error('keywords must not contain duplicate values.');
|
|
74
|
-
}
|
|
75
|
-
}
|
|
91
|
+
assertBrandlabSocialImage(input.socialImage, 'socialImage');
|
|
92
|
+
if (input.keywords !== undefined)
|
|
93
|
+
assertBrandlabKeywords(input.keywords);
|
|
76
94
|
if (input.article) {
|
|
77
95
|
if (!input.article.publishedTime) {
|
|
78
96
|
throw new Error('article.publishedTime is required for article metadata.');
|
|
@@ -162,7 +180,7 @@ function normalizeSocialImageUrl(siteUrl, imageUrl) {
|
|
|
162
180
|
}
|
|
163
181
|
return resolved.href;
|
|
164
182
|
}
|
|
165
|
-
|
|
183
|
+
function buildSeoMetadata(input) {
|
|
166
184
|
const titleMode = input.titleMode ?? 'templated';
|
|
167
185
|
const pageTitle = normalizeMetadataTitle(input.title, {
|
|
168
186
|
brandName: input.brandName,
|
|
@@ -208,7 +226,9 @@ export function buildSeoMetadata(input) {
|
|
|
208
226
|
alternates: {
|
|
209
227
|
canonical: canonicalUrl,
|
|
210
228
|
},
|
|
211
|
-
...(input.keywords && input.keywords.length > 0
|
|
229
|
+
...(input.keywords !== undefined && input.keywords.length > 0
|
|
230
|
+
? { keywords: typeof input.keywords === 'string' ? input.keywords : [...input.keywords] }
|
|
231
|
+
: {}),
|
|
212
232
|
...(robots ? { robots } : {}),
|
|
213
233
|
openGraph: {
|
|
214
234
|
title: resolvedTitle,
|
|
@@ -232,6 +252,7 @@ export function buildSeoMetadata(input) {
|
|
|
232
252
|
},
|
|
233
253
|
};
|
|
234
254
|
}
|
|
255
|
+
/** The one metadata builder: validates every input, then resolves canonical, robots and social output. */
|
|
235
256
|
export function buildBrandlabMetadata(input) {
|
|
236
257
|
assertBrandlabMetadataInput(input);
|
|
237
258
|
return buildSeoMetadata(input);
|
package/dist/metadata.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n normalizeBrandlabRoutePath,\n type BrandlabIndexingDirective,\n} from './routes.js'\n\nexport type BrandlabMetadataTitleMode = 'templated' | 'absolute'\n\nexport interface BrandlabSocialImage {\n url: string\n twitterUrl?: string\n width?: number\n height?: number\n alt?: string\n}\n\nexport interface BuildSeoMetadataInput {\n brandName: string\n siteUrl: string\n locale: string\n titleTemplate: string\n defaultSocialImage: BrandlabSocialImage\n title: string\n description: string\n pathname: string\n titleMode?: BrandlabMetadataTitleMode\n indexing: BrandlabIndexingDirective\n keywords?: string[]\n article?: {\n publishedTime?: string\n modifiedTime?: string\n authors?: string[]\n }\n socialImage?: BrandlabSocialImage\n}\n\nexport type BuildBrandlabMetadataInput = BuildSeoMetadataInput\n\nexport interface BrandlabSeoMetadata {\n title: string | { absolute: string }\n resolvedTitle: string\n description: string\n canonicalPath: string\n canonicalUrl: string\n alternates: { canonical: string }\n keywords?: string[]\n robots?: {\n index: boolean\n follow: boolean\n googleBot: { index: boolean; follow: boolean }\n }\n openGraph: {\n title: string\n description: string\n url: string\n siteName: string\n locale: string\n type: 'website' | 'article'\n images: BrandlabSocialImage[]\n publishedTime?: string\n modifiedTime?: string\n authors?: string[]\n }\n twitter: {\n card: 'summary_large_image'\n title: string\n description: string\n images: string[]\n }\n}\n\nconst BRANDLAB_INDEXING_DIRECTIVES = new Set<BrandlabIndexingDirective>([\n 'index',\n 'noindex-follow',\n 'noindex-nofollow',\n])\n\nfunction assertMetadataText(\n value: unknown,\n field: string,\n maximum: number,\n) {\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\nfunction assertSocialImage(image: BrandlabSocialImage, field: string) {\n if (!image || typeof image !== 'object' || Array.isArray(image)) {\n throw new Error(`${field} must be a social image record.`)\n }\n assertMetadataText(image.url, `${field}.url`, 2_048)\n if (image.twitterUrl !== undefined) {\n assertMetadataText(image.twitterUrl, `${field}.twitterUrl`, 2_048)\n }\n if ((image.width === undefined) !== (image.height === undefined)) {\n throw new Error(`${field}.width and height must be supplied together.`)\n }\n for (const [name, value] of [\n ['width', image.width],\n ['height', image.height],\n ] as const) {\n if (\n value !== undefined &&\n (!Number.isSafeInteger(value) || value < 1 || value > 10_000)\n ) {\n throw new Error(`${field}.${name} must be an integer between 1 and 10000.`)\n }\n }\n if (image.alt !== undefined) assertMetadataText(image.alt, `${field}.alt`, 300)\n}\n\nfunction assertMetadataDate(value: string, field: string) {\n if (\n !/^\\d{4}-\\d{2}-\\d{2}(?:T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?Z)?$/.test(value) ||\n Number.isNaN(Date.parse(value))\n ) {\n throw new Error(`${field} must be an ISO date or UTC timestamp.`)\n }\n}\n\nfunction assertBrandlabMetadataInput(input: BuildBrandlabMetadataInput) {\n assertMetadataText(input.brandName, 'brandName', 160)\n assertMetadataText(input.title, 'title', 160)\n assertMetadataText(input.description, 'description', 320)\n assertMetadataText(input.locale, 'locale', 20)\n if (!/^[a-z]{2}(?:_[A-Z]{2})?$/.test(input.locale)) {\n throw new Error('locale must use a language or language_REGION identifier.')\n }\n assertMetadataText(input.titleTemplate, 'titleTemplate', 240)\n if ((input.titleTemplate.match(/%s/g) ?? []).length !== 1) {\n throw new Error('titleTemplate must contain exactly one %s placeholder.')\n }\n if (!BRANDLAB_INDEXING_DIRECTIVES.has(input.indexing)) {\n throw new Error('indexing must use one explicit Brandlab indexing directive.')\n }\n if (input.pathname.includes('?') || input.pathname.includes('#')) {\n throw new Error('pathname must not contain a query string or hash.')\n }\n assertSocialImage(input.defaultSocialImage, 'defaultSocialImage')\n if (input.socialImage) assertSocialImage(input.socialImage, 'socialImage')\n if (input.keywords) {\n if (!Array.isArray(input.keywords) || input.keywords.length > 20) {\n throw new Error('keywords must contain no more than 20 reviewed values.')\n }\n input.keywords.forEach((keyword, index) =>\n assertMetadataText(keyword, `keywords[${index}]`, 100),\n )\n const normalized = input.keywords.map((keyword) => keyword.toLowerCase())\n if (new Set(normalized).size !== normalized.length) {\n throw new Error('keywords must not contain duplicate values.')\n }\n }\n if (input.article) {\n if (!input.article.publishedTime) {\n throw new Error('article.publishedTime is required for article metadata.')\n }\n assertMetadataDate(input.article.publishedTime, 'article.publishedTime')\n if (input.article.modifiedTime) {\n assertMetadataDate(input.article.modifiedTime, 'article.modifiedTime')\n if (\n Date.parse(input.article.modifiedTime) <\n Date.parse(input.article.publishedTime)\n ) {\n throw new Error('article.modifiedTime cannot precede article.publishedTime.')\n }\n }\n if (!input.article.authors?.length) {\n throw new Error('article.authors requires at least one reviewed author.')\n }\n input.article.authors.forEach((author, index) =>\n assertMetadataText(author, `article.authors[${index}]`, 160),\n )\n }\n}\n\nexport function normalizeMetadataTitle(\n title: string,\n options: { brandName: string; titleMode?: BrandlabMetadataTitleMode },\n) {\n const normalizedTitle = title.trim()\n\n if (!normalizedTitle) {\n throw new Error('Metadata title cannot be empty.')\n }\n\n const brandSuffix = `| ${options.brandName}`.toLowerCase()\n const brandName = options.brandName.trim().toLowerCase()\n\n if (\n options.titleMode !== 'absolute' &&\n normalizedTitle.toLowerCase().includes(brandSuffix)\n ) {\n throw new Error(\n `Metadata title \"${normalizedTitle}\" already includes the brand suffix. Pass a page-specific title only.`,\n )\n }\n\n if (\n options.titleMode !== 'absolute' &&\n brandName &&\n normalizedTitle.toLowerCase().includes(brandName)\n ) {\n throw new Error(\n `Metadata title \"${normalizedTitle}\" already includes the brand name. Pass a page-specific title only or use titleMode: \"absolute\".`,\n )\n }\n\n return normalizedTitle\n}\n\nexport function applyTitleTemplate(title: string, template: string) {\n return template.replace('%s', title)\n}\n\nexport function normalizeCanonicalPath(pathname: string) {\n if (!pathname) {\n throw new Error('pathname is required.')\n }\n\n if (/^https?:/i.test(pathname)) {\n throw new Error(`Canonical pathname must be root-relative: \"${pathname}\".`)\n }\n try {\n return normalizeBrandlabRoutePath(pathname.split(/[?#]/, 1)[0] ?? '')\n } catch {\n throw new Error(`Canonical pathname must be root-relative: \"${pathname}\".`)\n }\n}\n\nexport function getAbsoluteUrl(siteUrl: string, pathname: string) {\n let base: URL\n try {\n base = new URL(siteUrl)\n } catch {\n throw new Error(`siteUrl must be an absolute HTTPS origin: \"${siteUrl}\".`)\n }\n\n if (\n base.protocol !== 'https:' ||\n base.username ||\n base.password ||\n base.search ||\n base.hash ||\n (base.pathname !== '/' && base.pathname !== '')\n ) {\n throw new Error(`siteUrl must be an absolute HTTPS origin: \"${siteUrl}\".`)\n }\n\n const path = normalizeCanonicalPath(pathname)\n const resolved = new URL(path, base.origin)\n if (resolved.origin !== base.origin) {\n throw new Error(`Canonical pathname must remain on ${base.origin}: \"${pathname}\".`)\n }\n return resolved.href\n}\n\nfunction normalizeSocialImageUrl(siteUrl: string, imageUrl: string) {\n let resolved: URL\n try {\n resolved = new URL(imageUrl, getAbsoluteUrl(siteUrl, '/'))\n } catch {\n throw new Error(`Social image URL must resolve to absolute HTTPS: \"${imageUrl}\".`)\n }\n if (resolved.protocol !== 'https:' || resolved.username || resolved.password) {\n throw new Error(`Social image URL must resolve to absolute HTTPS: \"${imageUrl}\".`)\n }\n return resolved.href\n}\n\nexport function buildSeoMetadata(input: BuildSeoMetadataInput): BrandlabSeoMetadata {\n const titleMode = input.titleMode ?? 'templated'\n const pageTitle = normalizeMetadataTitle(input.title, {\n brandName: input.brandName,\n titleMode,\n })\n const resolvedTitle =\n titleMode === 'absolute' ? pageTitle : applyTitleTemplate(pageTitle, input.titleTemplate)\n const canonicalPath = normalizeCanonicalPath(input.pathname)\n const canonicalUrl = getAbsoluteUrl(input.siteUrl, canonicalPath)\n const defaultSocialImage = {\n ...input.defaultSocialImage,\n url: normalizeSocialImageUrl(input.siteUrl, input.defaultSocialImage.url),\n ...(input.defaultSocialImage.twitterUrl\n ? {\n twitterUrl: normalizeSocialImageUrl(\n input.siteUrl,\n input.defaultSocialImage.twitterUrl,\n ),\n }\n : {}),\n }\n const socialImage = input.socialImage\n ? {\n ...input.socialImage,\n url: normalizeSocialImageUrl(input.siteUrl, input.socialImage.url),\n }\n : defaultSocialImage\n const twitterImage = input.socialImage\n ? socialImage.url\n : defaultSocialImage.twitterUrl ?? defaultSocialImage.url\n const robots =\n input.indexing === 'index'\n ? undefined\n : {\n index: false,\n follow: input.indexing === 'noindex-follow',\n googleBot: {\n index: false,\n follow: input.indexing === 'noindex-follow',\n },\n }\n\n return {\n title: { absolute: resolvedTitle },\n resolvedTitle,\n description: input.description,\n canonicalPath,\n canonicalUrl,\n alternates: {\n canonical: canonicalUrl,\n },\n ...(input.keywords && input.keywords.length > 0 ? { keywords: input.keywords } : {}),\n ...(robots ? { robots } : {}),\n openGraph: {\n title: resolvedTitle,\n description: input.description,\n url: canonicalUrl,\n siteName: input.brandName,\n locale: input.locale,\n type: input.article ? 'article' : 'website',\n images: [socialImage],\n ...(input.article?.publishedTime ? { publishedTime: input.article.publishedTime } : {}),\n ...(input.article?.modifiedTime ? { modifiedTime: input.article.modifiedTime } : {}),\n ...(input.article?.authors && input.article.authors.length > 0\n ? { authors: input.article.authors }\n : {}),\n },\n twitter: {\n card: 'summary_large_image',\n title: resolvedTitle,\n description: input.description,\n images: [twitterImage],\n },\n }\n}\n\nexport function buildBrandlabMetadata(\n input: BuildBrandlabMetadataInput,\n): BrandlabSeoMetadata {\n assertBrandlabMetadataInput(input)\n return buildSeoMetadata(input)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n normalizeBrandlabRoutePath,\n type BrandlabIndexingDirective,\n} from './routes.js'\n\nexport type BrandlabMetadataTitleMode = 'templated' | 'absolute'\n\n/**\n * Meta keywords as production sites pass them: one comma-separated string, which\n * renders exactly as given (`'a, b, c'` renders `content=\"a, b, c\"`), or a list,\n * which Next joins with commas (`['a', 'b', 'c']` renders `content=\"a,b,c\"`).\n */\nexport type BrandlabMetadataKeywords = string | readonly string[]\n\nexport interface BrandlabSocialImage {\n url: string\n twitterUrl?: string\n width?: number\n height?: number\n alt?: string\n}\n\nexport interface BuildBrandlabMetadataInput {\n brandName: string\n siteUrl: string\n locale: string\n titleTemplate: string\n defaultSocialImage: BrandlabSocialImage\n title: string\n description: string\n pathname: string\n titleMode?: BrandlabMetadataTitleMode\n indexing: BrandlabIndexingDirective\n keywords?: BrandlabMetadataKeywords\n article?: {\n publishedTime?: string\n modifiedTime?: string\n authors?: string[]\n }\n socialImage?: BrandlabSocialImage\n}\n\nexport interface BrandlabSeoMetadata {\n title: string | { absolute: string }\n resolvedTitle: string\n description: string\n canonicalPath: string\n canonicalUrl: string\n alternates: { canonical: string }\n /** Handed to Next unchanged in form: a string stays a string, a list stays a list. */\n keywords?: string | string[]\n robots?: {\n index: boolean\n follow: boolean\n googleBot: { index: boolean; follow: boolean }\n }\n openGraph: {\n title: string\n description: string\n url: string\n siteName: string\n locale: string\n type: 'website' | 'article'\n images: BrandlabSocialImage[]\n publishedTime?: string\n modifiedTime?: string\n authors?: string[]\n }\n twitter: {\n card: 'summary_large_image'\n title: string\n description: string\n images: string[]\n }\n}\n\nconst BRANDLAB_INDEXING_DIRECTIVES = new Set<BrandlabIndexingDirective>([\n 'index',\n 'noindex-follow',\n 'noindex-nofollow',\n])\n\nfunction assertMetadataText(\n value: unknown,\n field: string,\n maximum: number,\n) {\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 assertBrandlabSocialImage(image: BrandlabSocialImage, field: string) {\n if (!image || typeof image !== 'object' || Array.isArray(image)) {\n throw new Error(`${field} must be a social image record.`)\n }\n assertMetadataText(image.url, `${field}.url`, 2_048)\n if (image.twitterUrl !== undefined) {\n assertMetadataText(image.twitterUrl, `${field}.twitterUrl`, 2_048)\n }\n if ((image.width === undefined) !== (image.height === undefined)) {\n throw new Error(`${field}.width and height must be supplied together.`)\n }\n for (const [name, value] of [\n ['width', image.width],\n ['height', image.height],\n ] as const) {\n if (\n value !== undefined &&\n (!Number.isSafeInteger(value) || value < 1 || value > 10_000)\n ) {\n throw new Error(`${field}.${name} must be an integer between 1 and 10000.`)\n }\n }\n if (image.alt !== undefined) assertMetadataText(image.alt, `${field}.alt`, 300)\n}\n\n/**\n * Validate keywords in either form without changing them. A string is one trimmed,\n * single-line value whose comma-separated entries follow the list rules: at most 20\n * distinct (case-insensitive) values of up to 100 characters.\n */\nexport function assertBrandlabKeywords(keywords: unknown, field = 'keywords') {\n let values: readonly unknown[]\n if (typeof keywords === 'string') {\n assertMetadataText(keywords, field, 2_100)\n values = keywords.split(',').map((keyword) => keyword.trim())\n } else if (Array.isArray(keywords)) {\n values = keywords\n } else {\n throw new Error(`${field} must be a comma-separated string or a list of reviewed values.`)\n }\n if (values.length > 20) {\n throw new Error(`${field} must contain no more than 20 reviewed values.`)\n }\n values.forEach((keyword, index) => assertMetadataText(keyword, `${field}[${index}]`, 100))\n const normalized = values.map((keyword) => String(keyword).toLowerCase())\n if (new Set(normalized).size !== normalized.length) {\n throw new Error(`${field} must not contain duplicate values.`)\n }\n}\n\nfunction assertMetadataDate(value: string, field: string) {\n if (\n !/^\\d{4}-\\d{2}-\\d{2}(?:T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?Z)?$/.test(value) ||\n Number.isNaN(Date.parse(value))\n ) {\n throw new Error(`${field} must be an ISO date or UTC timestamp.`)\n }\n}\n\nfunction assertBrandlabMetadataInput(input: BuildBrandlabMetadataInput) {\n assertMetadataText(input.brandName, 'brandName', 160)\n assertMetadataText(input.title, 'title', 160)\n assertMetadataText(input.description, 'description', 320)\n assertMetadataText(input.locale, 'locale', 20)\n if (!/^[a-z]{2}(?:_[A-Z]{2})?$/.test(input.locale)) {\n throw new Error('locale must use a language or language_REGION identifier.')\n }\n assertMetadataText(input.titleTemplate, 'titleTemplate', 240)\n if ((input.titleTemplate.match(/%s/g) ?? []).length !== 1) {\n throw new Error('titleTemplate must contain exactly one %s placeholder.')\n }\n if (!BRANDLAB_INDEXING_DIRECTIVES.has(input.indexing)) {\n throw new Error('indexing must use one explicit Brandlab indexing directive.')\n }\n if (input.pathname.includes('?') || input.pathname.includes('#')) {\n throw new Error('pathname must not contain a query string or hash.')\n }\n assertBrandlabSocialImage(input.defaultSocialImage, 'defaultSocialImage')\n if (input.socialImage) assertBrandlabSocialImage(input.socialImage, 'socialImage')\n if (input.keywords !== undefined) assertBrandlabKeywords(input.keywords)\n if (input.article) {\n if (!input.article.publishedTime) {\n throw new Error('article.publishedTime is required for article metadata.')\n }\n assertMetadataDate(input.article.publishedTime, 'article.publishedTime')\n if (input.article.modifiedTime) {\n assertMetadataDate(input.article.modifiedTime, 'article.modifiedTime')\n if (\n Date.parse(input.article.modifiedTime) <\n Date.parse(input.article.publishedTime)\n ) {\n throw new Error('article.modifiedTime cannot precede article.publishedTime.')\n }\n }\n if (!input.article.authors?.length) {\n throw new Error('article.authors requires at least one reviewed author.')\n }\n input.article.authors.forEach((author, index) =>\n assertMetadataText(author, `article.authors[${index}]`, 160),\n )\n }\n}\n\nexport function normalizeMetadataTitle(\n title: string,\n options: { brandName: string; titleMode?: BrandlabMetadataTitleMode },\n) {\n const normalizedTitle = title.trim()\n\n if (!normalizedTitle) {\n throw new Error('Metadata title cannot be empty.')\n }\n\n const brandSuffix = `| ${options.brandName}`.toLowerCase()\n const brandName = options.brandName.trim().toLowerCase()\n\n if (\n options.titleMode !== 'absolute' &&\n normalizedTitle.toLowerCase().includes(brandSuffix)\n ) {\n throw new Error(\n `Metadata title \"${normalizedTitle}\" already includes the brand suffix. Pass a page-specific title only.`,\n )\n }\n\n if (\n options.titleMode !== 'absolute' &&\n brandName &&\n normalizedTitle.toLowerCase().includes(brandName)\n ) {\n throw new Error(\n `Metadata title \"${normalizedTitle}\" already includes the brand name. Pass a page-specific title only or use titleMode: \"absolute\".`,\n )\n }\n\n return normalizedTitle\n}\n\nexport function applyTitleTemplate(title: string, template: string) {\n return template.replace('%s', title)\n}\n\nexport function normalizeCanonicalPath(pathname: string) {\n if (!pathname) {\n throw new Error('pathname is required.')\n }\n\n if (/^https?:/i.test(pathname)) {\n throw new Error(`Canonical pathname must be root-relative: \"${pathname}\".`)\n }\n try {\n return normalizeBrandlabRoutePath(pathname.split(/[?#]/, 1)[0] ?? '')\n } catch {\n throw new Error(`Canonical pathname must be root-relative: \"${pathname}\".`)\n }\n}\n\nexport function getAbsoluteUrl(siteUrl: string, pathname: string) {\n let base: URL\n try {\n base = new URL(siteUrl)\n } catch {\n throw new Error(`siteUrl must be an absolute HTTPS origin: \"${siteUrl}\".`)\n }\n\n if (\n base.protocol !== 'https:' ||\n base.username ||\n base.password ||\n base.search ||\n base.hash ||\n (base.pathname !== '/' && base.pathname !== '')\n ) {\n throw new Error(`siteUrl must be an absolute HTTPS origin: \"${siteUrl}\".`)\n }\n\n const path = normalizeCanonicalPath(pathname)\n const resolved = new URL(path, base.origin)\n if (resolved.origin !== base.origin) {\n throw new Error(`Canonical pathname must remain on ${base.origin}: \"${pathname}\".`)\n }\n return resolved.href\n}\n\nfunction normalizeSocialImageUrl(siteUrl: string, imageUrl: string) {\n let resolved: URL\n try {\n resolved = new URL(imageUrl, getAbsoluteUrl(siteUrl, '/'))\n } catch {\n throw new Error(`Social image URL must resolve to absolute HTTPS: \"${imageUrl}\".`)\n }\n if (resolved.protocol !== 'https:' || resolved.username || resolved.password) {\n throw new Error(`Social image URL must resolve to absolute HTTPS: \"${imageUrl}\".`)\n }\n return resolved.href\n}\n\nfunction buildSeoMetadata(input: BuildBrandlabMetadataInput): BrandlabSeoMetadata {\n const titleMode = input.titleMode ?? 'templated'\n const pageTitle = normalizeMetadataTitle(input.title, {\n brandName: input.brandName,\n titleMode,\n })\n const resolvedTitle =\n titleMode === 'absolute' ? pageTitle : applyTitleTemplate(pageTitle, input.titleTemplate)\n const canonicalPath = normalizeCanonicalPath(input.pathname)\n const canonicalUrl = getAbsoluteUrl(input.siteUrl, canonicalPath)\n const defaultSocialImage = {\n ...input.defaultSocialImage,\n url: normalizeSocialImageUrl(input.siteUrl, input.defaultSocialImage.url),\n ...(input.defaultSocialImage.twitterUrl\n ? {\n twitterUrl: normalizeSocialImageUrl(\n input.siteUrl,\n input.defaultSocialImage.twitterUrl,\n ),\n }\n : {}),\n }\n const socialImage = input.socialImage\n ? {\n ...input.socialImage,\n url: normalizeSocialImageUrl(input.siteUrl, input.socialImage.url),\n }\n : defaultSocialImage\n const twitterImage = input.socialImage\n ? socialImage.url\n : defaultSocialImage.twitterUrl ?? defaultSocialImage.url\n const robots =\n input.indexing === 'index'\n ? undefined\n : {\n index: false,\n follow: input.indexing === 'noindex-follow',\n googleBot: {\n index: false,\n follow: input.indexing === 'noindex-follow',\n },\n }\n\n return {\n title: { absolute: resolvedTitle },\n resolvedTitle,\n description: input.description,\n canonicalPath,\n canonicalUrl,\n alternates: {\n canonical: canonicalUrl,\n },\n ...(input.keywords !== undefined && input.keywords.length > 0\n ? { keywords: typeof input.keywords === 'string' ? input.keywords : [...input.keywords] }\n : {}),\n ...(robots ? { robots } : {}),\n openGraph: {\n title: resolvedTitle,\n description: input.description,\n url: canonicalUrl,\n siteName: input.brandName,\n locale: input.locale,\n type: input.article ? 'article' : 'website',\n images: [socialImage],\n ...(input.article?.publishedTime ? { publishedTime: input.article.publishedTime } : {}),\n ...(input.article?.modifiedTime ? { modifiedTime: input.article.modifiedTime } : {}),\n ...(input.article?.authors && input.article.authors.length > 0\n ? { authors: input.article.authors }\n : {}),\n },\n twitter: {\n card: 'summary_large_image',\n title: resolvedTitle,\n description: input.description,\n images: [twitterImage],\n },\n }\n}\n\n/** The one metadata builder: validates every input, then resolves canonical, robots and social output. */\nexport function buildBrandlabMetadata(\n input: BuildBrandlabMetadataInput,\n): BrandlabSeoMetadata {\n assertBrandlabMetadataInput(input)\n return buildSeoMetadata(input)\n}\n"]}
|
package/dist/project.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { GooglePlaceReviewsSelection } from './google-reviews.js';
|
|
2
|
-
export type BrandlabProjectProfile = 'next-local-service' | 'next-root-level';
|
|
3
2
|
export type BrandlabRouteFamily = 'services' | 'locations' | 'service-locations' | 'blog' | 'projects' | 'legal';
|
|
4
3
|
export type BrandlabBlogAdapter = 'none' | 'mdx' | 'structured';
|
|
5
4
|
export type BrandlabContactAttachmentMode = 'none' | 'images-optional' | 'images-required' | 'images-and-pdfs-optional';
|
|
@@ -16,11 +15,6 @@ export type BrandlabReviewsCapability = {
|
|
|
16
15
|
/** Stable route IDs where review proof is intentionally presented. */
|
|
17
16
|
routeIds: readonly string[];
|
|
18
17
|
};
|
|
19
|
-
export interface BrandlabRouteAlias {
|
|
20
|
-
from: string;
|
|
21
|
-
to: string;
|
|
22
|
-
status?: 301 | 308;
|
|
23
|
-
}
|
|
24
18
|
export interface BrandlabException {
|
|
25
19
|
ruleId: string;
|
|
26
20
|
rationale: string;
|
|
@@ -38,10 +32,8 @@ export type BrandlabSitemapConfig = {
|
|
|
38
32
|
};
|
|
39
33
|
export interface BrandlabProjectConfig {
|
|
40
34
|
projectId: string;
|
|
41
|
-
profile: BrandlabProjectProfile;
|
|
42
35
|
routes: {
|
|
43
36
|
families: readonly BrandlabRouteFamily[];
|
|
44
|
-
aliases?: readonly BrandlabRouteAlias[];
|
|
45
37
|
};
|
|
46
38
|
capabilities?: {
|
|
47
39
|
blog?: {
|
package/dist/project.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAA;AAEtE,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAA;AAEtE,MAAM,MAAM,mBAAmB,GAC3B,UAAU,GACV,WAAW,GACX,mBAAmB,GACnB,MAAM,GACN,UAAU,GACV,OAAO,CAAA;AAEX,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,KAAK,GAAG,YAAY,CAAA;AAC/D,MAAM,MAAM,6BAA6B,GACrC,MAAM,GACN,iBAAiB,GACjB,iBAAiB,GACjB,0BAA0B,CAAA;AAC9B,MAAM,MAAM,wBAAwB,GAChC,MAAM,GACN,uBAAuB,GACvB,mBAAmB,GACnB,gBAAgB,CAAA;AACpB,MAAM,MAAM,uBAAuB,GAAG,eAAe,GAAG,eAAe,CAAA;AACvE,MAAM,MAAM,yBAAyB,GAAG,WAAW,GAAG,SAAS,GAAG,MAAM,CAAA;AAExE,MAAM,MAAM,yBAAyB,GACjC;IAAE,QAAQ,EAAE,eAAe,CAAA;CAAE,GAC7B;IACE,QAAQ,EAAE,eAAe,CAAA;IACzB,IAAI,EAAE,yBAAyB,CAAA;IAC/B,oFAAoF;IACpF,SAAS,CAAC,EAAE,2BAA2B,CAAA;IACvC,sEAAsE;IACtE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAC5B,CAAA;AAEL,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,MAAM,qBAAqB,GAC7B;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IACE,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAEL,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE;QACN,QAAQ,EAAE,SAAS,mBAAmB,EAAE,CAAA;KACzC,CAAA;IACD,YAAY,CAAC,EAAE;QACb,IAAI,CAAC,EAAE;YAAE,OAAO,EAAE,mBAAmB,CAAA;SAAE,CAAA;QACvC,OAAO,CAAC,EAAE;YACR,YAAY,EAAE,MAAM,CAAA;YACpB,WAAW,EAAE,6BAA6B,CAAA;SAC3C,CAAA;QACD,SAAS,CAAC,EAAE;YAAE,QAAQ,EAAE,wBAAwB,CAAA;SAAE,CAAA;QAClD,OAAO,CAAC,EAAE,yBAAyB,CAAA;KACpC,CAAA;IACD,OAAO,CAAC,EAAE,qBAAqB,CAAA;IAC/B,mBAAmB,EAAE;QACnB,oBAAoB,EAAE;YACpB,IAAI,EAAE,MAAM,CAAA;YACZ,OAAO,CAAC,EAAE,MAAM,CAAA;YAChB,OAAO,CAAC,EAAE,MAAM,CAAA;YAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;YACjB,IAAI,CAAC,EAAE,MAAM,CAAA;SACd,CAAA;KACF,CAAA;IACD,UAAU,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAA;CAC1C;AAyED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,CAAC,CAAC,SAAS,qBAAqB,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAuHzF"}
|
package/dist/project.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { normalizeBrandlabRoutePath } from './routes.js';
|
|
2
2
|
import { assertBrandlabCanonicalEntityId } from './entity-id.js';
|
|
3
|
-
const
|
|
4
|
-
'
|
|
5
|
-
'
|
|
6
|
-
|
|
3
|
+
const PROJECT_CONFIG_KEYS = [
|
|
4
|
+
'projectId',
|
|
5
|
+
'routes',
|
|
6
|
+
'capabilities',
|
|
7
|
+
'sitemap',
|
|
8
|
+
'searchCertification',
|
|
9
|
+
'exceptions',
|
|
10
|
+
];
|
|
7
11
|
const ROUTE_FAMILIES = new Set([
|
|
8
12
|
'services',
|
|
9
13
|
'locations',
|
|
@@ -51,6 +55,12 @@ function assertRootRelativePath(path, field) {
|
|
|
51
55
|
throw new Error(`${field} must be a root-relative path without a query string or hash.`);
|
|
52
56
|
}
|
|
53
57
|
}
|
|
58
|
+
function assertKnownKeys(value, allowed, field, hint = '') {
|
|
59
|
+
const unsupported = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
60
|
+
if (unsupported.length > 0) {
|
|
61
|
+
throw new Error(`${field} contains unsupported keys: ${unsupported.join(', ')}.${hint}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
54
64
|
function assertUnique(values, field) {
|
|
55
65
|
const duplicate = values.find((value, index) => values.indexOf(value) !== index);
|
|
56
66
|
if (duplicate) {
|
|
@@ -62,39 +72,22 @@ function assertUnique(values, field) {
|
|
|
62
72
|
* policy deliberately do not belong in this object.
|
|
63
73
|
*/
|
|
64
74
|
export function defineBrandlabProject(config) {
|
|
75
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
76
|
+
throw new TypeError('defineBrandlabProject requires a project configuration.');
|
|
77
|
+
}
|
|
78
|
+
assertKnownKeys(config, PROJECT_CONFIG_KEYS, 'Project configuration');
|
|
65
79
|
assertNonEmpty(config.projectId, 'projectId');
|
|
66
80
|
assertBrandlabCanonicalEntityId(config.projectId);
|
|
67
|
-
if (!
|
|
68
|
-
throw new Error(
|
|
81
|
+
if (!config.routes || typeof config.routes !== 'object') {
|
|
82
|
+
throw new Error('routes.families is required.');
|
|
69
83
|
}
|
|
84
|
+
assertKnownKeys(config.routes, ['families'], 'routes', ' Redirects belong in the src/data/redirects.ts table.');
|
|
70
85
|
assertUnique(config.routes.families, 'routes.families');
|
|
71
86
|
config.routes.families.forEach((family, index) => {
|
|
72
87
|
if (!ROUTE_FAMILIES.has(family)) {
|
|
73
88
|
throw new Error(`routes.families[${index}] is unsupported.`);
|
|
74
89
|
}
|
|
75
90
|
});
|
|
76
|
-
const aliases = config.routes.aliases ?? [];
|
|
77
|
-
const normalizedAliases = aliases.map((alias) => ({
|
|
78
|
-
...alias,
|
|
79
|
-
from: normalizeBrandlabRoutePath(alias.from),
|
|
80
|
-
to: normalizeBrandlabRoutePath(alias.to),
|
|
81
|
-
}));
|
|
82
|
-
assertUnique(normalizedAliases.map((alias) => alias.from), 'routes.aliases.from');
|
|
83
|
-
aliases.forEach((alias, index) => {
|
|
84
|
-
assertRootRelativePath(alias.from, `routes.aliases[${index}].from`);
|
|
85
|
-
assertRootRelativePath(alias.to, `routes.aliases[${index}].to`);
|
|
86
|
-
if (alias.status !== undefined && alias.status !== 301 && alias.status !== 308) {
|
|
87
|
-
throw new Error(`routes.aliases[${index}].status must be 301 or 308.`);
|
|
88
|
-
}
|
|
89
|
-
if (normalizedAliases[index].from === normalizedAliases[index].to) {
|
|
90
|
-
throw new Error(`routes.aliases[${index}] cannot redirect a path to itself.`);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
const aliasTargets = new Set(normalizedAliases.map((alias) => alias.to));
|
|
94
|
-
const chainedAlias = normalizedAliases.find((alias) => aliasTargets.has(alias.from));
|
|
95
|
-
if (chainedAlias) {
|
|
96
|
-
throw new Error(`Route alias "${chainedAlias.from}" creates a redirect chain or cycle.`);
|
|
97
|
-
}
|
|
98
91
|
const representativeRoutes = config.searchCertification.representativeRoutes;
|
|
99
92
|
Object.entries(representativeRoutes).forEach(([name, path]) => {
|
|
100
93
|
if (path)
|
package/dist/project.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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","sourcesContent":["import { normalizeBrandlabRoutePath } from './routes.js'\nimport { assertBrandlabCanonicalEntityId } from './entity-id.js'\nimport type { GooglePlaceReviewsSelection } from './google-reviews.js'\n\nexport type BrandlabProjectProfile =\n | 'next-local-service'\n | 'next-root-level'\n\nexport type BrandlabRouteFamily =\n | 'services'\n | 'locations'\n | 'service-locations'\n | 'blog'\n | 'projects'\n | 'legal'\n\nexport type BrandlabBlogAdapter = 'none' | 'mdx' | 'structured'\nexport type BrandlabContactAttachmentMode =\n | 'none'\n | 'images-optional'\n | 'images-required'\n | 'images-and-pdfs-optional'\nexport type BrandlabLocationStrategy =\n | 'none'\n | 'approved-service-area'\n | 'physical-location'\n | 'multi-location'\nexport type BrandlabReviewsProvider = 'google-places' | 'project-owned'\nexport type BrandlabGoogleReviewsMode = 'link-only' | 'summary' | 'full'\n\nexport type BrandlabReviewsCapability =\n | { provider: 'project-owned' }\n | {\n provider: 'google-places'\n mode: BrandlabGoogleReviewsMode\n /** Optional presentation policy. Five-star selection is valid only in full mode. */\n selection?: GooglePlaceReviewsSelection\n /** Stable route IDs where review proof is intentionally presented. */\n routeIds: readonly string[]\n }\n\nexport interface BrandlabRouteAlias {\n from: string\n to: string\n status?: 301 | 308\n}\n\nexport interface BrandlabException {\n ruleId: string\n rationale: string\n owner: 'Romie'\n evidence: string\n expiresAt?: string\n reviewTrigger: string\n}\n\nexport type BrandlabSitemapConfig =\n | { mode: 'single' }\n | {\n mode: 'split'\n families: readonly string[]\n reason: string\n }\n\nexport interface BrandlabProjectConfig {\n projectId: string\n profile: BrandlabProjectProfile\n routes: {\n families: readonly BrandlabRouteFamily[]\n aliases?: readonly BrandlabRouteAlias[]\n }\n capabilities?: {\n blog?: { adapter: BrandlabBlogAdapter }\n contact?: {\n fieldProfile: string\n attachments: BrandlabContactAttachmentMode\n }\n locations?: { strategy: BrandlabLocationStrategy }\n reviews?: BrandlabReviewsCapability\n }\n sitemap?: BrandlabSitemapConfig\n searchCertification: {\n representativeRoutes: {\n home: string\n contact?: string\n service?: string\n location?: string\n blog?: string\n }\n }\n exceptions?: readonly BrandlabException[]\n}\n\nconst PROJECT_PROFILES = new Set<BrandlabProjectProfile>([\n 'next-local-service',\n 'next-root-level',\n])\nconst ROUTE_FAMILIES = new Set<BrandlabRouteFamily>([\n 'services',\n 'locations',\n 'service-locations',\n 'blog',\n 'projects',\n 'legal',\n])\nconst BLOG_ADAPTERS = new Set<BrandlabBlogAdapter>(['none', 'mdx', 'structured'])\nconst ATTACHMENT_MODES = new Set<BrandlabContactAttachmentMode>([\n 'none',\n 'images-optional',\n 'images-required',\n 'images-and-pdfs-optional',\n])\nconst LOCATION_STRATEGIES = new Set<BrandlabLocationStrategy>([\n 'none',\n 'approved-service-area',\n 'physical-location',\n 'multi-location',\n])\nconst REVIEWS_PROVIDERS = new Set<BrandlabReviewsProvider>([\n 'google-places',\n 'project-owned',\n])\nconst GOOGLE_REVIEWS_MODES = new Set<BrandlabGoogleReviewsMode>([\n 'link-only',\n 'summary',\n 'full',\n])\nconst GOOGLE_REVIEWS_SELECTIONS = new Set<GooglePlaceReviewsSelection>([\n 'relevance',\n 'five-star-featured',\n])\n\nfunction assertNonEmpty(value: string, field: string) {\n if (!value.trim()) {\n throw new Error(`${field} cannot be empty.`)\n }\n}\n\nfunction assertRootRelativePath(path: string, field: string) {\n try {\n normalizeBrandlabRoutePath(path)\n } catch {\n throw new Error(`${field} must be a root-relative path without a query string or hash.`)\n }\n}\n\nfunction assertUnique(values: readonly string[], field: string) {\n const duplicate = values.find((value, index) => values.indexOf(value) !== index)\n if (duplicate) {\n throw new Error(`${field} contains duplicate value \"${duplicate}\".`)\n }\n}\n\n/**\n * Defines project-owned variation only. Runtime versions and global Brandlab\n * policy deliberately do not belong in this object.\n */\nexport function defineBrandlabProject<const T extends BrandlabProjectConfig>(config: T): T {\n assertNonEmpty(config.projectId, 'projectId')\n\n assertBrandlabCanonicalEntityId(config.projectId)\n if (!PROJECT_PROFILES.has(config.profile)) {\n throw new Error(`Unsupported project profile \"${String(config.profile)}\".`)\n }\n\n assertUnique(config.routes.families, 'routes.families')\n config.routes.families.forEach((family, index) => {\n if (!ROUTE_FAMILIES.has(family)) {\n throw new Error(`routes.families[${index}] is unsupported.`)\n }\n })\n\n const aliases = config.routes.aliases ?? []\n const normalizedAliases = aliases.map((alias) => ({\n ...alias,\n from: normalizeBrandlabRoutePath(alias.from),\n to: normalizeBrandlabRoutePath(alias.to),\n }))\n assertUnique(\n normalizedAliases.map((alias) => alias.from),\n 'routes.aliases.from',\n )\n\n aliases.forEach((alias, index) => {\n assertRootRelativePath(alias.from, `routes.aliases[${index}].from`)\n assertRootRelativePath(alias.to, `routes.aliases[${index}].to`)\n if (alias.status !== undefined && alias.status !== 301 && alias.status !== 308) {\n throw new Error(`routes.aliases[${index}].status must be 301 or 308.`)\n }\n if (normalizedAliases[index]!.from === normalizedAliases[index]!.to) {\n throw new Error(`routes.aliases[${index}] cannot redirect a path to itself.`)\n }\n })\n\n const aliasTargets = new Set(normalizedAliases.map((alias) => alias.to))\n const chainedAlias = normalizedAliases.find((alias) => aliasTargets.has(alias.from))\n if (chainedAlias) {\n throw new Error(`Route alias \"${chainedAlias.from}\" creates a redirect chain or cycle.`)\n }\n\n const representativeRoutes = config.searchCertification.representativeRoutes\n Object.entries(representativeRoutes).forEach(([name, path]) => {\n if (path) assertRootRelativePath(path, `searchCertification.representativeRoutes.${name}`)\n })\n if (representativeRoutes.home !== '/') {\n throw new Error('searchCertification.representativeRoutes.home must be \"/\".')\n }\n\n if (config.capabilities?.contact) {\n assertNonEmpty(config.capabilities.contact.fieldProfile, 'contact.fieldProfile')\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(config.capabilities.contact.fieldProfile)) {\n throw new Error('contact.fieldProfile must be lowercase kebab-case.')\n }\n if (!ATTACHMENT_MODES.has(config.capabilities.contact.attachments)) {\n throw new Error('contact.attachments is unsupported.')\n }\n }\n if (\n config.capabilities?.blog &&\n !BLOG_ADAPTERS.has(config.capabilities.blog.adapter)\n ) {\n throw new Error('blog.adapter is unsupported.')\n }\n if (\n config.capabilities?.locations &&\n !LOCATION_STRATEGIES.has(config.capabilities.locations.strategy)\n ) {\n throw new Error('locations.strategy is unsupported.')\n }\n if (\n config.capabilities?.reviews &&\n !REVIEWS_PROVIDERS.has(config.capabilities.reviews.provider)\n ) {\n throw new Error('reviews.provider is unsupported.')\n }\n if (\n config.capabilities?.reviews?.provider === 'google-places' &&\n !config.routes.families.includes('legal')\n ) {\n throw new Error('Google Places reviews require the legal route family.')\n }\n if (config.capabilities?.reviews?.provider === 'google-places') {\n const reviews = config.capabilities.reviews\n if (!GOOGLE_REVIEWS_MODES.has(reviews.mode)) {\n throw new Error('reviews.mode is unsupported.')\n }\n if (reviews.selection && !GOOGLE_REVIEWS_SELECTIONS.has(reviews.selection)) {\n throw new Error('reviews.selection is unsupported.')\n }\n if (reviews.selection === 'five-star-featured' && reviews.mode !== 'full') {\n throw new Error('Five-star featured reviews require full mode.')\n }\n if (!Array.isArray(reviews.routeIds) || reviews.routeIds.length === 0) {\n throw new Error('Google Places reviews require at least one approved route ID.')\n }\n assertUnique(reviews.routeIds, 'reviews.routeIds')\n reviews.routeIds.forEach((routeId, index) => {\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(routeId)) {\n throw new Error(`reviews.routeIds[${index}] must be lowercase kebab-case.`)\n }\n })\n }\n\n if (config.sitemap?.mode === 'split') {\n if (config.sitemap.families.length === 0) {\n throw new Error('Split sitemap configuration requires at least one family.')\n }\n assertUnique(config.sitemap.families, 'sitemap.families')\n config.sitemap.families.forEach((path, index) =>\n assertRootRelativePath(path, `sitemap.families[${index}]`),\n )\n assertNonEmpty(config.sitemap.reason, 'sitemap.reason')\n }\n\n assertUnique(\n (config.exceptions ?? []).map((exception) => exception.ruleId),\n 'exceptions.ruleId',\n )\n\n ;(config.exceptions ?? []).forEach((exception, index) => {\n assertNonEmpty(exception.ruleId, `exceptions[${index}].ruleId`)\n assertNonEmpty(exception.rationale, `exceptions[${index}].rationale`)\n assertNonEmpty(exception.evidence, `exceptions[${index}].evidence`)\n assertNonEmpty(exception.reviewTrigger, `exceptions[${index}].reviewTrigger`)\n if (exception.owner !== 'Romie') {\n throw new Error(`exceptions[${index}].owner must be \"Romie\".`)\n }\n\n if (exception.expiresAt && Number.isNaN(Date.parse(exception.expiresAt))) {\n throw new Error(`exceptions[${index}].expiresAt must be a valid date.`)\n }\n })\n\n return config\n}\n"]}
|
|
1
|
+
{"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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","sourcesContent":["import { normalizeBrandlabRoutePath } from './routes.js'\nimport { assertBrandlabCanonicalEntityId } from './entity-id.js'\nimport type { GooglePlaceReviewsSelection } from './google-reviews.js'\n\nexport type BrandlabRouteFamily =\n | 'services'\n | 'locations'\n | 'service-locations'\n | 'blog'\n | 'projects'\n | 'legal'\n\nexport type BrandlabBlogAdapter = 'none' | 'mdx' | 'structured'\nexport type BrandlabContactAttachmentMode =\n | 'none'\n | 'images-optional'\n | 'images-required'\n | 'images-and-pdfs-optional'\nexport type BrandlabLocationStrategy =\n | 'none'\n | 'approved-service-area'\n | 'physical-location'\n | 'multi-location'\nexport type BrandlabReviewsProvider = 'google-places' | 'project-owned'\nexport type BrandlabGoogleReviewsMode = 'link-only' | 'summary' | 'full'\n\nexport type BrandlabReviewsCapability =\n | { provider: 'project-owned' }\n | {\n provider: 'google-places'\n mode: BrandlabGoogleReviewsMode\n /** Optional presentation policy. Five-star selection is valid only in full mode. */\n selection?: GooglePlaceReviewsSelection\n /** Stable route IDs where review proof is intentionally presented. */\n routeIds: readonly string[]\n }\n\nexport interface BrandlabException {\n ruleId: string\n rationale: string\n owner: 'Romie'\n evidence: string\n expiresAt?: string\n reviewTrigger: string\n}\n\nexport type BrandlabSitemapConfig =\n | { mode: 'single' }\n | {\n mode: 'split'\n families: readonly string[]\n reason: string\n }\n\nexport interface BrandlabProjectConfig {\n projectId: string\n routes: {\n families: readonly BrandlabRouteFamily[]\n }\n capabilities?: {\n blog?: { adapter: BrandlabBlogAdapter }\n contact?: {\n fieldProfile: string\n attachments: BrandlabContactAttachmentMode\n }\n locations?: { strategy: BrandlabLocationStrategy }\n reviews?: BrandlabReviewsCapability\n }\n sitemap?: BrandlabSitemapConfig\n searchCertification: {\n representativeRoutes: {\n home: string\n contact?: string\n service?: string\n location?: string\n blog?: string\n }\n }\n exceptions?: readonly BrandlabException[]\n}\n\nconst PROJECT_CONFIG_KEYS = [\n 'projectId',\n 'routes',\n 'capabilities',\n 'sitemap',\n 'searchCertification',\n 'exceptions',\n] as const\nconst ROUTE_FAMILIES = new Set<BrandlabRouteFamily>([\n 'services',\n 'locations',\n 'service-locations',\n 'blog',\n 'projects',\n 'legal',\n])\nconst BLOG_ADAPTERS = new Set<BrandlabBlogAdapter>(['none', 'mdx', 'structured'])\nconst ATTACHMENT_MODES = new Set<BrandlabContactAttachmentMode>([\n 'none',\n 'images-optional',\n 'images-required',\n 'images-and-pdfs-optional',\n])\nconst LOCATION_STRATEGIES = new Set<BrandlabLocationStrategy>([\n 'none',\n 'approved-service-area',\n 'physical-location',\n 'multi-location',\n])\nconst REVIEWS_PROVIDERS = new Set<BrandlabReviewsProvider>([\n 'google-places',\n 'project-owned',\n])\nconst GOOGLE_REVIEWS_MODES = new Set<BrandlabGoogleReviewsMode>([\n 'link-only',\n 'summary',\n 'full',\n])\nconst GOOGLE_REVIEWS_SELECTIONS = new Set<GooglePlaceReviewsSelection>([\n 'relevance',\n 'five-star-featured',\n])\n\nfunction assertNonEmpty(value: string, field: string) {\n if (!value.trim()) {\n throw new Error(`${field} cannot be empty.`)\n }\n}\n\nfunction assertRootRelativePath(path: string, field: string) {\n try {\n normalizeBrandlabRoutePath(path)\n } catch {\n throw new Error(`${field} must be a root-relative path without a query string or hash.`)\n }\n}\n\nfunction assertKnownKeys(value: object, allowed: readonly string[], field: string, hint = '') {\n const unsupported = Object.keys(value).filter((key) => !allowed.includes(key))\n if (unsupported.length > 0) {\n throw new Error(`${field} contains unsupported keys: ${unsupported.join(', ')}.${hint}`)\n }\n}\n\nfunction assertUnique(values: readonly string[], field: string) {\n const duplicate = values.find((value, index) => values.indexOf(value) !== index)\n if (duplicate) {\n throw new Error(`${field} contains duplicate value \"${duplicate}\".`)\n }\n}\n\n/**\n * Defines project-owned variation only. Runtime versions and global Brandlab\n * policy deliberately do not belong in this object.\n */\nexport function defineBrandlabProject<const T extends BrandlabProjectConfig>(config: T): T {\n if (!config || typeof config !== 'object' || Array.isArray(config)) {\n throw new TypeError('defineBrandlabProject requires a project configuration.')\n }\n assertKnownKeys(config, PROJECT_CONFIG_KEYS, 'Project configuration')\n assertNonEmpty(config.projectId, 'projectId')\n\n assertBrandlabCanonicalEntityId(config.projectId)\n\n if (!config.routes || typeof config.routes !== 'object') {\n throw new Error('routes.families is required.')\n }\n assertKnownKeys(\n config.routes,\n ['families'],\n 'routes',\n ' Redirects belong in the src/data/redirects.ts table.',\n )\n assertUnique(config.routes.families, 'routes.families')\n config.routes.families.forEach((family, index) => {\n if (!ROUTE_FAMILIES.has(family)) {\n throw new Error(`routes.families[${index}] is unsupported.`)\n }\n })\n\n const representativeRoutes = config.searchCertification.representativeRoutes\n Object.entries(representativeRoutes).forEach(([name, path]) => {\n if (path) assertRootRelativePath(path, `searchCertification.representativeRoutes.${name}`)\n })\n if (representativeRoutes.home !== '/') {\n throw new Error('searchCertification.representativeRoutes.home must be \"/\".')\n }\n\n if (config.capabilities?.contact) {\n assertNonEmpty(config.capabilities.contact.fieldProfile, 'contact.fieldProfile')\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(config.capabilities.contact.fieldProfile)) {\n throw new Error('contact.fieldProfile must be lowercase kebab-case.')\n }\n if (!ATTACHMENT_MODES.has(config.capabilities.contact.attachments)) {\n throw new Error('contact.attachments is unsupported.')\n }\n }\n if (\n config.capabilities?.blog &&\n !BLOG_ADAPTERS.has(config.capabilities.blog.adapter)\n ) {\n throw new Error('blog.adapter is unsupported.')\n }\n if (\n config.capabilities?.locations &&\n !LOCATION_STRATEGIES.has(config.capabilities.locations.strategy)\n ) {\n throw new Error('locations.strategy is unsupported.')\n }\n if (\n config.capabilities?.reviews &&\n !REVIEWS_PROVIDERS.has(config.capabilities.reviews.provider)\n ) {\n throw new Error('reviews.provider is unsupported.')\n }\n if (\n config.capabilities?.reviews?.provider === 'google-places' &&\n !config.routes.families.includes('legal')\n ) {\n throw new Error('Google Places reviews require the legal route family.')\n }\n if (config.capabilities?.reviews?.provider === 'google-places') {\n const reviews = config.capabilities.reviews\n if (!GOOGLE_REVIEWS_MODES.has(reviews.mode)) {\n throw new Error('reviews.mode is unsupported.')\n }\n if (reviews.selection && !GOOGLE_REVIEWS_SELECTIONS.has(reviews.selection)) {\n throw new Error('reviews.selection is unsupported.')\n }\n if (reviews.selection === 'five-star-featured' && reviews.mode !== 'full') {\n throw new Error('Five-star featured reviews require full mode.')\n }\n if (!Array.isArray(reviews.routeIds) || reviews.routeIds.length === 0) {\n throw new Error('Google Places reviews require at least one approved route ID.')\n }\n assertUnique(reviews.routeIds, 'reviews.routeIds')\n reviews.routeIds.forEach((routeId, index) => {\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(routeId)) {\n throw new Error(`reviews.routeIds[${index}] must be lowercase kebab-case.`)\n }\n })\n }\n\n if (config.sitemap?.mode === 'split') {\n if (config.sitemap.families.length === 0) {\n throw new Error('Split sitemap configuration requires at least one family.')\n }\n assertUnique(config.sitemap.families, 'sitemap.families')\n config.sitemap.families.forEach((path, index) =>\n assertRootRelativePath(path, `sitemap.families[${index}]`),\n )\n assertNonEmpty(config.sitemap.reason, 'sitemap.reason')\n }\n\n assertUnique(\n (config.exceptions ?? []).map((exception) => exception.ruleId),\n 'exceptions.ruleId',\n )\n\n ;(config.exceptions ?? []).forEach((exception, index) => {\n assertNonEmpty(exception.ruleId, `exceptions[${index}].ruleId`)\n assertNonEmpty(exception.rationale, `exceptions[${index}].rationale`)\n assertNonEmpty(exception.evidence, `exceptions[${index}].evidence`)\n assertNonEmpty(exception.reviewTrigger, `exceptions[${index}].reviewTrigger`)\n if (exception.owner !== 'Romie') {\n throw new Error(`exceptions[${index}].owner must be \"Romie\".`)\n }\n\n if (exception.expiresAt && Number.isNaN(Date.parse(exception.expiresAt))) {\n throw new Error(`exceptions[${index}].expiresAt must be a valid date.`)\n }\n })\n\n return config\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type BrandlabRouteCatalog } from './routes.js';
|
|
2
|
+
export type BrandlabRedirectStatus = 301 | 308;
|
|
3
|
+
/**
|
|
4
|
+
* One permanent redirect. `source` is a root-relative path or Next path
|
|
5
|
+
* pattern (`:slug`, `:path*`) written without a trailing slash; the Next
|
|
6
|
+
* adapter adds the single-hop trailing-slash variant.
|
|
7
|
+
*/
|
|
8
|
+
export interface BrandlabRedirect {
|
|
9
|
+
source: string;
|
|
10
|
+
/** Root-relative path (a query string or fragment is allowed) or an absolute HTTPS URL. */
|
|
11
|
+
destination: string;
|
|
12
|
+
/** 301 and 308 are both permanent; the existing status is preserved exactly. */
|
|
13
|
+
status: BrandlabRedirectStatus;
|
|
14
|
+
/** Applies only when the request Host header matches, for example a retired domain. */
|
|
15
|
+
host?: string;
|
|
16
|
+
}
|
|
17
|
+
export type BrandlabRedirectTable = readonly BrandlabRedirect[];
|
|
18
|
+
export type BrandlabRedirectIssueKind = 'invalid-entry' | 'invalid-source' | 'invalid-destination' | 'invalid-status' | 'invalid-host' | 'unknown-destination-parameter' | 'duplicate-source' | 'redirect-loop' | 'redirect-chain' | 'shadows-route' | 'unknown-destination';
|
|
19
|
+
export interface BrandlabRedirectIssue {
|
|
20
|
+
kind: BrandlabRedirectIssueKind;
|
|
21
|
+
index: number;
|
|
22
|
+
source: string;
|
|
23
|
+
detail: string;
|
|
24
|
+
}
|
|
25
|
+
/** Validates the single redirect table without inspecting the route catalogue. */
|
|
26
|
+
export declare function validateRedirects(redirects: BrandlabRedirectTable): BrandlabRedirectIssue[];
|
|
27
|
+
/**
|
|
28
|
+
* Declares the project's only redirect table (`src/data/redirects.ts`). Fails
|
|
29
|
+
* on malformed entries, duplicate sources, loops and multi-hop chains.
|
|
30
|
+
*/
|
|
31
|
+
export declare function defineRedirects<const T extends BrandlabRedirectTable>(redirects: T): T;
|
|
32
|
+
/**
|
|
33
|
+
* Checks the redirect table against the route catalogue: a redirect must not
|
|
34
|
+
* shadow a catalogued page, and every literal internal destination must be a
|
|
35
|
+
* catalogued page. Pattern destinations and absolute URLs are not resolved.
|
|
36
|
+
*/
|
|
37
|
+
export declare function validateRedirectsAgainstRoutes(redirects: BrandlabRedirectTable, routes: BrandlabRouteCatalog): BrandlabRedirectIssue[];
|
|
38
|
+
/** Throws with every route-catalogue mismatch listed. */
|
|
39
|
+
export declare function assertRedirectsAgainstRoutes(redirects: BrandlabRedirectTable, routes: BrandlabRouteCatalog): void;
|
|
40
|
+
//# sourceMappingURL=redirects.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirects.d.ts","sourceRoot":"","sources":["../src/redirects.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAGnF,MAAM,MAAM,sBAAsB,GAAG,GAAG,GAAG,GAAG,CAAA;AAE9C;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,2FAA2F;IAC3F,WAAW,EAAE,MAAM,CAAA;IACnB,gFAAgF;IAChF,MAAM,EAAE,sBAAsB,CAAA;IAC9B,uFAAuF;IACvF,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,qBAAqB,GAAG,SAAS,gBAAgB,EAAE,CAAA;AAE/D,MAAM,MAAM,yBAAyB,GACjC,eAAe,GACf,gBAAgB,GAChB,qBAAqB,GACrB,gBAAgB,GAChB,cAAc,GACd,+BAA+B,GAC/B,kBAAkB,GAClB,eAAe,GACf,gBAAgB,GAChB,eAAe,GACf,qBAAqB,CAAA;AAEzB,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,yBAAyB,CAAA;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf;AAmRD,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,qBAAqB,GAAG,qBAAqB,EAAE,CAE3F;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,CAAC,CAAC,SAAS,qBAAqB,EAAE,SAAS,EAAE,CAAC,GAAG,CAAC,CAItF;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,qBAAqB,EAChC,MAAM,EAAE,oBAAoB,GAC3B,qBAAqB,EAAE,CAgCzB;AAED,yDAAyD;AACzD,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,qBAAqB,EAChC,MAAM,EAAE,oBAAoB,GAC3B,IAAI,CAKN"}
|