dineway 0.1.3
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/LICENSE +9 -0
- package/README.md +89 -0
- package/dist/adapters-BlzWJG82.d.mts +106 -0
- package/dist/apply-CAPvMfoU.mjs +1339 -0
- package/dist/astro/index.d.mts +50 -0
- package/dist/astro/index.mjs +1326 -0
- package/dist/astro/middleware/auth.d.mts +30 -0
- package/dist/astro/middleware/auth.mjs +708 -0
- package/dist/astro/middleware/redirect.d.mts +21 -0
- package/dist/astro/middleware/redirect.mjs +62 -0
- package/dist/astro/middleware/request-context.d.mts +17 -0
- package/dist/astro/middleware/request-context.mjs +1371 -0
- package/dist/astro/middleware/setup.d.mts +19 -0
- package/dist/astro/middleware/setup.mjs +46 -0
- package/dist/astro/middleware.d.mts +12 -0
- package/dist/astro/middleware.mjs +1716 -0
- package/dist/astro/types.d.mts +269 -0
- package/dist/astro/types.mjs +1 -0
- package/dist/base64-F8-DUraK.mjs +58 -0
- package/dist/byline-DeWCMU_i.mjs +234 -0
- package/dist/bylines-DyqBV9EQ.mjs +137 -0
- package/dist/chunk-ClPoSABd.mjs +21 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +3987 -0
- package/dist/client/external-auth-headers.d.mts +38 -0
- package/dist/client/external-auth-headers.mjs +101 -0
- package/dist/client/index.d.mts +397 -0
- package/dist/client/index.mjs +345 -0
- package/dist/config-Cq8H0SfX.mjs +46 -0
- package/dist/connection-C9pxzuag.mjs +52 -0
- package/dist/content-zSgdNmnt.mjs +836 -0
- package/dist/db/index.d.mts +4 -0
- package/dist/db/index.mjs +62 -0
- package/dist/db/libsql.d.mts +10 -0
- package/dist/db/libsql.mjs +21 -0
- package/dist/db/postgres.d.mts +10 -0
- package/dist/db/postgres.mjs +29 -0
- package/dist/db/sqlite.d.mts +10 -0
- package/dist/db/sqlite.mjs +15 -0
- package/dist/default-WYlzADZL.mjs +80 -0
- package/dist/dialect-helpers-B9uSp2GJ.mjs +89 -0
- package/dist/error-DrxtnGPg.mjs +26 -0
- package/dist/index-C-jx21qs.d.mts +4771 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.mjs +30 -0
- package/dist/load-C6FCD1FU.mjs +27 -0
- package/dist/loader-qKmo0wAY.mjs +446 -0
- package/dist/manifest-schema-CTSEyIJ3.mjs +186 -0
- package/dist/media/index.d.mts +25 -0
- package/dist/media/index.mjs +54 -0
- package/dist/media/local-runtime.d.mts +38 -0
- package/dist/media/local-runtime.mjs +132 -0
- package/dist/media-DMTr80Gv.mjs +199 -0
- package/dist/mode-BlyYtIFO.mjs +22 -0
- package/dist/page/index.d.mts +148 -0
- package/dist/page/index.mjs +419 -0
- package/dist/placeholder-B3knXwNc.mjs +267 -0
- package/dist/placeholder-bOx1xCTY.d.mts +283 -0
- package/dist/plugin-utils.d.mts +57 -0
- package/dist/plugin-utils.mjs +77 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts +21 -0
- package/dist/plugins/adapt-sandbox-entry.mjs +112 -0
- package/dist/query-BiaPl_g2.mjs +459 -0
- package/dist/redirect-JPqLAbxa.mjs +328 -0
- package/dist/registry-DSd1GWB8.mjs +851 -0
- package/dist/request-context.d.mts +49 -0
- package/dist/request-context.mjs +42 -0
- package/dist/runner-B5l1JfOj.d.mts +26 -0
- package/dist/runner-BGUGywgG.mjs +1529 -0
- package/dist/runtime.d.mts +25 -0
- package/dist/runtime.mjs +41 -0
- package/dist/search-BNruJHDL.mjs +11054 -0
- package/dist/seed/index.d.mts +3 -0
- package/dist/seed/index.mjs +15 -0
- package/dist/seo/index.d.mts +69 -0
- package/dist/seo/index.mjs +69 -0
- package/dist/storage/local.d.mts +38 -0
- package/dist/storage/local.mjs +165 -0
- package/dist/storage/s3.d.mts +31 -0
- package/dist/storage/s3.mjs +174 -0
- package/dist/tokens-4vgYuXsZ.mjs +170 -0
- package/dist/transport-C5FYnid7.mjs +417 -0
- package/dist/transport-gIL-e43D.d.mts +41 -0
- package/dist/types-BawVha09.mjs +30 -0
- package/dist/types-BgQeVaPj.d.mts +192 -0
- package/dist/types-CLLdsG3g.d.mts +103 -0
- package/dist/types-D38djUXv.d.mts +1196 -0
- package/dist/types-DShnjzb6.mjs +15 -0
- package/dist/types-DkvMXalq.d.mts +425 -0
- package/dist/types-DuNbGKjF.mjs +74 -0
- package/dist/types-ju-_ORz7.d.mts +182 -0
- package/dist/validate-CXnRKfJK.mjs +327 -0
- package/dist/validate-CqRJb_xU.mjs +96 -0
- package/dist/validate-DVKJJ-M_.d.mts +377 -0
- package/locals.d.ts +47 -0
- package/package.json +313 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { G as PublicPageContext, M as PagePlacement, O as PageMetadataContribution, T as PageFragmentContribution, j as PageMetadataLinkRel, t as BreadcrumbItem } from "../types-D38djUXv.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/page/context.d.ts
|
|
4
|
+
/** Fields shared by both input forms */
|
|
5
|
+
interface PageContextFields {
|
|
6
|
+
kind: "content" | "custom";
|
|
7
|
+
pageType?: string;
|
|
8
|
+
title?: string | null;
|
|
9
|
+
pageTitle?: string | null;
|
|
10
|
+
description?: string | null;
|
|
11
|
+
canonical?: string | null;
|
|
12
|
+
image?: string | null;
|
|
13
|
+
content?: {
|
|
14
|
+
collection: string;
|
|
15
|
+
id: string;
|
|
16
|
+
slug?: string | null;
|
|
17
|
+
};
|
|
18
|
+
/** SEO overrides for OG/Twitter meta generation */
|
|
19
|
+
seo?: {
|
|
20
|
+
ogTitle?: string | null;
|
|
21
|
+
ogDescription?: string | null;
|
|
22
|
+
ogImage?: string | null;
|
|
23
|
+
robots?: string | null;
|
|
24
|
+
};
|
|
25
|
+
/** Article metadata for Open Graph article: tags */
|
|
26
|
+
articleMeta?: {
|
|
27
|
+
publishedTime?: string | null;
|
|
28
|
+
modifiedTime?: string | null;
|
|
29
|
+
author?: string | null;
|
|
30
|
+
};
|
|
31
|
+
/** Site name for structured data and og:site_name */
|
|
32
|
+
siteName?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Breadcrumb trail for this page, root first. Pass an empty array
|
|
35
|
+
* to explicitly opt out of breadcrumbs (e.g. homepage), or omit the
|
|
36
|
+
* field to let consumers fall back to their own derivation.
|
|
37
|
+
*/
|
|
38
|
+
breadcrumbs?: BreadcrumbItem[];
|
|
39
|
+
/** Public-facing site URL (origin) for structured data */
|
|
40
|
+
siteUrl?: string;
|
|
41
|
+
}
|
|
42
|
+
/** Input with Astro global -- used in .astro files */
|
|
43
|
+
interface AstroInput extends PageContextFields {
|
|
44
|
+
Astro: {
|
|
45
|
+
url: URL;
|
|
46
|
+
currentLocale?: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Input with explicit URL -- used outside .astro files */
|
|
50
|
+
interface UrlInput extends PageContextFields {
|
|
51
|
+
url: URL | string;
|
|
52
|
+
locale?: string;
|
|
53
|
+
}
|
|
54
|
+
type CreatePublicPageContextInput = AstroInput | UrlInput;
|
|
55
|
+
/**
|
|
56
|
+
* Build a PublicPageContext from template input.
|
|
57
|
+
*/
|
|
58
|
+
declare function createPublicPageContext(input: CreatePublicPageContextInput): PublicPageContext;
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/page/metadata.d.ts
|
|
61
|
+
interface ResolvedPageMetadata {
|
|
62
|
+
meta: Array<{
|
|
63
|
+
name: string;
|
|
64
|
+
content: string;
|
|
65
|
+
}>;
|
|
66
|
+
properties: Array<{
|
|
67
|
+
property: string;
|
|
68
|
+
content: string;
|
|
69
|
+
}>;
|
|
70
|
+
links: Array<{
|
|
71
|
+
rel: PageMetadataLinkRel;
|
|
72
|
+
href: string;
|
|
73
|
+
hreflang?: string;
|
|
74
|
+
}>;
|
|
75
|
+
jsonld: Array<{
|
|
76
|
+
id?: string;
|
|
77
|
+
json: string;
|
|
78
|
+
}>;
|
|
79
|
+
}
|
|
80
|
+
/** Escape a string for safe use in an HTML attribute value */
|
|
81
|
+
declare function escapeHtmlAttr(value: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* Safely serialize a value for embedding in a <script type="application/ld+json"> tag.
|
|
84
|
+
*
|
|
85
|
+
* Plain JSON.stringify is not sufficient because:
|
|
86
|
+
* - "</script>" in a nested string breaks out of the script tag
|
|
87
|
+
* - "<!--" can open an HTML comment
|
|
88
|
+
* - U+2028/U+2029 are line terminators in some JS engines
|
|
89
|
+
*/
|
|
90
|
+
declare function safeJsonLdSerialize(value: unknown): string;
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a flat list of contributions into deduplicated metadata.
|
|
93
|
+
* First contribution wins for any given dedupe key.
|
|
94
|
+
*/
|
|
95
|
+
declare function resolvePageMetadata(contributions: PageMetadataContribution[]): ResolvedPageMetadata;
|
|
96
|
+
/** Render resolved metadata to an HTML string for embedding in <head> */
|
|
97
|
+
declare function renderPageMetadata(metadata: ResolvedPageMetadata): string;
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/page/fragments.d.ts
|
|
100
|
+
/**
|
|
101
|
+
* Filter contributions to a specific placement and deduplicate.
|
|
102
|
+
* - Contributions with the same `key + placement` are deduped (first wins).
|
|
103
|
+
* - External scripts with the same `src + placement` are deduped.
|
|
104
|
+
*/
|
|
105
|
+
declare function resolveFragments(contributions: PageFragmentContribution[], placement: PagePlacement): PageFragmentContribution[];
|
|
106
|
+
/** Render a list of fragment contributions to an HTML string */
|
|
107
|
+
declare function renderFragments(contributions: PageFragmentContribution[], placement: PagePlacement): string;
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/page/seo-contributions.d.ts
|
|
110
|
+
/**
|
|
111
|
+
* Generate base metadata contributions from a page context's SEO data.
|
|
112
|
+
* Returns an empty array if no SEO-relevant data is present.
|
|
113
|
+
*/
|
|
114
|
+
declare function generateBaseSeoContributions(page: PublicPageContext): PageMetadataContribution[];
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/page/jsonld.d.ts
|
|
117
|
+
/**
|
|
118
|
+
* Remove null/undefined values from a JSON-LD object recursively.
|
|
119
|
+
* JSON-LD validators prefer absent keys over null values.
|
|
120
|
+
*/
|
|
121
|
+
declare function cleanJsonLd(obj: Record<string, unknown>): Record<string, unknown>;
|
|
122
|
+
/**
|
|
123
|
+
* Build a BlogPosting JSON-LD graph from page context.
|
|
124
|
+
* Used for article-type content pages.
|
|
125
|
+
*/
|
|
126
|
+
declare function buildBlogPostingJsonLd(page: PublicPageContext): Record<string, unknown> | null;
|
|
127
|
+
/**
|
|
128
|
+
* Build a WebSite JSON-LD graph from page context.
|
|
129
|
+
* Used for non-article pages (homepage, listing pages, etc.)
|
|
130
|
+
*/
|
|
131
|
+
declare function buildWebSiteJsonLd(page: PublicPageContext): Record<string, unknown> | null;
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/page/index.d.ts
|
|
134
|
+
/**
|
|
135
|
+
* Shape of the Dineway runtime methods used by the render components.
|
|
136
|
+
* Extracted here so all three components share a single type definition.
|
|
137
|
+
*/
|
|
138
|
+
interface DinewayPageRuntime {
|
|
139
|
+
collectPageMetadata: (page: PublicPageContext) => Promise<PageMetadataContribution[]>;
|
|
140
|
+
collectPageFragments: (page: PublicPageContext) => Promise<PageFragmentContribution[]>;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get the page runtime from Astro locals. Returns undefined when
|
|
144
|
+
* Dineway is not initialized (components render nothing in that case).
|
|
145
|
+
*/
|
|
146
|
+
declare function getPageRuntime(locals: Record<string, unknown>): DinewayPageRuntime | undefined;
|
|
147
|
+
//#endregion
|
|
148
|
+
export { type CreatePublicPageContextInput, DinewayPageRuntime, type ResolvedPageMetadata, buildBlogPostingJsonLd, buildWebSiteJsonLd, cleanJsonLd, createPublicPageContext, escapeHtmlAttr, generateBaseSeoContributions, getPageRuntime, renderFragments, renderPageMetadata, resolveFragments, resolvePageMetadata, safeJsonLdSerialize };
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
//#region src/page/context.ts
|
|
2
|
+
function isAstroInput(input) {
|
|
3
|
+
return "Astro" in input;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Build a PublicPageContext from template input.
|
|
7
|
+
*/
|
|
8
|
+
function createPublicPageContext(input) {
|
|
9
|
+
let url;
|
|
10
|
+
let path;
|
|
11
|
+
let locale;
|
|
12
|
+
if (isAstroInput(input)) {
|
|
13
|
+
url = input.Astro.url.href;
|
|
14
|
+
path = input.Astro.url.pathname;
|
|
15
|
+
locale = input.Astro.currentLocale ?? null;
|
|
16
|
+
} else {
|
|
17
|
+
const parsed = typeof input.url === "string" ? new URL(input.url) : input.url;
|
|
18
|
+
url = parsed.href;
|
|
19
|
+
path = parsed.pathname;
|
|
20
|
+
locale = input.locale ?? null;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
url,
|
|
24
|
+
path,
|
|
25
|
+
locale,
|
|
26
|
+
kind: input.kind,
|
|
27
|
+
pageType: input.pageType ?? (input.kind === "content" ? "article" : "website"),
|
|
28
|
+
title: input.title ?? null,
|
|
29
|
+
pageTitle: input.pageTitle ?? null,
|
|
30
|
+
description: input.description ?? null,
|
|
31
|
+
canonical: input.canonical ?? null,
|
|
32
|
+
image: input.image ?? null,
|
|
33
|
+
content: input.content ? {
|
|
34
|
+
collection: input.content.collection,
|
|
35
|
+
id: input.content.id,
|
|
36
|
+
slug: input.content.slug ?? null
|
|
37
|
+
} : void 0,
|
|
38
|
+
seo: input.seo,
|
|
39
|
+
articleMeta: input.articleMeta,
|
|
40
|
+
siteName: input.siteName,
|
|
41
|
+
breadcrumbs: input.breadcrumbs,
|
|
42
|
+
siteUrl: input.siteUrl
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/page/metadata.ts
|
|
48
|
+
/** Schemes safe for use in link href attributes */
|
|
49
|
+
const SAFE_HREF_RE = /^(https?|at):\/\//i;
|
|
50
|
+
const HTML_ESCAPE_MAP = {
|
|
51
|
+
"&": "&",
|
|
52
|
+
"<": "<",
|
|
53
|
+
">": ">",
|
|
54
|
+
"\"": """,
|
|
55
|
+
"'": "'"
|
|
56
|
+
};
|
|
57
|
+
const HTML_ESCAPE_RE = /[&<>"']/g;
|
|
58
|
+
/** Escape a string for safe use in an HTML attribute value */
|
|
59
|
+
function escapeHtmlAttr(value) {
|
|
60
|
+
return value.replace(HTML_ESCAPE_RE, (ch) => HTML_ESCAPE_MAP[ch] ?? ch);
|
|
61
|
+
}
|
|
62
|
+
/** Validate that a URL uses a safe scheme (http, https, at) */
|
|
63
|
+
function isSafeHref(url) {
|
|
64
|
+
return SAFE_HREF_RE.test(url);
|
|
65
|
+
}
|
|
66
|
+
const JSONLD_LT_RE = /</g;
|
|
67
|
+
const JSONLD_GT_RE = />/g;
|
|
68
|
+
const JSONLD_U2028_RE = /\u2028/g;
|
|
69
|
+
const JSONLD_U2029_RE = /\u2029/g;
|
|
70
|
+
/**
|
|
71
|
+
* Safely serialize a value for embedding in a <script type="application/ld+json"> tag.
|
|
72
|
+
*
|
|
73
|
+
* Plain JSON.stringify is not sufficient because:
|
|
74
|
+
* - "<\/script>" in a nested string breaks out of the script tag
|
|
75
|
+
* - "<!--" can open an HTML comment
|
|
76
|
+
* - U+2028/U+2029 are line terminators in some JS engines
|
|
77
|
+
*/
|
|
78
|
+
function safeJsonLdSerialize(value) {
|
|
79
|
+
return JSON.stringify(value).replace(JSONLD_LT_RE, "\\u003c").replace(JSONLD_GT_RE, "\\u003e").replace(JSONLD_U2028_RE, "\\u2028").replace(JSONLD_U2029_RE, "\\u2029");
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolve a flat list of contributions into deduplicated metadata.
|
|
83
|
+
* First contribution wins for any given dedupe key.
|
|
84
|
+
*/
|
|
85
|
+
function resolvePageMetadata(contributions) {
|
|
86
|
+
const result = {
|
|
87
|
+
meta: [],
|
|
88
|
+
properties: [],
|
|
89
|
+
links: [],
|
|
90
|
+
jsonld: []
|
|
91
|
+
};
|
|
92
|
+
const seenMeta = /* @__PURE__ */ new Set();
|
|
93
|
+
const seenProperties = /* @__PURE__ */ new Set();
|
|
94
|
+
const seenLinks = /* @__PURE__ */ new Set();
|
|
95
|
+
const seenJsonLd = /* @__PURE__ */ new Set();
|
|
96
|
+
for (const c of contributions) switch (c.kind) {
|
|
97
|
+
case "meta": {
|
|
98
|
+
const dedupeKey = c.key ?? c.name;
|
|
99
|
+
if (seenMeta.has(dedupeKey)) continue;
|
|
100
|
+
seenMeta.add(dedupeKey);
|
|
101
|
+
result.meta.push({
|
|
102
|
+
name: c.name,
|
|
103
|
+
content: c.content
|
|
104
|
+
});
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "property": {
|
|
108
|
+
const dedupeKey = c.key ?? c.property;
|
|
109
|
+
if (seenProperties.has(dedupeKey)) continue;
|
|
110
|
+
seenProperties.add(dedupeKey);
|
|
111
|
+
result.properties.push({
|
|
112
|
+
property: c.property,
|
|
113
|
+
content: c.content
|
|
114
|
+
});
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "link":
|
|
118
|
+
if (!isSafeHref(c.href)) {
|
|
119
|
+
if (import.meta.env?.DEV) console.warn(`[page:metadata] Rejected link contribution with unsafe href scheme: ${c.href}`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (c.rel === "canonical") {
|
|
123
|
+
if (seenLinks.has("canonical")) continue;
|
|
124
|
+
seenLinks.add("canonical");
|
|
125
|
+
} else {
|
|
126
|
+
const dedupeKey = c.key ?? c.hreflang ?? c.href;
|
|
127
|
+
if (seenLinks.has(dedupeKey)) continue;
|
|
128
|
+
seenLinks.add(dedupeKey);
|
|
129
|
+
}
|
|
130
|
+
result.links.push({
|
|
131
|
+
rel: c.rel,
|
|
132
|
+
href: c.href,
|
|
133
|
+
...c.hreflang && { hreflang: c.hreflang }
|
|
134
|
+
});
|
|
135
|
+
break;
|
|
136
|
+
case "jsonld":
|
|
137
|
+
if (c.id) {
|
|
138
|
+
if (seenJsonLd.has(c.id)) continue;
|
|
139
|
+
seenJsonLd.add(c.id);
|
|
140
|
+
}
|
|
141
|
+
result.jsonld.push({
|
|
142
|
+
id: c.id,
|
|
143
|
+
json: safeJsonLdSerialize(c.graph)
|
|
144
|
+
});
|
|
145
|
+
break;
|
|
146
|
+
default: break;
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
/** Render resolved metadata to an HTML string for embedding in <head> */
|
|
151
|
+
function renderPageMetadata(metadata) {
|
|
152
|
+
const parts = [];
|
|
153
|
+
for (const m of metadata.meta) parts.push(`<meta name="${escapeHtmlAttr(m.name)}" content="${escapeHtmlAttr(m.content)}">`);
|
|
154
|
+
for (const p of metadata.properties) parts.push(`<meta property="${escapeHtmlAttr(p.property)}" content="${escapeHtmlAttr(p.content)}">`);
|
|
155
|
+
for (const l of metadata.links) {
|
|
156
|
+
let tag = `<link rel="${escapeHtmlAttr(l.rel)}" href="${escapeHtmlAttr(l.href)}"`;
|
|
157
|
+
if (l.hreflang) tag += ` hreflang="${escapeHtmlAttr(l.hreflang)}"`;
|
|
158
|
+
tag += ">";
|
|
159
|
+
parts.push(tag);
|
|
160
|
+
}
|
|
161
|
+
for (const j of metadata.jsonld) parts.push(`<script type="application/ld+json">${j.json}<\/script>`);
|
|
162
|
+
return parts.join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/page/fragments.ts
|
|
167
|
+
/** Escape sequences that would break out of a script tag */
|
|
168
|
+
const SCRIPT_CLOSE_RE = /<\//g;
|
|
169
|
+
/**
|
|
170
|
+
* Filter contributions to a specific placement and deduplicate.
|
|
171
|
+
* - Contributions with the same `key + placement` are deduped (first wins).
|
|
172
|
+
* - External scripts with the same `src + placement` are deduped.
|
|
173
|
+
*/
|
|
174
|
+
function resolveFragments(contributions, placement) {
|
|
175
|
+
const filtered = contributions.filter((c) => c.placement === placement);
|
|
176
|
+
const seen = /* @__PURE__ */ new Set();
|
|
177
|
+
const result = [];
|
|
178
|
+
for (const c of filtered) {
|
|
179
|
+
if (c.key) {
|
|
180
|
+
const dedupeKey = `key:${c.key}`;
|
|
181
|
+
if (seen.has(dedupeKey)) continue;
|
|
182
|
+
seen.add(dedupeKey);
|
|
183
|
+
} else if (c.kind === "external-script") {
|
|
184
|
+
const dedupeKey = `src:${c.src}`;
|
|
185
|
+
if (seen.has(dedupeKey)) continue;
|
|
186
|
+
seen.add(dedupeKey);
|
|
187
|
+
}
|
|
188
|
+
result.push(c);
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
const EVENT_HANDLER_RE = /^on/i;
|
|
193
|
+
function renderAttributes(attrs) {
|
|
194
|
+
return Object.entries(attrs).filter(([k]) => !EVENT_HANDLER_RE.test(k)).map(([k, v]) => ` ${escapeHtmlAttr(k)}="${escapeHtmlAttr(v)}"`).join("");
|
|
195
|
+
}
|
|
196
|
+
/** Render a single fragment contribution to HTML */
|
|
197
|
+
function renderFragment(c) {
|
|
198
|
+
switch (c.kind) {
|
|
199
|
+
case "external-script": {
|
|
200
|
+
let tag = `<script src="${escapeHtmlAttr(c.src)}"`;
|
|
201
|
+
if (c.async) tag += " async";
|
|
202
|
+
if (c.defer) tag += " defer";
|
|
203
|
+
if (c.attributes) tag += renderAttributes(c.attributes);
|
|
204
|
+
tag += "><\/script>";
|
|
205
|
+
return tag;
|
|
206
|
+
}
|
|
207
|
+
case "inline-script": {
|
|
208
|
+
let tag = "<script";
|
|
209
|
+
if (c.attributes) tag += renderAttributes(c.attributes);
|
|
210
|
+
tag += `>${c.code.replace(SCRIPT_CLOSE_RE, "<\\/")}<\/script>`;
|
|
211
|
+
return tag;
|
|
212
|
+
}
|
|
213
|
+
case "html": return c.html;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Render a list of fragment contributions to an HTML string */
|
|
217
|
+
function renderFragments(contributions, placement) {
|
|
218
|
+
return resolveFragments(contributions, placement).map(renderFragment).join("\n");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/page/jsonld.ts
|
|
223
|
+
/**
|
|
224
|
+
* Remove null/undefined values from a JSON-LD object recursively.
|
|
225
|
+
* JSON-LD validators prefer absent keys over null values.
|
|
226
|
+
*/
|
|
227
|
+
function cleanJsonLd(obj) {
|
|
228
|
+
const cleaned = {};
|
|
229
|
+
for (const [key, value] of Object.entries(obj)) if (value !== void 0 && value !== null) if (typeof value === "object" && !Array.isArray(value)) cleaned[key] = cleanJsonLd(value);
|
|
230
|
+
else cleaned[key] = value;
|
|
231
|
+
return cleaned;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Build a BlogPosting JSON-LD graph from page context.
|
|
235
|
+
* Used for article-type content pages.
|
|
236
|
+
*/
|
|
237
|
+
function buildBlogPostingJsonLd(page) {
|
|
238
|
+
if (page.pageType !== "article" || !page.canonical) return null;
|
|
239
|
+
const ogTitle = page.seo?.ogTitle ?? page.pageTitle ?? page.title;
|
|
240
|
+
const description = page.seo?.ogDescription || page.description;
|
|
241
|
+
const ogImage = page.seo?.ogImage || page.image;
|
|
242
|
+
const publishedTime = page.articleMeta?.publishedTime;
|
|
243
|
+
const modifiedTime = page.articleMeta?.modifiedTime;
|
|
244
|
+
const author = page.articleMeta?.author;
|
|
245
|
+
const siteName = page.siteName;
|
|
246
|
+
return cleanJsonLd({
|
|
247
|
+
"@context": "https://schema.org",
|
|
248
|
+
"@type": "BlogPosting",
|
|
249
|
+
headline: ogTitle,
|
|
250
|
+
description,
|
|
251
|
+
image: ogImage || void 0,
|
|
252
|
+
url: page.canonical,
|
|
253
|
+
datePublished: publishedTime || void 0,
|
|
254
|
+
dateModified: modifiedTime || publishedTime || void 0,
|
|
255
|
+
author: author ? {
|
|
256
|
+
"@type": "Person",
|
|
257
|
+
name: author
|
|
258
|
+
} : void 0,
|
|
259
|
+
publisher: siteName ? {
|
|
260
|
+
"@type": "Organization",
|
|
261
|
+
name: siteName
|
|
262
|
+
} : void 0,
|
|
263
|
+
mainEntityOfPage: {
|
|
264
|
+
"@type": "WebPage",
|
|
265
|
+
"@id": page.canonical
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Build a WebSite JSON-LD graph from page context.
|
|
271
|
+
* Used for non-article pages (homepage, listing pages, etc.)
|
|
272
|
+
*/
|
|
273
|
+
function buildWebSiteJsonLd(page) {
|
|
274
|
+
const siteName = page.siteName;
|
|
275
|
+
if (!siteName) return null;
|
|
276
|
+
let siteUrl;
|
|
277
|
+
if (page.siteUrl) siteUrl = page.siteUrl;
|
|
278
|
+
else try {
|
|
279
|
+
siteUrl = new URL(page.url).origin;
|
|
280
|
+
} catch {
|
|
281
|
+
siteUrl = page.canonical || page.url;
|
|
282
|
+
}
|
|
283
|
+
return cleanJsonLd({
|
|
284
|
+
"@context": "https://schema.org",
|
|
285
|
+
"@type": "WebSite",
|
|
286
|
+
name: siteName,
|
|
287
|
+
url: siteUrl
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region src/page/seo-contributions.ts
|
|
293
|
+
/**
|
|
294
|
+
* Generate base metadata contributions from a page context's SEO data.
|
|
295
|
+
* Returns an empty array if no SEO-relevant data is present.
|
|
296
|
+
*/
|
|
297
|
+
function generateBaseSeoContributions(page) {
|
|
298
|
+
const contributions = [];
|
|
299
|
+
const description = page.description;
|
|
300
|
+
const ogTitle = page.seo?.ogTitle ?? page.pageTitle ?? page.title;
|
|
301
|
+
const ogDescription = page.seo?.ogDescription || description;
|
|
302
|
+
const ogImage = page.seo?.ogImage || page.image;
|
|
303
|
+
const robots = page.seo?.robots;
|
|
304
|
+
const canonical = page.canonical;
|
|
305
|
+
const siteName = page.siteName;
|
|
306
|
+
if (description) contributions.push({
|
|
307
|
+
kind: "meta",
|
|
308
|
+
name: "description",
|
|
309
|
+
content: description
|
|
310
|
+
});
|
|
311
|
+
if (robots) contributions.push({
|
|
312
|
+
kind: "meta",
|
|
313
|
+
name: "robots",
|
|
314
|
+
content: robots
|
|
315
|
+
});
|
|
316
|
+
if (canonical) contributions.push({
|
|
317
|
+
kind: "link",
|
|
318
|
+
rel: "canonical",
|
|
319
|
+
href: canonical
|
|
320
|
+
});
|
|
321
|
+
contributions.push({
|
|
322
|
+
kind: "property",
|
|
323
|
+
property: "og:type",
|
|
324
|
+
content: page.pageType === "article" ? "article" : "website"
|
|
325
|
+
});
|
|
326
|
+
if (ogTitle) contributions.push({
|
|
327
|
+
kind: "property",
|
|
328
|
+
property: "og:title",
|
|
329
|
+
content: ogTitle
|
|
330
|
+
});
|
|
331
|
+
if (ogDescription) contributions.push({
|
|
332
|
+
kind: "property",
|
|
333
|
+
property: "og:description",
|
|
334
|
+
content: ogDescription
|
|
335
|
+
});
|
|
336
|
+
if (ogImage) contributions.push({
|
|
337
|
+
kind: "property",
|
|
338
|
+
property: "og:image",
|
|
339
|
+
content: ogImage
|
|
340
|
+
});
|
|
341
|
+
if (canonical) contributions.push({
|
|
342
|
+
kind: "property",
|
|
343
|
+
property: "og:url",
|
|
344
|
+
content: canonical
|
|
345
|
+
});
|
|
346
|
+
if (siteName) contributions.push({
|
|
347
|
+
kind: "property",
|
|
348
|
+
property: "og:site_name",
|
|
349
|
+
content: siteName
|
|
350
|
+
});
|
|
351
|
+
contributions.push({
|
|
352
|
+
kind: "meta",
|
|
353
|
+
name: "twitter:card",
|
|
354
|
+
content: ogImage ? "summary_large_image" : "summary"
|
|
355
|
+
});
|
|
356
|
+
if (ogTitle) contributions.push({
|
|
357
|
+
kind: "meta",
|
|
358
|
+
name: "twitter:title",
|
|
359
|
+
content: ogTitle
|
|
360
|
+
});
|
|
361
|
+
if (ogDescription) contributions.push({
|
|
362
|
+
kind: "meta",
|
|
363
|
+
name: "twitter:description",
|
|
364
|
+
content: ogDescription
|
|
365
|
+
});
|
|
366
|
+
if (ogImage) contributions.push({
|
|
367
|
+
kind: "meta",
|
|
368
|
+
name: "twitter:image",
|
|
369
|
+
content: ogImage
|
|
370
|
+
});
|
|
371
|
+
if (page.pageType === "article" && page.articleMeta) {
|
|
372
|
+
const { publishedTime, modifiedTime, author } = page.articleMeta;
|
|
373
|
+
if (publishedTime) contributions.push({
|
|
374
|
+
kind: "property",
|
|
375
|
+
property: "article:published_time",
|
|
376
|
+
content: publishedTime
|
|
377
|
+
});
|
|
378
|
+
if (modifiedTime) contributions.push({
|
|
379
|
+
kind: "property",
|
|
380
|
+
property: "article:modified_time",
|
|
381
|
+
content: modifiedTime
|
|
382
|
+
});
|
|
383
|
+
if (author) contributions.push({
|
|
384
|
+
kind: "property",
|
|
385
|
+
property: "article:author",
|
|
386
|
+
content: author
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (page.pageType === "article") {
|
|
390
|
+
const blogPosting = buildBlogPostingJsonLd(page);
|
|
391
|
+
if (blogPosting) contributions.push({
|
|
392
|
+
kind: "jsonld",
|
|
393
|
+
id: "primary",
|
|
394
|
+
graph: blogPosting
|
|
395
|
+
});
|
|
396
|
+
} else if (siteName) {
|
|
397
|
+
const webSite = buildWebSiteJsonLd(page);
|
|
398
|
+
if (webSite) contributions.push({
|
|
399
|
+
kind: "jsonld",
|
|
400
|
+
id: "primary",
|
|
401
|
+
graph: webSite
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return contributions;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region src/page/index.ts
|
|
409
|
+
/**
|
|
410
|
+
* Get the page runtime from Astro locals. Returns undefined when
|
|
411
|
+
* Dineway is not initialized (components render nothing in that case).
|
|
412
|
+
*/
|
|
413
|
+
function getPageRuntime(locals) {
|
|
414
|
+
const dineway = locals.dineway;
|
|
415
|
+
if (dineway && typeof dineway === "object" && "collectPageMetadata" in dineway && "collectPageFragments" in dineway) return dineway;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
//#endregion
|
|
419
|
+
export { buildBlogPostingJsonLd, buildWebSiteJsonLd, cleanJsonLd, createPublicPageContext, escapeHtmlAttr, generateBaseSeoContributions, getPageRuntime, renderFragments, renderPageMetadata, resolveFragments, resolvePageMetadata, safeJsonLdSerialize };
|