@407dev/cms-astro 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 +69 -0
- package/dist/chunk-2TKIQIOE.js +118 -0
- package/dist/chunk-2TKIQIOE.js.map +1 -0
- package/dist/chunk-53OQ5RXJ.js +912 -0
- package/dist/chunk-53OQ5RXJ.js.map +1 -0
- package/dist/chunk-56OMNQLH.js +1013 -0
- package/dist/chunk-56OMNQLH.js.map +1 -0
- package/dist/chunk-5AOOMQ7K.js +1031 -0
- package/dist/chunk-5AOOMQ7K.js.map +1 -0
- package/dist/chunk-5XRUCFDC.js +743 -0
- package/dist/chunk-5XRUCFDC.js.map +1 -0
- package/dist/chunk-62ZHCOLZ.js +25 -0
- package/dist/chunk-62ZHCOLZ.js.map +1 -0
- package/dist/chunk-6AZLRSYA.js +126 -0
- package/dist/chunk-6AZLRSYA.js.map +1 -0
- package/dist/chunk-6L3ZTCFB.js +14 -0
- package/dist/chunk-6L3ZTCFB.js.map +1 -0
- package/dist/chunk-6O7ZNNDT.js +697 -0
- package/dist/chunk-6O7ZNNDT.js.map +1 -0
- package/dist/chunk-6PQIXTMK.js +697 -0
- package/dist/chunk-6PQIXTMK.js.map +1 -0
- package/dist/chunk-7AW6M7QW.js +1013 -0
- package/dist/chunk-7AW6M7QW.js.map +1 -0
- package/dist/chunk-7E3XJLKX.js +73 -0
- package/dist/chunk-7E3XJLKX.js.map +1 -0
- package/dist/chunk-7LM54OTA.js +1031 -0
- package/dist/chunk-7LM54OTA.js.map +1 -0
- package/dist/chunk-7LMUFCWX.js +126 -0
- package/dist/chunk-7LMUFCWX.js.map +1 -0
- package/dist/chunk-A3OGL4OX.js +706 -0
- package/dist/chunk-A3OGL4OX.js.map +1 -0
- package/dist/chunk-AO4UAWOV.js +1030 -0
- package/dist/chunk-AO4UAWOV.js.map +1 -0
- package/dist/chunk-B4YYLS7Y.js +14 -0
- package/dist/chunk-B4YYLS7Y.js.map +1 -0
- package/dist/chunk-BDFVGXM2.js +697 -0
- package/dist/chunk-BDFVGXM2.js.map +1 -0
- package/dist/chunk-CXM7NZVX.js +1031 -0
- package/dist/chunk-CXM7NZVX.js.map +1 -0
- package/dist/chunk-DMLOBKYE.js +96 -0
- package/dist/chunk-DMLOBKYE.js.map +1 -0
- package/dist/chunk-DXJSYGS6.js +743 -0
- package/dist/chunk-DXJSYGS6.js.map +1 -0
- package/dist/chunk-EFUSAQZM.js +72 -0
- package/dist/chunk-EFUSAQZM.js.map +1 -0
- package/dist/chunk-F7KDGPBN.js +1013 -0
- package/dist/chunk-F7KDGPBN.js.map +1 -0
- package/dist/chunk-FBSQT45X.js +126 -0
- package/dist/chunk-FBSQT45X.js.map +1 -0
- package/dist/chunk-FEPDOMVH.js +697 -0
- package/dist/chunk-FEPDOMVH.js.map +1 -0
- package/dist/chunk-FQDOL2QW.js +25 -0
- package/dist/chunk-FQDOL2QW.js.map +1 -0
- package/dist/chunk-GB3LGQJM.js +109 -0
- package/dist/chunk-GB3LGQJM.js.map +1 -0
- package/dist/chunk-HCNP6R3P.js +736 -0
- package/dist/chunk-HCNP6R3P.js.map +1 -0
- package/dist/chunk-J6WEJDRI.js +743 -0
- package/dist/chunk-J6WEJDRI.js.map +1 -0
- package/dist/chunk-JHEAPNJ2.js +117 -0
- package/dist/chunk-JHEAPNJ2.js.map +1 -0
- package/dist/chunk-JOJDJ6GW.js +25 -0
- package/dist/chunk-JOJDJ6GW.js.map +1 -0
- package/dist/chunk-KTDV6WYB.js +202 -0
- package/dist/chunk-KTDV6WYB.js.map +1 -0
- package/dist/chunk-KZWJBKPC.js +734 -0
- package/dist/chunk-KZWJBKPC.js.map +1 -0
- package/dist/chunk-LEAQBGL6.js +109 -0
- package/dist/chunk-LEAQBGL6.js.map +1 -0
- package/dist/chunk-LGZJL2Q7.js +697 -0
- package/dist/chunk-LGZJL2Q7.js.map +1 -0
- package/dist/chunk-LQPBU46R.js +109 -0
- package/dist/chunk-LQPBU46R.js.map +1 -0
- package/dist/chunk-M55NNUTY.js +1013 -0
- package/dist/chunk-M55NNUTY.js.map +1 -0
- package/dist/chunk-M6NCYCC3.js +1013 -0
- package/dist/chunk-M6NCYCC3.js.map +1 -0
- package/dist/chunk-MGN7OZAU.js +109 -0
- package/dist/chunk-MGN7OZAU.js.map +1 -0
- package/dist/chunk-MGYZOX4K.js +1013 -0
- package/dist/chunk-MGYZOX4K.js.map +1 -0
- package/dist/chunk-MY4IEKJS.js +1031 -0
- package/dist/chunk-MY4IEKJS.js.map +1 -0
- package/dist/chunk-NCQRGAB4.js +705 -0
- package/dist/chunk-NCQRGAB4.js.map +1 -0
- package/dist/chunk-NI4NHXI4.js +697 -0
- package/dist/chunk-NI4NHXI4.js.map +1 -0
- package/dist/chunk-NPI5N7AU.js +14 -0
- package/dist/chunk-NPI5N7AU.js.map +1 -0
- package/dist/chunk-QN4CPMJF.js +126 -0
- package/dist/chunk-QN4CPMJF.js.map +1 -0
- package/dist/chunk-QYUVAKMC.js +1013 -0
- package/dist/chunk-QYUVAKMC.js.map +1 -0
- package/dist/chunk-RAAT6PDT.js +25 -0
- package/dist/chunk-RAAT6PDT.js.map +1 -0
- package/dist/chunk-RDPO5VPS.js +1013 -0
- package/dist/chunk-RDPO5VPS.js.map +1 -0
- package/dist/chunk-RQHIW2PF.js +25 -0
- package/dist/chunk-RQHIW2PF.js.map +1 -0
- package/dist/chunk-RREVHA52.js +722 -0
- package/dist/chunk-RREVHA52.js.map +1 -0
- package/dist/chunk-S7PT4PZN.js +25 -0
- package/dist/chunk-S7PT4PZN.js.map +1 -0
- package/dist/chunk-SUXV5M32.js +109 -0
- package/dist/chunk-SUXV5M32.js.map +1 -0
- package/dist/chunk-SZEORUY6.js +696 -0
- package/dist/chunk-SZEORUY6.js.map +1 -0
- package/dist/chunk-T7NGGWGY.js +25 -0
- package/dist/chunk-T7NGGWGY.js.map +1 -0
- package/dist/chunk-UEVWP5VE.js +696 -0
- package/dist/chunk-UEVWP5VE.js.map +1 -0
- package/dist/chunk-UJNKNP7U.js +748 -0
- package/dist/chunk-UJNKNP7U.js.map +1 -0
- package/dist/chunk-V4XVFWFI.js +1013 -0
- package/dist/chunk-V4XVFWFI.js.map +1 -0
- package/dist/chunk-VJU2CIKZ.js +729 -0
- package/dist/chunk-VJU2CIKZ.js.map +1 -0
- package/dist/chunk-VRQUP5L3.js +202 -0
- package/dist/chunk-VRQUP5L3.js.map +1 -0
- package/dist/chunk-WBJ5E7CL.js +742 -0
- package/dist/chunk-WBJ5E7CL.js.map +1 -0
- package/dist/chunk-XJPVYMZM.js +24 -0
- package/dist/chunk-XJPVYMZM.js.map +1 -0
- package/dist/chunk-Y3Q65C7R.js +14 -0
- package/dist/chunk-Y3Q65C7R.js.map +1 -0
- package/dist/chunk-YHEGFPTR.js +1030 -0
- package/dist/chunk-YHEGFPTR.js.map +1 -0
- package/dist/chunk-ZDLBN46C.js +697 -0
- package/dist/chunk-ZDLBN46C.js.map +1 -0
- package/dist/dev/index.d.ts +122 -0
- package/dist/dev/index.js +662 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/fields/collections.d.ts +18 -0
- package/dist/fields/collections.js +37 -0
- package/dist/fields/collections.js.map +1 -0
- package/dist/index.d.ts +235 -0
- package/dist/index.js +956 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/index.d.ts +8 -0
- package/dist/middleware/index.js +13 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/preview-BEkLMD6N.d.ts +23 -0
- package/dist/preview-D9yewK8x.d.ts +24 -0
- package/dist/preview-DJEA4fj7.d.ts +23 -0
- package/dist/preview-DeD1Jn7U.d.ts +23 -0
- package/dist/toolbar/app.d.ts +28 -0
- package/dist/toolbar/app.js +1176 -0
- package/dist/toolbar/app.js.map +1 -0
- package/dist/types-15jfCQKb.d.ts +141 -0
- package/dist/types-BREdL9Gx.d.ts +140 -0
- package/dist/types-C6sUZNa2.d.ts +104 -0
- package/dist/types-CEYS5Fzc.d.ts +112 -0
- package/dist/types-DpeHpobR.d.ts +104 -0
- package/dist/types-NaKJe6Pg.d.ts +140 -0
- package/package.json +90 -0
- package/src/CmsHead.astro +25 -0
- package/src/CmsImage.astro +62 -0
- package/src/CmsVideo.astro +185 -0
- package/src/RichText.astro +32 -0
- package/src/client.test.ts +227 -0
- package/src/client.ts +164 -0
- package/src/dev/config.test.ts +258 -0
- package/src/dev/config.ts +319 -0
- package/src/dev/index.ts +5 -0
- package/src/dev/preview-token.ts +39 -0
- package/src/dev/routes.test.ts +354 -0
- package/src/dev/routes.ts +245 -0
- package/src/dev/token.test.ts +280 -0
- package/src/dev/token.ts +201 -0
- package/src/dev/verifier.test.ts +63 -0
- package/src/dev/verifier.ts +161 -0
- package/src/fields/attrs.ts +24 -0
- package/src/fields/collection.ts +72 -0
- package/src/fields/collections.ts +35 -0
- package/src/fields/entryScope.test.ts +62 -0
- package/src/fields/entryScope.ts +240 -0
- package/src/fields/group.ts +51 -0
- package/src/fields/helpers.test.ts +51 -0
- package/src/fields/helpers.ts +214 -0
- package/src/fields/index.ts +46 -0
- package/src/fields/scope.ts +21 -0
- package/src/fields/types.ts +119 -0
- package/src/head.test.ts +74 -0
- package/src/head.ts +123 -0
- package/src/image.test.ts +42 -0
- package/src/image.ts +64 -0
- package/src/index.test.ts +76 -0
- package/src/index.ts +13 -0
- package/src/integration.ts +218 -0
- package/src/jwt.ts +131 -0
- package/src/middleware/index.ts +4 -0
- package/src/middleware/preview.test.ts +184 -0
- package/src/middleware/preview.ts +206 -0
- package/src/preview/bridge-script.test.ts +683 -0
- package/src/preview/bridge-script.ts +763 -0
- package/src/preview/origins.ts +29 -0
- package/src/preview.ts +49 -0
- package/src/richtext/RichTextNode.astro +130 -0
- package/src/richtext/render.ts +248 -0
- package/src/richtext.test.ts +139 -0
- package/src/runtime/bundle.ts +28 -0
- package/src/runtime/store.ts +130 -0
- package/src/siteFiles.test.ts +93 -0
- package/src/siteFiles.ts +111 -0
- package/src/toolbar/app.test.ts +191 -0
- package/src/toolbar/app.ts +1091 -0
- package/src/toolbar/icon.ts +5 -0
- package/src/toolbar/widgets.test.ts +153 -0
- package/src/toolbar/widgets.ts +365 -0
- package/src/types.test.ts +154 -0
- package/src/types.ts +163 -0
- package/src/version.ts +3 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { getFieldType, listFieldTypes } from '@407dev/field-types';
|
|
2
|
+
import {
|
|
3
|
+
createComponent,
|
|
4
|
+
markHTMLString,
|
|
5
|
+
renderTemplate,
|
|
6
|
+
spreadAttributes,
|
|
7
|
+
} from 'astro/runtime/server/index.js';
|
|
8
|
+
import { renderRichTextHtml } from '../richtext/render';
|
|
9
|
+
import { getActiveBundle, recordMissingField } from '../runtime/store';
|
|
10
|
+
import type {
|
|
11
|
+
DualCallableHelper,
|
|
12
|
+
FieldComponentProps,
|
|
13
|
+
FieldHelpersMap,
|
|
14
|
+
FieldReadOptions,
|
|
15
|
+
} from './types';
|
|
16
|
+
|
|
17
|
+
const DEFAULT_TAGS: Record<string, string> = {
|
|
18
|
+
text: 'span',
|
|
19
|
+
number: 'span',
|
|
20
|
+
toggle: 'span',
|
|
21
|
+
select: 'span',
|
|
22
|
+
image: 'img',
|
|
23
|
+
img: 'img',
|
|
24
|
+
video: 'video',
|
|
25
|
+
vid: 'video',
|
|
26
|
+
richtext: 'div',
|
|
27
|
+
richText: 'div',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function getPlaceholderForType(typeId: string, fullKey: string): unknown {
|
|
31
|
+
const isDev = typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';
|
|
32
|
+
if (isDev) {
|
|
33
|
+
return `[${fullKey}]`;
|
|
34
|
+
}
|
|
35
|
+
const typeDef = getFieldType(typeId);
|
|
36
|
+
if (typeDef && typeDef.defaultValue !== undefined) {
|
|
37
|
+
return typeDef.defaultValue;
|
|
38
|
+
}
|
|
39
|
+
switch (typeId) {
|
|
40
|
+
case 'number':
|
|
41
|
+
return 0;
|
|
42
|
+
case 'toggle':
|
|
43
|
+
return false;
|
|
44
|
+
case 'image':
|
|
45
|
+
case 'video':
|
|
46
|
+
return null;
|
|
47
|
+
case 'richtext':
|
|
48
|
+
return null;
|
|
49
|
+
default:
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createDualCallableHelper<TValue = unknown, TDefault = TValue>(
|
|
55
|
+
typeId: string,
|
|
56
|
+
publicName: string,
|
|
57
|
+
prefix = '',
|
|
58
|
+
defaultKey = '',
|
|
59
|
+
): DualCallableHelper<TValue, TDefault> {
|
|
60
|
+
const typeDef = getFieldType(typeId) || getFieldType(publicName);
|
|
61
|
+
const defaultTag =
|
|
62
|
+
DEFAULT_TAGS[typeId] || DEFAULT_TAGS[publicName] || typeDef?.renderHint?.tag || 'span';
|
|
63
|
+
|
|
64
|
+
// biome-ignore lint/suspicious/noExplicitAny: Astro runtime internal component factory
|
|
65
|
+
const componentFactory = createComponent(async ($$result: any, $$props: any, _slots: any) => {
|
|
66
|
+
const rawKey = ($$props?.k ?? $$props?.key ?? defaultKey) as string;
|
|
67
|
+
const fullKey = prefix ? (rawKey ? `${prefix}.${rawKey}` : prefix) : rawKey;
|
|
68
|
+
const tag = ($$props?.as || $$props?.tag || defaultTag) as string;
|
|
69
|
+
const defaultValue = $$props?.defaultValue as TDefault | undefined;
|
|
70
|
+
|
|
71
|
+
const bundle = getActiveBundle();
|
|
72
|
+
const rawVal = fullKey ? bundle?.fields?.[fullKey] : undefined;
|
|
73
|
+
|
|
74
|
+
let displayVal: unknown = rawVal;
|
|
75
|
+
if (displayVal === undefined || displayVal === null) {
|
|
76
|
+
if (defaultValue !== undefined) {
|
|
77
|
+
displayVal = defaultValue;
|
|
78
|
+
} else {
|
|
79
|
+
if (fullKey) {
|
|
80
|
+
recordMissingField(fullKey);
|
|
81
|
+
}
|
|
82
|
+
displayVal = getPlaceholderForType(typeId, fullKey);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const {
|
|
87
|
+
k: _k,
|
|
88
|
+
key: _key,
|
|
89
|
+
defaultValue: _def,
|
|
90
|
+
as: _as,
|
|
91
|
+
tag: _tag,
|
|
92
|
+
components: _comp,
|
|
93
|
+
...htmlProps
|
|
94
|
+
} = ($$props || {}) as FieldComponentProps<TDefault>;
|
|
95
|
+
|
|
96
|
+
const attrs: Record<string, unknown> = {
|
|
97
|
+
...htmlProps,
|
|
98
|
+
};
|
|
99
|
+
if (fullKey) {
|
|
100
|
+
attrs['data-cms-field'] = fullKey;
|
|
101
|
+
}
|
|
102
|
+
if (!attrs['data-cms-kind']) {
|
|
103
|
+
attrs['data-cms-kind'] = typeId;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeId === 'image' || publicName === 'img') {
|
|
107
|
+
if (displayVal && typeof displayVal === 'object') {
|
|
108
|
+
const asset = displayVal as { url?: string; alt?: string; width?: number; height?: number };
|
|
109
|
+
if (asset.url && !attrs.src) attrs.src = asset.url;
|
|
110
|
+
if (asset.alt && !attrs.alt) attrs.alt = asset.alt;
|
|
111
|
+
if (asset.width && !attrs.width) attrs.width = asset.width;
|
|
112
|
+
if (asset.height && !attrs.height) attrs.height = asset.height;
|
|
113
|
+
} else if (typeof displayVal === 'string' && displayVal) {
|
|
114
|
+
if (!attrs.src) attrs.src = displayVal;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return renderTemplate`<img${spreadAttributes(attrs)}>`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (typeId === 'video' || publicName === 'vid') {
|
|
121
|
+
if (displayVal && typeof displayVal === 'object') {
|
|
122
|
+
const asset = displayVal as {
|
|
123
|
+
url?: string;
|
|
124
|
+
poster?: string;
|
|
125
|
+
width?: number;
|
|
126
|
+
height?: number;
|
|
127
|
+
};
|
|
128
|
+
if (asset.url && !attrs.src) attrs.src = asset.url;
|
|
129
|
+
if (asset.poster && !attrs.poster) attrs.poster = asset.poster;
|
|
130
|
+
if (asset.width && !attrs.width) attrs.width = asset.width;
|
|
131
|
+
if (asset.height && !attrs.height) attrs.height = asset.height;
|
|
132
|
+
} else if (typeof displayVal === 'string' && displayVal) {
|
|
133
|
+
if (!attrs.src) attrs.src = displayVal;
|
|
134
|
+
}
|
|
135
|
+
if (attrs.controls === undefined) attrs.controls = true;
|
|
136
|
+
|
|
137
|
+
return renderTemplate`<video${spreadAttributes(attrs)}></video>`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (typeId === 'richtext' || publicName === 'richText') {
|
|
141
|
+
const html = renderRichTextHtml(displayVal, { components: $$props?.components });
|
|
142
|
+
return renderTemplate`<${tag}${spreadAttributes(attrs)}>${markHTMLString(html)}</${tag}>`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return renderTemplate`<${tag}${spreadAttributes(attrs)}>${displayVal !== null && displayVal !== undefined ? String(displayVal) : ''}</${tag}>`;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const fn = (arg0?: unknown, ...rest: unknown[]): unknown => {
|
|
149
|
+
const isComponentCall =
|
|
150
|
+
arg0 &&
|
|
151
|
+
typeof arg0 === 'object' &&
|
|
152
|
+
'styles' in (arg0 as Record<string, unknown>) &&
|
|
153
|
+
'scripts' in (arg0 as Record<string, unknown>);
|
|
154
|
+
|
|
155
|
+
if (!isComponentCall) {
|
|
156
|
+
// Synchronous read call: f.text('key', opts) or hero.heading(opts)
|
|
157
|
+
let rawKey = defaultKey;
|
|
158
|
+
let options: FieldReadOptions<TDefault> = {};
|
|
159
|
+
|
|
160
|
+
if (typeof arg0 === 'string') {
|
|
161
|
+
rawKey = arg0;
|
|
162
|
+
options = (rest[0] || {}) as FieldReadOptions<TDefault>;
|
|
163
|
+
} else if (arg0 && typeof arg0 === 'object') {
|
|
164
|
+
options = arg0 as FieldReadOptions<TDefault>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const fullKey = prefix ? (rawKey ? `${prefix}.${rawKey}` : prefix) : rawKey;
|
|
168
|
+
const bundle = getActiveBundle();
|
|
169
|
+
const val = fullKey ? bundle?.fields?.[fullKey] : undefined;
|
|
170
|
+
|
|
171
|
+
if (val !== undefined && val !== null) {
|
|
172
|
+
return val as TValue;
|
|
173
|
+
}
|
|
174
|
+
if (options.defaultValue !== undefined) {
|
|
175
|
+
return options.defaultValue as unknown as TValue;
|
|
176
|
+
}
|
|
177
|
+
if (fullKey) {
|
|
178
|
+
recordMissingField(fullKey);
|
|
179
|
+
}
|
|
180
|
+
return getPlaceholderForType(typeId, fullKey) as TValue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Astro component execution: ($$result, $$props, $$slots)
|
|
184
|
+
return componentFactory(arg0, rest[0], rest[1]);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
Object.assign(fn, {
|
|
188
|
+
isAstroComponentFactory: true,
|
|
189
|
+
typeId,
|
|
190
|
+
publicName,
|
|
191
|
+
prefix,
|
|
192
|
+
defaultKey,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return fn as unknown as DualCallableHelper<TValue, TDefault>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function generateAllFieldHelpers(prefix = ''): FieldHelpersMap {
|
|
199
|
+
const helpers: Record<string, DualCallableHelper<unknown, unknown>> = {};
|
|
200
|
+
const types = listFieldTypes();
|
|
201
|
+
|
|
202
|
+
for (const def of types) {
|
|
203
|
+
helpers[def.id] = createDualCallableHelper(def.id, def.id, prefix);
|
|
204
|
+
if (def.aliases) {
|
|
205
|
+
for (const alias of def.aliases) {
|
|
206
|
+
helpers[alias] = createDualCallableHelper(def.id, alias, prefix);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return helpers as unknown as FieldHelpersMap;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export const createFieldHelpersMap = generateAllFieldHelpers;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { collectionPaths, defineCollection, getEntries, getEntry } from './collection';
|
|
2
|
+
import { createEntryScope, entryScope } from './entryScope';
|
|
3
|
+
import { defineGroup } from './group';
|
|
4
|
+
import { createFieldHelpersMap } from './helpers';
|
|
5
|
+
import { createScope } from './scope';
|
|
6
|
+
import type { FieldHelpersMap } from './types';
|
|
7
|
+
|
|
8
|
+
const baseHelpers = createFieldHelpersMap('');
|
|
9
|
+
|
|
10
|
+
export type CmsF = FieldHelpersMap & {
|
|
11
|
+
scope: typeof createScope;
|
|
12
|
+
entryScope: typeof createEntryScope;
|
|
13
|
+
group: typeof defineGroup;
|
|
14
|
+
entries: typeof getEntries;
|
|
15
|
+
entry: typeof getEntry;
|
|
16
|
+
defineCollection: typeof defineCollection;
|
|
17
|
+
collectionPaths: typeof collectionPaths;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const f: CmsF = {
|
|
21
|
+
...baseHelpers,
|
|
22
|
+
scope: createScope,
|
|
23
|
+
entryScope: createEntryScope,
|
|
24
|
+
group: defineGroup,
|
|
25
|
+
entries: getEntries,
|
|
26
|
+
entry: getEntry,
|
|
27
|
+
defineCollection,
|
|
28
|
+
collectionPaths,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
defineCollection,
|
|
33
|
+
collectionPaths,
|
|
34
|
+
getEntries,
|
|
35
|
+
getEntry,
|
|
36
|
+
defineGroup,
|
|
37
|
+
createScope,
|
|
38
|
+
createEntryScope,
|
|
39
|
+
entryScope,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export { z } from '@407dev/blocks';
|
|
43
|
+
|
|
44
|
+
export * from './attrs';
|
|
45
|
+
export * from './entryScope';
|
|
46
|
+
export * from './types';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { generateAllFieldHelpers } from './helpers';
|
|
2
|
+
import type { FieldScope } from './types';
|
|
3
|
+
|
|
4
|
+
export function createScope(prefix = ''): FieldScope {
|
|
5
|
+
const helpers = generateAllFieldHelpers(prefix);
|
|
6
|
+
|
|
7
|
+
const scopeFn = (key: string): string => {
|
|
8
|
+
return prefix ? `${prefix}.${key}` : key;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
Object.assign(scopeFn, helpers, {
|
|
12
|
+
key: (key: string): string => (prefix ? `${prefix}.${key}` : key),
|
|
13
|
+
scope: (subPrefix: string): FieldScope => {
|
|
14
|
+
const nextPrefix = prefix ? `${prefix}.${subPrefix}` : subPrefix;
|
|
15
|
+
return createScope(nextPrefix);
|
|
16
|
+
},
|
|
17
|
+
prefix,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return scopeFn as unknown as FieldScope;
|
|
21
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { CmsAsset, z } from '@407dev/blocks';
|
|
2
|
+
|
|
3
|
+
export interface FieldReadOptions<T = unknown> {
|
|
4
|
+
defaultValue?: T;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface FieldComponentProps<T = unknown> {
|
|
8
|
+
k?: string;
|
|
9
|
+
key?: string;
|
|
10
|
+
defaultValue?: T;
|
|
11
|
+
as?: string;
|
|
12
|
+
tag?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DualCallableHelper<TValue = unknown, TDefault = TValue> {
|
|
18
|
+
/** Synchronous read call: f.text('home.hero.heading', { defaultValue: '...' }) or group.field() */
|
|
19
|
+
(key?: string, options?: FieldReadOptions<TDefault>): TValue;
|
|
20
|
+
(options?: FieldReadOptions<TDefault>): TValue;
|
|
21
|
+
/** JSX / Astro template component call: <f.text k="home.hero.heading" /> */
|
|
22
|
+
(props?: FieldComponentProps<TDefault>): unknown;
|
|
23
|
+
/** Astro component factory call: ($$result, $$props, $$slots) */
|
|
24
|
+
(result: unknown, props: FieldComponentProps<TDefault>, slots?: unknown): unknown;
|
|
25
|
+
isAstroComponentFactory: true;
|
|
26
|
+
typeId: string;
|
|
27
|
+
publicName: string;
|
|
28
|
+
prefix?: string;
|
|
29
|
+
defaultKey?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface FieldHelpersMap {
|
|
33
|
+
text: DualCallableHelper<string>;
|
|
34
|
+
number: DualCallableHelper<number>;
|
|
35
|
+
toggle: DualCallableHelper<boolean>;
|
|
36
|
+
select: DualCallableHelper<string>;
|
|
37
|
+
img: DualCallableHelper<CmsAsset | string>;
|
|
38
|
+
image: DualCallableHelper<CmsAsset | string>;
|
|
39
|
+
vid: DualCallableHelper<CmsAsset | string>;
|
|
40
|
+
video: DualCallableHelper<CmsAsset | string>;
|
|
41
|
+
richText: DualCallableHelper<unknown>;
|
|
42
|
+
richtext: DualCallableHelper<unknown>;
|
|
43
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic index signature for arbitrary field types
|
|
44
|
+
[key: string]: any;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type FieldScope = FieldHelpersMap & {
|
|
48
|
+
(key: string): string;
|
|
49
|
+
key(key: string): string;
|
|
50
|
+
scope(subPrefix: string): FieldScope;
|
|
51
|
+
readonly prefix: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export interface GroupAccessor<TShape = Record<string, unknown>> {
|
|
55
|
+
attrs(): { 'data-cms-group': string };
|
|
56
|
+
key(key: string): string;
|
|
57
|
+
(key: string): string;
|
|
58
|
+
readonly groupName: string;
|
|
59
|
+
readonly prefix: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type BoundGroupAccessor<TShape> = GroupAccessor<TShape> & {
|
|
63
|
+
[K in keyof TShape]: TShape[K] extends DualCallableHelper<infer TVal, infer TDef>
|
|
64
|
+
? DualCallableHelper<TVal, TDef>
|
|
65
|
+
: DualCallableHelper<unknown>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export interface GroupDefinition<TShape = Record<string, unknown>> {
|
|
69
|
+
(scopeKey: string): BoundGroupAccessor<TShape>;
|
|
70
|
+
readonly groupName: string;
|
|
71
|
+
readonly shape: TShape;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface DefineCollectionOptions<TSchema extends z.ZodTypeAny> {
|
|
75
|
+
key: string;
|
|
76
|
+
schema: TSchema;
|
|
77
|
+
route?: string;
|
|
78
|
+
presentation?: 'visual' | 'form';
|
|
79
|
+
titleField?: string;
|
|
80
|
+
listColumns?: string[];
|
|
81
|
+
bodyField?: string;
|
|
82
|
+
orderable?: boolean;
|
|
83
|
+
seed?: Array<{ slug: string; content: z.input<TSchema> }>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface CollectionHandle<TSchema extends z.ZodTypeAny = z.ZodTypeAny> {
|
|
87
|
+
readonly key: string;
|
|
88
|
+
readonly schema: TSchema;
|
|
89
|
+
readonly route?: string;
|
|
90
|
+
readonly presentation?: 'visual' | 'form';
|
|
91
|
+
readonly titleField?: string;
|
|
92
|
+
readonly listColumns?: string[];
|
|
93
|
+
readonly bodyField?: string;
|
|
94
|
+
readonly orderable?: boolean;
|
|
95
|
+
readonly jsonSchema: Record<string, unknown>;
|
|
96
|
+
readonly schemaVersion: number;
|
|
97
|
+
readonly seed?: Array<{ slug: string; content: z.input<TSchema> }>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface CollectionEntry<TContent = Record<string, unknown>> {
|
|
101
|
+
slug: string;
|
|
102
|
+
content: TContent;
|
|
103
|
+
status: 'draft' | 'published' | 'scheduled';
|
|
104
|
+
published_at?: string | null;
|
|
105
|
+
position?: string | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type InferEntryContent<T> = T extends CollectionHandle<infer TSchema>
|
|
109
|
+
? z.infer<TSchema>
|
|
110
|
+
: Record<string, unknown>;
|
|
111
|
+
|
|
112
|
+
export interface CollectionStaticPath<TContent = Record<string, unknown>> {
|
|
113
|
+
params: {
|
|
114
|
+
slug: string;
|
|
115
|
+
};
|
|
116
|
+
props: {
|
|
117
|
+
entry: CollectionEntry<TContent>;
|
|
118
|
+
};
|
|
119
|
+
}
|
package/src/head.test.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { type HeadRouteInput, buildHeadTags } from './head';
|
|
3
|
+
import type { CmsSiteConfig } from './types';
|
|
4
|
+
|
|
5
|
+
describe('buildHeadTags', () => {
|
|
6
|
+
const mockSiteConfig: CmsSiteConfig = {
|
|
7
|
+
site_id: 'site-1',
|
|
8
|
+
redirects: [],
|
|
9
|
+
robots: {},
|
|
10
|
+
default_seo: {
|
|
11
|
+
title: 'Default Site Title',
|
|
12
|
+
description: 'Default site description',
|
|
13
|
+
canonical: 'https://example.com/fallback',
|
|
14
|
+
og_image: 'https://example.com/default-og.jpg',
|
|
15
|
+
twitter_card: 'summary',
|
|
16
|
+
},
|
|
17
|
+
sitemap: {},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const mockRoute: HeadRouteInput = {
|
|
21
|
+
path: '/about',
|
|
22
|
+
title: 'About Route',
|
|
23
|
+
seo: {
|
|
24
|
+
title: 'About Us | Custom Title',
|
|
25
|
+
description: 'Custom route description',
|
|
26
|
+
noindex: true,
|
|
27
|
+
og_image: {
|
|
28
|
+
id: 'asset-1',
|
|
29
|
+
url: 'https://example.com/custom-og.jpg',
|
|
30
|
+
alt: 'Custom OG',
|
|
31
|
+
focal_x: 0.5,
|
|
32
|
+
focal_y: 0.5,
|
|
33
|
+
mime_type: 'image/jpeg',
|
|
34
|
+
filename: 'custom.jpg',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
it('merges default SEO under route SEO overrides', () => {
|
|
40
|
+
const tags = buildHeadTags(mockRoute, mockSiteConfig, { siteUrl: 'https://example.com' });
|
|
41
|
+
|
|
42
|
+
const titleTag = tags.find((t) => t.tag === 'title');
|
|
43
|
+
expect(titleTag?.content).toBe('About Us | Custom Title');
|
|
44
|
+
|
|
45
|
+
const descTag = tags.find((t) => t.attrs?.name === 'description');
|
|
46
|
+
expect(descTag?.attrs?.content).toBe('Custom route description');
|
|
47
|
+
|
|
48
|
+
const canonicalTag = tags.find((t) => t.tag === 'link' && t.attrs?.rel === 'canonical');
|
|
49
|
+
expect(canonicalTag?.attrs?.href).toBe('https://example.com/fallback');
|
|
50
|
+
|
|
51
|
+
const robotsTag = tags.find((t) => t.attrs?.name === 'robots');
|
|
52
|
+
expect(robotsTag?.attrs?.content).toBe('noindex, nofollow');
|
|
53
|
+
|
|
54
|
+
const ogImageTag = tags.find((t) => t.attrs?.property === 'og:image');
|
|
55
|
+
expect(ogImageTag?.attrs?.content).toBe('https://example.com/custom-og.jpg');
|
|
56
|
+
|
|
57
|
+
const twitterTag = tags.find((t) => t.attrs?.name === 'twitter:card');
|
|
58
|
+
expect(twitterTag?.attrs?.content).toBe('summary');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('falls back to site defaults and generates canonical url from route path', () => {
|
|
62
|
+
const minimalRoute: HeadRouteInput = {
|
|
63
|
+
path: '/pricing',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const tags = buildHeadTags(minimalRoute, mockSiteConfig, { siteUrl: 'https://example.com' });
|
|
67
|
+
|
|
68
|
+
const titleTag = tags.find((t) => t.tag === 'title');
|
|
69
|
+
expect(titleTag?.content).toBe('Default Site Title');
|
|
70
|
+
|
|
71
|
+
const canonicalTag = tags.find((t) => t.tag === 'link' && t.attrs?.rel === 'canonical');
|
|
72
|
+
expect(canonicalTag?.attrs?.href).toBe('https://example.com/fallback');
|
|
73
|
+
});
|
|
74
|
+
});
|
package/src/head.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { CmsAsset } from '@407dev/blocks';
|
|
2
|
+
import type { CmsSiteConfig, RouteSeo } from './types';
|
|
3
|
+
|
|
4
|
+
export interface HeadTag {
|
|
5
|
+
tag: 'title' | 'meta' | 'link';
|
|
6
|
+
attrs?: Record<string, string>;
|
|
7
|
+
content?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface BuildHeadTagsOptions {
|
|
11
|
+
siteUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface HeadRouteInput {
|
|
15
|
+
path?: string;
|
|
16
|
+
pattern?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
seo?: RouteSeo;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Pure function building an array of head tag descriptors from route and site config.
|
|
24
|
+
* Merges site_settings.default_seo underneath routes.seo overrides.
|
|
25
|
+
*/
|
|
26
|
+
export function buildHeadTags(
|
|
27
|
+
route?: HeadRouteInput | null,
|
|
28
|
+
siteConfig?: CmsSiteConfig | null,
|
|
29
|
+
options?: BuildHeadTagsOptions,
|
|
30
|
+
): HeadTag[] {
|
|
31
|
+
const tags: HeadTag[] = [];
|
|
32
|
+
const defaultSeo: RouteSeo = siteConfig?.default_seo || {};
|
|
33
|
+
const routeSeo: RouteSeo = (route?.seo || {}) as RouteSeo;
|
|
34
|
+
|
|
35
|
+
// Title
|
|
36
|
+
const title = routeSeo.title || defaultSeo.title || route?.title || '';
|
|
37
|
+
if (title) {
|
|
38
|
+
tags.push({ tag: 'title', content: title });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Description
|
|
42
|
+
const description = routeSeo.description || defaultSeo.description;
|
|
43
|
+
if (description) {
|
|
44
|
+
tags.push({
|
|
45
|
+
tag: 'meta',
|
|
46
|
+
attrs: { name: 'description', content: description },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Canonical URL
|
|
51
|
+
const siteUrl = options?.siteUrl ? options.siteUrl.replace(/\/+$/, '') : '';
|
|
52
|
+
let canonical = routeSeo.canonical || defaultSeo.canonical;
|
|
53
|
+
if (!canonical && siteUrl && route?.path) {
|
|
54
|
+
const normPath =
|
|
55
|
+
route.path === '/' ? '' : route.path.startsWith('/') ? route.path : `/${route.path}`;
|
|
56
|
+
canonical = `${siteUrl}${normPath}`;
|
|
57
|
+
}
|
|
58
|
+
if (canonical) {
|
|
59
|
+
tags.push({
|
|
60
|
+
tag: 'link',
|
|
61
|
+
attrs: { rel: 'canonical', href: canonical },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Robots Noindex
|
|
66
|
+
if (routeSeo.noindex || defaultSeo.noindex) {
|
|
67
|
+
tags.push({
|
|
68
|
+
tag: 'meta',
|
|
69
|
+
attrs: { name: 'robots', content: 'noindex, nofollow' },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Open Graph
|
|
74
|
+
const ogTitle =
|
|
75
|
+
routeSeo.og_title || routeSeo.title || defaultSeo.og_title || defaultSeo.title || route?.title;
|
|
76
|
+
if (ogTitle) {
|
|
77
|
+
tags.push({
|
|
78
|
+
tag: 'meta',
|
|
79
|
+
attrs: { property: 'og:title', content: ogTitle },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ogDescription =
|
|
84
|
+
routeSeo.og_description ||
|
|
85
|
+
routeSeo.description ||
|
|
86
|
+
defaultSeo.og_description ||
|
|
87
|
+
defaultSeo.description;
|
|
88
|
+
if (ogDescription) {
|
|
89
|
+
tags.push({
|
|
90
|
+
tag: 'meta',
|
|
91
|
+
attrs: { property: 'og:description', content: ogDescription },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const rawOgImage = routeSeo.og_image || defaultSeo.og_image;
|
|
96
|
+
const ogImageUrl =
|
|
97
|
+
typeof rawOgImage === 'object' && rawOgImage !== null && 'url' in rawOgImage
|
|
98
|
+
? (rawOgImage as CmsAsset).url
|
|
99
|
+
: (rawOgImage as string | undefined);
|
|
100
|
+
|
|
101
|
+
if (ogImageUrl) {
|
|
102
|
+
tags.push({
|
|
103
|
+
tag: 'meta',
|
|
104
|
+
attrs: { property: 'og:image', content: ogImageUrl },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (canonical) {
|
|
109
|
+
tags.push({
|
|
110
|
+
tag: 'meta',
|
|
111
|
+
attrs: { property: 'og:url', content: canonical },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Twitter
|
|
116
|
+
const twitterCard = routeSeo.twitter_card || defaultSeo.twitter_card || 'summary_large_image';
|
|
117
|
+
tags.push({
|
|
118
|
+
tag: 'meta',
|
|
119
|
+
attrs: { name: 'twitter:card', content: twitterCard },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return tags;
|
|
123
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildCdnImageUrl, buildSrcSet, isCdnTransformableUrl } from './image';
|
|
3
|
+
|
|
4
|
+
describe('image helpers', () => {
|
|
5
|
+
it('identifies transformable URLs vs local/r2 URLs', () => {
|
|
6
|
+
expect(isCdnTransformableUrl('https://assets.mybrand.com/photo.jpg')).toBe(true);
|
|
7
|
+
expect(isCdnTransformableUrl('http://mybrand.com/img.png')).toBe(true);
|
|
8
|
+
expect(isCdnTransformableUrl('http://localhost:8787/assets/files/img.png')).toBe(false);
|
|
9
|
+
expect(isCdnTransformableUrl('https://pub-123.r2.dev/img.png')).toBe(false);
|
|
10
|
+
expect(isCdnTransformableUrl('data:image/png;base64,123')).toBe(false);
|
|
11
|
+
expect(isCdnTransformableUrl('')).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('builds CDN image URL with formatting options', () => {
|
|
15
|
+
const url = 'https://assets.mybrand.com/photo.jpg';
|
|
16
|
+
const cdnUrl = buildCdnImageUrl(url, { width: 800, fit: 'cover', quality: 85 });
|
|
17
|
+
expect(cdnUrl).toBe(
|
|
18
|
+
'/cdn-cgi/image/width=800,format=auto,fit=cover,quality=85/https://assets.mybrand.com/photo.jpg',
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('degrades to raw URL for local / non-zone hostnames', () => {
|
|
23
|
+
const localUrl = 'http://localhost:8787/assets/files/test.png';
|
|
24
|
+
expect(buildCdnImageUrl(localUrl, { width: 600 })).toBe(localUrl);
|
|
25
|
+
|
|
26
|
+
const r2DevUrl = 'https://pub-123.r2.dev/test.png';
|
|
27
|
+
expect(buildCdnImageUrl(r2DevUrl, { width: 600 })).toBe(r2DevUrl);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('builds responsive srcset strings', () => {
|
|
31
|
+
const url = 'https://assets.mybrand.com/photo.jpg';
|
|
32
|
+
const srcset = buildSrcSet(url, [400, 800]);
|
|
33
|
+
expect(srcset).toBe(
|
|
34
|
+
'/cdn-cgi/image/width=400,format=auto/https://assets.mybrand.com/photo.jpg 400w, /cdn-cgi/image/width=800,format=auto/https://assets.mybrand.com/photo.jpg 800w',
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns empty srcset for non-transformable URLs', () => {
|
|
39
|
+
const localUrl = 'http://localhost:8787/photo.jpg';
|
|
40
|
+
expect(buildSrcSet(localUrl)).toBe('');
|
|
41
|
+
});
|
|
42
|
+
});
|