diff --git a/dist/globals/SiteSettings/fields/general.d.ts b/dist/globals/SiteSettings/fields/general.d.ts index 78ed235..0a7c294 100644 --- a/dist/globals/SiteSettings/fields/general.d.ts +++ b/dist/globals/SiteSettings/fields/general.d.ts @@ -1,6 +1,6 @@ import type { Field } from 'payload'; /** * General site identity fields. - * Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo, favicon). + * Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo). */ export declare const generalFields: Field[]; diff --git a/dist/globals/SiteSettings/fields/general.js b/dist/globals/SiteSettings/fields/general.js index 34f2ac9..6f6023a 100644 --- a/dist/globals/SiteSettings/fields/general.js +++ b/dist/globals/SiteSettings/fields/general.js @@ -1,7 +1,7 @@ import { validateFaviconField } from '../../../modules/seo/index.js'; /** * General site identity fields. - * Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo, favicon). + * Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo). */ export const generalFields = [ { name: 'siteName', @@ -12,6 +12,14 @@ import { validateFaviconField } from '../../../modules/seo/index.js'; localized: true, required: true }, + { + name: 'siteDescription', + type: 'textarea', + admin: { + description: 'Fallback meta description when a page has none. Also used for llms.txt and Open Graph.' + }, + localized: true + }, { name: 'titleOrder', type: 'select', @@ -64,7 +72,7 @@ import { validateFaviconField } from '../../../modules/seo/index.js'; name: 'logo', type: 'upload', admin: { - description: 'Primary site logo. Also used for Organization structured data (buildOrganizationJsonLd).' + description: 'Primary site logo.' }, relationTo: 'media' }, @@ -83,8 +91,7 @@ import { validateFaviconField } from '../../../modules/seo/index.js'; description: 'Square icon, PNG or SVG, min. 48×48px (Google requires this to show it in search). Rendered via buildIconsMetadata.' }, hooks: { - // Warns the editor at save time if the favicon is too small (<48×48) or - // not square — Google won't display such favicons in search results. + // Warn the editor if the favicon is too small / not square for Google. beforeValidate: [ validateFaviconField ] diff --git a/dist/globals/SiteSettings/fields/general.js.map b/dist/globals/SiteSettings/fields/general.js.map index c8eefd2..28b16a7 100644 --- a/dist/globals/SiteSettings/fields/general.js.map +++ b/dist/globals/SiteSettings/fields/general.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../../../src/globals/SiteSettings/fields/general.ts"],"sourcesContent":["import type { Field } from 'payload'\n\nimport { validateFaviconField } from '../../../modules/seo/index.js'\n\n/**\n * General site identity fields.\n * Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo, favicon).\n */\nexport const generalFields: Field[] = [\n {\n name: 'siteName',\n type: 'text',\n admin: {\n description: 'Used in page titles and Open Graph metadata.',\n },\n localized: true,\n required: true,\n },\n {\n name: 'titleOrder',\n type: 'select',\n admin: {\n description: 'Which comes first in browser tabs.',\n },\n defaultValue: 'page-first',\n options: [\n { label: 'Page first — About Us | Acme', value: 'page-first' },\n { label: 'Site first — Acme | About Us', value: 'site-first' },\n ],\n },\n {\n name: 'titleSeparator',\n type: 'select',\n admin: {\n description: 'Separates the page title from the site name in browser tabs.',\n },\n defaultValue: '|',\n options: [\n { label: 'Pipe — Page | Site', value: '|' },\n { label: 'Dash — Page – Site', value: '–' },\n { label: 'Hyphen — Page - Site', value: '-' },\n { label: 'Bullet — Page · Site', value: '·' },\n { label: 'Slash — Page / Site', value: '/' },\n ],\n },\n {\n name: 'logo',\n type: 'upload',\n admin: {\n description:\n 'Primary site logo. Also used for Organization structured data (buildOrganizationJsonLd).',\n },\n relationTo: 'media',\n },\n {\n name: 'defaultShareImage',\n type: 'upload',\n admin: {\n description: 'Fallback Open Graph image when a page has none.',\n },\n relationTo: 'media',\n },\n {\n name: 'favicon',\n type: 'upload',\n admin: {\n description:\n 'Square icon, PNG or SVG, min. 48×48px (Google requires this to show it in search). Rendered via buildIconsMetadata.',\n },\n hooks: {\n // Warns the editor at save time if the favicon is too small (<48×48) or\n // not square — Google won't display such favicons in search results.\n beforeValidate: [validateFaviconField],\n },\n relationTo: 'media',\n },\n]\n"],"names":["validateFaviconField","generalFields","name","type","admin","description","localized","required","defaultValue","options","label","value","relationTo","hooks","beforeValidate"],"mappings":"AAEA,SAASA,oBAAoB,QAAQ,gCAA+B;AAEpE;;;CAGC,GACD,OAAO,MAAMC,gBAAyB;IACpC;QACEC,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAC,WAAW;QACXC,UAAU;IACZ;IACA;QACEL,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAG,cAAc;QACdC,SAAS;YACP;gBAAEC,OAAO;gBAAkCC,OAAO;YAAa;YAC/D;gBAAED,OAAO;gBAAkCC,OAAO;YAAa;SAChE;IACH;IACA;QACET,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAG,cAAc;QACdC,SAAS;YACP;gBAAEC,OAAO;gBAAwBC,OAAO;YAAI;YAC5C;gBAAED,OAAO;gBAAwBC,OAAO;YAAI;YAC5C;gBAAED,OAAO;gBAA0BC,OAAO;YAAI;YAC9C;gBAAED,OAAO;gBAA0BC,OAAO;YAAI;YAC9C;gBAAED,OAAO;gBAAyBC,OAAO;YAAI;SAC9C;IACH;IACA;QACET,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aACE;QACJ;QACAO,YAAY;IACd;IACA;QACEV,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAO,YAAY;IACd;IACA;QACEV,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aACE;QACJ;QACAQ,OAAO;YACL,wEAAwE;YACxE,qEAAqE;YACrEC,gBAAgB;gBAACd;aAAqB;QACxC;QACAY,YAAY;IACd;CACD,CAAA"} \ No newline at end of file +{"version":3,"sources":["../../../../src/globals/SiteSettings/fields/general.ts"],"sourcesContent":["import type { Field } from 'payload'\n\nimport { validateFaviconField } from '../../../modules/seo/index.js'\n\n/**\n * General site identity fields.\n * Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo).\n */\nexport const generalFields: Field[] = [\n {\n name: 'siteName',\n type: 'text',\n admin: {\n description: 'Used in page titles and Open Graph metadata.',\n },\n localized: true,\n required: true,\n },\n {\n name: 'siteDescription',\n type: 'textarea',\n admin: {\n description:\n 'Fallback meta description when a page has none. Also used for llms.txt and Open Graph.',\n },\n localized: true,\n },\n {\n name: 'titleOrder',\n type: 'select',\n admin: {\n description: 'Which comes first in browser tabs.',\n },\n defaultValue: 'page-first',\n options: [\n { label: 'Page first — About Us | Acme', value: 'page-first' },\n { label: 'Site first — Acme | About Us', value: 'site-first' },\n ],\n },\n {\n name: 'titleSeparator',\n type: 'select',\n admin: {\n description: 'Separates the page title from the site name in browser tabs.',\n },\n defaultValue: '|',\n options: [\n { label: 'Pipe — Page | Site', value: '|' },\n { label: 'Dash — Page – Site', value: '–' },\n { label: 'Hyphen — Page - Site', value: '-' },\n { label: 'Bullet — Page · Site', value: '·' },\n { label: 'Slash — Page / Site', value: '/' },\n ],\n },\n {\n name: 'logo',\n type: 'upload',\n admin: {\n description: 'Primary site logo.',\n },\n relationTo: 'media',\n },\n {\n name: 'defaultShareImage',\n type: 'upload',\n admin: {\n description: 'Fallback Open Graph image when a page has none.',\n },\n relationTo: 'media',\n },\n {\n name: 'favicon',\n type: 'upload',\n admin: {\n description:\n 'Square icon, PNG or SVG, min. 48×48px (Google requires this to show it in search). Rendered via buildIconsMetadata.',\n },\n hooks: {\n // Warn the editor if the favicon is too small / not square for Google.\n beforeValidate: [validateFaviconField],\n },\n relationTo: 'media',\n },\n]\n"],"names":["validateFaviconField","generalFields","name","type","admin","description","localized","required","defaultValue","options","label","value","relationTo","hooks","beforeValidate"],"mappings":"AAEA,SAASA,oBAAoB,QAAQ,gCAA+B;AAEpE;;;CAGC,GACD,OAAO,MAAMC,gBAAyB;IACpC;QACEC,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAC,WAAW;QACXC,UAAU;IACZ;IACA;QACEL,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aACE;QACJ;QACAC,WAAW;IACb;IACA;QACEJ,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAG,cAAc;QACdC,SAAS;YACP;gBAAEC,OAAO;gBAAkCC,OAAO;YAAa;YAC/D;gBAAED,OAAO;gBAAkCC,OAAO;YAAa;SAChE;IACH;IACA;QACET,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAG,cAAc;QACdC,SAAS;YACP;gBAAEC,OAAO;gBAAwBC,OAAO;YAAI;YAC5C;gBAAED,OAAO;gBAAwBC,OAAO;YAAI;YAC5C;gBAAED,OAAO;gBAA0BC,OAAO;YAAI;YAC9C;gBAAED,OAAO;gBAA0BC,OAAO;YAAI;YAC9C;gBAAED,OAAO;gBAAyBC,OAAO;YAAI;SAC9C;IACH;IACA;QACET,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAO,YAAY;IACd;IACA;QACEV,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAO,YAAY;IACd;IACA;QACEV,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aACE;QACJ;QACAQ,OAAO;YACL,uEAAuE;YACvEC,gBAAgB;gBAACd;aAAqB;QACxC;QACAY,YAAY;IACd;CACD,CAAA"} \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts index 85c224a..f71bd20 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 { buildArticleJsonLd, buildFaqJsonLd, buildIconsMetadata, buildLocalBusinessJsonLd, buildOrganizationJsonLd, buildServiceJsonLd, validateFaviconField, } from './modules/seo/index.js'; +export { buildArticleJsonLd, buildFaqJsonLd, buildIconsMetadata, buildLlmsTxt, 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 03c0836..f43adf7 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 { buildArticleJsonLd, buildFaqJsonLd, buildIconsMetadata, buildLocalBusinessJsonLd, buildOrganizationJsonLd, buildServiceJsonLd, validateFaviconField } from './modules/seo/index.js'; +export { buildArticleJsonLd, buildFaqJsonLd, buildIconsMetadata, buildLlmsTxt, 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 7872c60..e879239 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 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 +{"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 buildLlmsTxt,\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","buildLlmsTxt","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,YAAY,EACZC,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/seo/buildLlmsTxt.d.ts b/dist/modules/seo/buildLlmsTxt.d.ts new file mode 100644 index 0000000..1d019b3 --- /dev/null +++ b/dist/modules/seo/buildLlmsTxt.d.ts @@ -0,0 +1,45 @@ +import type { BasePayload } from 'payload'; +import type { I18nConfig } from '../i18n/index.js'; +type BuildLlmsTxtArgs = { + /** Absolute site URL (https://…). */ + baseUrl: string; + config: I18nConfig; + /** Locale for names/descriptions. Defaults to config.defaultLocale. */ + locale?: string; + /** Pages collection slug. Defaults to 'pages'. */ + pagesSlug?: string; + payload: BasePayload; + /** Site settings global slug. Defaults to 'site-settings'. */ + settingsSlug?: string; +}; +/** + * Builds the body of /llms.txt — a Markdown file describing the site for AI + * agents / LLM crawlers, per the llmstxt.org convention. Mirrors buildRobots / + * buildSitemapEntries: the plugin already knows the site's name, description, + * and pages, so it can generate this automatically. + * + * Structure (llmstxt.org): H1 site name, a blockquote/summary, then a list of + * key pages as Markdown links with short descriptions. Agents read this to + * understand the site quickly without crawling everything. + * + * Wire it as a route that returns text/plain: + * + * // app/llms.txt/route.ts + * import { buildLlmsTxt } from '@intecion/ipal-kit' + * import { getCachedPayload } from '@/lib/content' + * import { i18nConfig } from '@/i18n.config' + * export const dynamic = 'force-dynamic' + * export async function GET() { + * const body = await buildLlmsTxt({ + * payload: await getCachedPayload(), + * config: i18nConfig, + * baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!, + * }) + * return new Response(body, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } }) + * } + * + * Data comes from the panel (siteName, siteDescription, pages) — nothing + * hardcoded. Skips drafts and noindex pages (same as the sitemap). + */ +export declare function buildLlmsTxt({ baseUrl, config, locale, pagesSlug, payload, settingsSlug, }: BuildLlmsTxtArgs): Promise; +export {}; diff --git a/dist/modules/seo/buildLlmsTxt.js b/dist/modules/seo/buildLlmsTxt.js new file mode 100644 index 0000000..34084e1 --- /dev/null +++ b/dist/modules/seo/buildLlmsTxt.js @@ -0,0 +1,90 @@ +import { buildLocalizedPath } from '../i18n/index.js'; +/** + * Builds the body of /llms.txt — a Markdown file describing the site for AI + * agents / LLM crawlers, per the llmstxt.org convention. Mirrors buildRobots / + * buildSitemapEntries: the plugin already knows the site's name, description, + * and pages, so it can generate this automatically. + * + * Structure (llmstxt.org): H1 site name, a blockquote/summary, then a list of + * key pages as Markdown links with short descriptions. Agents read this to + * understand the site quickly without crawling everything. + * + * Wire it as a route that returns text/plain: + * + * // app/llms.txt/route.ts + * import { buildLlmsTxt } from '@intecion/ipal-kit' + * import { getCachedPayload } from '@/lib/content' + * import { i18nConfig } from '@/i18n.config' + * export const dynamic = 'force-dynamic' + * export async function GET() { + * const body = await buildLlmsTxt({ + * payload: await getCachedPayload(), + * config: i18nConfig, + * baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!, + * }) + * return new Response(body, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } }) + * } + * + * Data comes from the panel (siteName, siteDescription, pages) — nothing + * hardcoded. Skips drafts and noindex pages (same as the sitemap). + */ export async function buildLlmsTxt({ baseUrl, config, locale, pagesSlug = 'pages', payload, settingsSlug = 'site-settings' }) { + const loc = locale ?? config.defaultLocale; + const origin = baseUrl.replace(/\/$/, ''); + const settings = await payload.findGlobal({ + slug: settingsSlug, + depth: 0, + locale: loc + }); + const name = settings.siteName?.trim() || 'Website'; + const description = settings.siteDescription?.trim(); + const result = await payload.find({ + collection: pagesSlug, + depth: 0, + limit: 1000, + locale: loc, + where: { + _status: { + not_equals: 'draft' + } + } + }); + const lines = [ + `# ${name}`, + '' + ]; + if (description) { + lines.push(`> ${description}`, ''); + } + const pageLinks = []; + for (const raw of result.docs){ + if (raw._status && raw._status !== 'published') { + continue; + } + if (raw.meta?.noindex) { + continue; + } + const title = typeof raw.title === 'string' ? raw.title : undefined; + const slug = typeof raw.slug === 'string' ? raw.slug : undefined; + if (!title || !slug) { + continue; + } + const path = buildLocalizedPath({ + config, + locale: loc, + slugs: { + [loc]: slug + } + }); + if (!path) { + continue; + } + const pageDesc = typeof raw.meta?.description === 'string' ? ` — ${raw.meta.description}` : ''; + pageLinks.push(`- [${title}](${origin}${path})${pageDesc}`); + } + if (pageLinks.length > 0) { + lines.push('## Strony', '', ...pageLinks, ''); + } + return lines.join('\n'); +} + +//# sourceMappingURL=buildLlmsTxt.js.map \ No newline at end of file diff --git a/dist/modules/seo/buildLlmsTxt.js.map b/dist/modules/seo/buildLlmsTxt.js.map new file mode 100644 index 0000000..6de6de1 --- /dev/null +++ b/dist/modules/seo/buildLlmsTxt.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../../src/modules/seo/buildLlmsTxt.ts"],"sourcesContent":["import type { BasePayload } from 'payload'\n\nimport type { I18nConfig } from '../i18n/index.js'\n\nimport { buildLocalizedPath } from '../i18n/index.js'\n\ntype BuildLlmsTxtArgs = {\n /** Absolute site URL (https://…). */\n baseUrl: string\n config: I18nConfig\n /** Locale for names/descriptions. Defaults to config.defaultLocale. */\n locale?: string\n /** Pages collection slug. Defaults to 'pages'. */\n pagesSlug?: string\n payload: BasePayload\n /** Site settings global slug. Defaults to 'site-settings'. */\n settingsSlug?: string\n}\n\ntype SettingsShape = {\n siteDescription?: null | string\n siteName?: null | string\n}\n\ntype PageRow = {\n _status?: string\n id: number | string\n meta?: { description?: unknown; noindex?: boolean } | null\n slug?: unknown\n title?: unknown\n}\n\n/**\n * Builds the body of /llms.txt — a Markdown file describing the site for AI\n * agents / LLM crawlers, per the llmstxt.org convention. Mirrors buildRobots /\n * buildSitemapEntries: the plugin already knows the site's name, description,\n * and pages, so it can generate this automatically.\n *\n * Structure (llmstxt.org): H1 site name, a blockquote/summary, then a list of\n * key pages as Markdown links with short descriptions. Agents read this to\n * understand the site quickly without crawling everything.\n *\n * Wire it as a route that returns text/plain:\n *\n * // app/llms.txt/route.ts\n * import { buildLlmsTxt } from '@intecion/ipal-kit'\n * import { getCachedPayload } from '@/lib/content'\n * import { i18nConfig } from '@/i18n.config'\n * export const dynamic = 'force-dynamic'\n * export async function GET() {\n * const body = await buildLlmsTxt({\n * payload: await getCachedPayload(),\n * config: i18nConfig,\n * baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!,\n * })\n * return new Response(body, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } })\n * }\n *\n * Data comes from the panel (siteName, siteDescription, pages) — nothing\n * hardcoded. Skips drafts and noindex pages (same as the sitemap).\n */\nexport async function buildLlmsTxt({\n baseUrl,\n config,\n locale,\n pagesSlug = 'pages',\n payload,\n settingsSlug = 'site-settings',\n}: BuildLlmsTxtArgs): Promise {\n const loc = locale ?? config.defaultLocale\n const origin = baseUrl.replace(/\\/$/, '')\n\n const settings = (await payload.findGlobal({\n slug: settingsSlug as never,\n depth: 0,\n locale: loc as never,\n })) as SettingsShape\n\n const name = settings.siteName?.trim() || 'Website'\n const description = settings.siteDescription?.trim()\n\n const result = await payload.find({\n collection: pagesSlug as never,\n depth: 0,\n limit: 1000,\n locale: loc as never,\n where: { _status: { not_equals: 'draft' } } as never,\n })\n\n const lines: string[] = [`# ${name}`, '']\n if (description) {\n lines.push(`> ${description}`, '')\n }\n\n const pageLinks: string[] = []\n for (const raw of result.docs as PageRow[]) {\n if (raw._status && raw._status !== 'published') {continue}\n if (raw.meta?.noindex) {continue}\n\n const title = typeof raw.title === 'string' ? raw.title : undefined\n const slug = typeof raw.slug === 'string' ? raw.slug : undefined\n if (!title || !slug) {continue}\n\n const path = buildLocalizedPath({ config, locale: loc, slugs: { [loc]: slug } })\n if (!path) {continue}\n\n const pageDesc = typeof raw.meta?.description === 'string' ? ` — ${raw.meta.description}` : ''\n pageLinks.push(`- [${title}](${origin}${path})${pageDesc}`)\n }\n\n if (pageLinks.length > 0) {\n lines.push('## Strony', '', ...pageLinks, '')\n }\n\n return lines.join('\\n')\n}\n"],"names":["buildLocalizedPath","buildLlmsTxt","baseUrl","config","locale","pagesSlug","payload","settingsSlug","loc","defaultLocale","origin","replace","settings","findGlobal","slug","depth","name","siteName","trim","description","siteDescription","result","find","collection","limit","where","_status","not_equals","lines","push","pageLinks","raw","docs","meta","noindex","title","undefined","path","slugs","pageDesc","length","join"],"mappings":"AAIA,SAASA,kBAAkB,QAAQ,mBAAkB;AA4BrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,eAAeC,aAAa,EACjCC,OAAO,EACPC,MAAM,EACNC,MAAM,EACNC,YAAY,OAAO,EACnBC,OAAO,EACPC,eAAe,eAAe,EACb;IACjB,MAAMC,MAAMJ,UAAUD,OAAOM,aAAa;IAC1C,MAAMC,SAASR,QAAQS,OAAO,CAAC,OAAO;IAEtC,MAAMC,WAAY,MAAMN,QAAQO,UAAU,CAAC;QACzCC,MAAMP;QACNQ,OAAO;QACPX,QAAQI;IACV;IAEA,MAAMQ,OAAOJ,SAASK,QAAQ,EAAEC,UAAU;IAC1C,MAAMC,cAAcP,SAASQ,eAAe,EAAEF;IAE9C,MAAMG,SAAS,MAAMf,QAAQgB,IAAI,CAAC;QAChCC,YAAYlB;QACZU,OAAO;QACPS,OAAO;QACPpB,QAAQI;QACRiB,OAAO;YAAEC,SAAS;gBAAEC,YAAY;YAAQ;QAAE;IAC5C;IAEA,MAAMC,QAAkB;QAAC,CAAC,EAAE,EAAEZ,MAAM;QAAE;KAAG;IACzC,IAAIG,aAAa;QACfS,MAAMC,IAAI,CAAC,CAAC,EAAE,EAAEV,aAAa,EAAE;IACjC;IAEA,MAAMW,YAAsB,EAAE;IAC9B,KAAK,MAAMC,OAAOV,OAAOW,IAAI,CAAe;QAC1C,IAAID,IAAIL,OAAO,IAAIK,IAAIL,OAAO,KAAK,aAAa;YAAC;QAAQ;QACzD,IAAIK,IAAIE,IAAI,EAAEC,SAAS;YAAC;QAAQ;QAEhC,MAAMC,QAAQ,OAAOJ,IAAII,KAAK,KAAK,WAAWJ,IAAII,KAAK,GAAGC;QAC1D,MAAMtB,OAAO,OAAOiB,IAAIjB,IAAI,KAAK,WAAWiB,IAAIjB,IAAI,GAAGsB;QACvD,IAAI,CAACD,SAAS,CAACrB,MAAM;YAAC;QAAQ;QAE9B,MAAMuB,OAAOrC,mBAAmB;YAAEG;YAAQC,QAAQI;YAAK8B,OAAO;gBAAE,CAAC9B,IAAI,EAAEM;YAAK;QAAE;QAC9E,IAAI,CAACuB,MAAM;YAAC;QAAQ;QAEpB,MAAME,WAAW,OAAOR,IAAIE,IAAI,EAAEd,gBAAgB,WAAW,CAAC,GAAG,EAAEY,IAAIE,IAAI,CAACd,WAAW,EAAE,GAAG;QAC5FW,UAAUD,IAAI,CAAC,CAAC,GAAG,EAAEM,MAAM,EAAE,EAAEzB,SAAS2B,KAAK,CAAC,EAAEE,UAAU;IAC5D;IAEA,IAAIT,UAAUU,MAAM,GAAG,GAAG;QACxBZ,MAAMC,IAAI,CAAC,aAAa,OAAOC,WAAW;IAC5C;IAEA,OAAOF,MAAMa,IAAI,CAAC;AACpB"} \ No newline at end of file diff --git a/dist/modules/seo/buildMetadata.d.ts b/dist/modules/seo/buildMetadata.d.ts index d2f5b11..514e222 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 | Record; + homeSlug?: Record | string; /** Resolved OG image URL (page image or site defaultShareImage). */ imageUrl?: null | string; /** Current locale being rendered. */ @@ -63,6 +63,12 @@ type BuildMetadataArgs = { query?: string; /** Separator between page title and site name. Defaults to ' | '. */ separator?: string; + /** + * Global fallback description (from SiteSettings.siteDescription). Used when a + * page has no meta.description — better than emitting no description at all + * (Google would then scrape random on-page text for the snippet). + */ + siteDescription?: null | string; /** Site name for title composition and OG. */ siteName?: null | string; /** slug per locale for this document — drives canonical + hreflang. */ @@ -77,5 +83,5 @@ type BuildMetadataArgs = { * pieces (meta group, site name, image URL, localized slugs) and passes them * in — the plugin composes, it doesn't fetch. */ -export declare function buildMetadata({ baseUrl, config, homeSlug, imageUrl, locale, meta, order, pageTitle, prefix, query, separator, siteName, slugs, }: BuildMetadataArgs): PageMetadata; +export declare function buildMetadata({ baseUrl, config, homeSlug, imageUrl, locale, meta, order, pageTitle, prefix, query, separator, siteDescription, siteName, slugs, }: BuildMetadataArgs): PageMetadata; export {}; diff --git a/dist/modules/seo/buildMetadata.js b/dist/modules/seo/buildMetadata.js index 8814fac..508d55b 100644 --- a/dist/modules/seo/buildMetadata.js +++ b/dist/modules/seo/buildMetadata.js @@ -9,7 +9,7 @@ import { buildHreflangAlternates } from './hreflang.js'; * Designed for use inside Next.js `generateMetadata`. The caller resolves the * pieces (meta group, site name, image URL, localized slugs) and passes them * in — the plugin composes, it doesn't fetch. - */ export function buildMetadata({ baseUrl, config, homeSlug = 'home', imageUrl, locale, meta, order, pageTitle, prefix, query, separator, siteName, slugs }) { + */ export function buildMetadata({ baseUrl, config, homeSlug = 'home', imageUrl, locale, meta, order, pageTitle, prefix, query, separator, siteDescription, siteName, slugs }) { // Title source priority: titleOverride (exact, wins outright) > meta.title // (SEO title an editor set) > pageTitle (the document's own name). This means // a page with no SEO title still shows its name (e.g. 'Sprzątanie biur') @@ -22,7 +22,7 @@ import { buildHreflangAlternates } from './hreflang.js'; separator, siteName }); - const description = meta?.description?.trim() || undefined; + const description = meta?.description?.trim() || siteDescription?.trim() || undefined; const origin = baseUrl?.replace(/\/$/, '') ?? ''; const suffix = query ?? ''; const currentPath = buildLocalizedPath({ diff --git a/dist/modules/seo/buildMetadata.js.map b/dist/modules/seo/buildMetadata.js.map index f4a6428..0307984 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 | 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 +{"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?: Record | 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 /**\n * Global fallback description (from SiteSettings.siteDescription). Used when a\n * page has no meta.description — better than emitting no description at all\n * (Google would then scrape random on-page text for the snippet).\n */\n siteDescription?: null | 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 siteDescription,\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() || siteDescription?.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","siteDescription","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;AA2EvD;;;;;;;;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,eAAe,EACfC,QAAQ,EACRC,KAAK,EACa;IAClB,2EAA2E;IAC3E,8EAA8E;IAC9E,yEAAyE;IACzE,yEAAyE;IACzE,MAAMC,WAAWT,MAAMU,eAAeC;IACtC,MAAMC,oBAAoBZ,MAAMa,OAAOF,UAAUT,WAAWS,UAAUG;IACtE,MAAMD,QACJJ,YAAYjB,aAAa;QAAES;QAAOC,WAAWU;QAAmBP;QAAWE;IAAS;IACtF,MAAMQ,cAAcf,MAAMe,aAAaJ,UAAUL,iBAAiBK,UAAUG;IAC5E,MAAME,SAASrB,SAASsB,QAAQ,OAAO,OAAO;IAE9C,MAAMC,SAASd,SAAS;IAExB,MAAMe,cAAc5B,mBAAmB;QAAEK;QAAQC;QAAUE;QAAQI;QAAQK;IAAM;IACjF,MAAMY,YAAYD,cAAc,GAAGH,SAASG,cAAcD,QAAQ,GAAGJ;IAErE,MAAMO,YAAY5B,wBAAwB;QAAEE;QAASC;QAAQC;QAAUM;QAAQK;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,SAAS3B,WAAW;QAAC;YAAE4B,KAAK5B;QAAS;KAAE,GAAGgB;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;YACxB1B;QACF;QACA,2EAA2E;QAC3E,iEAAiE;QACjE,GAAIC,MAAM8B,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/index.d.ts b/dist/modules/seo/index.d.ts index 89b318b..e76e211 100644 --- a/dist/modules/seo/index.d.ts +++ b/dist/modules/seo/index.d.ts @@ -4,6 +4,7 @@ export { buildArticleJsonLd } from './buildArticleJsonLd.js'; export { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js'; export { buildFaqJsonLd } from './buildFaqJsonLd.js'; export { buildIconsMetadata } from './buildIconsMetadata.js'; +export { buildLlmsTxt } from './buildLlmsTxt.js'; export { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js'; export { buildMetadata } from './buildMetadata.js'; export type { PageMetadata } from './buildMetadata.js'; diff --git a/dist/modules/seo/index.js b/dist/modules/seo/index.js index f014582..3c59931 100644 --- a/dist/modules/seo/index.js +++ b/dist/modules/seo/index.js @@ -3,6 +3,7 @@ export { buildArticleJsonLd } from './buildArticleJsonLd.js'; export { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js'; export { buildFaqJsonLd } from './buildFaqJsonLd.js'; export { buildIconsMetadata } from './buildIconsMetadata.js'; +export { buildLlmsTxt } from './buildLlmsTxt.js'; export { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js'; export { buildMetadata } from './buildMetadata.js'; export { buildOrganizationJsonLd } from './buildOrganizationJsonLd.js'; diff --git a/dist/modules/seo/index.js.map b/dist/modules/seo/index.js.map index 4081dce..f41e556 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 { 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 +{"version":3,"sources":["../../../src/modules/seo/index.ts"],"sourcesContent":["export { buildAutoFillMetaHook } from './autoFillMeta.js'\nexport type { AutoFillMapping } from './autoFillMeta.js'\nexport { buildArticleJsonLd } from './buildArticleJsonLd.js'\nexport { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js'\nexport { buildFaqJsonLd } from './buildFaqJsonLd.js'\nexport { buildIconsMetadata } from './buildIconsMetadata.js'\nexport { buildLlmsTxt } from './buildLlmsTxt.js'\nexport { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js'\nexport { buildMetadata } from './buildMetadata.js'\nexport type { PageMetadata } from './buildMetadata.js'\nexport { buildOrganizationJsonLd } from './buildOrganizationJsonLd.js'\nexport { buildRobots } from './buildRobots.js'\nexport type { RobotsRules } from './buildRobots.js'\nexport { buildServiceJsonLd } from './buildServiceJsonLd.js'\nexport { buildSitemapEntries } from './buildSitemapEntries.js'\nexport type { SitemapEntry } from './buildSitemapEntries.js'\nexport { buildSiteNavigationJsonLd } from './buildSiteNavigationJsonLd.js'\nexport { buildWebSiteJsonLd } from './buildWebSiteJsonLd.js'\nexport { composeTitle } from './composeTitle.js'\nexport type { TitleOrder } from './composeTitle.js'\nexport { createMetadataGenerator } from './createMetadataGenerator.js'\nexport { createPageMetadata } from './createPageMetadata.js'\nexport { buildHreflangAlternates } from './hreflang.js'\nexport { injectAutoFillMeta } from './injectAutoFillMeta.js'\nexport { injectSeoTabs } from './injectSeoTabs.js'\nexport { readSiteMetaConfig } from './readSiteMetaConfig.js'\nexport type { SiteMetaConfig } from './readSiteMetaConfig.js'\nexport { buildSeoPlugin } from './seoPluginConfig.js'\nexport { slugsAcrossLocales } from './slugsAcrossLocales.js'\nexport type { SeoMeta, SeoOption } from './types.js'\nexport { validateFaviconField } from './validateFavicon.js'\n"],"names":["buildAutoFillMetaHook","buildArticleJsonLd","buildBreadcrumbJsonLd","buildFaqJsonLd","buildIconsMetadata","buildLlmsTxt","buildLocalBusinessJsonLd","buildMetadata","buildOrganizationJsonLd","buildRobots","buildServiceJsonLd","buildSitemapEntries","buildSiteNavigationJsonLd","buildWebSiteJsonLd","composeTitle","createMetadataGenerator","createPageMetadata","buildHreflangAlternates","injectAutoFillMeta","injectSeoTabs","readSiteMetaConfig","buildSeoPlugin","slugsAcrossLocales","validateFaviconField"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,oBAAmB;AAEzD,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,qBAAqB,QAAQ,6BAA4B;AAClE,SAASC,cAAc,QAAQ,sBAAqB;AACpD,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,YAAY,QAAQ,oBAAmB;AAChD,SAASC,wBAAwB,QAAQ,gCAA+B;AACxE,SAASC,aAAa,QAAQ,qBAAoB;AAElD,SAASC,uBAAuB,QAAQ,+BAA8B;AACtE,SAASC,WAAW,QAAQ,mBAAkB;AAE9C,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,SAASC,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 f8fbc7c..c7d12ac 100644 --- a/dist/modules/seo/readSiteMetaConfig.d.ts +++ b/dist/modules/seo/readSiteMetaConfig.d.ts @@ -1,10 +1,6 @@ import type { BasePayload } from 'payload'; import type { TitleOrder } from './composeTitle.js'; export type SiteMetaConfig = { - siteName: string | null; - /** Padded separator, e.g. ' | ' — the panel stores the bare character. */ - separator?: 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 @@ -12,6 +8,12 @@ export type SiteMetaConfig = { * instead of '/de', breaking return tags (a real GSC error). */ homeSlug?: Record; + order?: TitleOrder; + /** Padded separator, e.g. ' | ' — the panel stores the bare character. */ + separator?: string; + /** Global fallback meta description. */ + siteDescription?: null | string; + siteName: null | string; }; /** * Reads the site-wide inputs to title and URL composition from SiteSettings. @@ -21,9 +23,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({ payload, locale, settingsSlug, siteNameField, }: { - payload: BasePayload; +export declare function readSiteMetaConfig({ locale, payload, settingsSlug, siteNameField, }: { locale: string; + payload: BasePayload; settingsSlug?: string; siteNameField?: string; }): Promise; diff --git a/dist/modules/seo/readSiteMetaConfig.js b/dist/modules/seo/readSiteMetaConfig.js index 699d0c5..f35e0f1 100644 --- a/dist/modules/seo/readSiteMetaConfig.js +++ b/dist/modules/seo/readSiteMetaConfig.js @@ -5,13 +5,13 @@ * 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({ payload, locale, settingsSlug = 'site-settings', siteNameField = 'siteName' }) { + */ export async function readSiteMetaConfig({ locale, payload, 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, - locale: locale, - depth: 1 + depth: 1, + locale: locale }); const homepage = settings.homepage && typeof settings.homepage === 'object' ? settings.homepage : null; // Build the per-locale home slug map. The homepage's slug is localized, so a @@ -22,18 +22,22 @@ if (homepage?.id) { try { const homeDoc = await payload.findByID({ - collection: 'pages', id: homepage.id, - locale: 'all', - depth: 0 + collection: 'pages', + depth: 0, + locale: 'all' }); const slugField = homeDoc.slug; if (slugField && typeof slugField === 'object') { const map = {}; for (const [loc, value] of Object.entries(slugField)){ - if (typeof value === 'string') map[loc] = value; + if (typeof value === 'string') { + map[loc] = value; + } + } + if (Object.keys(map).length > 0) { + homeSlug = map; } - 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 @@ -54,11 +58,12 @@ }; } return { + siteDescription: settings.siteDescription ?? null, siteName: settings[siteNameField] ?? null, // The panel stores '|'; titles need it padded. - separator: settings.titleSeparator ? ` ${settings.titleSeparator} ` : undefined, + homeSlug, order: settings.titleOrder ?? undefined, - homeSlug + separator: settings.titleSeparator ? ` ${settings.titleSeparator} ` : undefined }; } diff --git a/dist/modules/seo/readSiteMetaConfig.js.map b/dist/modules/seo/readSiteMetaConfig.js.map index f408239..375f0bb 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'\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 +{"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 /**\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 order?: TitleOrder\n /** Padded separator, e.g. ' | ' — the panel stores the bare character. */\n separator?: string\n /** Global fallback meta description. */\n siteDescription?: null | 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 // 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 id: homepage.id,\n collection: 'pages' as never,\n depth: 0,\n locale: 'all' as never,\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 siteDescription: (settings.siteDescription as string | undefined) ?? null,\n siteName: (settings[siteNameField] as string | undefined) ?? null,\n // The panel stores '|'; titles need it padded.\n homeSlug,\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","homeSlug","id","homeDoc","findByID","collection","slugField","map","loc","value","Object","entries","keys","length","siteDescription","siteName","order","titleOrder","undefined","separator","titleSeparator"],"mappings":"AA2BA;;;;;;;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,6EAA6E;IAC7E,2EAA2E;IAC3E,0EAA0E;IAC1E,6CAA6C;IAC7C,IAAIC;IACJ,IAAID,UAAUE,IAAI;QAChB,IAAI;YACF,MAAMC,UAAW,MAAMV,QAAQW,QAAQ,CAAC;gBACtCF,IAAIF,SAASE,EAAE;gBACfG,YAAY;gBACZN,OAAO;gBACPP,QAAQ;YACV;YACA,MAAMc,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,UAAU;wBAACF,GAAG,CAACC,IAAI,GAAGC;oBAAK;gBAClD;gBACA,IAAIC,OAAOE,IAAI,CAACL,KAAKM,MAAM,GAAG,GAAG;oBAACZ,WAAWM;gBAAG;YAClD,OAAO,IAAI,OAAOD,cAAc,UAAU;gBACxC,0EAA0E;gBAC1E,qEAAqE;gBACrE,gEAAgE;gBAChEL,WAAW;oBAAE,CAACT,OAAO,EAAEc;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,CAACT,OAAO,EAAEQ,SAASF,IAAI;QAAC;IACvC;IAEA,OAAO;QACLgB,iBAAiB,AAAClB,SAASkB,eAAe,IAA2B;QACrEC,UAAU,AAACnB,QAAQ,CAACD,cAAc,IAA2B;QAC7D,+CAA+C;QAC/CM;QACAe,OAAOpB,SAASqB,UAAU,IAAIC;QAC9BC,WAAWvB,SAASwB,cAAc,GAAG,CAAC,CAAC,EAAExB,SAASwB,cAAc,CAAC,CAAC,CAAC,GAAGF;IACxE;AACF"} \ No newline at end of file diff --git a/docs/seo.md b/docs/seo.md index 52d4259..516477f 100644 --- a/docs/seo.md +++ b/docs/seo.md @@ -841,4 +841,47 @@ const jsonLd = buildArticleJsonLd({ }) ``` -Dane z dokumentu/panelu. Google wymaga headline + dat dla rich result. \ No newline at end of file +Dane z dokumentu/panelu. Google wymaga headline + dat dla rich result. + +## Fallback meta description (siteDescription) + +Strona bez `meta.description` → plugin używa globalnego `siteDescription` z +SiteSettings. Lepsze niż brak description (Google losowo wyciąga tekst ze strony). + +- Pole **siteDescription** w SiteSettings → General (localized, textarea) +- buildMetadata: `meta.description || siteDescription` +- Redaktor wypełnia raz globalnie; strony bez własnego opisu dziedziczą + +## llms.txt — opis strony dla agentów AI (GEO) + +`buildLlmsTxt` generuje `/llms.txt` (standard llmstxt.org) — plik Markdown +opisujący stronę dla agentów AI / crawlerów LLM. Na wzór buildRobots/sitemap: +plugin zna nazwę, opis i strony, więc generuje automatycznie. + +```ts +// app/llms.txt/route.ts +import { buildLlmsTxt } from '@intecion/ipal-kit' +import { getCachedPayload } from '@/lib/content' +import { i18nConfig } from '@/i18n.config' + +export const dynamic = 'force-dynamic' + +export async function GET() { + const body = await buildLlmsTxt({ + payload: await getCachedPayload(), + config: i18nConfig, + baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!, + }) + return new Response(body, { + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }) +} +``` + +Struktura: H1 (nazwa firmy), blockquote (siteDescription), lista stron jako +linki Markdown z opisami. Dane z panelu (siteName, siteDescription, strony), +pomija drafty i noindex. Poprawia widoczność w wyszukiwaniach AI (GEO — +Generative Engine Optimization) i audytach „Agentic Browsing”. + +Wymaga wypełnionego siteDescription i sensownych meta.description stron +(inaczej llms.txt będzie ubogi). \ No newline at end of file diff --git a/src/globals/SiteSettings/fields/general.ts b/src/globals/SiteSettings/fields/general.ts index 175cf61..f8515ba 100644 --- a/src/globals/SiteSettings/fields/general.ts +++ b/src/globals/SiteSettings/fields/general.ts @@ -4,7 +4,7 @@ import { validateFaviconField } from '../../../modules/seo/index.js' /** * General site identity fields. - * Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo, favicon). + * Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo). */ export const generalFields: Field[] = [ { @@ -16,6 +16,15 @@ export const generalFields: Field[] = [ localized: true, required: true, }, + { + name: 'siteDescription', + type: 'textarea', + admin: { + description: + 'Fallback meta description when a page has none. Also used for llms.txt and Open Graph.', + }, + localized: true, + }, { name: 'titleOrder', type: 'select', @@ -47,8 +56,7 @@ export const generalFields: Field[] = [ name: 'logo', type: 'upload', admin: { - description: - 'Primary site logo. Also used for Organization structured data (buildOrganizationJsonLd).', + description: 'Primary site logo.', }, relationTo: 'media', }, @@ -68,8 +76,7 @@ export const generalFields: Field[] = [ 'Square icon, PNG or SVG, min. 48×48px (Google requires this to show it in search). Rendered via buildIconsMetadata.', }, hooks: { - // Warns the editor at save time if the favicon is too small (<48×48) or - // not square — Google won't display such favicons in search results. + // Warn the editor if the favicon is too small / not square for Google. beforeValidate: [validateFaviconField], }, relationTo: 'media', diff --git a/src/index.ts b/src/index.ts index 293e2ca..a3628a9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -103,6 +103,7 @@ export { buildArticleJsonLd, buildFaqJsonLd, buildIconsMetadata, + buildLlmsTxt, buildLocalBusinessJsonLd, buildOrganizationJsonLd, buildServiceJsonLd, diff --git a/src/modules/seo/buildLlmsTxt.ts b/src/modules/seo/buildLlmsTxt.ts new file mode 100644 index 0000000..f9e52ad --- /dev/null +++ b/src/modules/seo/buildLlmsTxt.ts @@ -0,0 +1,116 @@ +import type { BasePayload } from 'payload' + +import type { I18nConfig } from '../i18n/index.js' + +import { buildLocalizedPath } from '../i18n/index.js' + +type BuildLlmsTxtArgs = { + /** Absolute site URL (https://…). */ + baseUrl: string + config: I18nConfig + /** Locale for names/descriptions. Defaults to config.defaultLocale. */ + locale?: string + /** Pages collection slug. Defaults to 'pages'. */ + pagesSlug?: string + payload: BasePayload + /** Site settings global slug. Defaults to 'site-settings'. */ + settingsSlug?: string +} + +type SettingsShape = { + siteDescription?: null | string + siteName?: null | string +} + +type PageRow = { + _status?: string + id: number | string + meta?: { description?: unknown; noindex?: boolean } | null + slug?: unknown + title?: unknown +} + +/** + * Builds the body of /llms.txt — a Markdown file describing the site for AI + * agents / LLM crawlers, per the llmstxt.org convention. Mirrors buildRobots / + * buildSitemapEntries: the plugin already knows the site's name, description, + * and pages, so it can generate this automatically. + * + * Structure (llmstxt.org): H1 site name, a blockquote/summary, then a list of + * key pages as Markdown links with short descriptions. Agents read this to + * understand the site quickly without crawling everything. + * + * Wire it as a route that returns text/plain: + * + * // app/llms.txt/route.ts + * import { buildLlmsTxt } from '@intecion/ipal-kit' + * import { getCachedPayload } from '@/lib/content' + * import { i18nConfig } from '@/i18n.config' + * export const dynamic = 'force-dynamic' + * export async function GET() { + * const body = await buildLlmsTxt({ + * payload: await getCachedPayload(), + * config: i18nConfig, + * baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!, + * }) + * return new Response(body, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } }) + * } + * + * Data comes from the panel (siteName, siteDescription, pages) — nothing + * hardcoded. Skips drafts and noindex pages (same as the sitemap). + */ +export async function buildLlmsTxt({ + baseUrl, + config, + locale, + pagesSlug = 'pages', + payload, + settingsSlug = 'site-settings', +}: BuildLlmsTxtArgs): Promise { + const loc = locale ?? config.defaultLocale + const origin = baseUrl.replace(/\/$/, '') + + const settings = (await payload.findGlobal({ + slug: settingsSlug as never, + depth: 0, + locale: loc as never, + })) as SettingsShape + + const name = settings.siteName?.trim() || 'Website' + const description = settings.siteDescription?.trim() + + const result = await payload.find({ + collection: pagesSlug as never, + depth: 0, + limit: 1000, + locale: loc as never, + where: { _status: { not_equals: 'draft' } } as never, + }) + + const lines: string[] = [`# ${name}`, ''] + if (description) { + lines.push(`> ${description}`, '') + } + + const pageLinks: string[] = [] + for (const raw of result.docs as PageRow[]) { + if (raw._status && raw._status !== 'published') {continue} + if (raw.meta?.noindex) {continue} + + const title = typeof raw.title === 'string' ? raw.title : undefined + const slug = typeof raw.slug === 'string' ? raw.slug : undefined + if (!title || !slug) {continue} + + const path = buildLocalizedPath({ config, locale: loc, slugs: { [loc]: slug } }) + if (!path) {continue} + + const pageDesc = typeof raw.meta?.description === 'string' ? ` — ${raw.meta.description}` : '' + pageLinks.push(`- [${title}](${origin}${path})${pageDesc}`) + } + + if (pageLinks.length > 0) { + lines.push('## Strony', '', ...pageLinks, '') + } + + return lines.join('\n') +} diff --git a/src/modules/seo/buildMetadata.ts b/src/modules/seo/buildMetadata.ts index fed751a..30bd8bf 100644 --- a/src/modules/seo/buildMetadata.ts +++ b/src/modules/seo/buildMetadata.ts @@ -35,7 +35,7 @@ type BuildMetadataArgs = { baseUrl?: string config: I18nConfig /** Home slug that collapses to the locale root. Defaults to 'home'. */ - homeSlug?: string | Record + homeSlug?: Record | string /** Resolved OG image URL (page image or site defaultShareImage). */ imageUrl?: null | string /** Current locale being rendered. */ @@ -67,6 +67,12 @@ type BuildMetadataArgs = { query?: string /** Separator between page title and site name. Defaults to ' | '. */ separator?: string + /** + * Global fallback description (from SiteSettings.siteDescription). Used when a + * page has no meta.description — better than emitting no description at all + * (Google would then scrape random on-page text for the snippet). + */ + siteDescription?: null | string /** Site name for title composition and OG. */ siteName?: null | string /** slug per locale for this document — drives canonical + hreflang. */ @@ -94,6 +100,7 @@ export function buildMetadata({ prefix, query, separator, + siteDescription, siteName, slugs, }: BuildMetadataArgs): PageMetadata { @@ -105,7 +112,7 @@ export function buildMetadata({ const resolvedPageTitle = meta?.title?.trim() || pageTitle?.trim() || undefined const title = override || composeTitle({ order, pageTitle: resolvedPageTitle, separator, siteName }) - const description = meta?.description?.trim() || undefined + const description = meta?.description?.trim() || siteDescription?.trim() || undefined const origin = baseUrl?.replace(/\/$/, '') ?? '' const suffix = query ?? '' diff --git a/src/modules/seo/index.ts b/src/modules/seo/index.ts index 894ac3f..074d270 100644 --- a/src/modules/seo/index.ts +++ b/src/modules/seo/index.ts @@ -4,6 +4,7 @@ export { buildArticleJsonLd } from './buildArticleJsonLd.js' export { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js' export { buildFaqJsonLd } from './buildFaqJsonLd.js' export { buildIconsMetadata } from './buildIconsMetadata.js' +export { buildLlmsTxt } from './buildLlmsTxt.js' export { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js' export { buildMetadata } from './buildMetadata.js' export type { PageMetadata } from './buildMetadata.js' diff --git a/src/modules/seo/readSiteMetaConfig.ts b/src/modules/seo/readSiteMetaConfig.ts index 3d10756..12abb8a 100644 --- a/src/modules/seo/readSiteMetaConfig.ts +++ b/src/modules/seo/readSiteMetaConfig.ts @@ -1,18 +1,15 @@ import type { BasePayload } from 'payload' + import type { TitleOrder } from './composeTitle.js' type SettingsShape = { - homepage?: { id: string | number; slug?: unknown } | string | number | null - titleSeparator?: string | null - titleOrder?: TitleOrder | null [key: string]: unknown + homepage?: { id: number | string; slug?: unknown } | null | number | string + titleOrder?: null | TitleOrder + titleSeparator?: null | string } export type SiteMetaConfig = { - siteName: string | null - /** Padded separator, e.g. ' | ' — the panel stores the bare character. */ - separator?: 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 @@ -20,6 +17,12 @@ export type SiteMetaConfig = { * instead of '/de', breaking return tags (a real GSC error). */ homeSlug?: Record + order?: TitleOrder + /** Padded separator, e.g. ' | ' — the panel stores the bare character. */ + separator?: string + /** Global fallback meta description. */ + siteDescription?: null | string + siteName: null | string } /** @@ -31,13 +34,13 @@ export type SiteMetaConfig = { * relationship — it should receive a resolved config. */ export async function readSiteMetaConfig({ - payload, - locale, - settingsSlug = 'site-settings', - siteNameField = 'siteName', - }: { - payload: BasePayload + locale, + payload, + settingsSlug = 'site-settings', + siteNameField = 'siteName', +}: { locale: string + payload: BasePayload settingsSlug?: string siteNameField?: string }): Promise { @@ -45,8 +48,8 @@ export async function readSiteMetaConfig({ // without a second query. const settings = (await payload.findGlobal({ slug: settingsSlug as never, - locale: locale as never, depth: 1, + locale: locale as never, })) as SettingsShape const homepage = @@ -60,18 +63,18 @@ export async function readSiteMetaConfig({ if (homepage?.id) { try { const homeDoc = (await payload.findByID({ - collection: 'pages' as never, id: homepage.id, - locale: 'all' as never, + collection: 'pages' as never, depth: 0, + locale: 'all' as never, })) as { slug?: unknown } const slugField = homeDoc.slug if (slugField && typeof slugField === 'object') { const map: Record = {} for (const [loc, value] of Object.entries(slugField as Record)) { - if (typeof value === 'string') map[loc] = value + if (typeof value === 'string') {map[loc] = value} } - if (Object.keys(map).length > 0) homeSlug = map + 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 @@ -90,10 +93,11 @@ export async function readSiteMetaConfig({ } return { + siteDescription: (settings.siteDescription as string | undefined) ?? null, siteName: (settings[siteNameField] as string | undefined) ?? null, // The panel stores '|'; titles need it padded. - separator: settings.titleSeparator ? ` ${settings.titleSeparator} ` : undefined, - order: settings.titleOrder ?? undefined, homeSlug, + order: settings.titleOrder ?? undefined, + separator: settings.titleSeparator ? ` ${settings.titleSeparator} ` : undefined, } -} \ No newline at end of file +}