Compare commits
39
Commits
v1.2.0
..
4a46651839
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a46651839 | ||
|
|
e7f06548fb | ||
|
|
e2a703fc72 | ||
|
|
de75d8374d | ||
|
|
b807fbe69a | ||
|
|
827cd9bcbc | ||
|
|
fa2b607979 | ||
|
|
4fe690e1e2 | ||
|
|
3553daa086 | ||
|
|
a4c5bcfbdf | ||
|
|
026c2696b6 | ||
|
|
6cb4168f44 | ||
|
|
b82ef82f50 | ||
|
|
9a18434f4a | ||
|
|
f126eacf8c | ||
|
|
848b12ce5d | ||
|
|
3d350bd9e5 | ||
|
|
e03dd8c5a6 | ||
|
|
9101a5b48e | ||
|
|
1186f4f620 | ||
|
|
988fcaf855 | ||
|
|
b27031f7c4 | ||
|
|
7ee60e7313 | ||
|
|
671090b1b1 | ||
|
|
0f03bb7446 | ||
|
|
55a1e5bf8f | ||
|
|
33ae035adc | ||
|
|
87d575f335 | ||
|
|
e4329f76b1 | ||
|
|
fa4cf20022 | ||
|
|
2ecfc2e12d | ||
|
|
db2ac80a1e | ||
|
|
99490bfc4d | ||
|
|
fce17654f8 | ||
|
|
419207ac12 | ||
|
|
e30ac71044 | ||
|
|
781e348ded | ||
|
|
e16a18e488 | ||
|
|
7cd3cbaae5 |
Vendored
+5
-2
@@ -1,5 +1,7 @@
|
||||
export { MaskedField } from '../globals/SiteIntegrations/components/MaskedField.js';
|
||||
export { TestEmailButton } from '../globals/SiteIntegrations/components/TestEmailButton.js';
|
||||
export { AccessibilityProvider, AccessibilityWidget, useAccessibility, } from '../modules/accessibility/client.js';
|
||||
export type { A11yClassNames, A11yState, A11yTexts } from '../modules/accessibility/client.js';
|
||||
export { Analytics } from '../modules/analytics/client.js';
|
||||
/**
|
||||
* Entry point: ipal-kit/client
|
||||
@@ -10,7 +12,8 @@ export { Analytics } from '../modules/analytics/client.js';
|
||||
*/
|
||||
export { ConsentProvider, CookieBanner, CookieButton, useConsent, useConsentContext, } from '../modules/consent/client.js';
|
||||
export type { CookieBannerClassNames } from '../modules/consent/client.js';
|
||||
export { buildLocalizedPath, getLocaleCodes, getLocalizedSlugs, switchLocalePath, } from '../modules/i18n/index.js';
|
||||
export type { I18nConfig, LocalizedSlugs } from '../modules/i18n/index.js';
|
||||
export { Turnstile } from '../modules/turnstile/client.js';
|
||||
export { TurnstileProvider, useTurnstile } from '../modules/turnstile/client.js';
|
||||
export type { TurnstileProps } from '../modules/turnstile/client.js';
|
||||
export { resolveFormMessage } from '../modules/notifications/resolveFormMessage.js';
|
||||
export type { FormNotificationTexts } from '../modules/notifications/types.js';
|
||||
|
||||
Vendored
+5
-1
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
export { MaskedField } from '../globals/SiteIntegrations/components/MaskedField.js';
|
||||
export { TestEmailButton } from '../globals/SiteIntegrations/components/TestEmailButton.js';
|
||||
export { AccessibilityProvider, AccessibilityWidget, useAccessibility } from '../modules/accessibility/client.js';
|
||||
export { Analytics } from '../modules/analytics/client.js';
|
||||
/**
|
||||
* Entry point: ipal-kit/client
|
||||
@@ -9,7 +10,10 @@ export { Analytics } from '../modules/analytics/client.js';
|
||||
* components. Kept separate from the main entry so server bundles don't pull in
|
||||
* client-only code.
|
||||
*/ export { ConsentProvider, CookieBanner, CookieButton, useConsent, useConsentContext } from '../modules/consent/client.js';
|
||||
// Pure i18n path helpers — no server/RSC deps, safe to import in client
|
||||
// components (e.g. a LanguageSwitcher that computes locale URLs on the client).
|
||||
export { buildLocalizedPath, getLocaleCodes, getLocalizedSlugs, switchLocalePath } from '../modules/i18n/index.js';
|
||||
export { Turnstile } from '../modules/turnstile/client.js';
|
||||
export { resolveFormMessage } from '../modules/notifications/resolveFormMessage.js';
|
||||
export { TurnstileProvider, useTurnstile } from '../modules/turnstile/client.js';
|
||||
|
||||
//# sourceMappingURL=client.js.map
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["'use client'\nexport { MaskedField } from '../globals/SiteIntegrations/components/MaskedField.js'\nexport { TestEmailButton } from '../globals/SiteIntegrations/components/TestEmailButton.js'\nexport { Analytics } from '../modules/analytics/client.js'\n/**\n * Entry point: ipal-kit/client\n *\n * Client-side ('use client') exports — React hooks, providers, and UI\n * components. Kept separate from the main entry so server bundles don't pull in\n * client-only code.\n */\nexport {\n ConsentProvider,\n CookieBanner,\n CookieButton,\n useConsent,\n useConsentContext,\n} from '../modules/consent/client.js'\nexport type { CookieBannerClassNames } from '../modules/consent/client.js'\nexport { Turnstile } from '../modules/turnstile/client.js'\nexport type { TurnstileProps } from '../modules/turnstile/client.js'\n\nexport { resolveFormMessage } from '../modules/notifications/resolveFormMessage.js'\nexport type { FormNotificationTexts } from '../modules/notifications/types.js'\n"],"names":["MaskedField","TestEmailButton","Analytics","ConsentProvider","CookieBanner","CookieButton","useConsent","useConsentContext","Turnstile","resolveFormMessage"],"mappings":"AAAA;AACA,SAASA,WAAW,QAAQ,wDAAuD;AACnF,SAASC,eAAe,QAAQ,4DAA2D;AAC3F,SAASC,SAAS,QAAQ,iCAAgC;AAC1D;;;;;;CAMC,GACD,SACEC,eAAe,EACfC,YAAY,EACZC,YAAY,EACZC,UAAU,EACVC,iBAAiB,QACZ,+BAA8B;AAErC,SAASC,SAAS,QAAQ,iCAAgC;AAG1D,SAASC,kBAAkB,QAAQ,iDAAgD"}
|
||||
{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["'use client'\nexport { MaskedField } from '../globals/SiteIntegrations/components/MaskedField.js'\nexport { TestEmailButton } from '../globals/SiteIntegrations/components/TestEmailButton.js'\nexport {\n AccessibilityProvider,\n AccessibilityWidget,\n useAccessibility,\n} from '../modules/accessibility/client.js'\nexport type { A11yClassNames, A11yState, A11yTexts } from '../modules/accessibility/client.js'\n\nexport { Analytics } from '../modules/analytics/client.js'\n/**\n * Entry point: ipal-kit/client\n *\n * Client-side ('use client') exports — React hooks, providers, and UI\n * components. Kept separate from the main entry so server bundles don't pull in\n * client-only code.\n */\nexport {\n ConsentProvider,\n CookieBanner,\n CookieButton,\n useConsent,\n useConsentContext,\n} from '../modules/consent/client.js'\nexport type { CookieBannerClassNames } from '../modules/consent/client.js'\n// Pure i18n path helpers — no server/RSC deps, safe to import in client\n// components (e.g. a LanguageSwitcher that computes locale URLs on the client).\nexport {\n buildLocalizedPath,\n getLocaleCodes,\n getLocalizedSlugs,\n switchLocalePath,\n} from '../modules/i18n/index.js'\nexport type { I18nConfig, LocalizedSlugs } from '../modules/i18n/index.js'\nexport { Turnstile } from '../modules/turnstile/client.js'\nexport { TurnstileProvider, useTurnstile } from '../modules/turnstile/client.js'\nexport type { TurnstileProps } from '../modules/turnstile/client.js'\n"],"names":["MaskedField","TestEmailButton","AccessibilityProvider","AccessibilityWidget","useAccessibility","Analytics","ConsentProvider","CookieBanner","CookieButton","useConsent","useConsentContext","buildLocalizedPath","getLocaleCodes","getLocalizedSlugs","switchLocalePath","Turnstile","TurnstileProvider","useTurnstile"],"mappings":"AAAA;AACA,SAASA,WAAW,QAAQ,wDAAuD;AACnF,SAASC,eAAe,QAAQ,4DAA2D;AAC3F,SACEC,qBAAqB,EACrBC,mBAAmB,EACnBC,gBAAgB,QACX,qCAAoC;AAG3C,SAASC,SAAS,QAAQ,iCAAgC;AAC1D;;;;;;CAMC,GACD,SACEC,eAAe,EACfC,YAAY,EACZC,YAAY,EACZC,UAAU,EACVC,iBAAiB,QACZ,+BAA8B;AAErC,wEAAwE;AACxE,gFAAgF;AAChF,SACEC,kBAAkB,EAClBC,cAAc,EACdC,iBAAiB,EACjBC,gBAAgB,QACX,2BAA0B;AAEjC,SAASC,SAAS,QAAQ,iCAAgC;AAC1D,SAASC,iBAAiB,EAAEC,YAAY,QAAQ,iCAAgC"}
|
||||
Vendored
+2
-2
@@ -6,11 +6,11 @@ import { notificationsFields } from './fields.js';
|
||||
*/ export function buildNotifications() {
|
||||
return {
|
||||
slug: 'notifications',
|
||||
label: 'Notifications',
|
||||
access: {
|
||||
read: ()=>true
|
||||
},
|
||||
fields: notificationsFields
|
||||
fields: notificationsFields,
|
||||
label: 'Notifications'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"sources":["../../../src/globals/Notifications/index.ts"],"sourcesContent":["import type { GlobalConfig } from 'payload'\nimport { notificationsFields } from './fields.js'\n\n/**\n * Builds the Notifications global — localized action-result texts. Readable by\n * any authenticated panel user; server-side helpers read it with overrideAccess\n * so the frontend can resolve texts without a session.\n */\nexport function buildNotifications(): GlobalConfig {\n return {\n slug: 'notifications',\n label: 'Notifications',\n access: {\n read: () => true, // texts are public-facing (shown to end users)\n },\n fields: notificationsFields,\n }\n}\n"],"names":["notificationsFields","buildNotifications","slug","label","access","read","fields"],"mappings":"AACA,SAASA,mBAAmB,QAAQ,cAAa;AAEjD;;;;CAIC,GACD,OAAO,SAASC;IACd,OAAO;QACLC,MAAM;QACNC,OAAO;QACPC,QAAQ;YACNC,MAAM,IAAM;QACd;QACAC,QAAQN;IACV;AACF"}
|
||||
{"version":3,"sources":["../../../src/globals/Notifications/index.ts"],"sourcesContent":["import type { GlobalConfig } from 'payload'\n\nimport { notificationsFields } from './fields.js'\n\n/**\n * Builds the Notifications global — localized action-result texts. Readable by\n * any authenticated panel user; server-side helpers read it with overrideAccess\n * so the frontend can resolve texts without a session.\n */\nexport function buildNotifications(): GlobalConfig {\n return {\n slug: 'notifications',\n access: {\n read: () => true, // texts are public-facing (shown to end users)\n },\n fields: notificationsFields,\n label: 'Notifications',\n }\n}\n"],"names":["notificationsFields","buildNotifications","slug","access","read","fields","label"],"mappings":"AAEA,SAASA,mBAAmB,QAAQ,cAAa;AAEjD;;;;CAIC,GACD,OAAO,SAASC;IACd,OAAO;QACLC,MAAM;QACNC,QAAQ;YACNC,MAAM,IAAM;QACd;QACAC,QAAQL;QACRM,OAAO;IACT;AACF"}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import type { Field } from 'payload';
|
||||
/**
|
||||
* General site identity fields.
|
||||
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo, favicon).
|
||||
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo).
|
||||
*/
|
||||
export declare const generalFields: Field[];
|
||||
|
||||
+11
-4
@@ -1,7 +1,7 @@
|
||||
import { validateFaviconField } from '../../../modules/seo/index.js';
|
||||
/**
|
||||
* General site identity fields.
|
||||
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo, favicon).
|
||||
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo).
|
||||
*/ export const generalFields = [
|
||||
{
|
||||
name: 'siteName',
|
||||
@@ -12,6 +12,14 @@ import { validateFaviconField } from '../../../modules/seo/index.js';
|
||||
localized: true,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'siteDescription',
|
||||
type: 'textarea',
|
||||
admin: {
|
||||
description: 'Fallback meta description when a page has none. Also used for llms.txt and Open Graph.'
|
||||
},
|
||||
localized: true
|
||||
},
|
||||
{
|
||||
name: 'titleOrder',
|
||||
type: 'select',
|
||||
@@ -64,7 +72,7 @@ import { validateFaviconField } from '../../../modules/seo/index.js';
|
||||
name: 'logo',
|
||||
type: 'upload',
|
||||
admin: {
|
||||
description: 'Primary site logo. Also used for Organization structured data (buildOrganizationJsonLd).'
|
||||
description: 'Primary site logo.'
|
||||
},
|
||||
relationTo: 'media'
|
||||
},
|
||||
@@ -83,8 +91,7 @@ import { validateFaviconField } from '../../../modules/seo/index.js';
|
||||
description: 'Square icon, PNG or SVG, min. 48×48px (Google requires this to show it in search). Rendered via buildIconsMetadata.'
|
||||
},
|
||||
hooks: {
|
||||
// Warns the editor at save time if the favicon is too small (<48×48) or
|
||||
// not square — Google won't display such favicons in search results.
|
||||
// Warn the editor if the favicon is too small / not square for Google.
|
||||
beforeValidate: [
|
||||
validateFaviconField
|
||||
]
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"sources":["../../../../src/globals/SiteSettings/fields/general.ts"],"sourcesContent":["import type { Field } from 'payload'\n\nimport { validateFaviconField } from '../../../modules/seo/index.js'\n\n/**\n * General site identity fields.\n * Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo, favicon).\n */\nexport const generalFields: Field[] = [\n {\n name: 'siteName',\n type: 'text',\n admin: {\n description: 'Used in page titles and Open Graph metadata.',\n },\n localized: true,\n required: true,\n },\n {\n name: 'titleOrder',\n type: 'select',\n admin: {\n description: 'Which comes first in browser tabs.',\n },\n defaultValue: 'page-first',\n options: [\n { label: 'Page first — About Us | Acme', value: 'page-first' },\n { label: 'Site first — Acme | About Us', value: 'site-first' },\n ],\n },\n {\n name: 'titleSeparator',\n type: 'select',\n admin: {\n description: 'Separates the page title from the site name in browser tabs.',\n },\n defaultValue: '|',\n options: [\n { label: 'Pipe — Page | Site', value: '|' },\n { label: 'Dash — Page – Site', value: '–' },\n { label: 'Hyphen — Page - Site', value: '-' },\n { label: 'Bullet — Page · Site', value: '·' },\n { label: 'Slash — Page / Site', value: '/' },\n ],\n },\n {\n name: 'logo',\n type: 'upload',\n admin: {\n description:\n 'Primary site logo. Also used for Organization structured data (buildOrganizationJsonLd).',\n },\n relationTo: 'media',\n },\n {\n name: 'defaultShareImage',\n type: 'upload',\n admin: {\n description: 'Fallback Open Graph image when a page has none.',\n },\n relationTo: 'media',\n },\n {\n name: 'favicon',\n type: 'upload',\n admin: {\n description:\n 'Square icon, PNG or SVG, min. 48×48px (Google requires this to show it in search). Rendered via buildIconsMetadata.',\n },\n hooks: {\n // Warns the editor at save time if the favicon is too small (<48×48) or\n // not square — Google won't display such favicons in search results.\n beforeValidate: [validateFaviconField],\n },\n relationTo: 'media',\n },\n]\n"],"names":["validateFaviconField","generalFields","name","type","admin","description","localized","required","defaultValue","options","label","value","relationTo","hooks","beforeValidate"],"mappings":"AAEA,SAASA,oBAAoB,QAAQ,gCAA+B;AAEpE;;;CAGC,GACD,OAAO,MAAMC,gBAAyB;IACpC;QACEC,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAC,WAAW;QACXC,UAAU;IACZ;IACA;QACEL,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAG,cAAc;QACdC,SAAS;YACP;gBAAEC,OAAO;gBAAkCC,OAAO;YAAa;YAC/D;gBAAED,OAAO;gBAAkCC,OAAO;YAAa;SAChE;IACH;IACA;QACET,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAG,cAAc;QACdC,SAAS;YACP;gBAAEC,OAAO;gBAAwBC,OAAO;YAAI;YAC5C;gBAAED,OAAO;gBAAwBC,OAAO;YAAI;YAC5C;gBAAED,OAAO;gBAA0BC,OAAO;YAAI;YAC9C;gBAAED,OAAO;gBAA0BC,OAAO;YAAI;YAC9C;gBAAED,OAAO;gBAAyBC,OAAO;YAAI;SAC9C;IACH;IACA;QACET,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aACE;QACJ;QACAO,YAAY;IACd;IACA;QACEV,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAO,YAAY;IACd;IACA;QACEV,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aACE;QACJ;QACAQ,OAAO;YACL,wEAAwE;YACxE,qEAAqE;YACrEC,gBAAgB;gBAACd;aAAqB;QACxC;QACAY,YAAY;IACd;CACD,CAAA"}
|
||||
{"version":3,"sources":["../../../../src/globals/SiteSettings/fields/general.ts"],"sourcesContent":["import type { Field } from 'payload'\n\nimport { validateFaviconField } from '../../../modules/seo/index.js'\n\n/**\n * General site identity fields.\n * Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo).\n */\nexport const generalFields: Field[] = [\n {\n name: 'siteName',\n type: 'text',\n admin: {\n description: 'Used in page titles and Open Graph metadata.',\n },\n localized: true,\n required: true,\n },\n {\n name: 'siteDescription',\n type: 'textarea',\n admin: {\n description:\n 'Fallback meta description when a page has none. Also used for llms.txt and Open Graph.',\n },\n localized: true,\n },\n {\n name: 'titleOrder',\n type: 'select',\n admin: {\n description: 'Which comes first in browser tabs.',\n },\n defaultValue: 'page-first',\n options: [\n { label: 'Page first — About Us | Acme', value: 'page-first' },\n { label: 'Site first — Acme | About Us', value: 'site-first' },\n ],\n },\n {\n name: 'titleSeparator',\n type: 'select',\n admin: {\n description: 'Separates the page title from the site name in browser tabs.',\n },\n defaultValue: '|',\n options: [\n { label: 'Pipe — Page | Site', value: '|' },\n { label: 'Dash — Page – Site', value: '–' },\n { label: 'Hyphen — Page - Site', value: '-' },\n { label: 'Bullet — Page · Site', value: '·' },\n { label: 'Slash — Page / Site', value: '/' },\n ],\n },\n {\n name: 'logo',\n type: 'upload',\n admin: {\n description: 'Primary site logo.',\n },\n relationTo: 'media',\n },\n {\n name: 'defaultShareImage',\n type: 'upload',\n admin: {\n description: 'Fallback Open Graph image when a page has none.',\n },\n relationTo: 'media',\n },\n {\n name: 'favicon',\n type: 'upload',\n admin: {\n description:\n 'Square icon, PNG or SVG, min. 48×48px (Google requires this to show it in search). Rendered via buildIconsMetadata.',\n },\n hooks: {\n // Warn the editor if the favicon is too small / not square for Google.\n beforeValidate: [validateFaviconField],\n },\n relationTo: 'media',\n },\n]\n"],"names":["validateFaviconField","generalFields","name","type","admin","description","localized","required","defaultValue","options","label","value","relationTo","hooks","beforeValidate"],"mappings":"AAEA,SAASA,oBAAoB,QAAQ,gCAA+B;AAEpE;;;CAGC,GACD,OAAO,MAAMC,gBAAyB;IACpC;QACEC,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAC,WAAW;QACXC,UAAU;IACZ;IACA;QACEL,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aACE;QACJ;QACAC,WAAW;IACb;IACA;QACEJ,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAG,cAAc;QACdC,SAAS;YACP;gBAAEC,OAAO;gBAAkCC,OAAO;YAAa;YAC/D;gBAAED,OAAO;gBAAkCC,OAAO;YAAa;SAChE;IACH;IACA;QACET,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAG,cAAc;QACdC,SAAS;YACP;gBAAEC,OAAO;gBAAwBC,OAAO;YAAI;YAC5C;gBAAED,OAAO;gBAAwBC,OAAO;YAAI;YAC5C;gBAAED,OAAO;gBAA0BC,OAAO;YAAI;YAC9C;gBAAED,OAAO;gBAA0BC,OAAO;YAAI;YAC9C;gBAAED,OAAO;gBAAyBC,OAAO;YAAI;SAC9C;IACH;IACA;QACET,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAO,YAAY;IACd;IACA;QACEV,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAO,YAAY;IACd;IACA;QACEV,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aACE;QACJ;QACAQ,OAAO;YACL,uEAAuE;YACvEC,gBAAgB;gBAACd;aAAqB;QACxC;QACAY,YAAY;IACd;CACD,CAAA"}
|
||||
Vendored
+12
-3
@@ -1,5 +1,7 @@
|
||||
export type { AccessOption, Role } from './modules/access/index.js';
|
||||
export { adminOnly, adminOnlyField, adminOrEditor, adminOrEditorField, adminOrSelf, authenticated, hasMinimumRole, isAdmin, isEditor, requireRole, requireRoleField, ROLE_HIERARCHY, } from './modules/access/index.js';
|
||||
export { A11Y_COOKIE, A11Y_DEFAULT, a11yAttributes, parseA11y, serializeA11y, } from './modules/accessibility/index.js';
|
||||
export type { A11yState } from './modules/accessibility/index.js';
|
||||
export type { AnalyticsConfig } from './modules/analytics/index.js';
|
||||
export { getAnalyticsConfig } from './modules/analytics/index.js';
|
||||
export { ACCEPT_ALL_CONSENT, CONSENT_CATEGORIES, CONSENT_COOKIE, CONSENT_MAX_AGE, CONSENT_VERSION, DEFAULT_CONSENT, getConsentTexts, parseConsent, REJECT_ALL_CONSENT, serializeConsent, setDefaultConsent, updateConsent, } from './modules/consent/index.js';
|
||||
@@ -16,25 +18,32 @@ export type { PanelSmtpAdapterArgs } from './modules/email/panelSmtpAdapter.js';
|
||||
export { buildFormsPlugin } from './modules/forms/formsPluginConfig.js';
|
||||
export type { FormsCollectionOverrides, FormsFieldsOverride, FormsOption, } from './modules/forms/types.js';
|
||||
export { createContentHelpers } from './modules/frontend/index.js';
|
||||
export { buildPreventDeleteSystemPage, buildRevalidateHook, buildValidateUniqueRole, setPublishedAtHook, trackSlugHistoryHook, } from './modules/hooks/index.js';
|
||||
export type { I18nConfig, LocaleDefinition, LocalizedSlugs } from './modules/i18n/index.js';
|
||||
export { buildLocalizedPath, getDefaultLocale, getLocaleCodes, getLocaleDefinition, getLocalizedSlugs, isValidLocale, LOCALE_COOKIE_NAME, matchAcceptLanguage, negotiateLocale, switchLocalePath, } from './modules/i18n/index.js';
|
||||
export type { LocaleMiddlewareResult } from './modules/i18n/index.js';
|
||||
export { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from './modules/i18n/index.js';
|
||||
export { normalizeFilename, normalizeFilenameHook } from './modules/media/index.js';
|
||||
export { getNotificationTexts, NOTIFICATION_FALLBACK, resolveFormMessage, } from './modules/notifications/index.js';
|
||||
export type { FormNotificationTexts, NotificationsData, NotificationTexts, } from './modules/notifications/index.js';
|
||||
export type { FormNotificationTexts, NotificationTexts } from './modules/notifications/index.js';
|
||||
export type { PagesOption, SystemPageRole } from './modules/pages/index.js';
|
||||
export { ALL_SYSTEM_PAGE_ROLES, getSystemPagePath } from './modules/pages/index.js';
|
||||
export type { GlobalQueryOptions } from './modules/payload/index.js';
|
||||
export { getGlobal, getSiteIntegrations, getSiteSettings, SITE_INTEGRATIONS_SLUG, SITE_SETTINGS_SLUG, } from './modules/payload/index.js';
|
||||
export { buildSecurityHeaders } from './modules/security/index.js';
|
||||
export { buildCsp } from './modules/security/index.js';
|
||||
export type { BuildCspArgs } from './modules/security/index.js';
|
||||
export type { BuildSecurityHeadersArgs, SecurityHeader } from './modules/security/index.js';
|
||||
export { buildFaqJsonLd, buildIconsMetadata, buildLocalBusinessJsonLd, buildOrganizationJsonLd, buildServiceJsonLd, validateFaviconField, } from './modules/seo/index.js';
|
||||
export { buildBreadcrumbJsonLd, buildSiteNavigationJsonLd, buildWebSiteJsonLd, } from './modules/seo/index.js';
|
||||
export type { PageMetadata, SeoMeta, SeoOption } from './modules/seo/index.js';
|
||||
export { buildHreflangAlternates, buildMetadata, composeTitle } from './modules/seo/index.js';
|
||||
export type { AutoFillMapping, RobotsRules, SitemapEntry } from './modules/seo/index.js';
|
||||
export { buildAutoFillMetaHook, buildRobots, buildSitemapEntries, createMetadataGenerator, createPageMetadata, injectAutoFillMeta, } from './modules/seo/index.js';
|
||||
export { buildIconsMetadata, buildOrganizationJsonLd, validateFaviconField, } from './modules/seo/index.js';
|
||||
export { buildBreadcrumbJsonLd, buildSiteNavigationJsonLd, buildWebSiteJsonLd, } from './modules/seo/index.js';
|
||||
export { buildFaqJsonLd, buildLocalBusinessJsonLd, buildServiceJsonLd, } from './modules/seo/index.js';
|
||||
export { buildArticleJsonLd } from './modules/seo/index.js';
|
||||
export { buildSitemapXml } from './modules/seo/index.js';
|
||||
export { buildLlmsTxt } from './modules/seo/index.js';
|
||||
export { buildSlugField, toSlug } from './modules/slug/index.js';
|
||||
export { buildR2Storage } from './modules/storage/index.js';
|
||||
export { ipalKit } from './plugin.js';
|
||||
|
||||
Vendored
+10
-6
@@ -1,4 +1,5 @@
|
||||
export { adminOnly, adminOnlyField, adminOrEditor, adminOrEditorField, adminOrSelf, authenticated, hasMinimumRole, isAdmin, isEditor, requireRole, requireRoleField, ROLE_HIERARCHY } from './modules/access/index.js';
|
||||
export { A11Y_COOKIE, A11Y_DEFAULT, a11yAttributes, parseA11y, serializeA11y } from './modules/accessibility/index.js';
|
||||
export { getAnalyticsConfig } from './modules/analytics/index.js';
|
||||
export { ACCEPT_ALL_CONSENT, CONSENT_CATEGORIES, CONSENT_COOKIE, CONSENT_MAX_AGE, CONSENT_VERSION, DEFAULT_CONSENT, getConsentTexts, parseConsent, REJECT_ALL_CONSENT, serializeConsent, setDefaultConsent, updateConsent } from './modules/consent/index.js';
|
||||
export { archiveFieldName, buildArchivePath, buildEntryPath, getArchiveEntries, parsePageParam, resolveRoute } from './modules/content/index.js';
|
||||
@@ -10,22 +11,25 @@ export { mailAdapter } from './modules/email/mailAdapter.js';
|
||||
export { panelSmtpAdapter } from './modules/email/panelSmtpAdapter.js';
|
||||
export { buildFormsPlugin } from './modules/forms/formsPluginConfig.js';
|
||||
export { createContentHelpers } from './modules/frontend/index.js';
|
||||
export { buildPreventDeleteSystemPage, buildRevalidateHook, buildValidateUniqueRole, setPublishedAtHook, trackSlugHistoryHook } from './modules/hooks/index.js';
|
||||
export { buildLocalizedPath, getDefaultLocale, getLocaleCodes, getLocaleDefinition, getLocalizedSlugs, isValidLocale, LOCALE_COOKIE_NAME, matchAcceptLanguage, negotiateLocale, switchLocalePath } from './modules/i18n/index.js';
|
||||
export { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from './modules/i18n/index.js';
|
||||
// Media — filename normalization hook for upload collections (Media).
|
||||
export { normalizeFilename, normalizeFilenameHook } from './modules/media/index.js';
|
||||
export { getNotificationTexts, NOTIFICATION_FALLBACK, resolveFormMessage } from './modules/notifications/index.js';
|
||||
export { ALL_SYSTEM_PAGE_ROLES, getSystemPagePath } from './modules/pages/index.js';
|
||||
export { getGlobal, getSiteIntegrations, getSiteSettings, SITE_INTEGRATIONS_SLUG, SITE_SETTINGS_SLUG } from './modules/payload/index.js';
|
||||
export { buildSecurityHeaders } from './modules/security/index.js';
|
||||
export { buildFaqJsonLd, buildIconsMetadata, buildLocalBusinessJsonLd, buildOrganizationJsonLd, buildServiceJsonLd, validateFaviconField } from './modules/seo/index.js';
|
||||
// Structured data (schema.org JSON-LD) — brand/sitelink signals for Google.
|
||||
// WebSite (+ optional SearchAction), BreadcrumbList (per page), SiteNavigation.
|
||||
export { buildBreadcrumbJsonLd, buildSiteNavigationJsonLd, buildWebSiteJsonLd } from './modules/seo/index.js';
|
||||
export { buildCsp } from './modules/security/index.js';
|
||||
export { buildHreflangAlternates, buildMetadata, composeTitle } from './modules/seo/index.js';
|
||||
export { buildAutoFillMetaHook, buildRobots, buildSitemapEntries, createMetadataGenerator, createPageMetadata, injectAutoFillMeta } from './modules/seo/index.js';
|
||||
export { buildIconsMetadata, buildOrganizationJsonLd, validateFaviconField } from './modules/seo/index.js';
|
||||
export { buildBreadcrumbJsonLd, buildSiteNavigationJsonLd, buildWebSiteJsonLd } from './modules/seo/index.js';
|
||||
// Local SEO structured data — LocalBusiness (map pack), Service (offering), FAQPage.
|
||||
export { buildFaqJsonLd, buildLocalBusinessJsonLd, buildServiceJsonLd } from './modules/seo/index.js';
|
||||
export { buildArticleJsonLd } from './modules/seo/index.js';
|
||||
export { buildSitemapXml } from './modules/seo/index.js';
|
||||
export { buildLlmsTxt } from './modules/seo/index.js';
|
||||
export { buildSlugField, toSlug } from './modules/slug/index.js';
|
||||
// Storage — Cloudflare R2 media offload, configured from .env.
|
||||
export { buildR2Storage } from './modules/storage/index.js';
|
||||
export { ipalKit } from './plugin.js';
|
||||
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,29 @@
|
||||
import { type A11yState } from './state.js';
|
||||
type A11yContextValue = {
|
||||
reset: () => void;
|
||||
set: <K extends keyof A11yState>(key: K, value: A11yState[K]) => void;
|
||||
state: A11yState;
|
||||
};
|
||||
/**
|
||||
* Provides accessibility preferences, persists them in a cookie, and applies them
|
||||
* as data-attributes on <html> so the project's CSS can react. Like
|
||||
* ConsentProvider for cookies — wrap the app once; the widget/button consume it.
|
||||
*
|
||||
* The plugin ships NO styles: it only sets attributes (data-a11y-*). The project
|
||||
* writes CSS for those it supports (see docs/accessibility.md). This keeps the
|
||||
* design in the project's hands.
|
||||
*
|
||||
* // layout.tsx
|
||||
* import { AccessibilityProvider } from '@intecion/ipal-kit/client'
|
||||
* <AccessibilityProvider>{children}</AccessibilityProvider>
|
||||
*
|
||||
* To avoid a flash, the project can read the a11y-prefs cookie server-side and
|
||||
* set the attributes on <html> during SSR (see docs). This provider re-applies
|
||||
* on the client and keeps them in sync.
|
||||
*/
|
||||
export declare function AccessibilityProvider({ children }: {
|
||||
children: React.ReactNode;
|
||||
}): import("react/jsx-runtime").JSX.Element;
|
||||
/** Access accessibility preferences + setters. Use inside AccessibilityProvider. */
|
||||
export declare function useAccessibility(): A11yContextValue;
|
||||
export {};
|
||||
@@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
import { jsx as _jsx } from "react/jsx-runtime";
|
||||
import { createContext, use, useCallback, useEffect, useState } from 'react';
|
||||
import { A11Y_COOKIE, A11Y_COOKIE_MAX_AGE, A11Y_DEFAULT, a11yAttributes, parseA11y, serializeA11y } from './state.js';
|
||||
const A11yContext = /*#__PURE__*/ createContext(null);
|
||||
function readCookie(name) {
|
||||
if (typeof document === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
|
||||
return match ? decodeURIComponent(match[1]) : undefined;
|
||||
}
|
||||
/**
|
||||
* Provides accessibility preferences, persists them in a cookie, and applies them
|
||||
* as data-attributes on <html> so the project's CSS can react. Like
|
||||
* ConsentProvider for cookies — wrap the app once; the widget/button consume it.
|
||||
*
|
||||
* The plugin ships NO styles: it only sets attributes (data-a11y-*). The project
|
||||
* writes CSS for those it supports (see docs/accessibility.md). This keeps the
|
||||
* design in the project's hands.
|
||||
*
|
||||
* // layout.tsx
|
||||
* import { AccessibilityProvider } from '@intecion/ipal-kit/client'
|
||||
* <AccessibilityProvider>{children}</AccessibilityProvider>
|
||||
*
|
||||
* To avoid a flash, the project can read the a11y-prefs cookie server-side and
|
||||
* set the attributes on <html> during SSR (see docs). This provider re-applies
|
||||
* on the client and keeps them in sync.
|
||||
*/ export function AccessibilityProvider({ children }) {
|
||||
const [state, setState] = useState(A11Y_DEFAULT);
|
||||
// Hydrate from cookie on mount.
|
||||
useEffect(()=>{
|
||||
setState(parseA11y(readCookie(A11Y_COOKIE)));
|
||||
}, []);
|
||||
// Apply attributes to <html> whenever state changes.
|
||||
useEffect(()=>{
|
||||
const el = document.documentElement;
|
||||
const attrs = a11yAttributes(state);
|
||||
for (const [attr, value] of Object.entries(attrs)){
|
||||
if (value === null) {
|
||||
el.removeAttribute(attr);
|
||||
} else {
|
||||
el.setAttribute(attr, value);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
state
|
||||
]);
|
||||
const persist = useCallback((next)=>{
|
||||
document.cookie = `${A11Y_COOKIE}=${encodeURIComponent(serializeA11y(next))}; path=/; max-age=${A11Y_COOKIE_MAX_AGE}; samesite=lax`;
|
||||
}, []);
|
||||
const set = useCallback((key, value)=>{
|
||||
setState((prev)=>{
|
||||
const next = {
|
||||
...prev,
|
||||
[key]: value
|
||||
};
|
||||
persist(next);
|
||||
return next;
|
||||
});
|
||||
}, [
|
||||
persist
|
||||
]);
|
||||
const reset = useCallback(()=>{
|
||||
setState(A11Y_DEFAULT);
|
||||
persist(A11Y_DEFAULT);
|
||||
}, [
|
||||
persist
|
||||
]);
|
||||
return /*#__PURE__*/ _jsx(A11yContext, {
|
||||
value: {
|
||||
reset,
|
||||
set,
|
||||
state
|
||||
},
|
||||
children: children
|
||||
});
|
||||
}
|
||||
/** Access accessibility preferences + setters. Use inside AccessibilityProvider. */ export function useAccessibility() {
|
||||
const ctx = use(A11yContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useAccessibility must be used within <AccessibilityProvider>');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=AccessibilityProvider.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,41 @@
|
||||
export type A11yTexts = {
|
||||
bigCursor?: string;
|
||||
close?: string;
|
||||
contrast?: string;
|
||||
contrastHigh?: string;
|
||||
contrastInverted?: string;
|
||||
grayscale?: string;
|
||||
lineHeight?: string;
|
||||
open?: string;
|
||||
readableFont?: string;
|
||||
reduceMotion?: string;
|
||||
reset?: string;
|
||||
textSize?: string;
|
||||
title?: string;
|
||||
underlineLinks?: string;
|
||||
};
|
||||
export type A11yClassNames = {
|
||||
active?: string;
|
||||
button?: string;
|
||||
closeButton?: string;
|
||||
control?: string;
|
||||
label?: string;
|
||||
panel?: string;
|
||||
resetButton?: string;
|
||||
row?: string;
|
||||
};
|
||||
/**
|
||||
* Accessibility toolbar: a floating button that opens a panel of options (text
|
||||
* size, line height, contrast, grayscale, underline links, readable font, reduce
|
||||
* motion, big cursor). Choices persist in a cookie and apply as data-attributes
|
||||
* on <html> (the project's CSS styles them).
|
||||
*
|
||||
* Unstyled by default — pass classNames to match the project's design (like
|
||||
* CookieBanner). Wrap the app in <AccessibilityProvider> first.
|
||||
*
|
||||
* <AccessibilityWidget classNames={{ button: 'a11y-btn', panel: 'a11y-panel' }} />
|
||||
*/
|
||||
export declare function AccessibilityWidget({ classNames, texts, }: {
|
||||
classNames?: A11yClassNames;
|
||||
texts?: A11yTexts;
|
||||
}): import("react/jsx-runtime").JSX.Element;
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
'use client';
|
||||
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
||||
import { useState } from 'react';
|
||||
import { useAccessibility } from './AccessibilityProvider.js';
|
||||
const DEFAULT_TEXTS = {
|
||||
bigCursor: 'Duży kursor',
|
||||
close: 'Zamknij',
|
||||
contrast: 'Kontrast',
|
||||
contrastHigh: 'Wysoki',
|
||||
contrastInverted: 'Odwrócony',
|
||||
grayscale: 'Skala szarości',
|
||||
lineHeight: 'Odstęp między liniami',
|
||||
open: 'Otwórz panel dostępności',
|
||||
readableFont: 'Czytelna czcionka',
|
||||
reduceMotion: 'Wyłącz animacje',
|
||||
reset: 'Resetuj',
|
||||
textSize: 'Rozmiar tekstu',
|
||||
title: 'Dostępność',
|
||||
underlineLinks: 'Podkreśl linki'
|
||||
};
|
||||
/**
|
||||
* Accessibility toolbar: a floating button that opens a panel of options (text
|
||||
* size, line height, contrast, grayscale, underline links, readable font, reduce
|
||||
* motion, big cursor). Choices persist in a cookie and apply as data-attributes
|
||||
* on <html> (the project's CSS styles them).
|
||||
*
|
||||
* Unstyled by default — pass classNames to match the project's design (like
|
||||
* CookieBanner). Wrap the app in <AccessibilityProvider> first.
|
||||
*
|
||||
* <AccessibilityWidget classNames={{ button: 'a11y-btn', panel: 'a11y-panel' }} />
|
||||
*/ export function AccessibilityWidget({ classNames, texts }) {
|
||||
const { reset, set, state } = useAccessibility();
|
||||
const [open, setOpen] = useState(false);
|
||||
const t = {
|
||||
...DEFAULT_TEXTS,
|
||||
...texts
|
||||
};
|
||||
const cn = classNames ?? {};
|
||||
const toggle = (key)=>set(key, !state[key]);
|
||||
const isActive = (on)=>on ? cn.active ?? '' : '';
|
||||
return /*#__PURE__*/ _jsxs(_Fragment, {
|
||||
children: [
|
||||
/*#__PURE__*/ _jsx("button", {
|
||||
"aria-expanded": open,
|
||||
"aria-label": t.open,
|
||||
className: cn.button,
|
||||
onClick: ()=>setOpen((o)=>!o),
|
||||
type: "button",
|
||||
children: /*#__PURE__*/ _jsx("span", {
|
||||
"aria-hidden": "true",
|
||||
children: "♿"
|
||||
})
|
||||
}),
|
||||
open && /*#__PURE__*/ _jsxs("div", {
|
||||
"aria-label": t.title,
|
||||
className: cn.panel,
|
||||
role: "dialog",
|
||||
children: [
|
||||
/*#__PURE__*/ _jsxs("div", {
|
||||
className: cn.row,
|
||||
children: [
|
||||
/*#__PURE__*/ _jsx("span", {
|
||||
className: cn.label,
|
||||
children: t.textSize
|
||||
}),
|
||||
/*#__PURE__*/ _jsx("div", {
|
||||
className: cn.control,
|
||||
children: [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3
|
||||
].map((n)=>/*#__PURE__*/ _jsxs("button", {
|
||||
className: isActive(state.textSize === n),
|
||||
onClick: ()=>set('textSize', n),
|
||||
type: "button",
|
||||
children: [
|
||||
"A",
|
||||
n > 0 ? '+'.repeat(n) : ''
|
||||
]
|
||||
}, n))
|
||||
})
|
||||
]
|
||||
}),
|
||||
/*#__PURE__*/ _jsxs("div", {
|
||||
className: cn.row,
|
||||
children: [
|
||||
/*#__PURE__*/ _jsx("span", {
|
||||
className: cn.label,
|
||||
children: t.lineHeight
|
||||
}),
|
||||
/*#__PURE__*/ _jsx("div", {
|
||||
className: cn.control,
|
||||
children: [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
].map((n)=>/*#__PURE__*/ _jsx("button", {
|
||||
className: isActive(state.lineHeight === n),
|
||||
onClick: ()=>set('lineHeight', n),
|
||||
type: "button",
|
||||
children: n === 0 ? '—' : '≡'.repeat(n)
|
||||
}, n))
|
||||
})
|
||||
]
|
||||
}),
|
||||
/*#__PURE__*/ _jsxs("div", {
|
||||
className: cn.row,
|
||||
children: [
|
||||
/*#__PURE__*/ _jsx("span", {
|
||||
className: cn.label,
|
||||
children: t.contrast
|
||||
}),
|
||||
/*#__PURE__*/ _jsxs("div", {
|
||||
className: cn.control,
|
||||
children: [
|
||||
/*#__PURE__*/ _jsx("button", {
|
||||
className: isActive(state.contrast === 'high'),
|
||||
onClick: ()=>set('contrast', state.contrast === 'high' ? 'default' : 'high'),
|
||||
type: "button",
|
||||
children: t.contrastHigh
|
||||
}),
|
||||
/*#__PURE__*/ _jsx("button", {
|
||||
className: isActive(state.contrast === 'inverted'),
|
||||
onClick: ()=>set('contrast', state.contrast === 'inverted' ? 'default' : 'inverted'),
|
||||
type: "button",
|
||||
children: t.contrastInverted
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
}),
|
||||
[
|
||||
[
|
||||
'grayscale',
|
||||
t.grayscale
|
||||
],
|
||||
[
|
||||
'underlineLinks',
|
||||
t.underlineLinks
|
||||
],
|
||||
[
|
||||
'readableFont',
|
||||
t.readableFont
|
||||
],
|
||||
[
|
||||
'reduceMotion',
|
||||
t.reduceMotion
|
||||
],
|
||||
[
|
||||
'bigCursor',
|
||||
t.bigCursor
|
||||
]
|
||||
].map(([key, label])=>/*#__PURE__*/ _jsxs("div", {
|
||||
className: cn.row,
|
||||
children: [
|
||||
/*#__PURE__*/ _jsx("span", {
|
||||
className: cn.label,
|
||||
children: label
|
||||
}),
|
||||
/*#__PURE__*/ _jsx("button", {
|
||||
"aria-pressed": Boolean(state[key]),
|
||||
className: `${cn.control ?? ''} ${isActive(Boolean(state[key]))}`,
|
||||
onClick: ()=>toggle(key),
|
||||
type: "button",
|
||||
children: state[key] ? 'ON' : 'OFF'
|
||||
})
|
||||
]
|
||||
}, key)),
|
||||
/*#__PURE__*/ _jsx("button", {
|
||||
className: cn.resetButton,
|
||||
onClick: reset,
|
||||
type: "button",
|
||||
children: t.reset
|
||||
}),
|
||||
/*#__PURE__*/ _jsx("button", {
|
||||
className: cn.closeButton,
|
||||
onClick: ()=>setOpen(false),
|
||||
type: "button",
|
||||
children: t.close
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
//# sourceMappingURL=AccessibilityWidget.js.map
|
||||
File diff suppressed because one or more lines are too long
+5
@@ -0,0 +1,5 @@
|
||||
export { AccessibilityProvider, useAccessibility } from './AccessibilityProvider.js';
|
||||
export { AccessibilityWidget } from './AccessibilityWidget.js';
|
||||
export type { A11yClassNames, A11yTexts } from './AccessibilityWidget.js';
|
||||
export type { A11yState } from './state.js';
|
||||
export { A11Y_COOKIE, a11yAttributes, parseA11y } from './state.js';
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
export { AccessibilityProvider, useAccessibility } from './AccessibilityProvider.js';
|
||||
export { AccessibilityWidget } from './AccessibilityWidget.js';
|
||||
export { A11Y_COOKIE, a11yAttributes, parseA11y } from './state.js';
|
||||
|
||||
//# sourceMappingURL=client.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/accessibility/client.ts"],"sourcesContent":["'use client'\nexport { AccessibilityProvider, useAccessibility } from './AccessibilityProvider.js'\nexport { AccessibilityWidget } from './AccessibilityWidget.js'\nexport type { A11yClassNames, A11yTexts } from './AccessibilityWidget.js'\nexport type { A11yState } from './state.js'\nexport { A11Y_COOKIE, a11yAttributes, parseA11y } from './state.js'\n"],"names":["AccessibilityProvider","useAccessibility","AccessibilityWidget","A11Y_COOKIE","a11yAttributes","parseA11y"],"mappings":"AAAA;AACA,SAASA,qBAAqB,EAAEC,gBAAgB,QAAQ,6BAA4B;AACpF,SAASC,mBAAmB,QAAQ,2BAA0B;AAG9D,SAASC,WAAW,EAAEC,cAAc,EAAEC,SAAS,QAAQ,aAAY"}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export { AccessibilityProvider, useAccessibility } from './AccessibilityProvider.js';
|
||||
export { AccessibilityWidget } from './AccessibilityWidget.js';
|
||||
export type { A11yClassNames, A11yTexts } from './AccessibilityWidget.js';
|
||||
export { A11Y_COOKIE, A11Y_COOKIE_MAX_AGE, A11Y_DEFAULT, a11yAttributes, parseA11y, serializeA11y, } from './state.js';
|
||||
export type { A11yState } from './state.js';
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
export { AccessibilityProvider, useAccessibility } from './AccessibilityProvider.js';
|
||||
export { AccessibilityWidget } from './AccessibilityWidget.js';
|
||||
export { A11Y_COOKIE, A11Y_COOKIE_MAX_AGE, A11Y_DEFAULT, a11yAttributes, parseA11y, serializeA11y } from './state.js';
|
||||
|
||||
//# sourceMappingURL=index.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/accessibility/index.ts"],"sourcesContent":["export { AccessibilityProvider, useAccessibility } from './AccessibilityProvider.js'\nexport { AccessibilityWidget } from './AccessibilityWidget.js'\nexport type { A11yClassNames, A11yTexts } from './AccessibilityWidget.js'\nexport {\n A11Y_COOKIE,\n A11Y_COOKIE_MAX_AGE,\n A11Y_DEFAULT,\n a11yAttributes,\n parseA11y,\n serializeA11y,\n} from './state.js'\nexport type { A11yState } from './state.js'\n"],"names":["AccessibilityProvider","useAccessibility","AccessibilityWidget","A11Y_COOKIE","A11Y_COOKIE_MAX_AGE","A11Y_DEFAULT","a11yAttributes","parseA11y","serializeA11y"],"mappings":"AAAA,SAASA,qBAAqB,EAAEC,gBAAgB,QAAQ,6BAA4B;AACpF,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,SACEC,WAAW,EACXC,mBAAmB,EACnBC,YAAY,EACZC,cAAc,EACdC,SAAS,EACTC,aAAa,QACR,aAAY"}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Accessibility preferences state. Each option maps to a data-attribute on
|
||||
* <html> (e.g. data-a11y-contrast="high"); the PROJECT's CSS reacts to those
|
||||
* attributes. The plugin sets the attributes and persists the choice — it does
|
||||
* NOT ship styles, so it never fights the project's design.
|
||||
*/
|
||||
export type A11yState = {
|
||||
/** Larger cursor. */
|
||||
bigCursor: boolean;
|
||||
/** 'default' | 'high' (high contrast) | 'inverted' (dark-on-light flip). */
|
||||
contrast: 'default' | 'high' | 'inverted';
|
||||
/** Grayscale filter on the whole page. */
|
||||
grayscale: boolean;
|
||||
/** Line spacing: 0 = default, 1..2 = looser. */
|
||||
lineHeight: 0 | 1 | 2;
|
||||
/** Readable font (project maps this to a dyslexia-friendly / simple font). */
|
||||
readableFont: boolean;
|
||||
/** Stop animations / transitions (prefers-reduced-motion equivalent). */
|
||||
reduceMotion: boolean;
|
||||
/** Text size step: 0 = default, 1..3 = larger. */
|
||||
textSize: 0 | 1 | 2 | 3;
|
||||
/** Underline all links (WCAG: don't rely on color alone). */
|
||||
underlineLinks: boolean;
|
||||
};
|
||||
export declare const A11Y_DEFAULT: A11yState;
|
||||
export declare const A11Y_COOKIE = "a11y-prefs";
|
||||
export declare const A11Y_COOKIE_MAX_AGE: number;
|
||||
/** Serialize for the cookie (compact). */
|
||||
export declare function serializeA11y(state: A11yState): string;
|
||||
/** Parse from the cookie; falls back to defaults on any bad value. */
|
||||
export declare function parseA11y(raw: null | string | undefined): A11yState;
|
||||
/**
|
||||
* Maps state → data-attributes to set on <html>. Returns { attr: value|null };
|
||||
* null means remove the attribute (option is at default). The project's CSS
|
||||
* targets these, e.g. `[data-a11y-contrast="high"] { … }`.
|
||||
*/
|
||||
export declare function a11yAttributes(state: A11yState): Record<string, null | string>;
|
||||
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Accessibility preferences state. Each option maps to a data-attribute on
|
||||
* <html> (e.g. data-a11y-contrast="high"); the PROJECT's CSS reacts to those
|
||||
* attributes. The plugin sets the attributes and persists the choice — it does
|
||||
* NOT ship styles, so it never fights the project's design.
|
||||
*/ export const A11Y_DEFAULT = {
|
||||
bigCursor: false,
|
||||
contrast: 'default',
|
||||
grayscale: false,
|
||||
lineHeight: 0,
|
||||
readableFont: false,
|
||||
reduceMotion: false,
|
||||
textSize: 0,
|
||||
underlineLinks: false
|
||||
};
|
||||
export const A11Y_COOKIE = 'a11y-prefs';
|
||||
export const A11Y_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year
|
||||
;
|
||||
/** Serialize for the cookie (compact). */ export function serializeA11y(state) {
|
||||
return JSON.stringify(state);
|
||||
}
|
||||
/** Parse from the cookie; falls back to defaults on any bad value. */ export function parseA11y(raw) {
|
||||
if (!raw) {
|
||||
return A11Y_DEFAULT;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
...A11Y_DEFAULT,
|
||||
...parsed
|
||||
};
|
||||
} catch {
|
||||
return A11Y_DEFAULT;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Maps state → data-attributes to set on <html>. Returns { attr: value|null };
|
||||
* null means remove the attribute (option is at default). The project's CSS
|
||||
* targets these, e.g. `[data-a11y-contrast="high"] { … }`.
|
||||
*/ export function a11yAttributes(state) {
|
||||
return {
|
||||
'data-a11y-contrast': state.contrast !== 'default' ? state.contrast : null,
|
||||
'data-a11y-cursor': state.bigCursor ? 'big' : null,
|
||||
'data-a11y-font': state.readableFont ? 'readable' : null,
|
||||
'data-a11y-grayscale': state.grayscale ? 'on' : null,
|
||||
'data-a11y-line': state.lineHeight > 0 ? String(state.lineHeight) : null,
|
||||
'data-a11y-motion': state.reduceMotion ? 'reduce' : null,
|
||||
'data-a11y-text': state.textSize > 0 ? String(state.textSize) : null,
|
||||
'data-a11y-underline': state.underlineLinks ? 'on' : null
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=state.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/accessibility/state.ts"],"sourcesContent":["/**\n * Accessibility preferences state. Each option maps to a data-attribute on\n * <html> (e.g. data-a11y-contrast=\"high\"); the PROJECT's CSS reacts to those\n * attributes. The plugin sets the attributes and persists the choice — it does\n * NOT ship styles, so it never fights the project's design.\n */\nexport type A11yState = {\n /** Larger cursor. */\n bigCursor: boolean\n /** 'default' | 'high' (high contrast) | 'inverted' (dark-on-light flip). */\n contrast: 'default' | 'high' | 'inverted'\n /** Grayscale filter on the whole page. */\n grayscale: boolean\n /** Line spacing: 0 = default, 1..2 = looser. */\n lineHeight: 0 | 1 | 2\n /** Readable font (project maps this to a dyslexia-friendly / simple font). */\n readableFont: boolean\n /** Stop animations / transitions (prefers-reduced-motion equivalent). */\n reduceMotion: boolean\n /** Text size step: 0 = default, 1..3 = larger. */\n textSize: 0 | 1 | 2 | 3\n /** Underline all links (WCAG: don't rely on color alone). */\n underlineLinks: boolean\n}\n\nexport const A11Y_DEFAULT: A11yState = {\n bigCursor: false,\n contrast: 'default',\n grayscale: false,\n lineHeight: 0,\n readableFont: false,\n reduceMotion: false,\n textSize: 0,\n underlineLinks: false,\n}\n\nexport const A11Y_COOKIE = 'a11y-prefs'\nexport const A11Y_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year\n\n/** Serialize for the cookie (compact). */\nexport function serializeA11y(state: A11yState): string {\n return JSON.stringify(state)\n}\n\n/** Parse from the cookie; falls back to defaults on any bad value. */\nexport function parseA11y(raw: null | string | undefined): A11yState {\n if (!raw) {return A11Y_DEFAULT}\n try {\n const parsed = JSON.parse(raw) as Partial<A11yState>\n return { ...A11Y_DEFAULT, ...parsed }\n } catch {\n return A11Y_DEFAULT\n }\n}\n\n/**\n * Maps state → data-attributes to set on <html>. Returns { attr: value|null };\n * null means remove the attribute (option is at default). The project's CSS\n * targets these, e.g. `[data-a11y-contrast=\"high\"] { … }`.\n */\nexport function a11yAttributes(state: A11yState): Record<string, null | string> {\n return {\n 'data-a11y-contrast': state.contrast !== 'default' ? state.contrast : null,\n 'data-a11y-cursor': state.bigCursor ? 'big' : null,\n 'data-a11y-font': state.readableFont ? 'readable' : null,\n 'data-a11y-grayscale': state.grayscale ? 'on' : null,\n 'data-a11y-line': state.lineHeight > 0 ? String(state.lineHeight) : null,\n 'data-a11y-motion': state.reduceMotion ? 'reduce' : null,\n 'data-a11y-text': state.textSize > 0 ? String(state.textSize) : null,\n 'data-a11y-underline': state.underlineLinks ? 'on' : null,\n }\n}\n"],"names":["A11Y_DEFAULT","bigCursor","contrast","grayscale","lineHeight","readableFont","reduceMotion","textSize","underlineLinks","A11Y_COOKIE","A11Y_COOKIE_MAX_AGE","serializeA11y","state","JSON","stringify","parseA11y","raw","parsed","parse","a11yAttributes","String"],"mappings":"AAAA;;;;;CAKC,GAoBD,OAAO,MAAMA,eAA0B;IACrCC,WAAW;IACXC,UAAU;IACVC,WAAW;IACXC,YAAY;IACZC,cAAc;IACdC,cAAc;IACdC,UAAU;IACVC,gBAAgB;AAClB,EAAC;AAED,OAAO,MAAMC,cAAc,aAAY;AACvC,OAAO,MAAMC,sBAAsB,KAAK,KAAK,KAAK,IAAI,SAAS;CAAV;AAErD,wCAAwC,GACxC,OAAO,SAASC,cAAcC,KAAgB;IAC5C,OAAOC,KAAKC,SAAS,CAACF;AACxB;AAEA,oEAAoE,GACpE,OAAO,SAASG,UAAUC,GAA8B;IACtD,IAAI,CAACA,KAAK;QAAC,OAAOhB;IAAY;IAC9B,IAAI;QACF,MAAMiB,SAASJ,KAAKK,KAAK,CAACF;QAC1B,OAAO;YAAE,GAAGhB,YAAY;YAAE,GAAGiB,MAAM;QAAC;IACtC,EAAE,OAAM;QACN,OAAOjB;IACT;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASmB,eAAeP,KAAgB;IAC7C,OAAO;QACL,sBAAsBA,MAAMV,QAAQ,KAAK,YAAYU,MAAMV,QAAQ,GAAG;QACtE,oBAAoBU,MAAMX,SAAS,GAAG,QAAQ;QAC9C,kBAAkBW,MAAMP,YAAY,GAAG,aAAa;QACpD,uBAAuBO,MAAMT,SAAS,GAAG,OAAO;QAChD,kBAAkBS,MAAMR,UAAU,GAAG,IAAIgB,OAAOR,MAAMR,UAAU,IAAI;QACpE,oBAAoBQ,MAAMN,YAAY,GAAG,WAAW;QACpD,kBAAkBM,MAAML,QAAQ,GAAG,IAAIa,OAAOR,MAAML,QAAQ,IAAI;QAChE,uBAAuBK,MAAMJ,cAAc,GAAG,OAAO;IACvD;AACF"}
|
||||
+22
-16
@@ -1,8 +1,14 @@
|
||||
import type { BasePayload, SanitizedConfig } from 'payload';
|
||||
import type { ContentOption, ResolvedRoute, ArchiveEntries } from '../content/index.js';
|
||||
import type { ArchiveEntries, ContentOption, ResolvedRoute } from '../content/index.js';
|
||||
import type { I18nConfig } from '../i18n/index.js';
|
||||
import type { SitemapEntry, RobotsRules } from '../seo/index.js';
|
||||
import type { RobotsRules, SitemapEntry } from '../seo/index.js';
|
||||
type CreateContentHelpersArgs = {
|
||||
/**
|
||||
* Absolute site origin for sitemap/robots URLs. Falls back to
|
||||
* NEXT_PUBLIC_SERVER_URL, then to a relative origin (which most crawlers
|
||||
* reject, so set one in production).
|
||||
*/
|
||||
baseUrl?: string;
|
||||
/**
|
||||
* The client's payload config promise (the default export of payload.config).
|
||||
* Passed in because the plugin never imports the client's config directly.
|
||||
@@ -10,21 +16,15 @@ type CreateContentHelpersArgs = {
|
||||
config: Promise<SanitizedConfig> | SanitizedConfig;
|
||||
/** Archive-backed collections, same value as the plugin option. */
|
||||
content?: ContentOption;
|
||||
/** SiteSettings global slug. Defaults to 'site-settings'. */
|
||||
settingsSlug?: string;
|
||||
/** Pages collection slug. Defaults to 'pages'. */
|
||||
pagesSlug?: string;
|
||||
/**
|
||||
* i18n config. Required only if you want the ready-made `sitemap` / `robots`
|
||||
* handlers — they need the locale list to emit hreflang.
|
||||
*/
|
||||
i18n?: I18nConfig;
|
||||
/**
|
||||
* Absolute site origin for sitemap/robots URLs. Falls back to
|
||||
* NEXT_PUBLIC_SERVER_URL, then to a relative origin (which most crawlers
|
||||
* reject, so set one in production).
|
||||
*/
|
||||
baseUrl?: string;
|
||||
/** Pages collection slug. Defaults to 'pages'. */
|
||||
pagesSlug?: string;
|
||||
/** SiteSettings global slug. Defaults to 'site-settings'. */
|
||||
settingsSlug?: string;
|
||||
};
|
||||
/**
|
||||
* Bundles the per-request data helpers a frontend needs — the same cached
|
||||
@@ -52,13 +52,19 @@ type CreateContentHelpersArgs = {
|
||||
* a URL is, fetching gets the listing. Metadata generation needs the first and
|
||||
* not the second, and a page component composes them in two obvious lines.
|
||||
*/
|
||||
export declare function createContentHelpers({ config, content, settingsSlug, pagesSlug, i18n, baseUrl, }: CreateContentHelpersArgs): {
|
||||
export declare function createContentHelpers({ baseUrl, config, content, i18n, pagesSlug, settingsSlug, }: CreateContentHelpersArgs): {
|
||||
generateStaticParams: () => Promise<Array<{
|
||||
locale: string;
|
||||
slug: string[];
|
||||
} | {
|
||||
slug: string[];
|
||||
}>>;
|
||||
getCachedPayload: () => Promise<BasePayload>;
|
||||
getConfiguredLocales: () => Promise<string[]>;
|
||||
getSettings: (locale: string) => Promise<import("payload").JsonObject>;
|
||||
resolveRoute: (locale: string, segments: string[] | undefined, page: number) => Promise<ResolvedRoute | null>;
|
||||
getEntries: (collection: string, locale: string, page: number, perPage: number) => Promise<ArchiveEntries>;
|
||||
sitemap: () => Promise<SitemapEntry[]>;
|
||||
getSettings: (locale: string) => Promise<import("payload").JsonObject>;
|
||||
resolveRoute: (locale: string, segments: string[] | undefined, page: number) => Promise<null | ResolvedRoute>;
|
||||
robots: () => RobotsRules;
|
||||
sitemap: () => Promise<SitemapEntry[]>;
|
||||
};
|
||||
export {};
|
||||
|
||||
+125
-16
@@ -1,7 +1,7 @@
|
||||
import { cache } from 'react';
|
||||
import { getPayload } from 'payload';
|
||||
import { resolveRoute as resolveRouteRaw, getArchiveEntries } from '../content/index.js';
|
||||
import { buildSitemapEntries, buildRobots } from '../seo/index.js';
|
||||
import { cache } from 'react';
|
||||
import { getArchiveEntries, resolveRoute as resolveRouteRaw } from '../content/index.js';
|
||||
import { buildRobots, buildSitemapEntries } from '../seo/index.js';
|
||||
/**
|
||||
* Bundles the per-request data helpers a frontend needs — the same cached
|
||||
* wrappers every project was writing by hand (getPayload, settings, locale
|
||||
@@ -27,7 +27,7 @@ import { buildSitemapEntries, buildRobots } from '../seo/index.js';
|
||||
* `resolveRoute` and `getEntries` are separate on purpose: routing decides what
|
||||
* a URL is, fetching gets the listing. Metadata generation needs the first and
|
||||
* not the second, and a page component composes them in two obvious lines.
|
||||
*/ export function createContentHelpers({ config, content, settingsSlug = 'site-settings', pagesSlug = 'pages', i18n, baseUrl }) {
|
||||
*/ export function createContentHelpers({ baseUrl, config, content, i18n, pagesSlug = 'pages', settingsSlug = 'site-settings' }) {
|
||||
const origin = baseUrl ?? process.env.NEXT_PUBLIC_SERVER_URL ?? '';
|
||||
const getCachedPayload = cache(async ()=>getPayload({
|
||||
config: await config
|
||||
@@ -40,29 +40,29 @@ import { buildSitemapEntries, buildRobots } from '../seo/index.js';
|
||||
const payload = await getCachedPayload();
|
||||
return payload.findGlobal({
|
||||
slug: settingsSlug,
|
||||
locale: locale,
|
||||
depth: 2
|
||||
depth: 2,
|
||||
locale: locale
|
||||
});
|
||||
});
|
||||
/** What does this URL point at? Routing only — no listing data. */ const resolveRoute = cache(async (locale, segments, page)=>{
|
||||
const payload = await getCachedPayload();
|
||||
return resolveRouteRaw({
|
||||
payload,
|
||||
locale,
|
||||
segments,
|
||||
page,
|
||||
content,
|
||||
locale,
|
||||
page,
|
||||
pagesSlug,
|
||||
payload,
|
||||
segments,
|
||||
settingsSlug
|
||||
});
|
||||
});
|
||||
/** One page of a collection's entries, for an archive listing. */ const getEntries = cache(async (collection, locale, page, perPage)=>{
|
||||
const payload = await getCachedPayload();
|
||||
return getArchiveEntries({
|
||||
payload,
|
||||
collection,
|
||||
locale,
|
||||
page,
|
||||
payload,
|
||||
perPage
|
||||
});
|
||||
});
|
||||
@@ -92,11 +92,11 @@ import { buildSitemapEntries, buildRobots } from '../seo/index.js';
|
||||
}
|
||||
try {
|
||||
return await buildSitemapEntries({
|
||||
payload: await getCachedPayload(),
|
||||
config: i18n,
|
||||
baseUrl: origin,
|
||||
config: i18n,
|
||||
content,
|
||||
pagesSlug,
|
||||
payload: await getCachedPayload(),
|
||||
settingsSlug
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -117,14 +117,123 @@ import { buildSitemapEntries, buildRobots } from '../seo/index.js';
|
||||
*/ const robots = ()=>buildRobots({
|
||||
baseUrl: origin
|
||||
});
|
||||
/**
|
||||
* Next.js generateStaticParams for the [[...slug]] route (or
|
||||
* [locale]/[[...slug]]). Returns every routable page as a params object, so
|
||||
* Next PRE-RENDERS them as static (SSG) instead of dynamic.
|
||||
*
|
||||
* Why this matters beyond convenience: an optional catch-all with no
|
||||
* generateStaticParams is treated as a DYNAMIC route (ƒ), which streams
|
||||
* metadata into <body> (crawlers miss it). Providing generateStaticParams
|
||||
* compiles routes as SSG (●) — the <head> is synchronous and complete. This is
|
||||
* the strongest fix for the metadata-in-head problem (stronger than ISR alone).
|
||||
*
|
||||
* Handles automatically:
|
||||
* - pages collection + content collections (with their archive prefix)
|
||||
* - excludes the homepage (maps to { slug: [] } — the root)
|
||||
* - excludes drafts and 404/500/system slugs
|
||||
* - KEEPS noindex pages (they must still render — noindex controls indexing,
|
||||
* not existence; skipping them would force dynamic rendering)
|
||||
* - localized slugs (string or per-locale map) both handled
|
||||
* - single-locale → { slug }[]; multi-locale → { locale, slug }[]
|
||||
*
|
||||
* Wire it in the project:
|
||||
* // app/(frontend)/[[...slug]]/page.tsx (or [locale]/[[...slug]])
|
||||
* export { generateStaticParams } from '@/lib/content'
|
||||
*/ const generateStaticParams = async ()=>{
|
||||
try {
|
||||
const payload = await getCachedPayload();
|
||||
const locales = i18n ? i18n.locales.map((l)=>l.code) : [
|
||||
undefined
|
||||
];
|
||||
const singleLocale = !i18n || i18n.locales.length === 1;
|
||||
// Home slug per locale, to exclude the homepage (it's the root, slug []).
|
||||
const settings = await payload.findGlobal({
|
||||
slug: settingsSlug,
|
||||
depth: 1,
|
||||
locale: 'all'
|
||||
}).catch(()=>null);
|
||||
const homeId = settings?.homepage?.id;
|
||||
const EXCLUDED = new Set([
|
||||
'404',
|
||||
'500',
|
||||
'error',
|
||||
'not-found'
|
||||
]);
|
||||
const params = [];
|
||||
for (const locale of locales){
|
||||
// NO where:{_status} filter — collections without drafts enabled don't
|
||||
// register the _status field, and querying it throws
|
||||
// "path cannot be queried: _status". We filter drafts in memory below,
|
||||
// which is safe for every collection (with or without drafts).
|
||||
const result = await payload.find({
|
||||
collection: pagesSlug,
|
||||
depth: 0,
|
||||
limit: 1000,
|
||||
locale: locale ?? 'all'
|
||||
});
|
||||
for (const raw of result.docs){
|
||||
// Draft filter in memory (safe whether or not the collection has drafts).
|
||||
if (raw._status && raw._status !== 'published') {
|
||||
continue;
|
||||
}
|
||||
// NOTE: unlike the sitemap, we do NOT skip meta.noindex here. A noindex
|
||||
// page (privacy, cookies, terms) still needs to render — users reach it
|
||||
// from the footer and crawlers read its <meta robots=noindex>. Pre-render
|
||||
// it as SSG so it's fast and its <head> is complete; noindex controls
|
||||
// INDEXING, not whether the page exists. Skipping it would force dynamic
|
||||
// rendering (the very streaming problem we're avoiding).
|
||||
if (homeId && raw.id === homeId) {
|
||||
// Homepage → root. Emit an empty-slug param so '/' (or '/pl') builds.
|
||||
const empty = singleLocale ? {
|
||||
slug: []
|
||||
} : {
|
||||
slug: [],
|
||||
locale: locale
|
||||
};
|
||||
if (!params.some((p)=>JSON.stringify(p) === JSON.stringify(empty))) {
|
||||
params.push(empty);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Slug may be a plain string OR a localized map ({ pl: 'kontakt' }) when
|
||||
// read with locale:'all' or left unflattened. Handle both, or localized
|
||||
// pages get silently dropped.
|
||||
const rawSlug = raw.slug;
|
||||
const slug = typeof rawSlug === 'string' ? rawSlug : rawSlug && typeof rawSlug === 'object' ? rawSlug[locale ?? ''] ?? Object.values(rawSlug)[0] : undefined;
|
||||
if (!slug || EXCLUDED.has(slug)) {
|
||||
continue;
|
||||
}
|
||||
// Multi-level slugs ('atrakcje/telefon') → array segments.
|
||||
const segments = String(slug).split('/').filter(Boolean);
|
||||
params.push(singleLocale ? {
|
||||
slug: segments
|
||||
} : {
|
||||
slug: segments,
|
||||
locale: locale
|
||||
});
|
||||
}
|
||||
}
|
||||
return params;
|
||||
} catch (err) {
|
||||
// DB unreachable — typically a container build (Docker/Coolify/CI) with no
|
||||
// database network. Return [] so the build doesn't crash: Next falls back
|
||||
// to on-demand rendering for the routes, which fill in once the DB is
|
||||
// reachable at runtime. Without this every project would need its own
|
||||
// try/catch here. (Same graceful-degradation as the sitemap handler.)
|
||||
console.warn('[ipal] generateStaticParams: database not reachable during build ' + '(Docker/CI) — returning empty params; routes render on-demand at runtime:', err);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
return {
|
||||
generateStaticParams,
|
||||
getCachedPayload,
|
||||
getConfiguredLocales,
|
||||
getEntries,
|
||||
getSettings,
|
||||
resolveRoute,
|
||||
getEntries,
|
||||
sitemap,
|
||||
robots
|
||||
robots,
|
||||
sitemap
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+35
@@ -0,0 +1,35 @@
|
||||
import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload';
|
||||
import type { I18nConfig } from '../i18n/index.js';
|
||||
type RevalidateFn = (path: string) => void;
|
||||
type BuildRevalidateHookArgs = {
|
||||
config: I18nConfig;
|
||||
/** Home slug (string or per-locale map) — home revalidates the root. */
|
||||
homeSlug?: Record<string, string> | string;
|
||||
/**
|
||||
* next/cache revalidatePath, INJECTED by the project. The plugin never imports
|
||||
* next/cache itself — that would crash when Payload runs as plain Node
|
||||
* (generate:importmap). The project passes it: `revalidatePath` from 'next/cache'.
|
||||
*/
|
||||
revalidatePath: RevalidateFn;
|
||||
};
|
||||
/**
|
||||
* Builds afterChange + afterDelete hooks that revalidate a page's ISR cache when
|
||||
* an editor saves or deletes it — so changes appear immediately instead of
|
||||
* waiting for the revalidate window. Without this, ISR means editors wait; with
|
||||
* it, ISR is usable for a CMS.
|
||||
*
|
||||
* Handles every locale, the root (home), AND a changed slug (revalidates both the
|
||||
* old and new path so neither goes stale). revalidatePath is injected — the
|
||||
* plugin never imports next/cache (safe under generate:importmap / plain Node).
|
||||
*
|
||||
* // in your Media/Pages collection config, from a project file that CAN import next/cache:
|
||||
* import { revalidatePath } from 'next/cache'
|
||||
* import { buildRevalidateHook } from '@intecion/ipal-kit'
|
||||
* const { afterChange, afterDelete } = buildRevalidateHook({ revalidatePath, config: i18nConfig })
|
||||
* // hooks: { afterChange: [afterChange], afterDelete: [afterDelete] }
|
||||
*/
|
||||
export declare function buildRevalidateHook({ config, homeSlug, revalidatePath, }: BuildRevalidateHookArgs): {
|
||||
afterChange: CollectionAfterChangeHook;
|
||||
afterDelete: CollectionAfterDeleteHook;
|
||||
};
|
||||
export {};
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { buildLocalizedPath, getLocaleCodes } from '../i18n/index.js';
|
||||
/** Resolve a doc's path in one locale (root for home). Null if no slug there. */ function pathForLocale(doc, locale, config, homeSlug) {
|
||||
const slugField = doc.slug;
|
||||
const slug = typeof slugField === 'string' ? slugField : slugField && typeof slugField === 'object' ? slugField[locale] : undefined;
|
||||
if (!slug) {
|
||||
return null;
|
||||
}
|
||||
return buildLocalizedPath({
|
||||
config,
|
||||
homeSlug,
|
||||
locale,
|
||||
slugs: {
|
||||
[locale]: slug
|
||||
}
|
||||
}) ?? null;
|
||||
}
|
||||
/**
|
||||
* Builds afterChange + afterDelete hooks that revalidate a page's ISR cache when
|
||||
* an editor saves or deletes it — so changes appear immediately instead of
|
||||
* waiting for the revalidate window. Without this, ISR means editors wait; with
|
||||
* it, ISR is usable for a CMS.
|
||||
*
|
||||
* Handles every locale, the root (home), AND a changed slug (revalidates both the
|
||||
* old and new path so neither goes stale). revalidatePath is injected — the
|
||||
* plugin never imports next/cache (safe under generate:importmap / plain Node).
|
||||
*
|
||||
* // in your Media/Pages collection config, from a project file that CAN import next/cache:
|
||||
* import { revalidatePath } from 'next/cache'
|
||||
* import { buildRevalidateHook } from '@intecion/ipal-kit'
|
||||
* const { afterChange, afterDelete } = buildRevalidateHook({ revalidatePath, config: i18nConfig })
|
||||
* // hooks: { afterChange: [afterChange], afterDelete: [afterDelete] }
|
||||
*/ export function buildRevalidateHook({ config, homeSlug, revalidatePath }) {
|
||||
const locales = getLocaleCodes(config);
|
||||
const afterChange = ({ doc, previousDoc })=>{
|
||||
const seen = new Set();
|
||||
for (const locale of locales){
|
||||
// New path.
|
||||
const newPath = pathForLocale(doc, locale, config, homeSlug);
|
||||
if (newPath && !seen.has(newPath)) {
|
||||
revalidatePath(newPath);
|
||||
seen.add(newPath);
|
||||
}
|
||||
// Old path, if the slug changed — so the old URL doesn't serve stale content.
|
||||
if (previousDoc) {
|
||||
const oldPath = pathForLocale(previousDoc, locale, config, homeSlug);
|
||||
if (oldPath && oldPath !== newPath && !seen.has(oldPath)) {
|
||||
revalidatePath(oldPath);
|
||||
seen.add(oldPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
};
|
||||
const afterDelete = ({ doc })=>{
|
||||
const seen = new Set();
|
||||
for (const locale of locales){
|
||||
const path = pathForLocale(doc, locale, config, homeSlug);
|
||||
if (path && !seen.has(path)) {
|
||||
revalidatePath(path);
|
||||
seen.add(path);
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
};
|
||||
return {
|
||||
afterChange,
|
||||
afterDelete
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=buildRevalidateHook.js.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
export { normalizeFilenameHook } from '../media/index.js';
|
||||
export { buildAutoFillMetaHook, validateFaviconField } from '../seo/index.js';
|
||||
export { buildRevalidateHook } from './buildRevalidateHook.js';
|
||||
export { buildPreventDeleteSystemPage } from './preventDeleteSystemPage.js';
|
||||
export { setPublishedAtHook } from './setPublishedAt.js';
|
||||
export { trackSlugHistoryHook } from './trackSlugHistory.js';
|
||||
export { buildValidateUniqueRole } from './validateUniqueRole.js';
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
// Re-eksport hooków domenowych (mieszkają w swoich modułach, tu dla przeglądu —
|
||||
// żeby był jeden katalog "wszystkie hooki pluginu"). Źródło prawdy to ich moduły.
|
||||
export { normalizeFilenameHook } from '../media/index.js';
|
||||
export { buildAutoFillMetaHook, validateFaviconField } from '../seo/index.js';
|
||||
export { buildRevalidateHook } from './buildRevalidateHook.js';
|
||||
export { buildPreventDeleteSystemPage } from './preventDeleteSystemPage.js';
|
||||
export { setPublishedAtHook } from './setPublishedAt.js';
|
||||
export { trackSlugHistoryHook } from './trackSlugHistory.js';
|
||||
export { buildValidateUniqueRole } from './validateUniqueRole.js';
|
||||
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/hooks/index.ts"],"sourcesContent":["// Re-eksport hooków domenowych (mieszkają w swoich modułach, tu dla przeglądu —\n// żeby był jeden katalog \"wszystkie hooki pluginu\"). Źródło prawdy to ich moduły.\nexport { normalizeFilenameHook } from '../media/index.js'\nexport { buildAutoFillMetaHook, validateFaviconField } from '../seo/index.js'\nexport { buildRevalidateHook } from './buildRevalidateHook.js'\nexport { buildPreventDeleteSystemPage } from './preventDeleteSystemPage.js'\nexport { setPublishedAtHook } from './setPublishedAt.js'\n\nexport { trackSlugHistoryHook } from './trackSlugHistory.js'\nexport { buildValidateUniqueRole } from './validateUniqueRole.js'\n"],"names":["normalizeFilenameHook","buildAutoFillMetaHook","validateFaviconField","buildRevalidateHook","buildPreventDeleteSystemPage","setPublishedAtHook","trackSlugHistoryHook","buildValidateUniqueRole"],"mappings":"AAAA,gFAAgF;AAChF,kFAAkF;AAClF,SAASA,qBAAqB,QAAQ,oBAAmB;AACzD,SAASC,qBAAqB,EAAEC,oBAAoB,QAAQ,kBAAiB;AAC7E,SAASC,mBAAmB,QAAQ,2BAA0B;AAC9D,SAASC,4BAA4B,QAAQ,+BAA8B;AAC3E,SAASC,kBAAkB,QAAQ,sBAAqB;AAExD,SAASC,oBAAoB,QAAQ,wBAAuB;AAC5D,SAASC,uBAAuB,QAAQ,0BAAyB"}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import type { CollectionBeforeDeleteHook } from 'payload';
|
||||
/**
|
||||
* Blocks deletion of a page assigned a System Page role (homepage,
|
||||
* privacyPolicy, cookiePolicy, termsOfService). An editor deleting the privacy
|
||||
* policy or homepage by accident would break routing and compliance links; this
|
||||
* stops it with a clear error. They must unassign the role first (deliberate).
|
||||
*
|
||||
* Reads the role assignments from SiteSettings (which page holds which role).
|
||||
*
|
||||
* hooks: { beforeDelete: [buildPreventDeleteSystemPage({ settingsSlug: 'site-settings' })] }
|
||||
*/
|
||||
export declare function buildPreventDeleteSystemPage(args?: {
|
||||
roleFields?: string[];
|
||||
settingsSlug?: string;
|
||||
}): CollectionBeforeDeleteHook;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { APIError } from 'payload';
|
||||
/**
|
||||
* Blocks deletion of a page assigned a System Page role (homepage,
|
||||
* privacyPolicy, cookiePolicy, termsOfService). An editor deleting the privacy
|
||||
* policy or homepage by accident would break routing and compliance links; this
|
||||
* stops it with a clear error. They must unassign the role first (deliberate).
|
||||
*
|
||||
* Reads the role assignments from SiteSettings (which page holds which role).
|
||||
*
|
||||
* hooks: { beforeDelete: [buildPreventDeleteSystemPage({ settingsSlug: 'site-settings' })] }
|
||||
*/ export function buildPreventDeleteSystemPage(args = {}) {
|
||||
const settingsSlug = args.settingsSlug ?? 'site-settings';
|
||||
const roleFields = args.roleFields ?? [
|
||||
'homepage',
|
||||
'privacyPolicy',
|
||||
'cookiePolicy',
|
||||
'termsOfService'
|
||||
];
|
||||
return async ({ id, req })=>{
|
||||
const settings = await req.payload.findGlobal({
|
||||
slug: settingsSlug,
|
||||
depth: 0
|
||||
}).catch(()=>null);
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
for (const field of roleFields){
|
||||
const assigned = settings[field];
|
||||
const assignedId = assigned && typeof assigned === 'object' ? assigned.id : assigned;
|
||||
if (assignedId != null && String(assignedId) === String(id)) {
|
||||
throw new APIError(`Nie można usunąć strony przypisanej do roli systemowej "${field}". ` + `Najpierw odłącz rolę w Site Settings.`, 400);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=preventDeleteSystemPage.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/hooks/preventDeleteSystemPage.ts"],"sourcesContent":["import type { CollectionBeforeDeleteHook } from 'payload'\n\nimport { APIError } from 'payload'\n\n/**\n * Blocks deletion of a page assigned a System Page role (homepage,\n * privacyPolicy, cookiePolicy, termsOfService). An editor deleting the privacy\n * policy or homepage by accident would break routing and compliance links; this\n * stops it with a clear error. They must unassign the role first (deliberate).\n *\n * Reads the role assignments from SiteSettings (which page holds which role).\n *\n * hooks: { beforeDelete: [buildPreventDeleteSystemPage({ settingsSlug: 'site-settings' })] }\n */\nexport function buildPreventDeleteSystemPage(\n args: { roleFields?: string[]; settingsSlug?: string } = {},\n): CollectionBeforeDeleteHook {\n const settingsSlug = args.settingsSlug ?? 'site-settings'\n const roleFields = args.roleFields ?? [\n 'homepage',\n 'privacyPolicy',\n 'cookiePolicy',\n 'termsOfService',\n ]\n\n return async ({ id, req }) => {\n const settings = (await req.payload\n .findGlobal({ slug: settingsSlug as never, depth: 0 })\n .catch(() => null)) as null | Record<string, unknown>\n if (!settings) {return}\n\n for (const field of roleFields) {\n const assigned = settings[field]\n const assignedId =\n assigned && typeof assigned === 'object' ? (assigned as { id?: unknown }).id : assigned\n if (assignedId != null && String(assignedId) === String(id)) {\n throw new APIError(\n `Nie można usunąć strony przypisanej do roli systemowej \"${field}\". ` +\n `Najpierw odłącz rolę w Site Settings.`,\n 400,\n )\n }\n }\n }\n}\n"],"names":["APIError","buildPreventDeleteSystemPage","args","settingsSlug","roleFields","id","req","settings","payload","findGlobal","slug","depth","catch","field","assigned","assignedId","String"],"mappings":"AAEA,SAASA,QAAQ,QAAQ,UAAS;AAElC;;;;;;;;;CASC,GACD,OAAO,SAASC,6BACdC,OAAyD,CAAC,CAAC;IAE3D,MAAMC,eAAeD,KAAKC,YAAY,IAAI;IAC1C,MAAMC,aAAaF,KAAKE,UAAU,IAAI;QACpC;QACA;QACA;QACA;KACD;IAED,OAAO,OAAO,EAAEC,EAAE,EAAEC,GAAG,EAAE;QACvB,MAAMC,WAAY,MAAMD,IAAIE,OAAO,CAChCC,UAAU,CAAC;YAAEC,MAAMP;YAAuBQ,OAAO;QAAE,GACnDC,KAAK,CAAC,IAAM;QACf,IAAI,CAACL,UAAU;YAAC;QAAM;QAEtB,KAAK,MAAMM,SAAST,WAAY;YAC9B,MAAMU,WAAWP,QAAQ,CAACM,MAAM;YAChC,MAAME,aACJD,YAAY,OAAOA,aAAa,WAAW,AAACA,SAA8BT,EAAE,GAAGS;YACjF,IAAIC,cAAc,QAAQC,OAAOD,gBAAgBC,OAAOX,KAAK;gBAC3D,MAAM,IAAIL,SACR,CAAC,wDAAwD,EAAEa,MAAM,GAAG,CAAC,GACnE,CAAC,qCAAqC,CAAC,EACzC;YAEJ;QACF;IACF;AACF"}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import type { CollectionBeforeChangeHook } from 'payload';
|
||||
/**
|
||||
* Sets `publishedAt` to now the first time a document transitions to published,
|
||||
* if it isn't already set. Saves editors from filling the date manually and
|
||||
* keeps blog/article dates accurate for Article JSON-LD and sitemaps.
|
||||
*
|
||||
* Attach to collections with drafts enabled (blog, articles):
|
||||
* hooks: { beforeChange: [setPublishedAtHook] }
|
||||
*
|
||||
* Only sets on the published transition; never overwrites an existing date
|
||||
* (an editor can still backdate manually).
|
||||
*/
|
||||
export declare const setPublishedAtHook: CollectionBeforeChangeHook;
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Sets `publishedAt` to now the first time a document transitions to published,
|
||||
* if it isn't already set. Saves editors from filling the date manually and
|
||||
* keeps blog/article dates accurate for Article JSON-LD and sitemaps.
|
||||
*
|
||||
* Attach to collections with drafts enabled (blog, articles):
|
||||
* hooks: { beforeChange: [setPublishedAtHook] }
|
||||
*
|
||||
* Only sets on the published transition; never overwrites an existing date
|
||||
* (an editor can still backdate manually).
|
||||
*/ export const setPublishedAtHook = ({ data, originalDoc })=>{
|
||||
const becomingPublished = data._status === 'published' && originalDoc?._status !== 'published';
|
||||
if (becomingPublished && !data.publishedAt) {
|
||||
data.publishedAt = new Date().toISOString();
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
//# sourceMappingURL=setPublishedAt.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/hooks/setPublishedAt.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\n/**\n * Sets `publishedAt` to now the first time a document transitions to published,\n * if it isn't already set. Saves editors from filling the date manually and\n * keeps blog/article dates accurate for Article JSON-LD and sitemaps.\n *\n * Attach to collections with drafts enabled (blog, articles):\n * hooks: { beforeChange: [setPublishedAtHook] }\n *\n * Only sets on the published transition; never overwrites an existing date\n * (an editor can still backdate manually).\n */\nexport const setPublishedAtHook: CollectionBeforeChangeHook = ({ data, originalDoc }) => {\n const becomingPublished = data._status === 'published' && originalDoc?._status !== 'published'\n if (becomingPublished && !data.publishedAt) {\n data.publishedAt = new Date().toISOString()\n }\n return data\n}\n"],"names":["setPublishedAtHook","data","originalDoc","becomingPublished","_status","publishedAt","Date","toISOString"],"mappings":"AAEA;;;;;;;;;;CAUC,GACD,OAAO,MAAMA,qBAAiD,CAAC,EAAEC,IAAI,EAAEC,WAAW,EAAE;IAClF,MAAMC,oBAAoBF,KAAKG,OAAO,KAAK,eAAeF,aAAaE,YAAY;IACnF,IAAID,qBAAqB,CAACF,KAAKI,WAAW,EAAE;QAC1CJ,KAAKI,WAAW,GAAG,IAAIC,OAAOC,WAAW;IAC3C;IACA,OAAON;AACT,EAAC"}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import type { CollectionBeforeChangeHook } from 'payload';
|
||||
/**
|
||||
* When a document's slug changes, appends the OLD slug to a `slugHistory` array
|
||||
* field. The project reads slugHistory to serve a 301 redirect from old URLs to
|
||||
* the current one — so changing a slug doesn't 404 the old address (a real SEO
|
||||
* loss / audit finding).
|
||||
*
|
||||
* Requires a `slugHistory` field on the collection:
|
||||
* { name: 'slugHistory', type: 'array', fields: [{ name: 'slug', type: 'text' }],
|
||||
* admin: { readOnly: true } }
|
||||
*
|
||||
* hooks: { beforeChange: [trackSlugHistoryHook] }
|
||||
*
|
||||
* The project then, in resolveRoute or a redirect check, looks up slugHistory and
|
||||
* 301s to the current slug. See docs/hooks.md.
|
||||
*/
|
||||
export declare const trackSlugHistoryHook: CollectionBeforeChangeHook;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* When a document's slug changes, appends the OLD slug to a `slugHistory` array
|
||||
* field. The project reads slugHistory to serve a 301 redirect from old URLs to
|
||||
* the current one — so changing a slug doesn't 404 the old address (a real SEO
|
||||
* loss / audit finding).
|
||||
*
|
||||
* Requires a `slugHistory` field on the collection:
|
||||
* { name: 'slugHistory', type: 'array', fields: [{ name: 'slug', type: 'text' }],
|
||||
* admin: { readOnly: true } }
|
||||
*
|
||||
* hooks: { beforeChange: [trackSlugHistoryHook] }
|
||||
*
|
||||
* The project then, in resolveRoute or a redirect check, looks up slugHistory and
|
||||
* 301s to the current slug. See docs/hooks.md.
|
||||
*/ export const trackSlugHistoryHook = ({ data, originalDoc })=>{
|
||||
const oldSlug = originalDoc?.slug;
|
||||
const newSlug = data.slug;
|
||||
if (typeof oldSlug === 'string' && typeof newSlug === 'string' && oldSlug !== newSlug && oldSlug.length > 0) {
|
||||
const history = Array.isArray(data.slugHistory) ? data.slugHistory : Array.isArray(originalDoc?.slugHistory) ? originalDoc.slugHistory : [];
|
||||
// Avoid duplicates; don't record the new slug itself.
|
||||
if (!history.some((h)=>h?.slug === oldSlug)) {
|
||||
data.slugHistory = [
|
||||
...history,
|
||||
{
|
||||
slug: oldSlug
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
//# sourceMappingURL=trackSlugHistory.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/hooks/trackSlugHistory.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\n/**\n * When a document's slug changes, appends the OLD slug to a `slugHistory` array\n * field. The project reads slugHistory to serve a 301 redirect from old URLs to\n * the current one — so changing a slug doesn't 404 the old address (a real SEO\n * loss / audit finding).\n *\n * Requires a `slugHistory` field on the collection:\n * { name: 'slugHistory', type: 'array', fields: [{ name: 'slug', type: 'text' }],\n * admin: { readOnly: true } }\n *\n * hooks: { beforeChange: [trackSlugHistoryHook] }\n *\n * The project then, in resolveRoute or a redirect check, looks up slugHistory and\n * 301s to the current slug. See docs/hooks.md.\n */\nexport const trackSlugHistoryHook: CollectionBeforeChangeHook = ({ data, originalDoc }) => {\n const oldSlug = originalDoc?.slug\n const newSlug = data.slug\n if (\n typeof oldSlug === 'string' &&\n typeof newSlug === 'string' &&\n oldSlug !== newSlug &&\n oldSlug.length > 0\n ) {\n const history: Array<{ slug: string }> = Array.isArray(data.slugHistory)\n ? data.slugHistory\n : Array.isArray(originalDoc?.slugHistory)\n ? originalDoc.slugHistory\n : []\n // Avoid duplicates; don't record the new slug itself.\n if (!history.some((h) => h?.slug === oldSlug)) {\n data.slugHistory = [...history, { slug: oldSlug }]\n }\n }\n return data\n}\n"],"names":["trackSlugHistoryHook","data","originalDoc","oldSlug","slug","newSlug","length","history","Array","isArray","slugHistory","some","h"],"mappings":"AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMA,uBAAmD,CAAC,EAAEC,IAAI,EAAEC,WAAW,EAAE;IACpF,MAAMC,UAAUD,aAAaE;IAC7B,MAAMC,UAAUJ,KAAKG,IAAI;IACzB,IACE,OAAOD,YAAY,YACnB,OAAOE,YAAY,YACnBF,YAAYE,WACZF,QAAQG,MAAM,GAAG,GACjB;QACA,MAAMC,UAAmCC,MAAMC,OAAO,CAACR,KAAKS,WAAW,IACnET,KAAKS,WAAW,GAChBF,MAAMC,OAAO,CAACP,aAAaQ,eACzBR,YAAYQ,WAAW,GACvB,EAAE;QACR,sDAAsD;QACtD,IAAI,CAACH,QAAQI,IAAI,CAAC,CAACC,IAAMA,GAAGR,SAASD,UAAU;YAC7CF,KAAKS,WAAW,GAAG;mBAAIH;gBAAS;oBAAEH,MAAMD;gBAAQ;aAAE;QACpD;IACF;IACA,OAAOF;AACT,EAAC"}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import type { FieldHook } from 'payload';
|
||||
/**
|
||||
* Field hook for a System Page role relationship in SiteSettings: ensures a page
|
||||
* isn't assigned to two roles at once (e.g. the same page as both homepage and
|
||||
* privacyPolicy), which would make routing ambiguous.
|
||||
*
|
||||
* Attach to each role field's beforeValidate. `siblingFields` are the OTHER role
|
||||
* field names to check against.
|
||||
*
|
||||
* hooks: { beforeValidate: [buildValidateUniqueRole({
|
||||
* siblingFields: ['privacyPolicy', 'cookiePolicy', 'termsOfService'],
|
||||
* })] }
|
||||
*/
|
||||
export declare function buildValidateUniqueRole(args: {
|
||||
siblingFields: string[];
|
||||
}): FieldHook;
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { APIError } from 'payload';
|
||||
/**
|
||||
* Field hook for a System Page role relationship in SiteSettings: ensures a page
|
||||
* isn't assigned to two roles at once (e.g. the same page as both homepage and
|
||||
* privacyPolicy), which would make routing ambiguous.
|
||||
*
|
||||
* Attach to each role field's beforeValidate. `siblingFields` are the OTHER role
|
||||
* field names to check against.
|
||||
*
|
||||
* hooks: { beforeValidate: [buildValidateUniqueRole({
|
||||
* siblingFields: ['privacyPolicy', 'cookiePolicy', 'termsOfService'],
|
||||
* })] }
|
||||
*/ export function buildValidateUniqueRole(args) {
|
||||
return ({ field, siblingData, value })=>{
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
const thisId = typeof value === 'object' ? value.id : value;
|
||||
for (const sibling of args.siblingFields){
|
||||
const other = siblingData?.[sibling];
|
||||
const otherId = other && typeof other === 'object' ? other.id : other;
|
||||
if (otherId != null && String(otherId) === String(thisId)) {
|
||||
const name = typeof field === 'object' && 'name' in field ? field.name : 'ta rola';
|
||||
throw new APIError(`Ta sama strona jest przypisana do "${name}" i "${sibling}". ` + `Każda rola systemowa musi wskazywać inną stronę.`, 400);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=validateUniqueRole.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/hooks/validateUniqueRole.ts"],"sourcesContent":["import type { FieldHook } from 'payload'\n\nimport { APIError } from 'payload'\n\n/**\n * Field hook for a System Page role relationship in SiteSettings: ensures a page\n * isn't assigned to two roles at once (e.g. the same page as both homepage and\n * privacyPolicy), which would make routing ambiguous.\n *\n * Attach to each role field's beforeValidate. `siblingFields` are the OTHER role\n * field names to check against.\n *\n * hooks: { beforeValidate: [buildValidateUniqueRole({\n * siblingFields: ['privacyPolicy', 'cookiePolicy', 'termsOfService'],\n * })] }\n */\nexport function buildValidateUniqueRole(args: { siblingFields: string[] }): FieldHook {\n return ({ field, siblingData, value }) => {\n if (value == null) {return value}\n const thisId = typeof value === 'object' ? (value as { id?: unknown }).id : value\n for (const sibling of args.siblingFields) {\n const other = (siblingData as Record<string, unknown>)?.[sibling]\n const otherId = other && typeof other === 'object' ? (other as { id?: unknown }).id : other\n if (otherId != null && String(otherId) === String(thisId)) {\n const name = typeof field === 'object' && 'name' in field ? field.name : 'ta rola'\n throw new APIError(\n `Ta sama strona jest przypisana do \"${name}\" i \"${sibling}\". ` +\n `Każda rola systemowa musi wskazywać inną stronę.`,\n 400,\n )\n }\n }\n return value\n }\n}\n"],"names":["APIError","buildValidateUniqueRole","args","field","siblingData","value","thisId","id","sibling","siblingFields","other","otherId","String","name"],"mappings":"AAEA,SAASA,QAAQ,QAAQ,UAAS;AAElC;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,wBAAwBC,IAAiC;IACvE,OAAO,CAAC,EAAEC,KAAK,EAAEC,WAAW,EAAEC,KAAK,EAAE;QACnC,IAAIA,SAAS,MAAM;YAAC,OAAOA;QAAK;QAChC,MAAMC,SAAS,OAAOD,UAAU,WAAW,AAACA,MAA2BE,EAAE,GAAGF;QAC5E,KAAK,MAAMG,WAAWN,KAAKO,aAAa,CAAE;YACxC,MAAMC,QAASN,aAAyC,CAACI,QAAQ;YACjE,MAAMG,UAAUD,SAAS,OAAOA,UAAU,WAAW,AAACA,MAA2BH,EAAE,GAAGG;YACtF,IAAIC,WAAW,QAAQC,OAAOD,aAAaC,OAAON,SAAS;gBACzD,MAAMO,OAAO,OAAOV,UAAU,YAAY,UAAUA,QAAQA,MAAMU,IAAI,GAAG;gBACzE,MAAM,IAAIb,SACR,CAAC,mCAAmC,EAAEa,KAAK,KAAK,EAAEL,QAAQ,GAAG,CAAC,GAC5D,CAAC,gDAAgD,CAAC,EACpD;YAEJ;QACF;QACA,OAAOH;IACT;AACF"}
|
||||
+22
-22
@@ -1,21 +1,21 @@
|
||||
import type { I18nConfig } from './types.js';
|
||||
import type { I18nConfig } from '../i18n/index.js';
|
||||
/**
|
||||
* Minimal request shape the middleware reads. Kept structural so the plugin
|
||||
* doesn't hard-depend on next/server types; a Next.js `NextRequest` satisfies it.
|
||||
*/
|
||||
type MiddlewareRequest = {
|
||||
nextUrl: {
|
||||
pathname: string;
|
||||
search: string;
|
||||
clone: () => URL;
|
||||
};
|
||||
cookies: {
|
||||
get: (name: string) => {
|
||||
value: string;
|
||||
} | undefined;
|
||||
};
|
||||
headers: {
|
||||
get: (name: string) => null | string;
|
||||
};
|
||||
nextUrl: {
|
||||
clone: () => URL;
|
||||
pathname: string;
|
||||
search: string;
|
||||
get: (name: string) => string | null;
|
||||
};
|
||||
url: string;
|
||||
};
|
||||
@@ -25,21 +25,23 @@ type MiddlewareRequest = {
|
||||
* `next` means let the request pass through untouched.
|
||||
*/
|
||||
export type LocaleMiddlewareResult = {
|
||||
cookie?: {
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
location: string;
|
||||
type: 'redirect';
|
||||
} | {
|
||||
cookie?: {
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
type: 'next';
|
||||
cookie?: {
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
} | {
|
||||
type: 'redirect';
|
||||
location: string;
|
||||
cookie?: {
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
type CreateLocaleMiddlewareArgs = {
|
||||
config: I18nConfig;
|
||||
/** Cookie name for the locale choice. Defaults to LOCALE_COOKIE_NAME. */
|
||||
cookieName?: string;
|
||||
/**
|
||||
* Consent category that gates *persisting* the locale cookie. The locale is
|
||||
* always detected (routing works regardless), but the choice is only written
|
||||
@@ -47,11 +49,9 @@ type CreateLocaleMiddlewareArgs = {
|
||||
* 'functional'. Pass 'necessary' to always persist (treat locale as strictly
|
||||
* necessary), which restores the pre-consent behaviour.
|
||||
*/
|
||||
consentCategory?: 'functional' | 'necessary';
|
||||
consentCategory?: 'necessary' | 'functional';
|
||||
/** Name of the consent cookie to read. Defaults to CONSENT_COOKIE. */
|
||||
consentCookieName?: string;
|
||||
/** Cookie name for the locale choice. Defaults to LOCALE_COOKIE_NAME. */
|
||||
cookieName?: string;
|
||||
};
|
||||
/**
|
||||
* Builds locale-routing logic for Next.js middleware.
|
||||
@@ -80,7 +80,7 @@ type CreateLocaleMiddlewareArgs = {
|
||||
* return res
|
||||
* }
|
||||
*/
|
||||
export declare function createLocaleMiddleware({ config, consentCategory, consentCookieName, cookieName, }: CreateLocaleMiddlewareArgs): (request: MiddlewareRequest) => LocaleMiddlewareResult;
|
||||
export declare function createLocaleMiddleware({ config, cookieName, consentCategory, consentCookieName, }: CreateLocaleMiddlewareArgs): (request: MiddlewareRequest) => LocaleMiddlewareResult;
|
||||
/**
|
||||
* Default Next.js middleware matcher: run on everything except API routes, the
|
||||
* admin panel, Next internals, and files with an extension (static assets).
|
||||
|
||||
+15
-7
@@ -1,5 +1,5 @@
|
||||
import { negotiateLocale, isValidLocale, LOCALE_COOKIE_NAME } from '../i18n/index.js';
|
||||
import { CONSENT_COOKIE, parseConsent } from '../consent/storage.js';
|
||||
import { isValidLocale, LOCALE_COOKIE_NAME, negotiateLocale } from '../i18n/index.js';
|
||||
/**
|
||||
* First path segment of a URL pathname, or '' for root.
|
||||
* '/pl/o-nas' → 'pl', '/o-nas' → 'o-nas', '/' → ''.
|
||||
@@ -32,18 +32,26 @@ import { isValidLocale, LOCALE_COOKIE_NAME, negotiateLocale } from '../i18n/inde
|
||||
* if (r.cookie) res.cookies.set(r.cookie.name, r.cookie.value)
|
||||
* return res
|
||||
* }
|
||||
*/ export function createLocaleMiddleware({ config, consentCategory = 'functional', consentCookieName = CONSENT_COOKIE, cookieName = LOCALE_COOKIE_NAME }) {
|
||||
*/ export function createLocaleMiddleware({ config, cookieName = LOCALE_COOKIE_NAME, consentCategory = 'functional', consentCookieName = CONSENT_COOKIE }) {
|
||||
// Whether the locale cookie may be written: 'necessary' is always granted;
|
||||
// 'functional' (default) requires the visitor to have consented.
|
||||
function mayPersistLocale(request) {
|
||||
if (consentCategory === 'necessary') {
|
||||
return true;
|
||||
}
|
||||
if (consentCategory === 'necessary') return true;
|
||||
const consent = parseConsent(request.cookies.get(consentCookieName)?.value);
|
||||
return consent?.[consentCategory] === true;
|
||||
}
|
||||
return function localeMiddleware(request) {
|
||||
const { pathname } = request.nextUrl;
|
||||
// Single-locale sites have no /pl, /en prefix and no negotiation — one
|
||||
// language, no redirect. The middleware becomes a pass-through: paths stay
|
||||
// as-is (/o-nas), nothing to detect or persist. (Projects that are truly
|
||||
// single-locale usually don't even mount this middleware, but guarding here
|
||||
// makes it safe if they do.)
|
||||
if (config.locales.length === 1) {
|
||||
return {
|
||||
type: 'next'
|
||||
};
|
||||
}
|
||||
// Already locale-prefixed (e.g. the visitor switched language by
|
||||
// navigating to /en). Routing is fine — but if the URL's locale differs
|
||||
// from the stored cookie, the visitor is *choosing* a language, and we
|
||||
@@ -68,9 +76,9 @@ import { isValidLocale, LOCALE_COOKIE_NAME, negotiateLocale } from '../i18n/inde
|
||||
}
|
||||
// Resolve the locale to use
|
||||
const locale = negotiateLocale({
|
||||
cookieLocale: request.cookies.get(cookieName)?.value ?? null,
|
||||
acceptLanguage: request.headers.get('accept-language'),
|
||||
config,
|
||||
cookieLocale: request.cookies.get(cookieName)?.value ?? null
|
||||
config
|
||||
});
|
||||
// Redirect to the locale-prefixed path, preserving the rest
|
||||
const url = request.nextUrl.clone();
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+19
-13
@@ -8,14 +8,22 @@ import type { I18nConfig } from './types.js';
|
||||
*/
|
||||
export type LocalizedSlugs = Record<string, string>;
|
||||
type BuildPathArgs = {
|
||||
config: I18nConfig;
|
||||
/**
|
||||
* Slug that represents the site root (served at /{locale} with no trailing
|
||||
* segment). Defaults to 'home'. Matched against the slug in the target locale.
|
||||
*/
|
||||
homeSlug?: string;
|
||||
/** slug per locale, e.g. { pl: 'strona-glowna', en: 'home' } */
|
||||
slugs: LocalizedSlugs;
|
||||
/** Target locale to build the path for */
|
||||
locale: string;
|
||||
config: I18nConfig;
|
||||
/**
|
||||
* Slug(s) representing the site root (served at /{locale} with no trailing
|
||||
* segment). Defaults to 'home'. Matched against the slug in the target locale.
|
||||
*
|
||||
* Can be a single string (same home slug in every locale) OR a per-locale map
|
||||
* (`{ pl: 'strona-glowna', de: 'startseite', en: 'home' }`). The map form is
|
||||
* REQUIRED for multilingual homepages whose slug differs per language —
|
||||
* otherwise the home page collapses to '/pl' but '/de/startseite' stays
|
||||
* un-collapsed, breaking hreflang return tags (a real GSC error).
|
||||
*/
|
||||
homeSlug?: string | Record<string, string>;
|
||||
/**
|
||||
* Localized segment the document lives under, e.g.
|
||||
* `{ pl: 'artykuly', en: 'articles' }` → /pl/artykuly/moj-post.
|
||||
@@ -25,8 +33,6 @@ type BuildPathArgs = {
|
||||
* A document under a prefix is never the home page, so homeSlug is ignored.
|
||||
*/
|
||||
prefix?: LocalizedSlugs;
|
||||
/** slug per locale, e.g. { pl: 'strona-glowna', en: 'home' } */
|
||||
slugs: LocalizedSlugs;
|
||||
};
|
||||
/**
|
||||
* Builds a locale-prefixed path for a document in a target locale.
|
||||
@@ -50,13 +56,13 @@ type BuildPathArgs = {
|
||||
* })
|
||||
* // → '/en/articles/my-post'
|
||||
*/
|
||||
export declare function buildLocalizedPath({ config, homeSlug, locale, prefix, slugs, }: BuildPathArgs): string | undefined;
|
||||
export declare function buildLocalizedPath({ slugs, locale, config, homeSlug, prefix, }: BuildPathArgs): string | undefined;
|
||||
type SwitchLocaleArgs = {
|
||||
config: I18nConfig;
|
||||
homeSlug?: string;
|
||||
prefix?: LocalizedSlugs;
|
||||
slugs: LocalizedSlugs;
|
||||
targetLocale: string;
|
||||
config: I18nConfig;
|
||||
homeSlug?: string | Record<string, string>;
|
||||
prefix?: LocalizedSlugs;
|
||||
};
|
||||
/**
|
||||
* Resolves the equivalent path for the same document in a different locale —
|
||||
@@ -66,5 +72,5 @@ type SwitchLocaleArgs = {
|
||||
* falls back to the archive it belongs to (/en/articles) if there is one, and
|
||||
* to the locale root otherwise — the closest place the visitor would want.
|
||||
*/
|
||||
export declare function switchLocalePath({ config, homeSlug, prefix, slugs, targetLocale, }: SwitchLocaleArgs): string;
|
||||
export declare function switchLocalePath({ slugs, targetLocale, config, homeSlug, prefix, }: SwitchLocaleArgs): string;
|
||||
export {};
|
||||
|
||||
Vendored
+23
-15
@@ -20,7 +20,7 @@ import { isValidLocale } from './helpers.js';
|
||||
* config,
|
||||
* })
|
||||
* // → '/en/articles/my-post'
|
||||
*/ export function buildLocalizedPath({ config, homeSlug = 'home', locale, prefix, slugs }) {
|
||||
*/ export function buildLocalizedPath({ slugs, locale, config, homeSlug = 'home', prefix }) {
|
||||
if (!isValidLocale(locale, config)) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -28,6 +28,12 @@ import { isValidLocale } from './helpers.js';
|
||||
if (!slug) {
|
||||
return undefined;
|
||||
}
|
||||
// Single-locale sites have no /pl, /en prefix — the language segment is
|
||||
// dropped entirely (path is /o-nas, not /pl/o-nas). Detected automatically:
|
||||
// one configured locale means one language, so no prefix is needed. The
|
||||
// project's folder structure matches (app/[[...slug]] without [locale]).
|
||||
const singleLocale = config.locales.length === 1;
|
||||
const localeSegment = singleLocale ? '' : `/${locale}`;
|
||||
if (prefix) {
|
||||
const segment = prefix[locale];
|
||||
// No archive slug in this locale means the entry is unreachable there —
|
||||
@@ -36,12 +42,18 @@ import { isValidLocale } from './helpers.js';
|
||||
if (!segment) {
|
||||
return undefined;
|
||||
}
|
||||
return `/${locale}/${segment}/${slug}`;
|
||||
return `${localeSegment}/${segment}/${slug}`;
|
||||
}
|
||||
if (slug === homeSlug) {
|
||||
return `/${locale}`;
|
||||
// Resolve the home slug for THIS locale. With a per-locale map, each language
|
||||
// has its own home slug (pl: 'strona-glowna', de: 'startseite'), so the check
|
||||
// below correctly collapses each language's home to its root — instead of only
|
||||
// the current-language slug matching and the others staying un-collapsed.
|
||||
const targetHomeSlug = typeof homeSlug === 'object' ? homeSlug[locale] : homeSlug;
|
||||
if (slug === targetHomeSlug) {
|
||||
// Home collapses to the root: '/' for single-locale, '/pl' otherwise.
|
||||
return localeSegment || '/';
|
||||
}
|
||||
return `/${locale}/${slug}`;
|
||||
return `${localeSegment}/${slug}`;
|
||||
}
|
||||
/**
|
||||
* Resolves the equivalent path for the same document in a different locale —
|
||||
@@ -50,21 +62,17 @@ import { isValidLocale } from './helpers.js';
|
||||
* Never dead-ends on a 404. When the document has no slug in the target locale,
|
||||
* falls back to the archive it belongs to (/en/articles) if there is one, and
|
||||
* to the locale root otherwise — the closest place the visitor would want.
|
||||
*/ export function switchLocalePath({ config, homeSlug = 'home', prefix, slugs, targetLocale }) {
|
||||
*/ export function switchLocalePath({ slugs, targetLocale, config, homeSlug = 'home', prefix }) {
|
||||
const path = buildLocalizedPath({
|
||||
slugs,
|
||||
locale: targetLocale,
|
||||
config,
|
||||
homeSlug,
|
||||
locale: targetLocale,
|
||||
prefix,
|
||||
slugs
|
||||
prefix
|
||||
});
|
||||
if (path) {
|
||||
return path;
|
||||
}
|
||||
if (path) return path;
|
||||
const archiveSegment = prefix?.[targetLocale];
|
||||
if (archiveSegment) {
|
||||
return `/${targetLocale}/${archiveSegment}`;
|
||||
}
|
||||
if (archiveSegment) return `/${targetLocale}/${archiveSegment}`;
|
||||
return `/${targetLocale}`;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -16,7 +16,7 @@ type GetSystemPagePathArgs = {
|
||||
* page's slug in the target locale equals this, the path collapses to the
|
||||
* locale root (/pl, /en).
|
||||
*/
|
||||
homeSlug?: string;
|
||||
homeSlug?: string | Record<string, string>;
|
||||
/** Target locale to build the path for. */
|
||||
locale: string;
|
||||
/**
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/pages/getSystemPagePath.ts"],"sourcesContent":["import type { I18nConfig } from '../i18n/index.js'\nimport type { SystemPageRole } from './types.js'\n\nimport { buildLocalizedPath, getLocalizedSlugs } from '../i18n/index.js'\n\n/**\n * The shape we need from an assigned system-page document: its localized slug\n * field, as returned by Payload when the parent is read with `locale: 'all'`.\n *\n * The plugin doesn't know the client's Pages type, so it depends only on this\n * minimal contract.\n */\ntype AssignedPage = {\n slug?: null | Record<string, unknown>\n}\n\ntype GetSystemPagePathArgs = {\n config: I18nConfig\n /**\n * Slug that represents the site root. Defaults to 'home'. When the assigned\n * page's slug in the target locale equals this, the path collapses to the\n * locale root (/pl, /en).\n */\n homeSlug?: string\n /** Target locale to build the path for. */\n locale: string\n /**\n * The resolved system-page assignment from SiteSettings — the related\n * document object (not just an ID), read with `locale: 'all'` so its slug\n * is a locale→value map. Pass null/undefined if the role is unassigned.\n */\n page: AssignedPage | null | undefined\n}\n\n/**\n * Resolves a system-page assignment to a locale-aware path.\n *\n * Bridges the Pages module (which document plays a role) and the i18n module\n * (how that document's localized slug becomes a URL). This is what powers\n * \"visit /pl → serve the homepage\": read SiteSettings.homepage, pass the\n * related document here, get /pl/strona-glowna (or /pl if it's the home slug).\n *\n * Returns undefined when the role is unassigned or the assigned page has no\n * slug in the target locale — the caller decides the fallback (e.g. 404,\n * redirect to default locale).\n *\n * @example\n * const settings = await payload.findGlobal({ slug: 'site-settings', locale: 'all', depth: 1 })\n * getSystemPagePath({ page: settings.homepage, locale: 'pl', config })\n * // → '/pl' (home slug collapses to root)\n * getSystemPagePath({ page: settings.privacyPolicy, locale: 'en', config })\n * // → '/en/privacy-policy'\n */\nexport function getSystemPagePath({\n config,\n homeSlug = 'home',\n locale,\n page,\n}: GetSystemPagePathArgs): string | undefined {\n if (!page?.slug) {\n return undefined\n }\n\n const slugs = getLocalizedSlugs({ config, slugField: page.slug })\n return buildLocalizedPath({ config, homeSlug, locale, slugs })\n}\n"],"names":["buildLocalizedPath","getLocalizedSlugs","getSystemPagePath","config","homeSlug","locale","page","slug","undefined","slugs","slugField"],"mappings":"AAGA,SAASA,kBAAkB,EAAEC,iBAAiB,QAAQ,mBAAkB;AA+BxE;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,kBAAkB,EAChCC,MAAM,EACNC,WAAW,MAAM,EACjBC,MAAM,EACNC,IAAI,EACkB;IACtB,IAAI,CAACA,MAAMC,MAAM;QACf,OAAOC;IACT;IAEA,MAAMC,QAAQR,kBAAkB;QAAEE;QAAQO,WAAWJ,KAAKC,IAAI;IAAC;IAC/D,OAAOP,mBAAmB;QAAEG;QAAQC;QAAUC;QAAQI;IAAM;AAC9D"}
|
||||
{"version":3,"sources":["../../../src/modules/pages/getSystemPagePath.ts"],"sourcesContent":["import type { I18nConfig } from '../i18n/index.js'\nimport type { SystemPageRole } from './types.js'\n\nimport { buildLocalizedPath, getLocalizedSlugs } from '../i18n/index.js'\n\n/**\n * The shape we need from an assigned system-page document: its localized slug\n * field, as returned by Payload when the parent is read with `locale: 'all'`.\n *\n * The plugin doesn't know the client's Pages type, so it depends only on this\n * minimal contract.\n */\ntype AssignedPage = {\n slug?: null | Record<string, unknown>\n}\n\ntype GetSystemPagePathArgs = {\n config: I18nConfig\n /**\n * Slug that represents the site root. Defaults to 'home'. When the assigned\n * page's slug in the target locale equals this, the path collapses to the\n * locale root (/pl, /en).\n */\n homeSlug?: string | Record<string, string>\n /** Target locale to build the path for. */\n locale: string\n /**\n * The resolved system-page assignment from SiteSettings — the related\n * document object (not just an ID), read with `locale: 'all'` so its slug\n * is a locale→value map. Pass null/undefined if the role is unassigned.\n */\n page: AssignedPage | null | undefined\n}\n\n/**\n * Resolves a system-page assignment to a locale-aware path.\n *\n * Bridges the Pages module (which document plays a role) and the i18n module\n * (how that document's localized slug becomes a URL). This is what powers\n * \"visit /pl → serve the homepage\": read SiteSettings.homepage, pass the\n * related document here, get /pl/strona-glowna (or /pl if it's the home slug).\n *\n * Returns undefined when the role is unassigned or the assigned page has no\n * slug in the target locale — the caller decides the fallback (e.g. 404,\n * redirect to default locale).\n *\n * @example\n * const settings = await payload.findGlobal({ slug: 'site-settings', locale: 'all', depth: 1 })\n * getSystemPagePath({ page: settings.homepage, locale: 'pl', config })\n * // → '/pl' (home slug collapses to root)\n * getSystemPagePath({ page: settings.privacyPolicy, locale: 'en', config })\n * // → '/en/privacy-policy'\n */\nexport function getSystemPagePath({\n config,\n homeSlug = 'home',\n locale,\n page,\n}: GetSystemPagePathArgs): string | undefined {\n if (!page?.slug) {\n return undefined\n }\n\n const slugs = getLocalizedSlugs({ config, slugField: page.slug })\n return buildLocalizedPath({ config, homeSlug, locale, slugs })\n}\n"],"names":["buildLocalizedPath","getLocalizedSlugs","getSystemPagePath","config","homeSlug","locale","page","slug","undefined","slugs","slugField"],"mappings":"AAGA,SAASA,kBAAkB,EAAEC,iBAAiB,QAAQ,mBAAkB;AA+BxE;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,kBAAkB,EAChCC,MAAM,EACNC,WAAW,MAAM,EACjBC,MAAM,EACNC,IAAI,EACkB;IACtB,IAAI,CAACA,MAAMC,MAAM;QACf,OAAOC;IACT;IAEA,MAAMC,QAAQR,kBAAkB;QAAEE;QAAQO,WAAWJ,KAAKC,IAAI;IAAC;IAC/D,OAAOP,mBAAmB;QAAEG;QAAQC;QAAUC;QAAQI;IAAM;AAC9D"}
|
||||
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
export type BuildCspArgs = {
|
||||
/** Google Analytics / GTM — adds googletagmanager + google-analytics. */
|
||||
analytics?: boolean;
|
||||
/** Extra sources per directive, merged with the built-ins. */
|
||||
extra?: Partial<Record<CspDirective, string[]>>;
|
||||
/** Google Maps embeds — adds maps.google.com / *.gstatic.com. */
|
||||
googleMaps?: boolean;
|
||||
/** 'enforce' → Content-Security-Policy; 'report-only' → …-Report-Only header. */
|
||||
mode?: 'enforce' | 'report-only';
|
||||
/** Media/R2 public URL (from R2_PUBLIC_URL) — added to img-src. */
|
||||
r2Url?: string;
|
||||
/** Cloudflare Turnstile — adds challenges.cloudflare.com to script/frame/connect. */
|
||||
turnstile?: boolean;
|
||||
/** YouTube embeds — adds youtube to frame-src. */
|
||||
youtube?: boolean;
|
||||
};
|
||||
type CspDirective = 'base-uri' | 'connect-src' | 'default-src' | 'font-src' | 'form-action' | 'frame-ancestors' | 'frame-src' | 'img-src' | 'media-src' | 'object-src' | 'script-src' | 'style-src' | 'worker-src';
|
||||
/**
|
||||
* Builds a Content-Security-Policy string with the hard OWASP/Lighthouse-required
|
||||
* directives baked in, and opt-in sources for common third parties. Solves the
|
||||
* real risk of hand-writing raw CSP per project and forgetting `base-uri 'self'`
|
||||
* or `object-src 'none'`.
|
||||
*
|
||||
* CSP still lives in the project (it lists the project's own domains), but this
|
||||
* helper standardizes the skeleton so every project's CSP has the same hardened
|
||||
* base — you only flip flags for what the project actually loads.
|
||||
*
|
||||
* Returns { key, value } ready for buildSecurityHeaders `additional`:
|
||||
*
|
||||
* import { buildCsp, buildSecurityHeaders } from '@intecion/ipal-kit'
|
||||
* const csp = buildCsp({
|
||||
* mode: 'report-only', // start here; switch to 'enforce' when clean
|
||||
* r2Url: process.env.R2_PUBLIC_URL,
|
||||
* turnstile: true, analytics: true,
|
||||
* })
|
||||
* const headers = buildSecurityHeaders({ hsts: prod, additional: [csp] })
|
||||
*
|
||||
* Deploy CSP carefully: start with mode:'report-only', check the console for
|
||||
* violations across the whole site (forms/Turnstile, gallery/R2, embeds), add
|
||||
* missing sources via `extra`, THEN switch to 'enforce'. See docs/security.md.
|
||||
*/
|
||||
export declare function buildCsp(args?: BuildCspArgs): {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
export {};
|
||||
Vendored
+109
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Builds a Content-Security-Policy string with the hard OWASP/Lighthouse-required
|
||||
* directives baked in, and opt-in sources for common third parties. Solves the
|
||||
* real risk of hand-writing raw CSP per project and forgetting `base-uri 'self'`
|
||||
* or `object-src 'none'`.
|
||||
*
|
||||
* CSP still lives in the project (it lists the project's own domains), but this
|
||||
* helper standardizes the skeleton so every project's CSP has the same hardened
|
||||
* base — you only flip flags for what the project actually loads.
|
||||
*
|
||||
* Returns { key, value } ready for buildSecurityHeaders `additional`:
|
||||
*
|
||||
* import { buildCsp, buildSecurityHeaders } from '@intecion/ipal-kit'
|
||||
* const csp = buildCsp({
|
||||
* mode: 'report-only', // start here; switch to 'enforce' when clean
|
||||
* r2Url: process.env.R2_PUBLIC_URL,
|
||||
* turnstile: true, analytics: true,
|
||||
* })
|
||||
* const headers = buildSecurityHeaders({ hsts: prod, additional: [csp] })
|
||||
*
|
||||
* Deploy CSP carefully: start with mode:'report-only', check the console for
|
||||
* violations across the whole site (forms/Turnstile, gallery/R2, embeds), add
|
||||
* missing sources via `extra`, THEN switch to 'enforce'. See docs/security.md.
|
||||
*/ export function buildCsp(args = {}) {
|
||||
const { analytics, extra = {}, googleMaps, mode = 'enforce', r2Url, turnstile, youtube } = args;
|
||||
const src = {
|
||||
'default-src': [
|
||||
"'self'"
|
||||
],
|
||||
// 'unsafe-inline' is hard to avoid with Next/analytics; 'unsafe-eval' is NOT
|
||||
// added by default (weakens CSP) — add via extra only if a library needs it.
|
||||
'connect-src': [
|
||||
"'self'"
|
||||
],
|
||||
'font-src': [
|
||||
"'self'",
|
||||
'https://fonts.gstatic.com',
|
||||
'data:'
|
||||
],
|
||||
'form-action': [
|
||||
"'self'"
|
||||
],
|
||||
'frame-src': [],
|
||||
'img-src': [
|
||||
"'self'",
|
||||
'data:',
|
||||
'blob:'
|
||||
],
|
||||
'media-src': [],
|
||||
'script-src': [
|
||||
"'self'",
|
||||
"'unsafe-inline'"
|
||||
],
|
||||
'style-src': [
|
||||
"'self'",
|
||||
"'unsafe-inline'",
|
||||
'https://fonts.googleapis.com'
|
||||
],
|
||||
'worker-src': [],
|
||||
// HARD defaults (OWASP/Lighthouse) — always on, no reason to omit:
|
||||
'base-uri': [
|
||||
"'self'"
|
||||
],
|
||||
'frame-ancestors': [
|
||||
"'none'"
|
||||
],
|
||||
'object-src': [
|
||||
"'none'"
|
||||
]
|
||||
};
|
||||
if (r2Url) {
|
||||
src['img-src'].push(r2Url);
|
||||
}
|
||||
if (turnstile) {
|
||||
src['script-src'].push('https://challenges.cloudflare.com');
|
||||
src['frame-src'].push('https://challenges.cloudflare.com');
|
||||
src['connect-src'].push('https://challenges.cloudflare.com');
|
||||
}
|
||||
if (analytics) {
|
||||
src['script-src'].push('https://www.googletagmanager.com');
|
||||
src['connect-src'].push('https://www.google-analytics.com', 'https://www.googletagmanager.com');
|
||||
src['img-src'].push('https://www.google-analytics.com', 'https://www.googletagmanager.com');
|
||||
}
|
||||
if (youtube) {
|
||||
src['frame-src'].push('https://www.youtube.com', 'https://www.youtube-nocookie.com');
|
||||
}
|
||||
if (googleMaps) {
|
||||
src['frame-src'].push('https://www.google.com', 'https://maps.google.com');
|
||||
src['script-src'].push('https://maps.googleapis.com');
|
||||
src['img-src'].push('https://maps.gstatic.com', 'https://*.googleapis.com');
|
||||
}
|
||||
// Merge caller extras.
|
||||
for (const [dir, values] of Object.entries(extra)){
|
||||
if (values && values.length) {
|
||||
src[dir] = [
|
||||
...src[dir] ?? [],
|
||||
...values
|
||||
];
|
||||
}
|
||||
}
|
||||
const value = Object.entries(src).filter(([, values])=>values.length > 0).map(([dir, values])=>`${dir} ${values.join(' ')}`).join('; ');
|
||||
const key = mode === 'report-only' ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy';
|
||||
return {
|
||||
key,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=buildCsp.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
@@ -13,6 +13,14 @@ export type BuildSecurityHeadersArgs = {
|
||||
* domains (scripts, images, fonts, analytics). Keep CSP in your project.
|
||||
*/
|
||||
additional?: SecurityHeader[];
|
||||
/**
|
||||
* Cross-Origin-Opener-Policy. Default 'same-origin' — isolates the browsing
|
||||
* context so a malicious page can't hold a window.opener reference (protects
|
||||
* against XS-Leaks / Spectre-class attacks). Project-independent, so it's a
|
||||
* default. Use 'same-origin-allow-popups' if you open OAuth/payment popups
|
||||
* that need window.opener; false to omit.
|
||||
*/
|
||||
coop?: 'same-origin' | 'same-origin-allow-popups' | false;
|
||||
/**
|
||||
* X-Frame-Options value. 'DENY' (default) blocks all framing; 'SAMEORIGIN'
|
||||
* allows same-origin framing. Note: CSP frame-ancestors supersedes this in
|
||||
|
||||
+8
-1
@@ -26,7 +26,7 @@
|
||||
* },
|
||||
* }
|
||||
*/ export function buildSecurityHeaders(args = {}) {
|
||||
const { additional = [], frameOptions = 'DENY', hsts = true, hstsIncludeSubDomains = true, hstsMaxAge = 63072000, hstsPreload = false, permissionsPolicy = 'camera=(), microphone=(), geolocation=()', referrerPolicy = 'strict-origin-when-cross-origin' } = args;
|
||||
const { additional = [], coop = 'same-origin', frameOptions = 'DENY', hsts = true, hstsIncludeSubDomains = true, hstsMaxAge = 63072000, hstsPreload = false, permissionsPolicy = 'camera=(), microphone=(), geolocation=()', referrerPolicy = 'strict-origin-when-cross-origin' } = args;
|
||||
const headers = [];
|
||||
if (hsts) {
|
||||
const parts = [
|
||||
@@ -66,6 +66,13 @@
|
||||
value: permissionsPolicy
|
||||
});
|
||||
}
|
||||
// COOP — isolates the browsing context (XS-Leaks / Spectre protection).
|
||||
if (coop) {
|
||||
headers.push({
|
||||
key: 'Cross-Origin-Opener-Policy',
|
||||
value: coop
|
||||
});
|
||||
}
|
||||
// Merge additional: same-key entries override the defaults above.
|
||||
for (const extra of additional){
|
||||
const i = headers.findIndex((h)=>h.key.toLowerCase() === extra.key.toLowerCase());
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+3
-1
@@ -1,2 +1,4 @@
|
||||
export { buildCsp } from './buildCsp.js';
|
||||
export type { BuildCspArgs } from './buildCsp.js';
|
||||
export { buildSecurityHeaders } from './buildSecurityHeaders.js';
|
||||
export type { BuildSecurityHeadersArgs, SecurityHeader } from './buildSecurityHeaders.js';
|
||||
export type { BuildSecurityHeadersArgs, SecurityHeader, } from './buildSecurityHeaders.js';
|
||||
|
||||
Vendored
+1
@@ -1,3 +1,4 @@
|
||||
export { buildCsp } from './buildCsp.js';
|
||||
export { buildSecurityHeaders } from './buildSecurityHeaders.js';
|
||||
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/security/index.ts"],"sourcesContent":["export { buildSecurityHeaders } from './buildSecurityHeaders.js'\nexport type { BuildSecurityHeadersArgs, SecurityHeader } from './buildSecurityHeaders.js'\n"],"names":["buildSecurityHeaders"],"mappings":"AAAA,SAASA,oBAAoB,QAAQ,4BAA2B"}
|
||||
{"version":3,"sources":["../../../src/modules/security/index.ts"],"sourcesContent":["export { buildCsp } from './buildCsp.js'\nexport type { BuildCspArgs } from './buildCsp.js'\nexport { buildSecurityHeaders } from './buildSecurityHeaders.js'\nexport type { BuildSecurityHeadersArgs, SecurityHeader, } from './buildSecurityHeaders.js'\n"],"names":["buildCsp","buildSecurityHeaders"],"mappings":"AAAA,SAASA,QAAQ,QAAQ,gBAAe;AAExC,SAASC,oBAAoB,QAAQ,4BAA2B"}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
type MediaLike = {
|
||||
url?: string | null;
|
||||
} | null | undefined;
|
||||
type ArticleJsonLdArgs = {
|
||||
/** Article headline (the title). */
|
||||
headline: string;
|
||||
/** Absolute URL of the article. */
|
||||
url: string;
|
||||
description?: string;
|
||||
/** Cover/hero image. */
|
||||
image?: MediaLike;
|
||||
/** ISO date the article was first published, e.g. '2026-01-15'. */
|
||||
datePublished?: string;
|
||||
/** ISO date of the last modification. Defaults to datePublished. */
|
||||
dateModified?: string;
|
||||
/** Author name (person or org). */
|
||||
author?: string;
|
||||
/** Publisher (usually the site/organization) name. */
|
||||
publisherName?: string;
|
||||
/** Publisher logo (for the Article rich result). */
|
||||
publisherLogo?: MediaLike;
|
||||
/** 'Article' (default), 'BlogPosting', or 'NewsArticle'. */
|
||||
type?: 'Article' | 'BlogPosting' | 'NewsArticle';
|
||||
};
|
||||
/**
|
||||
* Builds Article / BlogPosting JSON-LD (schema.org) for blog posts and articles.
|
||||
* Enables article rich results in Google (headline, date, author, image). Pairs
|
||||
* with the plugin's content collections (blog/archives).
|
||||
*
|
||||
* Per-article (each post emits its own), data from the document + panel:
|
||||
*
|
||||
* import { buildArticleJsonLd } from '@intecion/ipal-kit'
|
||||
* const jsonLd = buildArticleJsonLd({
|
||||
* headline: post.title, url: postUrl, description: post.excerpt,
|
||||
* image: post.coverImage, datePublished: post.publishedAt,
|
||||
* author: post.author, publisherName: settings.siteName,
|
||||
* publisherLogo: settings.logo, type: 'BlogPosting',
|
||||
* })
|
||||
*
|
||||
* All data from the document/panel — nothing hardcoded. Google requires headline
|
||||
* and dates for the rich result; supply them from the post's real fields.
|
||||
*/
|
||||
export declare function buildArticleJsonLd({ headline, url, description, image, datePublished, dateModified, author, publisherName, publisherLogo, type, }: ArticleJsonLdArgs): {
|
||||
publisher?: {
|
||||
logo?: {
|
||||
'@type': string;
|
||||
url: string | undefined;
|
||||
} | undefined;
|
||||
'@type': string;
|
||||
name: string;
|
||||
} | undefined;
|
||||
author?: {
|
||||
'@type': string;
|
||||
name: string;
|
||||
} | undefined;
|
||||
dateModified?: string | undefined;
|
||||
datePublished?: string | undefined;
|
||||
image?: string | undefined;
|
||||
description?: string | undefined;
|
||||
'@context': string;
|
||||
'@type': "Article" | "BlogPosting" | "NewsArticle";
|
||||
headline: string;
|
||||
url: string;
|
||||
mainEntityOfPage: {
|
||||
'@type': string;
|
||||
'@id': string;
|
||||
};
|
||||
};
|
||||
export {};
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Builds Article / BlogPosting JSON-LD (schema.org) for blog posts and articles.
|
||||
* Enables article rich results in Google (headline, date, author, image). Pairs
|
||||
* with the plugin's content collections (blog/archives).
|
||||
*
|
||||
* Per-article (each post emits its own), data from the document + panel:
|
||||
*
|
||||
* import { buildArticleJsonLd } from '@intecion/ipal-kit'
|
||||
* const jsonLd = buildArticleJsonLd({
|
||||
* headline: post.title, url: postUrl, description: post.excerpt,
|
||||
* image: post.coverImage, datePublished: post.publishedAt,
|
||||
* author: post.author, publisherName: settings.siteName,
|
||||
* publisherLogo: settings.logo, type: 'BlogPosting',
|
||||
* })
|
||||
*
|
||||
* All data from the document/panel — nothing hardcoded. Google requires headline
|
||||
* and dates for the rich result; supply them from the post's real fields.
|
||||
*/ export function buildArticleJsonLd({ headline, url, description, image, datePublished, dateModified, author, publisherName, publisherLogo, type = 'Article' }) {
|
||||
const imageUrl = image?.url;
|
||||
const logoUrl = publisherLogo?.url;
|
||||
const abs = (u)=>u ? u.startsWith('http') ? u : `${url.split('/').slice(0, 3).join('/')}${u}` : undefined;
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': type,
|
||||
headline,
|
||||
url,
|
||||
mainEntityOfPage: {
|
||||
'@type': 'WebPage',
|
||||
'@id': url
|
||||
},
|
||||
...description ? {
|
||||
description
|
||||
} : {},
|
||||
...imageUrl ? {
|
||||
image: abs(imageUrl)
|
||||
} : {},
|
||||
...datePublished ? {
|
||||
datePublished
|
||||
} : {},
|
||||
...dateModified || datePublished ? {
|
||||
dateModified: dateModified ?? datePublished
|
||||
} : {},
|
||||
...author ? {
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
name: author
|
||||
}
|
||||
} : {},
|
||||
...publisherName ? {
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: publisherName,
|
||||
...logoUrl ? {
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: abs(logoUrl)
|
||||
}
|
||||
} : {}
|
||||
}
|
||||
} : {}
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=buildArticleJsonLd.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/seo/buildArticleJsonLd.ts"],"sourcesContent":["type MediaLike = { url?: string | null } | null | undefined\n\ntype ArticleJsonLdArgs = {\n /** Article headline (the title). */\n headline: string\n /** Absolute URL of the article. */\n url: string\n description?: string\n /** Cover/hero image. */\n image?: MediaLike\n /** ISO date the article was first published, e.g. '2026-01-15'. */\n datePublished?: string\n /** ISO date of the last modification. Defaults to datePublished. */\n dateModified?: string\n /** Author name (person or org). */\n author?: string\n /** Publisher (usually the site/organization) name. */\n publisherName?: string\n /** Publisher logo (for the Article rich result). */\n publisherLogo?: MediaLike\n /** 'Article' (default), 'BlogPosting', or 'NewsArticle'. */\n type?: 'Article' | 'BlogPosting' | 'NewsArticle'\n}\n\n/**\n * Builds Article / BlogPosting JSON-LD (schema.org) for blog posts and articles.\n * Enables article rich results in Google (headline, date, author, image). Pairs\n * with the plugin's content collections (blog/archives).\n *\n * Per-article (each post emits its own), data from the document + panel:\n *\n * import { buildArticleJsonLd } from '@intecion/ipal-kit'\n * const jsonLd = buildArticleJsonLd({\n * headline: post.title, url: postUrl, description: post.excerpt,\n * image: post.coverImage, datePublished: post.publishedAt,\n * author: post.author, publisherName: settings.siteName,\n * publisherLogo: settings.logo, type: 'BlogPosting',\n * })\n *\n * All data from the document/panel — nothing hardcoded. Google requires headline\n * and dates for the rich result; supply them from the post's real fields.\n */\nexport function buildArticleJsonLd({\n headline,\n url,\n description,\n image,\n datePublished,\n dateModified,\n author,\n publisherName,\n publisherLogo,\n type = 'Article',\n}: ArticleJsonLdArgs) {\n const imageUrl = image?.url\n const logoUrl = publisherLogo?.url\n\n const abs = (u: string | null | undefined): string | undefined =>\n u ? (u.startsWith('http') ? u : `${url.split('/').slice(0, 3).join('/')}${u}`) : undefined\n\n return {\n '@context': 'https://schema.org',\n '@type': type,\n headline,\n url,\n mainEntityOfPage: { '@type': 'WebPage', '@id': url },\n ...(description ? { description } : {}),\n ...(imageUrl ? { image: abs(imageUrl) } : {}),\n ...(datePublished ? { datePublished } : {}),\n ...(dateModified || datePublished ? { dateModified: dateModified ?? datePublished } : {}),\n ...(author ? { author: { '@type': 'Person', name: author } } : {}),\n ...(publisherName\n ? {\n publisher: {\n '@type': 'Organization',\n name: publisherName,\n ...(logoUrl ? { logo: { '@type': 'ImageObject', url: abs(logoUrl) } } : {}),\n },\n }\n : {}),\n }\n}\n"],"names":["buildArticleJsonLd","headline","url","description","image","datePublished","dateModified","author","publisherName","publisherLogo","type","imageUrl","logoUrl","abs","u","startsWith","split","slice","join","undefined","mainEntityOfPage","name","publisher","logo"],"mappings":"AAwBA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASA,mBAAmB,EACjCC,QAAQ,EACRC,GAAG,EACHC,WAAW,EACXC,KAAK,EACLC,aAAa,EACbC,YAAY,EACZC,MAAM,EACNC,aAAa,EACbC,aAAa,EACbC,OAAO,SAAS,EACE;IAClB,MAAMC,WAAWP,OAAOF;IACxB,MAAMU,UAAUH,eAAeP;IAE/B,MAAMW,MAAM,CAACC,IACXA,IAAKA,EAAEC,UAAU,CAAC,UAAUD,IAAI,GAAGZ,IAAIc,KAAK,CAAC,KAAKC,KAAK,CAAC,GAAG,GAAGC,IAAI,CAAC,OAAOJ,GAAG,GAAIK;IAEnF,OAAO;QACL,YAAY;QACZ,SAAST;QACTT;QACAC;QACAkB,kBAAkB;YAAE,SAAS;YAAW,OAAOlB;QAAI;QACnD,GAAIC,cAAc;YAAEA;QAAY,IAAI,CAAC,CAAC;QACtC,GAAIQ,WAAW;YAAEP,OAAOS,IAAIF;QAAU,IAAI,CAAC,CAAC;QAC5C,GAAIN,gBAAgB;YAAEA;QAAc,IAAI,CAAC,CAAC;QAC1C,GAAIC,gBAAgBD,gBAAgB;YAAEC,cAAcA,gBAAgBD;QAAc,IAAI,CAAC,CAAC;QACxF,GAAIE,SAAS;YAAEA,QAAQ;gBAAE,SAAS;gBAAUc,MAAMd;YAAO;QAAE,IAAI,CAAC,CAAC;QACjE,GAAIC,gBACA;YACEc,WAAW;gBACT,SAAS;gBACTD,MAAMb;gBACN,GAAII,UAAU;oBAAEW,MAAM;wBAAE,SAAS;wBAAerB,KAAKW,IAAID;oBAAS;gBAAE,IAAI,CAAC,CAAC;YAC5E;QACF,IACA,CAAC,CAAC;IACR;AACF"}
|
||||
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
import type { BasePayload } from 'payload';
|
||||
import type { I18nConfig } from '../i18n/index.js';
|
||||
type BuildLlmsTxtArgs = {
|
||||
/** Absolute site URL (https://…). */
|
||||
baseUrl: string;
|
||||
config: I18nConfig;
|
||||
/** Locale for names/descriptions. Defaults to config.defaultLocale. */
|
||||
locale?: string;
|
||||
/** Pages collection slug. Defaults to 'pages'. */
|
||||
pagesSlug?: string;
|
||||
payload: BasePayload;
|
||||
/** Site settings global slug. Defaults to 'site-settings'. */
|
||||
settingsSlug?: string;
|
||||
};
|
||||
/**
|
||||
* Builds the body of /llms.txt — a Markdown file describing the site for AI
|
||||
* agents / LLM crawlers, per the llmstxt.org convention. Mirrors buildRobots /
|
||||
* buildSitemapEntries: the plugin already knows the site's name, description,
|
||||
* and pages, so it can generate this automatically.
|
||||
*
|
||||
* Structure (llmstxt.org): H1 site name, a blockquote/summary, then a list of
|
||||
* key pages as Markdown links with short descriptions. Agents read this to
|
||||
* understand the site quickly without crawling everything.
|
||||
*
|
||||
* Wire it as a route that returns text/plain:
|
||||
*
|
||||
* // app/llms.txt/route.ts
|
||||
* import { buildLlmsTxt } from '@intecion/ipal-kit'
|
||||
* import { getCachedPayload } from '@/lib/content'
|
||||
* import { i18nConfig } from '@/i18n.config'
|
||||
* export const dynamic = 'force-dynamic'
|
||||
* export async function GET() {
|
||||
* const body = await buildLlmsTxt({
|
||||
* payload: await getCachedPayload(),
|
||||
* config: i18nConfig,
|
||||
* baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!,
|
||||
* })
|
||||
* return new Response(body, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } })
|
||||
* }
|
||||
*
|
||||
* Data comes from the panel (siteName, siteDescription, pages) — nothing
|
||||
* hardcoded. Skips drafts and noindex pages (same as the sitemap).
|
||||
*/
|
||||
export declare function buildLlmsTxt({ baseUrl, config, locale, pagesSlug, payload, settingsSlug, }: BuildLlmsTxtArgs): Promise<string>;
|
||||
export {};
|
||||
Vendored
+90
@@ -0,0 +1,90 @@
|
||||
import { buildLocalizedPath } from '../i18n/index.js';
|
||||
/**
|
||||
* Builds the body of /llms.txt — a Markdown file describing the site for AI
|
||||
* agents / LLM crawlers, per the llmstxt.org convention. Mirrors buildRobots /
|
||||
* buildSitemapEntries: the plugin already knows the site's name, description,
|
||||
* and pages, so it can generate this automatically.
|
||||
*
|
||||
* Structure (llmstxt.org): H1 site name, a blockquote/summary, then a list of
|
||||
* key pages as Markdown links with short descriptions. Agents read this to
|
||||
* understand the site quickly without crawling everything.
|
||||
*
|
||||
* Wire it as a route that returns text/plain:
|
||||
*
|
||||
* // app/llms.txt/route.ts
|
||||
* import { buildLlmsTxt } from '@intecion/ipal-kit'
|
||||
* import { getCachedPayload } from '@/lib/content'
|
||||
* import { i18nConfig } from '@/i18n.config'
|
||||
* export const dynamic = 'force-dynamic'
|
||||
* export async function GET() {
|
||||
* const body = await buildLlmsTxt({
|
||||
* payload: await getCachedPayload(),
|
||||
* config: i18nConfig,
|
||||
* baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!,
|
||||
* })
|
||||
* return new Response(body, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } })
|
||||
* }
|
||||
*
|
||||
* Data comes from the panel (siteName, siteDescription, pages) — nothing
|
||||
* hardcoded. Skips drafts and noindex pages (same as the sitemap).
|
||||
*/ export async function buildLlmsTxt({ baseUrl, config, locale, pagesSlug = 'pages', payload, settingsSlug = 'site-settings' }) {
|
||||
const loc = locale ?? config.defaultLocale;
|
||||
const origin = baseUrl.replace(/\/$/, '');
|
||||
const settings = await payload.findGlobal({
|
||||
slug: settingsSlug,
|
||||
depth: 0,
|
||||
locale: loc
|
||||
});
|
||||
const name = settings.siteName?.trim() || 'Website';
|
||||
const description = settings.siteDescription?.trim();
|
||||
// NO where:{_status} filter — collections without drafts enabled don't
|
||||
// register the _status field, and querying it throws
|
||||
// "path cannot be queried: _status" (a real bug report). Draft filtering
|
||||
// happens in memory below, which is safe for every collection. Same as
|
||||
// buildSitemapEntries and generateStaticParams.
|
||||
const result = await payload.find({
|
||||
collection: pagesSlug,
|
||||
depth: 0,
|
||||
limit: 1000,
|
||||
locale: loc
|
||||
});
|
||||
const lines = [
|
||||
`# ${name}`,
|
||||
''
|
||||
];
|
||||
if (description) {
|
||||
lines.push(`> ${description}`, '');
|
||||
}
|
||||
const pageLinks = [];
|
||||
for (const raw of result.docs){
|
||||
if (raw._status && raw._status !== 'published') {
|
||||
continue;
|
||||
}
|
||||
if (raw.meta?.noindex) {
|
||||
continue;
|
||||
}
|
||||
const title = typeof raw.title === 'string' ? raw.title : undefined;
|
||||
const slug = typeof raw.slug === 'string' ? raw.slug : undefined;
|
||||
if (!title || !slug) {
|
||||
continue;
|
||||
}
|
||||
const path = buildLocalizedPath({
|
||||
config,
|
||||
locale: loc,
|
||||
slugs: {
|
||||
[loc]: slug
|
||||
}
|
||||
});
|
||||
if (!path) {
|
||||
continue;
|
||||
}
|
||||
const pageDesc = typeof raw.meta?.description === 'string' ? ` — ${raw.meta.description}` : '';
|
||||
pageLinks.push(`- [${title}](${origin}${path})${pageDesc}`);
|
||||
}
|
||||
if (pageLinks.length > 0) {
|
||||
lines.push('## Strony', '', ...pageLinks, '');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
//# sourceMappingURL=buildLlmsTxt.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+16
-2
@@ -19,6 +19,7 @@ export type PageMetadata = {
|
||||
locale?: string;
|
||||
title: string;
|
||||
};
|
||||
/** robots directives — set to noindex/follow for legal/thin/search pages. */
|
||||
robots?: {
|
||||
follow: boolean;
|
||||
index: boolean;
|
||||
@@ -30,7 +31,7 @@ type BuildMetadataArgs = {
|
||||
baseUrl?: string;
|
||||
config: I18nConfig;
|
||||
/** Home slug that collapses to the locale root. Defaults to 'home'. */
|
||||
homeSlug?: string;
|
||||
homeSlug?: Record<string, string> | string;
|
||||
/** Resolved OG image URL (page image or site defaultShareImage). */
|
||||
imageUrl?: null | string;
|
||||
/** Current locale being rendered. */
|
||||
@@ -39,6 +40,13 @@ type BuildMetadataArgs = {
|
||||
meta?: null | SeoMeta;
|
||||
/** Page title or site name first. Defaults to 'page-first'. */
|
||||
order?: TitleOrder;
|
||||
/**
|
||||
* The document's own title (e.g. page.title = 'Sprzątanie biur'). Used as the
|
||||
* page-title source when meta.title is empty — the browser tab and search
|
||||
* result should show the page name, not go blank, when an editor didn't fill
|
||||
* the SEO title. Priority: titleOverride > meta.title > pageTitle.
|
||||
*/
|
||||
pageTitle?: null | string;
|
||||
/**
|
||||
* Localized segment the document lives under (an archive page's slugs).
|
||||
* Feeds both canonical and hreflang, so /pl/artykuly/moj-post and
|
||||
@@ -55,6 +63,12 @@ type BuildMetadataArgs = {
|
||||
query?: string;
|
||||
/** Separator between page title and site name. Defaults to ' | '. */
|
||||
separator?: string;
|
||||
/**
|
||||
* Global fallback description (from SiteSettings.siteDescription). Used when a
|
||||
* page has no meta.description — better than emitting no description at all
|
||||
* (Google would then scrape random on-page text for the snippet).
|
||||
*/
|
||||
siteDescription?: null | string;
|
||||
/** Site name for title composition and OG. */
|
||||
siteName?: null | string;
|
||||
/** slug per locale for this document — drives canonical + hreflang. */
|
||||
@@ -69,5 +83,5 @@ type BuildMetadataArgs = {
|
||||
* pieces (meta group, site name, image URL, localized slugs) and passes them
|
||||
* in — the plugin composes, it doesn't fetch.
|
||||
*/
|
||||
export declare function buildMetadata({ baseUrl, config, homeSlug, imageUrl, locale, meta, order, prefix, query, separator, siteName, slugs, }: BuildMetadataArgs): PageMetadata;
|
||||
export declare function buildMetadata({ baseUrl, config, homeSlug, imageUrl, locale, meta, order, pageTitle, prefix, query, separator, siteDescription, siteName, slugs, }: BuildMetadataArgs): PageMetadata;
|
||||
export {};
|
||||
|
||||
Vendored
+16
-11
@@ -9,17 +9,20 @@ import { buildHreflangAlternates } from './hreflang.js';
|
||||
* Designed for use inside Next.js `generateMetadata`. The caller resolves the
|
||||
* pieces (meta group, site name, image URL, localized slugs) and passes them
|
||||
* in — the plugin composes, it doesn't fetch.
|
||||
*/ export function buildMetadata({ baseUrl, config, homeSlug = 'home', imageUrl, locale, meta, order, prefix, query, separator, siteName, slugs }) {
|
||||
// titleOverride wins outright: an editor who filled it in wants that exact
|
||||
// string in the tab, not a composition.
|
||||
*/ export function buildMetadata({ baseUrl, config, homeSlug = 'home', imageUrl, locale, meta, order, pageTitle, prefix, query, separator, siteDescription, siteName, slugs }) {
|
||||
// Title source priority: titleOverride (exact, wins outright) > meta.title
|
||||
// (SEO title an editor set) > pageTitle (the document's own name). This means
|
||||
// a page with no SEO title still shows its name (e.g. 'Sprzątanie biur')
|
||||
// composed with the site name, instead of just the site name or a blank.
|
||||
const override = meta?.titleOverride?.trim();
|
||||
const resolvedPageTitle = meta?.title?.trim() || pageTitle?.trim() || undefined;
|
||||
const title = override || composeTitle({
|
||||
order,
|
||||
pageTitle: meta?.title,
|
||||
pageTitle: resolvedPageTitle,
|
||||
separator,
|
||||
siteName
|
||||
});
|
||||
const description = meta?.description?.trim() || undefined;
|
||||
const description = meta?.description?.trim() || siteDescription?.trim() || undefined;
|
||||
const origin = baseUrl?.replace(/\/$/, '') ?? '';
|
||||
const suffix = query ?? '';
|
||||
const currentPath = buildLocalizedPath({
|
||||
@@ -60,12 +63,6 @@ import { buildHreflangAlternates } from './hreflang.js';
|
||||
languages
|
||||
}
|
||||
},
|
||||
...meta?.noindex ? {
|
||||
robots: {
|
||||
follow: true,
|
||||
index: false
|
||||
}
|
||||
} : {},
|
||||
openGraph: {
|
||||
title,
|
||||
...description && {
|
||||
@@ -75,7 +72,15 @@ import { buildHreflangAlternates } from './hreflang.js';
|
||||
images
|
||||
},
|
||||
locale
|
||||
},
|
||||
// noindex → tell search engines to exclude the page but still follow links
|
||||
// (authority flows through). For legal/thin/search-result pages.
|
||||
...meta?.noindex ? {
|
||||
robots: {
|
||||
follow: true,
|
||||
index: false
|
||||
}
|
||||
} : {}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -29,7 +29,7 @@ type BuildSitemapArgs = {
|
||||
* Slug of the page that is the site root (collapses to /{locale}).
|
||||
* Read from System Pages when omitted.
|
||||
*/
|
||||
homeSlug?: string;
|
||||
homeSlug?: Record<string, string> | string;
|
||||
/** Pages collection slug. Defaults to 'pages'. */
|
||||
pagesSlug?: string;
|
||||
payload: BasePayload;
|
||||
|
||||
+44
-5
@@ -1,13 +1,40 @@
|
||||
import { archiveFieldName } from '../content/index.js';
|
||||
import { buildLocalizedPath, getLocalizedSlugs } from '../i18n/index.js';
|
||||
import { buildLocalizedPath, getLocaleCodes, getLocalizedSlugs } from '../i18n/index.js';
|
||||
import { buildHreflangAlternates } from './hreflang.js';
|
||||
/** Skip drafts and anything flagged noindex in the SEO tab. */ function isIndexable(doc) {
|
||||
/**
|
||||
* Slugs that must never appear in the sitemap — error/system pages that exist as
|
||||
* documents (e.g. a '404' page in the Pages collection) but should not be
|
||||
* indexed. A sitemap should list only real, HTTP-200 content; a '/pl/404' entry
|
||||
* is an audit finding. Matched against the slug in any locale.
|
||||
*/ const EXCLUDED_SITEMAP_SLUGS = new Set([
|
||||
'404',
|
||||
'500',
|
||||
'error',
|
||||
'not-found'
|
||||
]);
|
||||
/** True if the doc's slug (in any locale) is an excluded system/error slug. */ function hasExcludedSlug(slug) {
|
||||
if (typeof slug === 'string') {
|
||||
return EXCLUDED_SITEMAP_SLUGS.has(slug);
|
||||
}
|
||||
if (slug && typeof slug === 'object') {
|
||||
for (const value of Object.values(slug)){
|
||||
if (typeof value === 'string' && EXCLUDED_SITEMAP_SLUGS.has(value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/** Skip drafts, noindex, and system/error pages (404 etc.). */ function isIndexable(doc) {
|
||||
if (doc._status && doc._status !== 'published') {
|
||||
return false;
|
||||
}
|
||||
if (doc.meta?.noindex) {
|
||||
return false;
|
||||
}
|
||||
if (hasExcludedSlug(doc.slug)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
@@ -83,7 +110,13 @@ import { buildHreflangAlternates } from './hreflang.js';
|
||||
depth: 1,
|
||||
locale: 'all'
|
||||
});
|
||||
const resolvedHomeSlug = homeSlug ?? extractSlugInLocale(settings.homepage, defaultLocale) ?? 'home';
|
||||
// Home slug as a per-locale MAP so every language's homepage collapses to its
|
||||
// root in the sitemap (/pl, /de, /en) — not just the default locale. Priority:
|
||||
// caller-provided homeSlug > full map from the homepage relationship > single
|
||||
// default-locale slug > 'home'. slugMapAllLocales already builds the map (it's
|
||||
// used for archive prefixes below); reusing it here fixes /de/startseite
|
||||
// appearing in the sitemap instead of /de.
|
||||
const resolvedHomeSlug = homeSlug ?? slugMapAllLocales(settings.homepage, locales) ?? extractSlugInLocale(settings.homepage, defaultLocale) ?? 'home';
|
||||
// Which collections to walk: pages (no prefix) + each content collection with
|
||||
// its archive-page slugs as the localized prefix.
|
||||
const collections = [
|
||||
@@ -114,13 +147,19 @@ import { buildHreflangAlternates } from './hreflang.js';
|
||||
if (!isIndexable(raw)) {
|
||||
continue;
|
||||
}
|
||||
// Emit the entry under the default locale's URL; alternates cover the rest.
|
||||
const entry = entryFor(raw, defaultLocale, config, baseUrl, resolvedHomeSlug, collection.prefixSlugs, changeFrequency);
|
||||
// Emit a separate <url> for EACH locale. Google's sitemap spec for
|
||||
// localized sites requires one <url> per language version (each with its
|
||||
// own <loc> plus xhtml:link alternates), not a single default-locale <url>
|
||||
// with the others hidden only in alternates. Emitting only the default
|
||||
// locale makes GSC count 8 URLs instead of 8×3 — a real reporting gap.
|
||||
for (const locale of getLocaleCodes(config)){
|
||||
const entry = entryFor(raw, locale, config, baseUrl, resolvedHomeSlug, collection.prefixSlugs, changeFrequency);
|
||||
if (entry) {
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
/** Pulls a slug string from a populated relationship in a specific locale. */ function extractSlugInLocale(rel, locale) {
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+45
@@ -0,0 +1,45 @@
|
||||
import type { SitemapEntry } from './buildSitemapEntries.js';
|
||||
type BuildSitemapXmlOptions = {
|
||||
/**
|
||||
* URL of a CSS stylesheet to make the sitemap readable in the browser, e.g.
|
||||
* '/sitemap.css'. Uses `type="text/css"` — the W3C "Associating Style Sheets
|
||||
* with XML" mechanism, which is NOT deprecated (unlike XSLT / type="text/xsl",
|
||||
* which Chrome/WebKit are removing). CSS on XML shows no warning, styles the
|
||||
* raw tags directly (e.g. `url { display: block }` turns the wall of text into
|
||||
* cards), and crawlers ignore the directive entirely.
|
||||
*/
|
||||
cssUrl?: string;
|
||||
};
|
||||
/**
|
||||
* Serializes sitemap entries to a clean, indented XML STRING — valid for crawlers
|
||||
* and (with `cssUrl`) styled in the browser via plain CSS.
|
||||
*
|
||||
* Two viewing modes:
|
||||
* - No cssUrl → the browser's native formatted XML tree (indented, collapsible).
|
||||
* - With cssUrl → a `<?xml-stylesheet type="text/css">` directive; the project's
|
||||
* CSS styles the XML tags (cards, labels via ::before). NOT XSLT — that's being
|
||||
* removed from browsers and shows a deprecation warning. CSS is safe and
|
||||
* W3C-standard.
|
||||
*
|
||||
* // app/sitemap.xml/route.ts
|
||||
* import { buildSitemapXml } from '@intecion/ipal-kit'
|
||||
* import { sitemap } from '@/lib/content'
|
||||
* export const dynamic = 'force-dynamic'
|
||||
* export async function GET() {
|
||||
* const entries = await sitemap()
|
||||
* const xml = buildSitemapXml(entries, { cssUrl: '/sitemap.css' })
|
||||
* return new Response(xml, {
|
||||
* headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
||||
* })
|
||||
* }
|
||||
*
|
||||
* Put sitemap.css in the project's /public and style the tags (see docs/seo.md
|
||||
* for a starter). Note: <loc> is an XML tag, not <a href> — CSS can't make it a
|
||||
* clickable link (some browsers auto-detect URLs); the win is readability, not
|
||||
* clickability.
|
||||
*
|
||||
* NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one.
|
||||
* Two sitemaps confuse crawlers.
|
||||
*/
|
||||
export declare function buildSitemapXml(entries: SitemapEntry[], opts?: BuildSitemapXmlOptions): string;
|
||||
export {};
|
||||
Vendored
+58
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Serializes sitemap entries to a clean, indented XML STRING — valid for crawlers
|
||||
* and (with `cssUrl`) styled in the browser via plain CSS.
|
||||
*
|
||||
* Two viewing modes:
|
||||
* - No cssUrl → the browser's native formatted XML tree (indented, collapsible).
|
||||
* - With cssUrl → a `<?xml-stylesheet type="text/css">` directive; the project's
|
||||
* CSS styles the XML tags (cards, labels via ::before). NOT XSLT — that's being
|
||||
* removed from browsers and shows a deprecation warning. CSS is safe and
|
||||
* W3C-standard.
|
||||
*
|
||||
* // app/sitemap.xml/route.ts
|
||||
* import { buildSitemapXml } from '@intecion/ipal-kit'
|
||||
* import { sitemap } from '@/lib/content'
|
||||
* export const dynamic = 'force-dynamic'
|
||||
* export async function GET() {
|
||||
* const entries = await sitemap()
|
||||
* const xml = buildSitemapXml(entries, { cssUrl: '/sitemap.css' })
|
||||
* return new Response(xml, {
|
||||
* headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
||||
* })
|
||||
* }
|
||||
*
|
||||
* Put sitemap.css in the project's /public and style the tags (see docs/seo.md
|
||||
* for a starter). Note: <loc> is an XML tag, not <a href> — CSS can't make it a
|
||||
* clickable link (some browsers auto-detect URLs); the win is readability, not
|
||||
* clickability.
|
||||
*
|
||||
* NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one.
|
||||
* Two sitemaps confuse crawlers.
|
||||
*/ export function buildSitemapXml(entries, opts = {}) {
|
||||
const { cssUrl } = opts;
|
||||
const esc = (s)=>s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
const urls = entries.map((e)=>{
|
||||
const parts = [
|
||||
` <loc>${esc(e.url)}</loc>`
|
||||
];
|
||||
if (e.lastModified) {
|
||||
const iso = e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified);
|
||||
parts.push(` <lastmod>${esc(iso)}</lastmod>`);
|
||||
}
|
||||
if (e.changeFrequency) parts.push(` <changefreq>${e.changeFrequency}</changefreq>`);
|
||||
if (typeof e.priority === 'number') parts.push(` <priority>${e.priority}</priority>`);
|
||||
const alternates = e.alternates?.languages;
|
||||
if (alternates) {
|
||||
for (const [lang, href] of Object.entries(alternates)){
|
||||
if (typeof href === 'string') {
|
||||
parts.push(` <xhtml:link rel="alternate" hreflang="${esc(lang)}" href="${esc(href)}"/>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ` <url>\n${parts.join('\n')}\n </url>`;
|
||||
}).join('\n');
|
||||
const stylesheet = cssUrl ? `<?xml-stylesheet type="text/css" href="${esc(cssUrl)}"?>\n` : '';
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>\n` + stylesheet + `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ` + `xmlns:xhtml="http://www.w3.org/1999/xhtml">\n` + urls + `\n</urlset>`;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=buildSitemapXml.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
Vendored
+6
-6
@@ -1,14 +1,14 @@
|
||||
/** Which part comes first in a composed title. */
|
||||
export type TitleOrder = 'page-first' | 'site-first';
|
||||
type ComposeTitleArgs = {
|
||||
/** Defaults to 'page-first' — the page title is what a visitor scans for. */
|
||||
order?: TitleOrder;
|
||||
/** Page-specific title, e.g. 'About Us'. */
|
||||
pageTitle?: null | string;
|
||||
pageTitle?: string | null;
|
||||
/** Site name, e.g. 'Acme Inc'. */
|
||||
siteName?: string | null;
|
||||
/** Separator between page title and site name. Defaults to ' | '. */
|
||||
separator?: string;
|
||||
/** Site name, e.g. 'Acme Inc'. */
|
||||
siteName?: null | string;
|
||||
/** Defaults to 'page-first' — the page title is what a visitor scans for. */
|
||||
order?: TitleOrder;
|
||||
};
|
||||
/**
|
||||
* Composes a full document title from a page title and the site name.
|
||||
@@ -21,5 +21,5 @@ type ComposeTitleArgs = {
|
||||
*
|
||||
* Pure function — no dependency on Payload or request state.
|
||||
*/
|
||||
export declare function composeTitle({ order, pageTitle, separator, siteName, }: ComposeTitleArgs): string;
|
||||
export declare function composeTitle({ pageTitle, siteName, separator, order, }: ComposeTitleArgs): string;
|
||||
export {};
|
||||
|
||||
Vendored
+9
-1
@@ -8,10 +8,18 @@
|
||||
* - neither: ""
|
||||
*
|
||||
* Pure function — no dependency on Payload or request state.
|
||||
*/ export function composeTitle({ order = 'page-first', pageTitle, separator = ' | ', siteName }) {
|
||||
*/ export function composeTitle({ pageTitle, siteName, separator = ' | ', order = 'page-first' }) {
|
||||
const page = pageTitle?.trim();
|
||||
const site = siteName?.trim();
|
||||
if (page && site) {
|
||||
// Dedup: if the page title already ends with the site name (an editor wrote
|
||||
// "Budowa auta – RCC" into meta.title), don't append it again → avoid
|
||||
// "Budowa auta – RCC – RCC". Match the site name at the end, with or without
|
||||
// the separator before it.
|
||||
const sep = separator.trim();
|
||||
if (page === site || page.endsWith(`${sep} ${site}`) || page.endsWith(` ${site}`)) {
|
||||
return page;
|
||||
}
|
||||
return order === 'site-first' ? `${site}${separator}${page}` : `${page}${separator}${site}`;
|
||||
}
|
||||
return page || site || '';
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/seo/composeTitle.ts"],"sourcesContent":["/** Which part comes first in a composed title. */\nexport type TitleOrder = 'page-first' | 'site-first'\n\ntype ComposeTitleArgs = {\n /** Defaults to 'page-first' — the page title is what a visitor scans for. */\n order?: TitleOrder\n /** Page-specific title, e.g. 'About Us'. */\n pageTitle?: null | string\n /** Separator between page title and site name. Defaults to ' | '. */\n separator?: string\n /** Site name, e.g. 'Acme Inc'. */\n siteName?: null | string\n}\n\n/**\n * Composes a full document title from a page title and the site name.\n *\n * - both, page-first: \"About Us | Acme Inc\"\n * - both, site-first: \"Acme Inc | About Us\"\n * - page only: \"About Us\"\n * - site only: \"Acme Inc\"\n * - neither: \"\"\n *\n * Pure function — no dependency on Payload or request state.\n */\nexport function composeTitle({\n order = 'page-first',\n pageTitle,\n separator = ' | ',\n siteName,\n}: ComposeTitleArgs): string {\n const page = pageTitle?.trim()\n const site = siteName?.trim()\n\n if (page && site) {\n return order === 'site-first' ? `${site}${separator}${page}` : `${page}${separator}${site}`\n }\n return page || site || ''\n}\n"],"names":["composeTitle","order","pageTitle","separator","siteName","page","trim","site"],"mappings":"AAAA,gDAAgD,GAchD;;;;;;;;;;CAUC,GACD,OAAO,SAASA,aAAa,EAC3BC,QAAQ,YAAY,EACpBC,SAAS,EACTC,YAAY,KAAK,EACjBC,QAAQ,EACS;IACjB,MAAMC,OAAOH,WAAWI;IACxB,MAAMC,OAAOH,UAAUE;IAEvB,IAAID,QAAQE,MAAM;QAChB,OAAON,UAAU,eAAe,GAAGM,OAAOJ,YAAYE,MAAM,GAAG,GAAGA,OAAOF,YAAYI,MAAM;IAC7F;IACA,OAAOF,QAAQE,QAAQ;AACzB"}
|
||||
{"version":3,"sources":["../../../src/modules/seo/composeTitle.ts"],"sourcesContent":["/** Which part comes first in a composed title. */\nexport type TitleOrder = 'page-first' | 'site-first'\n\ntype ComposeTitleArgs = {\n /** Page-specific title, e.g. 'About Us'. */\n pageTitle?: string | null\n /** Site name, e.g. 'Acme Inc'. */\n siteName?: string | null\n /** Separator between page title and site name. Defaults to ' | '. */\n separator?: string\n /** Defaults to 'page-first' — the page title is what a visitor scans for. */\n order?: TitleOrder\n}\n\n/**\n * Composes a full document title from a page title and the site name.\n *\n * - both, page-first: \"About Us | Acme Inc\"\n * - both, site-first: \"Acme Inc | About Us\"\n * - page only: \"About Us\"\n * - site only: \"Acme Inc\"\n * - neither: \"\"\n *\n * Pure function — no dependency on Payload or request state.\n */\nexport function composeTitle({\n pageTitle,\n siteName,\n separator = ' | ',\n order = 'page-first',\n}: ComposeTitleArgs): string {\n const page = pageTitle?.trim()\n const site = siteName?.trim()\n\n if (page && site) {\n // Dedup: if the page title already ends with the site name (an editor wrote\n // \"Budowa auta – RCC\" into meta.title), don't append it again → avoid\n // \"Budowa auta – RCC – RCC\". Match the site name at the end, with or without\n // the separator before it.\n const sep = separator.trim()\n if (page === site || page.endsWith(`${sep} ${site}`) || page.endsWith(` ${site}`)) {\n return page\n }\n return order === 'site-first' ? `${site}${separator}${page}` : `${page}${separator}${site}`\n }\n return page || site || ''\n}\n"],"names":["composeTitle","pageTitle","siteName","separator","order","page","trim","site","sep","endsWith"],"mappings":"AAAA,gDAAgD,GAchD;;;;;;;;;;CAUC,GACD,OAAO,SAASA,aAAa,EAC3BC,SAAS,EACTC,QAAQ,EACRC,YAAY,KAAK,EACjBC,QAAQ,YAAY,EACH;IACjB,MAAMC,OAAOJ,WAAWK;IACxB,MAAMC,OAAOL,UAAUI;IAEvB,IAAID,QAAQE,MAAM;QAChB,4EAA4E;QAC5E,sEAAsE;QACtE,6EAA6E;QAC7E,2BAA2B;QAC3B,MAAMC,MAAML,UAAUG,IAAI;QAC1B,IAAID,SAASE,QAAQF,KAAKI,QAAQ,CAAC,GAAGD,IAAI,CAAC,EAAED,MAAM,KAAKF,KAAKI,QAAQ,CAAC,CAAC,CAAC,EAAEF,MAAM,GAAG;YACjF,OAAOF;QACT;QACA,OAAOD,UAAU,eAAe,GAAGG,OAAOJ,YAAYE,MAAM,GAAG,GAAGA,OAAOF,YAAYI,MAAM;IAC7F;IACA,OAAOF,QAAQE,QAAQ;AACzB"}
|
||||
+1
-1
@@ -14,7 +14,7 @@ type CreateMetadataGeneratorArgs = {
|
||||
baseUrl?: string;
|
||||
config: I18nConfig;
|
||||
/** Home slug that collapses to the locale root. Defaults to 'home'. */
|
||||
homeSlug?: string;
|
||||
homeSlug?: string | Record<string, string>;
|
||||
/**
|
||||
* Resolves the document to build metadata for, given route params.
|
||||
* The client supplies this (they own the collections and routing); it should
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/seo/createMetadataGenerator.ts"],"sourcesContent":["import type { BasePayload } from 'payload'\n\nimport type { I18nConfig, LocalizedSlugs } from '../i18n/index.js'\nimport type { PageMetadata } from './buildMetadata.js'\nimport type { SeoMeta } from './types.js'\n\nimport { getLocalizedSlugs } from '../i18n/index.js'\nimport { buildMetadata } from './buildMetadata.js'\n\n/**\n * A document as needed for metadata: its SEO meta group and localized slug.\n */\ntype MetadataDocument = {\n meta?: null | SeoMeta\n slug?: null | Record<string, unknown>\n}\n\ntype CreateMetadataGeneratorArgs = {\n /** Absolute site origin, e.g. 'https://example.com'. */\n baseUrl?: string\n config: I18nConfig\n /** Home slug that collapses to the locale root. Defaults to 'home'. */\n homeSlug?: string\n /**\n * Resolves the document to build metadata for, given route params.\n * The client supplies this (they own the collections and routing); it should\n * fetch with `locale: 'all'` so the slug field is a locale→value map.\n */\n resolveDocument: (args: {\n locale: string\n params: Record<string, string | string[]>\n payload: BasePayload\n }) => Promise<MetadataDocument | null>\n /** Resolves the OG image URL for the document, if any. */\n resolveImageUrl?: (args: {\n doc: MetadataDocument\n locale: string\n payload: BasePayload\n }) => Promise<null | string>\n /** Resolves the site name (e.g. from SiteSettings). */\n resolveSiteName?: (args: { locale: string; payload: BasePayload }) => Promise<null | string>\n}\n\n/**\n * Builds a metadata generator, collapsing the usual generateMetadata\n * boilerplate into a single wired-up function.\n *\n * The client provides resolvers (they own collections/routing); the plugin\n * owns the assembly (title composition, canonical, hreflang, OG).\n *\n * The returned function takes `{ payload, params, locale }` — Next calls\n * `generateMetadata({ params })` without those, and the plugin never calls\n * getPayload itself, so the client wraps it in their route file:\n *\n * ```ts\n * // app/(frontend)/[locale]/[[...slug]]/page.tsx\n * const generate = createMetadataGenerator({\n * config: i18nConfig,\n * baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,\n * resolveDocument: async ({ payload, params, locale }) => { ... },\n * })\n *\n * export async function generateMetadata({ params }) {\n * const { locale, slug } = await params\n * const payload = await getPayload({ config: await config })\n * return generate({ payload, params: { slug: slug ?? [] }, locale })\n * }\n * ```\n *\n * `resolveDocument` should fetch with `locale: 'all'` so the slug field comes\n * back as a locale→value map — that's what hreflang alternates are built from.\n */\nexport function createMetadataGenerator(args: CreateMetadataGeneratorArgs) {\n const { baseUrl, config, homeSlug, resolveDocument, resolveImageUrl, resolveSiteName } = args\n\n return async function generateMetadata(context: {\n locale: string\n params: Record<string, string | string[]>\n payload: BasePayload\n }): Promise<PageMetadata> {\n const { locale, params, payload } = context\n\n const doc = await resolveDocument({ locale, params, payload })\n\n const slugs: LocalizedSlugs = doc?.slug\n ? getLocalizedSlugs({ config, slugField: doc.slug })\n : {}\n\n const [siteName, imageUrl] = await Promise.all([\n resolveSiteName?.({ locale, payload }) ?? Promise.resolve(null),\n doc && resolveImageUrl ? resolveImageUrl({ doc, locale, payload }) : Promise.resolve(null),\n ])\n\n return buildMetadata({\n baseUrl,\n config,\n homeSlug,\n imageUrl,\n locale,\n meta: doc?.meta,\n siteName,\n slugs,\n })\n }\n}\n"],"names":["getLocalizedSlugs","buildMetadata","createMetadataGenerator","args","baseUrl","config","homeSlug","resolveDocument","resolveImageUrl","resolveSiteName","generateMetadata","context","locale","params","payload","doc","slugs","slug","slugField","siteName","imageUrl","Promise","all","resolve","meta"],"mappings":"AAMA,SAASA,iBAAiB,QAAQ,mBAAkB;AACpD,SAASC,aAAa,QAAQ,qBAAoB;AAoClD;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,SAASC,wBAAwBC,IAAiC;IACvE,MAAM,EAAEC,OAAO,EAAEC,MAAM,EAAEC,QAAQ,EAAEC,eAAe,EAAEC,eAAe,EAAEC,eAAe,EAAE,GAAGN;IAEzF,OAAO,eAAeO,iBAAiBC,OAItC;QACC,MAAM,EAAEC,MAAM,EAAEC,MAAM,EAAEC,OAAO,EAAE,GAAGH;QAEpC,MAAMI,MAAM,MAAMR,gBAAgB;YAAEK;YAAQC;YAAQC;QAAQ;QAE5D,MAAME,QAAwBD,KAAKE,OAC/BjB,kBAAkB;YAAEK;YAAQa,WAAWH,IAAIE,IAAI;QAAC,KAChD,CAAC;QAEL,MAAM,CAACE,UAAUC,SAAS,GAAG,MAAMC,QAAQC,GAAG,CAAC;YAC7Cb,kBAAkB;gBAAEG;gBAAQE;YAAQ,MAAMO,QAAQE,OAAO,CAAC;YAC1DR,OAAOP,kBAAkBA,gBAAgB;gBAAEO;gBAAKH;gBAAQE;YAAQ,KAAKO,QAAQE,OAAO,CAAC;SACtF;QAED,OAAOtB,cAAc;YACnBG;YACAC;YACAC;YACAc;YACAR;YACAY,MAAMT,KAAKS;YACXL;YACAH;QACF;IACF;AACF"}
|
||||
{"version":3,"sources":["../../../src/modules/seo/createMetadataGenerator.ts"],"sourcesContent":["import type { BasePayload } from 'payload'\n\nimport type { I18nConfig, LocalizedSlugs } from '../i18n/index.js'\nimport type { PageMetadata } from './buildMetadata.js'\nimport type { SeoMeta } from './types.js'\n\nimport { getLocalizedSlugs } from '../i18n/index.js'\nimport { buildMetadata } from './buildMetadata.js'\n\n/**\n * A document as needed for metadata: its SEO meta group and localized slug.\n */\ntype MetadataDocument = {\n meta?: null | SeoMeta\n slug?: null | Record<string, unknown>\n}\n\ntype CreateMetadataGeneratorArgs = {\n /** Absolute site origin, e.g. 'https://example.com'. */\n baseUrl?: string\n config: I18nConfig\n /** Home slug that collapses to the locale root. Defaults to 'home'. */\n homeSlug?: string | Record<string, string>\n /**\n * Resolves the document to build metadata for, given route params.\n * The client supplies this (they own the collections and routing); it should\n * fetch with `locale: 'all'` so the slug field is a locale→value map.\n */\n resolveDocument: (args: {\n locale: string\n params: Record<string, string | string[]>\n payload: BasePayload\n }) => Promise<MetadataDocument | null>\n /** Resolves the OG image URL for the document, if any. */\n resolveImageUrl?: (args: {\n doc: MetadataDocument\n locale: string\n payload: BasePayload\n }) => Promise<null | string>\n /** Resolves the site name (e.g. from SiteSettings). */\n resolveSiteName?: (args: { locale: string; payload: BasePayload }) => Promise<null | string>\n}\n\n/**\n * Builds a metadata generator, collapsing the usual generateMetadata\n * boilerplate into a single wired-up function.\n *\n * The client provides resolvers (they own collections/routing); the plugin\n * owns the assembly (title composition, canonical, hreflang, OG).\n *\n * The returned function takes `{ payload, params, locale }` — Next calls\n * `generateMetadata({ params })` without those, and the plugin never calls\n * getPayload itself, so the client wraps it in their route file:\n *\n * ```ts\n * // app/(frontend)/[locale]/[[...slug]]/page.tsx\n * const generate = createMetadataGenerator({\n * config: i18nConfig,\n * baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,\n * resolveDocument: async ({ payload, params, locale }) => { ... },\n * })\n *\n * export async function generateMetadata({ params }) {\n * const { locale, slug } = await params\n * const payload = await getPayload({ config: await config })\n * return generate({ payload, params: { slug: slug ?? [] }, locale })\n * }\n * ```\n *\n * `resolveDocument` should fetch with `locale: 'all'` so the slug field comes\n * back as a locale→value map — that's what hreflang alternates are built from.\n */\nexport function createMetadataGenerator(args: CreateMetadataGeneratorArgs) {\n const { baseUrl, config, homeSlug, resolveDocument, resolveImageUrl, resolveSiteName } = args\n\n return async function generateMetadata(context: {\n locale: string\n params: Record<string, string | string[]>\n payload: BasePayload\n }): Promise<PageMetadata> {\n const { locale, params, payload } = context\n\n const doc = await resolveDocument({ locale, params, payload })\n\n const slugs: LocalizedSlugs = doc?.slug\n ? getLocalizedSlugs({ config, slugField: doc.slug })\n : {}\n\n const [siteName, imageUrl] = await Promise.all([\n resolveSiteName?.({ locale, payload }) ?? Promise.resolve(null),\n doc && resolveImageUrl ? resolveImageUrl({ doc, locale, payload }) : Promise.resolve(null),\n ])\n\n return buildMetadata({\n baseUrl,\n config,\n homeSlug,\n imageUrl,\n locale,\n meta: doc?.meta,\n siteName,\n slugs,\n })\n }\n}\n"],"names":["getLocalizedSlugs","buildMetadata","createMetadataGenerator","args","baseUrl","config","homeSlug","resolveDocument","resolveImageUrl","resolveSiteName","generateMetadata","context","locale","params","payload","doc","slugs","slug","slugField","siteName","imageUrl","Promise","all","resolve","meta"],"mappings":"AAMA,SAASA,iBAAiB,QAAQ,mBAAkB;AACpD,SAASC,aAAa,QAAQ,qBAAoB;AAoClD;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,SAASC,wBAAwBC,IAAiC;IACvE,MAAM,EAAEC,OAAO,EAAEC,MAAM,EAAEC,QAAQ,EAAEC,eAAe,EAAEC,eAAe,EAAEC,eAAe,EAAE,GAAGN;IAEzF,OAAO,eAAeO,iBAAiBC,OAItC;QACC,MAAM,EAAEC,MAAM,EAAEC,MAAM,EAAEC,OAAO,EAAE,GAAGH;QAEpC,MAAMI,MAAM,MAAMR,gBAAgB;YAAEK;YAAQC;YAAQC;QAAQ;QAE5D,MAAME,QAAwBD,KAAKE,OAC/BjB,kBAAkB;YAAEK;YAAQa,WAAWH,IAAIE,IAAI;QAAC,KAChD,CAAC;QAEL,MAAM,CAACE,UAAUC,SAAS,GAAG,MAAMC,QAAQC,GAAG,CAAC;YAC7Cb,kBAAkB;gBAAEG;gBAAQE;YAAQ,MAAMO,QAAQE,OAAO,CAAC;YAC1DR,OAAOP,kBAAkBA,gBAAgB;gBAAEO;gBAAKH;gBAAQE;YAAQ,KAAKO,QAAQE,OAAO,CAAC;SACtF;QAED,OAAOtB,cAAc;YACnBG;YACAC;YACAC;YACAc;YACAR;YACAY,MAAMT,KAAKS;YACXL;YACAH;QACF;IACF;AACF"}
|
||||
+1
@@ -90,6 +90,7 @@ import { slugsAcrossLocales } from './slugsAcrossLocales.js';
|
||||
...base,
|
||||
imageUrl: resolveOgImage(doc),
|
||||
meta: doc.meta,
|
||||
pageTitle: doc.title,
|
||||
prefix,
|
||||
query,
|
||||
slugs
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+7
-15
@@ -1,42 +1,34 @@
|
||||
import type { I18nConfig, LocalizedSlugs } from '../i18n/index.js';
|
||||
type BuildHreflangArgs = {
|
||||
/** slug per locale for the current document, e.g. { pl: 'o-nas', en: 'about' } */
|
||||
slugs: LocalizedSlugs;
|
||||
config: I18nConfig;
|
||||
/** Absolute site origin, e.g. 'https://example.com'. Omit for relative paths. */
|
||||
baseUrl?: string;
|
||||
config: I18nConfig;
|
||||
/** Home slug that collapses to the locale root. Defaults to 'home'. */
|
||||
homeSlug?: string;
|
||||
homeSlug?: string | Record<string, string>;
|
||||
/**
|
||||
* Localized segment the document lives under (an archive page's slugs),
|
||||
* e.g. { pl: 'artykuly', en: 'articles' }. Locales missing from the prefix
|
||||
* are omitted — an entry with no archive in that language has no URL there.
|
||||
*/
|
||||
prefix?: LocalizedSlugs;
|
||||
/** slug per locale for the current document, e.g. { pl: 'o-nas', en: 'about' } */
|
||||
slugs: LocalizedSlugs;
|
||||
};
|
||||
/**
|
||||
* Builds a map of locale → URL for hreflang alternate links, suitable for
|
||||
* Next.js Metadata `alternates.languages`.
|
||||
*
|
||||
* Bridges SEO and i18n: for each configured locale that the document has a
|
||||
* slug in, it produces the locale-aware path (via buildLocalizedPath),
|
||||
* optionally prefixed with an absolute origin.
|
||||
*
|
||||
* Also emits `x-default` pointing at the default locale — the version Google
|
||||
* serves when the user's language/region matches no hreflang, and the fallback
|
||||
* when the root ('/') redirect is ambiguous (Googlebot with no/foreign
|
||||
* Accept-Language).
|
||||
*
|
||||
* @example
|
||||
* buildHreflangAlternates({
|
||||
* slugs: { pl: 'o-nas', en: 'about' },
|
||||
* config,
|
||||
* baseUrl: 'https://example.com',
|
||||
* })
|
||||
* // → {
|
||||
* // pl: 'https://example.com/pl/o-nas',
|
||||
* // en: 'https://example.com/en/about',
|
||||
* // 'x-default': 'https://example.com/pl/o-nas',
|
||||
* // }
|
||||
* // → { pl: 'https://example.com/pl/o-nas', en: 'https://example.com/en/about' }
|
||||
*/
|
||||
export declare function buildHreflangAlternates({ baseUrl, config, homeSlug, prefix, slugs, }: BuildHreflangArgs): Record<string, string>;
|
||||
export declare function buildHreflangAlternates({ slugs, config, baseUrl, homeSlug, prefix, }: BuildHreflangArgs): Record<string, string>;
|
||||
export {};
|
||||
|
||||
Vendored
+15
-16
@@ -1,37 +1,36 @@
|
||||
import { buildLocalizedPath, getDefaultLocale, getLocaleCodes } from '../i18n/index.js';
|
||||
import { buildLocalizedPath, getLocaleCodes } from '../i18n/index.js';
|
||||
/**
|
||||
* Builds a map of locale → URL for hreflang alternate links, suitable for
|
||||
* Next.js Metadata `alternates.languages`.
|
||||
*
|
||||
* Bridges SEO and i18n: for each configured locale that the document has a
|
||||
* slug in, it produces the locale-aware path (via buildLocalizedPath),
|
||||
* optionally prefixed with an absolute origin.
|
||||
*
|
||||
* Also emits `x-default` pointing at the default locale — the version Google
|
||||
* serves when the user's language/region matches no hreflang, and the fallback
|
||||
* when the root ('/') redirect is ambiguous (Googlebot with no/foreign
|
||||
* Accept-Language).
|
||||
*
|
||||
* @example
|
||||
* buildHreflangAlternates({
|
||||
* slugs: { pl: 'o-nas', en: 'about' },
|
||||
* config,
|
||||
* baseUrl: 'https://example.com',
|
||||
* })
|
||||
* // → {
|
||||
* // pl: 'https://example.com/pl/o-nas',
|
||||
* // en: 'https://example.com/en/about',
|
||||
* // 'x-default': 'https://example.com/pl/o-nas',
|
||||
* // }
|
||||
*/ export function buildHreflangAlternates({ baseUrl, config, homeSlug = 'home', prefix, slugs }) {
|
||||
* // → { pl: 'https://example.com/pl/o-nas', en: 'https://example.com/en/about' }
|
||||
*/ export function buildHreflangAlternates({ slugs, config, baseUrl, homeSlug = 'home', prefix }) {
|
||||
const origin = baseUrl?.replace(/\/$/, '') ?? '';
|
||||
const alternates = {};
|
||||
// Single-locale sites have no language alternatives — hreflang describes
|
||||
// relationships BETWEEN language versions, and there's only one. Emitting
|
||||
// hreflang (or x-default) here would be wrong, so return empty: the page keeps
|
||||
// its canonical, but no alternate-language links.
|
||||
if (config.locales.length === 1) {
|
||||
return alternates;
|
||||
}
|
||||
for (const locale of getLocaleCodes(config)){
|
||||
const path = buildLocalizedPath({
|
||||
slugs,
|
||||
locale,
|
||||
config,
|
||||
homeSlug,
|
||||
locale,
|
||||
prefix,
|
||||
slugs
|
||||
prefix
|
||||
});
|
||||
if (path) {
|
||||
alternates[locale] = `${origin}${path}`;
|
||||
@@ -42,7 +41,7 @@ import { buildLocalizedPath, getDefaultLocale, getLocaleCodes } from '../i18n/in
|
||||
// redirect is ambiguous (Googlebot with no/foreign Accept-Language). Point it
|
||||
// at the default locale (the primary market) so search shows that version by
|
||||
// default instead of guessing. Only set when the default locale has a URL.
|
||||
const defaultLocalePath = alternates[getDefaultLocale(config)];
|
||||
const defaultLocalePath = alternates[config.defaultLocale];
|
||||
if (defaultLocalePath) {
|
||||
alternates['x-default'] = defaultLocalePath;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/seo/hreflang.ts"],"sourcesContent":["import type { I18nConfig, LocalizedSlugs } from '../i18n/index.js'\n\nimport { buildLocalizedPath, getDefaultLocale, getLocaleCodes } from '../i18n/index.js'\n\ntype BuildHreflangArgs = {\n /** Absolute site origin, e.g. 'https://example.com'. Omit for relative paths. */\n baseUrl?: string\n config: I18nConfig\n /** Home slug that collapses to the locale root. Defaults to 'home'. */\n homeSlug?: string\n /**\n * Localized segment the document lives under (an archive page's slugs),\n * e.g. { pl: 'artykuly', en: 'articles' }. Locales missing from the prefix\n * are omitted — an entry with no archive in that language has no URL there.\n */\n prefix?: LocalizedSlugs\n /** slug per locale for the current document, e.g. { pl: 'o-nas', en: 'about' } */\n slugs: LocalizedSlugs\n}\n\n/**\n * Next.js Metadata `alternates.languages`.\n *\n * Bridges SEO and i18n: for each configured locale that the document has a\n * slug in, it produces the locale-aware path (via buildLocalizedPath),\n * optionally prefixed with an absolute origin.\n *\n * Also emits `x-default` pointing at the default locale — the version Google\n * serves when the user's language/region matches no hreflang, and the fallback\n * when the root ('/') redirect is ambiguous (Googlebot with no/foreign\n * Accept-Language).\n *\n * @example\n * buildHreflangAlternates({\n * slugs: { pl: 'o-nas', en: 'about' },\n * config,\n * baseUrl: 'https://example.com',\n * })\n * // → {\n * // pl: 'https://example.com/pl/o-nas',\n * // en: 'https://example.com/en/about',\n * // 'x-default': 'https://example.com/pl/o-nas',\n * // }\n */\nexport function buildHreflangAlternates({\n baseUrl,\n config,\n homeSlug = 'home',\n prefix,\n slugs,\n}: BuildHreflangArgs): Record<string, string> {\n const origin = baseUrl?.replace(/\\/$/, '') ?? ''\n const alternates: Record<string, string> = {}\n\n for (const locale of getLocaleCodes(config)) {\n const path = buildLocalizedPath({ config, homeSlug, locale, prefix, slugs })\n if (path) {\n alternates[locale] = `${origin}${path}`\n }\n }\n\n // x-default: the version Google serves when the user's language/region doesn't\n // match any hreflang — and, crucially here, the fallback when the root ('/')\n // redirect is ambiguous (Googlebot with no/foreign Accept-Language). Point it\n // at the default locale (the primary market) so search shows that version by\n // default instead of guessing. Only set when the default locale has a URL.\n const defaultLocalePath = alternates[getDefaultLocale(config)]\n if (defaultLocalePath) {\n alternates['x-default'] = defaultLocalePath\n }\n\n return alternates\n}\n"],"names":["buildLocalizedPath","getDefaultLocale","getLocaleCodes","buildHreflangAlternates","baseUrl","config","homeSlug","prefix","slugs","origin","replace","alternates","locale","path","defaultLocalePath"],"mappings":"AAEA,SAASA,kBAAkB,EAAEC,gBAAgB,EAAEC,cAAc,QAAQ,mBAAkB;AAkBvF;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GACD,OAAO,SAASC,wBAAwB,EACtCC,OAAO,EACPC,MAAM,EACNC,WAAW,MAAM,EACjBC,MAAM,EACNC,KAAK,EACa;IAClB,MAAMC,SAASL,SAASM,QAAQ,OAAO,OAAO;IAC9C,MAAMC,aAAqC,CAAC;IAE5C,KAAK,MAAMC,UAAUV,eAAeG,QAAS;QAC3C,MAAMQ,OAAOb,mBAAmB;YAAEK;YAAQC;YAAUM;YAAQL;YAAQC;QAAM;QAC1E,IAAIK,MAAM;YACRF,UAAU,CAACC,OAAO,GAAG,GAAGH,SAASI,MAAM;QACzC;IACF;IAEA,+EAA+E;IAC/E,6EAA6E;IAC7E,8EAA8E;IAC9E,6EAA6E;IAC7E,2EAA2E;IAC3E,MAAMC,oBAAoBH,UAAU,CAACV,iBAAiBI,QAAQ;IAC9D,IAAIS,mBAAmB;QACrBH,UAAU,CAAC,YAAY,GAAGG;IAC5B;IAEA,OAAOH;AACT"}
|
||||
{"version":3,"sources":["../../../src/modules/seo/hreflang.ts"],"sourcesContent":["import type { I18nConfig, LocalizedSlugs } from '../i18n/index.js'\nimport { buildLocalizedPath, getLocaleCodes } from '../i18n/index.js'\n\ntype BuildHreflangArgs = {\n /** slug per locale for the current document, e.g. { pl: 'o-nas', en: 'about' } */\n slugs: LocalizedSlugs\n config: I18nConfig\n /** Absolute site origin, e.g. 'https://example.com'. Omit for relative paths. */\n baseUrl?: string\n /** Home slug that collapses to the locale root. Defaults to 'home'. */\n homeSlug?: string | Record<string, string>\n /**\n * Localized segment the document lives under (an archive page's slugs),\n * e.g. { pl: 'artykuly', en: 'articles' }. Locales missing from the prefix\n * are omitted — an entry with no archive in that language has no URL there.\n */\n prefix?: LocalizedSlugs\n}\n\n/**\n * Builds a map of locale → URL for hreflang alternate links, suitable for\n * Next.js Metadata `alternates.languages`.\n *\n * Bridges SEO and i18n: for each configured locale that the document has a\n * slug in, it produces the locale-aware path (via buildLocalizedPath),\n * optionally prefixed with an absolute origin.\n *\n * @example\n * buildHreflangAlternates({\n * slugs: { pl: 'o-nas', en: 'about' },\n * config,\n * baseUrl: 'https://example.com',\n * })\n * // → { pl: 'https://example.com/pl/o-nas', en: 'https://example.com/en/about' }\n */\nexport function buildHreflangAlternates({\n slugs,\n config,\n baseUrl,\n homeSlug = 'home',\n prefix,\n}: BuildHreflangArgs): Record<string, string> {\n const origin = baseUrl?.replace(/\\/$/, '') ?? ''\n const alternates: Record<string, string> = {}\n\n // Single-locale sites have no language alternatives — hreflang describes\n // relationships BETWEEN language versions, and there's only one. Emitting\n // hreflang (or x-default) here would be wrong, so return empty: the page keeps\n // its canonical, but no alternate-language links.\n if (config.locales.length === 1) {\n return alternates\n }\n\n for (const locale of getLocaleCodes(config)) {\n const path = buildLocalizedPath({ slugs, locale, config, homeSlug, prefix })\n if (path) {\n alternates[locale] = `${origin}${path}`\n }\n }\n\n // x-default: the version Google serves when the user's language/region doesn't\n // match any hreflang — and, crucially here, the fallback when the root ('/')\n // redirect is ambiguous (Googlebot with no/foreign Accept-Language). Point it\n // at the default locale (the primary market) so search shows that version by\n // default instead of guessing. Only set when the default locale has a URL.\n const defaultLocalePath = alternates[config.defaultLocale]\n if (defaultLocalePath) {\n alternates['x-default'] = defaultLocalePath\n }\n\n return alternates\n}\n"],"names":["buildLocalizedPath","getLocaleCodes","buildHreflangAlternates","slugs","config","baseUrl","homeSlug","prefix","origin","replace","alternates","locales","length","locale","path","defaultLocalePath","defaultLocale"],"mappings":"AACA,SAASA,kBAAkB,EAAEC,cAAc,QAAQ,mBAAkB;AAkBrE;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,wBAAwB,EACtCC,KAAK,EACLC,MAAM,EACNC,OAAO,EACPC,WAAW,MAAM,EACjBC,MAAM,EACY;IAClB,MAAMC,SAASH,SAASI,QAAQ,OAAO,OAAO;IAC9C,MAAMC,aAAqC,CAAC;IAE5C,yEAAyE;IACzE,0EAA0E;IAC1E,+EAA+E;IAC/E,kDAAkD;IAClD,IAAIN,OAAOO,OAAO,CAACC,MAAM,KAAK,GAAG;QAC/B,OAAOF;IACT;IAEA,KAAK,MAAMG,UAAUZ,eAAeG,QAAS;QAC3C,MAAMU,OAAOd,mBAAmB;YAAEG;YAAOU;YAAQT;YAAQE;YAAUC;QAAO;QAC1E,IAAIO,MAAM;YACRJ,UAAU,CAACG,OAAO,GAAG,GAAGL,SAASM,MAAM;QACzC;IACF;IAEA,+EAA+E;IAC/E,6EAA6E;IAC7E,8EAA8E;IAC9E,6EAA6E;IAC7E,2EAA2E;IAC3E,MAAMC,oBAAoBL,UAAU,CAACN,OAAOY,aAAa,CAAC;IAC1D,IAAID,mBAAmB;QACrBL,UAAU,CAAC,YAAY,GAAGK;IAC5B;IAEA,OAAOL;AACT"}
|
||||
Vendored
+3
@@ -1,8 +1,10 @@
|
||||
export { buildAutoFillMetaHook } from './autoFillMeta.js';
|
||||
export type { AutoFillMapping } from './autoFillMeta.js';
|
||||
export { buildArticleJsonLd } from './buildArticleJsonLd.js';
|
||||
export { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js';
|
||||
export { buildFaqJsonLd } from './buildFaqJsonLd.js';
|
||||
export { buildIconsMetadata } from './buildIconsMetadata.js';
|
||||
export { buildLlmsTxt } from './buildLlmsTxt.js';
|
||||
export { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js';
|
||||
export { buildMetadata } from './buildMetadata.js';
|
||||
export type { PageMetadata } from './buildMetadata.js';
|
||||
@@ -12,6 +14,7 @@ export type { RobotsRules } from './buildRobots.js';
|
||||
export { buildServiceJsonLd } from './buildServiceJsonLd.js';
|
||||
export { buildSitemapEntries } from './buildSitemapEntries.js';
|
||||
export type { SitemapEntry } from './buildSitemapEntries.js';
|
||||
export { buildSitemapXml } from './buildSitemapXml.js';
|
||||
export { buildSiteNavigationJsonLd } from './buildSiteNavigationJsonLd.js';
|
||||
export { buildWebSiteJsonLd } from './buildWebSiteJsonLd.js';
|
||||
export { composeTitle } from './composeTitle.js';
|
||||
|
||||
Vendored
+3
@@ -1,13 +1,16 @@
|
||||
export { buildAutoFillMetaHook } from './autoFillMeta.js';
|
||||
export { buildArticleJsonLd } from './buildArticleJsonLd.js';
|
||||
export { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js';
|
||||
export { buildFaqJsonLd } from './buildFaqJsonLd.js';
|
||||
export { buildIconsMetadata } from './buildIconsMetadata.js';
|
||||
export { buildLlmsTxt } from './buildLlmsTxt.js';
|
||||
export { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js';
|
||||
export { buildMetadata } from './buildMetadata.js';
|
||||
export { buildOrganizationJsonLd } from './buildOrganizationJsonLd.js';
|
||||
export { buildRobots } from './buildRobots.js';
|
||||
export { buildServiceJsonLd } from './buildServiceJsonLd.js';
|
||||
export { buildSitemapEntries } from './buildSitemapEntries.js';
|
||||
export { buildSitemapXml } from './buildSitemapXml.js';
|
||||
export { buildSiteNavigationJsonLd } from './buildSiteNavigationJsonLd.js';
|
||||
export { buildWebSiteJsonLd } from './buildWebSiteJsonLd.js';
|
||||
export { composeTitle } from './composeTitle.js';
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/seo/index.ts"],"sourcesContent":["export { buildAutoFillMetaHook } from './autoFillMeta.js'\nexport type { AutoFillMapping } from './autoFillMeta.js'\nexport { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js'\nexport { buildFaqJsonLd } from './buildFaqJsonLd.js'\nexport { buildIconsMetadata } from './buildIconsMetadata.js'\nexport { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js'\nexport { buildMetadata } from './buildMetadata.js'\nexport type { PageMetadata } from './buildMetadata.js'\nexport { buildOrganizationJsonLd } from './buildOrganizationJsonLd.js'\nexport { buildRobots } from './buildRobots.js'\nexport type { RobotsRules } from './buildRobots.js'\nexport { buildServiceJsonLd } from './buildServiceJsonLd.js'\nexport { buildSitemapEntries } from './buildSitemapEntries.js'\nexport type { SitemapEntry } from './buildSitemapEntries.js'\nexport { buildSiteNavigationJsonLd } from './buildSiteNavigationJsonLd.js'\nexport { buildWebSiteJsonLd } from './buildWebSiteJsonLd.js'\nexport { composeTitle } from './composeTitle.js'\nexport type { TitleOrder } from './composeTitle.js'\nexport { createMetadataGenerator } from './createMetadataGenerator.js'\nexport { createPageMetadata } from './createPageMetadata.js'\nexport { buildHreflangAlternates } from './hreflang.js'\nexport { injectAutoFillMeta } from './injectAutoFillMeta.js'\nexport { injectSeoTabs } from './injectSeoTabs.js'\nexport { readSiteMetaConfig } from './readSiteMetaConfig.js'\nexport type { SiteMetaConfig } from './readSiteMetaConfig.js'\nexport { buildSeoPlugin } from './seoPluginConfig.js'\nexport { slugsAcrossLocales } from './slugsAcrossLocales.js'\nexport type { SeoMeta, SeoOption } from './types.js'\nexport { validateFaviconField } from './validateFavicon.js'\n"],"names":["buildAutoFillMetaHook","buildBreadcrumbJsonLd","buildFaqJsonLd","buildIconsMetadata","buildLocalBusinessJsonLd","buildMetadata","buildOrganizationJsonLd","buildRobots","buildServiceJsonLd","buildSitemapEntries","buildSiteNavigationJsonLd","buildWebSiteJsonLd","composeTitle","createMetadataGenerator","createPageMetadata","buildHreflangAlternates","injectAutoFillMeta","injectSeoTabs","readSiteMetaConfig","buildSeoPlugin","slugsAcrossLocales","validateFaviconField"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,oBAAmB;AAEzD,SAASC,qBAAqB,QAAQ,6BAA4B;AAClE,SAASC,cAAc,QAAQ,sBAAqB;AACpD,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,wBAAwB,QAAQ,gCAA+B;AACxE,SAASC,aAAa,QAAQ,qBAAoB;AAElD,SAASC,uBAAuB,QAAQ,+BAA8B;AACtE,SAASC,WAAW,QAAQ,mBAAkB;AAE9C,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,SAASC,yBAAyB,QAAQ,iCAAgC;AAC1E,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,YAAY,QAAQ,oBAAmB;AAEhD,SAASC,uBAAuB,QAAQ,+BAA8B;AACtE,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,uBAAuB,QAAQ,gBAAe;AACvD,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,aAAa,QAAQ,qBAAoB;AAClD,SAASC,kBAAkB,QAAQ,0BAAyB;AAE5D,SAASC,cAAc,QAAQ,uBAAsB;AACrD,SAASC,kBAAkB,QAAQ,0BAAyB;AAE5D,SAASC,oBAAoB,QAAQ,uBAAsB"}
|
||||
{"version":3,"sources":["../../../src/modules/seo/index.ts"],"sourcesContent":["export { buildAutoFillMetaHook } from './autoFillMeta.js'\nexport type { AutoFillMapping } from './autoFillMeta.js'\nexport { buildArticleJsonLd } from './buildArticleJsonLd.js'\nexport { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js'\nexport { buildFaqJsonLd } from './buildFaqJsonLd.js'\nexport { buildIconsMetadata } from './buildIconsMetadata.js'\nexport { buildLlmsTxt } from './buildLlmsTxt.js'\nexport { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js'\nexport { buildMetadata } from './buildMetadata.js'\nexport type { PageMetadata } from './buildMetadata.js'\nexport { buildOrganizationJsonLd } from './buildOrganizationJsonLd.js'\nexport { buildRobots } from './buildRobots.js'\nexport type { RobotsRules } from './buildRobots.js'\nexport { buildServiceJsonLd } from './buildServiceJsonLd.js'\nexport { buildSitemapEntries } from './buildSitemapEntries.js'\nexport type { SitemapEntry } from './buildSitemapEntries.js'\nexport { buildSitemapXml } from './buildSitemapXml.js'\nexport { buildSiteNavigationJsonLd } from './buildSiteNavigationJsonLd.js'\nexport { buildWebSiteJsonLd } from './buildWebSiteJsonLd.js'\nexport { composeTitle } from './composeTitle.js'\nexport type { TitleOrder } from './composeTitle.js'\nexport { createMetadataGenerator } from './createMetadataGenerator.js'\nexport { createPageMetadata } from './createPageMetadata.js'\nexport { buildHreflangAlternates } from './hreflang.js'\nexport { injectAutoFillMeta } from './injectAutoFillMeta.js'\nexport { injectSeoTabs } from './injectSeoTabs.js'\nexport { readSiteMetaConfig } from './readSiteMetaConfig.js'\nexport type { SiteMetaConfig } from './readSiteMetaConfig.js'\nexport { buildSeoPlugin } from './seoPluginConfig.js'\nexport { slugsAcrossLocales } from './slugsAcrossLocales.js'\nexport type { SeoMeta, SeoOption } from './types.js'\nexport { validateFaviconField } from './validateFavicon.js'\n"],"names":["buildAutoFillMetaHook","buildArticleJsonLd","buildBreadcrumbJsonLd","buildFaqJsonLd","buildIconsMetadata","buildLlmsTxt","buildLocalBusinessJsonLd","buildMetadata","buildOrganizationJsonLd","buildRobots","buildServiceJsonLd","buildSitemapEntries","buildSitemapXml","buildSiteNavigationJsonLd","buildWebSiteJsonLd","composeTitle","createMetadataGenerator","createPageMetadata","buildHreflangAlternates","injectAutoFillMeta","injectSeoTabs","readSiteMetaConfig","buildSeoPlugin","slugsAcrossLocales","validateFaviconField"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,oBAAmB;AAEzD,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,qBAAqB,QAAQ,6BAA4B;AAClE,SAASC,cAAc,QAAQ,sBAAqB;AACpD,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,YAAY,QAAQ,oBAAmB;AAChD,SAASC,wBAAwB,QAAQ,gCAA+B;AACxE,SAASC,aAAa,QAAQ,qBAAoB;AAElD,SAASC,uBAAuB,QAAQ,+BAA8B;AACtE,SAASC,WAAW,QAAQ,mBAAkB;AAE9C,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,SAASC,eAAe,QAAQ,uBAAsB;AACtD,SAASC,yBAAyB,QAAQ,iCAAgC;AAC1E,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,YAAY,QAAQ,oBAAmB;AAEhD,SAASC,uBAAuB,QAAQ,+BAA8B;AACtE,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,uBAAuB,QAAQ,gBAAe;AACvD,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,aAAa,QAAQ,qBAAoB;AAClD,SAASC,kBAAkB,QAAQ,0BAAyB;AAE5D,SAASC,cAAc,QAAQ,uBAAsB;AACrD,SAASC,kBAAkB,QAAQ,0BAAyB;AAE5D,SAASC,oBAAoB,QAAQ,uBAAsB"}
|
||||
+9
-2
@@ -1,11 +1,18 @@
|
||||
import type { BasePayload } from 'payload';
|
||||
import type { TitleOrder } from './composeTitle.js';
|
||||
export type SiteMetaConfig = {
|
||||
/** Slug of the page assigned as Homepage; collapses to the locale root. */
|
||||
homeSlug?: string;
|
||||
/**
|
||||
* Home slug per locale, e.g. { pl: 'strona-glowna', de: 'startseite' }. A map
|
||||
* (not a single string) so multilingual homepages collapse to root in EVERY
|
||||
* language — otherwise hreflang points other languages at '/de/startseite'
|
||||
* instead of '/de', breaking return tags (a real GSC error).
|
||||
*/
|
||||
homeSlug?: Record<string, string>;
|
||||
order?: TitleOrder;
|
||||
/** Padded separator, e.g. ' | ' — the panel stores the bare character. */
|
||||
separator?: string;
|
||||
/** Global fallback meta description. */
|
||||
siteDescription?: null | string;
|
||||
siteName: null | string;
|
||||
};
|
||||
/**
|
||||
|
||||
+45
-1
@@ -14,10 +14,54 @@
|
||||
locale: locale
|
||||
});
|
||||
const homepage = settings.homepage && typeof settings.homepage === 'object' ? settings.homepage : null;
|
||||
// Build the per-locale home slug map. The homepage's slug is localized, so a
|
||||
// single-locale read only gives THIS language's slug. Re-read the homepage
|
||||
// with locale:'all' to get every language's slug — needed so hreflang can
|
||||
// collapse each language's home to its root.
|
||||
let homeSlug;
|
||||
if (homepage?.id) {
|
||||
try {
|
||||
const homeDoc = await payload.findByID({
|
||||
id: homepage.id,
|
||||
collection: 'pages',
|
||||
depth: 0,
|
||||
locale: 'all'
|
||||
});
|
||||
const slugField = homeDoc.slug;
|
||||
if (slugField && typeof slugField === 'object') {
|
||||
const map = {};
|
||||
for (const [loc, value] of Object.entries(slugField)){
|
||||
if (typeof value === 'string') {
|
||||
map[loc] = value;
|
||||
}
|
||||
}
|
||||
if (Object.keys(map).length > 0) {
|
||||
homeSlug = map;
|
||||
}
|
||||
} else if (typeof slugField === 'string') {
|
||||
// Non-localized slug (same in every language) — key it by current locale;
|
||||
// buildLocalizedPath tolerates a partial map. Non-localized homepage
|
||||
// slugs are rare, so this branch is a safety net, not the norm.
|
||||
homeSlug = {
|
||||
[locale]: slugField
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fall back to the single-locale slug below.
|
||||
}
|
||||
}
|
||||
// Fallback: if the 'all' read didn't yield a map, use the current locale's slug
|
||||
// for at least this locale (better than nothing).
|
||||
if (!homeSlug && typeof homepage?.slug === 'string') {
|
||||
homeSlug = {
|
||||
[locale]: homepage.slug
|
||||
};
|
||||
}
|
||||
return {
|
||||
siteDescription: settings.siteDescription ?? null,
|
||||
siteName: settings[siteNameField] ?? null,
|
||||
// The panel stores '|'; titles need it padded.
|
||||
homeSlug: typeof homepage?.slug === 'string' ? homepage.slug : undefined,
|
||||
homeSlug,
|
||||
order: settings.titleOrder ?? undefined,
|
||||
separator: settings.titleSeparator ? ` ${settings.titleSeparator} ` : undefined
|
||||
};
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Provides the Turnstile site key once for the whole app, like ConsentProvider
|
||||
* for cookies. The project reads the key server-side and passes it here in the
|
||||
* layout; forms then use <TurnstileWidget /> + useTurnstile() with NO per-form
|
||||
* key wiring.
|
||||
*
|
||||
* // layout.tsx (server) → read key, pass to provider
|
||||
* import { TurnstileProvider } from '@intecion/ipal-kit/client'
|
||||
* const siteKey = await getTurnstileSiteKey() // your server helper
|
||||
* <TurnstileProvider siteKey={siteKey}>{children}</TurnstileProvider>
|
||||
*
|
||||
* When siteKey is null (Turnstile not configured), widgets render nothing and
|
||||
* token stays null — forms should treat "no Turnstile" as allowed in dev.
|
||||
*/
|
||||
export declare function TurnstileProvider({ siteKey, children, }: {
|
||||
siteKey: string | null;
|
||||
children: React.ReactNode;
|
||||
}): import("react/jsx-runtime").JSX.Element;
|
||||
/**
|
||||
* Hook giving a form the Turnstile token + a widget bound to the provider's key.
|
||||
* No per-form siteKey plumbing — the provider supplies it.
|
||||
*
|
||||
* const { token, TurnstileWidget, reset } = useTurnstile()
|
||||
* // in JSX: <TurnstileWidget />
|
||||
* // at submit: submitForm({ ..., turnstileToken: token })
|
||||
* // after submit: reset() // clear for the next submission
|
||||
*/
|
||||
export declare function useTurnstile(): {
|
||||
token: string | null;
|
||||
TurnstileWidget: (props?: {
|
||||
theme?: 'light' | 'dark' | 'auto';
|
||||
}) => React.ReactNode;
|
||||
reset: () => void;
|
||||
/** True when Turnstile is configured (site key present). */
|
||||
enabled: boolean;
|
||||
};
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
'use client';
|
||||
import { jsx as _jsx } from "react/jsx-runtime";
|
||||
import { createContext, useCallback, useContext, useState } from 'react';
|
||||
import { Turnstile } from './Turnstile.js';
|
||||
const TurnstileContext = /*#__PURE__*/ createContext(null);
|
||||
/**
|
||||
* Provides the Turnstile site key once for the whole app, like ConsentProvider
|
||||
* for cookies. The project reads the key server-side and passes it here in the
|
||||
* layout; forms then use <TurnstileWidget /> + useTurnstile() with NO per-form
|
||||
* key wiring.
|
||||
*
|
||||
* // layout.tsx (server) → read key, pass to provider
|
||||
* import { TurnstileProvider } from '@intecion/ipal-kit/client'
|
||||
* const siteKey = await getTurnstileSiteKey() // your server helper
|
||||
* <TurnstileProvider siteKey={siteKey}>{children}</TurnstileProvider>
|
||||
*
|
||||
* When siteKey is null (Turnstile not configured), widgets render nothing and
|
||||
* token stays null — forms should treat "no Turnstile" as allowed in dev.
|
||||
*/ export function TurnstileProvider({ siteKey, children }) {
|
||||
const [token, setToken] = useState(null);
|
||||
return /*#__PURE__*/ _jsx(TurnstileContext.Provider, {
|
||||
value: {
|
||||
siteKey,
|
||||
token,
|
||||
setToken
|
||||
},
|
||||
children: children
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Hook giving a form the Turnstile token + a widget bound to the provider's key.
|
||||
* No per-form siteKey plumbing — the provider supplies it.
|
||||
*
|
||||
* const { token, TurnstileWidget, reset } = useTurnstile()
|
||||
* // in JSX: <TurnstileWidget />
|
||||
* // at submit: submitForm({ ..., turnstileToken: token })
|
||||
* // after submit: reset() // clear for the next submission
|
||||
*/ export function useTurnstile() {
|
||||
const ctx = useContext(TurnstileContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useTurnstile must be used within <TurnstileProvider>');
|
||||
}
|
||||
const { siteKey, token, setToken } = ctx;
|
||||
const reset = useCallback(()=>setToken(null), [
|
||||
setToken
|
||||
]);
|
||||
const TurnstileWidget = useCallback((props)=>{
|
||||
if (!siteKey) return null;
|
||||
return /*#__PURE__*/ _jsx(Turnstile, {
|
||||
siteKey: siteKey,
|
||||
onToken: setToken,
|
||||
theme: props?.theme
|
||||
});
|
||||
}, [
|
||||
siteKey,
|
||||
setToken
|
||||
]);
|
||||
return {
|
||||
token,
|
||||
TurnstileWidget,
|
||||
reset,
|
||||
enabled: Boolean(siteKey)
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=TurnstileProvider.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/turnstile/TurnstileProvider.tsx"],"sourcesContent":["'use client'\n\nimport { createContext, useCallback, useContext, useState } from 'react'\nimport { Turnstile } from './Turnstile.js'\n\ntype TurnstileContextValue = {\n /** Public site key from the provider (read server-side, passed once). */\n siteKey: string | null\n /** Current token (null until solved / after expiry). */\n token: string | null\n setToken: (t: string | null) => void\n}\n\nconst TurnstileContext = createContext<TurnstileContextValue | null>(null)\n\n/**\n * Provides the Turnstile site key once for the whole app, like ConsentProvider\n * for cookies. The project reads the key server-side and passes it here in the\n * layout; forms then use <TurnstileWidget /> + useTurnstile() with NO per-form\n * key wiring.\n *\n * // layout.tsx (server) → read key, pass to provider\n * import { TurnstileProvider } from '@intecion/ipal-kit/client'\n * const siteKey = await getTurnstileSiteKey() // your server helper\n * <TurnstileProvider siteKey={siteKey}>{children}</TurnstileProvider>\n *\n * When siteKey is null (Turnstile not configured), widgets render nothing and\n * token stays null — forms should treat \"no Turnstile\" as allowed in dev.\n */\nexport function TurnstileProvider({\n siteKey,\n children,\n}: {\n siteKey: string | null\n children: React.ReactNode\n}) {\n const [token, setToken] = useState<string | null>(null)\n return (\n <TurnstileContext.Provider value={{ siteKey, token, setToken }}>\n {children}\n </TurnstileContext.Provider>\n )\n}\n\n/**\n * Hook giving a form the Turnstile token + a widget bound to the provider's key.\n * No per-form siteKey plumbing — the provider supplies it.\n *\n * const { token, TurnstileWidget, reset } = useTurnstile()\n * // in JSX: <TurnstileWidget />\n * // at submit: submitForm({ ..., turnstileToken: token })\n * // after submit: reset() // clear for the next submission\n */\nexport function useTurnstile(): {\n token: string | null\n TurnstileWidget: (props?: { theme?: 'light' | 'dark' | 'auto' }) => React.ReactNode\n reset: () => void\n /** True when Turnstile is configured (site key present). */\n enabled: boolean\n} {\n const ctx = useContext(TurnstileContext)\n if (!ctx) {\n throw new Error('useTurnstile must be used within <TurnstileProvider>')\n }\n const { siteKey, token, setToken } = ctx\n\n const reset = useCallback(() => setToken(null), [setToken])\n\n const TurnstileWidget = useCallback(\n (props?: { theme?: 'light' | 'dark' | 'auto' }) => {\n if (!siteKey) return null\n return <Turnstile siteKey={siteKey} onToken={setToken} theme={props?.theme} />\n },\n [siteKey, setToken],\n )\n\n return { token, TurnstileWidget, reset, enabled: Boolean(siteKey) }\n}\n"],"names":["createContext","useCallback","useContext","useState","Turnstile","TurnstileContext","TurnstileProvider","siteKey","children","token","setToken","Provider","value","useTurnstile","ctx","Error","reset","TurnstileWidget","props","onToken","theme","enabled","Boolean"],"mappings":"AAAA;;AAEA,SAASA,aAAa,EAAEC,WAAW,EAAEC,UAAU,EAAEC,QAAQ,QAAQ,QAAO;AACxE,SAASC,SAAS,QAAQ,iBAAgB;AAU1C,MAAMC,iCAAmBL,cAA4C;AAErE;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASM,kBAAkB,EAChCC,OAAO,EACPC,QAAQ,EAIT;IACC,MAAM,CAACC,OAAOC,SAAS,GAAGP,SAAwB;IAClD,qBACE,KAACE,iBAAiBM,QAAQ;QAACC,OAAO;YAAEL;YAASE;YAAOC;QAAS;kBAC1DF;;AAGP;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASK;IAOd,MAAMC,MAAMZ,WAAWG;IACvB,IAAI,CAACS,KAAK;QACR,MAAM,IAAIC,MAAM;IAClB;IACA,MAAM,EAAER,OAAO,EAAEE,KAAK,EAAEC,QAAQ,EAAE,GAAGI;IAErC,MAAME,QAAQf,YAAY,IAAMS,SAAS,OAAO;QAACA;KAAS;IAE1D,MAAMO,kBAAkBhB,YACtB,CAACiB;QACC,IAAI,CAACX,SAAS,OAAO;QACrB,qBAAO,KAACH;YAAUG,SAASA;YAASY,SAAST;YAAUU,OAAOF,OAAOE;;IACvE,GACA;QAACb;QAASG;KAAS;IAGrB,OAAO;QAAED;QAAOQ;QAAiBD;QAAOK,SAASC,QAAQf;IAAS;AACpE"}
|
||||
Vendored
+1
@@ -1,2 +1,3 @@
|
||||
export { Turnstile } from './Turnstile.js';
|
||||
export type { TurnstileProps } from './Turnstile.js';
|
||||
export { TurnstileProvider, useTurnstile } from './TurnstileProvider.js';
|
||||
|
||||
Vendored
+1
@@ -2,5 +2,6 @@
|
||||
// Client-only exports — the Turnstile widget. Kept separate from index.ts so
|
||||
// the server-only verify never leaks into a browser bundle.
|
||||
export { Turnstile } from './Turnstile.js';
|
||||
export { TurnstileProvider, useTurnstile } from './TurnstileProvider.js';
|
||||
|
||||
//# sourceMappingURL=client.js.map
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user