R2 storage from env + filename normalization

This commit is contained in:
2026-08-27 12:31:09 +02:00
parent 6a8361d710
commit 9acb22e2f9
16 changed files with 820 additions and 60 deletions
+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 -2
View File
@@ -20,6 +20,9 @@ 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';
@@ -31,7 +34,6 @@ export { buildHreflangAlternates, buildMetadata, composeTitle } from './modules/
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';
+1
View File
@@ -0,0 +1 @@
export { normalizeFilename, normalizeFilenameHook } from './normalizeFilename.js';
+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;
+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;
+1
View File
@@ -0,0 +1 @@
export { buildR2Storage } from './buildR2Storage.js';