diff --git a/dist/index.d.ts b/dist/index.d.ts index 6bf35a2..993356c 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -39,6 +39,7 @@ export { buildIconsMetadata, buildOrganizationJsonLd, validateFaviconField, } fr export { buildBreadcrumbJsonLd, buildSiteNavigationJsonLd, buildWebSiteJsonLd, } from './modules/seo/index.js'; export { buildFaqJsonLd, buildLocalBusinessJsonLd, buildServiceJsonLd, } from './modules/seo/index.js'; export { buildArticleJsonLd } from './modules/seo/index.js'; +export { buildSitemapXml } from './modules/seo/index.js'; export { buildLlmsTxt } from './modules/seo/index.js'; export { buildSlugField, toSlug } from './modules/slug/index.js'; export { buildR2Storage } from './modules/storage/index.js'; diff --git a/dist/index.js b/dist/index.js index 386edd2..f0313f1 100644 --- a/dist/index.js +++ b/dist/index.js @@ -25,6 +25,7 @@ export { buildBreadcrumbJsonLd, buildSiteNavigationJsonLd, buildWebSiteJsonLd } // Local SEO structured data — LocalBusiness (map pack), Service (offering), FAQPage. export { buildFaqJsonLd, buildLocalBusinessJsonLd, buildServiceJsonLd } from './modules/seo/index.js'; export { buildArticleJsonLd } from './modules/seo/index.js'; +export { buildSitemapXml } from './modules/seo/index.js'; export { buildLlmsTxt } from './modules/seo/index.js'; export { buildSlugField, toSlug } from './modules/slug/index.js'; export { buildR2Storage } from './modules/storage/index.js'; diff --git a/dist/index.js.map b/dist/index.js.map index 233a022..40fc462 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'\nexport { normalizeFilename, normalizeFilenameHook } from './modules/media/index.js'\nexport {\n getNotificationTexts,\n NOTIFICATION_FALLBACK,\n resolveFormMessage,\n} from './modules/notifications/index.js'\nexport type { FormNotificationTexts, NotificationTexts } 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 { buildCsp } from './modules/security/index.js'\nexport type { BuildCspArgs } from './modules/security/index.js'\nexport type { BuildSecurityHeadersArgs, SecurityHeader } from './modules/security/index.js'\n\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 {\n buildIconsMetadata,\n buildOrganizationJsonLd,\n validateFaviconField,\n} from './modules/seo/index.js'\nexport {\n buildBreadcrumbJsonLd,\n buildSiteNavigationJsonLd,\n buildWebSiteJsonLd,\n} from './modules/seo/index.js'\n// Local SEO structured data — LocalBusiness (map pack), Service (offering), FAQPage.\nexport {\n buildFaqJsonLd,\n buildLocalBusinessJsonLd,\n buildServiceJsonLd,\n} from './modules/seo/index.js'\nexport { buildArticleJsonLd } from './modules/seo/index.js'\nexport { buildLlmsTxt } from './modules/seo/index.js'\nexport { buildSlugField, toSlug } from './modules/slug/index.js'\nexport { buildR2Storage } from './modules/storage/index.js'\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","buildCsp","buildHreflangAlternates","buildMetadata","composeTitle","buildAutoFillMetaHook","buildRobots","buildSitemapEntries","createMetadataGenerator","createPageMetadata","injectAutoFillMeta","buildIconsMetadata","buildOrganizationJsonLd","validateFaviconField","buildBreadcrumbJsonLd","buildSiteNavigationJsonLd","buildWebSiteJsonLd","buildFaqJsonLd","buildLocalBusinessJsonLd","buildServiceJsonLd","buildArticleJsonLd","buildLlmsTxt","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,SAASC,iBAAiB,EAAEC,qBAAqB,QAAQ,2BAA0B;AACnF,SACEC,oBAAoB,EACpBC,qBAAqB,EACrBC,kBAAkB,QACb,mCAAkC;AAGzC,SAASC,qBAAqB,EAAEC,iBAAiB,QAAQ,2BAA0B;AAEnF,SACEC,SAAS,EACTC,mBAAmB,EACnBC,eAAe,EACfC,sBAAsB,EACtBC,kBAAkB,QACb,6BAA4B;AACnC,SAASC,oBAAoB,QAAQ,8BAA6B;AAClE,SAASC,QAAQ,QAAQ,8BAA6B;AAKtD,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,SACEC,kBAAkB,EAClBC,uBAAuB,EACvBC,oBAAoB,QACf,yBAAwB;AAC/B,SACEC,qBAAqB,EACrBC,yBAAyB,EACzBC,kBAAkB,QACb,yBAAwB;AAC/B,qFAAqF;AACrF,SACEC,cAAc,EACdC,wBAAwB,EACxBC,kBAAkB,QACb,yBAAwB;AAC/B,SAASC,kBAAkB,QAAQ,yBAAwB;AAC3D,SAASC,YAAY,QAAQ,yBAAwB;AACrD,SAASC,cAAc,EAAEC,MAAM,QAAQ,0BAAyB;AAChE,SAASC,cAAc,QAAQ,6BAA4B;AAC3D,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'\nexport { normalizeFilename, normalizeFilenameHook } from './modules/media/index.js'\nexport {\n getNotificationTexts,\n NOTIFICATION_FALLBACK,\n resolveFormMessage,\n} from './modules/notifications/index.js'\nexport type { FormNotificationTexts, NotificationTexts } 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 { buildCsp } from './modules/security/index.js'\nexport type { BuildCspArgs } from './modules/security/index.js'\nexport type { BuildSecurityHeadersArgs, SecurityHeader } from './modules/security/index.js'\n\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 {\n buildIconsMetadata,\n buildOrganizationJsonLd,\n validateFaviconField,\n} from './modules/seo/index.js'\nexport {\n buildBreadcrumbJsonLd,\n buildSiteNavigationJsonLd,\n buildWebSiteJsonLd,\n} from './modules/seo/index.js'\n// Local SEO structured data — LocalBusiness (map pack), Service (offering), FAQPage.\nexport {\n buildFaqJsonLd,\n buildLocalBusinessJsonLd,\n buildServiceJsonLd,\n} from './modules/seo/index.js'\nexport { buildArticleJsonLd } from './modules/seo/index.js'\nexport { buildSitemapXml } from './modules/seo/index.js'\nexport { buildLlmsTxt } from './modules/seo/index.js'\nexport { buildSlugField, toSlug } from './modules/slug/index.js'\nexport { buildR2Storage } from './modules/storage/index.js'\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","buildCsp","buildHreflangAlternates","buildMetadata","composeTitle","buildAutoFillMetaHook","buildRobots","buildSitemapEntries","createMetadataGenerator","createPageMetadata","injectAutoFillMeta","buildIconsMetadata","buildOrganizationJsonLd","validateFaviconField","buildBreadcrumbJsonLd","buildSiteNavigationJsonLd","buildWebSiteJsonLd","buildFaqJsonLd","buildLocalBusinessJsonLd","buildServiceJsonLd","buildArticleJsonLd","buildSitemapXml","buildLlmsTxt","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,SAASC,iBAAiB,EAAEC,qBAAqB,QAAQ,2BAA0B;AACnF,SACEC,oBAAoB,EACpBC,qBAAqB,EACrBC,kBAAkB,QACb,mCAAkC;AAGzC,SAASC,qBAAqB,EAAEC,iBAAiB,QAAQ,2BAA0B;AAEnF,SACEC,SAAS,EACTC,mBAAmB,EACnBC,eAAe,EACfC,sBAAsB,EACtBC,kBAAkB,QACb,6BAA4B;AACnC,SAASC,oBAAoB,QAAQ,8BAA6B;AAClE,SAASC,QAAQ,QAAQ,8BAA6B;AAKtD,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,SACEC,kBAAkB,EAClBC,uBAAuB,EACvBC,oBAAoB,QACf,yBAAwB;AAC/B,SACEC,qBAAqB,EACrBC,yBAAyB,EACzBC,kBAAkB,QACb,yBAAwB;AAC/B,qFAAqF;AACrF,SACEC,cAAc,EACdC,wBAAwB,EACxBC,kBAAkB,QACb,yBAAwB;AAC/B,SAASC,kBAAkB,QAAQ,yBAAwB;AAC3D,SAASC,eAAe,QAAQ,yBAAwB;AACxD,SAASC,YAAY,QAAQ,yBAAwB;AACrD,SAASC,cAAc,EAAEC,MAAM,QAAQ,0BAAyB;AAChE,SAASC,cAAc,QAAQ,6BAA4B;AAC3D,SAASC,OAAO,QAAQ,cAAa"} \ No newline at end of file diff --git a/dist/modules/seo/buildSitemapXml.d.ts b/dist/modules/seo/buildSitemapXml.d.ts new file mode 100644 index 0000000..970b389 --- /dev/null +++ b/dist/modules/seo/buildSitemapXml.d.ts @@ -0,0 +1,33 @@ +import type { SitemapEntry } from './buildSitemapEntries.js'; +/** + * Serializes sitemap entries to an XML STRING with an XSL stylesheet reference, + * so /sitemap.xml renders as a readable table in the browser (not raw XML) while + * staying a valid sitemap for crawlers. + * + * Why this exists alongside the Next MetadataRoute sitemap: Next's app/sitemap.ts + * (returning SitemapEntry[]) does NOT let you inject . To get + * the styled table, serve a custom route that returns this string instead: + * + * // app/sitemap.xml/route.ts + * import { buildSitemapXml } from '@intecion/ipal-kit' + * import { sitemap } from '@/lib/content' // your entries source + * export const dynamic = 'force-dynamic' + * export async function GET() { + * const entries = await sitemap() + * const xml = buildSitemapXml(entries, { stylesheetUrl: '/sitemap.xsl' }) + * return new Response(xml, { + * headers: { 'Content-Type': 'application/xml; charset=utf-8' }, + * }) + * } + * + * Put sitemap.xsl in the project's /public (copy from the plugin's assets, or + * serve it from a route). The points browsers at it; crawlers + * ignore it and read the XML. Include hreflang alternates as . + * + * NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one + * (the styled route OR the Next MetadataRoute). Two sitemaps at different paths + * confuse crawlers. + */ +export declare function buildSitemapXml(entries: SitemapEntry[], opts?: { + stylesheetUrl?: string; +}): string; diff --git a/dist/modules/seo/buildSitemapXml.js b/dist/modules/seo/buildSitemapXml.js new file mode 100644 index 0000000..5ef5640 --- /dev/null +++ b/dist/modules/seo/buildSitemapXml.js @@ -0,0 +1,57 @@ +/** + * Serializes sitemap entries to an XML STRING with an XSL stylesheet reference, + * so /sitemap.xml renders as a readable table in the browser (not raw XML) while + * staying a valid sitemap for crawlers. + * + * Why this exists alongside the Next MetadataRoute sitemap: Next's app/sitemap.ts + * (returning SitemapEntry[]) does NOT let you inject . To get + * the styled table, serve a custom route that returns this string instead: + * + * // app/sitemap.xml/route.ts + * import { buildSitemapXml } from '@intecion/ipal-kit' + * import { sitemap } from '@/lib/content' // your entries source + * export const dynamic = 'force-dynamic' + * export async function GET() { + * const entries = await sitemap() + * const xml = buildSitemapXml(entries, { stylesheetUrl: '/sitemap.xsl' }) + * return new Response(xml, { + * headers: { 'Content-Type': 'application/xml; charset=utf-8' }, + * }) + * } + * + * Put sitemap.xsl in the project's /public (copy from the plugin's assets, or + * serve it from a route). The points browsers at it; crawlers + * ignore it and read the XML. Include hreflang alternates as . + * + * NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one + * (the styled route OR the Next MetadataRoute). Two sitemaps at different paths + * confuse crawlers. + */ export function buildSitemapXml(entries, opts = {}) { + const { stylesheetUrl } = opts; + const esc = (s)=>s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); + const urls = entries.map((e)=>{ + const parts = [ + ` ${esc(e.url)}` + ]; + if (e.lastModified) { + const iso = e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified); + parts.push(` ${esc(iso)}`); + } + if (e.changeFrequency) parts.push(` ${e.changeFrequency}`); + if (typeof e.priority === 'number') parts.push(` ${e.priority}`); + // hreflang alternates + const alternates = e.alternates?.languages; + if (alternates) { + for (const [lang, href] of Object.entries(alternates)){ + if (typeof href === 'string') { + parts.push(` `); + } + } + } + return ` \n${parts.join('\n')}\n `; + }).join('\n'); + const stylesheet = stylesheetUrl ? `\n` : ''; + return `\n` + stylesheet + `\n` + urls + `\n`; +} + +//# sourceMappingURL=buildSitemapXml.js.map \ No newline at end of file diff --git a/dist/modules/seo/buildSitemapXml.js.map b/dist/modules/seo/buildSitemapXml.js.map new file mode 100644 index 0000000..73e23f7 --- /dev/null +++ b/dist/modules/seo/buildSitemapXml.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../../src/modules/seo/buildSitemapXml.ts"],"sourcesContent":["import type { SitemapEntry } from './buildSitemapEntries.js'\n\n/**\n * Serializes sitemap entries to an XML STRING with an XSL stylesheet reference,\n * so /sitemap.xml renders as a readable table in the browser (not raw XML) while\n * staying a valid sitemap for crawlers.\n *\n * Why this exists alongside the Next MetadataRoute sitemap: Next's app/sitemap.ts\n * (returning SitemapEntry[]) does NOT let you inject . To get\n * the styled table, serve a custom route that returns this string instead:\n *\n * // app/sitemap.xml/route.ts\n * import { buildSitemapXml } from '@intecion/ipal-kit'\n * import { sitemap } from '@/lib/content' // your entries source\n * export const dynamic = 'force-dynamic'\n * export async function GET() {\n * const entries = await sitemap()\n * const xml = buildSitemapXml(entries, { stylesheetUrl: '/sitemap.xsl' })\n * return new Response(xml, {\n * headers: { 'Content-Type': 'application/xml; charset=utf-8' },\n * })\n * }\n *\n * Put sitemap.xsl in the project's /public (copy from the plugin's assets, or\n * serve it from a route). The points browsers at it; crawlers\n * ignore it and read the XML. Include hreflang alternates as .\n *\n * NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one\n * (the styled route OR the Next MetadataRoute). Two sitemaps at different paths\n * confuse crawlers.\n */\nexport function buildSitemapXml(\n entries: SitemapEntry[],\n opts: { stylesheetUrl?: string } = {},\n): string {\n const { stylesheetUrl } = opts\n\n const esc = (s: string): string =>\n s\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n\n const urls = entries\n .map((e) => {\n const parts = [` ${esc(e.url)}`]\n if (e.lastModified) {\n const iso =\n e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified)\n parts.push(` ${esc(iso)}`)\n }\n if (e.changeFrequency) parts.push(` ${e.changeFrequency}`)\n if (typeof e.priority === 'number') parts.push(` ${e.priority}`)\n // hreflang alternates\n const alternates = e.alternates?.languages\n if (alternates) {\n for (const [lang, href] of Object.entries(alternates)) {\n if (typeof href === 'string') {\n parts.push(\n ` `,\n )\n }\n }\n }\n return ` \\n${parts.join('\\n')}\\n `\n })\n .join('\\n')\n\n const stylesheet = stylesheetUrl\n ? `\\n`\n : ''\n\n return (\n `\\n` +\n stylesheet +\n `\\n` +\n urls +\n `\\n`\n )\n}\n"],"names":["buildSitemapXml","entries","opts","stylesheetUrl","esc","s","replace","urls","map","e","parts","url","lastModified","iso","Date","toISOString","String","push","changeFrequency","priority","alternates","languages","lang","href","Object","join","stylesheet"],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,SAASA,gBACdC,OAAuB,EACvBC,OAAmC,CAAC,CAAC;IAErC,MAAM,EAAEC,aAAa,EAAE,GAAGD;IAE1B,MAAME,MAAM,CAACC,IACXA,EACGC,OAAO,CAAC,MAAM,SACdA,OAAO,CAAC,MAAM,QACdA,OAAO,CAAC,MAAM,QACdA,OAAO,CAAC,MAAM,UACdA,OAAO,CAAC,MAAM;IAEnB,MAAMC,OAAON,QACVO,GAAG,CAAC,CAACC;QACJ,MAAMC,QAAQ;YAAC,CAAC,SAAS,EAAEN,IAAIK,EAAEE,GAAG,EAAE,MAAM,CAAC;SAAC;QAC9C,IAAIF,EAAEG,YAAY,EAAE;YAClB,MAAMC,MACJJ,EAAEG,YAAY,YAAYE,OAAOL,EAAEG,YAAY,CAACG,WAAW,KAAKC,OAAOP,EAAEG,YAAY;YACvFF,MAAMO,IAAI,CAAC,CAAC,aAAa,EAAEb,IAAIS,KAAK,UAAU,CAAC;QACjD;QACA,IAAIJ,EAAES,eAAe,EAAER,MAAMO,IAAI,CAAC,CAAC,gBAAgB,EAAER,EAAES,eAAe,CAAC,aAAa,CAAC;QACrF,IAAI,OAAOT,EAAEU,QAAQ,KAAK,UAAUT,MAAMO,IAAI,CAAC,CAAC,cAAc,EAAER,EAAEU,QAAQ,CAAC,WAAW,CAAC;QACvF,sBAAsB;QACtB,MAAMC,aAAaX,EAAEW,UAAU,EAAEC;QACjC,IAAID,YAAY;YACd,KAAK,MAAM,CAACE,MAAMC,KAAK,IAAIC,OAAOvB,OAAO,CAACmB,YAAa;gBACrD,IAAI,OAAOG,SAAS,UAAU;oBAC5Bb,MAAMO,IAAI,CACR,CAAC,0CAA0C,EAAEb,IAAIkB,MAAM,QAAQ,EAAElB,IAAImB,MAAM,GAAG,CAAC;gBAEnF;YACF;QACF;QACA,OAAO,CAAC,SAAS,EAAEb,MAAMe,IAAI,CAAC,MAAM,UAAU,CAAC;IACjD,GACCA,IAAI,CAAC;IAER,MAAMC,aAAavB,gBACf,CAAC,uCAAuC,EAAEC,IAAID,eAAe,KAAK,CAAC,GACnE;IAEJ,OACE,CAAC,wCAAwC,CAAC,GAC1CuB,aACA,CAAC,4DAA4D,CAAC,GAC9D,CAAC,6CAA6C,CAAC,GAC/CnB,OACA,CAAC,WAAW,CAAC;AAEjB"} \ No newline at end of file diff --git a/dist/modules/seo/index.d.ts b/dist/modules/seo/index.d.ts index e76e211..1046bc6 100644 --- a/dist/modules/seo/index.d.ts +++ b/dist/modules/seo/index.d.ts @@ -14,6 +14,7 @@ export type { RobotsRules } from './buildRobots.js'; export { buildServiceJsonLd } from './buildServiceJsonLd.js'; export { buildSitemapEntries } from './buildSitemapEntries.js'; export type { SitemapEntry } from './buildSitemapEntries.js'; +export { buildSitemapXml } from './buildSitemapXml.js'; export { buildSiteNavigationJsonLd } from './buildSiteNavigationJsonLd.js'; export { buildWebSiteJsonLd } from './buildWebSiteJsonLd.js'; export { composeTitle } from './composeTitle.js'; diff --git a/dist/modules/seo/index.js b/dist/modules/seo/index.js index 3c59931..3be7324 100644 --- a/dist/modules/seo/index.js +++ b/dist/modules/seo/index.js @@ -10,6 +10,7 @@ export { buildOrganizationJsonLd } from './buildOrganizationJsonLd.js'; export { buildRobots } from './buildRobots.js'; export { buildServiceJsonLd } from './buildServiceJsonLd.js'; export { buildSitemapEntries } from './buildSitemapEntries.js'; +export { buildSitemapXml } from './buildSitemapXml.js'; export { buildSiteNavigationJsonLd } from './buildSiteNavigationJsonLd.js'; export { buildWebSiteJsonLd } from './buildWebSiteJsonLd.js'; export { composeTitle } from './composeTitle.js'; diff --git a/dist/modules/seo/index.js.map b/dist/modules/seo/index.js.map index f41e556..7789036 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 { 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 +{"version":3,"sources":["../../../src/modules/seo/index.ts"],"sourcesContent":["export { buildAutoFillMetaHook } from './autoFillMeta.js'\nexport type { AutoFillMapping } from './autoFillMeta.js'\nexport { buildArticleJsonLd } from './buildArticleJsonLd.js'\nexport { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js'\nexport { buildFaqJsonLd } from './buildFaqJsonLd.js'\nexport { buildIconsMetadata } from './buildIconsMetadata.js'\nexport { buildLlmsTxt } from './buildLlmsTxt.js'\nexport { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js'\nexport { buildMetadata } from './buildMetadata.js'\nexport type { PageMetadata } from './buildMetadata.js'\nexport { buildOrganizationJsonLd } from './buildOrganizationJsonLd.js'\nexport { buildRobots } from './buildRobots.js'\nexport type { RobotsRules } from './buildRobots.js'\nexport { buildServiceJsonLd } from './buildServiceJsonLd.js'\nexport { buildSitemapEntries } from './buildSitemapEntries.js'\nexport type { SitemapEntry } from './buildSitemapEntries.js'\nexport { buildSitemapXml } from './buildSitemapXml.js'\nexport { buildSiteNavigationJsonLd } from './buildSiteNavigationJsonLd.js'\nexport { buildWebSiteJsonLd } from './buildWebSiteJsonLd.js'\nexport { composeTitle } from './composeTitle.js'\nexport type { TitleOrder } from './composeTitle.js'\nexport { createMetadataGenerator } from './createMetadataGenerator.js'\nexport { createPageMetadata } from './createPageMetadata.js'\nexport { buildHreflangAlternates } from './hreflang.js'\nexport { injectAutoFillMeta } from './injectAutoFillMeta.js'\nexport { injectSeoTabs } from './injectSeoTabs.js'\nexport { readSiteMetaConfig } from './readSiteMetaConfig.js'\nexport type { SiteMetaConfig } from './readSiteMetaConfig.js'\nexport { buildSeoPlugin } from './seoPluginConfig.js'\nexport { slugsAcrossLocales } from './slugsAcrossLocales.js'\nexport type { SeoMeta, SeoOption } from './types.js'\nexport { validateFaviconField } from './validateFavicon.js'\n"],"names":["buildAutoFillMetaHook","buildArticleJsonLd","buildBreadcrumbJsonLd","buildFaqJsonLd","buildIconsMetadata","buildLlmsTxt","buildLocalBusinessJsonLd","buildMetadata","buildOrganizationJsonLd","buildRobots","buildServiceJsonLd","buildSitemapEntries","buildSitemapXml","buildSiteNavigationJsonLd","buildWebSiteJsonLd","composeTitle","createMetadataGenerator","createPageMetadata","buildHreflangAlternates","injectAutoFillMeta","injectSeoTabs","readSiteMetaConfig","buildSeoPlugin","slugsAcrossLocales","validateFaviconField"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,oBAAmB;AAEzD,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,qBAAqB,QAAQ,6BAA4B;AAClE,SAASC,cAAc,QAAQ,sBAAqB;AACpD,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,YAAY,QAAQ,oBAAmB;AAChD,SAASC,wBAAwB,QAAQ,gCAA+B;AACxE,SAASC,aAAa,QAAQ,qBAAoB;AAElD,SAASC,uBAAuB,QAAQ,+BAA8B;AACtE,SAASC,WAAW,QAAQ,mBAAkB;AAE9C,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,SAASC,eAAe,QAAQ,uBAAsB;AACtD,SAASC,yBAAyB,QAAQ,iCAAgC;AAC1E,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,YAAY,QAAQ,oBAAmB;AAEhD,SAASC,uBAAuB,QAAQ,+BAA8B;AACtE,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,uBAAuB,QAAQ,gBAAe;AACvD,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,aAAa,QAAQ,qBAAoB;AAClD,SAASC,kBAAkB,QAAQ,0BAAyB;AAE5D,SAASC,cAAc,QAAQ,uBAAsB;AACrD,SAASC,kBAAkB,QAAQ,0BAAyB;AAE5D,SAASC,oBAAoB,QAAQ,uBAAsB"} \ No newline at end of file diff --git a/docs/seo.md b/docs/seo.md index 516477f..5ed20fc 100644 --- a/docs/seo.md +++ b/docs/seo.md @@ -884,4 +884,47 @@ 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 +(inaczej llms.txt będzie ubogi). + +## Sitemap jako czytelna tabela (XSL stylesheet) + +Domyślny `/sitemap.xml` to surowy XML — Google go czyta, ale człowiek widzi +„ścianę tagów". Można ostylować przez XSL (przeglądarka renderuje tabelę), +zachowując poprawność dla crawlerów. + +### Ograniczenie Next + +Standardowy `app/sitemap.ts` (Next MetadataRoute) **nie pozwala** wstrzyknąć +``. Żeby mieć styl, serwuj sitemap własnym route przez +`buildSitemapXml` (string XML z odwołaniem do XSL): + +```ts +// app/sitemap.xml/route.ts (zamiast app/sitemap.ts) +import { buildSitemapXml } from '@intecion/ipal-kit' +import { sitemap } from '@/lib/content' + +export const dynamic = 'force-dynamic' + +export async function GET() { + const entries = await sitemap() + const xml = buildSitemapXml(entries, { stylesheetUrl: '/sitemap.xsl' }) + return new Response(xml, { + headers: { 'Content-Type': 'application/xml; charset=utf-8' }, + }) +} +``` + +### Plik XSL w /public + +Skopiuj `sitemap.xsl` (z pluginu: `node_modules/@intecion/ipal-kit/dist/modules/seo/assets/sitemap.xsl`) +do `public/sitemap.xsl` w projekcie. Zawiera responsywną tabelę (numer, URL, +data, języki) z dark mode. `stylesheetUrl: '/sitemap.xsl'` wskazuje na niego. + +### WAŻNE — jeden sitemap, nie dwa + +Jeśli używasz route `app/sitemap.xml/route.ts` (styled), **USUŃ** `app/sitemap.ts` +(MetadataRoute). Dwa sitemapy pod różnymi ścieżkami mylą crawlery. Wybierz jeden: +- **styled** (`sitemap.xml/route.ts` + buildSitemapXml + XSL) — ładna tabela +- **prosty** (`sitemap.ts` + reeksport z lib/content) — bez stylu, mniej kodu + +Styl to kosmetyka (Google czyta oba tak samo) — rób, jeśli klient/audyt tego chce. \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index d195200..d12b5ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -125,6 +125,7 @@ export { buildServiceJsonLd, } from './modules/seo/index.js' export { buildArticleJsonLd } from './modules/seo/index.js' +export { buildSitemapXml } from './modules/seo/index.js' export { buildLlmsTxt } from './modules/seo/index.js' export { buildSlugField, toSlug } from './modules/slug/index.js' export { buildR2Storage } from './modules/storage/index.js' diff --git a/src/modules/seo/assets/sitemap.xsl b/src/modules/seo/assets/sitemap.xsl new file mode 100644 index 0000000..1dcca51 --- /dev/null +++ b/src/modules/seo/assets/sitemap.xsl @@ -0,0 +1,85 @@ + + + + + + + + + Sitemap + + + +
+

Mapa witryny (Sitemap)

+

+ Ten plik jest odczytywany przez wyszukiwarki. + Adresów: +

+ + + + + + + + + + + + + + + + + + + +
#Adres URLOstatnia zmianaJęzyki
+ + + + + , + +
+
+ + +
+
diff --git a/src/modules/seo/buildSitemapXml.ts b/src/modules/seo/buildSitemapXml.ts new file mode 100644 index 0000000..75205af --- /dev/null +++ b/src/modules/seo/buildSitemapXml.ts @@ -0,0 +1,83 @@ +import type { SitemapEntry } from './buildSitemapEntries.js' + +/** + * Serializes sitemap entries to an XML STRING with an XSL stylesheet reference, + * so /sitemap.xml renders as a readable table in the browser (not raw XML) while + * staying a valid sitemap for crawlers. + * + * Why this exists alongside the Next MetadataRoute sitemap: Next's app/sitemap.ts + * (returning SitemapEntry[]) does NOT let you inject . To get + * the styled table, serve a custom route that returns this string instead: + * + * // app/sitemap.xml/route.ts + * import { buildSitemapXml } from '@intecion/ipal-kit' + * import { sitemap } from '@/lib/content' // your entries source + * export const dynamic = 'force-dynamic' + * export async function GET() { + * const entries = await sitemap() + * const xml = buildSitemapXml(entries, { stylesheetUrl: '/sitemap.xsl' }) + * return new Response(xml, { + * headers: { 'Content-Type': 'application/xml; charset=utf-8' }, + * }) + * } + * + * Put sitemap.xsl in the project's /public (copy from the plugin's assets, or + * serve it from a route). The points browsers at it; crawlers + * ignore it and read the XML. Include hreflang alternates as . + * + * NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one + * (the styled route OR the Next MetadataRoute). Two sitemaps at different paths + * confuse crawlers. + */ +export function buildSitemapXml( + entries: SitemapEntry[], + opts: { stylesheetUrl?: string } = {}, +): string { + const { stylesheetUrl } = opts + + const esc = (s: string): string => + s + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + + const urls = entries + .map((e) => { + const parts = [` ${esc(e.url)}`] + if (e.lastModified) { + const iso = + e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified) + parts.push(` ${esc(iso)}`) + } + if (e.changeFrequency) parts.push(` ${e.changeFrequency}`) + if (typeof e.priority === 'number') parts.push(` ${e.priority}`) + // hreflang alternates + const alternates = e.alternates?.languages + if (alternates) { + for (const [lang, href] of Object.entries(alternates)) { + if (typeof href === 'string') { + parts.push( + ` `, + ) + } + } + } + return ` \n${parts.join('\n')}\n ` + }) + .join('\n') + + const stylesheet = stylesheetUrl + ? `\n` + : '' + + return ( + `\n` + + stylesheet + + `\n` + + urls + + `\n` + ) +} diff --git a/src/modules/seo/index.ts b/src/modules/seo/index.ts index 074d270..8431bba 100644 --- a/src/modules/seo/index.ts +++ b/src/modules/seo/index.ts @@ -14,6 +14,7 @@ export type { RobotsRules } from './buildRobots.js' export { buildServiceJsonLd } from './buildServiceJsonLd.js' export { buildSitemapEntries } from './buildSitemapEntries.js' export type { SitemapEntry } from './buildSitemapEntries.js' +export { buildSitemapXml } from './buildSitemapXml.js' export { buildSiteNavigationJsonLd } from './buildSiteNavigationJsonLd.js' export { buildWebSiteJsonLd } from './buildWebSiteJsonLd.js' export { composeTitle } from './composeTitle.js'