diff --git a/dist/index.d.ts b/dist/index.d.ts index aeaa907..85c224a 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -29,7 +29,7 @@ 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 type { BuildSecurityHeadersArgs, SecurityHeader } from './modules/security/index.js'; -export { buildFaqJsonLd, buildIconsMetadata, buildLocalBusinessJsonLd, buildOrganizationJsonLd, buildServiceJsonLd, validateFaviconField, } from './modules/seo/index.js'; +export { buildArticleJsonLd, 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'; diff --git a/dist/index.js b/dist/index.js index 346fda5..03c0836 100644 --- a/dist/index.js +++ b/dist/index.js @@ -18,7 +18,7 @@ export { getNotificationTexts, NOTIFICATION_FALLBACK, resolveFormMessage } from 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'; +export { buildArticleJsonLd, 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'; diff --git a/dist/index.js.map b/dist/index.js.map index 8364bf1..7872c60 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export type { AccessOption, Role } from './modules/access/index.js'\nexport {\n adminOnly,\n adminOnlyField,\n adminOrEditor,\n adminOrEditorField,\n adminOrSelf,\n authenticated,\n hasMinimumRole,\n isAdmin,\n isEditor,\n requireRole,\n requireRoleField,\n ROLE_HIERARCHY,\n} from './modules/access/index.js'\nexport type { AnalyticsConfig } from './modules/analytics/index.js'\nexport { getAnalyticsConfig } from './modules/analytics/index.js'\nexport {\n ACCEPT_ALL_CONSENT,\n CONSENT_CATEGORIES,\n CONSENT_COOKIE,\n CONSENT_MAX_AGE,\n CONSENT_VERSION,\n DEFAULT_CONSENT,\n getConsentTexts,\n parseConsent,\n REJECT_ALL_CONSENT,\n serializeConsent,\n setDefaultConsent,\n updateConsent,\n} from './modules/consent/index.js'\nexport type { ConsentCategory, ConsentState, ConsentTexts } from './modules/consent/index.js'\nexport type {\n ContentCollectionOption,\n ContentOption,\n ResolvedRoute,\n} from './modules/content/index.js'\nexport {\n archiveFieldName,\n buildArchivePath,\n buildEntryPath,\n getArchiveEntries,\n parsePageParam,\n resolveRoute,\n} from './modules/content/index.js'\nexport type { ArchiveEntries } from './modules/content/index.js'\nexport { graphAdapter } from './modules/email/graphAdapter.js'\nexport type { GraphAdapterArgs } from './modules/email/graphAdapter.js'\nexport { mailAdapter } from './modules/email/mailAdapter.js'\nexport type { MailAdapterArgs } from './modules/email/mailAdapter.js'\n// Imported straight from the file, NOT from ./modules/email/index.js — that\n// barrel re-exports sendEmail, which imports 'server-only' and would crash when\n// Payload loads the config (or runs generate:importmap) as a plain Node script.\nexport { panelSmtpAdapter } from './modules/email/panelSmtpAdapter.js'\nexport type { PanelSmtpAdapterArgs } from './modules/email/panelSmtpAdapter.js'\nexport { buildFormsPlugin } from './modules/forms/formsPluginConfig.js'\nexport type {\n FormsCollectionOverrides,\n FormsFieldsOverride,\n FormsOption,\n} from './modules/forms/types.js'\nexport { createContentHelpers } from './modules/frontend/index.js'\nexport type { I18nConfig, LocaleDefinition, LocalizedSlugs } from './modules/i18n/index.js'\nexport {\n buildLocalizedPath,\n getDefaultLocale,\n getLocaleCodes,\n getLocaleDefinition,\n getLocalizedSlugs,\n isValidLocale,\n LOCALE_COOKIE_NAME,\n matchAcceptLanguage,\n negotiateLocale,\n switchLocalePath,\n} from './modules/i18n/index.js'\nexport type { LocaleMiddlewareResult } from './modules/i18n/index.js'\nexport { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from './modules/i18n/index.js'\n// Media — filename normalization hook for upload collections (Media).\nexport { normalizeFilename, normalizeFilenameHook } from './modules/media/index.js'\nexport {\n getNotificationTexts,\n NOTIFICATION_FALLBACK,\n resolveFormMessage,\n} from './modules/notifications/index.js'\nexport type {\n FormNotificationTexts,\n NotificationsData,\n NotificationTexts,\n} from './modules/notifications/index.js'\nexport type { PagesOption, SystemPageRole } from './modules/pages/index.js'\nexport { ALL_SYSTEM_PAGE_ROLES, getSystemPagePath } from './modules/pages/index.js'\nexport type { GlobalQueryOptions } from './modules/payload/index.js'\nexport {\n getGlobal,\n getSiteIntegrations,\n getSiteSettings,\n SITE_INTEGRATIONS_SLUG,\n SITE_SETTINGS_SLUG,\n} from './modules/payload/index.js'\nexport { buildSecurityHeaders } from './modules/security/index.js'\nexport type { BuildSecurityHeadersArgs, SecurityHeader } from './modules/security/index.js'\nexport {\n buildFaqJsonLd,\n buildIconsMetadata,\n buildLocalBusinessJsonLd,\n buildOrganizationJsonLd,\n buildServiceJsonLd,\n validateFaviconField,\n} from './modules/seo/index.js'\n// Structured data (schema.org JSON-LD) — brand/sitelink signals for Google.\n// WebSite (+ optional SearchAction), BreadcrumbList (per page), SiteNavigation.\nexport {\n buildBreadcrumbJsonLd,\n buildSiteNavigationJsonLd,\n buildWebSiteJsonLd,\n} from './modules/seo/index.js'\nexport type { PageMetadata, SeoMeta, SeoOption } from './modules/seo/index.js'\nexport { buildHreflangAlternates, buildMetadata, composeTitle } from './modules/seo/index.js'\nexport type { AutoFillMapping, RobotsRules, SitemapEntry } from './modules/seo/index.js'\nexport {\n buildAutoFillMetaHook,\n buildRobots,\n buildSitemapEntries,\n createMetadataGenerator,\n createPageMetadata,\n injectAutoFillMeta,\n} from './modules/seo/index.js'\nexport { buildSlugField, toSlug } from './modules/slug/index.js'\n// Storage — Cloudflare R2 media offload, configured from .env.\nexport { buildR2Storage } from './modules/storage/index.js'\n\nexport { ipalKit } from './plugin.js'\nexport type { IpalOptions } from './types.js'\n"],"names":["adminOnly","adminOnlyField","adminOrEditor","adminOrEditorField","adminOrSelf","authenticated","hasMinimumRole","isAdmin","isEditor","requireRole","requireRoleField","ROLE_HIERARCHY","getAnalyticsConfig","ACCEPT_ALL_CONSENT","CONSENT_CATEGORIES","CONSENT_COOKIE","CONSENT_MAX_AGE","CONSENT_VERSION","DEFAULT_CONSENT","getConsentTexts","parseConsent","REJECT_ALL_CONSENT","serializeConsent","setDefaultConsent","updateConsent","archiveFieldName","buildArchivePath","buildEntryPath","getArchiveEntries","parsePageParam","resolveRoute","graphAdapter","mailAdapter","panelSmtpAdapter","buildFormsPlugin","createContentHelpers","buildLocalizedPath","getDefaultLocale","getLocaleCodes","getLocaleDefinition","getLocalizedSlugs","isValidLocale","LOCALE_COOKIE_NAME","matchAcceptLanguage","negotiateLocale","switchLocalePath","createLocaleMiddleware","DEFAULT_MIDDLEWARE_MATCHER","normalizeFilename","normalizeFilenameHook","getNotificationTexts","NOTIFICATION_FALLBACK","resolveFormMessage","ALL_SYSTEM_PAGE_ROLES","getSystemPagePath","getGlobal","getSiteIntegrations","getSiteSettings","SITE_INTEGRATIONS_SLUG","SITE_SETTINGS_SLUG","buildSecurityHeaders","buildFaqJsonLd","buildIconsMetadata","buildLocalBusinessJsonLd","buildOrganizationJsonLd","buildServiceJsonLd","validateFaviconField","buildBreadcrumbJsonLd","buildSiteNavigationJsonLd","buildWebSiteJsonLd","buildHreflangAlternates","buildMetadata","composeTitle","buildAutoFillMetaHook","buildRobots","buildSitemapEntries","createMetadataGenerator","createPageMetadata","injectAutoFillMeta","buildSlugField","toSlug","buildR2Storage","ipalKit"],"mappings":"AACA,SACEA,SAAS,EACTC,cAAc,EACdC,aAAa,EACbC,kBAAkB,EAClBC,WAAW,EACXC,aAAa,EACbC,cAAc,EACdC,OAAO,EACPC,QAAQ,EACRC,WAAW,EACXC,gBAAgB,EAChBC,cAAc,QACT,4BAA2B;AAElC,SAASC,kBAAkB,QAAQ,+BAA8B;AACjE,SACEC,kBAAkB,EAClBC,kBAAkB,EAClBC,cAAc,EACdC,eAAe,EACfC,eAAe,EACfC,eAAe,EACfC,eAAe,EACfC,YAAY,EACZC,kBAAkB,EAClBC,gBAAgB,EAChBC,iBAAiB,EACjBC,aAAa,QACR,6BAA4B;AAOnC,SACEC,gBAAgB,EAChBC,gBAAgB,EAChBC,cAAc,EACdC,iBAAiB,EACjBC,cAAc,EACdC,YAAY,QACP,6BAA4B;AAEnC,SAASC,YAAY,QAAQ,kCAAiC;AAE9D,SAASC,WAAW,QAAQ,iCAAgC;AAE5D,4EAA4E;AAC5E,gFAAgF;AAChF,gFAAgF;AAChF,SAASC,gBAAgB,QAAQ,sCAAqC;AAEtE,SAASC,gBAAgB,QAAQ,uCAAsC;AAMvE,SAASC,oBAAoB,QAAQ,8BAA6B;AAElE,SACEC,kBAAkB,EAClBC,gBAAgB,EAChBC,cAAc,EACdC,mBAAmB,EACnBC,iBAAiB,EACjBC,aAAa,EACbC,kBAAkB,EAClBC,mBAAmB,EACnBC,eAAe,EACfC,gBAAgB,QACX,0BAAyB;AAEhC,SAASC,sBAAsB,EAAEC,0BAA0B,QAAQ,0BAAyB;AAC5F,sEAAsE;AACtE,SAASC,iBAAiB,EAAEC,qBAAqB,QAAQ,2BAA0B;AACnF,SACEC,oBAAoB,EACpBC,qBAAqB,EACrBC,kBAAkB,QACb,mCAAkC;AAOzC,SAASC,qBAAqB,EAAEC,iBAAiB,QAAQ,2BAA0B;AAEnF,SACEC,SAAS,EACTC,mBAAmB,EACnBC,eAAe,EACfC,sBAAsB,EACtBC,kBAAkB,QACb,6BAA4B;AACnC,SAASC,oBAAoB,QAAQ,8BAA6B;AAElE,SACEC,cAAc,EACdC,kBAAkB,EAClBC,wBAAwB,EACxBC,uBAAuB,EACvBC,kBAAkB,EAClBC,oBAAoB,QACf,yBAAwB;AAC/B,4EAA4E;AAC5E,gFAAgF;AAChF,SACEC,qBAAqB,EACrBC,yBAAyB,EACzBC,kBAAkB,QACb,yBAAwB;AAE/B,SAASC,uBAAuB,EAAEC,aAAa,EAAEC,YAAY,QAAQ,yBAAwB;AAE7F,SACEC,qBAAqB,EACrBC,WAAW,EACXC,mBAAmB,EACnBC,uBAAuB,EACvBC,kBAAkB,EAClBC,kBAAkB,QACb,yBAAwB;AAC/B,SAASC,cAAc,EAAEC,MAAM,QAAQ,0BAAyB;AAChE,+DAA+D;AAC/D,SAASC,cAAc,QAAQ,6BAA4B;AAE3D,SAASC,OAAO,QAAQ,cAAa"} \ No newline at end of file +{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export type { AccessOption, Role } from './modules/access/index.js'\nexport {\n adminOnly,\n adminOnlyField,\n adminOrEditor,\n adminOrEditorField,\n adminOrSelf,\n authenticated,\n hasMinimumRole,\n isAdmin,\n isEditor,\n requireRole,\n requireRoleField,\n ROLE_HIERARCHY,\n} from './modules/access/index.js'\nexport type { AnalyticsConfig } from './modules/analytics/index.js'\nexport { getAnalyticsConfig } from './modules/analytics/index.js'\nexport {\n ACCEPT_ALL_CONSENT,\n CONSENT_CATEGORIES,\n CONSENT_COOKIE,\n CONSENT_MAX_AGE,\n CONSENT_VERSION,\n DEFAULT_CONSENT,\n getConsentTexts,\n parseConsent,\n REJECT_ALL_CONSENT,\n serializeConsent,\n setDefaultConsent,\n updateConsent,\n} from './modules/consent/index.js'\nexport type { ConsentCategory, ConsentState, ConsentTexts } from './modules/consent/index.js'\nexport type {\n ContentCollectionOption,\n ContentOption,\n ResolvedRoute,\n} from './modules/content/index.js'\nexport {\n archiveFieldName,\n buildArchivePath,\n buildEntryPath,\n getArchiveEntries,\n parsePageParam,\n resolveRoute,\n} from './modules/content/index.js'\nexport type { ArchiveEntries } from './modules/content/index.js'\nexport { graphAdapter } from './modules/email/graphAdapter.js'\nexport type { GraphAdapterArgs } from './modules/email/graphAdapter.js'\nexport { mailAdapter } from './modules/email/mailAdapter.js'\nexport type { MailAdapterArgs } from './modules/email/mailAdapter.js'\n// Imported straight from the file, NOT from ./modules/email/index.js — that\n// barrel re-exports sendEmail, which imports 'server-only' and would crash when\n// Payload loads the config (or runs generate:importmap) as a plain Node script.\nexport { panelSmtpAdapter } from './modules/email/panelSmtpAdapter.js'\nexport type { PanelSmtpAdapterArgs } from './modules/email/panelSmtpAdapter.js'\nexport { buildFormsPlugin } from './modules/forms/formsPluginConfig.js'\nexport type {\n FormsCollectionOverrides,\n FormsFieldsOverride,\n FormsOption,\n} from './modules/forms/types.js'\nexport { createContentHelpers } from './modules/frontend/index.js'\nexport type { I18nConfig, LocaleDefinition, LocalizedSlugs } from './modules/i18n/index.js'\nexport {\n buildLocalizedPath,\n getDefaultLocale,\n getLocaleCodes,\n getLocaleDefinition,\n getLocalizedSlugs,\n isValidLocale,\n LOCALE_COOKIE_NAME,\n matchAcceptLanguage,\n negotiateLocale,\n switchLocalePath,\n} from './modules/i18n/index.js'\nexport type { LocaleMiddlewareResult } from './modules/i18n/index.js'\nexport { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from './modules/i18n/index.js'\n// Media — filename normalization hook for upload collections (Media).\nexport { normalizeFilename, normalizeFilenameHook } from './modules/media/index.js'\nexport {\n getNotificationTexts,\n NOTIFICATION_FALLBACK,\n resolveFormMessage,\n} from './modules/notifications/index.js'\nexport type {\n FormNotificationTexts,\n NotificationsData,\n NotificationTexts,\n} from './modules/notifications/index.js'\nexport type { PagesOption, SystemPageRole } from './modules/pages/index.js'\nexport { ALL_SYSTEM_PAGE_ROLES, getSystemPagePath } from './modules/pages/index.js'\nexport type { GlobalQueryOptions } from './modules/payload/index.js'\nexport {\n getGlobal,\n getSiteIntegrations,\n getSiteSettings,\n SITE_INTEGRATIONS_SLUG,\n SITE_SETTINGS_SLUG,\n} from './modules/payload/index.js'\nexport { buildSecurityHeaders } from './modules/security/index.js'\nexport type { BuildSecurityHeadersArgs, SecurityHeader } from './modules/security/index.js'\nexport {\n buildArticleJsonLd,\n buildFaqJsonLd,\n buildIconsMetadata,\n buildLocalBusinessJsonLd,\n buildOrganizationJsonLd,\n buildServiceJsonLd,\n validateFaviconField,\n} from './modules/seo/index.js'\n// Structured data (schema.org JSON-LD) — brand/sitelink signals for Google.\n// WebSite (+ optional SearchAction), BreadcrumbList (per page), SiteNavigation.\nexport {\n buildBreadcrumbJsonLd,\n buildSiteNavigationJsonLd,\n buildWebSiteJsonLd,\n} from './modules/seo/index.js'\nexport type { PageMetadata, SeoMeta, SeoOption } from './modules/seo/index.js'\nexport { buildHreflangAlternates, buildMetadata, composeTitle } from './modules/seo/index.js'\nexport type { AutoFillMapping, RobotsRules, SitemapEntry } from './modules/seo/index.js'\nexport {\n buildAutoFillMetaHook,\n buildRobots,\n buildSitemapEntries,\n createMetadataGenerator,\n createPageMetadata,\n injectAutoFillMeta,\n} from './modules/seo/index.js'\nexport { buildSlugField, toSlug } from './modules/slug/index.js'\n// Storage — Cloudflare R2 media offload, configured from .env.\nexport { buildR2Storage } from './modules/storage/index.js'\n\nexport { ipalKit } from './plugin.js'\nexport type { IpalOptions } from './types.js'\n"],"names":["adminOnly","adminOnlyField","adminOrEditor","adminOrEditorField","adminOrSelf","authenticated","hasMinimumRole","isAdmin","isEditor","requireRole","requireRoleField","ROLE_HIERARCHY","getAnalyticsConfig","ACCEPT_ALL_CONSENT","CONSENT_CATEGORIES","CONSENT_COOKIE","CONSENT_MAX_AGE","CONSENT_VERSION","DEFAULT_CONSENT","getConsentTexts","parseConsent","REJECT_ALL_CONSENT","serializeConsent","setDefaultConsent","updateConsent","archiveFieldName","buildArchivePath","buildEntryPath","getArchiveEntries","parsePageParam","resolveRoute","graphAdapter","mailAdapter","panelSmtpAdapter","buildFormsPlugin","createContentHelpers","buildLocalizedPath","getDefaultLocale","getLocaleCodes","getLocaleDefinition","getLocalizedSlugs","isValidLocale","LOCALE_COOKIE_NAME","matchAcceptLanguage","negotiateLocale","switchLocalePath","createLocaleMiddleware","DEFAULT_MIDDLEWARE_MATCHER","normalizeFilename","normalizeFilenameHook","getNotificationTexts","NOTIFICATION_FALLBACK","resolveFormMessage","ALL_SYSTEM_PAGE_ROLES","getSystemPagePath","getGlobal","getSiteIntegrations","getSiteSettings","SITE_INTEGRATIONS_SLUG","SITE_SETTINGS_SLUG","buildSecurityHeaders","buildArticleJsonLd","buildFaqJsonLd","buildIconsMetadata","buildLocalBusinessJsonLd","buildOrganizationJsonLd","buildServiceJsonLd","validateFaviconField","buildBreadcrumbJsonLd","buildSiteNavigationJsonLd","buildWebSiteJsonLd","buildHreflangAlternates","buildMetadata","composeTitle","buildAutoFillMetaHook","buildRobots","buildSitemapEntries","createMetadataGenerator","createPageMetadata","injectAutoFillMeta","buildSlugField","toSlug","buildR2Storage","ipalKit"],"mappings":"AACA,SACEA,SAAS,EACTC,cAAc,EACdC,aAAa,EACbC,kBAAkB,EAClBC,WAAW,EACXC,aAAa,EACbC,cAAc,EACdC,OAAO,EACPC,QAAQ,EACRC,WAAW,EACXC,gBAAgB,EAChBC,cAAc,QACT,4BAA2B;AAElC,SAASC,kBAAkB,QAAQ,+BAA8B;AACjE,SACEC,kBAAkB,EAClBC,kBAAkB,EAClBC,cAAc,EACdC,eAAe,EACfC,eAAe,EACfC,eAAe,EACfC,eAAe,EACfC,YAAY,EACZC,kBAAkB,EAClBC,gBAAgB,EAChBC,iBAAiB,EACjBC,aAAa,QACR,6BAA4B;AAOnC,SACEC,gBAAgB,EAChBC,gBAAgB,EAChBC,cAAc,EACdC,iBAAiB,EACjBC,cAAc,EACdC,YAAY,QACP,6BAA4B;AAEnC,SAASC,YAAY,QAAQ,kCAAiC;AAE9D,SAASC,WAAW,QAAQ,iCAAgC;AAE5D,4EAA4E;AAC5E,gFAAgF;AAChF,gFAAgF;AAChF,SAASC,gBAAgB,QAAQ,sCAAqC;AAEtE,SAASC,gBAAgB,QAAQ,uCAAsC;AAMvE,SAASC,oBAAoB,QAAQ,8BAA6B;AAElE,SACEC,kBAAkB,EAClBC,gBAAgB,EAChBC,cAAc,EACdC,mBAAmB,EACnBC,iBAAiB,EACjBC,aAAa,EACbC,kBAAkB,EAClBC,mBAAmB,EACnBC,eAAe,EACfC,gBAAgB,QACX,0BAAyB;AAEhC,SAASC,sBAAsB,EAAEC,0BAA0B,QAAQ,0BAAyB;AAC5F,sEAAsE;AACtE,SAASC,iBAAiB,EAAEC,qBAAqB,QAAQ,2BAA0B;AACnF,SACEC,oBAAoB,EACpBC,qBAAqB,EACrBC,kBAAkB,QACb,mCAAkC;AAOzC,SAASC,qBAAqB,EAAEC,iBAAiB,QAAQ,2BAA0B;AAEnF,SACEC,SAAS,EACTC,mBAAmB,EACnBC,eAAe,EACfC,sBAAsB,EACtBC,kBAAkB,QACb,6BAA4B;AACnC,SAASC,oBAAoB,QAAQ,8BAA6B;AAElE,SACEC,kBAAkB,EAClBC,cAAc,EACdC,kBAAkB,EAClBC,wBAAwB,EACxBC,uBAAuB,EACvBC,kBAAkB,EAClBC,oBAAoB,QACf,yBAAwB;AAC/B,4EAA4E;AAC5E,gFAAgF;AAChF,SACEC,qBAAqB,EACrBC,yBAAyB,EACzBC,kBAAkB,QACb,yBAAwB;AAE/B,SAASC,uBAAuB,EAAEC,aAAa,EAAEC,YAAY,QAAQ,yBAAwB;AAE7F,SACEC,qBAAqB,EACrBC,WAAW,EACXC,mBAAmB,EACnBC,uBAAuB,EACvBC,kBAAkB,EAClBC,kBAAkB,QACb,yBAAwB;AAC/B,SAASC,cAAc,EAAEC,MAAM,QAAQ,0BAAyB;AAChE,+DAA+D;AAC/D,SAASC,cAAc,QAAQ,6BAA4B;AAE3D,SAASC,OAAO,QAAQ,cAAa"} \ No newline at end of file diff --git a/dist/modules/i18n/localizedPath.d.ts b/dist/modules/i18n/localizedPath.d.ts index 112706a..9495129 100644 --- a/dist/modules/i18n/localizedPath.d.ts +++ b/dist/modules/i18n/localizedPath.d.ts @@ -8,14 +8,22 @@ import type { I18nConfig } from './types.js'; */ export type LocalizedSlugs = Record; 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; /** * 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; + 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 {}; diff --git a/dist/modules/i18n/localizedPath.js b/dist/modules/i18n/localizedPath.js index 3a65ce5..8949f08 100644 --- a/dist/modules/i18n/localizedPath.js +++ b/dist/modules/i18n/localizedPath.js @@ -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; } @@ -44,7 +44,12 @@ import { isValidLocale } from './helpers.js'; } return `${localeSegment}/${segment}/${slug}`; } - if (slug === homeSlug) { + // 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 || '/'; } @@ -57,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}`; } diff --git a/dist/modules/i18n/localizedPath.js.map b/dist/modules/i18n/localizedPath.js.map index fa8b475..8297e8f 100644 --- a/dist/modules/i18n/localizedPath.js.map +++ b/dist/modules/i18n/localizedPath.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../../src/modules/i18n/localizedPath.ts"],"sourcesContent":["import type { I18nConfig } from './types.js'\n\nimport { isValidLocale } from './helpers.js'\n\n/**\n * Minimal shape the path builder needs from a document.\n *\n * The plugin doesn't know the client's Pages type, so it depends only on\n * this contract: a map of locale code → slug for that locale. The template\n * supplies it (e.g. by reading the localized slug field across locales).\n */\nexport type LocalizedSlugs = Record\n\ntype BuildPathArgs = {\n config: I18nConfig\n /**\n * Slug that represents the site root (served at /{locale} with no trailing\n * segment). Defaults to 'home'. Matched against the slug in the target locale.\n */\n homeSlug?: string\n /** Target locale to build the path for */\n locale: string\n /**\n * Localized segment the document lives under, e.g.\n * `{ pl: 'artykuly', en: 'articles' }` → /pl/artykuly/moj-post.\n *\n * These are the slugs of the collection's archive page, so the prefix is\n * whatever an editor named that page — and it differs per locale for free.\n * A document under a prefix is never the home page, so homeSlug is ignored.\n */\n prefix?: LocalizedSlugs\n /** slug per locale, e.g. { pl: 'strona-glowna', en: 'home' } */\n slugs: LocalizedSlugs\n}\n\n/**\n * Builds a locale-prefixed path for a document in a target locale.\n *\n * Always prefixes the locale: /{locale} or /{locale}/{slug}. The home slug\n * collapses to the locale root. Returns undefined if the document has no slug\n * in the target locale (caller decides fallback behavior).\n *\n * @example\n * buildLocalizedPath({ slugs: { pl: 'strona-glowna', en: 'home' }, locale: 'en', config })\n * // → '/en' (home slug collapses to root)\n *\n * buildLocalizedPath({ slugs: { pl: 'o-nas', en: 'about' }, locale: 'en', config })\n * // → '/en/about'\n *\n * buildLocalizedPath({\n * slugs: { pl: 'moj-post', en: 'my-post' },\n * prefix: { pl: 'artykuly', en: 'articles' },\n * locale: 'en',\n * config,\n * })\n * // → '/en/articles/my-post'\n */\nexport function buildLocalizedPath({\n config,\n homeSlug = 'home',\n locale,\n prefix,\n slugs,\n}: BuildPathArgs): string | undefined {\n if (!isValidLocale(locale, config)) {\n return undefined\n }\n\n const slug = slugs[locale]\n if (!slug) {\n return undefined\n }\n\n // Single-locale sites have no /pl, /en prefix — the language segment is\n // dropped entirely (path is /o-nas, not /pl/o-nas). Detected automatically:\n // one configured locale means one language, so no prefix is needed. The\n // project's folder structure matches (app/[[...slug]] without [locale]).\n const singleLocale = config.locales.length === 1\n const localeSegment = singleLocale ? '' : `/${locale}`\n\n if (prefix) {\n const segment = prefix[locale]\n // No archive slug in this locale means the entry is unreachable there —\n // there's no path to point at, so hreflang should omit it rather than\n // invent /en/moj-post.\n if (!segment) {\n return undefined\n }\n return `${localeSegment}/${segment}/${slug}`\n }\n\n if (slug === homeSlug) {\n // Home collapses to the root: '/' for single-locale, '/pl' otherwise.\n return localeSegment || '/'\n }\n\n return `${localeSegment}/${slug}`\n}\n\ntype SwitchLocaleArgs = {\n config: I18nConfig\n homeSlug?: string\n prefix?: LocalizedSlugs\n slugs: LocalizedSlugs\n targetLocale: string\n}\n\n/**\n * Resolves the equivalent path for the same document in a different locale —\n * the language-switcher use case (/pl/strona-glowna → /en/home).\n *\n * Never dead-ends on a 404. When the document has no slug in the target locale,\n * falls back to the archive it belongs to (/en/articles) if there is one, and\n * to the locale root otherwise — the closest place the visitor would want.\n */\nexport function switchLocalePath({\n config,\n homeSlug = 'home',\n prefix,\n slugs,\n targetLocale,\n}: SwitchLocaleArgs): string {\n const path = buildLocalizedPath({ config, homeSlug, locale: targetLocale, prefix, slugs })\n if (path) {return path}\n\n const archiveSegment = prefix?.[targetLocale]\n if (archiveSegment) {return `/${targetLocale}/${archiveSegment}`}\n\n return `/${targetLocale}`\n}\n"],"names":["isValidLocale","buildLocalizedPath","config","homeSlug","locale","prefix","slugs","undefined","slug","singleLocale","locales","length","localeSegment","segment","switchLocalePath","targetLocale","path","archiveSegment"],"mappings":"AAEA,SAASA,aAAa,QAAQ,eAAc;AAiC5C;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASC,mBAAmB,EACjCC,MAAM,EACNC,WAAW,MAAM,EACjBC,MAAM,EACNC,MAAM,EACNC,KAAK,EACS;IACd,IAAI,CAACN,cAAcI,QAAQF,SAAS;QAClC,OAAOK;IACT;IAEA,MAAMC,OAAOF,KAAK,CAACF,OAAO;IAC1B,IAAI,CAACI,MAAM;QACT,OAAOD;IACT;IAEA,wEAAwE;IACxE,4EAA4E;IAC5E,wEAAwE;IACxE,yEAAyE;IACzE,MAAME,eAAeP,OAAOQ,OAAO,CAACC,MAAM,KAAK;IAC/C,MAAMC,gBAAgBH,eAAe,KAAK,CAAC,CAAC,EAAEL,QAAQ;IAEtD,IAAIC,QAAQ;QACV,MAAMQ,UAAUR,MAAM,CAACD,OAAO;QAC9B,wEAAwE;QACxE,sEAAsE;QACtE,uBAAuB;QACvB,IAAI,CAACS,SAAS;YACZ,OAAON;QACT;QACA,OAAO,GAAGK,cAAc,CAAC,EAAEC,QAAQ,CAAC,EAAEL,MAAM;IAC9C;IAEA,IAAIA,SAASL,UAAU;QACrB,sEAAsE;QACtE,OAAOS,iBAAiB;IAC1B;IAEA,OAAO,GAAGA,cAAc,CAAC,EAAEJ,MAAM;AACnC;AAUA;;;;;;;CAOC,GACD,OAAO,SAASM,iBAAiB,EAC/BZ,MAAM,EACNC,WAAW,MAAM,EACjBE,MAAM,EACNC,KAAK,EACLS,YAAY,EACK;IACjB,MAAMC,OAAOf,mBAAmB;QAAEC;QAAQC;QAAUC,QAAQW;QAAcV;QAAQC;IAAM;IACxF,IAAIU,MAAM;QAAC,OAAOA;IAAI;IAEtB,MAAMC,iBAAiBZ,QAAQ,CAACU,aAAa;IAC7C,IAAIE,gBAAgB;QAAC,OAAO,CAAC,CAAC,EAAEF,aAAa,CAAC,EAAEE,gBAAgB;IAAA;IAEhE,OAAO,CAAC,CAAC,EAAEF,cAAc;AAC3B"} \ No newline at end of file +{"version":3,"sources":["../../../src/modules/i18n/localizedPath.ts"],"sourcesContent":["import type { I18nConfig } from './types.js'\nimport { isValidLocale } from './helpers.js'\n\n/**\n * Minimal shape the path builder needs from a document.\n *\n * The plugin doesn't know the client's Pages type, so it depends only on\n * this contract: a map of locale code → slug for that locale. The template\n * supplies it (e.g. by reading the localized slug field across locales).\n */\nexport type LocalizedSlugs = Record\n\ntype BuildPathArgs = {\n /** slug per locale, e.g. { pl: 'strona-glowna', en: 'home' } */\n slugs: LocalizedSlugs\n /** Target locale to build the path for */\n locale: string\n config: I18nConfig\n /**\n * Slug(s) representing the site root (served at /{locale} with no trailing\n * segment). Defaults to 'home'. Matched against the slug in the target locale.\n *\n * Can be a single string (same home slug in every locale) OR a per-locale map\n * (`{ pl: 'strona-glowna', de: 'startseite', en: 'home' }`). The map form is\n * REQUIRED for multilingual homepages whose slug differs per language —\n * otherwise the home page collapses to '/pl' but '/de/startseite' stays\n * un-collapsed, breaking hreflang return tags (a real GSC error).\n */\n homeSlug?: string | Record\n /**\n * Localized segment the document lives under, e.g.\n * `{ pl: 'artykuly', en: 'articles' }` → /pl/artykuly/moj-post.\n *\n * These are the slugs of the collection's archive page, so the prefix is\n * whatever an editor named that page — and it differs per locale for free.\n * A document under a prefix is never the home page, so homeSlug is ignored.\n */\n prefix?: LocalizedSlugs\n}\n\n/**\n * Builds a locale-prefixed path for a document in a target locale.\n *\n * Always prefixes the locale: /{locale} or /{locale}/{slug}. The home slug\n * collapses to the locale root. Returns undefined if the document has no slug\n * in the target locale (caller decides fallback behavior).\n *\n * @example\n * buildLocalizedPath({ slugs: { pl: 'strona-glowna', en: 'home' }, locale: 'en', config })\n * // → '/en' (home slug collapses to root)\n *\n * buildLocalizedPath({ slugs: { pl: 'o-nas', en: 'about' }, locale: 'en', config })\n * // → '/en/about'\n *\n * buildLocalizedPath({\n * slugs: { pl: 'moj-post', en: 'my-post' },\n * prefix: { pl: 'artykuly', en: 'articles' },\n * locale: 'en',\n * config,\n * })\n * // → '/en/articles/my-post'\n */\nexport function buildLocalizedPath({\n slugs,\n locale,\n config,\n homeSlug = 'home',\n prefix,\n}: BuildPathArgs): string | undefined {\n if (!isValidLocale(locale, config)) {\n return undefined\n }\n\n const slug = slugs[locale]\n if (!slug) {\n return undefined\n }\n\n // Single-locale sites have no /pl, /en prefix — the language segment is\n // dropped entirely (path is /o-nas, not /pl/o-nas). Detected automatically:\n // one configured locale means one language, so no prefix is needed. The\n // project's folder structure matches (app/[[...slug]] without [locale]).\n const singleLocale = config.locales.length === 1\n const localeSegment = singleLocale ? '' : `/${locale}`\n\n if (prefix) {\n const segment = prefix[locale]\n // No archive slug in this locale means the entry is unreachable there —\n // there's no path to point at, so hreflang should omit it rather than\n // invent /en/moj-post.\n if (!segment) {\n return undefined\n }\n return `${localeSegment}/${segment}/${slug}`\n }\n\n // Resolve the home slug for THIS locale. With a per-locale map, each language\n // has its own home slug (pl: 'strona-glowna', de: 'startseite'), so the check\n // below correctly collapses each language's home to its root — instead of only\n // the current-language slug matching and the others staying un-collapsed.\n const targetHomeSlug = typeof homeSlug === 'object' ? homeSlug[locale] : homeSlug\n\n if (slug === targetHomeSlug) {\n // Home collapses to the root: '/' for single-locale, '/pl' otherwise.\n return localeSegment || '/'\n }\n\n return `${localeSegment}/${slug}`\n}\n\ntype SwitchLocaleArgs = {\n slugs: LocalizedSlugs\n targetLocale: string\n config: I18nConfig\n homeSlug?: string | Record\n prefix?: LocalizedSlugs\n}\n\n/**\n * Resolves the equivalent path for the same document in a different locale —\n * the language-switcher use case (/pl/strona-glowna → /en/home).\n *\n * Never dead-ends on a 404. When the document has no slug in the target locale,\n * falls back to the archive it belongs to (/en/articles) if there is one, and\n * to the locale root otherwise — the closest place the visitor would want.\n */\nexport function switchLocalePath({\n slugs,\n targetLocale,\n config,\n homeSlug = 'home',\n prefix,\n}: SwitchLocaleArgs): string {\n const path = buildLocalizedPath({ slugs, locale: targetLocale, config, homeSlug, prefix })\n if (path) return path\n\n const archiveSegment = prefix?.[targetLocale]\n if (archiveSegment) return `/${targetLocale}/${archiveSegment}`\n\n return `/${targetLocale}`\n}\n"],"names":["isValidLocale","buildLocalizedPath","slugs","locale","config","homeSlug","prefix","undefined","slug","singleLocale","locales","length","localeSegment","segment","targetHomeSlug","switchLocalePath","targetLocale","path","archiveSegment"],"mappings":"AACA,SAASA,aAAa,QAAQ,eAAc;AAuC5C;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASC,mBAAmB,EACjCC,KAAK,EACLC,MAAM,EACNC,MAAM,EACNC,WAAW,MAAM,EACjBC,MAAM,EACQ;IACd,IAAI,CAACN,cAAcG,QAAQC,SAAS;QAClC,OAAOG;IACT;IAEA,MAAMC,OAAON,KAAK,CAACC,OAAO;IAC1B,IAAI,CAACK,MAAM;QACT,OAAOD;IACT;IAEA,wEAAwE;IACxE,4EAA4E;IAC5E,wEAAwE;IACxE,yEAAyE;IACzE,MAAME,eAAeL,OAAOM,OAAO,CAACC,MAAM,KAAK;IAC/C,MAAMC,gBAAgBH,eAAe,KAAK,CAAC,CAAC,EAAEN,QAAQ;IAEtD,IAAIG,QAAQ;QACV,MAAMO,UAAUP,MAAM,CAACH,OAAO;QAC9B,wEAAwE;QACxE,sEAAsE;QACtE,uBAAuB;QACvB,IAAI,CAACU,SAAS;YACZ,OAAON;QACT;QACA,OAAO,GAAGK,cAAc,CAAC,EAAEC,QAAQ,CAAC,EAAEL,MAAM;IAC9C;IAEA,8EAA8E;IAC9E,8EAA8E;IAC9E,+EAA+E;IAC/E,0EAA0E;IAC1E,MAAMM,iBAAiB,OAAOT,aAAa,WAAWA,QAAQ,CAACF,OAAO,GAAGE;IAEzE,IAAIG,SAASM,gBAAgB;QAC3B,sEAAsE;QACtE,OAAOF,iBAAiB;IAC1B;IAEA,OAAO,GAAGA,cAAc,CAAC,EAAEJ,MAAM;AACnC;AAUA;;;;;;;CAOC,GACD,OAAO,SAASO,iBAAiB,EAC/Bb,KAAK,EACLc,YAAY,EACZZ,MAAM,EACNC,WAAW,MAAM,EACjBC,MAAM,EACW;IACjB,MAAMW,OAAOhB,mBAAmB;QAAEC;QAAOC,QAAQa;QAAcZ;QAAQC;QAAUC;IAAO;IACxF,IAAIW,MAAM,OAAOA;IAEjB,MAAMC,iBAAiBZ,QAAQ,CAACU,aAAa;IAC7C,IAAIE,gBAAgB,OAAO,CAAC,CAAC,EAAEF,aAAa,CAAC,EAAEE,gBAAgB;IAE/D,OAAO,CAAC,CAAC,EAAEF,cAAc;AAC3B"} \ No newline at end of file diff --git a/dist/modules/pages/getSystemPagePath.d.ts b/dist/modules/pages/getSystemPagePath.d.ts index 45532ec..eca6476 100644 --- a/dist/modules/pages/getSystemPagePath.d.ts +++ b/dist/modules/pages/getSystemPagePath.d.ts @@ -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; /** Target locale to build the path for. */ locale: string; /** diff --git a/dist/modules/pages/getSystemPagePath.js.map b/dist/modules/pages/getSystemPagePath.js.map index 5ca5cca..06febd1 100644 --- a/dist/modules/pages/getSystemPagePath.js.map +++ b/dist/modules/pages/getSystemPagePath.js.map @@ -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\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"} \ No newline at end of file +{"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\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\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"} \ No newline at end of file diff --git a/dist/modules/seo/buildArticleJsonLd.d.ts b/dist/modules/seo/buildArticleJsonLd.d.ts new file mode 100644 index 0000000..44a2b6a --- /dev/null +++ b/dist/modules/seo/buildArticleJsonLd.d.ts @@ -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 {}; diff --git a/dist/modules/seo/buildArticleJsonLd.js b/dist/modules/seo/buildArticleJsonLd.js new file mode 100644 index 0000000..8c5dac0 --- /dev/null +++ b/dist/modules/seo/buildArticleJsonLd.js @@ -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 \ No newline at end of file diff --git a/dist/modules/seo/buildArticleJsonLd.js.map b/dist/modules/seo/buildArticleJsonLd.js.map new file mode 100644 index 0000000..f216b9e --- /dev/null +++ b/dist/modules/seo/buildArticleJsonLd.js.map @@ -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"} \ No newline at end of file diff --git a/dist/modules/seo/buildMetadata.d.ts b/dist/modules/seo/buildMetadata.d.ts index a69a257..d2f5b11 100644 --- a/dist/modules/seo/buildMetadata.d.ts +++ b/dist/modules/seo/buildMetadata.d.ts @@ -31,7 +31,7 @@ type BuildMetadataArgs = { baseUrl?: string; config: I18nConfig; /** Home slug that collapses to the locale root. Defaults to 'home'. */ - homeSlug?: string; + homeSlug?: string | Record; /** Resolved OG image URL (page image or site defaultShareImage). */ imageUrl?: null | string; /** Current locale being rendered. */ diff --git a/dist/modules/seo/buildMetadata.js.map b/dist/modules/seo/buildMetadata.js.map index fb4a2d4..f4a6428 100644 --- a/dist/modules/seo/buildMetadata.js.map +++ b/dist/modules/seo/buildMetadata.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../../src/modules/seo/buildMetadata.ts"],"sourcesContent":["import type { I18nConfig, LocalizedSlugs } from '../i18n/index.js'\nimport type { TitleOrder } from './composeTitle.js'\nimport type { SeoMeta } from './types.js'\n\nimport { buildLocalizedPath } from '../i18n/index.js'\nimport { composeTitle } from './composeTitle.js'\nimport { buildHreflangAlternates } from './hreflang.js'\n\n/**\n * Subset of Next.js `Metadata` this helper produces. Kept local so the plugin\n * doesn't depend on `next` types; the shape is assignable to Next's Metadata.\n */\nexport type PageMetadata = {\n alternates?: {\n canonical?: string\n languages?: Record\n }\n description?: string\n openGraph?: {\n description?: string\n images?: { url: string }[]\n locale?: string\n title: string\n }\n /** robots directives — set to noindex/follow for legal/thin/search pages. */\n robots?: {\n follow: boolean\n index: boolean\n }\n title: string\n}\n\ntype BuildMetadataArgs = {\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 /** Resolved OG image URL (page image or site defaultShareImage). */\n imageUrl?: null | string\n /** Current locale being rendered. */\n locale: string\n /** SEO meta from the document (plugin-seo group). */\n meta?: null | SeoMeta\n /** Page title or site name first. Defaults to 'page-first'. */\n order?: TitleOrder\n /**\n * The document's own title (e.g. page.title = 'Sprzątanie biur'). Used as the\n * page-title source when meta.title is empty — the browser tab and search\n * result should show the page name, not go blank, when an editor didn't fill\n * the SEO title. Priority: titleOverride > meta.title > pageTitle.\n */\n pageTitle?: null | string\n /**\n * Localized segment the document lives under (an archive page's slugs).\n * Feeds both canonical and hreflang, so /pl/artykuly/moj-post and\n * /en/articles/my-post point at each other correctly.\n */\n prefix?: LocalizedSlugs\n /**\n * Query string appended to canonical and every hreflang, e.g. '?page=2'.\n *\n * A paginated listing must be canonical to itself — pointing page 2 at page 1\n * tells Google the entries on it don't exist. Alternates carry the same page,\n * since /pl/artykuly?page=2 corresponds to /en/articles?page=2.\n */\n query?: string\n /** Separator between page title and site name. Defaults to ' | '. */\n separator?: string\n /** Site name for title composition and OG. */\n siteName?: null | string\n /** slug per locale for this document — drives canonical + hreflang. */\n slugs: LocalizedSlugs\n}\n\n/**\n * Assembles a Next.js-compatible Metadata object from document SEO fields and\n * site-level data. Locale-aware: canonical points at the current locale's\n * path, and hreflang alternates cover every locale the document exists in.\n *\n * Designed for use inside Next.js `generateMetadata`. The caller resolves the\n * pieces (meta group, site name, image URL, localized slugs) and passes them\n * in — the plugin composes, it doesn't fetch.\n */\nexport function buildMetadata({\n baseUrl,\n config,\n homeSlug = 'home',\n imageUrl,\n locale,\n meta,\n order,\n pageTitle,\n prefix,\n query,\n separator,\n siteName,\n slugs,\n}: BuildMetadataArgs): PageMetadata {\n // Title source priority: titleOverride (exact, wins outright) > meta.title\n // (SEO title an editor set) > pageTitle (the document's own name). This means\n // a page with no SEO title still shows its name (e.g. 'Sprzątanie biur')\n // composed with the site name, instead of just the site name or a blank.\n const override = meta?.titleOverride?.trim()\n const resolvedPageTitle = meta?.title?.trim() || pageTitle?.trim() || undefined\n const title =\n override || composeTitle({ order, pageTitle: resolvedPageTitle, separator, siteName })\n const description = meta?.description?.trim() || undefined\n const origin = baseUrl?.replace(/\\/$/, '') ?? ''\n\n const suffix = query ?? ''\n\n const currentPath = buildLocalizedPath({ config, homeSlug, locale, prefix, slugs })\n const canonical = currentPath ? `${origin}${currentPath}${suffix}` : undefined\n\n const languages = buildHreflangAlternates({ baseUrl, config, homeSlug, prefix, slugs })\n if (suffix) {\n for (const code of Object.keys(languages)) {\n languages[code] = `${languages[code]}${suffix}`\n }\n }\n\n const images = imageUrl ? [{ url: imageUrl }] : undefined\n\n return {\n title,\n ...(description && { description }),\n alternates: {\n ...(canonical && { canonical }),\n ...(Object.keys(languages).length > 0 && { languages }),\n },\n openGraph: {\n title,\n ...(description && { description }),\n ...(images && { images }),\n locale,\n },\n // noindex → tell search engines to exclude the page but still follow links\n // (authority flows through). For legal/thin/search-result pages.\n ...(meta?.noindex ? { robots: { follow: true, index: false } } : {}),\n }\n}\n"],"names":["buildLocalizedPath","composeTitle","buildHreflangAlternates","buildMetadata","baseUrl","config","homeSlug","imageUrl","locale","meta","order","pageTitle","prefix","query","separator","siteName","slugs","override","titleOverride","trim","resolvedPageTitle","title","undefined","description","origin","replace","suffix","currentPath","canonical","languages","code","Object","keys","images","url","alternates","length","openGraph","noindex","robots","follow","index"],"mappings":"AAIA,SAASA,kBAAkB,QAAQ,mBAAkB;AACrD,SAASC,YAAY,QAAQ,oBAAmB;AAChD,SAASC,uBAAuB,QAAQ,gBAAe;AAqEvD;;;;;;;;CAQC,GACD,OAAO,SAASC,cAAc,EAC5BC,OAAO,EACPC,MAAM,EACNC,WAAW,MAAM,EACjBC,QAAQ,EACRC,MAAM,EACNC,IAAI,EACJC,KAAK,EACLC,SAAS,EACTC,MAAM,EACNC,KAAK,EACLC,SAAS,EACTC,QAAQ,EACRC,KAAK,EACa;IAClB,2EAA2E;IAC3E,8EAA8E;IAC9E,yEAAyE;IACzE,yEAAyE;IACzE,MAAMC,WAAWR,MAAMS,eAAeC;IACtC,MAAMC,oBAAoBX,MAAMY,OAAOF,UAAUR,WAAWQ,UAAUG;IACtE,MAAMD,QACJJ,YAAYhB,aAAa;QAAES;QAAOC,WAAWS;QAAmBN;QAAWC;IAAS;IACtF,MAAMQ,cAAcd,MAAMc,aAAaJ,UAAUG;IACjD,MAAME,SAASpB,SAASqB,QAAQ,OAAO,OAAO;IAE9C,MAAMC,SAASb,SAAS;IAExB,MAAMc,cAAc3B,mBAAmB;QAAEK;QAAQC;QAAUE;QAAQI;QAAQI;IAAM;IACjF,MAAMY,YAAYD,cAAc,GAAGH,SAASG,cAAcD,QAAQ,GAAGJ;IAErE,MAAMO,YAAY3B,wBAAwB;QAAEE;QAASC;QAAQC;QAAUM;QAAQI;IAAM;IACrF,IAAIU,QAAQ;QACV,KAAK,MAAMI,QAAQC,OAAOC,IAAI,CAACH,WAAY;YACzCA,SAAS,CAACC,KAAK,GAAG,GAAGD,SAAS,CAACC,KAAK,GAAGJ,QAAQ;QACjD;IACF;IAEA,MAAMO,SAAS1B,WAAW;QAAC;YAAE2B,KAAK3B;QAAS;KAAE,GAAGe;IAEhD,OAAO;QACLD;QACA,GAAIE,eAAe;YAAEA;QAAY,CAAC;QAClCY,YAAY;YACV,GAAIP,aAAa;gBAAEA;YAAU,CAAC;YAC9B,GAAIG,OAAOC,IAAI,CAACH,WAAWO,MAAM,GAAG,KAAK;gBAAEP;YAAU,CAAC;QACxD;QACAQ,WAAW;YACThB;YACA,GAAIE,eAAe;gBAAEA;YAAY,CAAC;YAClC,GAAIU,UAAU;gBAAEA;YAAO,CAAC;YACxBzB;QACF;QACA,2EAA2E;QAC3E,iEAAiE;QACjE,GAAIC,MAAM6B,UAAU;YAAEC,QAAQ;gBAAEC,QAAQ;gBAAMC,OAAO;YAAM;QAAE,IAAI,CAAC,CAAC;IACrE;AACF"} \ No newline at end of file +{"version":3,"sources":["../../../src/modules/seo/buildMetadata.ts"],"sourcesContent":["import type { I18nConfig, LocalizedSlugs } from '../i18n/index.js'\nimport type { TitleOrder } from './composeTitle.js'\nimport type { SeoMeta } from './types.js'\n\nimport { buildLocalizedPath } from '../i18n/index.js'\nimport { composeTitle } from './composeTitle.js'\nimport { buildHreflangAlternates } from './hreflang.js'\n\n/**\n * Subset of Next.js `Metadata` this helper produces. Kept local so the plugin\n * doesn't depend on `next` types; the shape is assignable to Next's Metadata.\n */\nexport type PageMetadata = {\n alternates?: {\n canonical?: string\n languages?: Record\n }\n description?: string\n openGraph?: {\n description?: string\n images?: { url: string }[]\n locale?: string\n title: string\n }\n /** robots directives — set to noindex/follow for legal/thin/search pages. */\n robots?: {\n follow: boolean\n index: boolean\n }\n title: string\n}\n\ntype BuildMetadataArgs = {\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\n /** Resolved OG image URL (page image or site defaultShareImage). */\n imageUrl?: null | string\n /** Current locale being rendered. */\n locale: string\n /** SEO meta from the document (plugin-seo group). */\n meta?: null | SeoMeta\n /** Page title or site name first. Defaults to 'page-first'. */\n order?: TitleOrder\n /**\n * The document's own title (e.g. page.title = 'Sprzątanie biur'). Used as the\n * page-title source when meta.title is empty — the browser tab and search\n * result should show the page name, not go blank, when an editor didn't fill\n * the SEO title. Priority: titleOverride > meta.title > pageTitle.\n */\n pageTitle?: null | string\n /**\n * Localized segment the document lives under (an archive page's slugs).\n * Feeds both canonical and hreflang, so /pl/artykuly/moj-post and\n * /en/articles/my-post point at each other correctly.\n */\n prefix?: LocalizedSlugs\n /**\n * Query string appended to canonical and every hreflang, e.g. '?page=2'.\n *\n * A paginated listing must be canonical to itself — pointing page 2 at page 1\n * tells Google the entries on it don't exist. Alternates carry the same page,\n * since /pl/artykuly?page=2 corresponds to /en/articles?page=2.\n */\n query?: string\n /** Separator between page title and site name. Defaults to ' | '. */\n separator?: string\n /** Site name for title composition and OG. */\n siteName?: null | string\n /** slug per locale for this document — drives canonical + hreflang. */\n slugs: LocalizedSlugs\n}\n\n/**\n * Assembles a Next.js-compatible Metadata object from document SEO fields and\n * site-level data. Locale-aware: canonical points at the current locale's\n * path, and hreflang alternates cover every locale the document exists in.\n *\n * Designed for use inside Next.js `generateMetadata`. The caller resolves the\n * pieces (meta group, site name, image URL, localized slugs) and passes them\n * in — the plugin composes, it doesn't fetch.\n */\nexport function buildMetadata({\n baseUrl,\n config,\n homeSlug = 'home',\n imageUrl,\n locale,\n meta,\n order,\n pageTitle,\n prefix,\n query,\n separator,\n siteName,\n slugs,\n}: BuildMetadataArgs): PageMetadata {\n // Title source priority: titleOverride (exact, wins outright) > meta.title\n // (SEO title an editor set) > pageTitle (the document's own name). This means\n // a page with no SEO title still shows its name (e.g. 'Sprzątanie biur')\n // composed with the site name, instead of just the site name or a blank.\n const override = meta?.titleOverride?.trim()\n const resolvedPageTitle = meta?.title?.trim() || pageTitle?.trim() || undefined\n const title =\n override || composeTitle({ order, pageTitle: resolvedPageTitle, separator, siteName })\n const description = meta?.description?.trim() || undefined\n const origin = baseUrl?.replace(/\\/$/, '') ?? ''\n\n const suffix = query ?? ''\n\n const currentPath = buildLocalizedPath({ config, homeSlug, locale, prefix, slugs })\n const canonical = currentPath ? `${origin}${currentPath}${suffix}` : undefined\n\n const languages = buildHreflangAlternates({ baseUrl, config, homeSlug, prefix, slugs })\n if (suffix) {\n for (const code of Object.keys(languages)) {\n languages[code] = `${languages[code]}${suffix}`\n }\n }\n\n const images = imageUrl ? [{ url: imageUrl }] : undefined\n\n return {\n title,\n ...(description && { description }),\n alternates: {\n ...(canonical && { canonical }),\n ...(Object.keys(languages).length > 0 && { languages }),\n },\n openGraph: {\n title,\n ...(description && { description }),\n ...(images && { images }),\n locale,\n },\n // noindex → tell search engines to exclude the page but still follow links\n // (authority flows through). For legal/thin/search-result pages.\n ...(meta?.noindex ? { robots: { follow: true, index: false } } : {}),\n }\n}\n"],"names":["buildLocalizedPath","composeTitle","buildHreflangAlternates","buildMetadata","baseUrl","config","homeSlug","imageUrl","locale","meta","order","pageTitle","prefix","query","separator","siteName","slugs","override","titleOverride","trim","resolvedPageTitle","title","undefined","description","origin","replace","suffix","currentPath","canonical","languages","code","Object","keys","images","url","alternates","length","openGraph","noindex","robots","follow","index"],"mappings":"AAIA,SAASA,kBAAkB,QAAQ,mBAAkB;AACrD,SAASC,YAAY,QAAQ,oBAAmB;AAChD,SAASC,uBAAuB,QAAQ,gBAAe;AAqEvD;;;;;;;;CAQC,GACD,OAAO,SAASC,cAAc,EAC5BC,OAAO,EACPC,MAAM,EACNC,WAAW,MAAM,EACjBC,QAAQ,EACRC,MAAM,EACNC,IAAI,EACJC,KAAK,EACLC,SAAS,EACTC,MAAM,EACNC,KAAK,EACLC,SAAS,EACTC,QAAQ,EACRC,KAAK,EACa;IAClB,2EAA2E;IAC3E,8EAA8E;IAC9E,yEAAyE;IACzE,yEAAyE;IACzE,MAAMC,WAAWR,MAAMS,eAAeC;IACtC,MAAMC,oBAAoBX,MAAMY,OAAOF,UAAUR,WAAWQ,UAAUG;IACtE,MAAMD,QACJJ,YAAYhB,aAAa;QAAES;QAAOC,WAAWS;QAAmBN;QAAWC;IAAS;IACtF,MAAMQ,cAAcd,MAAMc,aAAaJ,UAAUG;IACjD,MAAME,SAASpB,SAASqB,QAAQ,OAAO,OAAO;IAE9C,MAAMC,SAASb,SAAS;IAExB,MAAMc,cAAc3B,mBAAmB;QAAEK;QAAQC;QAAUE;QAAQI;QAAQI;IAAM;IACjF,MAAMY,YAAYD,cAAc,GAAGH,SAASG,cAAcD,QAAQ,GAAGJ;IAErE,MAAMO,YAAY3B,wBAAwB;QAAEE;QAASC;QAAQC;QAAUM;QAAQI;IAAM;IACrF,IAAIU,QAAQ;QACV,KAAK,MAAMI,QAAQC,OAAOC,IAAI,CAACH,WAAY;YACzCA,SAAS,CAACC,KAAK,GAAG,GAAGD,SAAS,CAACC,KAAK,GAAGJ,QAAQ;QACjD;IACF;IAEA,MAAMO,SAAS1B,WAAW;QAAC;YAAE2B,KAAK3B;QAAS;KAAE,GAAGe;IAEhD,OAAO;QACLD;QACA,GAAIE,eAAe;YAAEA;QAAY,CAAC;QAClCY,YAAY;YACV,GAAIP,aAAa;gBAAEA;YAAU,CAAC;YAC9B,GAAIG,OAAOC,IAAI,CAACH,WAAWO,MAAM,GAAG,KAAK;gBAAEP;YAAU,CAAC;QACxD;QACAQ,WAAW;YACThB;YACA,GAAIE,eAAe;gBAAEA;YAAY,CAAC;YAClC,GAAIU,UAAU;gBAAEA;YAAO,CAAC;YACxBzB;QACF;QACA,2EAA2E;QAC3E,iEAAiE;QACjE,GAAIC,MAAM6B,UAAU;YAAEC,QAAQ;gBAAEC,QAAQ;gBAAMC,OAAO;YAAM;QAAE,IAAI,CAAC,CAAC;IACrE;AACF"} \ No newline at end of file diff --git a/dist/modules/seo/buildSitemapEntries.d.ts b/dist/modules/seo/buildSitemapEntries.d.ts index 5da6a4c..049acbf 100644 --- a/dist/modules/seo/buildSitemapEntries.d.ts +++ b/dist/modules/seo/buildSitemapEntries.d.ts @@ -1,6 +1,6 @@ import type { BasePayload } from 'payload'; -import type { I18nConfig } from '../i18n/index.js'; import type { ContentOption } from '../content/index.js'; +import type { I18nConfig } from '../i18n/index.js'; /** * One sitemap entry, shaped for Next's `app/sitemap.ts`. * @@ -10,31 +10,31 @@ import type { ContentOption } from '../content/index.js'; * is the common, weaker kind. */ export type SitemapEntry = { - url: string; - lastModified?: string | Date; - changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'; - priority?: number; alternates?: { languages: Record; }; + changeFrequency?: 'always' | 'daily' | 'hourly' | 'monthly' | 'never' | 'weekly' | 'yearly'; + lastModified?: Date | string; + priority?: number; + url: string; }; type BuildSitemapArgs = { - payload: BasePayload; - config: I18nConfig; /** Absolute origin, e.g. 'https://example.com'. Required for valid sitemap URLs. */ baseUrl: string; - /** Pages collection slug. Defaults to 'pages'. */ - pagesSlug?: string; + changeFrequency?: SitemapEntry['changeFrequency']; + config: I18nConfig; /** Archive-backed collections, same value as the plugin option. */ content?: ContentOption; - /** SiteSettings global slug. Defaults to 'site-settings'. */ - settingsSlug?: string; /** * Slug of the page that is the site root (collapses to /{locale}). * Read from System Pages when omitted. */ - homeSlug?: string; - changeFrequency?: SitemapEntry['changeFrequency']; + homeSlug?: Record | string; + /** Pages collection slug. Defaults to 'pages'. */ + pagesSlug?: string; + payload: BasePayload; + /** SiteSettings global slug. Defaults to 'site-settings'. */ + settingsSlug?: string; }; /** * Collects every public URL — pages and archive entries — as sitemap entries @@ -57,5 +57,5 @@ type BuildSitemapArgs = { * } * ``` */ -export declare function buildSitemapEntries({ payload, config, baseUrl, pagesSlug, content, settingsSlug, homeSlug, changeFrequency, }: BuildSitemapArgs): Promise; +export declare function buildSitemapEntries({ baseUrl, changeFrequency, config, content, homeSlug, pagesSlug, payload, settingsSlug, }: BuildSitemapArgs): Promise; export {}; diff --git a/dist/modules/seo/buildSitemapEntries.js b/dist/modules/seo/buildSitemapEntries.js index 30dcfed..6db0b55 100644 --- a/dist/modules/seo/buildSitemapEntries.js +++ b/dist/modules/seo/buildSitemapEntries.js @@ -1,7 +1,6 @@ -import { getLocalizedSlugs } from '../i18n/index.js'; -import { buildLocalizedPath } from '../i18n/index.js'; -import { buildHreflangAlternates } from './hreflang.js'; import { archiveFieldName } from '../content/index.js'; +import { buildLocalizedPath, getLocaleCodes, getLocalizedSlugs } from '../i18n/index.js'; +import { buildHreflangAlternates } from './hreflang.js'; /** * 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 @@ -10,22 +9,32 @@ import { archiveFieldName } from '../content/index.js'; */ const EXCLUDED_SITEMAP_SLUGS = new Set([ '404', '500', - 'not-found', - 'error' + '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 (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; + 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; + if (doc._status && doc._status !== 'published') { + return false; + } + if (doc.meta?.noindex) { + return false; + } + if (hasExcludedSlug(doc.slug)) { + return false; + } return true; } /** @@ -35,24 +44,26 @@ import { archiveFieldName } from '../content/index.js'; * every locale (including itself, per Google's guidance). */ function entryFor(doc, locale, config, baseUrl, homeSlug, prefix, changeFrequency) { const slugs = doc.slug && typeof doc.slug === 'object' ? getLocalizedSlugs({ - slugField: doc.slug, - config + config, + slugField: doc.slug }) : {}; const path = buildLocalizedPath({ - slugs, - locale, config, homeSlug, - prefix + locale, + prefix, + slugs }); - if (!path) return null; + if (!path) { + return null; + } const origin = baseUrl.replace(/\/$/, ''); const languages = buildHreflangAlternates({ - slugs, - config, baseUrl, + config, homeSlug, - prefix + prefix, + slugs }); return { url: `${origin}${path}`, @@ -89,15 +100,15 @@ import { archiveFieldName } from '../content/index.js'; * }) * } * ``` - */ export async function buildSitemapEntries({ payload, config, baseUrl, pagesSlug = 'pages', content, settingsSlug = 'site-settings', homeSlug, changeFrequency = 'weekly' }) { + */ export async function buildSitemapEntries({ baseUrl, changeFrequency = 'weekly', config, content, homeSlug, pagesSlug = 'pages', payload, settingsSlug = 'site-settings' }) { const locales = config.locales.map((l)=>l.code); const defaultLocale = config.defaultLocale; // Resolve homeSlug and archive prefixes from System Pages (read once, in all // locales so archive prefixes are available per language). const settings = await payload.findGlobal({ slug: settingsSlug, - locale: 'all', - depth: 1 + depth: 1, + locale: 'all' }); const resolvedHomeSlug = homeSlug ?? extractSlugInLocale(settings.homepage, defaultLocale) ?? 'home'; // Which collections to walk: pages (no prefix) + each content collection with @@ -121,24 +132,38 @@ import { archiveFieldName } from '../content/index.js'; // alternates without re-querying per locale. const result = await payload.find({ collection: collection.slug, - locale: 'all', depth: 0, limit: 0, + locale: 'all', pagination: false }); for (const raw of result.docs){ - 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); - if (entry) entries.push(entry); + if (!isIndexable(raw)) { + continue; + } + // Emit a separate for EACH locale. Google's sitemap spec for + // localized sites requires one per language version (each with its + // own plus xhtml:link alternates), not a single default-locale + // 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) { - if (!rel || typeof rel !== 'object') return undefined; + if (!rel || typeof rel !== 'object') { + return undefined; + } const slug = rel.slug; - if (typeof slug === 'string') return slug; + if (typeof slug === 'string') { + return slug; + } if (slug && typeof slug === 'object') { const v = slug[locale]; return typeof v === 'string' ? v : undefined; @@ -146,13 +171,19 @@ import { archiveFieldName } from '../content/index.js'; return undefined; } /** Builds a locale→slug map from a populated archive relationship. */ function slugMapAllLocales(rel, locales) { - if (!rel || typeof rel !== 'object') return undefined; + if (!rel || typeof rel !== 'object') { + return undefined; + } const slug = rel.slug; - if (!slug || typeof slug !== 'object') return undefined; + if (!slug || typeof slug !== 'object') { + return undefined; + } const map = {}; for (const locale of locales){ const v = slug[locale]; - if (typeof v === 'string') map[locale] = v; + if (typeof v === 'string') { + map[locale] = v; + } } return Object.keys(map).length ? map : undefined; } diff --git a/dist/modules/seo/buildSitemapEntries.js.map b/dist/modules/seo/buildSitemapEntries.js.map index 309b0d5..a63ed89 100644 --- a/dist/modules/seo/buildSitemapEntries.js.map +++ b/dist/modules/seo/buildSitemapEntries.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../../src/modules/seo/buildSitemapEntries.ts"],"sourcesContent":["import type { BasePayload } from 'payload'\nimport type { I18nConfig } from '../i18n/index.js'\nimport { getLocalizedSlugs } from '../i18n/index.js'\nimport { buildLocalizedPath } from '../i18n/index.js'\nimport { buildHreflangAlternates } from './hreflang.js'\nimport type { ContentOption } from '../content/index.js'\nimport { archiveFieldName } from '../content/index.js'\n\n/**\n * One sitemap entry, shaped for Next's `app/sitemap.ts`.\n *\n * `alternates.languages` is the important part for a multilingual site: Next\n * renders it as `` per URL, which is\n * exactly what Google uses to connect language versions. A sitemap without it\n * is the common, weaker kind.\n */\nexport type SitemapEntry = {\n url: string\n lastModified?: string | Date\n changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'\n priority?: number\n alternates?: { languages: Record }\n}\n\ntype CollectionEntry = {\n slug: string\n /** Localized segment for entries (archive page slugs), when applicable. */\n prefixSlugs?: Record\n}\n\ntype BuildSitemapArgs = {\n payload: BasePayload\n config: I18nConfig\n /** Absolute origin, e.g. 'https://example.com'. Required for valid sitemap URLs. */\n baseUrl: string\n /** Pages collection slug. Defaults to 'pages'. */\n pagesSlug?: string\n /** Archive-backed collections, same value as the plugin option. */\n content?: ContentOption\n /** SiteSettings global slug. Defaults to 'site-settings'. */\n settingsSlug?: string\n /**\n * Slug of the page that is the site root (collapses to /{locale}).\n * Read from System Pages when omitted.\n */\n homeSlug?: string\n changeFrequency?: SitemapEntry['changeFrequency']\n}\n\ntype DocRow = {\n id: string | number\n slug?: unknown\n updatedAt?: string\n _status?: string\n meta?: { noindex?: boolean } | null\n}\n\n/**\n * Slugs that must never appear in the sitemap — error/system pages that exist as\n * documents (e.g. a '404' page in the Pages collection) but should not be\n * indexed. A sitemap should list only real, HTTP-200 content; a '/pl/404' entry\n * is an audit finding. Matched against the slug in any locale.\n */\nconst EXCLUDED_SITEMAP_SLUGS = new Set(['404', '500', 'not-found', 'error'])\n\n/** True if the doc's slug (in any locale) is an excluded system/error slug. */\nfunction hasExcludedSlug(slug: unknown): boolean {\n if (typeof slug === 'string') return EXCLUDED_SITEMAP_SLUGS.has(slug)\n if (slug && typeof slug === 'object') {\n for (const value of Object.values(slug as Record)) {\n if (typeof value === 'string' && EXCLUDED_SITEMAP_SLUGS.has(value)) return true\n }\n }\n return false\n}\n\n/** Skip drafts, noindex, and system/error pages (404 etc.). */\nfunction isIndexable(doc: DocRow): boolean {\n if (doc._status && doc._status !== 'published') return false\n if (doc.meta?.noindex) return false\n if (hasExcludedSlug(doc.slug)) return false\n return true\n}\n\n/**\n * Turns one document into a sitemap entry per default locale isn't needed —\n * one entry with all languages as alternates is the correct, de-duplicated\n * shape. The `url` is the current locale's path; `alternates.languages` carries\n * every locale (including itself, per Google's guidance).\n */\nfunction entryFor(\n doc: DocRow,\n locale: string,\n config: I18nConfig,\n baseUrl: string,\n homeSlug: string | undefined,\n prefix: Record | undefined,\n changeFrequency: SitemapEntry['changeFrequency'],\n): SitemapEntry | null {\n const slugs =\n doc.slug && typeof doc.slug === 'object'\n ? getLocalizedSlugs({ slugField: doc.slug as Record, config })\n : {}\n\n const path = buildLocalizedPath({ slugs, locale, config, homeSlug, prefix })\n if (!path) return null\n\n const origin = baseUrl.replace(/\\/$/, '')\n const languages = buildHreflangAlternates({ slugs, config, baseUrl, homeSlug, prefix })\n\n return {\n url: `${origin}${path}`,\n ...(doc.updatedAt ? { lastModified: doc.updatedAt } : {}),\n ...(changeFrequency ? { changeFrequency } : {}),\n ...(Object.keys(languages).length ? { alternates: { languages } } : {}),\n }\n}\n\n/**\n * Collects every public URL — pages and archive entries — as sitemap entries\n * with per-URL hreflang and lastmod.\n *\n * Reuses the same path/hreflang builders as page metadata, so the sitemap can't\n * drift from what the pages actually render (a classic source of sitemap bugs:\n * a URL listed one way and served another). Drafts and noindex documents are\n * omitted.\n *\n * ```ts\n * // app/sitemap.ts\n * export default async function sitemap() {\n * return buildSitemapEntries({\n * payload: await getPayload({ config }),\n * config: i18nConfig,\n * baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!,\n * content: contentConfig,\n * })\n * }\n * ```\n */\nexport async function buildSitemapEntries({\n payload,\n config,\n baseUrl,\n pagesSlug = 'pages',\n content,\n settingsSlug = 'site-settings',\n homeSlug,\n changeFrequency = 'weekly',\n}: BuildSitemapArgs): Promise {\n const locales = config.locales.map((l) => l.code)\n const defaultLocale = config.defaultLocale\n\n // Resolve homeSlug and archive prefixes from System Pages (read once, in all\n // locales so archive prefixes are available per language).\n const settings = (await payload.findGlobal({\n slug: settingsSlug as never,\n locale: 'all' as never,\n depth: 1,\n })) as Record\n\n const resolvedHomeSlug =\n homeSlug ?? extractSlugInLocale(settings.homepage, defaultLocale) ?? 'home'\n\n // Which collections to walk: pages (no prefix) + each content collection with\n // its archive-page slugs as the localized prefix.\n const collections: CollectionEntry[] = [{ slug: pagesSlug }]\n\n for (const c of content?.collections ?? []) {\n const archive = settings[archiveFieldName(c.slug)]\n const prefixSlugs = archive ? slugMapAllLocales(archive, locales) : undefined\n collections.push({ slug: c.slug, prefixSlugs })\n }\n\n const entries: SitemapEntry[] = []\n\n for (const collection of collections) {\n // Read every doc once, across locales, so one row yields all its language\n // alternates without re-querying per locale.\n const result = await payload.find({\n collection: collection.slug as never,\n locale: 'all' as never,\n depth: 0,\n limit: 0, // no pagination — sitemap wants everything\n pagination: false as never,\n })\n\n for (const raw of result.docs as DocRow[]) {\n if (!isIndexable(raw)) continue\n\n // Emit the entry under the default locale's URL; alternates cover the rest.\n const entry = entryFor(\n raw,\n defaultLocale,\n config,\n baseUrl,\n resolvedHomeSlug,\n collection.prefixSlugs,\n changeFrequency,\n )\n if (entry) entries.push(entry)\n }\n }\n\n return entries\n}\n\n/** Pulls a slug string from a populated relationship in a specific locale. */\nfunction extractSlugInLocale(rel: unknown, locale: string): string | undefined {\n if (!rel || typeof rel !== 'object') return undefined\n const slug = (rel as { slug?: unknown }).slug\n if (typeof slug === 'string') return slug\n if (slug && typeof slug === 'object') {\n const v = (slug as Record)[locale]\n return typeof v === 'string' ? v : undefined\n }\n return undefined\n}\n\n/** Builds a locale→slug map from a populated archive relationship. */\nfunction slugMapAllLocales(rel: unknown, locales: string[]): Record | undefined {\n if (!rel || typeof rel !== 'object') return undefined\n const slug = (rel as { slug?: unknown }).slug\n if (!slug || typeof slug !== 'object') return undefined\n\n const map: Record = {}\n for (const locale of locales) {\n const v = (slug as Record)[locale]\n if (typeof v === 'string') map[locale] = v\n }\n return Object.keys(map).length ? map : undefined\n}\n"],"names":["getLocalizedSlugs","buildLocalizedPath","buildHreflangAlternates","archiveFieldName","EXCLUDED_SITEMAP_SLUGS","Set","hasExcludedSlug","slug","has","value","Object","values","isIndexable","doc","_status","meta","noindex","entryFor","locale","config","baseUrl","homeSlug","prefix","changeFrequency","slugs","slugField","path","origin","replace","languages","url","updatedAt","lastModified","keys","length","alternates","buildSitemapEntries","payload","pagesSlug","content","settingsSlug","locales","map","l","code","defaultLocale","settings","findGlobal","depth","resolvedHomeSlug","extractSlugInLocale","homepage","collections","c","archive","prefixSlugs","slugMapAllLocales","undefined","push","entries","collection","result","find","limit","pagination","raw","docs","entry","rel","v"],"mappings":"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"} \ No newline at end of file +{"version":3,"sources":["../../../src/modules/seo/buildSitemapEntries.ts"],"sourcesContent":["import type { BasePayload } from 'payload'\n\nimport type { ContentOption } from '../content/index.js'\nimport type { I18nConfig } from '../i18n/index.js'\n\nimport { archiveFieldName } from '../content/index.js'\nimport { buildLocalizedPath, getLocaleCodes , getLocalizedSlugs } from '../i18n/index.js'\nimport { buildHreflangAlternates } from './hreflang.js'\n\n/**\n * One sitemap entry, shaped for Next's `app/sitemap.ts`.\n *\n * `alternates.languages` is the important part for a multilingual site: Next\n * renders it as `` per URL, which is\n * exactly what Google uses to connect language versions. A sitemap without it\n * is the common, weaker kind.\n */\nexport type SitemapEntry = {\n alternates?: { languages: Record }\n changeFrequency?: 'always' | 'daily' | 'hourly' | 'monthly' | 'never' | 'weekly' | 'yearly'\n lastModified?: Date | string\n priority?: number\n url: string\n}\n\ntype CollectionEntry = {\n /** Localized segment for entries (archive page slugs), when applicable. */\n prefixSlugs?: Record\n slug: string\n}\n\ntype BuildSitemapArgs = {\n /** Absolute origin, e.g. 'https://example.com'. Required for valid sitemap URLs. */\n baseUrl: string\n changeFrequency?: SitemapEntry['changeFrequency']\n config: I18nConfig\n /** Archive-backed collections, same value as the plugin option. */\n content?: ContentOption\n /**\n * Slug of the page that is the site root (collapses to /{locale}).\n * Read from System Pages when omitted.\n */\n homeSlug?: Record | string\n /** Pages collection slug. Defaults to 'pages'. */\n pagesSlug?: string\n payload: BasePayload\n /** SiteSettings global slug. Defaults to 'site-settings'. */\n settingsSlug?: string\n}\n\ntype DocRow = {\n _status?: string\n id: number | string\n meta?: { noindex?: boolean } | null\n slug?: unknown\n updatedAt?: string\n}\n\n/**\n * Slugs that must never appear in the sitemap — error/system pages that exist as\n * documents (e.g. a '404' page in the Pages collection) but should not be\n * indexed. A sitemap should list only real, HTTP-200 content; a '/pl/404' entry\n * is an audit finding. Matched against the slug in any locale.\n */\nconst EXCLUDED_SITEMAP_SLUGS = new Set(['404', '500', 'error', 'not-found'])\n\n/** True if the doc's slug (in any locale) is an excluded system/error slug. */\nfunction hasExcludedSlug(slug: unknown): boolean {\n if (typeof slug === 'string') {return EXCLUDED_SITEMAP_SLUGS.has(slug)}\n if (slug && typeof slug === 'object') {\n for (const value of Object.values(slug as Record)) {\n if (typeof value === 'string' && EXCLUDED_SITEMAP_SLUGS.has(value)) {return true}\n }\n }\n return false\n}\n\n/** Skip drafts, noindex, and system/error pages (404 etc.). */\nfunction isIndexable(doc: DocRow): boolean {\n if (doc._status && doc._status !== 'published') {return false}\n if (doc.meta?.noindex) {return false}\n if (hasExcludedSlug(doc.slug)) {return false}\n return true\n}\n\n/**\n * Turns one document into a sitemap entry per default locale isn't needed —\n * one entry with all languages as alternates is the correct, de-duplicated\n * shape. The `url` is the current locale's path; `alternates.languages` carries\n * every locale (including itself, per Google's guidance).\n */\nfunction entryFor(\n doc: DocRow,\n locale: string,\n config: I18nConfig,\n baseUrl: string,\n homeSlug: Record | string | undefined,\n prefix: Record | undefined,\n changeFrequency: SitemapEntry['changeFrequency'],\n): null | SitemapEntry {\n const slugs =\n doc.slug && typeof doc.slug === 'object'\n ? getLocalizedSlugs({ config, slugField: doc.slug as Record })\n : {}\n\n const path = buildLocalizedPath({ config, homeSlug, locale, prefix, slugs })\n if (!path) {return null}\n\n const origin = baseUrl.replace(/\\/$/, '')\n const languages = buildHreflangAlternates({ baseUrl, config, homeSlug, prefix, slugs })\n\n return {\n url: `${origin}${path}`,\n ...(doc.updatedAt ? { lastModified: doc.updatedAt } : {}),\n ...(changeFrequency ? { changeFrequency } : {}),\n ...(Object.keys(languages).length ? { alternates: { languages } } : {}),\n }\n}\n\n/**\n * Collects every public URL — pages and archive entries — as sitemap entries\n * with per-URL hreflang and lastmod.\n *\n * Reuses the same path/hreflang builders as page metadata, so the sitemap can't\n * drift from what the pages actually render (a classic source of sitemap bugs:\n * a URL listed one way and served another). Drafts and noindex documents are\n * omitted.\n *\n * ```ts\n * // app/sitemap.ts\n * export default async function sitemap() {\n * return buildSitemapEntries({\n * payload: await getPayload({ config }),\n * config: i18nConfig,\n * baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!,\n * content: contentConfig,\n * })\n * }\n * ```\n */\nexport async function buildSitemapEntries({\n baseUrl,\n changeFrequency = 'weekly',\n config,\n content,\n homeSlug,\n pagesSlug = 'pages',\n payload,\n settingsSlug = 'site-settings',\n}: BuildSitemapArgs): Promise {\n const locales = config.locales.map((l) => l.code)\n const defaultLocale = config.defaultLocale\n\n // Resolve homeSlug and archive prefixes from System Pages (read once, in all\n // locales so archive prefixes are available per language).\n const settings = (await payload.findGlobal({\n slug: settingsSlug as never,\n depth: 1,\n locale: 'all' as never,\n })) as Record\n\n const resolvedHomeSlug =\n homeSlug ?? extractSlugInLocale(settings.homepage, defaultLocale) ?? 'home'\n\n // Which collections to walk: pages (no prefix) + each content collection with\n // its archive-page slugs as the localized prefix.\n const collections: CollectionEntry[] = [{ slug: pagesSlug }]\n\n for (const c of content?.collections ?? []) {\n const archive = settings[archiveFieldName(c.slug)]\n const prefixSlugs = archive ? slugMapAllLocales(archive, locales) : undefined\n collections.push({ slug: c.slug, prefixSlugs })\n }\n\n const entries: SitemapEntry[] = []\n\n for (const collection of collections) {\n // Read every doc once, across locales, so one row yields all its language\n // alternates without re-querying per locale.\n const result = await payload.find({\n collection: collection.slug as never,\n depth: 0,\n limit: 0, // no pagination — sitemap wants everything\n locale: 'all' as never,\n pagination: false as never,\n })\n\n for (const raw of result.docs as DocRow[]) {\n if (!isIndexable(raw)) {continue}\n\n // Emit a separate for EACH locale. Google's sitemap spec for\n // localized sites requires one per language version (each with its\n // own plus xhtml:link alternates), not a single default-locale \n // with the others hidden only in alternates. Emitting only the default\n // locale makes GSC count 8 URLs instead of 8×3 — a real reporting gap.\n for (const locale of getLocaleCodes(config)) {\n const entry = entryFor(\n raw,\n locale,\n config,\n baseUrl,\n resolvedHomeSlug,\n collection.prefixSlugs,\n changeFrequency,\n )\n if (entry) {entries.push(entry)}\n }\n }\n }\n\n return entries\n}\n\n/** Pulls a slug string from a populated relationship in a specific locale. */\nfunction extractSlugInLocale(rel: unknown, locale: string): string | undefined {\n if (!rel || typeof rel !== 'object') {return undefined}\n const slug = (rel as { slug?: unknown }).slug\n if (typeof slug === 'string') {return slug}\n if (slug && typeof slug === 'object') {\n const v = (slug as Record)[locale]\n return typeof v === 'string' ? v : undefined\n }\n return undefined\n}\n\n/** Builds a locale→slug map from a populated archive relationship. */\nfunction slugMapAllLocales(rel: unknown, locales: string[]): Record | undefined {\n if (!rel || typeof rel !== 'object') {return undefined}\n const slug = (rel as { slug?: unknown }).slug\n if (!slug || typeof slug !== 'object') {return undefined}\n\n const map: Record = {}\n for (const locale of locales) {\n const v = (slug as Record)[locale]\n if (typeof v === 'string') {map[locale] = v}\n }\n return Object.keys(map).length ? map : undefined\n}\n"],"names":["archiveFieldName","buildLocalizedPath","getLocaleCodes","getLocalizedSlugs","buildHreflangAlternates","EXCLUDED_SITEMAP_SLUGS","Set","hasExcludedSlug","slug","has","value","Object","values","isIndexable","doc","_status","meta","noindex","entryFor","locale","config","baseUrl","homeSlug","prefix","changeFrequency","slugs","slugField","path","origin","replace","languages","url","updatedAt","lastModified","keys","length","alternates","buildSitemapEntries","content","pagesSlug","payload","settingsSlug","locales","map","l","code","defaultLocale","settings","findGlobal","depth","resolvedHomeSlug","extractSlugInLocale","homepage","collections","c","archive","prefixSlugs","slugMapAllLocales","undefined","push","entries","collection","result","find","limit","pagination","raw","docs","entry","rel","v"],"mappings":"AAKA,SAASA,gBAAgB,QAAQ,sBAAqB;AACtD,SAASC,kBAAkB,EAAEC,cAAc,EAAGC,iBAAiB,QAAS,mBAAkB;AAC1F,SAASC,uBAAuB,QAAQ,gBAAe;AAmDvD;;;;;CAKC,GACD,MAAMC,yBAAyB,IAAIC,IAAI;IAAC;IAAO;IAAO;IAAS;CAAY;AAE3E,6EAA6E,GAC7E,SAASC,gBAAgBC,IAAa;IACpC,IAAI,OAAOA,SAAS,UAAU;QAAC,OAAOH,uBAAuBI,GAAG,CAACD;IAAK;IACtE,IAAIA,QAAQ,OAAOA,SAAS,UAAU;QACpC,KAAK,MAAME,SAASC,OAAOC,MAAM,CAACJ,MAAkC;YAClE,IAAI,OAAOE,UAAU,YAAYL,uBAAuBI,GAAG,CAACC,QAAQ;gBAAC,OAAO;YAAI;QAClF;IACF;IACA,OAAO;AACT;AAEA,6DAA6D,GAC7D,SAASG,YAAYC,GAAW;IAC9B,IAAIA,IAAIC,OAAO,IAAID,IAAIC,OAAO,KAAK,aAAa;QAAC,OAAO;IAAK;IAC7D,IAAID,IAAIE,IAAI,EAAEC,SAAS;QAAC,OAAO;IAAK;IACpC,IAAIV,gBAAgBO,IAAIN,IAAI,GAAG;QAAC,OAAO;IAAK;IAC5C,OAAO;AACT;AAEA;;;;;CAKC,GACD,SAASU,SACPJ,GAAW,EACXK,MAAc,EACdC,MAAkB,EAClBC,OAAe,EACfC,QAAqD,EACrDC,MAA0C,EAC1CC,eAAgD;IAEhD,MAAMC,QACJX,IAAIN,IAAI,IAAI,OAAOM,IAAIN,IAAI,KAAK,WAC5BL,kBAAkB;QAAEiB;QAAQM,WAAWZ,IAAIN,IAAI;IAA4B,KAC3E,CAAC;IAEP,MAAMmB,OAAO1B,mBAAmB;QAAEmB;QAAQE;QAAUH;QAAQI;QAAQE;IAAM;IAC1E,IAAI,CAACE,MAAM;QAAC,OAAO;IAAI;IAEvB,MAAMC,SAASP,QAAQQ,OAAO,CAAC,OAAO;IACtC,MAAMC,YAAY1B,wBAAwB;QAAEiB;QAASD;QAAQE;QAAUC;QAAQE;IAAM;IAErF,OAAO;QACLM,KAAK,GAAGH,SAASD,MAAM;QACvB,GAAIb,IAAIkB,SAAS,GAAG;YAAEC,cAAcnB,IAAIkB,SAAS;QAAC,IAAI,CAAC,CAAC;QACxD,GAAIR,kBAAkB;YAAEA;QAAgB,IAAI,CAAC,CAAC;QAC9C,GAAIb,OAAOuB,IAAI,CAACJ,WAAWK,MAAM,GAAG;YAAEC,YAAY;gBAAEN;YAAU;QAAE,IAAI,CAAC,CAAC;IACxE;AACF;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,eAAeO,oBAAoB,EACxChB,OAAO,EACPG,kBAAkB,QAAQ,EAC1BJ,MAAM,EACNkB,OAAO,EACPhB,QAAQ,EACRiB,YAAY,OAAO,EACnBC,OAAO,EACPC,eAAe,eAAe,EACb;IACjB,MAAMC,UAAUtB,OAAOsB,OAAO,CAACC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI;IAChD,MAAMC,gBAAgB1B,OAAO0B,aAAa;IAE1C,6EAA6E;IAC7E,2DAA2D;IAC3D,MAAMC,WAAY,MAAMP,QAAQQ,UAAU,CAAC;QACzCxC,MAAMiC;QACNQ,OAAO;QACP9B,QAAQ;IACV;IAEA,MAAM+B,mBACJ5B,YAAY6B,oBAAoBJ,SAASK,QAAQ,EAAEN,kBAAkB;IAEvE,8EAA8E;IAC9E,kDAAkD;IAClD,MAAMO,cAAiC;QAAC;YAAE7C,MAAM+B;QAAU;KAAE;IAE5D,KAAK,MAAMe,KAAKhB,SAASe,eAAe,EAAE,CAAE;QAC1C,MAAME,UAAUR,QAAQ,CAAC/C,iBAAiBsD,EAAE9C,IAAI,EAAE;QAClD,MAAMgD,cAAcD,UAAUE,kBAAkBF,SAASb,WAAWgB;QACpEL,YAAYM,IAAI,CAAC;YAAEnD,MAAM8C,EAAE9C,IAAI;YAAEgD;QAAY;IAC/C;IAEA,MAAMI,UAA0B,EAAE;IAElC,KAAK,MAAMC,cAAcR,YAAa;QACpC,0EAA0E;QAC1E,6CAA6C;QAC7C,MAAMS,SAAS,MAAMtB,QAAQuB,IAAI,CAAC;YAChCF,YAAYA,WAAWrD,IAAI;YAC3ByC,OAAO;YACPe,OAAO;YACP7C,QAAQ;YACR8C,YAAY;QACd;QAEA,KAAK,MAAMC,OAAOJ,OAAOK,IAAI,CAAc;YACzC,IAAI,CAACtD,YAAYqD,MAAM;gBAAC;YAAQ;YAEhC,mEAAmE;YACnE,yEAAyE;YACzE,2EAA2E;YAC3E,uEAAuE;YACvE,uEAAuE;YACvE,KAAK,MAAM/C,UAAUjB,eAAekB,QAAS;gBAC3C,MAAMgD,QAAQlD,SACZgD,KACA/C,QACAC,QACAC,SACA6B,kBACAW,WAAWL,WAAW,EACtBhC;gBAEF,IAAI4C,OAAO;oBAACR,QAAQD,IAAI,CAACS;gBAAM;YACjC;QACF;IACF;IAEA,OAAOR;AACT;AAEA,4EAA4E,GAC5E,SAAST,oBAAoBkB,GAAY,EAAElD,MAAc;IACvD,IAAI,CAACkD,OAAO,OAAOA,QAAQ,UAAU;QAAC,OAAOX;IAAS;IACtD,MAAMlD,OAAO,AAAC6D,IAA2B7D,IAAI;IAC7C,IAAI,OAAOA,SAAS,UAAU;QAAC,OAAOA;IAAI;IAC1C,IAAIA,QAAQ,OAAOA,SAAS,UAAU;QACpC,MAAM8D,IAAI,AAAC9D,IAAgC,CAACW,OAAO;QACnD,OAAO,OAAOmD,MAAM,WAAWA,IAAIZ;IACrC;IACA,OAAOA;AACT;AAEA,oEAAoE,GACpE,SAASD,kBAAkBY,GAAY,EAAE3B,OAAiB;IACxD,IAAI,CAAC2B,OAAO,OAAOA,QAAQ,UAAU;QAAC,OAAOX;IAAS;IACtD,MAAMlD,OAAO,AAAC6D,IAA2B7D,IAAI;IAC7C,IAAI,CAACA,QAAQ,OAAOA,SAAS,UAAU;QAAC,OAAOkD;IAAS;IAExD,MAAMf,MAA8B,CAAC;IACrC,KAAK,MAAMxB,UAAUuB,QAAS;QAC5B,MAAM4B,IAAI,AAAC9D,IAAgC,CAACW,OAAO;QACnD,IAAI,OAAOmD,MAAM,UAAU;YAAC3B,GAAG,CAACxB,OAAO,GAAGmD;QAAC;IAC7C;IACA,OAAO3D,OAAOuB,IAAI,CAACS,KAAKR,MAAM,GAAGQ,MAAMe;AACzC"} \ No newline at end of file diff --git a/dist/modules/seo/composeTitle.d.ts b/dist/modules/seo/composeTitle.d.ts index 3c92e24..53f87ef 100644 --- a/dist/modules/seo/composeTitle.d.ts +++ b/dist/modules/seo/composeTitle.d.ts @@ -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 {}; diff --git a/dist/modules/seo/composeTitle.js b/dist/modules/seo/composeTitle.js index 5d5b958..e19b4cd 100644 --- a/dist/modules/seo/composeTitle.js +++ b/dist/modules/seo/composeTitle.js @@ -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 || ''; diff --git a/dist/modules/seo/composeTitle.js.map b/dist/modules/seo/composeTitle.js.map index bfefe1e..007b0d5 100644 --- a/dist/modules/seo/composeTitle.js.map +++ b/dist/modules/seo/composeTitle.js.map @@ -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"} \ No newline at end of file +{"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"} \ No newline at end of file diff --git a/dist/modules/seo/createMetadataGenerator.d.ts b/dist/modules/seo/createMetadataGenerator.d.ts index ffe4467..e484cee 100644 --- a/dist/modules/seo/createMetadataGenerator.d.ts +++ b/dist/modules/seo/createMetadataGenerator.d.ts @@ -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; /** * Resolves the document to build metadata for, given route params. * The client supplies this (they own the collections and routing); it should diff --git a/dist/modules/seo/createMetadataGenerator.js.map b/dist/modules/seo/createMetadataGenerator.js.map index bd001e5..a935a9e 100644 --- a/dist/modules/seo/createMetadataGenerator.js.map +++ b/dist/modules/seo/createMetadataGenerator.js.map @@ -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\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\n payload: BasePayload\n }) => Promise\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\n /** Resolves the site name (e.g. from SiteSettings). */\n resolveSiteName?: (args: { locale: string; payload: BasePayload }) => Promise\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\n payload: BasePayload\n }): Promise {\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"} \ No newline at end of file +{"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\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\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\n payload: BasePayload\n }) => Promise\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\n /** Resolves the site name (e.g. from SiteSettings). */\n resolveSiteName?: (args: { locale: string; payload: BasePayload }) => Promise\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\n payload: BasePayload\n }): Promise {\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"} \ No newline at end of file diff --git a/dist/modules/seo/hreflang.d.ts b/dist/modules/seo/hreflang.d.ts index 101e0be..11497a9 100644 --- a/dist/modules/seo/hreflang.d.ts +++ b/dist/modules/seo/hreflang.d.ts @@ -6,7 +6,7 @@ type BuildHreflangArgs = { /** Absolute site origin, e.g. 'https://example.com'. Omit for relative paths. */ baseUrl?: string; /** Home slug that collapses to the locale root. Defaults to 'home'. */ - homeSlug?: string; + homeSlug?: string | Record; /** * Localized segment the document lives under (an archive page's slugs), * e.g. { pl: 'artykuly', en: 'articles' }. Locales missing from the prefix diff --git a/dist/modules/seo/hreflang.js.map b/dist/modules/seo/hreflang.js.map index 8b0a2c5..bfa282d 100644 --- a/dist/modules/seo/hreflang.js.map +++ b/dist/modules/seo/hreflang.js.map @@ -1 +1 @@ -{"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\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 {\n const origin = baseUrl?.replace(/\\/$/, '') ?? ''\n const alternates: Record = {}\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"} \ No newline at end of file +{"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\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 {\n const origin = baseUrl?.replace(/\\/$/, '') ?? ''\n const alternates: Record = {}\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"} \ No newline at end of file diff --git a/dist/modules/seo/index.d.ts b/dist/modules/seo/index.d.ts index adabd71..89b318b 100644 --- a/dist/modules/seo/index.d.ts +++ b/dist/modules/seo/index.d.ts @@ -1,5 +1,6 @@ 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'; diff --git a/dist/modules/seo/index.js b/dist/modules/seo/index.js index f7b25d2..f014582 100644 --- a/dist/modules/seo/index.js +++ b/dist/modules/seo/index.js @@ -1,4 +1,5 @@ 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'; diff --git a/dist/modules/seo/index.js.map b/dist/modules/seo/index.js.map index 5211e11..4081dce 100644 --- a/dist/modules/seo/index.js.map +++ b/dist/modules/seo/index.js.map @@ -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"} \ No newline at end of file +{"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 { 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","buildArticleJsonLd","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,kBAAkB,QAAQ,0BAAyB;AAC5D,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"} \ No newline at end of file diff --git a/dist/modules/seo/readSiteMetaConfig.d.ts b/dist/modules/seo/readSiteMetaConfig.d.ts index a7ca12b..f8fbc7c 100644 --- a/dist/modules/seo/readSiteMetaConfig.d.ts +++ b/dist/modules/seo/readSiteMetaConfig.d.ts @@ -1,12 +1,17 @@ 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; - order?: TitleOrder; + siteName: string | null; /** Padded separator, e.g. ' | ' — the panel stores the bare character. */ separator?: string; - siteName: null | string; + order?: TitleOrder; + /** + * 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; }; /** * Reads the site-wide inputs to title and URL composition from SiteSettings. @@ -16,9 +21,9 @@ export type SiteMetaConfig = { * that the separator arrives unpadded, or that the home slug hides inside a * relationship — it should receive a resolved config. */ -export declare function readSiteMetaConfig({ locale, payload, settingsSlug, siteNameField, }: { - locale: string; +export declare function readSiteMetaConfig({ payload, locale, settingsSlug, siteNameField, }: { payload: BasePayload; + locale: string; settingsSlug?: string; siteNameField?: string; }): Promise; diff --git a/dist/modules/seo/readSiteMetaConfig.js b/dist/modules/seo/readSiteMetaConfig.js index f6f8b61..699d0c5 100644 --- a/dist/modules/seo/readSiteMetaConfig.js +++ b/dist/modules/seo/readSiteMetaConfig.js @@ -5,21 +5,60 @@ * whatever an editor set in the panel. Metadata generation shouldn't also know * that the separator arrives unpadded, or that the home slug hides inside a * relationship — it should receive a resolved config. - */ export async function readSiteMetaConfig({ locale, payload, settingsSlug = 'site-settings', siteNameField = 'siteName' }) { + */ export async function readSiteMetaConfig({ payload, locale, settingsSlug = 'site-settings', siteNameField = 'siteName' }) { // depth 1 populates the homepage relationship, so its slug is available // without a second query. const settings = await payload.findGlobal({ slug: settingsSlug, - depth: 1, - locale: locale + locale: locale, + depth: 1 }); 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({ + collection: 'pages', + id: homepage.id, + locale: 'all', + depth: 0 + }); + 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 { siteName: settings[siteNameField] ?? null, // The panel stores '|'; titles need it padded. - homeSlug: typeof homepage?.slug === 'string' ? homepage.slug : undefined, + separator: settings.titleSeparator ? ` ${settings.titleSeparator} ` : undefined, order: settings.titleOrder ?? undefined, - separator: settings.titleSeparator ? ` ${settings.titleSeparator} ` : undefined + homeSlug }; } diff --git a/dist/modules/seo/readSiteMetaConfig.js.map b/dist/modules/seo/readSiteMetaConfig.js.map index 940557a..f408239 100644 --- a/dist/modules/seo/readSiteMetaConfig.js.map +++ b/dist/modules/seo/readSiteMetaConfig.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../../src/modules/seo/readSiteMetaConfig.ts"],"sourcesContent":["import type { BasePayload } from 'payload'\n\nimport type { TitleOrder } from './composeTitle.js'\n\ntype SettingsShape = {\n [key: string]: unknown\n homepage?: { id: number | string; slug?: unknown } | null | number | string\n titleOrder?: null | TitleOrder\n titleSeparator?: null | string\n}\n\nexport type SiteMetaConfig = {\n /** Slug of the page assigned as Homepage; collapses to the locale root. */\n homeSlug?: string\n order?: TitleOrder\n /** Padded separator, e.g. ' | ' — the panel stores the bare character. */\n separator?: string\n siteName: null | string\n}\n\n/**\n * Reads the site-wide inputs to title and URL composition from SiteSettings.\n *\n * Split out of metadata assembly because these are one concern with one source:\n * whatever an editor set in the panel. Metadata generation shouldn't also know\n * that the separator arrives unpadded, or that the home slug hides inside a\n * relationship — it should receive a resolved config.\n */\nexport async function readSiteMetaConfig({\n locale,\n payload,\n settingsSlug = 'site-settings',\n siteNameField = 'siteName',\n}: {\n locale: string\n payload: BasePayload\n settingsSlug?: string\n siteNameField?: string\n}): Promise {\n // depth 1 populates the homepage relationship, so its slug is available\n // without a second query.\n const settings = (await payload.findGlobal({\n slug: settingsSlug as never,\n depth: 1,\n locale: locale as never,\n })) as SettingsShape\n\n const homepage =\n settings.homepage && typeof settings.homepage === 'object' ? settings.homepage : null\n\n return {\n siteName: (settings[siteNameField] as string | undefined) ?? null,\n // The panel stores '|'; titles need it padded.\n homeSlug: typeof homepage?.slug === 'string' ? homepage.slug : undefined,\n order: settings.titleOrder ?? undefined,\n separator: settings.titleSeparator ? ` ${settings.titleSeparator} ` : undefined,\n }\n}\n"],"names":["readSiteMetaConfig","locale","payload","settingsSlug","siteNameField","settings","findGlobal","slug","depth","homepage","siteName","homeSlug","undefined","order","titleOrder","separator","titleSeparator"],"mappings":"AAoBA;;;;;;;CAOC,GACD,OAAO,eAAeA,mBAAmB,EACvCC,MAAM,EACNC,OAAO,EACPC,eAAe,eAAe,EAC9BC,gBAAgB,UAAU,EAM3B;IACC,wEAAwE;IACxE,0BAA0B;IAC1B,MAAMC,WAAY,MAAMH,QAAQI,UAAU,CAAC;QACzCC,MAAMJ;QACNK,OAAO;QACPP,QAAQA;IACV;IAEA,MAAMQ,WACJJ,SAASI,QAAQ,IAAI,OAAOJ,SAASI,QAAQ,KAAK,WAAWJ,SAASI,QAAQ,GAAG;IAEnF,OAAO;QACLC,UAAU,AAACL,QAAQ,CAACD,cAAc,IAA2B;QAC7D,+CAA+C;QAC/CO,UAAU,OAAOF,UAAUF,SAAS,WAAWE,SAASF,IAAI,GAAGK;QAC/DC,OAAOR,SAASS,UAAU,IAAIF;QAC9BG,WAAWV,SAASW,cAAc,GAAG,CAAC,CAAC,EAAEX,SAASW,cAAc,CAAC,CAAC,CAAC,GAAGJ;IACxE;AACF"} \ No newline at end of file +{"version":3,"sources":["../../../src/modules/seo/readSiteMetaConfig.ts"],"sourcesContent":["import type { BasePayload } from 'payload'\nimport type { TitleOrder } from './composeTitle.js'\n\ntype SettingsShape = {\n homepage?: { id: string | number; slug?: unknown } | string | number | null\n titleSeparator?: string | null\n titleOrder?: TitleOrder | null\n [key: string]: unknown\n}\n\nexport type SiteMetaConfig = {\n siteName: string | null\n /** Padded separator, e.g. ' | ' — the panel stores the bare character. */\n separator?: string\n order?: TitleOrder\n /**\n * Home slug per locale, e.g. { pl: 'strona-glowna', de: 'startseite' }. A map\n * (not a single string) so multilingual homepages collapse to root in EVERY\n * language — otherwise hreflang points other languages at '/de/startseite'\n * instead of '/de', breaking return tags (a real GSC error).\n */\n homeSlug?: Record\n}\n\n/**\n * Reads the site-wide inputs to title and URL composition from SiteSettings.\n *\n * Split out of metadata assembly because these are one concern with one source:\n * whatever an editor set in the panel. Metadata generation shouldn't also know\n * that the separator arrives unpadded, or that the home slug hides inside a\n * relationship — it should receive a resolved config.\n */\nexport async function readSiteMetaConfig({\n payload,\n locale,\n settingsSlug = 'site-settings',\n siteNameField = 'siteName',\n }: {\n payload: BasePayload\n locale: string\n settingsSlug?: string\n siteNameField?: string\n}): Promise {\n // depth 1 populates the homepage relationship, so its slug is available\n // without a second query.\n const settings = (await payload.findGlobal({\n slug: settingsSlug as never,\n locale: locale as never,\n depth: 1,\n })) as SettingsShape\n\n const homepage =\n settings.homepage && typeof settings.homepage === 'object' ? settings.homepage : null\n\n // Build the per-locale home slug map. The homepage's slug is localized, so a\n // single-locale read only gives THIS language's slug. Re-read the homepage\n // with locale:'all' to get every language's slug — needed so hreflang can\n // collapse each language's home to its root.\n let homeSlug: Record | undefined\n if (homepage?.id) {\n try {\n const homeDoc = (await payload.findByID({\n collection: 'pages' as never,\n id: homepage.id,\n locale: 'all' as never,\n depth: 0,\n })) as { slug?: unknown }\n const slugField = homeDoc.slug\n if (slugField && typeof slugField === 'object') {\n const map: Record = {}\n for (const [loc, value] of Object.entries(slugField as Record)) {\n if (typeof value === 'string') map[loc] = value\n }\n if (Object.keys(map).length > 0) homeSlug = map\n } else if (typeof slugField === 'string') {\n // Non-localized slug (same in every language) — key it by current locale;\n // buildLocalizedPath tolerates a partial map. Non-localized homepage\n // slugs are rare, so this branch is a safety net, not the norm.\n homeSlug = { [locale]: slugField }\n }\n } catch {\n // Fall back to the single-locale slug below.\n }\n }\n\n // Fallback: if the 'all' read didn't yield a map, use the current locale's slug\n // for at least this locale (better than nothing).\n if (!homeSlug && typeof homepage?.slug === 'string') {\n homeSlug = { [locale]: homepage.slug }\n }\n\n return {\n siteName: (settings[siteNameField] as string | undefined) ?? null,\n // The panel stores '|'; titles need it padded.\n separator: settings.titleSeparator ? ` ${settings.titleSeparator} ` : undefined,\n order: settings.titleOrder ?? undefined,\n homeSlug,\n }\n}"],"names":["readSiteMetaConfig","payload","locale","settingsSlug","siteNameField","settings","findGlobal","slug","depth","homepage","homeSlug","id","homeDoc","findByID","collection","slugField","map","loc","value","Object","entries","keys","length","siteName","separator","titleSeparator","undefined","order","titleOrder"],"mappings":"AAwBA;;;;;;;CAOC,GACD,OAAO,eAAeA,mBAAmB,EACEC,OAAO,EACPC,MAAM,EACNC,eAAe,eAAe,EAC9BC,gBAAgB,UAAU,EAMpE;IACC,wEAAwE;IACxE,0BAA0B;IAC1B,MAAMC,WAAY,MAAMJ,QAAQK,UAAU,CAAC;QACzCC,MAAMJ;QACND,QAAQA;QACRM,OAAO;IACT;IAEA,MAAMC,WACJJ,SAASI,QAAQ,IAAI,OAAOJ,SAASI,QAAQ,KAAK,WAAWJ,SAASI,QAAQ,GAAG;IAEnF,6EAA6E;IAC7E,2EAA2E;IAC3E,0EAA0E;IAC1E,6CAA6C;IAC7C,IAAIC;IACJ,IAAID,UAAUE,IAAI;QAChB,IAAI;YACF,MAAMC,UAAW,MAAMX,QAAQY,QAAQ,CAAC;gBACtCC,YAAY;gBACZH,IAAIF,SAASE,EAAE;gBACfT,QAAQ;gBACRM,OAAO;YACT;YACA,MAAMO,YAAYH,QAAQL,IAAI;YAC9B,IAAIQ,aAAa,OAAOA,cAAc,UAAU;gBAC9C,MAAMC,MAA8B,CAAC;gBACrC,KAAK,MAAM,CAACC,KAAKC,MAAM,IAAIC,OAAOC,OAAO,CAACL,WAAuC;oBAC/E,IAAI,OAAOG,UAAU,UAAUF,GAAG,CAACC,IAAI,GAAGC;gBAC5C;gBACA,IAAIC,OAAOE,IAAI,CAACL,KAAKM,MAAM,GAAG,GAAGZ,WAAWM;YAC9C,OAAO,IAAI,OAAOD,cAAc,UAAU;gBACxC,0EAA0E;gBAC1E,qEAAqE;gBACrE,gEAAgE;gBAChEL,WAAW;oBAAE,CAACR,OAAO,EAAEa;gBAAU;YACnC;QACF,EAAE,OAAM;QACN,6CAA6C;QAC/C;IACF;IAEA,gFAAgF;IAChF,kDAAkD;IAClD,IAAI,CAACL,YAAY,OAAOD,UAAUF,SAAS,UAAU;QACnDG,WAAW;YAAE,CAACR,OAAO,EAAEO,SAASF,IAAI;QAAC;IACvC;IAEA,OAAO;QACLgB,UAAU,AAAClB,QAAQ,CAACD,cAAc,IAA2B;QAC7D,+CAA+C;QAC/CoB,WAAWnB,SAASoB,cAAc,GAAG,CAAC,CAAC,EAAEpB,SAASoB,cAAc,CAAC,CAAC,CAAC,GAAGC;QACtEC,OAAOtB,SAASuB,UAAU,IAAIF;QAC9BhB;IACF;AACF"} \ No newline at end of file diff --git a/docs/seo.md b/docs/seo.md index d7031c4..52d4259 100644 --- a/docs/seo.md +++ b/docs/seo.md @@ -784,4 +784,61 @@ const jsonLd = buildFaqJsonLd( WAŻNE: Q&A musi odpowiadać widocznej treści strony (Google flaguje rozbieżność). Nie wymyślaj pytań, których nie ma na stronie. -Wszystkie: dane z panelu (company, bloki), jako `