@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,9 @@
|
|
|
1
|
+
export function getRandomPlaceholderUrl(width = 600, height = 400) {
|
|
2
|
+
const pastelColors = ['ebfaf5', 'c3c8cd', 'faf0e6', 'f5f5eb', 'f0ebfa'];
|
|
3
|
+
|
|
4
|
+
const randomIndex = Math.floor(Math.random() * pastelColors.length);
|
|
5
|
+
|
|
6
|
+
const randomColor = pastelColors[randomIndex];
|
|
7
|
+
|
|
8
|
+
return `https://placehold.co/${width}x${height}/${randomColor}/${randomColor}`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { WiredCategory } from './collections/categories';
|
|
2
|
+
import type {
|
|
3
|
+
CmsLink,
|
|
4
|
+
CmsMainMenu,
|
|
5
|
+
CmsMenuItem,
|
|
6
|
+
} from './collections/header-footer';
|
|
7
|
+
import type { WiredPage } from './collections/pages';
|
|
8
|
+
import type { WiredTag } from './collections/tags';
|
|
9
|
+
import type { WiredTeamMember } from './collections/team-members';
|
|
10
|
+
import type { Page } from './payload-types';
|
|
11
|
+
import type { Routes } from './routes';
|
|
12
|
+
|
|
13
|
+
type SubmenuItem = NonNullable<CmsMenuItem['submenuItems']>[0];
|
|
14
|
+
|
|
15
|
+
function customLink(label: string, url: string): CmsLink {
|
|
16
|
+
return { type: 'custom', label, url };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sortByLabel(items: SubmenuItem[]): SubmenuItem[] {
|
|
20
|
+
return [...items].sort((a, b) => {
|
|
21
|
+
const labelA = a.link?.label ?? '';
|
|
22
|
+
const labelB = b.link?.label ?? '';
|
|
23
|
+
return labelA.localeCompare(labelB);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function pageToSubmenuItem(routes: Routes, page: WiredPage): SubmenuItem {
|
|
28
|
+
return {
|
|
29
|
+
type: 'link',
|
|
30
|
+
link: customLink(page.title, routes.page(page)),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function categoryToSubmenuItem(
|
|
35
|
+
routes: Routes,
|
|
36
|
+
category: WiredCategory,
|
|
37
|
+
): SubmenuItem {
|
|
38
|
+
return {
|
|
39
|
+
type: 'link',
|
|
40
|
+
link: customLink(category.title, routes.blog('category', category.path)),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function tagToSubmenuItem(routes: Routes, tag: WiredTag): SubmenuItem {
|
|
45
|
+
return {
|
|
46
|
+
type: 'link',
|
|
47
|
+
link: customLink(tag.title, routes.blog('tag', tag.slug)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function teamMemberToSubmenuItem(
|
|
52
|
+
routes: Routes,
|
|
53
|
+
member: WiredTeamMember,
|
|
54
|
+
): SubmenuItem {
|
|
55
|
+
return {
|
|
56
|
+
type: 'link',
|
|
57
|
+
link: customLink(
|
|
58
|
+
`${member.firstName} ${member.lastName}`.trim(),
|
|
59
|
+
routes.team('member', member.slug),
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type MenuCollectionData = {
|
|
65
|
+
pages: WiredPage[];
|
|
66
|
+
categories: WiredCategory[];
|
|
67
|
+
tags: WiredTag[];
|
|
68
|
+
teamMembers: WiredTeamMember[];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type ResolvedNested = {
|
|
72
|
+
submenuItems: SubmenuItem[] | null | undefined;
|
|
73
|
+
mainLink?: CmsLink;
|
|
74
|
+
hasMainLink?: boolean;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** The words the menu adds of its own. Site copy, so the site supplies it. */
|
|
78
|
+
export type MenuLabels = {
|
|
79
|
+
/** Heads the team-members submenu: the site's `team.title`. */
|
|
80
|
+
team: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function resolveNested(
|
|
84
|
+
menuItem: CmsMenuItem,
|
|
85
|
+
data: MenuCollectionData,
|
|
86
|
+
routes: Routes,
|
|
87
|
+
labels: MenuLabels,
|
|
88
|
+
): ResolvedNested {
|
|
89
|
+
const { nestedType } = menuItem;
|
|
90
|
+
|
|
91
|
+
if (nestedType === 'nested-pages') {
|
|
92
|
+
const parentPage = menuItem.parentPage;
|
|
93
|
+
if (!parentPage || typeof parentPage === 'string') {
|
|
94
|
+
return { submenuItems: menuItem.submenuItems };
|
|
95
|
+
}
|
|
96
|
+
const parentId = (parentPage as Page).id;
|
|
97
|
+
const wiredParent = data.pages.find((p) => p.id === parentId);
|
|
98
|
+
if (!wiredParent) {
|
|
99
|
+
return { submenuItems: menuItem.submenuItems };
|
|
100
|
+
}
|
|
101
|
+
const mainLink = customLink(wiredParent.title, routes.page(wiredParent));
|
|
102
|
+
return {
|
|
103
|
+
submenuItems: wiredParent.children.map((child) =>
|
|
104
|
+
pageToSubmenuItem(routes, child),
|
|
105
|
+
),
|
|
106
|
+
mainLink,
|
|
107
|
+
hasMainLink: true,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (nestedType === 'collection') {
|
|
112
|
+
const slug = menuItem.collectionSlug;
|
|
113
|
+
|
|
114
|
+
if (slug === 'categories') {
|
|
115
|
+
return {
|
|
116
|
+
submenuItems: sortByLabel(
|
|
117
|
+
data.categories.map((c) => categoryToSubmenuItem(routes, c)),
|
|
118
|
+
),
|
|
119
|
+
hasMainLink: false,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (slug === 'tags') {
|
|
124
|
+
return {
|
|
125
|
+
submenuItems: sortByLabel(
|
|
126
|
+
data.tags.map((t) => tagToSubmenuItem(routes, t)),
|
|
127
|
+
),
|
|
128
|
+
hasMainLink: false,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (slug === 'team-members') {
|
|
133
|
+
return {
|
|
134
|
+
submenuItems: sortByLabel(
|
|
135
|
+
data.teamMembers.map((m) => teamMemberToSubmenuItem(routes, m)),
|
|
136
|
+
),
|
|
137
|
+
mainLink: customLink(labels.team, routes.team()),
|
|
138
|
+
hasMainLink: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { submenuItems: menuItem.submenuItems };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { submenuItems: menuItem.submenuItems };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function resolveMenuItems(
|
|
149
|
+
mainMenu: CmsMainMenu,
|
|
150
|
+
data: MenuCollectionData,
|
|
151
|
+
// Menu entries become `type: 'custom'` links holding a finished URL, so
|
|
152
|
+
// `cmsToHref` never sees them — build `routes` with the homepage, so it
|
|
153
|
+
// collapses to `/` here.
|
|
154
|
+
routes: Routes,
|
|
155
|
+
labels: MenuLabels,
|
|
156
|
+
): CmsMainMenu {
|
|
157
|
+
return mainMenu.map((entry) => {
|
|
158
|
+
const menuItem = entry.menuItem;
|
|
159
|
+
if (!menuItem || menuItem.type !== 'nested') {
|
|
160
|
+
return entry;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { submenuItems, mainLink, hasMainLink } = resolveNested(
|
|
164
|
+
menuItem,
|
|
165
|
+
data,
|
|
166
|
+
routes,
|
|
167
|
+
labels,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const resolvedMainLink = mainLink ?? menuItem.mainLink;
|
|
171
|
+
const resolvedHasMainLink =
|
|
172
|
+
hasMainLink !== undefined ? hasMainLink : menuItem.hasMainLink;
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
...entry,
|
|
176
|
+
menuItem: {
|
|
177
|
+
...menuItem,
|
|
178
|
+
submenuItems,
|
|
179
|
+
mainLink: resolvedMainLink,
|
|
180
|
+
hasMainLink: resolvedHasMainLink,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
}
|
package/src/routes.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { Locale, Locales } from './locales';
|
|
2
|
+
import type { Page } from './payload-types';
|
|
3
|
+
|
|
4
|
+
function trimSlashes(str: string) {
|
|
5
|
+
return str.replace(/^\/+|\/+$/g, '');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type BasePath = `/${string}/` | '/';
|
|
9
|
+
function page(base: BasePath, dynamic: string) {
|
|
10
|
+
// If dynamic is null we must have bad data, we only restrict things with types
|
|
11
|
+
if (!dynamic) {
|
|
12
|
+
return '/404'; // give a 500 error or somehting here in future
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return base + trimSlashes(dynamic);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The page configured as the site homepage. It is served at `/` (or `/en/`) and
|
|
20
|
+
* is deliberately not published at its own slug, so every page URL this module
|
|
21
|
+
* builds for it has to collapse to the site root. Read it with `getHomePageRef`
|
|
22
|
+
* on the server, or take it as a prop inside a hydrated island.
|
|
23
|
+
*/
|
|
24
|
+
export type HomePageRef = { id: string; path: string } | null;
|
|
25
|
+
|
|
26
|
+
type Type =
|
|
27
|
+
| 'blog'
|
|
28
|
+
| 'page'
|
|
29
|
+
| 'category'
|
|
30
|
+
| 'author'
|
|
31
|
+
| 'post'
|
|
32
|
+
| 'tag'
|
|
33
|
+
| 'team-member';
|
|
34
|
+
|
|
35
|
+
export type CreateRoutes = (
|
|
36
|
+
locale: Locale,
|
|
37
|
+
homePage?: HomePageRef,
|
|
38
|
+
) => ReturnType<typeof buildRoutes>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The site's route builder, bound to its locale configuration. Pure, so it is
|
|
42
|
+
* as usable inside a hydrated island as on the server.
|
|
43
|
+
*/
|
|
44
|
+
export function defineRoutes({ prefixPath }: Pick<Locales, 'prefixPath'>) {
|
|
45
|
+
return ((locale: Locale, homePage: HomePageRef = null) =>
|
|
46
|
+
buildRoutes(prefixPath, locale, homePage)) satisfies CreateRoutes;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildRoutes(
|
|
50
|
+
prefixPath: Locales['prefixPath'],
|
|
51
|
+
locale: Locale,
|
|
52
|
+
homePage: HomePageRef,
|
|
53
|
+
) {
|
|
54
|
+
const p = (path: string) => {
|
|
55
|
+
const prefixed = prefixPath(path, locale);
|
|
56
|
+
return prefixed.endsWith('/') ? prefixed : `${prefixed}/`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const rootRoutes = {
|
|
60
|
+
index: () => p('/'),
|
|
61
|
+
search: () => p(locale === 'en' ? '/search' : '/buscar'),
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
const teamBase = locale === 'en' ? '/team' : '/equipo';
|
|
65
|
+
const teamRoutes = {
|
|
66
|
+
index: () => p(teamBase),
|
|
67
|
+
member: (slug: string) => p(page(`${teamBase}/`, slug)),
|
|
68
|
+
// File endpoint — no trailing slash before the extension.
|
|
69
|
+
vcard: (slug: string) =>
|
|
70
|
+
`${prefixPath(page(`${teamBase}/`, slug), locale)}.vcf`,
|
|
71
|
+
} as const;
|
|
72
|
+
|
|
73
|
+
const blogRoutes = {
|
|
74
|
+
index: () => p('/blog'),
|
|
75
|
+
more: () => p('/blog/2'),
|
|
76
|
+
page: (n: number) => p(`/blog/${n}`) as string,
|
|
77
|
+
category: (path: string) => p(page('/blog/category/', path)),
|
|
78
|
+
author: (slug: string) => p(page('/blog/author/', slug)),
|
|
79
|
+
post: (slug: string) => p(page('/blog/', slug)),
|
|
80
|
+
tag: (slug: string) => p(page('/blog/tag/', slug)),
|
|
81
|
+
} as const;
|
|
82
|
+
|
|
83
|
+
type RootRoutes = typeof rootRoutes;
|
|
84
|
+
const root = <T extends keyof RootRoutes = 'index'>(
|
|
85
|
+
name: T = 'index' as T,
|
|
86
|
+
...params: Parameters<RootRoutes[T]>
|
|
87
|
+
): ReturnType<RootRoutes[T]> => (rootRoutes[name] as any)(...params);
|
|
88
|
+
|
|
89
|
+
type BlogRoutes = typeof blogRoutes;
|
|
90
|
+
const blog = <T extends keyof BlogRoutes = 'index'>(
|
|
91
|
+
name: T = 'index' as T,
|
|
92
|
+
...params: Parameters<BlogRoutes[T]>
|
|
93
|
+
): ReturnType<BlogRoutes[T]> => (blogRoutes[name] as any)(...params);
|
|
94
|
+
|
|
95
|
+
type TeamRoutes = typeof teamRoutes;
|
|
96
|
+
const team = <T extends keyof TeamRoutes = 'index'>(
|
|
97
|
+
name: T = 'index' as T,
|
|
98
|
+
...params: Parameters<TeamRoutes[T]>
|
|
99
|
+
): ReturnType<TeamRoutes[T]> => (teamRoutes[name] as any)(...params);
|
|
100
|
+
|
|
101
|
+
// Callers hand us either a full document (match by id) or a bare path (match
|
|
102
|
+
// by path) — the homepage has to collapse to the root either way.
|
|
103
|
+
const isHomePage = (slugOrPage: string | Page) => {
|
|
104
|
+
if (!homePage) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return typeof slugOrPage === 'string'
|
|
109
|
+
? trimSlashes(slugOrPage) === homePage.path
|
|
110
|
+
: slugOrPage.id === homePage.id;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const pageRoute = (slugOrPage: string | Page) => {
|
|
114
|
+
if (isHomePage(slugOrPage)) {
|
|
115
|
+
return rootRoutes.index();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (typeof slugOrPage !== 'string') {
|
|
119
|
+
const url = slugOrPage.breadcrumbs?.at(-1)?.url;
|
|
120
|
+
if (url) {
|
|
121
|
+
return p(url);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return p(page('/', slugOrPage.slug));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return p(page('/', slugOrPage));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
locale,
|
|
132
|
+
root,
|
|
133
|
+
blog,
|
|
134
|
+
team,
|
|
135
|
+
page: pageRoute,
|
|
136
|
+
search: rootRoutes.search,
|
|
137
|
+
find(routeType: Type | string, slugOrPath: string | undefined) {
|
|
138
|
+
const type = routeType as Type;
|
|
139
|
+
|
|
140
|
+
if (type === 'blog') {
|
|
141
|
+
return blog();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!slugOrPath) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (type === 'page') {
|
|
149
|
+
return pageRoute(slugOrPath);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (type === 'team-member') {
|
|
153
|
+
return team('member', slugOrPath);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (['category', 'author', 'post', 'tag'].includes(type)) {
|
|
157
|
+
return blog(type, slugOrPath);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
fromCMSCollection(
|
|
161
|
+
collectionSlug: string,
|
|
162
|
+
doc: { slug: string } & Partial<Page>,
|
|
163
|
+
) {
|
|
164
|
+
switch (collectionSlug) {
|
|
165
|
+
case 'posts':
|
|
166
|
+
return blog('post', doc.slug);
|
|
167
|
+
case 'pages':
|
|
168
|
+
return pageRoute(doc as Page);
|
|
169
|
+
case 'categories':
|
|
170
|
+
return blog('category', doc.slug);
|
|
171
|
+
case 'authors':
|
|
172
|
+
return blog('author', doc.slug);
|
|
173
|
+
case 'tags':
|
|
174
|
+
return blog('tag', doc.slug);
|
|
175
|
+
case 'team-members':
|
|
176
|
+
return team('member', doc.slug);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return undefined;
|
|
180
|
+
},
|
|
181
|
+
} as const;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type Routes = ReturnType<typeof buildRoutes>;
|