@getquick/content 0.1.0 → 0.3.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.
@@ -0,0 +1,218 @@
1
+ //#region frontend/src/metadata.ts
2
+ /** Scoped OG-1 candidate contract; adoption and public asset verification are separate gates. */
3
+ const metadataBaseline = "OG-1";
4
+ /** Inputs are plain text. Source-specific HTML/entity decoding belongs in the mapper. */
5
+ function normalizeMetadataText(value) {
6
+ return value.replace(/\s+/gu, " ").trim();
7
+ }
8
+ function resolveMetadata(input) {
9
+ const diagnostics = [];
10
+ const fail = (field, message) => diagnostics.push({
11
+ code: "invalid-metadata",
12
+ field,
13
+ message
14
+ });
15
+ const language = input.language ?? input.site.language;
16
+ const text = (value, field) => {
17
+ const result = normalizeMetadataText(value ?? "");
18
+ if (!result) fail(field, "Expected nonempty decoded plain text");
19
+ if (result.includes("\0") || /[\uD800-\uDFFF]/u.test(result)) fail(field, "Text contains NUL or unpaired UTF-16 surrogate and cannot survive HTML/UTF-8 serialization");
20
+ return result;
21
+ };
22
+ const locale = (value, field) => {
23
+ let htmlLocale;
24
+ try {
25
+ htmlLocale = new Intl.Locale(value.lang);
26
+ } catch {
27
+ fail(field, "Invalid BCP 47 HTML language");
28
+ }
29
+ if (!["ltr", "rtl"].includes(value.dir)) fail(field, "Invalid HTML direction");
30
+ const match = /^([a-z]{2,3})[_-]([a-z]{2})(?:_[a-z0-9]+)*$/iu.exec(value.locale);
31
+ if (!match) {
32
+ fail(field, "Explicit regional locale required; no territory is inferred");
33
+ return "";
34
+ }
35
+ if (htmlLocale && (htmlLocale.language.toLowerCase() !== match[1].toLowerCase() || htmlLocale.region && htmlLocale.region.toUpperCase() !== match[2].toUpperCase())) fail(field, "HTML and OG locales disagree");
36
+ return `${match[1].toLowerCase()}_${match[2].toUpperCase()}`;
37
+ };
38
+ const ogLocale = locale(language, "language");
39
+ const head = [];
40
+ const meta = (name, content, property = true) => head.push({
41
+ tag: "meta",
42
+ attrs: {
43
+ [property ? "property" : "name"]: name,
44
+ content
45
+ }
46
+ });
47
+ const applicable = input.state === "public" && input.status === 200;
48
+ meta("robots", applicable && !input.noindex ? "index, follow" : "noindex, nofollow", false);
49
+ const result = {
50
+ baseline: metadataBaseline,
51
+ ready: false,
52
+ documentReady: false,
53
+ graphReady: false,
54
+ applicable,
55
+ htmlAttributes: {
56
+ lang: language.lang,
57
+ dir: language.dir
58
+ },
59
+ head,
60
+ diagnostics
61
+ };
62
+ if (!applicable) {
63
+ if (input.statusTitle !== void 0) {
64
+ const title = text(input.statusTitle, "statusTitle");
65
+ if (!diagnostics.some((d) => d.field === "statusTitle")) head.push({
66
+ tag: "title",
67
+ text: title
68
+ });
69
+ }
70
+ result.ready = diagnostics.length === 0;
71
+ result.documentReady = result.ready && head.some((node) => node.tag === "title");
72
+ return result;
73
+ }
74
+ let origin = "";
75
+ try {
76
+ const parsed = new URL(input.site.origin);
77
+ if (parsed.href !== `${parsed.origin}/` || parsed.protocol !== "https:" || parsed.username || parsed.password || /^(localhost|.*\.localhost|.*\.local|.*\.invalid|127\..*|0\.0\.0\.0|\[::1\])$/iu.test(parsed.hostname)) throw new Error();
78
+ origin = parsed.origin;
79
+ } catch {
80
+ fail("site.origin", "Expected reviewed public HTTPS origin");
81
+ }
82
+ const url = (value, field, identity) => {
83
+ try {
84
+ const parsed = new URL(value ?? "");
85
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.hash || /^(localhost|.*\.localhost|.*\.local|.*\.invalid|127\..*|0\.0\.0\.0|\[::1\])$/iu.test(parsed.hostname)) throw new Error();
86
+ if (identity && parsed.origin !== origin) throw new Error();
87
+ for (const key of parsed.searchParams.keys()) if (/^(utm_|fbclid$|gclid$|token$|sig$|signature$|expires$|x-amz-|x-goog-|awsaccesskeyid$|preview)/iu.test(key) || identity && !input.site.identityQueryKeys?.includes(key)) throw new Error();
88
+ return parsed.href;
89
+ } catch {
90
+ fail(field, "Invalid public HTTPS URL or unreviewed canonical identity/query");
91
+ return "";
92
+ }
93
+ };
94
+ const selected = (key) => input.overrides?.[key] ?? input.page?.[key] ?? input.site[key];
95
+ const title = text(selected("title"), "title");
96
+ const shareTitle = text(input.overrides?.shareTitle ?? title, "shareTitle");
97
+ const description = text(selected("description"), "description");
98
+ const name = text(input.site.name, "site.name");
99
+ const canonical = url(input.canonical, "canonical", true);
100
+ head.push({
101
+ tag: "title",
102
+ text: title
103
+ }, {
104
+ tag: "link",
105
+ attrs: {
106
+ rel: "canonical",
107
+ href: canonical
108
+ }
109
+ });
110
+ meta("description", description, false);
111
+ for (const [key, value] of Object.entries({
112
+ title: shareTitle,
113
+ type: input.kind === "article" ? "article" : "website",
114
+ url: canonical,
115
+ description,
116
+ site_name: name,
117
+ locale: ogLocale
118
+ })) meta(`og:${key}`, value);
119
+ const images = selected("images");
120
+ if (!images?.length) fail("images", "At least one reviewed share image required");
121
+ images?.forEach((image, index) => {
122
+ const field = `images.${index}`;
123
+ const src = url(image.url, `${field}.url`, false);
124
+ const alt = text(image.alt, `${field}.alt`);
125
+ if (![image.width, image.height].every((n) => Number.isSafeInteger(n) && n > 0)) fail(field, "Known positive integer dimensions required");
126
+ if (image.type && !["image/jpeg", "image/png"].includes(image.type)) fail(field, "OG-1 supports verified JPEG/PNG MIME only");
127
+ meta("og:image", src);
128
+ meta("og:image:alt", alt);
129
+ meta("og:image:width", String(image.width));
130
+ meta("og:image:height", String(image.height));
131
+ if (image.type) meta("og:image:type", image.type);
132
+ });
133
+ const hasPublishedTranslations = input.translations?.some((t) => t.published) ?? false;
134
+ const languageKey = (value) => {
135
+ try {
136
+ return new Intl.Locale(value.lang).baseName;
137
+ } catch {
138
+ return;
139
+ }
140
+ };
141
+ if (hasPublishedTranslations && input.language) locale(input.site.language, "site.language");
142
+ const defaultLanguage = languageKey(input.site.language);
143
+ let defaultCanonical = defaultLanguage && languageKey(language) === defaultLanguage ? canonical : void 0;
144
+ const locales = /* @__PURE__ */ new Set([ogLocale]);
145
+ const langs = /* @__PURE__ */ new Set([language.lang.toLowerCase()]);
146
+ if (hasPublishedTranslations) head.push({
147
+ tag: "link",
148
+ attrs: {
149
+ rel: "alternate",
150
+ hreflang: language.lang,
151
+ href: canonical
152
+ }
153
+ });
154
+ input.translations?.filter((t) => t.published).forEach((translation, index) => {
155
+ const field = `translations.${index}`;
156
+ const translatedLocale = locale(translation, field);
157
+ const href = url(translation.canonical, field, true);
158
+ if (langs.has(translation.lang.toLowerCase())) {
159
+ fail(field, "Duplicate/current translation language");
160
+ return;
161
+ }
162
+ langs.add(translation.lang.toLowerCase());
163
+ if (defaultLanguage && languageKey(translation) === defaultLanguage) defaultCanonical = href;
164
+ head.push({
165
+ tag: "link",
166
+ attrs: {
167
+ rel: "alternate",
168
+ hreflang: translation.lang,
169
+ href
170
+ }
171
+ });
172
+ if (!locales.has(translatedLocale)) {
173
+ meta("og:locale:alternate", translatedLocale);
174
+ locales.add(translatedLocale);
175
+ }
176
+ });
177
+ if (hasPublishedTranslations && defaultCanonical) head.push({
178
+ tag: "link",
179
+ attrs: {
180
+ rel: "alternate",
181
+ hreflang: "x-default",
182
+ href: defaultCanonical
183
+ }
184
+ });
185
+ if (input.kind === "article" && input.article) {
186
+ for (const [key, value] of [["published_time", input.article.publishedTime], ["modified_time", input.article.modifiedTime]]) {
187
+ if (value === void 0) continue;
188
+ if (!/^\d{4}-\d\d-\d\dT\d\d:\d\d(?::\d\d(?:\.\d+)?)?(?:Z|[+-]\d\d:\d\d)$/u.test(value) || !Number.isFinite(Date.parse(value)) || Number(value.slice(8, 10)) > new Date(Date.UTC(Number(value.slice(0, 4)), Number(value.slice(5, 7)), 0)).getUTCDate()) fail(`article.${key}`, "Timestamp must include a timezone");
189
+ else meta(`article:${key}`, value);
190
+ }
191
+ input.article.authors?.forEach((author) => meta("article:author", url(author, "article.authors", false)));
192
+ }
193
+ result.ready = diagnostics.length === 0;
194
+ result.graphReady = result.ready;
195
+ result.documentReady = !diagnostics.some((d) => [
196
+ "title",
197
+ "description",
198
+ "canonical",
199
+ "site.origin"
200
+ ].includes(d.field) || d.field === "language" && /HTML|BCP/u.test(d.message));
201
+ if (!result.ready) {
202
+ const invalid = (field) => diagnostics.some((d) => d.field === field);
203
+ result.head = head.filter((node) => {
204
+ if (node.tag === "title") return !invalid("title");
205
+ if (node.tag === "meta") return node.attrs.name === "robots" || node.attrs.name === "description" && !invalid("description");
206
+ if (node.attrs.rel === "canonical") return !invalid("canonical");
207
+ return node.attrs.rel === "alternate" && !invalid("canonical") && !diagnostics.some((d) => d.field.startsWith("translations."));
208
+ });
209
+ } else result.htmlAttributes.prefix = "og: https://ogp.me/ns#";
210
+ return result;
211
+ }
212
+ const escape = (value) => value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
213
+ /** Safe HTML fragment for Astro set:html or other server serialization; preserves repeated image groups. */
214
+ function renderMetadataHead(metadata) {
215
+ return metadata.head.map((node) => node.tag === "title" ? `<title>${escape(node.text)}</title>` : `<${node.tag} ${Object.entries(node.attrs).map(([key, value]) => `${key}="${escape(value)}"`).join(" ")}>`).join("\n");
216
+ }
217
+ //#endregion
218
+ export { metadataBaseline, normalizeMetadataText, renderMetadataHead, resolveMetadata };
package/dist/server.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as createContentRuntime, g as ContentBindings, i as ContentRuntime, n as ContentConfig, r as ContentConsentConfig, t as WordPressClient } from "./wordpress-CPL40-vq.js";
2
- export { type ContentBindings, ContentConfig, ContentConsentConfig, ContentRuntime, type WordPressClient, createContentRuntime };
1
+ import { T as ContentBindings, a as ContentExtension, c as createContentRuntime, i as ContentConsentConfig, n as CollectionConfig, o as ContentRuntime, r as ContentConfig, s as ExtensionNode, t as WordPressClient } from "./wordpress-D_HG2rEt.js";
2
+ export { CollectionConfig, type ContentBindings, ContentConfig, ContentConsentConfig, ContentExtension, ContentRuntime, ExtensionNode, type WordPressClient, createContentRuntime };