@justanarthur/payload-www 0.3.5 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +317 -307
- package/dist/access.d.ts +2 -2
- package/dist/access.js +3 -17
- package/dist/blocks.d.ts +0 -10
- package/dist/blocks.js +4 -19
- package/dist/collections.d.ts +33 -225
- package/dist/collections.js +185 -639
- package/dist/config.d.ts +19 -105
- package/dist/config.js +263 -844
- package/dist/fields.d.ts +4 -46
- package/dist/fields.js +177 -53
- package/dist/imagehash.js +0 -15
- package/dist/import-map-provider.d.ts +0 -16
- package/dist/import-map-provider.js +0 -15
- package/dist/metadata.d.ts +24 -89
- package/dist/metadata.js +123 -255
- package/dist/pages.d.ts +37 -319
- package/dist/pages.js +355 -963
- package/dist/render-pages.d.ts +69 -564
- package/dist/render-pages.js +476 -1482
- package/dist/sitemap.d.ts +10 -0
- package/dist/sitemap.js +324 -0
- package/dist/translator.js +0 -15
- package/dist/utils.d.ts +4 -12
- package/dist/utils.js +6 -42
- package/package.json +75 -178
- package/dist/components.d.ts +0 -6
- package/dist/components.js +0 -39
- package/dist/core-access.d.ts +0 -11
- package/dist/core-access.js +0 -34
- package/dist/core-blocks.d.ts +0 -24
- package/dist/core-blocks.js +0 -79
- package/dist/core-fields.d.ts +0 -73
- package/dist/core-fields.js +0 -167
- package/dist/core-utils.d.ts +0 -16
- package/dist/core-utils.js +0 -63
- package/dist/data-collections.d.ts +0 -229
- package/dist/data-collections.js +0 -676
- package/dist/data-seed.d.ts +0 -76
- package/dist/data-seed.js +0 -212
- package/dist/data-test.d.ts +0 -30
- package/dist/data-test.js +0 -1110
- package/dist/globals.d.ts +0 -69
- package/dist/globals.js +0 -251
- package/dist/hooks.d.ts +0 -108
- package/dist/hooks.js +0 -228
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -39
- package/dist/render-components.d.ts +0 -42
- package/dist/render-components.js +0 -154
- package/dist/render-metadata.d.ts +0 -122
- package/dist/render-metadata.js +0 -390
- package/dist/render-utils.d.ts +0 -166
- package/dist/render-utils.js +0 -398
- package/dist/seed.d.ts +0 -76
- package/dist/seed.js +0 -212
- package/dist/server.d.ts +0 -980
- package/dist/server.js +0 -2231
- package/dist/test.d.ts +0 -30
- package/dist/test.js +0 -1110
- package/dist/with-www-config.d.ts +0 -108
- package/dist/with-www-config.js +0 -1006
package/README.md
CHANGED
|
@@ -1,382 +1,392 @@
|
|
|
1
1
|
# @justanarthur/payload-www
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
A reusable Payload CMS website template: collections, globals, blocks, fields, access, JSON-LD / hreflang metadata, Next.js page renderers, sitemap, and the default plugin set (SEO + imagehash + translator + MCP).
|
|
4
|
+
|
|
5
|
+
The lib reads your [next-intl](https://next-intl.dev) routing config so locale validation, URL shape,
|
|
6
|
+
hreflang alternates, and the language switcher share a single source of truth with the rest of the
|
|
7
|
+
app.
|
|
8
|
+
|
|
9
|
+
The composition root is `createWWWConfig()` — see [Quick start](#quick-start) below.
|
|
9
10
|
|
|
10
11
|
## What's inside
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
| piece | exported from | purpose |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| Composer | [`/config`](#quick-start) | `createWWWConfig()` — returns `{ withWWWConfig }`. One call wires Pages + Posts + Header + Footer + the default plugin set. |
|
|
16
|
+
| Pages + Posts collections | `createPagesCollection`, `createPostsCollection` (internal — composed by the composer) | Pages (title, blocks tab, slug, drafts, revalidation); Posts (title, excerpt, richText, drafts, revalidation). |
|
|
17
|
+
| Header + Footer globals | `createHeaderGlobal`, `createFooterGlobal` (internal) | Both nav blocks with `navColumn` / `navItem`. Extend via `link({ extraFields })`. |
|
|
18
|
+
| Static-page collection | `createWWWCollectionGlobal` ([`/collections`](#collections)) | Generic factory for system pages (404, 500, search-empty) — keyed by a discriminator, no slug. |
|
|
19
|
+
| Default plugins | composed by `createWWWConfig()` | `seoPlugin`, `imageHashPlugin`, `translator`, `mcpPlugin`. Tune via `defaultPluginsConfigs`. |
|
|
20
|
+
| Fields | [`/fields`](#fields) | `link`, `linkGroup` (with `disableLabel` / `appearances` / `localized` / `relationTo` / `extraFields`), `slugField`, `appearanceOptions`. |
|
|
21
|
+
| Access | [`/access`](#access) | `anyone`, `authenticated`, `authenticatedOrPublished`. |
|
|
22
|
+
| Revalidation hooks | [`/collections`](#collections) (`createRevalidateCollectionGlobalHook`, `createCollectionCacheKey`, `populatePublishedAt`) | Tag-based cache invalidation + published-at population. |
|
|
23
|
+
| Metadata | [`/metadata`](#metadata) | `buildArticleLd`, `buildBreadcrumbsLd`, `buildOrganizationLd`, `buildWebSiteLd`, `buildProductLd`, `buildRootJsonLd`; slug transforms; `queryDocBySlug`, `queryAllDocs`, `queryAllLocaleSlugs`. |
|
|
24
|
+
| Next.js page renderers | [`/render-pages`](#render-pages) | `createCollectionPageExports`, `createRootLayoutExports`; default render components `PagesPage`, `PostsPage`, `HeaderPage`, `FooterPage`, `RootJsonLd`. |
|
|
25
|
+
| Sitemap | [`/sitemap`](#sitemap) | `createSitemapFromCollections` (Next.js file-convention helper). |
|
|
26
|
+
| Plugin re-exports | [`/imagehash`](#plugin-re-exports), [`/translator`](#plugin-re-exports) | Drop-in for hosts that don't want to import the sibling packages directly. |
|
|
26
27
|
|
|
27
28
|
## Quick start
|
|
28
29
|
|
|
29
|
-
### 1. Wire the composer
|
|
30
|
-
|
|
31
30
|
```ts
|
|
32
31
|
// payload.config.ts
|
|
33
32
|
import { buildConfig } from 'payload'
|
|
34
|
-
import { createWWWConfig } from '@justanarthur/payload-www/
|
|
35
|
-
import {
|
|
33
|
+
import { createWWWConfig } from '@justanarthur/payload-www/config'
|
|
34
|
+
import { blocks } from '@/components/blocks'
|
|
35
|
+
import { plugins } from '@/plugins'
|
|
36
|
+
|
|
37
|
+
const { withWWWConfig } = createWWWConfig()
|
|
38
|
+
|
|
39
|
+
export default buildConfig(withWWWConfig({
|
|
40
|
+
blocks, // Page block set
|
|
41
|
+
plugins, // host plugins appended after the lib's defaults
|
|
42
|
+
collections: (defaults) => [
|
|
43
|
+
...defaults,
|
|
44
|
+
Media,
|
|
45
|
+
Users
|
|
46
|
+
],
|
|
47
|
+
globals: (defaults) => [
|
|
48
|
+
...defaults.map(g => g.slug === 'header'
|
|
49
|
+
? { ...g, custom: { [payloadWwwName]: { path: '@/components/Header/Component#Header' } }, fields: headerFields }
|
|
50
|
+
: g.slug === 'footer'
|
|
51
|
+
? { ...g, custom: { [payloadWwwName]: { path: '@/components/Footer/Component#Footer' } }, fields: footerFields }
|
|
52
|
+
: g),
|
|
53
|
+
createStaticGlobal(),
|
|
54
|
+
createMessagesGlobal()
|
|
55
|
+
],
|
|
56
|
+
defaultPluginsConfigs: { // tune the lib's default plugin set
|
|
57
|
+
seo: (d) => ({ ...d, collections: ['pages', 'posts'], openaiApiKey: process.env.OPENAI_API_KEY }),
|
|
58
|
+
imageHash: (d) => ({ ...d, algorithm: 'lqip-modern' }),
|
|
59
|
+
translator: (d) => ({ ...d, autoTranslate: true, collections: ['pages', 'posts'], globals: ['header', 'footer', 'messages'] })
|
|
60
|
+
},
|
|
61
|
+
localization: { /* ... */ },
|
|
62
|
+
db: postgresAdapter({ /* ... */ }),
|
|
63
|
+
// ...rest of Payload config
|
|
64
|
+
}))
|
|
65
|
+
```
|
|
36
66
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
67
|
+
`createWWWConfig()` takes **no arguments**. You tune everything via the `WWWInputConfig` passed to
|
|
68
|
+
`withWWWConfig`. The composer reads its own package name from `package.json` and uses it as the
|
|
69
|
+
`custom.<packageName>` key — that's how the block / collection / global `custom.path` entries get
|
|
70
|
+
threaded into Payload's import map.
|
|
41
71
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
globals: [], // optional extra globals
|
|
46
|
-
// ...rest of your config
|
|
47
|
-
})
|
|
48
|
-
)
|
|
49
|
-
```
|
|
72
|
+
The `defaultPluginsConfigs` map lets you keep the defaults (seoPlugin, imageHashPlugin, translator,
|
|
73
|
+
mcpPlugin) and tweak their constructor args without re-importing them. Pass `(defaults) => …` to
|
|
74
|
+
merge or `…` to replace.
|
|
50
75
|
|
|
51
|
-
|
|
52
|
-
default plugin set (seoPlugin, imageHashPlugin, translator). Use the
|
|
53
|
-
`defaultPlugins` callback to drop or extend the list:
|
|
76
|
+
## Page exports (Next.js App Router)
|
|
54
77
|
|
|
55
78
|
```ts
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
### 2. Define your next-intl routing
|
|
79
|
+
// app/(frontend)/[locale]/layout.tsx
|
|
80
|
+
import { createRootLayoutExports } from '@justanarthur/payload-www/render-pages'
|
|
81
|
+
import { importMap } from '@/app/(payload)/admin/importMap'
|
|
82
|
+
import { routing } from '@/i18n/routing'
|
|
83
|
+
import config from '@payload-config'
|
|
84
|
+
import { getServerSideURL } from '@/lib/utils/getURL'
|
|
64
85
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
const { default: RootLayout, generateStaticParams } = createRootLayoutExports(
|
|
87
|
+
{ config, importMap, routing },
|
|
88
|
+
{ getServerSideURL }
|
|
89
|
+
)
|
|
68
90
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
import { defineRouting } from 'next-intl/routing'
|
|
72
|
-
|
|
73
|
-
export const routing = defineRouting({
|
|
74
|
-
locales: ['en', 'uk'],
|
|
75
|
-
defaultLocale: 'en',
|
|
76
|
-
// 'as-needed' means `/about` for the default locale and
|
|
77
|
-
// `/uk/about` for the others — the lib mirrors this in
|
|
78
|
-
// hreflang alternates and the language switcher.
|
|
79
|
-
localePrefix: 'as-needed',
|
|
80
|
-
labels: { en: 'English', uk: 'Українська' }
|
|
81
|
-
})
|
|
91
|
+
export default RootLayout
|
|
92
|
+
export { generateStaticParams }
|
|
82
93
|
```
|
|
83
94
|
|
|
84
|
-
### 3. Page exports
|
|
85
|
-
|
|
86
95
|
```ts
|
|
87
|
-
// app/(frontend)/[locale]/page.tsx — home
|
|
88
|
-
// app/(frontend)/[locale]/[...slug]/page.tsx — catch-all
|
|
96
|
+
// app/(frontend)/[locale]/[[...slug]]/page.tsx — Pages home + catch-all
|
|
89
97
|
import { createCollectionPageExports } from '@justanarthur/payload-www/render-pages'
|
|
90
|
-
import
|
|
91
|
-
import { importMap } from '@/app/(payload)/admin/importMap
|
|
98
|
+
import config from '@payload-config'
|
|
99
|
+
import { importMap } from '@/app/(payload)/admin/importMap'
|
|
92
100
|
import { routing } from '@/i18n/routing'
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
import { getServerSideURL } from '@/lib/utils/getURL'
|
|
102
|
+
|
|
103
|
+
const {
|
|
104
|
+
default: Page,
|
|
105
|
+
generateMetadata,
|
|
106
|
+
generateStaticParams,
|
|
107
|
+
generateSitemap
|
|
108
|
+
} = createCollectionPageExports(
|
|
109
|
+
{ config, importMap, routing, slugShape: 'catch-all' },
|
|
110
|
+
{ getServerSideURL }
|
|
111
|
+
)
|
|
103
112
|
|
|
104
113
|
export default Page
|
|
105
|
-
export { generateMetadata, generateStaticParams }
|
|
114
|
+
export { generateMetadata, generateStaticParams, generateSitemap }
|
|
106
115
|
```
|
|
107
116
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Next.js draft mode is on. No opt-in required.
|
|
111
|
-
|
|
112
|
-
#### Showcase sidebar + home extras
|
|
113
|
-
|
|
114
|
-
The home route can render inside a `<PageShowcase>` two-column layout
|
|
115
|
-
(sidebar with metadata, JSON-LD, and a language switcher) and append a
|
|
116
|
-
`homeExtras` block (recent pages, recent posts, etc.):
|
|
117
|
+
`slugShape` is `'single'` (default) or `'catch-all'`. Use `'catch-all'` if your route segment is
|
|
118
|
+
`[[...slug]]` (Pages-style), `'single'` for `[slug]` (Posts-style — file convention).
|
|
117
119
|
|
|
118
120
|
```ts
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
generateMeta,
|
|
125
|
-
showcase: { enabled: true }, // wrap in <PageShowcase>
|
|
126
|
-
homeExtras: async ({ locale }) => {
|
|
127
|
-
const { pages, posts } = await fetchRecent(locale)
|
|
128
|
-
return <RecentLists pages={pages} posts={posts} />
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
)
|
|
121
|
+
// app/(frontend)/[locale]/posts/[slug]/page.tsx
|
|
122
|
+
const { default: PostPage, generateMetadata, generateStaticParams } = createCollectionPageExports(
|
|
123
|
+
{ config, importMap, routing, slug: 'posts', slugShape: 'single' },
|
|
124
|
+
{ getServerSideURL }
|
|
125
|
+
)
|
|
132
126
|
```
|
|
133
127
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
### `createCollectionPageExports(args, deps)` — args
|
|
129
|
+
|
|
130
|
+
| arg | type | default | notes |
|
|
131
|
+
|---|---|---|---|
|
|
132
|
+
| `config` | `Promise<SanitizedConfig>` | required | the host's `payload.config.ts` |
|
|
133
|
+
| `importMap` | `ImportMap` | required | the host's `app/(payload)/admin/importMap` |
|
|
134
|
+
| `routing` | `RoutingConfig` | required | `{ locales, defaultLocale, localePrefix, labels? }` from `next-intl/routing`'s `defineRouting` |
|
|
135
|
+
| `slug` | `string` | `'pages'` | collection slug to query |
|
|
136
|
+
| `slugShape` | `'single' \| 'catch-all'` | `'single'` | drives `generateStaticParams` + slug segmentation |
|
|
137
137
|
|
|
138
|
-
###
|
|
138
|
+
### `createCollectionPageExports(args, deps)` — deps
|
|
139
|
+
|
|
140
|
+
| dep | type | notes |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `getServerSideURL` | `() => string` | host's absolute-URL helper |
|
|
143
|
+
| `pagePathPrefix` | `string` | optional URL prefix for `generateSitemap` only — not used for rendering |
|
|
144
|
+
|
|
145
|
+
### What you get back
|
|
139
146
|
|
|
140
147
|
```ts
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
matcher: ['/', '/((?!api|_next|_vercel|admin|next|.*\\..*).*)']
|
|
148
|
+
{
|
|
149
|
+
default: Page, // the page component
|
|
150
|
+
generateMetadata, // Next.js MetadataRoute hook
|
|
151
|
+
generateStaticParams, // Next.js static-params hook
|
|
152
|
+
generateSitemap // MetadataRoute.Sitemap producer for /sitemap.ts
|
|
147
153
|
}
|
|
148
154
|
```
|
|
149
155
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
### `createRootLayoutExports(args, deps)`
|
|
157
|
+
|
|
158
|
+
| arg | type | notes |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| `config` | `Promise<SanitizedConfig>` | required |
|
|
161
|
+
| `importMap` | `ImportMap` | required |
|
|
162
|
+
| `routing` | `RoutingConfig` | required |
|
|
163
|
+
|
|
164
|
+
| dep | type | notes |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| `getServerSideURL` | `() => string` | if provided, the SEO plugin's `RootJsonLd` (`Organization` / `WebSite` JSON-LD) is auto-injected as the first child of `<html>` |
|
|
167
|
+
| `providers` | `(args) => ReactNode` | wraps `{children}` between the rendered Header and Footer |
|
|
168
|
+
| `htmlAttrs` | `(locale) => HTMLAttributes<HTMLHtmlElement>` | extra `<html>` attributes per locale (defaults: `lang={locale}`, `suppressHydrationWarning`) |
|
|
169
|
+
|
|
170
|
+
The layout reads `header` + `footer` globals in parallel and wraps the children in
|
|
171
|
+
`<NextIntlClientProvider>`. Locale is pulled from the `[locale]` route segment and validated against
|
|
172
|
+
`routing.locales` — unknown locales trigger `notFound()`.
|
|
173
|
+
|
|
174
|
+
## Sitemap
|
|
155
175
|
|
|
156
176
|
```ts
|
|
157
177
|
// app/(frontend)/sitemap.ts
|
|
158
|
-
import {
|
|
159
|
-
import
|
|
160
|
-
import { getServerSideURL } from '@/
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
//
|
|
165
|
-
// the
|
|
166
|
-
|
|
167
|
-
collections: ['pages', 'posts'],
|
|
168
|
-
config: configPromise,
|
|
178
|
+
import { createSitemapFromCollections } from '@justanarthur/payload-www/sitemap'
|
|
179
|
+
import config from '@payload-config'
|
|
180
|
+
import { getServerSideURL } from '@/lib/utils/getURL'
|
|
181
|
+
|
|
182
|
+
export default createSitemapFromCollections({
|
|
183
|
+
getServerSideURL,
|
|
184
|
+
pagePathPrefix: '' // empty for root-mounted (Pages)
|
|
185
|
+
// pagePathPrefix: '/posts' // use this for the Posts catch-all
|
|
186
|
+
}, {
|
|
169
187
|
getServerSideURL,
|
|
170
|
-
|
|
171
|
-
// (`/about`), other locales prefixed (`/uk/about`). Matches the
|
|
172
|
-
// host's next-intl `localePrefix` so sitemap URLs match the
|
|
173
|
-
// actual route shape.
|
|
174
|
-
localePrefix: 'as-needed',
|
|
175
|
-
// Collections mounted under a sub-route (here: Posts at
|
|
176
|
-
// `/posts/[...slug]`) need their sitemap URLs prefixed so they
|
|
177
|
-
// match the real route. Pages live at the root, so no prefix.
|
|
178
|
-
urlPrefixes: { posts: '/posts' }
|
|
188
|
+
pagePathPrefix: '/posts'
|
|
179
189
|
})
|
|
180
190
|
```
|
|
181
191
|
|
|
182
|
-
|
|
183
|
-
`
|
|
192
|
+
`createSitemapFromCollections(...args)` takes one or more deps-shaped objects (the same shape
|
|
193
|
+
`createCollectionPageExports`'s deps accept) and returns a Next.js `MetadataRoute.Sitemap`-compatible
|
|
194
|
+
function. Mount one per collection under a sub-route, or a single call for the root.
|
|
184
195
|
|
|
185
|
-
|
|
196
|
+
The lib's Pages `afterChange` hook fires `revalidateTag(collection_pages_<slug>_<locale>, 'max')`,
|
|
197
|
+
which is the key `createCollectionPageExports` / `createSitemapFromCollections` reads from — so
|
|
198
|
+
edits refresh the sitemap without manual rebuilds.
|
|
186
199
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
metadata / sitemap / static-params plumbing (system pages have no
|
|
193
|
-
URL):
|
|
200
|
+
## Static pages (404 / 500 / system)
|
|
201
|
+
|
|
202
|
+
There's no dedicated `createStaticPageExports` (yet). System pages render via a normal
|
|
203
|
+
`createCollectionPageExports({ slug: 'static-pages', slugShape: 'single' })` mount, addressed by a
|
|
204
|
+
discriminator instead of a slug:
|
|
194
205
|
|
|
195
206
|
```ts
|
|
196
207
|
// app/(frontend)/[locale]/not-found.tsx
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const { default: NotFound } = createStaticPageExports({
|
|
202
|
-
config: configPromise,
|
|
203
|
-
importMap,
|
|
204
|
-
})
|
|
208
|
+
const { default: NotFound } = createCollectionPageExports(
|
|
209
|
+
{ config, importMap, routing, slug: 'static-pages', slugShape: 'single' },
|
|
210
|
+
{ getServerSideURL }
|
|
211
|
+
)
|
|
205
212
|
|
|
206
213
|
export default NotFound
|
|
207
214
|
```
|
|
208
215
|
|
|
209
|
-
`
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
216
|
+
The `static-pages` collection is built via `createWWWCollectionGlobal({...}, { slug: 'static-pages', renderPath: '@/components/StaticPage/Component#StaticPage', isDraft: false })`. Editors pick a `kind` (`'not-found'`, `'server-error'`, `'search-empty'`, `'offline'`), populate the `blocks` tab with the same block set you passed to `createWWWConfig`, and the host's not-found / server-error route renders the row. `populatePublishedAt` and the revalidation hooks are wired automatically.
|
|
217
|
+
|
|
218
|
+
## Collections
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import {
|
|
222
|
+
createWWWCollectionGlobal, // generic factory (used internally for static-pages)
|
|
223
|
+
createRevalidateCollectionGlobalHook, // the afterChange/afterDelete pair
|
|
224
|
+
createCollectionCacheKey, // produces the tag string
|
|
225
|
+
queryDoc // server-side helper used by renderers
|
|
226
|
+
} from '@justanarthur/payload-www/collections'
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`createWWWCollectionGlobal(fields, { slug, renderPath, isGlobalConfig?, isDraft? })`:
|
|
230
|
+
|
|
231
|
+
| arg | type | notes |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| `fields` | `Field[]` | the collection's field set (the factory adds `slug`, `publishedAt`, access, hooks) |
|
|
234
|
+
| `slug` | `string` | collection slug |
|
|
235
|
+
| `renderPath` | `string` | import-map path to the render component (`'@/components/Foo/Component#Foo'`) |
|
|
236
|
+
| `isGlobalConfig` | `boolean` | `true` for globals, `false` (default) for collections |
|
|
237
|
+
| `isDraft` | `boolean` | `true` (default) enables Payload's drafts + autosave; `false` for system pages |
|
|
215
238
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
admin-only — not rendered. `kind` is `unique`, so the database
|
|
220
|
-
enforces one row per system page. Drafts + autosave mirror `pages`.
|
|
221
|
-
The translator plugin includes `'static-pages'` in its default
|
|
222
|
-
`collections` list, so SK content fills automatically when you expand
|
|
223
|
-
`localization.locales`.
|
|
239
|
+
The factory wires `custom[packageName] = { path: renderPath }`, access (`create`/`update`/`delete`
|
|
240
|
+
require auth, `read` is `anyone` or `authenticatedOrPublished` depending on `isDraft`), the
|
|
241
|
+
`afterChange`/`afterDelete` revalidation hook, and `populatePublishedAt` (`beforeChange`).
|
|
224
242
|
|
|
225
|
-
Revalidation
|
|
226
|
-
`createRevalidateCollectionHook` — no URL fan-out (system pages have
|
|
227
|
-
no slug), but the per-id tag (`collection_static-pages_<id>`) and the
|
|
228
|
-
collection-wide tag (`static-pages` via the `sitemapTag` override)
|
|
229
|
-
fire on every change.
|
|
243
|
+
### Revalidation
|
|
230
244
|
|
|
231
|
-
|
|
245
|
+
`createRevalidateCollectionGlobalHook()` returns `{ afterChange, afterDelete }` — the same function
|
|
246
|
+
used for both. On every save / delete of a published doc it fires:
|
|
232
247
|
|
|
233
|
-
|
|
248
|
+
```
|
|
249
|
+
revalidateTag(`<slug><slug>_<locale>`, 'max') // collections
|
|
250
|
+
revalidateTag(`<globalSlug>_<locale>`, 'max') // globals
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`createCollectionCacheKey({ collectionSlug, slug, locale })` produces the same key string — use it
|
|
254
|
+
in your own `unstable_cache` / `fetch` cache keys if you want them invalidated by the lib's hooks.
|
|
255
|
+
|
|
256
|
+
> The `// @ts-expect-error` and `// todo sitemap caching and revalidation` markers in
|
|
257
|
+
> `createRevalidateCollectionGlobalHook.ts` are intentional, not bugs — the hook covers tag-based
|
|
258
|
+
> invalidation; URL revalidation and the unified sitemap tag are still TODO.
|
|
259
|
+
|
|
260
|
+
## Fields
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import { link, linkGroup, appearanceOptions, slugField } from '@justanarthur/payload-www/fields'
|
|
264
|
+
|
|
265
|
+
// link({ extraFields: [...] }) — append host fields (description, navHover, …)
|
|
266
|
+
// linkGroup({ appearances: ['default', 'outline'] })
|
|
267
|
+
// slugField({ localized: false, nested: true })
|
|
268
|
+
// appearanceOptions — for selects that should match link `appearances`
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`link({ extraFields })` is the extension point for host-specific nav-link shapes. The lib's
|
|
272
|
+
`createHeaderGlobal` / `createFooterGlobal` accept `navColumnLinkFields` / `navItemLinkFields` that
|
|
273
|
+
are forwarded into their `navColumn` / `navItem` blocks.
|
|
274
|
+
|
|
275
|
+
## Access
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import { anyone, authenticated, authenticatedOrPublished } from '@justanarthur/payload-www/access'
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
- `anyone` — always true.
|
|
282
|
+
- `authenticated` — true when the request has a user.
|
|
283
|
+
- `authenticatedOrPublished` — true when authenticated **or** the doc is `_status: 'published'`.
|
|
284
|
+
|
|
285
|
+
## Metadata
|
|
234
286
|
|
|
235
287
|
```ts
|
|
236
288
|
import {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
createPostsCollection,
|
|
240
|
-
createStaticPagesCollection,
|
|
241
|
-
createHeaderGlobal,
|
|
242
|
-
createFooterGlobal,
|
|
243
|
-
generatePreviewPath, // admin.preview URL builder
|
|
244
|
-
HOME_PAGE_SLUG, PAGES_SLUG, POSTS_SLUG, STATIC_PAGES_SLUG,
|
|
245
|
-
link, linkGroup, // fields
|
|
246
|
-
appearanceOptions,
|
|
247
|
-
anyone, authenticated, // access
|
|
248
|
-
authenticatedOrPublished,
|
|
249
|
-
createRevalidatePageHooks, // hooks
|
|
250
|
-
createRevalidateCollectionHook, // hooks (canonical; createRevalidatePageHooks is a deprecated Pages preset)
|
|
251
|
-
createRevalidateGlobalHook,
|
|
252
|
-
buildArticleLd, // metadata
|
|
253
|
-
buildBreadcrumbsLd,
|
|
289
|
+
buildArticleLd, // Article JSON-LD
|
|
290
|
+
buildBreadcrumbsLd, // BreadcrumbList JSON-LD
|
|
254
291
|
buildOrganizationLd,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
292
|
+
buildWebSiteLd,
|
|
293
|
+
buildProductLd,
|
|
294
|
+
buildRootJsonLd, // combined Organization + WebSite + Product (used by RootJsonLd)
|
|
295
|
+
queryDocBySlug,
|
|
296
|
+
queryAllDocs, // for generateStaticParams
|
|
297
|
+
queryAllLocaleSlugs, // for hreflang alternates
|
|
298
|
+
paramsSlugToSlug, // turn [locale]/[[...slug]] params → stored slug
|
|
299
|
+
slugToParamsSlug // turn stored slug → params for generateStaticParams
|
|
300
|
+
} from '@justanarthur/payload-www/metadata'
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Blocks
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
import { RenderBlocks } from '@justanarthur/payload-www/blocks'
|
|
307
|
+
|
|
308
|
+
export function PageBody({ blocks }) {
|
|
309
|
+
return <RenderBlocks blocks={blocks} />
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
`RenderBlocks` reads each block's `custom[packageName].path` from Payload and dynamically imports
|
|
314
|
+
the matching component from the host's `importMap`.
|
|
315
|
+
|
|
316
|
+
## Plugin re-exports
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import { imageHashPlugin } from '@justanarthur/payload-www/imagehash'
|
|
320
|
+
import { translator } from '@justanarthur/payload-www/translator'
|
|
321
|
+
import { seoPlugin } from '@justanarthur/payload-plugin-seo' // no re-export here, import directly
|
|
322
|
+
import { mcpPlugin } from '@payloadcms/plugin-mcp' // no re-export here, import directly
|
|
268
323
|
```
|
|
269
324
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
|
282
|
-
|
|
283
|
-
| `@justanarthur/payload-www/
|
|
284
|
-
| `@justanarthur/payload-www/render-
|
|
285
|
-
| `@justanarthur/payload-www/
|
|
286
|
-
| `@justanarthur/payload-www/
|
|
287
|
-
| `@justanarthur/payload-www/
|
|
288
|
-
| `@justanarthur/payload-www/
|
|
289
|
-
| `@justanarthur/payload-www/
|
|
290
|
-
| `@justanarthur/payload-www/
|
|
291
|
-
| `@justanarthur/payload-www/
|
|
292
|
-
| `@justanarthur/payload-www/
|
|
293
|
-
| `@justanarthur/payload-www/
|
|
294
|
-
| `@justanarthur/payload-www/
|
|
295
|
-
| `@justanarthur/payload-www/
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
| `localePrefix` | `'always' \| 'as-needed' \| 'never'` | Mirrors next-intl. Default `'always'`. Set to `'as-needed'` (with `defaultLocale`) when the host uses as-needed routing. |
|
|
323
|
-
| `defaultLocale` | `string` | Default locale for `localePrefix: 'as-needed'`. Falls back to `req.payload.config.localization.defaultLocale`. |
|
|
324
|
-
| `pathMode` | `'url' \| 'tag-only'` | Default `'url'` — fans out `revalidatePath` per locale × slug. Set `'tag-only'` for collections without a URL (e.g. `staticPages`); the hook still fires the per-id tag (`collection_<slug>_<id>`) and the collection-wide tag (via `sitemapTag`). |
|
|
325
|
-
|
|
326
|
-
The canonical hook for all collections — Pages, Posts, and host-defined. Pages internally uses this with `collectionSlug: 'pages'`, `urlPathPrefix: ''`. Fires `revalidatePath` for **every** declared locale (not just the request locale), handles slug renames while published, fires `revalidateTag('collection_<slug>_<id>', 'max')` for hosts that cache by id, and respects `req.context.disableRevalidate` for seed scripts.
|
|
327
|
-
|
|
328
|
-
`createRevalidatePageHooks()` is a deprecated alias for `createRevalidateCollectionHook({ collectionSlug: 'pages', urlPathPrefix: '' })` — kept for backward compat with hosts that imported this name directly.
|
|
329
|
-
|
|
330
|
-
`createCollectionPageExports({ config, importMap, slug?, renderPath?, routing }, deps, options?)`:
|
|
331
|
-
|
|
332
|
-
| Param | Type | Description |
|
|
333
|
-
|--------------|-----------------|------------------------------------------------------------------------|
|
|
334
|
-
| `config` | `Promise<SanitizedConfig>` | Resolved Payload config from `payload.config.ts` |
|
|
335
|
-
| `importMap` | `ImportMap` | The host's `app/(payload)/admin/importMap.js` |
|
|
336
|
-
| `slug` | `string` | Default `'pages'` |
|
|
337
|
-
| `renderPath` | `string` | Override the lib's `PAGES_RENDER_PATH` |
|
|
338
|
-
| `routing` | `PageRouting` | The host's `defineRouting({...})` result — drives URL building, hreflang, and the language switcher |
|
|
339
|
-
| `deps.getServerSideURL` | `() => string` | Host's absolute URL helper |
|
|
340
|
-
| `deps.generateMeta` | `(args) => Promise<Metadata>` | Host's metadata composer |
|
|
341
|
-
| `deps.notFoundOnMissing` | `boolean` | Default `true` — render 404 for unknown slugs |
|
|
342
|
-
| `deps.showcase` | `ShowcaseOptions \| false` | When `{ enabled: true }`, wraps the body in `<PageShowcase>` |
|
|
343
|
-
| `deps.homeExtras` | `(args) => ReactNode \| Promise<ReactNode>` | Content appended to the home route (slug = `''`) |
|
|
344
|
-
| `options.jsonLd` | `boolean \| JsonLdEntry[]` | Default `{ type: 'website' }` for every page |
|
|
345
|
-
| `options.changefreq` | `string` | Default `'weekly'` |
|
|
346
|
-
| `options.priority` | `number` | Default `0.5` |
|
|
347
|
-
| `options.websiteName` | `string` | Override the auto-generated `WebSite` JSON-LD `name` |
|
|
348
|
-
|
|
349
|
-
`createSitemapFile({ ... })`:
|
|
350
|
-
|
|
351
|
-
| Option | Type | Description |
|
|
352
|
-
|-----------------|-----------------|------------------------------------------------------------------------|
|
|
353
|
-
| `collections` | `string[]` | Collection slugs whose docs appear in the sitemap |
|
|
354
|
-
| `config` | `Promise<any>` | The Payload config promise |
|
|
355
|
-
| `getServerSideURL` | `() => string` | Host's absolute URL helper |
|
|
356
|
-
| `localePrefix` | `'always' \| 'as-needed' \| 'never'` | Default `'always'`. Mirrors next-intl's `localePrefix` so the sitemap URLs match the host's route shape. With `'as-needed'`, the default locale renders without a prefix. |
|
|
357
|
-
| `locales` | `string[]` | Optional locale filter. Defaults to every `config.localization.locales`. |
|
|
358
|
-
| `urlPrefixes` | `Record<string, string>` | Per-collection URL path prefix. Default `''`. Pass `{ posts: '/posts' }` for a collection mounted under a sub-route. |
|
|
359
|
-
| `perCollection` | `Record<string, { priority?, changefreq? }>` | Per-collection overrides |
|
|
360
|
-
|
|
361
|
-
## Migration notes
|
|
362
|
-
|
|
363
|
-
- `src/middleware.ts` is deprecated in favor of `src/proxy.ts` (next-intl ≥4). The lib doesn't ship a middleware — wire `createMiddleware(routing)` in your own `proxy.ts`.
|
|
364
|
-
- `PageRouting` is a new required arg on `createCollectionPageExports`. It's a structural subset of the next-intl `defineRouting` result (locales, defaultLocale, localePrefix, labels), so passing your routing object directly works.
|
|
365
|
-
- `createBaseSeed` now sets `_status: 'published'` on created / updated pages and posts by default. Pass `status: 'draft'` per entry to keep it as a draft.
|
|
325
|
+
Use these if you want to compose the default plugin set manually outside `createWWWConfig`. Full
|
|
326
|
+
plugin docs:
|
|
327
|
+
|
|
328
|
+
- [`@justanarthur/payload-plugin-seo`](../../plugins/seo/README.md)
|
|
329
|
+
- [`@justanarthur/payload-imagehash-plugin`](../../plugins/imagehash/README.md)
|
|
330
|
+
- [`@justanarthur/payload-plugin-translator`](../../plugins/translate/README.md)
|
|
331
|
+
|
|
332
|
+
## Public subpath exports
|
|
333
|
+
|
|
334
|
+
The package's `package.json#exports` map:
|
|
335
|
+
|
|
336
|
+
| Subpath | What's there |
|
|
337
|
+
|---|---|
|
|
338
|
+
| `@justanarthur/payload-www/config` | `createWWWConfig`, `WWWConfigApi`, `WWWInputConfig` |
|
|
339
|
+
| `@justanarthur/payload-www/render-pages` | `createCollectionPageExports`, `createRootLayoutExports`, `PagesPage`, `PostsPage`, `HeaderPage`, `FooterPage`, `RootJsonLd` + types |
|
|
340
|
+
| `@justanarthur/payload-www/pages` | subset of `/render-pages` (no `createRootLayoutExports`, no `PostsPage`, no `RootJsonLd`) |
|
|
341
|
+
| `@justanarthur/payload-www/sitemap` | `createSitemapFromCollections` |
|
|
342
|
+
| `@justanarthur/payload-www/blocks` | `RenderBlocks`, `RenderBlocksProps` |
|
|
343
|
+
| `@justanarthur/payload-www/collections` | `createWWWCollectionGlobal`, `createRevalidateCollectionGlobalHook`, `createCollectionCacheKey`, `queryDoc` |
|
|
344
|
+
| `@justanarthur/payload-www/fields` | `link`, `linkGroup`, `appearanceOptions`, `slugField`, `LinkAppearances`, `LinkOptions` |
|
|
345
|
+
| `@justanarthur/payload-www/access` | `anyone`, `authenticated`, `authenticatedOrPublished` |
|
|
346
|
+
| `@justanarthur/payload-www/metadata` | `buildArticleLd`, `buildBreadcrumbsLd`, `buildOrganizationLd`, `buildWebSiteLd`, `buildProductLd`, `buildRootJsonLd`, `queryDocBySlug`, `queryAllDocs`, `queryAllLocaleSlugs`, `paramsSlugToSlug`, `slugToParamsSlug` + types |
|
|
347
|
+
| `@justanarthur/payload-www/utils` | `generateImportName`, `getFromImportMap` |
|
|
348
|
+
| `@justanarthur/payload-www/imagehash` | `imageHashPlugin`, `BlurhashPluginOptions` (re-export of `@justanarthur/payload-imagehash-plugin`) |
|
|
349
|
+
| `@justanarthur/payload-www/translator` | `translator` (re-export of `@justanarthur/payload-plugin-translator`) |
|
|
350
|
+
| `@justanarthur/payload-www/import-map-provider` | `setImportMapProvider`, `getImportMap` (stubs in the current build — reserved for future use) |
|
|
351
|
+
|
|
352
|
+
There is **no root import** (`.`) and **no `/server`, `/with-www-config`, `/globals`, `/hooks`,
|
|
353
|
+
`/render-utils`, `/render-components`, `/render-metadata`, `/components`, `/seed`, `/test`,
|
|
354
|
+
`/data-seed`, `/data-test`, `/data-collections`** in the published exports — paths the previous
|
|
355
|
+
README advertised that will fail at import time.
|
|
356
|
+
|
|
357
|
+
## What the previous README got wrong
|
|
358
|
+
|
|
359
|
+
For agents migrating from older docs:
|
|
360
|
+
|
|
361
|
+
- `createWWWConfig({ locales, blocks })` → now `createWWWConfig()` (no args). Locales come from your
|
|
362
|
+
Payload `localization` config; `blocks` come from the `WWWInputConfig.blocks` field.
|
|
363
|
+
- `defaultPlugins` callback → now `defaultPluginsConfigs: { seo, imageHash, translator, mcp }` map
|
|
364
|
+
on `WWWInputConfig`.
|
|
365
|
+
- `createRevalidateCollectionHook({ collectionSlug, urlPathPrefix, … })` (CHANGELOG `[Unreleased]`)
|
|
366
|
+
→ **not implemented yet**. The current hook is `createRevalidateCollectionGlobalHook()` (no args),
|
|
367
|
+
exported from `/collections`. Per-locale URL fan-out + `revalidatePath` does not happen — only
|
|
368
|
+
`revalidateTag` fires.
|
|
369
|
+
- `createSitemapFile` → **not implemented**. The current export is `createSitemapFromCollections`,
|
|
370
|
+
used per-collection from your `app/(frontend)/sitemap.ts`.
|
|
371
|
+
- `createPreviewHandler`, `LocaleSwitcher`, `LivePreviewListener`, `PageShowcase`, `HomePage`,
|
|
372
|
+
`createStaticPageExports`, `createStaticPagesCollection` → **none of these exist** as exports in
|
|
373
|
+
the current build. System pages render through a normal `createCollectionPageExports` mount (see
|
|
374
|
+
[Static pages](#static-pages-404--500--system) above).
|
|
375
|
+
- `Pages / Posts slug` is `localized: true` by default — slug lives in `<collection>_locales`. Pass
|
|
376
|
+
`slugField({ localized: false })` to opt out per-collection.
|
|
366
377
|
|
|
367
378
|
## Building
|
|
368
379
|
|
|
369
380
|
```bash
|
|
370
381
|
bun install
|
|
371
|
-
bun run build #
|
|
382
|
+
bun run build # bunup + scripts/strip-createRequire.mjs
|
|
372
383
|
bun run typecheck # tsc --noEmit
|
|
373
384
|
bun run test # vitest run
|
|
374
385
|
```
|
|
375
386
|
|
|
376
|
-
The lib
|
|
377
|
-
|
|
378
|
-
`src/exports/`, each re-exports from the implementation.
|
|
387
|
+
The lib uses the same `bunup` + `src/exports/*` shim pattern as the sibling plugins. One shim file
|
|
388
|
+
per subpath under `src/exports/`, each re-exporting from the implementation.
|
|
379
389
|
|
|
380
|
-
##
|
|
390
|
+
## Licence
|
|
381
391
|
|
|
382
392
|
MIT
|