Compare commits

..
12 Commits
57 changed files with 1669 additions and 80 deletions
+1
View File
@@ -7,3 +7,4 @@
*/
export { RenderBlocks } from '../modules/blocks/index.js';
export type { BlockComponentMap, BlockData, EnhanceProps, RenderBlocksProps, } from '../modules/blocks/index.js';
export { MediaPreconnect } from '../modules/storage/MediaPreconnect.js';
+1
View File
@@ -5,5 +5,6 @@
* lives here rather than in the main package entry to keep React out of the
* server-config bundle.
*/ export { RenderBlocks } from '../modules/blocks/index.js';
export { MediaPreconnect } from '../modules/storage/MediaPreconnect.js';
//# sourceMappingURL=rsc.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"sources":["../../src/exports/rsc.ts"],"sourcesContent":["/**\n * Entry point: ipal-kit/rsc\n *\n * Server-component exports. RenderBlocks is a React Server Component, so it\n * lives here rather than in the main package entry to keep React out of the\n * server-config bundle.\n */\nexport { RenderBlocks } from '../modules/blocks/index.js'\nexport type {\n BlockComponentMap,\n BlockData,\n EnhanceProps,\n RenderBlocksProps,\n} from '../modules/blocks/index.js'\n"],"names":["RenderBlocks"],"mappings":"AAAA;;;;;;CAMC,GACD,SAASA,YAAY,QAAQ,6BAA4B"}
{"version":3,"sources":["../../src/exports/rsc.ts"],"sourcesContent":["/**\n * Entry point: ipal-kit/rsc\n *\n * Server-component exports. RenderBlocks is a React Server Component, so it\n * lives here rather than in the main package entry to keep React out of the\n * server-config bundle.\n */\nexport { RenderBlocks } from '../modules/blocks/index.js'\nexport type {\n BlockComponentMap,\n BlockData,\n EnhanceProps,\n RenderBlocksProps,\n} from '../modules/blocks/index.js'\nexport { MediaPreconnect } from '../modules/storage/MediaPreconnect.js'\n"],"names":["RenderBlocks","MediaPreconnect"],"mappings":"AAAA;;;;;;CAMC,GACD,SAASA,YAAY,QAAQ,6BAA4B;AAOzD,SAASC,eAAe,QAAQ,wCAAuC"}
+4
View File
@@ -14,6 +14,10 @@ type BuildSiteIntegrationsArgs = {
* impossible to enter.)
*
* Unnamed tabs keep data flat (siteIntegrations.ga4MeasurementId).
*
* Note: R2 storage credentials are NOT here — storage is infrastructure and
* binds at boot, so its config lives in .env (R2_BUCKET, R2_ENDPOINT, ...),
* consumed by buildR2Storage. See docs/storage.md.
*/
export declare function buildSiteIntegrations({ additionalFields, }?: BuildSiteIntegrationsArgs): GlobalConfig;
export {};
+4 -5
View File
@@ -1,7 +1,6 @@
import { isAdmin } from '../../modules/access/index.js';
import { analyticsFields } from './fields/analytics.js';
import { smtpFields } from './fields/smtp.js';
import { storageFields } from './fields/storage.js';
import { turnstileFields } from './fields/turnstile.js';
/**
* Builds the SiteIntegrations global.
@@ -14,6 +13,10 @@ import { turnstileFields } from './fields/turnstile.js';
* impossible to enter.)
*
* Unnamed tabs keep data flat (siteIntegrations.ga4MeasurementId).
*
* Note: R2 storage credentials are NOT here — storage is infrastructure and
* binds at boot, so its config lives in .env (R2_BUCKET, R2_ENDPOINT, ...),
* consumed by buildR2Storage. See docs/storage.md.
*/ export function buildSiteIntegrations({ additionalFields } = {}) {
return {
slug: 'site-integrations',
@@ -42,10 +45,6 @@ import { turnstileFields } from './fields/turnstile.js';
fields: smtpFields,
label: 'SMTP'
},
{
fields: storageFields,
label: 'Storage'
},
...additionalFields?.length ? [
{
fields: additionalFields,
+1 -1
View File
@@ -1 +1 @@
{"version":3,"sources":["../../../src/globals/SiteIntegrations/index.ts"],"sourcesContent":["import type { Field, GlobalConfig } from 'payload'\n\nimport { isAdmin } from '../../modules/access/index.js'\nimport { analyticsFields } from './fields/analytics.js'\nimport { smtpFields } from './fields/smtp.js'\nimport { storageFields } from './fields/storage.js'\nimport { turnstileFields } from './fields/turnstile.js'\n\ntype BuildSiteIntegrationsArgs = {\n /** Extra fields injected by the client project */\n additionalFields?: Field[]\n}\n\n/**\n * Builds the SiteIntegrations global.\n *\n * Holds third-party service credentials. Access is enforced at the global\n * level — the whole global requires an authenticated user — so secrets stay\n * out of anonymous API responses while remaining editable in the admin panel\n * and readable via the server-side Local API. (Field-level read:false was\n * avoided because it also hides fields from the admin UI, making them\n * impossible to enter.)\n *\n * Unnamed tabs keep data flat (siteIntegrations.ga4MeasurementId).\n */\nexport function buildSiteIntegrations({\n additionalFields,\n}: BuildSiteIntegrationsArgs = {}): GlobalConfig {\n return {\n slug: 'site-integrations',\n access: {\n // Admin-only — secrets live here. Anonymous and non-admin users get\n // nothing through the API; admins read/edit in the panel and via Local API.\n read: ({ req: { user } }) => isAdmin(user),\n update: ({ req: { user } }) => isAdmin(user),\n },\n admin: {\n group: 'Settings',\n },\n fields: [\n {\n type: 'tabs',\n tabs: [\n { fields: analyticsFields, label: 'Analytics' },\n { fields: turnstileFields, label: 'Turnstile' },\n { fields: smtpFields, label: 'SMTP' },\n { fields: storageFields, label: 'Storage' },\n ...(additionalFields?.length ? [{ fields: additionalFields, label: 'Custom' }] : []),\n ],\n },\n ],\n label: 'Site Integrations',\n }\n}\n"],"names":["isAdmin","analyticsFields","smtpFields","storageFields","turnstileFields","buildSiteIntegrations","additionalFields","slug","access","read","req","user","update","admin","group","fields","type","tabs","label","length"],"mappings":"AAEA,SAASA,OAAO,QAAQ,gCAA+B;AACvD,SAASC,eAAe,QAAQ,wBAAuB;AACvD,SAASC,UAAU,QAAQ,mBAAkB;AAC7C,SAASC,aAAa,QAAQ,sBAAqB;AACnD,SAASC,eAAe,QAAQ,wBAAuB;AAOvD;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,sBAAsB,EACpCC,gBAAgB,EACU,GAAG,CAAC,CAAC;IAC/B,OAAO;QACLC,MAAM;QACNC,QAAQ;YACN,oEAAoE;YACpE,4EAA4E;YAC5EC,MAAM,CAAC,EAAEC,KAAK,EAAEC,IAAI,EAAE,EAAE,GAAKX,QAAQW;YACrCC,QAAQ,CAAC,EAAEF,KAAK,EAAEC,IAAI,EAAE,EAAE,GAAKX,QAAQW;QACzC;QACAE,OAAO;YACLC,OAAO;QACT;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;oBACJ;wBAAEF,QAAQd;wBAAiBiB,OAAO;oBAAY;oBAC9C;wBAAEH,QAAQX;wBAAiBc,OAAO;oBAAY;oBAC9C;wBAAEH,QAAQb;wBAAYgB,OAAO;oBAAO;oBACpC;wBAAEH,QAAQZ;wBAAee,OAAO;oBAAU;uBACtCZ,kBAAkBa,SAAS;wBAAC;4BAAEJ,QAAQT;4BAAkBY,OAAO;wBAAS;qBAAE,GAAG,EAAE;iBACpF;YACH;SACD;QACDA,OAAO;IACT;AACF"}
{"version":3,"sources":["../../../src/globals/SiteIntegrations/index.ts"],"sourcesContent":["import type { Field, GlobalConfig } from 'payload'\n\nimport { isAdmin } from '../../modules/access/index.js'\nimport { analyticsFields } from './fields/analytics.js'\nimport { smtpFields } from './fields/smtp.js'\nimport { turnstileFields } from './fields/turnstile.js'\n\ntype BuildSiteIntegrationsArgs = {\n /** Extra fields injected by the client project */\n additionalFields?: Field[]\n}\n\n/**\n * Builds the SiteIntegrations global.\n *\n * Holds third-party service credentials. Access is enforced at the global\n * level — the whole global requires an authenticated user — so secrets stay\n * out of anonymous API responses while remaining editable in the admin panel\n * and readable via the server-side Local API. (Field-level read:false was\n * avoided because it also hides fields from the admin UI, making them\n * impossible to enter.)\n *\n * Unnamed tabs keep data flat (siteIntegrations.ga4MeasurementId).\n *\n * Note: R2 storage credentials are NOT here — storage is infrastructure and\n * binds at boot, so its config lives in .env (R2_BUCKET, R2_ENDPOINT, ...),\n * consumed by buildR2Storage. See docs/storage.md.\n */\nexport function buildSiteIntegrations({\n additionalFields,\n}: BuildSiteIntegrationsArgs = {}): GlobalConfig {\n return {\n slug: 'site-integrations',\n access: {\n // Admin-only — secrets live here. Anonymous and non-admin users get\n // nothing through the API; admins read/edit in the panel and via Local API.\n read: ({ req: { user } }) => isAdmin(user),\n update: ({ req: { user } }) => isAdmin(user),\n },\n admin: {\n group: 'Settings',\n },\n fields: [\n {\n type: 'tabs',\n tabs: [\n { fields: analyticsFields, label: 'Analytics' },\n { fields: turnstileFields, label: 'Turnstile' },\n { fields: smtpFields, label: 'SMTP' },\n ...(additionalFields?.length ? [{ fields: additionalFields, label: 'Custom' }] : []),\n ],\n },\n ],\n label: 'Site Integrations',\n }\n}\n"],"names":["isAdmin","analyticsFields","smtpFields","turnstileFields","buildSiteIntegrations","additionalFields","slug","access","read","req","user","update","admin","group","fields","type","tabs","label","length"],"mappings":"AAEA,SAASA,OAAO,QAAQ,gCAA+B;AACvD,SAASC,eAAe,QAAQ,wBAAuB;AACvD,SAASC,UAAU,QAAQ,mBAAkB;AAC7C,SAASC,eAAe,QAAQ,wBAAuB;AAOvD;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,sBAAsB,EACpCC,gBAAgB,EACU,GAAG,CAAC,CAAC;IAC/B,OAAO;QACLC,MAAM;QACNC,QAAQ;YACN,oEAAoE;YACpE,4EAA4E;YAC5EC,MAAM,CAAC,EAAEC,KAAK,EAAEC,IAAI,EAAE,EAAE,GAAKV,QAAQU;YACrCC,QAAQ,CAAC,EAAEF,KAAK,EAAEC,IAAI,EAAE,EAAE,GAAKV,QAAQU;QACzC;QACAE,OAAO;YACLC,OAAO;QACT;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;oBACJ;wBAAEF,QAAQb;wBAAiBgB,OAAO;oBAAY;oBAC9C;wBAAEH,QAAQX;wBAAiBc,OAAO;oBAAY;oBAC9C;wBAAEH,QAAQZ;wBAAYe,OAAO;oBAAO;uBAChCZ,kBAAkBa,SAAS;wBAAC;4BAAEJ,QAAQT;4BAAkBY,OAAO;wBAAS;qBAAE,GAAG,EAAE;iBACpF;YACH;SACD;QACDA,OAAO;IACT;AACF"}
+1 -1
View File
@@ -1,6 +1,6 @@
import type { Field } from 'payload';
/**
* General site identity fields.
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo).
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo, favicon).
*/
export declare const generalFields: Field[];
+11 -3
View File
@@ -1,6 +1,7 @@
import { validateFaviconField } from '../../../modules/seo/index.js';
/**
* General site identity fields.
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo).
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo, favicon).
*/ export const generalFields = [
{
name: 'siteName',
@@ -63,7 +64,7 @@
name: 'logo',
type: 'upload',
admin: {
description: 'Primary site logo.'
description: 'Primary site logo. Also used for Organization structured data (buildOrganizationJsonLd).'
},
relationTo: 'media'
},
@@ -79,7 +80,14 @@
name: 'favicon',
type: 'upload',
admin: {
description: 'Square source icon (PNG or SVG) for the browser tab. Rendered by the frontend.'
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.
beforeValidate: [
validateFaviconField
]
},
relationTo: 'media'
}
+1 -1
View File
@@ -1 +1 @@
{"version":3,"sources":["../../../../src/globals/SiteSettings/fields/general.ts"],"sourcesContent":["import type { Field } from 'payload'\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: '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: 'Square source icon (PNG or SVG) for the browser tab. Rendered by the frontend.',\n },\n relationTo: 'media',\n },\n]\n"],"names":["generalFields","name","type","admin","description","localized","required","defaultValue","options","label","value","relationTo"],"mappings":"AAEA;;;CAGC,GACD,OAAO,MAAMA,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,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,aAAa;QACf;QACAO,YAAY;IACd;CACD,CAAA"}
{"version":3,"sources":["../../../../src/globals/SiteSettings/fields/general.ts"],"sourcesContent":["import type { Field } from 'payload'\n\nimport { validateFaviconField } from '../../../modules/seo/index.js'\n\n/**\n * General site identity fields.\n * Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo, 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"}
+5 -2
View File
@@ -20,18 +20,21 @@ export type { I18nConfig, LocaleDefinition, LocalizedSlugs } from './modules/i18
export { buildLocalizedPath, getDefaultLocale, getLocaleCodes, getLocaleDefinition, getLocalizedSlugs, isValidLocale, LOCALE_COOKIE_NAME, matchAcceptLanguage, negotiateLocale, switchLocalePath, } from './modules/i18n/index.js';
export type { LocaleMiddlewareResult } from './modules/i18n/index.js';
export { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from './modules/i18n/index.js';
export { normalizeFilename, normalizeFilenameHook } from './modules/media/index.js';
export { getNotificationTexts, NOTIFICATION_FALLBACK, resolveFormMessage, } from './modules/notifications/index.js';
export type { FormNotificationTexts, NotificationsData, NotificationTexts, } from './modules/notifications/index.js';
export type { PagesOption, SystemPageRole } from './modules/pages/index.js';
export { ALL_SYSTEM_PAGE_ROLES, getSystemPagePath } from './modules/pages/index.js';
export type { GlobalQueryOptions } from './modules/payload/index.js';
export { getGlobal, getSiteIntegrations, getSiteSettings, SITE_INTEGRATIONS_SLUG, SITE_SETTINGS_SLUG, } from './modules/payload/index.js';
export { buildSecurityHeaders } from './modules/security/index.js';
export type { BuildSecurityHeadersArgs, SecurityHeader } from './modules/security/index.js';
export { buildIconsMetadata, buildOrganizationJsonLd, validateFaviconField, } from './modules/seo/index.js';
export type { PageMetadata, SeoMeta, SeoOption } from './modules/seo/index.js';
export { buildHreflangAlternates, buildMetadata, composeTitle } from './modules/seo/index.js';
export type { AutoFillMapping, RobotsRules, SitemapEntry } from './modules/seo/index.js';
export { buildAutoFillMetaHook, buildRobots, buildSitemapEntries, createMetadataGenerator, createPageMetadata, injectAutoFillMeta, } from './modules/seo/index.js';
export { buildSlugField, toSlug } from './modules/slug/index.js';
export { NOTIFICATION_FALLBACK, getNotificationTexts, resolveFormMessage, } from './modules/notifications/index.js';
export type { FormNotificationTexts, NotificationsData, NotificationTexts, } from './modules/notifications/index.js';
export { buildR2Storage } from './modules/storage/index.js';
export { ipalKit } from './plugin.js';
export type { IpalOptions } from './types.js';
+6 -1
View File
@@ -12,13 +12,18 @@ export { buildFormsPlugin } from './modules/forms/formsPluginConfig.js';
export { createContentHelpers } from './modules/frontend/index.js';
export { buildLocalizedPath, getDefaultLocale, getLocaleCodes, getLocaleDefinition, getLocalizedSlugs, isValidLocale, LOCALE_COOKIE_NAME, matchAcceptLanguage, negotiateLocale, switchLocalePath } from './modules/i18n/index.js';
export { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from './modules/i18n/index.js';
// Media — filename normalization hook for upload collections (Media).
export { normalizeFilename, normalizeFilenameHook } from './modules/media/index.js';
export { getNotificationTexts, NOTIFICATION_FALLBACK, resolveFormMessage } from './modules/notifications/index.js';
export { ALL_SYSTEM_PAGE_ROLES, getSystemPagePath } from './modules/pages/index.js';
export { getGlobal, getSiteIntegrations, getSiteSettings, SITE_INTEGRATIONS_SLUG, SITE_SETTINGS_SLUG } from './modules/payload/index.js';
export { buildSecurityHeaders } from './modules/security/index.js';
export { buildIconsMetadata, buildOrganizationJsonLd, validateFaviconField } from './modules/seo/index.js';
export { buildHreflangAlternates, buildMetadata, composeTitle } from './modules/seo/index.js';
export { buildAutoFillMetaHook, buildRobots, buildSitemapEntries, createMetadataGenerator, createPageMetadata, injectAutoFillMeta } from './modules/seo/index.js';
export { buildSlugField, toSlug } from './modules/slug/index.js';
export { NOTIFICATION_FALLBACK, getNotificationTexts, resolveFormMessage } from './modules/notifications/index.js';
// Storage — Cloudflare R2 media offload, configured from .env.
export { buildR2Storage } from './modules/storage/index.js';
export { ipalKit } from './plugin.js';
//# sourceMappingURL=index.js.map
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
export { normalizeFilename, normalizeFilenameHook } from './normalizeFilename.js';
+3
View File
@@ -0,0 +1,3 @@
export { normalizeFilename, normalizeFilenameHook } from './normalizeFilename.js';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/media/index.ts"],"sourcesContent":["export { normalizeFilename, normalizeFilenameHook } from './normalizeFilename.js'\n"],"names":["normalizeFilename","normalizeFilenameHook"],"mappings":"AAAA,SAASA,iBAAiB,EAAEC,qBAAqB,QAAQ,yBAAwB"}
+25
View File
@@ -0,0 +1,25 @@
import type { CollectionBeforeOperationHook } from 'payload';
/**
* Normalizes a filename: slugifies the NAME part (diacritics, spaces, case)
* while preserving the extension. Keeps uploaded media URLs clean and portable.
*
* "Zdjęcie jeden nad morzem.jpg" → "zdjecie-jeden-nad-morzem.jpg"
* "Faktura #12 (2024).PDF" → "faktura-12-2024.pdf"
* "already-clean.webp" → "already-clean.webp"
*
* Why not toSlug(): toSlug uses strict:true, which would strip the dot and
* merge name+extension. Here we split on the LAST dot, slug the stem, lowercase
* the extension, and rejoin.
*/
export declare function normalizeFilename(filename: string): string;
/**
* beforeOperation hook for an upload collection (e.g. Media). Rewrites the
* incoming file's name to its normalized form before Payload stores it, so both
* the stored file and its DB filename are clean. Works with local disk and with
* cloud storage adapters (R2/S3) — it runs before the storage layer.
*
* Wire into your Media collection:
* import { normalizeFilenameHook } from '@intecion/ipal-kit'
* hooks: { beforeOperation: [normalizeFilenameHook] }
*/
export declare const normalizeFilenameHook: CollectionBeforeOperationHook;
+57
View File
@@ -0,0 +1,57 @@
import slugify from 'slugify';
/**
* Normalizes a filename: slugifies the NAME part (diacritics, spaces, case)
* while preserving the extension. Keeps uploaded media URLs clean and portable.
*
* "Zdjęcie jeden nad morzem.jpg" → "zdjecie-jeden-nad-morzem.jpg"
* "Faktura #12 (2024).PDF" → "faktura-12-2024.pdf"
* "already-clean.webp" → "already-clean.webp"
*
* Why not toSlug(): toSlug uses strict:true, which would strip the dot and
* merge name+extension. Here we split on the LAST dot, slug the stem, lowercase
* the extension, and rejoin.
*/ export function normalizeFilename(filename) {
const lastDot = filename.lastIndexOf('.');
// No extension (or leading-dot dotfile) → slug the whole thing.
if (lastDot <= 0) {
return slugify(filename, {
lower: true,
strict: true,
trim: true
});
}
const stem = filename.slice(0, lastDot);
const ext = filename.slice(lastDot + 1).toLowerCase();
const cleanStem = slugify(stem, {
lower: true,
strict: true,
trim: true
});
const cleanExt = slugify(ext, {
lower: true,
strict: true,
trim: true
});
// Stem could slug to empty (e.g. filename was all symbols) — fall back so we
// never produce a nameless file.
const safeStem = cleanStem || 'plik';
return cleanExt ? `${safeStem}.${cleanExt}` : safeStem;
}
/**
* beforeOperation hook for an upload collection (e.g. Media). Rewrites the
* incoming file's name to its normalized form before Payload stores it, so both
* the stored file and its DB filename are clean. Works with local disk and with
* cloud storage adapters (R2/S3) — it runs before the storage layer.
*
* Wire into your Media collection:
* import { normalizeFilenameHook } from '@intecion/ipal-kit'
* hooks: { beforeOperation: [normalizeFilenameHook] }
*/ export const normalizeFilenameHook = ({ req, operation })=>{
if (operation !== 'create' && operation !== 'update') return;
const file = req.file;
if (file?.name) {
file.name = normalizeFilename(file.name);
}
};
//# sourceMappingURL=normalizeFilename.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/media/normalizeFilename.ts"],"sourcesContent":["import type { CollectionBeforeOperationHook } from 'payload'\nimport slugify from 'slugify'\n\n/**\n * Normalizes a filename: slugifies the NAME part (diacritics, spaces, case)\n * while preserving the extension. Keeps uploaded media URLs clean and portable.\n *\n * \"Zdjęcie jeden nad morzem.jpg\" → \"zdjecie-jeden-nad-morzem.jpg\"\n * \"Faktura #12 (2024).PDF\" → \"faktura-12-2024.pdf\"\n * \"already-clean.webp\" → \"already-clean.webp\"\n *\n * Why not toSlug(): toSlug uses strict:true, which would strip the dot and\n * merge name+extension. Here we split on the LAST dot, slug the stem, lowercase\n * the extension, and rejoin.\n */\nexport function normalizeFilename(filename: string): string {\n const lastDot = filename.lastIndexOf('.')\n\n // No extension (or leading-dot dotfile) → slug the whole thing.\n if (lastDot <= 0) {\n return slugify(filename, { lower: true, strict: true, trim: true })\n }\n\n const stem = filename.slice(0, lastDot)\n const ext = filename.slice(lastDot + 1).toLowerCase()\n\n const cleanStem = slugify(stem, { lower: true, strict: true, trim: true })\n const cleanExt = slugify(ext, { lower: true, strict: true, trim: true })\n\n // Stem could slug to empty (e.g. filename was all symbols) — fall back so we\n // never produce a nameless file.\n const safeStem = cleanStem || 'plik'\n\n return cleanExt ? `${safeStem}.${cleanExt}` : safeStem\n}\n\n/**\n * beforeOperation hook for an upload collection (e.g. Media). Rewrites the\n * incoming file's name to its normalized form before Payload stores it, so both\n * the stored file and its DB filename are clean. Works with local disk and with\n * cloud storage adapters (R2/S3) — it runs before the storage layer.\n *\n * Wire into your Media collection:\n * import { normalizeFilenameHook } from '@intecion/ipal-kit'\n * hooks: { beforeOperation: [normalizeFilenameHook] }\n */\nexport const normalizeFilenameHook: CollectionBeforeOperationHook = ({ req, operation }) => {\n if (operation !== 'create' && operation !== 'update') return\n const file = req.file\n if (file?.name) {\n file.name = normalizeFilename(file.name)\n }\n}\n"],"names":["slugify","normalizeFilename","filename","lastDot","lastIndexOf","lower","strict","trim","stem","slice","ext","toLowerCase","cleanStem","cleanExt","safeStem","normalizeFilenameHook","req","operation","file","name"],"mappings":"AACA,OAAOA,aAAa,UAAS;AAE7B;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,kBAAkBC,QAAgB;IAChD,MAAMC,UAAUD,SAASE,WAAW,CAAC;IAErC,gEAAgE;IAChE,IAAID,WAAW,GAAG;QAChB,OAAOH,QAAQE,UAAU;YAAEG,OAAO;YAAMC,QAAQ;YAAMC,MAAM;QAAK;IACnE;IAEA,MAAMC,OAAON,SAASO,KAAK,CAAC,GAAGN;IAC/B,MAAMO,MAAMR,SAASO,KAAK,CAACN,UAAU,GAAGQ,WAAW;IAEnD,MAAMC,YAAYZ,QAAQQ,MAAM;QAAEH,OAAO;QAAMC,QAAQ;QAAMC,MAAM;IAAK;IACxE,MAAMM,WAAWb,QAAQU,KAAK;QAAEL,OAAO;QAAMC,QAAQ;QAAMC,MAAM;IAAK;IAEtE,6EAA6E;IAC7E,iCAAiC;IACjC,MAAMO,WAAWF,aAAa;IAE9B,OAAOC,WAAW,GAAGC,SAAS,CAAC,EAAED,UAAU,GAAGC;AAChD;AAEA;;;;;;;;;CASC,GACD,OAAO,MAAMC,wBAAuD,CAAC,EAAEC,GAAG,EAAEC,SAAS,EAAE;IACrF,IAAIA,cAAc,YAAYA,cAAc,UAAU;IACtD,MAAMC,OAAOF,IAAIE,IAAI;IACrB,IAAIA,MAAMC,MAAM;QACdD,KAAKC,IAAI,GAAGlB,kBAAkBiB,KAAKC,IAAI;IACzC;AACF,EAAC"}
+33
View File
@@ -0,0 +1,33 @@
import type { Metadata } from 'next';
type MediaLike = {
height?: null | number;
mimeType?: null | string;
url?: null | string;
width?: null | number;
} | null | undefined;
/**
* Builds Next.js `icons` metadata (favicon / apple-touch-icon) from the panel's
* favicon upload, so the browser tab AND Google get a proper <link rel="icon">.
*
* Why the plugin must do this (not the project): favicon-in-Google has strict
* rules — a real <link rel="icon"> in <head>, square, ≥48×48, at a stable URL.
* Leaving it to each project meant inconsistent hand-rolled tags and no favicon
* in search results. This generates the tags correctly, every time, from the
* panel field.
*
* Favicon is GLOBAL (same across pages), so call this once in the ROOT layout's
* generateMetadata — not per page:
*
* import { buildIconsMetadata } from '@intecion/ipal-kit'
* export async function generateMetadata(): Promise<Metadata> {
* const settings = await getSettings(locale)
* return buildIconsMetadata(settings.favicon)
* }
*
* Google notes: it caches favicons separately and slowly (days/weeks), and only
* shows them for icons it deems valid. Warn on too-small icons at upload time
* (see the media validation hook) so editors don't ship a <48px favicon Google
* will reject.
*/
export declare function buildIconsMetadata(favicon: MediaLike): Metadata;
export {};
+54
View File
@@ -0,0 +1,54 @@
/**
* Builds Next.js `icons` metadata (favicon / apple-touch-icon) from the panel's
* favicon upload, so the browser tab AND Google get a proper <link rel="icon">.
*
* Why the plugin must do this (not the project): favicon-in-Google has strict
* rules — a real <link rel="icon"> in <head>, square, ≥48×48, at a stable URL.
* Leaving it to each project meant inconsistent hand-rolled tags and no favicon
* in search results. This generates the tags correctly, every time, from the
* panel field.
*
* Favicon is GLOBAL (same across pages), so call this once in the ROOT layout's
* generateMetadata — not per page:
*
* import { buildIconsMetadata } from '@intecion/ipal-kit'
* export async function generateMetadata(): Promise<Metadata> {
* const settings = await getSettings(locale)
* return buildIconsMetadata(settings.favicon)
* }
*
* Google notes: it caches favicons separately and slowly (days/weeks), and only
* shows them for icons it deems valid. Warn on too-small icons at upload time
* (see the media validation hook) so editors don't ship a <48px favicon Google
* will reject.
*/ export function buildIconsMetadata(favicon) {
const url = favicon?.url;
if (!url) {
return {};
}
const isSvg = favicon?.mimeType === 'image/svg+xml' || url.endsWith('.svg');
return {
icons: {
// Main favicon. SVG scales; PNG should be ≥48×48 (ideally 96 or 192).
icon: isSvg ? [
{
type: 'image/svg+xml',
url
}
] : [
{
sizes: 'any',
url
}
],
// Apple touch icon (home-screen bookmark on iOS). Reuses the same asset.
apple: [
{
url
}
]
}
};
}
//# sourceMappingURL=buildIconsMetadata.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/seo/buildIconsMetadata.ts"],"sourcesContent":["import type { Metadata } from 'next'\n\ntype MediaLike =\n | { height?: null | number; mimeType?: null | string; url?: null | string; width?: null | number }\n | null\n | undefined\n\n/**\n * Builds Next.js `icons` metadata (favicon / apple-touch-icon) from the panel's\n * favicon upload, so the browser tab AND Google get a proper <link rel=\"icon\">.\n *\n * Why the plugin must do this (not the project): favicon-in-Google has strict\n * rules — a real <link rel=\"icon\"> in <head>, square, ≥48×48, at a stable URL.\n * Leaving it to each project meant inconsistent hand-rolled tags and no favicon\n * in search results. This generates the tags correctly, every time, from the\n * panel field.\n *\n * Favicon is GLOBAL (same across pages), so call this once in the ROOT layout's\n * generateMetadata — not per page:\n *\n * import { buildIconsMetadata } from '@intecion/ipal-kit'\n * export async function generateMetadata(): Promise<Metadata> {\n * const settings = await getSettings(locale)\n * return buildIconsMetadata(settings.favicon)\n * }\n *\n * Google notes: it caches favicons separately and slowly (days/weeks), and only\n * shows them for icons it deems valid. Warn on too-small icons at upload time\n * (see the media validation hook) so editors don't ship a <48px favicon Google\n * will reject.\n */\nexport function buildIconsMetadata(favicon: MediaLike): Metadata {\n const url = favicon?.url\n if (!url) {return {}}\n\n const isSvg = favicon?.mimeType === 'image/svg+xml' || url.endsWith('.svg')\n\n return {\n icons: {\n // Main favicon. SVG scales; PNG should be ≥48×48 (ideally 96 or 192).\n icon: isSvg ? [{ type: 'image/svg+xml', url }] : [{ sizes: 'any', url }],\n // Apple touch icon (home-screen bookmark on iOS). Reuses the same asset.\n apple: [{ url }],\n },\n }\n}\n"],"names":["buildIconsMetadata","favicon","url","isSvg","mimeType","endsWith","icons","icon","type","sizes","apple"],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GACD,OAAO,SAASA,mBAAmBC,OAAkB;IACnD,MAAMC,MAAMD,SAASC;IACrB,IAAI,CAACA,KAAK;QAAC,OAAO,CAAC;IAAC;IAEpB,MAAMC,QAAQF,SAASG,aAAa,mBAAmBF,IAAIG,QAAQ,CAAC;IAEpE,OAAO;QACLC,OAAO;YACL,sEAAsE;YACtEC,MAAMJ,QAAQ;gBAAC;oBAAEK,MAAM;oBAAiBN;gBAAI;aAAE,GAAG;gBAAC;oBAAEO,OAAO;oBAAOP;gBAAI;aAAE;YACxE,yEAAyE;YACzEQ,OAAO;gBAAC;oBAAER;gBAAI;aAAE;QAClB;IACF;AACF"}
+41
View File
@@ -0,0 +1,41 @@
type MediaLike = {
url?: null | string;
} | null | undefined;
type OrganizationJsonLdArgs = {
/** Logo media (from panel). Google uses this for brand knowledge panels. */
logo?: MediaLike;
/** Organization / site name. */
name: string;
/** Optional social / official profile URLs (sameAs). */
sameAs?: string[];
/** Absolute site URL (https://…). */
url: string;
};
/**
* Builds Organization JSON-LD (schema.org) — helps Google associate the site
* with a brand: name, logo, official links. Improves how the site appears in
* search (brand recognition, logo in knowledge panels) and is a signal used
* alongside favicon for identity.
*
* Returns a plain object; the project renders it as a <script type="application/
* ld+json"> in the root layout:
*
* import { buildOrganizationJsonLd } from '@intecion/ipal-kit'
* const jsonLd = buildOrganizationJsonLd({
* name: settings.siteName, url: baseUrl, logo: settings.logo,
* })
* <script type="application/ld+json"
* dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
*
* Data comes from the panel (siteName, logo) — nothing hardcoded. Emit once in
* the root layout (Organization is site-wide, not per page).
*/
export declare function buildOrganizationJsonLd({ name, logo, sameAs, url }: OrganizationJsonLdArgs): {
sameAs?: string[] | undefined;
logo?: string | undefined;
name: string;
'@context': string;
'@type': string;
url: string;
};
export {};
+35
View File
@@ -0,0 +1,35 @@
/**
* Builds Organization JSON-LD (schema.org) — helps Google associate the site
* with a brand: name, logo, official links. Improves how the site appears in
* search (brand recognition, logo in knowledge panels) and is a signal used
* alongside favicon for identity.
*
* Returns a plain object; the project renders it as a <script type="application/
* ld+json"> in the root layout:
*
* import { buildOrganizationJsonLd } from '@intecion/ipal-kit'
* const jsonLd = buildOrganizationJsonLd({
* name: settings.siteName, url: baseUrl, logo: settings.logo,
* })
* <script type="application/ld+json"
* dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
*
* Data comes from the panel (siteName, logo) — nothing hardcoded. Emit once in
* the root layout (Organization is site-wide, not per page).
*/ export function buildOrganizationJsonLd({ name, logo, sameAs, url }) {
const logoUrl = logo?.url;
return {
name,
'@context': 'https://schema.org',
'@type': 'Organization',
url,
...logoUrl ? {
logo: logoUrl.startsWith('http') ? logoUrl : `${url}${logoUrl}`
} : {},
...sameAs && sameAs.length > 0 ? {
sameAs
} : {}
};
}
//# sourceMappingURL=buildOrganizationJsonLd.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/seo/buildOrganizationJsonLd.ts"],"sourcesContent":["type MediaLike = { url?: null | string } | null | undefined\n\ntype OrganizationJsonLdArgs = {\n /** Logo media (from panel). Google uses this for brand knowledge panels. */\n logo?: MediaLike\n /** Organization / site name. */\n name: string\n /** Optional social / official profile URLs (sameAs). */\n sameAs?: string[]\n /** Absolute site URL (https://…). */\n url: string\n}\n\n/**\n * Builds Organization JSON-LD (schema.org) — helps Google associate the site\n * with a brand: name, logo, official links. Improves how the site appears in\n * search (brand recognition, logo in knowledge panels) and is a signal used\n * alongside favicon for identity.\n *\n * Returns a plain object; the project renders it as a <script type=\"application/\n * ld+json\"> in the root layout:\n *\n * import { buildOrganizationJsonLd } from '@intecion/ipal-kit'\n * const jsonLd = buildOrganizationJsonLd({\n * name: settings.siteName, url: baseUrl, logo: settings.logo,\n * })\n * <script type=\"application/ld+json\"\n * dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />\n *\n * Data comes from the panel (siteName, logo) — nothing hardcoded. Emit once in\n * the root layout (Organization is site-wide, not per page).\n */\nexport function buildOrganizationJsonLd({ name, logo, sameAs, url }: OrganizationJsonLdArgs) {\n const logoUrl = logo?.url\n\n return {\n name,\n '@context': 'https://schema.org',\n '@type': 'Organization',\n url,\n ...(logoUrl ? { logo: logoUrl.startsWith('http') ? logoUrl : `${url}${logoUrl}` } : {}),\n ...(sameAs && sameAs.length > 0 ? { sameAs } : {}),\n }\n}\n"],"names":["buildOrganizationJsonLd","name","logo","sameAs","url","logoUrl","startsWith","length"],"mappings":"AAaA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASA,wBAAwB,EAAEC,IAAI,EAAEC,IAAI,EAAEC,MAAM,EAAEC,GAAG,EAA0B;IACzF,MAAMC,UAAUH,MAAME;IAEtB,OAAO;QACLH;QACA,YAAY;QACZ,SAAS;QACTG;QACA,GAAIC,UAAU;YAAEH,MAAMG,QAAQC,UAAU,CAAC,UAAUD,UAAU,GAAGD,MAAMC,SAAS;QAAC,IAAI,CAAC,CAAC;QACtF,GAAIF,UAAUA,OAAOI,MAAM,GAAG,IAAI;YAAEJ;QAAO,IAAI,CAAC,CAAC;IACnD;AACF"}
+3
View File
@@ -1,7 +1,9 @@
export { buildAutoFillMetaHook } from './autoFillMeta.js';
export type { AutoFillMapping } from './autoFillMeta.js';
export { buildIconsMetadata } from './buildIconsMetadata.js';
export { buildMetadata } from './buildMetadata.js';
export type { PageMetadata } from './buildMetadata.js';
export { buildOrganizationJsonLd } from './buildOrganizationJsonLd.js';
export { buildRobots } from './buildRobots.js';
export type { RobotsRules } from './buildRobots.js';
export { buildSitemapEntries } from './buildSitemapEntries.js';
@@ -18,3 +20,4 @@ export type { SiteMetaConfig } from './readSiteMetaConfig.js';
export { buildSeoPlugin } from './seoPluginConfig.js';
export { slugsAcrossLocales } from './slugsAcrossLocales.js';
export type { SeoMeta, SeoOption } from './types.js';
export { validateFaviconField } from './validateFavicon.js';
+3
View File
@@ -1,5 +1,7 @@
export { buildAutoFillMetaHook } from './autoFillMeta.js';
export { buildIconsMetadata } from './buildIconsMetadata.js';
export { buildMetadata } from './buildMetadata.js';
export { buildOrganizationJsonLd } from './buildOrganizationJsonLd.js';
export { buildRobots } from './buildRobots.js';
export { buildSitemapEntries } from './buildSitemapEntries.js';
export { composeTitle } from './composeTitle.js';
@@ -11,5 +13,6 @@ export { injectSeoTabs } from './injectSeoTabs.js';
export { readSiteMetaConfig } from './readSiteMetaConfig.js';
export { buildSeoPlugin } from './seoPluginConfig.js';
export { slugsAcrossLocales } from './slugsAcrossLocales.js';
export { validateFaviconField } from './validateFavicon.js';
//# sourceMappingURL=index.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"sources":["../../../src/modules/seo/index.ts"],"sourcesContent":["export { buildAutoFillMetaHook } from './autoFillMeta.js'\nexport type { AutoFillMapping } from './autoFillMeta.js'\nexport { buildMetadata } from './buildMetadata.js'\nexport type { PageMetadata } from './buildMetadata.js'\nexport { buildRobots } from './buildRobots.js'\nexport type { RobotsRules } from './buildRobots.js'\nexport { buildSitemapEntries } from './buildSitemapEntries.js'\nexport type { SitemapEntry } from './buildSitemapEntries.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'\n"],"names":["buildAutoFillMetaHook","buildMetadata","buildRobots","buildSitemapEntries","composeTitle","createMetadataGenerator","createPageMetadata","buildHreflangAlternates","injectAutoFillMeta","injectSeoTabs","readSiteMetaConfig","buildSeoPlugin","slugsAcrossLocales"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,oBAAmB;AAEzD,SAASC,aAAa,QAAQ,qBAAoB;AAElD,SAASC,WAAW,QAAQ,mBAAkB;AAE9C,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,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"}
{"version":3,"sources":["../../../src/modules/seo/index.ts"],"sourcesContent":["export { buildAutoFillMetaHook } from './autoFillMeta.js'\nexport type { AutoFillMapping } from './autoFillMeta.js'\nexport { buildIconsMetadata } from './buildIconsMetadata.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 { buildSitemapEntries } from './buildSitemapEntries.js'\nexport type { SitemapEntry } from './buildSitemapEntries.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","buildIconsMetadata","buildMetadata","buildOrganizationJsonLd","buildRobots","buildSitemapEntries","composeTitle","createMetadataGenerator","createPageMetadata","buildHreflangAlternates","injectAutoFillMeta","injectSeoTabs","readSiteMetaConfig","buildSeoPlugin","slugsAcrossLocales","validateFaviconField"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,oBAAmB;AAEzD,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,aAAa,QAAQ,qBAAoB;AAElD,SAASC,uBAAuB,QAAQ,+BAA8B;AACtE,SAASC,WAAW,QAAQ,mBAAkB;AAE9C,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,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"}
+15
View File
@@ -0,0 +1,15 @@
import type { FieldHook } from 'payload';
/**
* Field validation for the favicon upload: Google rejects favicons under 48×48,
* so warn the editor at save time if the uploaded icon is too small or not
* square. This ENFORCES the requirement instead of silently shipping a favicon
* Google won't display.
*
* Attach to the favicon field's validate (or as a beforeValidate hook on the
* Media relationship). Non-blocking by default — returns a warning string that
* Payload surfaces; make it throw if you want a hard block.
*
* Note: dimensions come from the related Media doc (Payload stores width/height
* for image uploads), so this checks the resolved upload, not the raw file.
*/
export declare const validateFaviconField: FieldHook;
+44
View File
@@ -0,0 +1,44 @@
/**
* Field validation for the favicon upload: Google rejects favicons under 48×48,
* so warn the editor at save time if the uploaded icon is too small or not
* square. This ENFORCES the requirement instead of silently shipping a favicon
* Google won't display.
*
* Attach to the favicon field's validate (or as a beforeValidate hook on the
* Media relationship). Non-blocking by default — returns a warning string that
* Payload surfaces; make it throw if you want a hard block.
*
* Note: dimensions come from the related Media doc (Payload stores width/height
* for image uploads), so this checks the resolved upload, not the raw file.
*/ export const validateFaviconField = async ({ req, value })=>{
if (!value) {
return value;
} // no favicon set → nothing to validate (optional field)
try {
const media = await req.payload.findByID({
id: typeof value === 'object' ? value.id : value,
collection: 'media',
depth: 0
});
const width = media.width;
const height = media.height;
const mimeType = media.mimeType;
// SVG scales infinitely — skip size checks.
if (mimeType === 'image/svg+xml') {
return value;
}
if (typeof width === 'number' && typeof height === 'number') {
if (width < 48 || height < 48) {
req.payload.logger.warn(`[ipal] Favicon is ${width}×${height}px. Google requires ≥48×48 to ` + `display it in search results. Upload a larger square icon (96 or 192px).`);
}
if (width !== height) {
req.payload.logger.warn(`[ipal] Favicon is not square (${width}×${height}). Use a square icon ` + `so it isn't cropped in the browser tab or search results.`);
}
}
} catch {
// Media lookup failed — don't block the save over a validation warning.
}
return value;
};
//# sourceMappingURL=validateFavicon.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/seo/validateFavicon.ts"],"sourcesContent":["import type { FieldHook } from 'payload'\n\n/**\n * Field validation for the favicon upload: Google rejects favicons under 48×48,\n * so warn the editor at save time if the uploaded icon is too small or not\n * square. This ENFORCES the requirement instead of silently shipping a favicon\n * Google won't display.\n *\n * Attach to the favicon field's validate (or as a beforeValidate hook on the\n * Media relationship). Non-blocking by default — returns a warning string that\n * Payload surfaces; make it throw if you want a hard block.\n *\n * Note: dimensions come from the related Media doc (Payload stores width/height\n * for image uploads), so this checks the resolved upload, not the raw file.\n */\nexport const validateFaviconField: FieldHook = async ({ req, value }) => {\n if (!value) {return value} // no favicon set → nothing to validate (optional field)\n\n try {\n const media = await req.payload.findByID({\n id: typeof value === 'object' ? (value as { id: string }).id : value,\n collection: 'media',\n depth: 0,\n })\n\n const width = (media as { width?: number }).width\n const height = (media as { height?: number }).height\n const mimeType = (media as { mimeType?: string }).mimeType\n\n // SVG scales infinitely — skip size checks.\n if (mimeType === 'image/svg+xml') {return value}\n\n if (typeof width === 'number' && typeof height === 'number') {\n if (width < 48 || height < 48) {\n req.payload.logger.warn(\n `[ipal] Favicon is ${width}×${height}px. Google requires ≥48×48 to ` +\n `display it in search results. Upload a larger square icon (96 or 192px).`,\n )\n }\n if (width !== height) {\n req.payload.logger.warn(\n `[ipal] Favicon is not square (${width}×${height}). Use a square icon ` +\n `so it isn't cropped in the browser tab or search results.`,\n )\n }\n }\n } catch {\n // Media lookup failed — don't block the save over a validation warning.\n }\n\n return value\n}\n"],"names":["validateFaviconField","req","value","media","payload","findByID","id","collection","depth","width","height","mimeType","logger","warn"],"mappings":"AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMA,uBAAkC,OAAO,EAAEC,GAAG,EAAEC,KAAK,EAAE;IAClE,IAAI,CAACA,OAAO;QAAC,OAAOA;IAAK,EAAE,wDAAwD;IAEnF,IAAI;QACF,MAAMC,QAAQ,MAAMF,IAAIG,OAAO,CAACC,QAAQ,CAAC;YACvCC,IAAI,OAAOJ,UAAU,WAAW,AAACA,MAAyBI,EAAE,GAAGJ;YAC/DK,YAAY;YACZC,OAAO;QACT;QAEA,MAAMC,QAAQ,AAACN,MAA6BM,KAAK;QACjD,MAAMC,SAAS,AAACP,MAA8BO,MAAM;QACpD,MAAMC,WAAW,AAACR,MAAgCQ,QAAQ;QAE1D,4CAA4C;QAC5C,IAAIA,aAAa,iBAAiB;YAAC,OAAOT;QAAK;QAE/C,IAAI,OAAOO,UAAU,YAAY,OAAOC,WAAW,UAAU;YAC3D,IAAID,QAAQ,MAAMC,SAAS,IAAI;gBAC7BT,IAAIG,OAAO,CAACQ,MAAM,CAACC,IAAI,CACrB,CAAC,kBAAkB,EAAEJ,MAAM,CAAC,EAAEC,OAAO,8BAA8B,CAAC,GAClE,CAAC,wEAAwE,CAAC;YAEhF;YACA,IAAID,UAAUC,QAAQ;gBACpBT,IAAIG,OAAO,CAACQ,MAAM,CAACC,IAAI,CACrB,CAAC,8BAA8B,EAAEJ,MAAM,CAAC,EAAEC,OAAO,qBAAqB,CAAC,GACrE,CAAC,yDAAyD,CAAC;YAEjE;QACF;IACF,EAAE,OAAM;IACN,wEAAwE;IAC1E;IAEA,OAAOR;AACT,EAAC"}
+19
View File
@@ -0,0 +1,19 @@
/**
* Emits <link rel="preconnect"> + <link rel="dns-prefetch"> for the media CDN
* domain (R2_PUBLIC_URL), so the browser opens the TLS/DNS connection to the
* media host early — before it hits the first <img>. Saves ~150–300ms on the
* first image load.
*
* Reads the domain from R2_PUBLIC_URL (the same env var buildR2Storage uses),
* so there's ONE source of truth — no per-project hardcoded domain. Renders
* nothing when R2_PUBLIC_URL isn't set (local disk / no CDN → nothing to
* preconnect).
*
* Server Component — drop it in the <head> of your locale layout:
*
* import { MediaPreconnect } from '@intecion/ipal-kit/rsc'
* // in <head> (or top of <body> — Next hoists link tags):
* <MediaPreconnect />
*/
export declare function MediaPreconnect(): import("react/jsx-runtime").JSX.Element | null;
export default MediaPreconnect;
+47
View File
@@ -0,0 +1,47 @@
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
/**
* Emits <link rel="preconnect"> + <link rel="dns-prefetch"> for the media CDN
* domain (R2_PUBLIC_URL), so the browser opens the TLS/DNS connection to the
* media host early — before it hits the first <img>. Saves ~150–300ms on the
* first image load.
*
* Reads the domain from R2_PUBLIC_URL (the same env var buildR2Storage uses),
* so there's ONE source of truth — no per-project hardcoded domain. Renders
* nothing when R2_PUBLIC_URL isn't set (local disk / no CDN → nothing to
* preconnect).
*
* Server Component — drop it in the <head> of your locale layout:
*
* import { MediaPreconnect } from '@intecion/ipal-kit/rsc'
* // in <head> (or top of <body> — Next hoists link tags):
* <MediaPreconnect />
*/ export function MediaPreconnect() {
const publicUrl = process.env.R2_PUBLIC_URL?.replace(/\/$/, '');
if (!publicUrl) {
return null;
}
// Origin only (scheme + host) — preconnect targets an origin, not a path.
let origin;
try {
origin = new URL(publicUrl).origin;
} catch {
return null // malformed URL → skip rather than emit a broken tag
;
}
return /*#__PURE__*/ _jsxs(_Fragment, {
children: [
/*#__PURE__*/ _jsx("link", {
crossOrigin: "anonymous",
href: origin,
rel: "preconnect"
}),
/*#__PURE__*/ _jsx("link", {
href: origin,
rel: "dns-prefetch"
})
]
});
}
export default MediaPreconnect;
//# sourceMappingURL=MediaPreconnect.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/storage/MediaPreconnect.tsx"],"sourcesContent":["/**\n * Emits <link rel=\"preconnect\"> + <link rel=\"dns-prefetch\"> for the media CDN\n * domain (R2_PUBLIC_URL), so the browser opens the TLS/DNS connection to the\n * media host early — before it hits the first <img>. Saves ~150–300ms on the\n * first image load.\n *\n * Reads the domain from R2_PUBLIC_URL (the same env var buildR2Storage uses),\n * so there's ONE source of truth — no per-project hardcoded domain. Renders\n * nothing when R2_PUBLIC_URL isn't set (local disk / no CDN → nothing to\n * preconnect).\n *\n * Server Component — drop it in the <head> of your locale layout:\n *\n * import { MediaPreconnect } from '@intecion/ipal-kit/rsc'\n * // in <head> (or top of <body> — Next hoists link tags):\n * <MediaPreconnect />\n */\nexport function MediaPreconnect() {\n const publicUrl = process.env.R2_PUBLIC_URL?.replace(/\\/$/, '')\n if (!publicUrl) {return null}\n\n // Origin only (scheme + host) — preconnect targets an origin, not a path.\n let origin: string\n try {\n origin = new URL(publicUrl).origin\n } catch {\n return null // malformed URL → skip rather than emit a broken tag\n }\n\n return (\n <>\n <link crossOrigin=\"anonymous\" href={origin} rel=\"preconnect\" />\n <link href={origin} rel=\"dns-prefetch\" />\n </>\n )\n}\n\nexport default MediaPreconnect\n"],"names":["MediaPreconnect","publicUrl","process","env","R2_PUBLIC_URL","replace","origin","URL","link","crossOrigin","href","rel"],"mappings":";AAAA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASA;IACd,MAAMC,YAAYC,QAAQC,GAAG,CAACC,aAAa,EAAEC,QAAQ,OAAO;IAC5D,IAAI,CAACJ,WAAW;QAAC,OAAO;IAAI;IAE5B,0EAA0E;IAC1E,IAAIK;IACJ,IAAI;QACFA,SAAS,IAAIC,IAAIN,WAAWK,MAAM;IACpC,EAAE,OAAM;QACN,OAAO,KAAK,qDAAqD;;IACnE;IAEA,qBACE;;0BACE,KAACE;gBAAKC,aAAY;gBAAYC,MAAMJ;gBAAQK,KAAI;;0BAChD,KAACH;gBAAKE,MAAMJ;gBAAQK,KAAI;;;;AAG9B;AAEA,eAAeX,gBAAe"}
+18
View File
@@ -0,0 +1,18 @@
import type { Plugin } from 'payload';
/**
* Cloudflare R2 media storage — configured from environment variables (agency
* infrastructure, not per-project panel data). R2 is S3-compatible, so we use
* @payloadcms/storage-s3 pointed at the R2 endpoint.
*
* Storage is infrastructure (like the database or PAYLOAD_SECRET): it binds at
* boot, and its credentials are agency-owned — so it lives in .env, not the
* panel. See docs/storage.md for the required variables.
*
* Returns the storage plugin when all R2 vars are present; otherwise returns a
* no-op passthrough so the project falls back to Payload's default local disk
* storage (useful in dev without R2). This mirrors how mailAdapter degrades
* gracefully when a transport isn't configured.
*
* @param collections - slugs of upload collections to offload to R2 (e.g. ['media'])
*/
export declare const buildR2Storage: (collections?: string[]) => Plugin;
+72
View File
@@ -0,0 +1,72 @@
import { s3Storage } from '@payloadcms/storage-s3';
/**
* Cloudflare R2 media storage — configured from environment variables (agency
* infrastructure, not per-project panel data). R2 is S3-compatible, so we use
* @payloadcms/storage-s3 pointed at the R2 endpoint.
*
* Storage is infrastructure (like the database or PAYLOAD_SECRET): it binds at
* boot, and its credentials are agency-owned — so it lives in .env, not the
* panel. See docs/storage.md for the required variables.
*
* Returns the storage plugin when all R2 vars are present; otherwise returns a
* no-op passthrough so the project falls back to Payload's default local disk
* storage (useful in dev without R2). This mirrors how mailAdapter degrades
* gracefully when a transport isn't configured.
*
* @param collections - slugs of upload collections to offload to R2 (e.g. ['media'])
*/ export const buildR2Storage = (collections = [
'media'
])=>{
const bucket = process.env.R2_BUCKET;
const endpoint = process.env.R2_ENDPOINT;
const accessKeyId = process.env.R2_ACCESS_KEY_ID;
const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY;
// Any missing → skip R2, fall back to local disk. Warn so it's not silent.
if (!bucket || !endpoint || !accessKeyId || !secretAccessKey) {
return (config)=>{
// Only warn when SOME vars are set (partial config = likely a mistake).
if (bucket || endpoint || accessKeyId || secretAccessKey) {
console.warn('[ipal] R2 storage: incomplete env (need R2_BUCKET, R2_ENDPOINT, ' + 'R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY). Falling back to local disk.');
}
return config;
};
}
// Public URL for served media. R2 is private by default; its S3 endpoint only
// accepts uploads and won't serve files (403). With a custom domain
// (media.klient.pl → bucket) set R2_PUBLIC_URL so Payload generates URLs
// pointing there. Without it, uploads work but images don't display publicly.
// See docs/storage.md.
const publicUrl = process.env.R2_PUBLIC_URL?.replace(/\/$/, '') // strip trailing slash
;
// generateFileURL is a PER-COLLECTION option in @payloadcms/storage-s3 (not a
// top-level one) — R2 needs it to point served URLs at the custom domain
// instead of the private S3 endpoint. Each collection gets either `true`
// (plain offload) or an object carrying generateFileURL when a public URL is set.
const generateFileURL = publicUrl ? ({ filename, prefix })=>[
publicUrl,
prefix,
filename
].filter(Boolean).join('/') : undefined;
const collectionsConfig = {};
for (const slug of collections){
collectionsConfig[slug] = generateFileURL ? {
generateFileURL
} : true;
}
return s3Storage({
bucket,
collections: collectionsConfig,
config: {
credentials: {
accessKeyId,
secretAccessKey
},
endpoint,
region: 'auto',
// R2 requires path-style addressing for S3 compatibility.
forcePathStyle: true
}
});
};
//# sourceMappingURL=buildR2Storage.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/storage/buildR2Storage.ts"],"sourcesContent":["import type { Plugin } from 'payload'\n\nimport { s3Storage } from '@payloadcms/storage-s3'\n\n/**\n * Cloudflare R2 media storage — configured from environment variables (agency\n * infrastructure, not per-project panel data). R2 is S3-compatible, so we use\n * @payloadcms/storage-s3 pointed at the R2 endpoint.\n *\n * Storage is infrastructure (like the database or PAYLOAD_SECRET): it binds at\n * boot, and its credentials are agency-owned — so it lives in .env, not the\n * panel. See docs/storage.md for the required variables.\n *\n * Returns the storage plugin when all R2 vars are present; otherwise returns a\n * no-op passthrough so the project falls back to Payload's default local disk\n * storage (useful in dev without R2). This mirrors how mailAdapter degrades\n * gracefully when a transport isn't configured.\n *\n * @param collections - slugs of upload collections to offload to R2 (e.g. ['media'])\n */\nexport const buildR2Storage = (collections: string[] = ['media']): Plugin => {\n const bucket = process.env.R2_BUCKET\n const endpoint = process.env.R2_ENDPOINT\n const accessKeyId = process.env.R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY\n\n // Any missing → skip R2, fall back to local disk. Warn so it's not silent.\n if (!bucket || !endpoint || !accessKeyId || !secretAccessKey) {\n return (config) => {\n // Only warn when SOME vars are set (partial config = likely a mistake).\n if (bucket || endpoint || accessKeyId || secretAccessKey) {\n console.warn(\n '[ipal] R2 storage: incomplete env (need R2_BUCKET, R2_ENDPOINT, ' +\n 'R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY). Falling back to local disk.',\n )\n }\n return config\n }\n }\n\n // Public URL for served media. R2 is private by default; its S3 endpoint only\n // accepts uploads and won't serve files (403). With a custom domain\n // (media.klient.pl → bucket) set R2_PUBLIC_URL so Payload generates URLs\n // pointing there. Without it, uploads work but images don't display publicly.\n // See docs/storage.md.\n const publicUrl = process.env.R2_PUBLIC_URL?.replace(/\\/$/, '') // strip trailing slash\n\n // generateFileURL is a PER-COLLECTION option in @payloadcms/storage-s3 (not a\n // top-level one) — R2 needs it to point served URLs at the custom domain\n // instead of the private S3 endpoint. Each collection gets either `true`\n // (plain offload) or an object carrying generateFileURL when a public URL is set.\n const generateFileURL = publicUrl\n ? ({ filename, prefix }: { filename: string; prefix?: string }) =>\n [publicUrl, prefix, filename].filter(Boolean).join('/')\n : undefined\n\n const collectionsConfig: Record<string, { generateFileURL: typeof generateFileURL } | true> = {}\n for (const slug of collections) {\n collectionsConfig[slug] = generateFileURL ? { generateFileURL } : true\n }\n\n return s3Storage({\n bucket,\n collections: collectionsConfig,\n config: {\n credentials: { accessKeyId, secretAccessKey },\n endpoint,\n region: 'auto', // R2 uses 'auto'\n // R2 requires path-style addressing for S3 compatibility.\n forcePathStyle: true,\n },\n })\n}\n"],"names":["s3Storage","buildR2Storage","collections","bucket","process","env","R2_BUCKET","endpoint","R2_ENDPOINT","accessKeyId","R2_ACCESS_KEY_ID","secretAccessKey","R2_SECRET_ACCESS_KEY","config","console","warn","publicUrl","R2_PUBLIC_URL","replace","generateFileURL","filename","prefix","filter","Boolean","join","undefined","collectionsConfig","slug","credentials","region","forcePathStyle"],"mappings":"AAEA,SAASA,SAAS,QAAQ,yBAAwB;AAElD;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAMC,iBAAiB,CAACC,cAAwB;IAAC;CAAQ;IAC9D,MAAMC,SAASC,QAAQC,GAAG,CAACC,SAAS;IACpC,MAAMC,WAAWH,QAAQC,GAAG,CAACG,WAAW;IACxC,MAAMC,cAAcL,QAAQC,GAAG,CAACK,gBAAgB;IAChD,MAAMC,kBAAkBP,QAAQC,GAAG,CAACO,oBAAoB;IAExD,2EAA2E;IAC3E,IAAI,CAACT,UAAU,CAACI,YAAY,CAACE,eAAe,CAACE,iBAAiB;QAC5D,OAAO,CAACE;YACN,wEAAwE;YACxE,IAAIV,UAAUI,YAAYE,eAAeE,iBAAiB;gBACxDG,QAAQC,IAAI,CACV,qEACE;YAEN;YACA,OAAOF;QACT;IACF;IAEA,8EAA8E;IAC9E,oEAAoE;IACpE,yEAAyE;IACzE,8EAA8E;IAC9E,uBAAuB;IACvB,MAAMG,YAAYZ,QAAQC,GAAG,CAACY,aAAa,EAAEC,QAAQ,OAAO,IAAI,uBAAuB;;IAEvF,8EAA8E;IAC9E,yEAAyE;IACzE,yEAAyE;IACzE,kFAAkF;IAClF,MAAMC,kBAAkBH,YACpB,CAAC,EAAEI,QAAQ,EAAEC,MAAM,EAAyC,GAC1D;YAACL;YAAWK;YAAQD;SAAS,CAACE,MAAM,CAACC,SAASC,IAAI,CAAC,OACrDC;IAEJ,MAAMC,oBAAwF,CAAC;IAC/F,KAAK,MAAMC,QAAQzB,YAAa;QAC9BwB,iBAAiB,CAACC,KAAK,GAAGR,kBAAkB;YAAEA;QAAgB,IAAI;IACpE;IAEA,OAAOnB,UAAU;QACfG;QACAD,aAAawB;QACbb,QAAQ;YACNe,aAAa;gBAAEnB;gBAAaE;YAAgB;YAC5CJ;YACAsB,QAAQ;YACR,0DAA0D;YAC1DC,gBAAgB;QAClB;IACF;AACF,EAAC"}
+1
View File
@@ -0,0 +1 @@
export { buildR2Storage } from './buildR2Storage.js';
+3
View File
@@ -0,0 +1,3 @@
export { buildR2Storage } from './buildR2Storage.js';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/storage/index.ts"],"sourcesContent":["export { buildR2Storage } from './buildR2Storage.js'\n"],"names":["buildR2Storage"],"mappings":"AAAA,SAASA,cAAc,QAAQ,sBAAqB"}
+80 -1
View File
@@ -247,4 +247,83 @@ const entries = await buildSitemapEntries({
```
Przy dziesiątkach tysięcy URL-i Next ma `generateSitemaps` do dzielenia na
części — dodasz, gdyby realnie było ich tyle. Jedna sitemapa wystarcza do ~50k.
części — dodasz, gdyby realnie było ich tyle. Jedna sitemapa wystarcza do ~50k.
## Favicon w Google + Organization (branding w wyszukiwarce)
Favicon i structured data wpływają na to, jak strona wygląda w wynikach Google.
Plugin generuje jedno i drugie z panelu — projekt tylko wpina w root layout.
### Favicon — dlaczego się nie pokazywał
Google ma twarde wymogi: `<link rel="icon">` w `<head>`, kwadratowy, **≥48×48px**,
stały URL. Gdy projekt renderował favicon „po swojemu", często był za mały, źle
otagowany albo nieobecny w head → Google go nie pokazywał. Plugin robi to teraz
poprawnie.
### Wpięcie favicon (root layout)
Favicon jest GLOBALNY (ten sam wszędzie) — wpina się RAZ w root layout, nie per strona:
```tsx
// app/(frontend)/[locale]/layout.tsx
import type { Metadata } from 'next'
import { buildIconsMetadata } from '@intecion/ipal-kit'
import { getSettings } from '@/lib/payload'
export async function generateMetadata({ params }): Promise<Metadata> {
const { locale } = await params
const settings = await getSettings(locale)
return buildIconsMetadata(settings.favicon) // z pola favicon (panel)
}
```
`buildIconsMetadata` generuje poprawne `icons` (favicon + apple-touch) z pola
favicon. SVG → skaluje się; PNG → powinien być ≥48×48 (walidacja ostrzega, patrz niżej).
### Wymuszenie rozmiaru (walidacja)
Pole favicon w SiteSettings ma walidację `validateFaviconField` — ostrzega
redaktora przy zapisie, jeśli favicon jest <48×48 albo nie kwadratowy. Redaktor
widzi ostrzeżenie, zamiast po cichu wgrać favicon, którego Google nie pokaże.
### Organization JSON-LD (branding)
Pomaga Google powiązać stronę z marką (nazwa, logo) — lepsze wyświetlanie w
wynikach, logo w knowledge panel.
```tsx
// root layout — RAZ (Organization jest globalny)
import { buildOrganizationJsonLd } from '@intecion/ipal-kit'
const jsonLd = buildOrganizationJsonLd({
name: settings.siteName,
url: process.env.NEXT_PUBLIC_SERVER_URL!,
logo: settings.logo,
sameAs: settings.socialLinks, // opcjonalne: profile społecznościowe
})
// w JSX layoutu:
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
```
Dane z panelu (siteName, logo) — nic na sztywno.
### Po wdrożeniu — cierpliwość z Google
Google **cache'uje favicon osobno i wolno** (dni, czasem tygodnie). Po poprawnym
wpięciu favicon nie pojawi się natychmiast — Googlebot musi ponownie odwiedzić
stronę główną. Przyspieszenie: Search Console → prośba o ponowne indeksowanie
strony głównej. Sprawdź też, czy `/` nie blokuje Googlebota (robots) i czy
favicon URL jest publiczny (nie za auth).
### Weryfikacja
1. Otwórz stronę → DevTools → Elements → `<head>` → sprawdź `<link rel="icon">`
z poprawnym URL.
2. Otwórz sam URL favicon w przeglądarce — obraz się pokazuje, ≥48×48.
3. Rich Results Test (Google) — wklej URL strony, sprawdź Organization.
4. Search Console → poproś o ponowne indeksowanie strony głównej.
+211
View File
@@ -0,0 +1,211 @@
# storage — media na Cloudflare R2
Offload mediów (obrazy, pliki) do Cloudflare R2 zamiast lokalnego dysku. R2 jest
S3-kompatybilny; plugin dostarcza `buildR2Storage`, który czyta dane z `.env`
i konfiguruje adapter.
> **Storage to infrastruktura, nie treść.** Dane R2 (klucze, bucket) idą do
> `.env` — jak DATABASE_URI, PAYLOAD_SECRET, GRAPH_*. NIE do panelu (to sekrety
> agencyjne, wiążą się przy starcie, nie zmienia ich redaktor).
## Zależność
```bash
pnpm add @payloadcms/storage-s3
```
## Zmienne .env
Patrz [R2-ENV-przyklad](../R2-ENV-przyklad.md) po pełną instrukcję skąd wziąć wartości.
```bash
R2_BUCKET=nazwa-bucketa
R2_ENDPOINT=https://<ACCOUNT_ID>.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=<access-key-id>
R2_SECRET_ACCESS_KEY=<secret-access-key>
```
## Wpięcie (payload.config.ts)
```ts
import { buildR2Storage } from '@intecion/ipal-kit'
export default buildConfig({
// ...
plugins: [
ipalKit({ /* ... */ }),
buildR2Storage(['media']), // slugi kolekcji upload do offloadu
],
})
```
`buildR2Storage` przyjmuje listę kolekcji upload (domyślnie `['media']`). Jeśli
masz więcej kolekcji plików: `buildR2Storage(['media', 'documents'])`.
## Zachowanie (fallback)
- **Wszystkie 4 zmienne** → media w R2.
- **Brak zmiennych** → fallback na lokalny dysk (dev działa bez R2, zero konfiguracji).
- **Część zmiennych** → ostrzeżenie w logu + fallback (częściowa konfiguracja =
pewnie pomyłka).
To wzorzec „degrade gracefully" — jak mailAdapter, który wraca do SMTP, gdy brak
Graph. Projekt działa niezależnie od tego, czy R2 jest skonfigurowany.
## Publiczny dostęp + custom domena (WAŻNE — krok po kroku)
R2 domyślnie prywatny. Upload zadziała, ale obrazy się NIE wyświetlą (403),
dopóki nie skonfigurujesz publicznego odczytu przez custom domenę. To proces
w Cloudflare (nie w kodzie), wieloetapowy — poniżej dokładnie.
### Dlaczego custom domena, nie „r2.dev"
R2 oferuje szybki publiczny URL `*.r2.dev`, ALE:
- jest rate-limitowany (nie do produkcji)
- nie przechodzi przez cache Cloudflare (brak CDN, wolniej, drożej)
- brzydki URL (nie Twoja domena)
Dla produkcji ZAWSZE custom domena (np. `media.klient.pl`) — daje CDN, cache,
własny URL. r2.dev tylko do szybkiego testu.
### Warunek wstępny: domena w Cloudflare
Custom domena dla R2 wymaga, żeby domena (albo subdomena) była zarządzana przez
Cloudflare (nameservery klienta wskazują na Cloudflare). Jeśli domena klienta
jest u innego rejestratora — trzeba ją najpierw dodać do Cloudflare (Add Site)
i przełączyć nameservery. Sama subdomena `media.klient.pl` wystarczy, jeśli
główna domena jest już w Cloudflare.
### Krok po kroku — podpięcie custom domeny
1. **Cloudflare Dashboard → R2 → wybierz bucket**
2. Zakładka **Settings** → sekcja **Public access** → **Custom Domains**
3. **Connect Domain** → wpisz subdomenę, np. `media.klient.pl`
4. Cloudflare automatycznie doda rekord CNAME (bo domena jest w Cloudflare) i
wystawi certyfikat SSL. Poczekaj, aż status = **Active** (kilka minut).
5. Od tej chwili pliki są publiczne pod `https://media.klient.pl/<klucz-pliku>`.
### Krok: ustaw publiczny URL w projekcie
Payload musi generować URL-e mediów wskazujące na custom domenę, nie na endpoint
S3. Dodaj zmienną i przekaż ją do adaptera:
```bash
# .env
R2_PUBLIC_URL=https://media.klient.pl
```
Adapter `buildR2Storage` czyta ją i ustawia jako bazowy URL mediów (jeśli
ustawiona). Bez niej Payload zwróci URL wskazujący na prywatny endpoint S3 →
403 na froncie. (Patrz aktualizacja buildR2Storage niżej.)
### Weryfikacja
1. Wgraj obraz w panelu (Media).
2. Sprawdź URL obrazu w panelu — powinien być `https://media.klient.pl/...`,
NIE `https://<account>.r2.cloudflarestorage.com/...`.
3. Otwórz URL w przeglądarce — obraz się pokazuje (nie 403).
4. Na froncie `<img src>` działa.
### Częsty błąd: 403 mimo custom domeny
- **URL wskazuje na endpoint S3, nie custom domenę** → brakuje `R2_PUBLIC_URL`
albo adapter jej nie używa. Sprawdź URL w panelu.
- **Custom domena nie Active** → poczekaj na SSL/CNAME w Cloudflare.
- **Public access wyłączony** → w bucket Settings sprawdź, czy custom domena jest
podpięta (nie tylko utworzona).
Bez tego media wgrają się do R2, ale front pokaże 403. Konfiguracja domeny jest
po stronie Cloudflare, publiczny URL po stronie projektu (.env).
## Migracja istniejących mediów
Jeśli projekt miał media lokalnie i przełączasz na R2 — nowe uploady idą do R2,
ale STARE zostają na dysku (i znikną przy redeployu bez wolumenu). Przed
przełączeniem na produkcji przenieś istniejące pliki do bucketa (np. `rclone`
albo ręcznie przez R2 dashboard), inaczej stare obrazy znikną.
## Weryfikacja
```bash
# po wpięciu i ustawieniu .env:
pnpm dev
# wgraj obraz w panelu (Media) → sprawdź w Cloudflare R2, czy plik się pojawił
```
## Dev na lokalnym I na R2 (seedowanie podczas developmentu)
Fallback (brak zmiennych → lokalny dysk) oznacza, że **dev działa w obu trybach**:
- **Dev bez R2 w .env** → media na lokalnym dysku. Szybki start, zero konfiguracji.
- **Dev z R2 w .env** → media w R2 już podczas developmentu. Przydatne, gdy
seedujesz treść w devie i chcesz, żeby od razu lądowała w buckecie (np. wspólny
bucket dev, albo test realnego flow przed produkcją).
Przełączasz trybem po prostu obecnością zmiennych R2 w `.env`. Ten sam kod,
`buildR2Storage` sam wykrywa. Nie musisz nic zmieniać w configu między trybami.
> Jeśli seedujesz w devie do R2 — pamiętaj, że to realny bucket. Używaj osobnego
> bucketa dev (nie produkcyjnego), żeby nie mieszać danych testowych z realnymi.
## Normalizacja nazw plików (automatyczna)
Plik `normalizeFilenameHook` czyści nazwy wgrywanych plików — slugifikuje nazwę,
zachowuje rozszerzenie:
```
"Zdjęcie jeden nad morzem.jpg" → "zdjecie-jeden-nad-morzem.jpg"
"Faktura #12 (2024).PDF" → "faktura-12-2024.pdf"
```
Wpięcie w kolekcję Media (projekt):
```ts
import { normalizeFilenameHook } from '@intecion/ipal-kit'
export const Media: CollectionConfig = {
slug: 'media',
upload: { staticDir: 'media' /* ... */ },
hooks: {
beforeOperation: [normalizeFilenameHook], // czyści nazwę przed zapisem
},
fields: [ /* alt itd. */ ],
}
```
Działa z lokalnym dyskiem i z R2/S3 (hook biegnie PRZED warstwą storage, więc
czysta nazwa trafia i do bazy, i do bucketa). Dlaczego to ważne:
- **URL-e mediów są czyste** — `/media/zdjecie-nad-morzem.jpg`, nie
`/media/Zdjęcie%20jeden%20nad%20morzem.jpg` (spacje/diakrytyki w URL = problemy).
- **Przenośność** — nazwa bez polskich znaków/spacji działa wszędzie (CDN, S3, systemy plików).
- **Bez kolizji kodowania** — spacje i `#`, `()` w nazwach plików potrafią psuć
ścieżki i cache.
Sama funkcja `normalizeFilename(name)` też jest wyeksportowana, gdybyś potrzebował
jej poza hookiem.
## Preconnect do domeny mediów (wydajność)
Komponent `MediaPreconnect` generuje `<link rel="preconnect">` + `dns-prefetch`
dla domeny mediów (R2_PUBLIC_URL) — przeglądarka nawiązuje połączenie TLS/DNS
z CDN zawczasu, zanim napotka pierwszy `<img>`. Zysk ~150–300 ms na pierwszym
obrazie.
```tsx
// layout.tsx — w <head> (albo górze <body>, Next hoistuje link tagi)
import { MediaPreconnect } from '@intecion/ipal-kit/rsc'
<head>
<MediaPreconnect />
</head>
```
Czyta domenę z **R2_PUBLIC_URL** (to samo źródło co buildR2Storage) — zero
hardkodu, jedno źródło prawdy. Gdy R2_PUBLIC_URL nie ustawione (lokalny dysk,
brak CDN) → nie renderuje nic. Zmiana domeny mediów = zmiana jednej zmiennej
env, komponent podąża automatycznie.
> NIE hardkoduj `<link rel="preconnect" href="https://media.klient.pl">` ręcznie
> w layoutcie — to zaszywa domenę klienta w kodzie. Użyj MediaPreconnect, który
> bierze ją z env.
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@intecion/ipal-kit",
"version": "1.0.18",
"version": "1.1.1",
"description": "Intecion Payload Advanced Library — a Payload CMS 3 plugin: i18n, SEO, forms, consent, analytics, blog/archives.",
"license": "MIT",
"repository": {
@@ -60,6 +60,7 @@
"test:int": "vitest"
},
"dependencies": {
"@payloadcms/storage-s3": "^3.88.0",
"lucide-react": "^0.400.0",
"nodemailer": "^8.0.1",
"server-only": "^0.0.1",
+509
View File
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@payloadcms/storage-s3':
specifier: ^3.88.0
version: 3.88.0(@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])
lucide-react:
specifier: ^0.400.0
version: 0.400.0([email protected])
@@ -133,6 +136,88 @@ packages:
resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==}
engines: {node: '>= 16'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-Dtu0gr4dnATZAPwEYbpCsG+MpLM7OAliy2gTepEFQwl1vZ6DL3QMH2FveMa3HLvPsOdhJsPRB3KtxVhph9T75A==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-8+UH1uBWwjbRu0ugTBhJ/cu3rL/vzf1tm9/3+xZGf1uybIO3HTF2xLJJaTKkRPvzxz8/n/w+YrwoHGZ2Q/a2Zg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-4dxJX2pueKHtFyxdtT/IH385NpAWjbNjpbbIN6L9YK58dbae6ldyiLSom1EV5nPoOxKtWLXiqch+yo3NjXfleQ==}
engines: {node: '>=20.0.0'}
peerDependencies:
'@aws-sdk/client-s3': ^3.1119.0
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-wMIsNumRVKaNMKhvU/s9VrdEwE8S6gSzXp4RygFG5BEMnGkkXf8cjh8zf7cKJBpUDpqTWqwbz5isEgp9rH6Lng==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-yEqb0ispm9uXZ7nk5stErMB7yn43j3XCAaHM15MsdWaCk7RvEmGjYdbyHZcH86JpUg7mk1v1m2xwzLj2o+3uCg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/[email protected]':
resolution: {integrity: sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==}
engines: {node: '>=20.0.0'}
'@aws/[email protected]':
resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==}
engines: {node: '>=18.0.0'}
'@babel/[email protected]':
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
engines: {node: '>=6.9.0'}
@@ -1646,6 +1731,13 @@ packages:
next: '>=15.2.9 <15.3.0 || >=15.3.9 <15.4.0 || >=15.4.11 <15.5.0 || >=16.2.6 <17.0.0'
payload: 3.88.0
'@payloadcms/[email protected]':
resolution: {integrity: sha512-wIcRVedtBZIQMu7dpSE1TOJKJd0HuVSUNPrV2KxIbtqQFm+ly34NCJYY0m1+K0YS6my8BKdcB4jj2exg8454Ig==}
peerDependencies:
payload: 3.88.0
react: ^19.0.1 || ^19.1.2 || ^19.2.1
react-dom: ^19.0.1 || ^19.1.2 || ^19.2.1
'@payloadcms/[email protected]':
resolution: {integrity: sha512-C915jEHADVN9xW7fSq/bYIZig1zjfQo+t4wrgiqjOCmB81oMgyqLRB3RlL2e6y4PzBUtgss2VS40vn/tVD/61Q==}
peerDependencies:
@@ -1671,6 +1763,12 @@ packages:
react: ^19.0.1 || ^19.1.2 || ^19.2.1
react-dom: ^19.0.1 || ^19.1.2 || ^19.2.1
'@payloadcms/[email protected]':
resolution: {integrity: sha512-+35JRD4y7ImnIJuPSBhECHGGml0A+xju9BT/zW+lRUycDvvA0cqZNP+t95OVoZrxIB6V75Vg8GKPvAaRAmgOEw==}
engines: {node: ^18.20.2 || >=20.9.0}
peerDependencies:
payload: 3.88.0
'@payloadcms/[email protected]':
resolution: {integrity: sha512-AZVQjjW5pKxO+K9ODeh6yw6/EfXK1hxGPw55KzzDC5srO3hjNskgyWtkauKvbz+S0+xFspANbRc4OD/Rg/74WA==}
@@ -1839,6 +1937,30 @@ packages:
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
engines: {node: '>=14.16'}
'@smithy/[email protected]':
resolution: {integrity: sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==}
engines: {node: '>=18.0.0'}
'@smithy/[email protected]':
resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==}
engines: {node: '>=18.0.0'}
'@smithy/[email protected]':
resolution: {integrity: sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw==}
engines: {node: '>=18.0.0'}
'@smithy/[email protected]':
resolution: {integrity: sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA==}
engines: {node: '>=18.0.0'}
'@smithy/[email protected]':
resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==}
engines: {node: '>=18.0.0'}
'@smithy/[email protected]':
resolution: {integrity: sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==}
engines: {node: '>=18.0.0'}
'@standard-schema/[email protected]':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -2563,6 +2685,9 @@ packages:
[email protected]:
resolution: {integrity: sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==}
[email protected]:
resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==}
[email protected]:
resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==}
@@ -2596,6 +2721,9 @@ packages:
resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==}
engines: {node: '>=4.0'}
[email protected]:
resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==}
[email protected]:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
@@ -2866,6 +2994,10 @@ packages:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
[email protected]:
resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==}
engines: {node: '>=12.20'}
@@ -3363,10 +3495,18 @@ packages:
[email protected]:
resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
[email protected]:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
[email protected]:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
[email protected]:
resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
engines: {node: '>=12.0.0'}
@@ -3455,6 +3595,9 @@ packages:
resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==}
[email protected]:
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
@@ -3470,6 +3613,10 @@ packages:
resolution: {integrity: sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==}
engines: {node: '>=12'}
[email protected]:
resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==}
engines: {node: '>= 8'}
[email protected]:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
@@ -3577,6 +3724,14 @@ packages:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting [email protected]
[email protected]:
resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
@@ -3672,6 +3827,10 @@ packages:
[email protected]:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
[email protected]:
resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
@@ -3725,6 +3884,9 @@ packages:
[email protected]:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
[email protected]:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
[email protected]:
resolution: {integrity: sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==}
@@ -3894,6 +4056,10 @@ packages:
resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==}
engines: {node: '>=16'}
@@ -4098,6 +4264,9 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
[email protected]:
resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==}
[email protected]:
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
@@ -4430,6 +4599,10 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -4682,6 +4855,10 @@ packages:
[email protected]:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
[email protected]:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
[email protected]:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
@@ -4721,6 +4898,10 @@ packages:
[email protected]:
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
[email protected]:
resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -4966,6 +5147,9 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==}
[email protected]:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
@@ -5393,6 +5577,10 @@ packages:
resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==}
engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
hasBin: true
[email protected]:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -5490,6 +5678,190 @@ snapshots:
'@types/json-schema': 7.0.15
js-yaml: 4.3.0
'@aws-sdk/[email protected]':
dependencies:
'@aws-sdk/core': 3.977.9
'@aws-sdk/types': 3.974.5
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@aws-sdk/checksums': 3.1000.29
'@aws-sdk/core': 3.977.9
'@aws-sdk/credential-provider-node': 3.972.81
'@aws-sdk/middleware-sdk-s3': 3.972.75
'@aws-sdk/signature-v4-multi-region': 3.996.46
'@aws-sdk/types': 3.974.5
'@smithy/core': 3.33.3
'@smithy/fetch-http-handler': 5.7.2
'@smithy/node-http-handler': 4.11.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@aws-sdk/types': 3.974.5
'@aws-sdk/xml-builder': 3.972.40
'@aws/lambda-invoke-store': 0.3.0
'@smithy/core': 3.33.3
'@smithy/signature-v4': 5.7.3
'@smithy/types': 4.17.2
bowser: 2.14.1
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@aws-sdk/core': 3.977.9
'@aws-sdk/types': 3.974.5
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@aws-sdk/core': 3.977.9
'@aws-sdk/types': 3.974.5
'@smithy/core': 3.33.3
'@smithy/fetch-http-handler': 5.7.2
'@smithy/node-http-handler': 4.11.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@aws-sdk/core': 3.977.9
'@aws-sdk/credential-provider-env': 3.972.70
'@aws-sdk/credential-provider-http': 3.972.72
'@aws-sdk/credential-provider-login': 3.972.77
'@aws-sdk/credential-provider-process': 3.972.70
'@aws-sdk/credential-provider-sso': 3.973.14
'@aws-sdk/credential-provider-web-identity': 3.972.76
'@aws-sdk/nested-clients': 3.997.44
'@aws-sdk/types': 3.974.5
'@smithy/core': 3.33.3
'@smithy/credential-provider-imds': 4.5.2
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@aws-sdk/core': 3.977.9
'@aws-sdk/nested-clients': 3.997.44
'@aws-sdk/types': 3.974.5
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@aws-sdk/credential-provider-env': 3.972.70
'@aws-sdk/credential-provider-http': 3.972.72
'@aws-sdk/credential-provider-ini': 3.973.15
'@aws-sdk/credential-provider-process': 3.972.70
'@aws-sdk/credential-provider-sso': 3.973.14
'@aws-sdk/credential-provider-web-identity': 3.972.76
'@aws-sdk/types': 3.974.5
'@smithy/core': 3.33.3
'@smithy/credential-provider-imds': 4.5.2
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@aws-sdk/core': 3.977.9
'@aws-sdk/types': 3.974.5
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@aws-sdk/core': 3.977.9
'@aws-sdk/nested-clients': 3.997.44
'@aws-sdk/token-providers': 3.1116.0
'@aws-sdk/types': 3.974.5
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@aws-sdk/core': 3.977.9
'@aws-sdk/nested-clients': 3.997.44
'@aws-sdk/types': 3.974.5
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected](@aws-sdk/[email protected])':
dependencies:
'@aws-sdk/client-s3': 3.1119.0
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
buffer: 5.6.0
events: 3.3.0
stream-browserify: 3.0.0
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@aws-sdk/core': 3.977.9
'@aws-sdk/signature-v4-multi-region': 3.996.46
'@aws-sdk/types': 3.974.5
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@aws-sdk/core': 3.977.9
'@aws-sdk/signature-v4-multi-region': 3.996.46
'@aws-sdk/types': 3.974.5
'@smithy/core': 3.33.3
'@smithy/fetch-http-handler': 5.7.2
'@smithy/node-http-handler': 4.11.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@aws-sdk/core': 3.977.9
'@aws-sdk/signature-v4-multi-region': 3.996.46
'@aws-sdk/types': 3.974.5
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@aws-sdk/types': 3.974.5
'@smithy/signature-v4': 5.7.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@aws-sdk/core': 3.977.9
'@aws-sdk/nested-clients': 3.997.44
'@aws-sdk/types': 3.974.5
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws-sdk/[email protected]':
dependencies:
'@smithy/types': 4.17.2
tslib: 2.8.1
'@aws/[email protected]': {}
'@babel/[email protected]':
dependencies:
'@babel/helper-validator-identifier': 7.29.7
@@ -7020,6 +7392,21 @@ snapshots:
- supports-color
- typescript
'@payloadcms/[email protected](@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])':
dependencies:
'@payloadcms/ui': 3.88.0(@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])
find-node-modules: 2.1.3
payload: 3.88.0([email protected])([email protected])
range-parser: 1.2.1
react: 19.2.6
react-dom: 19.2.6([email protected])
transitivePeerDependencies:
- '@types/react'
- monaco-editor
- next
- supports-color
- typescript
'@payloadcms/[email protected](@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])':
dependencies:
'@payloadcms/translations': 3.88.0
@@ -7094,6 +7481,22 @@ snapshots:
- utf-8-validate
- yjs
'@payloadcms/[email protected](@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])':
dependencies:
'@aws-sdk/client-s3': 3.1119.0
'@aws-sdk/lib-storage': 3.1119.0(@aws-sdk/[email protected])
'@aws-sdk/s3-request-presigner': 3.1119.0
'@payloadcms/plugin-cloud-storage': 3.88.0(@types/[email protected])([email protected])([email protected](@babel/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])
payload: 3.88.0([email protected])([email protected])
transitivePeerDependencies:
- '@types/react'
- monaco-editor
- next
- react
- react-dom
- supports-color
- typescript
'@payloadcms/[email protected]':
dependencies:
date-fns: 4.1.0
@@ -7220,6 +7623,39 @@ snapshots:
'@sindresorhus/[email protected]': {}
'@smithy/[email protected]':
dependencies:
'@smithy/types': 4.17.2
tslib: 2.8.1
'@smithy/[email protected]':
dependencies:
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@smithy/[email protected]':
dependencies:
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@smithy/[email protected]':
dependencies:
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@smithy/[email protected]':
dependencies:
'@smithy/core': 3.33.3
'@smithy/types': 4.17.2
tslib: 2.8.1
'@smithy/[email protected]':
dependencies:
tslib: 2.8.1
'@standard-schema/[email protected]': {}
'@swc-node/[email protected](@swc/[email protected])(@swc/[email protected])':
@@ -8043,6 +8479,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
balanced-match: 1.0.2
@@ -8078,6 +8516,11 @@ snapshots:
dependencies:
'@types/node': 22.19.9
[email protected]:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
[email protected]:
dependencies:
base64-js: 1.5.1
@@ -8330,6 +8773,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
@@ -9098,6 +9543,8 @@ snapshots:
transitivePeerDependencies:
- bare-abort-controller
[email protected]: {}
[email protected]:
dependencies:
cross-spawn: 7.0.6
@@ -9110,6 +9557,10 @@ snapshots:
signal-exit: 3.0.7
strip-final-newline: 2.0.0
[email protected]:
dependencies:
homedir-polyfill: 1.0.3
[email protected]: {}
[email protected]:
@@ -9204,6 +9655,11 @@ snapshots:
make-dir: 3.1.0
pkg-dir: 4.2.0
[email protected]:
dependencies:
findup-sync: 4.0.0
merge: 2.1.1
[email protected]: {}
[email protected]:
@@ -9220,6 +9676,13 @@ snapshots:
dependencies:
semver-regex: 4.0.5
[email protected]:
dependencies:
detect-file: 1.0.0
is-glob: 4.0.3
micromatch: 4.0.8
resolve-dir: 1.0.1
[email protected]:
dependencies:
flatted: 3.4.2
@@ -9330,6 +9793,20 @@ snapshots:
once: 1.4.0
path-is-absolute: 1.0.1
[email protected]:
dependencies:
global-prefix: 1.0.2
is-windows: 1.0.2
resolve-dir: 1.0.1
[email protected]:
dependencies:
expand-tilde: 2.0.2
homedir-polyfill: 1.0.3
ini: 1.3.8
is-windows: 1.0.2
which: 1.3.1
[email protected]: {}
[email protected]: {}
@@ -9425,6 +9902,10 @@ snapshots:
dependencies:
react-is: 16.13.1
[email protected]:
dependencies:
parse-passwd: 1.0.0
[email protected]: {}
[email protected]: {}
@@ -9467,6 +9948,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
kind-of: 6.0.3
@@ -9630,6 +10113,8 @@ snapshots:
call-bound: 1.0.4
get-intrinsic: 1.3.0
[email protected]: {}
[email protected]:
dependencies:
is-inside-container: 1.0.0
@@ -9855,6 +10340,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
decode-named-character-reference: 1.3.0
@@ -10320,6 +10807,8 @@ snapshots:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
[email protected]: {}
[email protected]: {}
[email protected]: {}
@@ -10610,6 +11099,12 @@ snapshots:
string_decoder: 1.1.1
util-deprecate: 1.0.2
[email protected]:
dependencies:
inherits: 2.0.4
string_decoder: 1.1.1
util-deprecate: 1.0.2
[email protected]:
dependencies:
picomatch: 2.3.2
@@ -10653,6 +11148,11 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
expand-tilde: 2.0.2
global-modules: 1.0.0
[email protected]: {}
[email protected]: {}
@@ -10979,6 +11479,11 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
[email protected]:
dependencies:
inherits: 2.0.4
readable-stream: 3.6.2
[email protected]: {}
[email protected]:
@@ -11479,6 +11984,10 @@ snapshots:
gopd: 1.2.0
has-tostringtag: 1.0.2
[email protected]:
dependencies:
isexe: 2.0.0
[email protected]:
dependencies:
isexe: 2.0.0
+1
View File
@@ -12,3 +12,4 @@ export type {
EnhanceProps,
RenderBlocksProps,
} from '../modules/blocks/index.js'
export { MediaPreconnect } from '../modules/storage/MediaPreconnect.js'
@@ -1,44 +0,0 @@
import type { Field } from 'payload'
/**
* Cloudflare R2 storage credentials.
* Reserved for future use — media offloading to R2.
*
* Protected at the global level (SiteIntegrations requires an authenticated
* user), so the access keys stay editable in the admin panel while remaining
* inaccessible to anonymous API requests.
*/
export const storageFields: Field[] = [
{
name: 'r2Bucket',
type: 'text',
admin: {
description: 'R2 bucket name.',
},
},
{
name: 'r2Endpoint',
type: 'text',
admin: {
description: 'R2 S3-compatible endpoint URL.',
},
},
{
name: 'r2AccessKeyId',
type: 'text',
admin: {
description: 'R2 access key ID.',
},
},
{
name: 'r2SecretAccessKey',
type: 'text',
admin: {
description: 'R2 secret access key.',
// Masked in the UI (••••) — stored plaintext, readable for R2 auth.
components: {
Field: '@intecion/ipal-kit/client#MaskedField',
},
},
},
]
+4 -2
View File
@@ -3,7 +3,6 @@ import type { Field, GlobalConfig } from 'payload'
import { isAdmin } from '../../modules/access/index.js'
import { analyticsFields } from './fields/analytics.js'
import { smtpFields } from './fields/smtp.js'
import { storageFields } from './fields/storage.js'
import { turnstileFields } from './fields/turnstile.js'
type BuildSiteIntegrationsArgs = {
@@ -22,6 +21,10 @@ type BuildSiteIntegrationsArgs = {
* impossible to enter.)
*
* Unnamed tabs keep data flat (siteIntegrations.ga4MeasurementId).
*
* Note: R2 storage credentials are NOT here — storage is infrastructure and
* binds at boot, so its config lives in .env (R2_BUCKET, R2_ENDPOINT, ...),
* consumed by buildR2Storage. See docs/storage.md.
*/
export function buildSiteIntegrations({
additionalFields,
@@ -44,7 +47,6 @@ export function buildSiteIntegrations({
{ fields: analyticsFields, label: 'Analytics' },
{ fields: turnstileFields, label: 'Turnstile' },
{ fields: smtpFields, label: 'SMTP' },
{ fields: storageFields, label: 'Storage' },
...(additionalFields?.length ? [{ fields: additionalFields, label: 'Custom' }] : []),
],
},
+12 -3
View File
@@ -1,8 +1,10 @@
import type { Field } from 'payload'
import { validateFaviconField } from '../../../modules/seo/index.js'
/**
* General site identity fields.
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo).
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo, favicon).
*/
export const generalFields: Field[] = [
{
@@ -45,7 +47,8 @@ export const generalFields: Field[] = [
name: 'logo',
type: 'upload',
admin: {
description: 'Primary site logo.',
description:
'Primary site logo. Also used for Organization structured data (buildOrganizationJsonLd).',
},
relationTo: 'media',
},
@@ -61,7 +64,13 @@ export const generalFields: Field[] = [
name: 'favicon',
type: 'upload',
admin: {
description: 'Square source icon (PNG or SVG) for the browser tab. Rendered by the frontend.',
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.
beforeValidate: [validateFaviconField],
},
relationTo: 'media',
},
+19 -12
View File
@@ -75,6 +75,18 @@ export {
} from './modules/i18n/index.js'
export type { LocaleMiddlewareResult } from './modules/i18n/index.js'
export { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from './modules/i18n/index.js'
// Media — filename normalization hook for upload collections (Media).
export { normalizeFilename, normalizeFilenameHook } from './modules/media/index.js'
export {
getNotificationTexts,
NOTIFICATION_FALLBACK,
resolveFormMessage,
} from './modules/notifications/index.js'
export type {
FormNotificationTexts,
NotificationsData,
NotificationTexts,
} from './modules/notifications/index.js'
export type { PagesOption, SystemPageRole } from './modules/pages/index.js'
export { ALL_SYSTEM_PAGE_ROLES, getSystemPagePath } from './modules/pages/index.js'
export type { GlobalQueryOptions } from './modules/payload/index.js'
@@ -87,6 +99,11 @@ export {
} from './modules/payload/index.js'
export { buildSecurityHeaders } from './modules/security/index.js'
export type { BuildSecurityHeadersArgs, SecurityHeader } from './modules/security/index.js'
export {
buildIconsMetadata,
buildOrganizationJsonLd,
validateFaviconField,
} from './modules/seo/index.js'
export type { PageMetadata, SeoMeta, SeoOption } from './modules/seo/index.js'
export { buildHreflangAlternates, buildMetadata, composeTitle } from './modules/seo/index.js'
export type { AutoFillMapping, RobotsRules, SitemapEntry } from './modules/seo/index.js'
@@ -99,18 +116,8 @@ export {
injectAutoFillMeta,
} from './modules/seo/index.js'
export { buildSlugField, toSlug } from './modules/slug/index.js'
export {
NOTIFICATION_FALLBACK,
getNotificationTexts,
resolveFormMessage,
} from './modules/notifications/index.js'
export type {
FormNotificationTexts,
NotificationsData,
NotificationTexts,
} from './modules/notifications/index.js'
// Storage — Cloudflare R2 media offload, configured from .env.
export { buildR2Storage } from './modules/storage/index.js'
export { ipalKit } from './plugin.js'
export type { IpalOptions } from './types.js'
+1
View File
@@ -0,0 +1 @@
export { normalizeFilename, normalizeFilenameHook } from './normalizeFilename.js'
+53
View File
@@ -0,0 +1,53 @@
import type { CollectionBeforeOperationHook } from 'payload'
import slugify from 'slugify'
/**
* Normalizes a filename: slugifies the NAME part (diacritics, spaces, case)
* while preserving the extension. Keeps uploaded media URLs clean and portable.
*
* "Zdjęcie jeden nad morzem.jpg" → "zdjecie-jeden-nad-morzem.jpg"
* "Faktura #12 (2024).PDF" → "faktura-12-2024.pdf"
* "already-clean.webp" → "already-clean.webp"
*
* Why not toSlug(): toSlug uses strict:true, which would strip the dot and
* merge name+extension. Here we split on the LAST dot, slug the stem, lowercase
* the extension, and rejoin.
*/
export function normalizeFilename(filename: string): string {
const lastDot = filename.lastIndexOf('.')
// No extension (or leading-dot dotfile) → slug the whole thing.
if (lastDot <= 0) {
return slugify(filename, { lower: true, strict: true, trim: true })
}
const stem = filename.slice(0, lastDot)
const ext = filename.slice(lastDot + 1).toLowerCase()
const cleanStem = slugify(stem, { lower: true, strict: true, trim: true })
const cleanExt = slugify(ext, { lower: true, strict: true, trim: true })
// Stem could slug to empty (e.g. filename was all symbols) — fall back so we
// never produce a nameless file.
const safeStem = cleanStem || 'plik'
return cleanExt ? `${safeStem}.${cleanExt}` : safeStem
}
/**
* beforeOperation hook for an upload collection (e.g. Media). Rewrites the
* incoming file's name to its normalized form before Payload stores it, so both
* the stored file and its DB filename are clean. Works with local disk and with
* cloud storage adapters (R2/S3) — it runs before the storage layer.
*
* Wire into your Media collection:
* import { normalizeFilenameHook } from '@intecion/ipal-kit'
* hooks: { beforeOperation: [normalizeFilenameHook] }
*/
export const normalizeFilenameHook: CollectionBeforeOperationHook = ({ req, operation }) => {
if (operation !== 'create' && operation !== 'update') return
const file = req.file
if (file?.name) {
file.name = normalizeFilename(file.name)
}
}
+46
View File
@@ -0,0 +1,46 @@
import type { Metadata } from 'next'
type MediaLike =
| { height?: null | number; mimeType?: null | string; url?: null | string; width?: null | number }
| null
| undefined
/**
* Builds Next.js `icons` metadata (favicon / apple-touch-icon) from the panel's
* favicon upload, so the browser tab AND Google get a proper <link rel="icon">.
*
* Why the plugin must do this (not the project): favicon-in-Google has strict
* rules — a real <link rel="icon"> in <head>, square, ≥48×48, at a stable URL.
* Leaving it to each project meant inconsistent hand-rolled tags and no favicon
* in search results. This generates the tags correctly, every time, from the
* panel field.
*
* Favicon is GLOBAL (same across pages), so call this once in the ROOT layout's
* generateMetadata — not per page:
*
* import { buildIconsMetadata } from '@intecion/ipal-kit'
* export async function generateMetadata(): Promise<Metadata> {
* const settings = await getSettings(locale)
* return buildIconsMetadata(settings.favicon)
* }
*
* Google notes: it caches favicons separately and slowly (days/weeks), and only
* shows them for icons it deems valid. Warn on too-small icons at upload time
* (see the media validation hook) so editors don't ship a <48px favicon Google
* will reject.
*/
export function buildIconsMetadata(favicon: MediaLike): Metadata {
const url = favicon?.url
if (!url) {return {}}
const isSvg = favicon?.mimeType === 'image/svg+xml' || url.endsWith('.svg')
return {
icons: {
// Main favicon. SVG scales; PNG should be ≥48×48 (ideally 96 or 192).
icon: isSvg ? [{ type: 'image/svg+xml', url }] : [{ sizes: 'any', url }],
// Apple touch icon (home-screen bookmark on iOS). Reuses the same asset.
apple: [{ url }],
},
}
}
@@ -0,0 +1,44 @@
type MediaLike = { url?: null | string } | null | undefined
type OrganizationJsonLdArgs = {
/** Logo media (from panel). Google uses this for brand knowledge panels. */
logo?: MediaLike
/** Organization / site name. */
name: string
/** Optional social / official profile URLs (sameAs). */
sameAs?: string[]
/** Absolute site URL (https://…). */
url: string
}
/**
* Builds Organization JSON-LD (schema.org) — helps Google associate the site
* with a brand: name, logo, official links. Improves how the site appears in
* search (brand recognition, logo in knowledge panels) and is a signal used
* alongside favicon for identity.
*
* Returns a plain object; the project renders it as a <script type="application/
* ld+json"> in the root layout:
*
* import { buildOrganizationJsonLd } from '@intecion/ipal-kit'
* const jsonLd = buildOrganizationJsonLd({
* name: settings.siteName, url: baseUrl, logo: settings.logo,
* })
* <script type="application/ld+json"
* dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
*
* Data comes from the panel (siteName, logo) — nothing hardcoded. Emit once in
* the root layout (Organization is site-wide, not per page).
*/
export function buildOrganizationJsonLd({ name, logo, sameAs, url }: OrganizationJsonLdArgs) {
const logoUrl = logo?.url
return {
name,
'@context': 'https://schema.org',
'@type': 'Organization',
url,
...(logoUrl ? { logo: logoUrl.startsWith('http') ? logoUrl : `${url}${logoUrl}` } : {}),
...(sameAs && sameAs.length > 0 ? { sameAs } : {}),
}
}
+3
View File
@@ -1,7 +1,9 @@
export { buildAutoFillMetaHook } from './autoFillMeta.js'
export type { AutoFillMapping } from './autoFillMeta.js'
export { buildIconsMetadata } from './buildIconsMetadata.js'
export { buildMetadata } from './buildMetadata.js'
export type { PageMetadata } from './buildMetadata.js'
export { buildOrganizationJsonLd } from './buildOrganizationJsonLd.js'
export { buildRobots } from './buildRobots.js'
export type { RobotsRules } from './buildRobots.js'
export { buildSitemapEntries } from './buildSitemapEntries.js'
@@ -18,3 +20,4 @@ export type { SiteMetaConfig } from './readSiteMetaConfig.js'
export { buildSeoPlugin } from './seoPluginConfig.js'
export { slugsAcrossLocales } from './slugsAcrossLocales.js'
export type { SeoMeta, SeoOption } from './types.js'
export { validateFaviconField } from './validateFavicon.js'
+52
View File
@@ -0,0 +1,52 @@
import type { FieldHook } from 'payload'
/**
* Field validation for the favicon upload: Google rejects favicons under 48×48,
* so warn the editor at save time if the uploaded icon is too small or not
* square. This ENFORCES the requirement instead of silently shipping a favicon
* Google won't display.
*
* Attach to the favicon field's validate (or as a beforeValidate hook on the
* Media relationship). Non-blocking by default — returns a warning string that
* Payload surfaces; make it throw if you want a hard block.
*
* Note: dimensions come from the related Media doc (Payload stores width/height
* for image uploads), so this checks the resolved upload, not the raw file.
*/
export const validateFaviconField: FieldHook = async ({ req, value }) => {
if (!value) {return value} // no favicon set → nothing to validate (optional field)
try {
const media = await req.payload.findByID({
id: typeof value === 'object' ? (value as { id: string }).id : value,
collection: 'media',
depth: 0,
})
const width = (media as { width?: number }).width
const height = (media as { height?: number }).height
const mimeType = (media as { mimeType?: string }).mimeType
// SVG scales infinitely — skip size checks.
if (mimeType === 'image/svg+xml') {return value}
if (typeof width === 'number' && typeof height === 'number') {
if (width < 48 || height < 48) {
req.payload.logger.warn(
`[ipal] Favicon is ${width}×${height}px. Google requires ≥48×48 to ` +
`display it in search results. Upload a larger square icon (96 or 192px).`,
)
}
if (width !== height) {
req.payload.logger.warn(
`[ipal] Favicon is not square (${width}×${height}). Use a square icon ` +
`so it isn't cropped in the browser tab or search results.`,
)
}
}
} catch {
// Media lookup failed — don't block the save over a validation warning.
}
return value
}
+38
View File
@@ -0,0 +1,38 @@
/**
* Emits <link rel="preconnect"> + <link rel="dns-prefetch"> for the media CDN
* domain (R2_PUBLIC_URL), so the browser opens the TLS/DNS connection to the
* media host early — before it hits the first <img>. Saves ~150–300ms on the
* first image load.
*
* Reads the domain from R2_PUBLIC_URL (the same env var buildR2Storage uses),
* so there's ONE source of truth — no per-project hardcoded domain. Renders
* nothing when R2_PUBLIC_URL isn't set (local disk / no CDN → nothing to
* preconnect).
*
* Server Component — drop it in the <head> of your locale layout:
*
* import { MediaPreconnect } from '@intecion/ipal-kit/rsc'
* // in <head> (or top of <body> — Next hoists link tags):
* <MediaPreconnect />
*/
export function MediaPreconnect() {
const publicUrl = process.env.R2_PUBLIC_URL?.replace(/\/$/, '')
if (!publicUrl) {return null}
// Origin only (scheme + host) — preconnect targets an origin, not a path.
let origin: string
try {
origin = new URL(publicUrl).origin
} catch {
return null // malformed URL → skip rather than emit a broken tag
}
return (
<>
<link crossOrigin="anonymous" href={origin} rel="preconnect" />
<link href={origin} rel="dns-prefetch" />
</>
)
}
export default MediaPreconnect
+73
View File
@@ -0,0 +1,73 @@
import type { Plugin } from 'payload'
import { s3Storage } from '@payloadcms/storage-s3'
/**
* Cloudflare R2 media storage — configured from environment variables (agency
* infrastructure, not per-project panel data). R2 is S3-compatible, so we use
* @payloadcms/storage-s3 pointed at the R2 endpoint.
*
* Storage is infrastructure (like the database or PAYLOAD_SECRET): it binds at
* boot, and its credentials are agency-owned — so it lives in .env, not the
* panel. See docs/storage.md for the required variables.
*
* Returns the storage plugin when all R2 vars are present; otherwise returns a
* no-op passthrough so the project falls back to Payload's default local disk
* storage (useful in dev without R2). This mirrors how mailAdapter degrades
* gracefully when a transport isn't configured.
*
* @param collections - slugs of upload collections to offload to R2 (e.g. ['media'])
*/
export const buildR2Storage = (collections: string[] = ['media']): Plugin => {
const bucket = process.env.R2_BUCKET
const endpoint = process.env.R2_ENDPOINT
const accessKeyId = process.env.R2_ACCESS_KEY_ID
const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY
// Any missing → skip R2, fall back to local disk. Warn so it's not silent.
if (!bucket || !endpoint || !accessKeyId || !secretAccessKey) {
return (config) => {
// Only warn when SOME vars are set (partial config = likely a mistake).
if (bucket || endpoint || accessKeyId || secretAccessKey) {
console.warn(
'[ipal] R2 storage: incomplete env (need R2_BUCKET, R2_ENDPOINT, ' +
'R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY). Falling back to local disk.',
)
}
return config
}
}
// Public URL for served media. R2 is private by default; its S3 endpoint only
// accepts uploads and won't serve files (403). With a custom domain
// (media.klient.pl → bucket) set R2_PUBLIC_URL so Payload generates URLs
// pointing there. Without it, uploads work but images don't display publicly.
// See docs/storage.md.
const publicUrl = process.env.R2_PUBLIC_URL?.replace(/\/$/, '') // strip trailing slash
// generateFileURL is a PER-COLLECTION option in @payloadcms/storage-s3 (not a
// top-level one) — R2 needs it to point served URLs at the custom domain
// instead of the private S3 endpoint. Each collection gets either `true`
// (plain offload) or an object carrying generateFileURL when a public URL is set.
const generateFileURL = publicUrl
? ({ filename, prefix }: { filename: string; prefix?: string }) =>
[publicUrl, prefix, filename].filter(Boolean).join('/')
: undefined
const collectionsConfig: Record<string, { generateFileURL: typeof generateFileURL } | true> = {}
for (const slug of collections) {
collectionsConfig[slug] = generateFileURL ? { generateFileURL } : true
}
return s3Storage({
bucket,
collections: collectionsConfig,
config: {
credentials: { accessKeyId, secretAccessKey },
endpoint,
region: 'auto', // R2 uses 'auto'
// R2 requires path-style addressing for S3 compatibility.
forcePathStyle: true,
},
})
}
+1
View File
@@ -0,0 +1 @@
export { buildR2Storage } from './buildR2Storage.js'