@postedin/cms-client 0.1.0
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 +66 -0
- package/bin/dissect/cli.mjs +138 -0
- package/bin/dissect/dissect.mjs +290 -0
- package/bin/profile/build.mjs +106 -0
- package/bin/profile/fetch-log.mjs +298 -0
- package/bin/profile/format.mjs +90 -0
- package/bin/profile/interference-summary.mjs +558 -0
- package/bin/profile/interference.mjs +604 -0
- package/bin/profile/measure.mjs +137 -0
- package/bin/profile/report.mjs +90 -0
- package/bin/profile/site-env.mjs +16 -0
- package/bin/profile/summarize.mjs +429 -0
- package/dist/browser.d.ts +145 -0
- package/dist/browser.js +11 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunk-6V54ITTK.js +197 -0
- package/dist/chunk-6V54ITTK.js.map +1 -0
- package/dist/chunk-MNZ7DIGC.js +51 -0
- package/dist/chunk-MNZ7DIGC.js.map +1 -0
- package/dist/form-proxy/upload-policy.d.ts +40 -0
- package/dist/form-proxy/upload-policy.js +17 -0
- package/dist/form-proxy/upload-policy.js.map +1 -0
- package/dist/index.d.ts +570 -0
- package/dist/index.js +1636 -0
- package/dist/index.js.map +1 -0
- package/dist/payload-types.d.ts +8985 -0
- package/dist/payload-types.js +1 -0
- package/dist/payload-types.js.map +1 -0
- package/package.json +74 -0
- package/src/api.ts +387 -0
- package/src/blog-listing.ts +75 -0
- package/src/browser.ts +24 -0
- package/src/client.ts +144 -0
- package/src/cms-to-href.ts +70 -0
- package/src/cms.ts +86 -0
- package/src/collections/appearance.ts +94 -0
- package/src/collections/areas.ts +29 -0
- package/src/collections/authors.ts +27 -0
- package/src/collections/banners.ts +14 -0
- package/src/collections/categories.ts +111 -0
- package/src/collections/forms.ts +29 -0
- package/src/collections/header-footer.ts +19 -0
- package/src/collections/image-links.ts +14 -0
- package/src/collections/media.ts +18 -0
- package/src/collections/options.ts +10 -0
- package/src/collections/pages.ts +83 -0
- package/src/collections/posts.ts +249 -0
- package/src/collections/project.ts +16 -0
- package/src/collections/questions.ts +35 -0
- package/src/collections/seo.ts +10 -0
- package/src/collections/tags.ts +25 -0
- package/src/collections/team-members.ts +79 -0
- package/src/config-time.ts +98 -0
- package/src/context.ts +12 -0
- package/src/decode-html.ts +8 -0
- package/src/form-proxy/cms-client.ts +95 -0
- package/src/form-proxy/cms-errors.ts +73 -0
- package/src/form-proxy/cms-write.ts +44 -0
- package/src/form-proxy/http.ts +96 -0
- package/src/form-proxy/index.ts +73 -0
- package/src/form-proxy/rate-limit.ts +46 -0
- package/src/form-proxy/submissions.ts +88 -0
- package/src/form-proxy/types.ts +23 -0
- package/src/form-proxy/upload-policy.ts +92 -0
- package/src/form-proxy/uploads.ts +81 -0
- package/src/home-page.ts +83 -0
- package/src/index.ts +68 -0
- package/src/loader.ts +83 -0
- package/src/locales.ts +80 -0
- package/src/payload-types.ts +10854 -0
- package/src/placeholder.ts +9 -0
- package/src/resolve-menu-items.ts +184 -0
- package/src/routes.ts +184 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { CmsLink } from './collections/header-footer';
|
|
2
|
+
import type { Locale } from './locales';
|
|
3
|
+
import type { Author, Category, Page, Post, Tag } from './payload-types';
|
|
4
|
+
import type { CreateRoutes, HomePageRef } from './routes';
|
|
5
|
+
|
|
6
|
+
export type CmsToHref = ReturnType<typeof defineCmsToHref>;
|
|
7
|
+
|
|
8
|
+
/** `cmsToHref`, bound to the site's route builder. Pure; islands use it too. */
|
|
9
|
+
export function defineCmsToHref(createRoutes: CreateRoutes) {
|
|
10
|
+
return (
|
|
11
|
+
link: Partial<CmsLink>,
|
|
12
|
+
locale: Locale,
|
|
13
|
+
homePage: HomePageRef,
|
|
14
|
+
): string | undefined => cmsToHref(createRoutes, link, locale, homePage);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function cmsToHref(
|
|
18
|
+
createRoutes: CreateRoutes,
|
|
19
|
+
{ type, url, reference }: Partial<CmsLink>,
|
|
20
|
+
locale: Locale,
|
|
21
|
+
// The page configured as the site homepage — read it with `getHomePageRef`
|
|
22
|
+
// (server) or take it as a prop (islands). `createRoutes` owns the collapse
|
|
23
|
+
// to `/`, so every `routes.page()` caller gets the same behaviour; passing it
|
|
24
|
+
// here just hands it through. Required so a new call site can't silently fall
|
|
25
|
+
// back to the wrong URL.
|
|
26
|
+
homePage: HomePageRef,
|
|
27
|
+
): string | undefined {
|
|
28
|
+
if (type === 'custom') {
|
|
29
|
+
return url ?? undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!reference?.relationTo || !reference.value) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const routes = createRoutes(locale, homePage);
|
|
37
|
+
|
|
38
|
+
if (reference.relationTo === 'pages') {
|
|
39
|
+
const page = reference.value as Page | string;
|
|
40
|
+
|
|
41
|
+
// An unpopulated reference is just an id, so there is no path to route by.
|
|
42
|
+
// The homepage is still recoverable — it routes by id.
|
|
43
|
+
if (typeof page === 'string') {
|
|
44
|
+
return homePage && page === homePage.id ? routes.root() : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const rawPath = page.breadcrumbs?.at(-1)?.url ?? page.slug;
|
|
48
|
+
if (!rawPath) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const path = rawPath.replace(/^\//, '');
|
|
52
|
+
return routes.find('page', path);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (reference.relationTo === 'categories') {
|
|
56
|
+
return routes.find('category', (reference.value as Category).slug);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (reference.relationTo === 'posts') {
|
|
60
|
+
return routes.find('post', (reference.value as Post).slug);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (reference.relationTo === 'authors') {
|
|
64
|
+
return routes.find('author', (reference.value as Author).slug);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (reference.relationTo === 'tags') {
|
|
68
|
+
return routes.find('tag', (reference.value as Tag).slug);
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/cms.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import appearance from './collections/appearance';
|
|
2
|
+
import areas from './collections/areas';
|
|
3
|
+
import authors from './collections/authors';
|
|
4
|
+
import banners from './collections/banners';
|
|
5
|
+
import categories, { type CountedCategory } from './collections/categories';
|
|
6
|
+
import forms from './collections/forms';
|
|
7
|
+
import headerFooter from './collections/header-footer';
|
|
8
|
+
import imageLinks from './collections/image-links';
|
|
9
|
+
import media from './collections/media';
|
|
10
|
+
import options from './collections/options';
|
|
11
|
+
import pages from './collections/pages';
|
|
12
|
+
import posts from './collections/posts';
|
|
13
|
+
import project from './collections/project';
|
|
14
|
+
import questions from './collections/questions';
|
|
15
|
+
import seo from './collections/seo';
|
|
16
|
+
import tags from './collections/tags';
|
|
17
|
+
import teamMembers from './collections/team-members';
|
|
18
|
+
import type { Context } from './context';
|
|
19
|
+
import type { Locale } from './locales';
|
|
20
|
+
|
|
21
|
+
export type Cms = ReturnType<typeof buildCms>;
|
|
22
|
+
|
|
23
|
+
export function buildCms(ctx: Context, locale: Locale) {
|
|
24
|
+
const categoriesService = categories(ctx, locale);
|
|
25
|
+
const postsService = posts(ctx, locale);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
locale,
|
|
29
|
+
areas: areas(ctx, locale),
|
|
30
|
+
authors: authors(ctx, locale),
|
|
31
|
+
banners: banners(ctx, locale),
|
|
32
|
+
appearance: appearance(ctx),
|
|
33
|
+
imageLinks: imageLinks(ctx, locale),
|
|
34
|
+
categories: {
|
|
35
|
+
...categoriesService,
|
|
36
|
+
populated: async () => {
|
|
37
|
+
const cats = await categoriesService.all();
|
|
38
|
+
const allPosts = await postsService.all();
|
|
39
|
+
const ids = new Set([
|
|
40
|
+
...allPosts.map((p) => p.category?.id).filter(Boolean),
|
|
41
|
+
...allPosts.flatMap((p) =>
|
|
42
|
+
(p.secondaryCategories ?? []).map((c) => c.id),
|
|
43
|
+
),
|
|
44
|
+
]);
|
|
45
|
+
return cats.filter((c) => ids.has(c.id));
|
|
46
|
+
},
|
|
47
|
+
// Categories that hold posts, each with how many. `populated()`
|
|
48
|
+
// answers only whether that number is non-zero and has five callers
|
|
49
|
+
// depending on its shape, so this is a second reading of the same
|
|
50
|
+
// fold rather than a change to it. Both collections are already
|
|
51
|
+
// cached, so it costs no extra fetch.
|
|
52
|
+
withCounts: async (): Promise<CountedCategory[]> => {
|
|
53
|
+
const cats = await categoriesService.all();
|
|
54
|
+
const allPosts = await postsService.all();
|
|
55
|
+
|
|
56
|
+
const counts = new Map<string, number>();
|
|
57
|
+
for (const post of allPosts) {
|
|
58
|
+
const ids = new Set(
|
|
59
|
+
[
|
|
60
|
+
post.category?.id,
|
|
61
|
+
...(post.secondaryCategories ?? []).map((c) => c.id),
|
|
62
|
+
].filter((id): id is string => !!id),
|
|
63
|
+
);
|
|
64
|
+
for (const id of ids) {
|
|
65
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return cats
|
|
70
|
+
.map((c) => ({ ...c, postCount: counts.get(c.id) ?? 0 }))
|
|
71
|
+
.filter((c) => c.postCount > 0);
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
forms: forms(ctx, locale),
|
|
75
|
+
headerFooter: headerFooter(ctx, locale),
|
|
76
|
+
media: media(ctx, locale),
|
|
77
|
+
options: options(ctx, locale),
|
|
78
|
+
pages: pages(ctx, locale),
|
|
79
|
+
posts: postsService,
|
|
80
|
+
project: project(ctx),
|
|
81
|
+
questions: questions(ctx, locale),
|
|
82
|
+
seo: seo(ctx, locale),
|
|
83
|
+
tags: tags(ctx, locale),
|
|
84
|
+
teamMembers: teamMembers(ctx, locale),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Context } from '../context';
|
|
2
|
+
import { createGlobalLoader } from '../loader';
|
|
3
|
+
import type { Appearance } from '../payload-types';
|
|
4
|
+
|
|
5
|
+
export type WiredAppearance = Appearance & {
|
|
6
|
+
branding: NonNullable<Appearance['branding']> & {
|
|
7
|
+
brandColor: NonNullable<NonNullable<Appearance['branding']>['brandColor']>;
|
|
8
|
+
logo: NonNullable<NonNullable<Appearance['branding']>['logo']>;
|
|
9
|
+
};
|
|
10
|
+
backgroundColor: {
|
|
11
|
+
lightTheme: string;
|
|
12
|
+
darkTheme: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* What a blank Appearance field falls back to. Both are CSS values, and both
|
|
18
|
+
* are a site's own: the template states them in its theme file, so its
|
|
19
|
+
* defaults are variables that file defines.
|
|
20
|
+
*/
|
|
21
|
+
export interface AppearanceFallbacks {
|
|
22
|
+
brandColor: string;
|
|
23
|
+
backgroundColor: { lightTheme: string; darkTheme: string };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_APPEARANCE_FALLBACKS: AppearanceFallbacks = {
|
|
27
|
+
// The theme ramp's 500.
|
|
28
|
+
brandColor: 'var(--color-theme-500)',
|
|
29
|
+
backgroundColor: {
|
|
30
|
+
lightTheme: 'var(--site-background-light)',
|
|
31
|
+
darkTheme: 'var(--site-background-dark)',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function removeInvalid<T extends Record<string, any>>(
|
|
36
|
+
obj: T | null | undefined,
|
|
37
|
+
): Partial<T> {
|
|
38
|
+
if (obj === null || obj === undefined) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const cleaned: Partial<T> = {};
|
|
43
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
44
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
45
|
+
cleaned[key as keyof T] = value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return cleaned;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Appearance (theme/colors/fonts) is fetched once, at config time, and handed
|
|
53
|
+
// to `createClient`. It has no text content that needs translation, so every
|
|
54
|
+
// locale reads the same document.
|
|
55
|
+
//
|
|
56
|
+
// Header theming is deliberately absent: it is not an Appearance field, so it
|
|
57
|
+
// lives in the site.
|
|
58
|
+
export default function (ctx: Context) {
|
|
59
|
+
return createGlobalLoader<Appearance, WiredAppearance>(
|
|
60
|
+
async () => {
|
|
61
|
+
if (!ctx.options.appearance) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
'cms.appearance() needs the `appearance` option of createClient: the Appearance document fetched at config time.',
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return ctx.options.appearance;
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
transform: (appearance) => {
|
|
70
|
+
const fallbacks =
|
|
71
|
+
ctx.options.appearanceFallbacks ?? DEFAULT_APPEARANCE_FALLBACKS;
|
|
72
|
+
return {
|
|
73
|
+
...appearance,
|
|
74
|
+
branding: {
|
|
75
|
+
...appearance.branding,
|
|
76
|
+
brandColor: appearance.branding?.brandColor || fallbacks.brandColor,
|
|
77
|
+
logo: {
|
|
78
|
+
type: appearance.branding?.logo?.type,
|
|
79
|
+
dark: appearance.branding?.logo?.dark,
|
|
80
|
+
darkSvg: appearance.branding?.logo?.darkSvg,
|
|
81
|
+
light: appearance.branding?.logo?.light,
|
|
82
|
+
lightSvg: appearance.branding?.logo?.lightSvg,
|
|
83
|
+
},
|
|
84
|
+
icon: appearance.branding?.icon,
|
|
85
|
+
},
|
|
86
|
+
backgroundColor: {
|
|
87
|
+
...fallbacks.backgroundColor,
|
|
88
|
+
...removeInvalid(appearance.backgroundColor),
|
|
89
|
+
},
|
|
90
|
+
} as WiredAppearance;
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Context } from '../context';
|
|
2
|
+
import type { Area, Page } from '../payload-types';
|
|
3
|
+
import type { Locale } from '../locales';
|
|
4
|
+
import { createCollectionLoader } from '../loader';
|
|
5
|
+
|
|
6
|
+
export interface WiredArea extends Omit<Area, 'page'> {
|
|
7
|
+
page?: Page | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function (ctx: Context, locale: Locale) {
|
|
11
|
+
const areas = createCollectionLoader(
|
|
12
|
+
() =>
|
|
13
|
+
ctx.api.fetchCmsCollection<Area>('areas', {
|
|
14
|
+
sort: '_order',
|
|
15
|
+
locale,
|
|
16
|
+
}),
|
|
17
|
+
{
|
|
18
|
+
transform: (areas): WiredArea[] =>
|
|
19
|
+
areas.map((area) => ({
|
|
20
|
+
...area,
|
|
21
|
+
page: typeof area.page === 'object' ? area.page : null,
|
|
22
|
+
})),
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
all: areas,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Context } from '../context';
|
|
2
|
+
import type { Author } from '../payload-types';
|
|
3
|
+
import type { Locale } from '../locales';
|
|
4
|
+
import { createCollectionLoader } from '../loader';
|
|
5
|
+
|
|
6
|
+
export interface WiredAuthor extends Omit<Author, 'slug'> {
|
|
7
|
+
slug: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function (ctx: Context, locale: Locale) {
|
|
11
|
+
const authors = createCollectionLoader(
|
|
12
|
+
async () => ctx.api.fetchCmsCollection<Author>('authors', { locale }),
|
|
13
|
+
{
|
|
14
|
+
transform: (authors) => {
|
|
15
|
+
const validAuthors = authors.filter((author) => author.slug);
|
|
16
|
+
return validAuthors as WiredAuthor[];
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
all: authors,
|
|
23
|
+
findBySlug: async (slug: string) => {
|
|
24
|
+
return authors.find((author) => author.slug === slug);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Context } from '../context';
|
|
2
|
+
import type { Banner } from '../payload-types';
|
|
3
|
+
import type { Locale } from '../locales';
|
|
4
|
+
import { createCollectionLoader } from '../loader';
|
|
5
|
+
|
|
6
|
+
export default function (ctx: Context, locale: Locale) {
|
|
7
|
+
const banners = createCollectionLoader(async () =>
|
|
8
|
+
ctx.api.fetchCmsCollection<Banner>('banners', { locale }),
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
all: banners,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Context } from '../context';
|
|
2
|
+
import type { Category } from '../payload-types';
|
|
3
|
+
import type { Locale } from '../locales';
|
|
4
|
+
import { decodeHtml } from '../decode-html';
|
|
5
|
+
import type { Locales } from '../locales';
|
|
6
|
+
import { createCollectionLoader } from '../loader';
|
|
7
|
+
|
|
8
|
+
export type CategoryBreadcrumb = { label: string; url: string };
|
|
9
|
+
|
|
10
|
+
export interface WiredCategory extends Omit<Category, 'parent' | 'slug'> {
|
|
11
|
+
slug: string;
|
|
12
|
+
parent?: WiredCategory;
|
|
13
|
+
children: WiredCategory[];
|
|
14
|
+
path: string;
|
|
15
|
+
breadcrumbs: CategoryBreadcrumb[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A category plus how many posts are filed under it. The count follows the same
|
|
20
|
+
* rule as `cms.posts.findByCategory`, counting a post under its primary and its
|
|
21
|
+
* secondary categories alike, so the number a listing prints is the number of
|
|
22
|
+
* entries the category page then shows.
|
|
23
|
+
*/
|
|
24
|
+
export interface CountedCategory extends WiredCategory {
|
|
25
|
+
postCount: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getCategoryPath(category: WiredCategory): string {
|
|
29
|
+
if (!category.parent) {
|
|
30
|
+
return category.slug;
|
|
31
|
+
}
|
|
32
|
+
return `${getCategoryPath(category.parent)}/${category.slug}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getCategoryBreadcrumbs(
|
|
36
|
+
category: WiredCategory,
|
|
37
|
+
locale: Locale,
|
|
38
|
+
prefixPath: Locales['prefixPath'],
|
|
39
|
+
): CategoryBreadcrumb[] {
|
|
40
|
+
const ancestors = category.parent
|
|
41
|
+
? getCategoryBreadcrumbs(category.parent, locale, prefixPath)
|
|
42
|
+
: [];
|
|
43
|
+
const path = getCategoryPath(category);
|
|
44
|
+
return [
|
|
45
|
+
...ancestors,
|
|
46
|
+
{
|
|
47
|
+
label: category.title,
|
|
48
|
+
url: prefixPath(`/blog/category/${path}`, locale),
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default function (ctx: Context, locale: Locale) {
|
|
54
|
+
const categories = createCollectionLoader(
|
|
55
|
+
async () => ctx.api.fetchCmsCollection<Category>('categories', { locale }),
|
|
56
|
+
{
|
|
57
|
+
transform: (categories) => {
|
|
58
|
+
const catMap = new Map<string, WiredCategory>(
|
|
59
|
+
categories
|
|
60
|
+
.filter((category) => category.slug)
|
|
61
|
+
.map((category) => [
|
|
62
|
+
category.id,
|
|
63
|
+
{
|
|
64
|
+
...category,
|
|
65
|
+
title: decodeHtml(category.title),
|
|
66
|
+
children: [],
|
|
67
|
+
path: category.slug as string,
|
|
68
|
+
breadcrumbs: [],
|
|
69
|
+
} as WiredCategory,
|
|
70
|
+
]),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const wireRelationships = (category: WiredCategory) => {
|
|
74
|
+
if (typeof category.parent === 'string') {
|
|
75
|
+
const parent = catMap.get(category.parent);
|
|
76
|
+
if (parent) {
|
|
77
|
+
category.parent = parent;
|
|
78
|
+
parent.children.push(category);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
catMap.forEach((category) => wireRelationships(category));
|
|
84
|
+
|
|
85
|
+
catMap.forEach((category) => {
|
|
86
|
+
category.path = getCategoryPath(category);
|
|
87
|
+
category.breadcrumbs = getCategoryBreadcrumbs(
|
|
88
|
+
category,
|
|
89
|
+
locale,
|
|
90
|
+
ctx.locales.prefixPath,
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return Array.from(catMap.values());
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
all: categories,
|
|
101
|
+
wire: async (category: Category | string | null | undefined) => {
|
|
102
|
+
if (!category) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const id = typeof category === 'string' ? category : category.id;
|
|
107
|
+
|
|
108
|
+
return (await categories()).find((cat) => cat.id === id);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Context } from '../context';
|
|
2
|
+
import type { Form } from '../payload-types';
|
|
3
|
+
import type { Locale } from '../locales';
|
|
4
|
+
import { createCollectionLoader } from '../loader';
|
|
5
|
+
|
|
6
|
+
export default function (ctx: Context, locale: Locale) {
|
|
7
|
+
// Forms are fetched on their own instead of relying on the copy populated
|
|
8
|
+
// inside page/global blocks: those requests disable locale fallback, so an
|
|
9
|
+
// untranslated label would come back null and render an unusable field.
|
|
10
|
+
// A default-locale label beats a blank one.
|
|
11
|
+
const forms = createCollectionLoader(() =>
|
|
12
|
+
ctx.api.fetchCmsCollection<Form>('forms', { locale, fallback: 'default' }),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const findById = async (id: string): Promise<Form | null> =>
|
|
16
|
+
(await forms.find((form: Form) => form.id === id)) ?? null;
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
all: forms,
|
|
20
|
+
findById,
|
|
21
|
+
// Swaps a form reference coming from a block for its localized document
|
|
22
|
+
resolve: async (form: Form | string): Promise<Form | null> => {
|
|
23
|
+
if (typeof form === 'string') {
|
|
24
|
+
return await findById(form);
|
|
25
|
+
}
|
|
26
|
+
return (await findById(form.id)) ?? form;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Context } from '../context';
|
|
2
|
+
import type { HeaderFooter } from '../payload-types';
|
|
3
|
+
import type { Locale } from '../locales';
|
|
4
|
+
import { createGlobalLoader } from '../loader';
|
|
5
|
+
|
|
6
|
+
export type CmsHeader = NonNullable<NonNullable<HeaderFooter>['header']>;
|
|
7
|
+
export type CmsFooter = NonNullable<NonNullable<HeaderFooter>['footer']>;
|
|
8
|
+
|
|
9
|
+
export type CmsMainMenu = NonNullable<CmsHeader['mainMenu']>;
|
|
10
|
+
export type CmsMenuItem = NonNullable<CmsMainMenu[0]['menuItem']>;
|
|
11
|
+
export type CmsLink = NonNullable<CmsMenuItem['mainLink']>;
|
|
12
|
+
export type CmsSocialLinks = NonNullable<CmsHeader['socialLinks']>;
|
|
13
|
+
export type CmsCallToActionLinks = NonNullable<CmsHeader['callToActionLinks']>;
|
|
14
|
+
|
|
15
|
+
export default function (ctx: Context, locale: Locale) {
|
|
16
|
+
return createGlobalLoader(async () =>
|
|
17
|
+
ctx.api.fetchCmsGlobalCollection<HeaderFooter>('header-footer', { locale }),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Context } from '../context';
|
|
2
|
+
import type { ImageLink } from '../payload-types';
|
|
3
|
+
import type { Locale } from '../locales';
|
|
4
|
+
import { createCollectionLoader } from '../loader';
|
|
5
|
+
|
|
6
|
+
export default function (ctx: Context, locale: Locale) {
|
|
7
|
+
const imageLinks = createCollectionLoader(async () =>
|
|
8
|
+
ctx.api.fetchCmsCollection<ImageLink>('image-links', { locale }),
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
all: imageLinks,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Context } from '../context';
|
|
2
|
+
import type { Media } from '../payload-types';
|
|
3
|
+
import type { Locale } from '../locales';
|
|
4
|
+
import { createCollectionLoader } from '../loader';
|
|
5
|
+
|
|
6
|
+
export default function (ctx: Context, locale: Locale) {
|
|
7
|
+
// depth: 0 — an upload's only relationship is `project`, which the CMS's
|
|
8
|
+
// multi-tenant plugin pins at maxDepth 0, so the response is identical to
|
|
9
|
+
// the default depth=2 one. Populating it costs roughly 3x per page, and
|
|
10
|
+
// this is the biggest collection a build reads.
|
|
11
|
+
const media = createCollectionLoader(async () =>
|
|
12
|
+
ctx.api.fetchCmsCollection<Media>('media', { locale, depth: 0 }),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
all: media,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Context } from '../context';
|
|
2
|
+
import type { Options } from '../payload-types';
|
|
3
|
+
import type { Locale } from '../locales';
|
|
4
|
+
import { createGlobalLoader } from '../loader';
|
|
5
|
+
|
|
6
|
+
export default function (ctx: Context, locale: Locale) {
|
|
7
|
+
return createGlobalLoader(async () =>
|
|
8
|
+
ctx.api.fetchCmsGlobalCollection<Options>('options', { locale }),
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Context } from '../context';
|
|
2
|
+
import type { Page } from '../payload-types';
|
|
3
|
+
import type { Locale } from '../locales';
|
|
4
|
+
import { createCollectionLoader } from '../loader';
|
|
5
|
+
|
|
6
|
+
export interface WiredPage extends Omit<Page, 'parent'> {
|
|
7
|
+
parent?: WiredPage;
|
|
8
|
+
children: WiredPage[];
|
|
9
|
+
path: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function (ctx: Context, locale: Locale) {
|
|
13
|
+
const pages = createCollectionLoader(
|
|
14
|
+
async () =>
|
|
15
|
+
ctx.api.fetchCmsCollection<Page>('pages', {
|
|
16
|
+
status: 'published',
|
|
17
|
+
locale,
|
|
18
|
+
}),
|
|
19
|
+
{
|
|
20
|
+
transform: (rawPages): WiredPage[] => {
|
|
21
|
+
// Pass 1: build map with path derived from Payload's breadcrumbs plugin
|
|
22
|
+
const pageMap = new Map<string, WiredPage>(
|
|
23
|
+
rawPages
|
|
24
|
+
.filter((page) => page.slug)
|
|
25
|
+
.map((page) => {
|
|
26
|
+
const path = (page.breadcrumbs?.at(-1)?.url ?? page.slug).replace(
|
|
27
|
+
/^\//,
|
|
28
|
+
'',
|
|
29
|
+
);
|
|
30
|
+
return [page.id, { ...page, children: [], path } as WiredPage];
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Pass 2: wire parent references and populate children[]
|
|
35
|
+
// parent may be a string ID (depth=0) or a populated object (depth>0)
|
|
36
|
+
pageMap.forEach((page) => {
|
|
37
|
+
const rawParent = page.parent as
|
|
38
|
+
| WiredPage
|
|
39
|
+
| string
|
|
40
|
+
| null
|
|
41
|
+
| undefined;
|
|
42
|
+
const parentId =
|
|
43
|
+
typeof rawParent === 'string' ? rawParent : rawParent?.id;
|
|
44
|
+
if (parentId) {
|
|
45
|
+
const parent = pageMap.get(parentId);
|
|
46
|
+
if (parent) {
|
|
47
|
+
page.parent = parent;
|
|
48
|
+
parent.children.push(page);
|
|
49
|
+
} else {
|
|
50
|
+
page.parent = undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Pass 3: sort each parent's children by order (nulls sort last)
|
|
56
|
+
pageMap.forEach((page) => {
|
|
57
|
+
page.children.sort((a, b) => {
|
|
58
|
+
if (a.order == null && b.order == null) {
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
if (a.order == null) {
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
if (b.order == null) {
|
|
65
|
+
return -1;
|
|
66
|
+
}
|
|
67
|
+
return a.order - b.order;
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return Array.from(pageMap.values());
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
all: pages,
|
|
78
|
+
findByPath: async (path: string) =>
|
|
79
|
+
(await pages()).find((p) => p.path === path),
|
|
80
|
+
findById: async (id: string) =>
|
|
81
|
+
(await pages()).find((p) => p.id === id) ?? null,
|
|
82
|
+
};
|
|
83
|
+
}
|