@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.
Files changed (73) hide show
  1. package/README.md +66 -0
  2. package/bin/dissect/cli.mjs +138 -0
  3. package/bin/dissect/dissect.mjs +290 -0
  4. package/bin/profile/build.mjs +106 -0
  5. package/bin/profile/fetch-log.mjs +298 -0
  6. package/bin/profile/format.mjs +90 -0
  7. package/bin/profile/interference-summary.mjs +558 -0
  8. package/bin/profile/interference.mjs +604 -0
  9. package/bin/profile/measure.mjs +137 -0
  10. package/bin/profile/report.mjs +90 -0
  11. package/bin/profile/site-env.mjs +16 -0
  12. package/bin/profile/summarize.mjs +429 -0
  13. package/dist/browser.d.ts +145 -0
  14. package/dist/browser.js +11 -0
  15. package/dist/browser.js.map +1 -0
  16. package/dist/chunk-6V54ITTK.js +197 -0
  17. package/dist/chunk-6V54ITTK.js.map +1 -0
  18. package/dist/chunk-MNZ7DIGC.js +51 -0
  19. package/dist/chunk-MNZ7DIGC.js.map +1 -0
  20. package/dist/form-proxy/upload-policy.d.ts +40 -0
  21. package/dist/form-proxy/upload-policy.js +17 -0
  22. package/dist/form-proxy/upload-policy.js.map +1 -0
  23. package/dist/index.d.ts +570 -0
  24. package/dist/index.js +1636 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/payload-types.d.ts +8985 -0
  27. package/dist/payload-types.js +1 -0
  28. package/dist/payload-types.js.map +1 -0
  29. package/package.json +74 -0
  30. package/src/api.ts +387 -0
  31. package/src/blog-listing.ts +75 -0
  32. package/src/browser.ts +24 -0
  33. package/src/client.ts +144 -0
  34. package/src/cms-to-href.ts +70 -0
  35. package/src/cms.ts +86 -0
  36. package/src/collections/appearance.ts +94 -0
  37. package/src/collections/areas.ts +29 -0
  38. package/src/collections/authors.ts +27 -0
  39. package/src/collections/banners.ts +14 -0
  40. package/src/collections/categories.ts +111 -0
  41. package/src/collections/forms.ts +29 -0
  42. package/src/collections/header-footer.ts +19 -0
  43. package/src/collections/image-links.ts +14 -0
  44. package/src/collections/media.ts +18 -0
  45. package/src/collections/options.ts +10 -0
  46. package/src/collections/pages.ts +83 -0
  47. package/src/collections/posts.ts +249 -0
  48. package/src/collections/project.ts +16 -0
  49. package/src/collections/questions.ts +35 -0
  50. package/src/collections/seo.ts +10 -0
  51. package/src/collections/tags.ts +25 -0
  52. package/src/collections/team-members.ts +79 -0
  53. package/src/config-time.ts +98 -0
  54. package/src/context.ts +12 -0
  55. package/src/decode-html.ts +8 -0
  56. package/src/form-proxy/cms-client.ts +95 -0
  57. package/src/form-proxy/cms-errors.ts +73 -0
  58. package/src/form-proxy/cms-write.ts +44 -0
  59. package/src/form-proxy/http.ts +96 -0
  60. package/src/form-proxy/index.ts +73 -0
  61. package/src/form-proxy/rate-limit.ts +46 -0
  62. package/src/form-proxy/submissions.ts +88 -0
  63. package/src/form-proxy/types.ts +23 -0
  64. package/src/form-proxy/upload-policy.ts +92 -0
  65. package/src/form-proxy/uploads.ts +81 -0
  66. package/src/home-page.ts +83 -0
  67. package/src/index.ts +68 -0
  68. package/src/loader.ts +83 -0
  69. package/src/locales.ts +80 -0
  70. package/src/payload-types.ts +10854 -0
  71. package/src/placeholder.ts +9 -0
  72. package/src/resolve-menu-items.ts +184 -0
  73. 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>;