Files
ipal-kit/dist/modules/frontend/createContentHelpers.js
T

131 lines
5.4 KiB
JavaScript

import { cache } from 'react';
import { getPayload } from 'payload';
import { resolveRoute as resolveRouteRaw, getArchiveEntries } from '../content/index.js';
import { buildSitemapEntries, buildRobots } from '../seo/index.js';
/**
* Bundles the per-request data helpers a frontend needs — the same cached
* wrappers every project was writing by hand (getPayload, settings, locale
* list, route resolution, archive entries).
*
* Everything is wrapped in React `cache()`, so within one request a value is
* fetched once no matter how many times it's asked for — which matters because
* Next runs generateMetadata and the page component separately, and both hit
* these. Crucially the Payload instance is cached *here*, once, so every helper
* shares it; that's why this is a factory and not loose functions importing a
* shared module.
*
* ```ts
* // src/lib/content.ts
* import { createContentHelpers } from 'ipal-kit'
* import config from '@/payload.config'
* import { contentConfig } from '@/content.config'
*
* export const { getCachedPayload, getSettings, getConfiguredLocales, resolveRoute, getEntries } =
* createContentHelpers({ config, content: contentConfig })
* ```
*
* `resolveRoute` and `getEntries` are separate on purpose: routing decides what
* a URL is, fetching gets the listing. Metadata generation needs the first and
* not the second, and a page component composes them in two obvious lines.
*/ export function createContentHelpers({ config, content, settingsSlug = 'site-settings', pagesSlug = 'pages', i18n, baseUrl }) {
const origin = baseUrl ?? process.env.NEXT_PUBLIC_SERVER_URL ?? '';
const getCachedPayload = cache(async ()=>getPayload({
config: await config
}));
const getConfiguredLocales = cache(async ()=>{
const c = await config;
return c.localization ? c.localization.locales.map((l)=>l.code) : [];
});
const getSettings = cache(async (locale)=>{
const payload = await getCachedPayload();
return payload.findGlobal({
slug: settingsSlug,
locale: locale,
depth: 2
});
});
/** What does this URL point at? Routing only — no listing data. */ const resolveRoute = cache(async (locale, segments, page)=>{
const payload = await getCachedPayload();
return resolveRouteRaw({
payload,
locale,
segments,
page,
content,
pagesSlug,
settingsSlug
});
});
/** One page of a collection's entries, for an archive listing. */ const getEntries = cache(async (collection, locale, page, perPage)=>{
const payload = await getCachedPayload();
return getArchiveEntries({
payload,
collection,
locale,
page,
perPage
});
});
/**
* Ready-made handler for Next's `app/sitemap.ts` — every page and entry with
* per-URL hreflang and lastmod. Re-export it directly:
*
* ```ts
* // app/sitemap.ts
* export { sitemap as default } from '@/lib/content'
* export const dynamic = 'force-dynamic' // generate at runtime, not build
* ```
*
* IMPORTANT — container deploys (Coolify/Docker/Railway/CI): Next treats
* sitemap.ts as STATIC by default and prerenders it during `next build`, which
* calls into Payload → the database. The build container usually has no access
* to the internal Docker network, so the DB connection fails (ENOTFOUND) and
* the build dies. Two defenses:
* 1. `export const dynamic = 'force-dynamic'` in app/sitemap.ts — skips build
* prerender, generates at runtime when the DB is reachable (recommended).
* 2. This handler also catches DB errors and returns [] so that even without
* (1) the build won't crash — it just ships an empty sitemap until the
* next runtime regeneration. (1) is still preferred; (2) is a safety net.
*/ const sitemap = cache(async ()=>{
if (!i18n) {
throw new Error('[ipal] createContentHelpers: pass `i18n` to use the sitemap handler.');
}
try {
return await buildSitemapEntries({
payload: await getCachedPayload(),
config: i18n,
baseUrl: origin,
content,
pagesSlug,
settingsSlug
});
} catch (error) {
// DB unreachable (typically a container build with no DB network) — return
// an empty sitemap instead of failing the build. Runtime regeneration will
// produce the real one once the DB is reachable. See dynamic='force-dynamic'.
console.warn('[ipal] sitemap: could not reach the database, returning empty entries ' + "(add `export const dynamic = 'force-dynamic'` to app/sitemap.ts to " + 'generate at runtime and avoid build-time DB access):', error);
return [];
}
});
/**
* Ready-made handler for Next's `app/robots.ts`. Re-export directly:
*
* ```ts
* // app/robots.ts
* export { robots as default } from '@/lib/content'
* ```
*/ const robots = ()=>buildRobots({
baseUrl: origin
});
return {
getCachedPayload,
getConfiguredLocales,
getSettings,
resolveRoute,
getEntries,
sitemap,
robots
};
}
//# sourceMappingURL=createContentHelpers.js.map