@duffcloudservices/cms 0.10.0 → 0.11.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 +59 -2
- package/dist/chunk-DAYLLSEE.js +3 -0
- package/dist/{chunk-KCWMS7P4.js.map → chunk-DAYLLSEE.js.map} +1 -1
- package/dist/{chunk-UPAMLKOQ.js → chunk-F3EIWEZD.js} +158 -11
- package/dist/chunk-F3EIWEZD.js.map +1 -0
- package/dist/index.d.ts +74 -6
- package/dist/index.js +88 -6
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +91 -3
- package/dist/plugins/index.js +350 -82
- package/dist/plugins/index.js.map +1 -1
- package/dist/seo/index.d.ts +132 -226
- package/dist/seo/index.js +2 -2
- package/dist/spliceHeadHtml-CsBEucGy.d.ts +254 -0
- package/dist/{vitepressTransform-DeEzgGWU.d.ts → vitepressTransform-DfmABXmK.d.ts} +53 -2
- package/package.json +14 -2
- package/src/components/DcsCallButton.test.ts +126 -0
- package/src/components/DcsCallButton.vue +185 -0
- package/src/components/DcsReviewShowcase.vue +13 -2
- package/src/components/LiteMediaEmbed.test.ts +229 -0
- package/src/components/LiteMediaEmbed.vue +399 -0
- package/src/composables/useMediaCarousel.ts +6 -1
- package/src/composables/useReviewContent.test.ts +86 -0
- package/src/composables/useReviewContent.ts +34 -2
- package/src/composables/useSiteVisitorSession.test.ts +120 -0
- package/src/composables/useSiteVisitorSession.ts +160 -0
- package/dist/chunk-KCWMS7P4.js +0 -3
- package/dist/chunk-UPAMLKOQ.js.map +0 -1
package/dist/seo/index.d.ts
CHANGED
|
@@ -1,248 +1,154 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export { h as HeadLinkTag, H as HeadMetaTag, i as HeadScriptTag, j as HeadTagOverrides, R as ResolvedHeadTags, b as buildHeadTags, f as escapeJsonLd, g as generateJsonLd, a as generateOpenGraphMeta, c as generateTwitterMeta, d as renderHeadTags, r as resolvePageSeo, s as spliceHeadHtml, e as stripManagedHeadTags } from '../spliceHeadHtml-CsBEucGy.js';
|
|
2
|
+
import { Y as PageRouteEntry } from '../vitepressTransform-DfmABXmK.js';
|
|
3
|
+
export { A as AI_BOTS, y as BlogMeta, x as BreadcrumbCrumb, v as BuildLlmsParams, u as BuildRobotsParams, B as BuildSitemapParams, E as ContentConfig, C as CreateSeoTransformPageDataOptions, D as DcsRobotsOptions, F as FaqSource, a7 as NormalisedFaq, a6 as NormalisedReview, R as ResolvedPageOverrides, a8 as ReviewSchemaParts, z as ReviewSource, w as SchemaObject, S as SeoPageContext, r as SeoPageTypeRule, t as VitePressHeadConfig, V as VitePressPageData, q as absolutizeUrl, j as breadcrumbTrailFromRoute, k as buildBlogPosting, h as buildBreadcrumbList, l as buildFaqPage, g as buildGlobalGraph, $ as buildHasCredential, f as buildLlmsTxt, m as buildReviewSchemaParts, e as buildRobotsTxt, a as buildSitemapXml, b as buildVitePressSeoHead, c as createSeoTransformPageData, d as defaultRelativePathToRoute, a3 as deriveSameAs, o as filterRealFaq, n as filterRealReviews, a2 as findBusinessLicense, a0 as findLocalBusinessSchema, p as findReviewItemsForPage, a1 as graphAbsorbs, a4 as graphIds, a5 as isLocalBusinessType, i as isRouteIndexable, Z as loadPagesManifest, _ as parsePagesManifest, s as slugToTitle } from '../vitepressTransform-DfmABXmK.js';
|
|
3
4
|
import 'vue';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
7
|
+
* Build-time BODY prerender for the Vue-SPA static-HTML emitter.
|
|
8
|
+
*
|
|
9
|
+
* The companion to `emitStaticSeoHtml` (which bakes per-route `<head>` meta +
|
|
10
|
+
* JSON-LD). Today every SPA route's emitted `<body>` is literally
|
|
11
|
+
* `<div id="app"></div>` — allow-listed AI crawlers see the `@graph` + llms.txt
|
|
12
|
+
* but ZERO body prose (blog copy, service descriptions, GEO-citable content).
|
|
13
|
+
* This module fills the mount container with the REAL rendered DOM so a non-JS
|
|
14
|
+
* crawler sees the page's body content.
|
|
15
|
+
*
|
|
16
|
+
* ── Why a render pass, not a content.yaml dump ────────────────────────────────
|
|
17
|
+
* `.dcs/content.yaml` stores only visual-editor TEXT OVERRIDES — "Vue component
|
|
18
|
+
* `defaults` objects are the source of truth for baseline text" (its own header).
|
|
19
|
+
* Blog-post prose lives entirely in the site's `src/data/*.ts` and renders via
|
|
20
|
+
* `v-html`; it is NOT in content.yaml at all. So the only faithful, generic way
|
|
21
|
+
* to obtain a route's body is to RENDER the built app. This module is therefore
|
|
22
|
+
* renderer-agnostic: it takes a {@link RouteRenderer} (the production default is
|
|
23
|
+
* a headless-Playwright renderer of the just-built `dist/`; tests inject a fake)
|
|
24
|
+
* and is otherwise pure — deterministic, `fs`-only, and unit-testable.
|
|
25
|
+
*
|
|
26
|
+
* ── Fabrication safety (the A-34 scar) ────────────────────────────────────────
|
|
27
|
+
* A prior cms SSR path baked fabricated review defaults into crawler-visible
|
|
28
|
+
* HTML. The default renderer drives the PRODUCTION client bundle, where Vite has
|
|
29
|
+
* statically replaced `import.meta.env.DEV` with `false`, so the dev-only
|
|
30
|
+
* `defaults` honesty gate (`useReviewContent.ts`) resolves to the honest empty
|
|
31
|
+
* state — fabrication is structurally impossible, not merely avoided. Any route
|
|
32
|
+
* that instead CRASHES (uncaught exception, or the app never mounts) surfaces as
|
|
33
|
+
* a {@link BodyPrerenderError} and FAILS THE BUILD LOUD rather than shipping a
|
|
34
|
+
* broken/empty body.
|
|
28
35
|
*/
|
|
29
36
|
|
|
30
|
-
/** A `<meta>` tag — either a `name=`/`content=` or `property=`/`content=` pair. */
|
|
31
|
-
interface HeadMetaTag {
|
|
32
|
-
name?: string;
|
|
33
|
-
property?: string;
|
|
34
|
-
content: string;
|
|
35
|
-
}
|
|
36
|
-
/** A `<link>` tag (canonical, alternate, etc.). */
|
|
37
|
-
interface HeadLinkTag {
|
|
38
|
-
rel: string;
|
|
39
|
-
href: string;
|
|
40
|
-
hreflang?: string;
|
|
41
|
-
}
|
|
42
|
-
/** A `<script type="application/ld+json">` tag carrying serialised JSON-LD. */
|
|
43
|
-
interface HeadScriptTag {
|
|
44
|
-
type: string;
|
|
45
|
-
/** Pre-serialised JSON-LD string (already `JSON.stringify`-ed). */
|
|
46
|
-
children: string;
|
|
47
|
-
}
|
|
48
37
|
/**
|
|
49
|
-
*
|
|
38
|
+
* Normalise a route path to the on-disk output file the head emitter wrote:
|
|
39
|
+
* '/' -> '<outDir>/index.html'
|
|
40
|
+
* '/services' -> '<outDir>/services/index.html'
|
|
41
|
+
* '/blog/my-post' -> '<outDir>/blog/my-post/index.html'
|
|
50
42
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* - `link` covers canonical + hreflang alternates.
|
|
54
|
-
* - `script` covers JSON-LD (global + page schemas).
|
|
55
|
-
* - `jsonLd` is the same JSON-LD as parsed objects, for callers that want the
|
|
56
|
-
* structured form (e.g. `useSEO().getSchema()`).
|
|
43
|
+
* Lives here (the lower-level module) so both this pass and `dcsSeoPlugin`'s
|
|
44
|
+
* head emitter import it without a plugin↔module cycle.
|
|
57
45
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
jsonLd: object[];
|
|
64
|
-
/** The fully-resolved page SEO (merged global + page) used to build tags. */
|
|
65
|
-
resolved: ResolvedPageSeo;
|
|
66
|
-
}
|
|
67
|
-
/** Optional overrides applied on top of the resolved config when building tags. */
|
|
68
|
-
interface HeadTagOverrides {
|
|
69
|
-
/** Override the resolved `<title>`. */
|
|
70
|
-
title?: string;
|
|
71
|
-
/**
|
|
72
|
-
* Page-specific title fallback used when `seo.yaml` has no `title` for this
|
|
73
|
-
* page (e.g. the route `title` from `pages.yaml`). Unlike `global.defaultTitle`
|
|
74
|
-
* this IS run through `titleTemplate`, so un-configured routes (blog posts,
|
|
75
|
-
* etc.) get unique titles rather than the global default.
|
|
76
|
-
*/
|
|
77
|
-
fallbackTitle?: string;
|
|
78
|
-
/** Override the resolved meta description. */
|
|
79
|
-
description?: string;
|
|
80
|
-
/** Override the meta keywords value. */
|
|
81
|
-
keywords?: string;
|
|
82
|
-
/**
|
|
83
|
-
* Force the robots directive (e.g. `'noindex, nofollow'`). When supplied this
|
|
84
|
-
* wins over both page- and global-level robots.
|
|
85
|
-
*/
|
|
86
|
-
robots?: string;
|
|
87
|
-
/** Replace the JSON-LD schema objects entirely (already-built objects). */
|
|
88
|
-
schemas?: object[];
|
|
89
|
-
/** Extra meta tags appended after the generated ones. */
|
|
90
|
-
meta?: HeadMetaTag[];
|
|
91
|
-
/**
|
|
92
|
-
* Emit a `<meta name="keywords">` tag from the page's `keywords` field.
|
|
93
|
-
*
|
|
94
|
-
* Defaults to `false` so the `useSEO` runtime path stays byte-identical to
|
|
95
|
-
* its historical output (which never emitted keywords). The static-HTML
|
|
96
|
-
* emitter opts in (`true`) to surface page keywords in the baked `<head>`.
|
|
97
|
-
*/
|
|
98
|
-
includeKeywords?: boolean;
|
|
46
|
+
declare function routeToOutputFile(outDir: string, routePath: string): string;
|
|
47
|
+
/** The rendered body of one route plus any fatal render errors observed. */
|
|
48
|
+
interface RenderedRoute {
|
|
49
|
+
/** The `innerHTML` of the app mount container (`#app`) after the app rendered. */
|
|
50
|
+
bodyHtml: string;
|
|
99
51
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
52
|
+
* FATAL render errors (uncaught exceptions / failed mount) that mean the body
|
|
53
|
+
* is untrustworthy. A non-empty list makes `prerenderBodies` throw a
|
|
54
|
+
* {@link BodyPrerenderError} so the build fails loud instead of shipping wrong
|
|
55
|
+
* content. Benign offline noise (blocked external asset, expected API 401) is
|
|
56
|
+
* NOT reported here by the default renderer.
|
|
105
57
|
*/
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Ordered Home → … → current breadcrumb trail (absolute item URLs). When it
|
|
109
|
-
* has more than one hop a `BreadcrumbList` is emitted; the home page (≤ 1 hop)
|
|
110
|
-
* emits none.
|
|
111
|
-
*/
|
|
112
|
-
breadcrumbTrail?: BreadcrumbCrumb[];
|
|
113
|
-
/**
|
|
114
|
-
* REAL review items (from `.dcs/content.yaml`) for the LocalBusiness node.
|
|
115
|
-
* Honest Review[] + aggregateRating are emitted ONLY for items with a numeric
|
|
116
|
-
* rating + non-empty text + authorName; empty/missing ⇒ nothing.
|
|
117
|
-
*/
|
|
118
|
-
reviews?: ReviewSource[];
|
|
119
|
-
/**
|
|
120
|
-
* Structured FAQ pairs (frontmatter `faq:` / `.dcs/faq.yaml`). A `FAQPage` is
|
|
121
|
-
* emitted ONLY when at least one entry has a non-empty question AND answer.
|
|
122
|
-
*/
|
|
123
|
-
faq?: FaqSource[];
|
|
124
|
-
/**
|
|
125
|
-
* Blog-post metadata. When `headline` is present a `BlogPosting` is emitted —
|
|
126
|
-
* UNLESS the page's own schemas already hand-author a `BlogPosting` (then the
|
|
127
|
-
* builder defers to the authored copy to avoid duplication).
|
|
128
|
-
*/
|
|
129
|
-
blogMeta?: BlogMeta;
|
|
58
|
+
errors: string[];
|
|
130
59
|
}
|
|
131
60
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* `og:title` fallback so OG stays consistent with `<title>`.
|
|
136
|
-
*/
|
|
137
|
-
declare function generateOpenGraphMeta(og: SeoOpenGraphConfig, global: GlobalSeoConfig, resolvedTitle: string, pageDescription: string, canonical: string): Array<{
|
|
138
|
-
property: string;
|
|
139
|
-
content: string;
|
|
140
|
-
}>;
|
|
141
|
-
/**
|
|
142
|
-
* Generate Twitter Card meta tags from config.
|
|
143
|
-
*
|
|
144
|
-
* @param resolvedTitle - the final, template-applied page title (Twitter title
|
|
145
|
-
* fallback), mirroring the OG behaviour.
|
|
146
|
-
*/
|
|
147
|
-
declare function generateTwitterMeta(twitter: SeoTwitterConfig, global: GlobalSeoConfig, resolvedTitle: string, pageDescription: string): Array<{
|
|
148
|
-
name: string;
|
|
149
|
-
content: string;
|
|
150
|
-
}>;
|
|
151
|
-
/**
|
|
152
|
-
* Generate JSON-LD schema objects from schema configs, auto-populating common
|
|
153
|
-
* WebSite properties from global config.
|
|
61
|
+
* A build-time renderer that returns the rendered body for a route path. The
|
|
62
|
+
* production implementation ({@link createPlaywrightRenderer}) drives the built
|
|
63
|
+
* SPA in a headless browser; unit tests pass a deterministic fake.
|
|
154
64
|
*/
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
*
|
|
159
|
-
* Behavioural changes from the historical in-composable version (both bug
|
|
160
|
-
* fixes):
|
|
161
|
-
* 1. `openGraph.title` falls back to the **template-applied** page title, not
|
|
162
|
-
* the raw `page.title`, so `og:title` matches `<title>`.
|
|
163
|
-
* 2. The `titleTemplate` is applied **only** to a page-specific title
|
|
164
|
-
* (`page.title`, or the `fallbackTitle` arg). It is no longer applied to
|
|
165
|
-
* `global.defaultTitle`, which is already the complete brand title —
|
|
166
|
-
* templating it produced `"Brand | Default Title | Brand"` doubling on any
|
|
167
|
-
* page without its own `seo.yaml` entry.
|
|
168
|
-
*
|
|
169
|
-
* @param fallbackTitle - a page-specific title to use when `seo.yaml` has no
|
|
170
|
-
* `title` for this page (e.g. the route `title` from `pages.yaml`). It IS run
|
|
171
|
-
* through `titleTemplate`; `global.defaultTitle` is the last resort and is not.
|
|
172
|
-
*/
|
|
173
|
-
declare function resolvePageSeo(pageSlug: string, pagePath: string | undefined, seoConfig: SeoConfiguration | undefined, fallbackTitle?: string): ResolvedPageSeo;
|
|
174
|
-
/**
|
|
175
|
-
* Build the complete, framework-agnostic set of `<head>` tags for a page.
|
|
176
|
-
*
|
|
177
|
-
* This is the function both the `useSEO` runtime and the build-time emitter
|
|
178
|
-
* call, guaranteeing identical output. Pass `overrides` to mirror the
|
|
179
|
-
* composable's `applyHead(overrides)` behaviour, or to force `robots` (used by
|
|
180
|
-
* the emitter's `noindex` option).
|
|
181
|
-
*/
|
|
182
|
-
declare function buildHeadTags(pageSlug: string, pagePath: string | undefined, seoConfig: SeoConfiguration | undefined, overrides?: HeadTagOverrides): ResolvedHeadTags;
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Pure, framework-free `<head>` splicing for the build-time SEO emitter.
|
|
186
|
-
*
|
|
187
|
-
* Given a built `index.html` shell and a set of resolved head tags (from
|
|
188
|
-
* `buildHeadTags`), this produces a new HTML string where the SEO-managed
|
|
189
|
-
* tags — `<title>`, `description`, `keywords`, `robots`, `canonical`,
|
|
190
|
-
* verification, all `og:*` / `article:*` properties, all `twitter:*` names,
|
|
191
|
-
* and `application/ld+json` scripts — have been **replaced** (not duplicated)
|
|
192
|
-
* with the resolved set.
|
|
193
|
-
*
|
|
194
|
-
* Design goals:
|
|
195
|
-
* - **Idempotent**: running it twice yields the same output (it strips the
|
|
196
|
-
* managed tags first, then re-inserts the canonical set).
|
|
197
|
-
* - **Deterministic**: tag order is fixed by `renderHeadTags`.
|
|
198
|
-
* - **Conservative**: only tags we own are touched. Charset, viewport, CSP,
|
|
199
|
-
* theme-color, favicons, stylesheets, and the app script are left intact.
|
|
200
|
-
*
|
|
201
|
-
* This is intentionally regex-based (no DOM dependency) to mirror the existing
|
|
202
|
-
* `dcsCdnImagePlugin` post-build HTML rewriting and to keep the emitter free of
|
|
203
|
-
* heavy parser deps at build time.
|
|
204
|
-
*/
|
|
205
|
-
|
|
65
|
+
interface RouteRenderer {
|
|
66
|
+
renderRoute(routePath: string): Promise<RenderedRoute>;
|
|
67
|
+
}
|
|
206
68
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* text (review bodies, FAQ Q&A, blog descriptions) carrying `</script>` could
|
|
212
|
-
* break out of the script and inject markup (stored XSS).
|
|
213
|
-
*
|
|
214
|
-
* Per OWASP "JSON in an HTML context", we escape the three HTML-significant
|
|
215
|
-
* characters as their `\uXXXX` JSON escapes. Because these characters only ever
|
|
216
|
-
* appear INSIDE JSON string literals (never as JSON structure), the result is
|
|
217
|
-
* still byte-for-byte valid JSON that `JSON.parse` round-trips:
|
|
218
|
-
* `<` → `<` (defeats `</script>` and `<!--` breakout)
|
|
219
|
-
* `>` → `>`
|
|
220
|
-
* `&` → `&`
|
|
221
|
-
*
|
|
222
|
-
* Exported so BOTH static-SEO sinks (the SPA `spliceHeadHtml` path AND the
|
|
223
|
-
* VitePress `transformPageData` head emit) share one guard.
|
|
69
|
+
* Thrown when a route's render produced FATAL errors (a crash or a mount
|
|
70
|
+
* failure). Deliberately escapes the plugin's never-break-the-build try/catch so
|
|
71
|
+
* a component that would crash or emit wrong content FAILS THE BUILD (hard
|
|
72
|
+
* requirement: never ship fabricated/broken body content).
|
|
224
73
|
*/
|
|
225
|
-
declare
|
|
74
|
+
declare class BodyPrerenderError extends Error {
|
|
75
|
+
readonly route: string;
|
|
76
|
+
readonly errors: string[];
|
|
77
|
+
constructor(route: string, errors: string[]);
|
|
78
|
+
}
|
|
226
79
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
80
|
+
* Replace the inner content of the FIRST `<div id="app">…</div>` mount container
|
|
81
|
+
* with `bodyInnerHtml`, leaving `<head>`, the app `<script>`, and everything
|
|
82
|
+
* else untouched. Free of a DOM dependency (mirrors `spliceHeadHtml`), and
|
|
83
|
+
* robust to nested `<div>`s via depth counting so it also works when re-run over
|
|
84
|
+
* an already-filled container (idempotent given a deterministic renderer).
|
|
85
|
+
*
|
|
86
|
+
* Returns the HTML unchanged when there is no `#app` container (defensive — the
|
|
87
|
+
* caller logs + skips), so a non-SPA shell can never be corrupted.
|
|
229
88
|
*/
|
|
230
|
-
declare function
|
|
89
|
+
declare function spliceBodyHtml(html: string, bodyInnerHtml: string): string;
|
|
90
|
+
/** Inputs for {@link prerenderBodies}. */
|
|
91
|
+
interface PrerenderBodiesParams {
|
|
92
|
+
/** Absolute path to the built SPA output directory (`dist`). */
|
|
93
|
+
outDir: string;
|
|
94
|
+
/** The SAME route set the head emitter used (from `.dcs/pages.yaml`). */
|
|
95
|
+
routes: PageRouteEntry[];
|
|
96
|
+
/** The renderer that returns each route's rendered body. */
|
|
97
|
+
renderer: RouteRenderer;
|
|
98
|
+
/** Routes to skip entirely — matched against route `path` OR `slug`. */
|
|
99
|
+
exclude?: string[];
|
|
100
|
+
/**
|
|
101
|
+
* Routes that received `robots: noindex,nofollow` (auth-gated / private). These
|
|
102
|
+
* are NOT body-prerendered — a noindex/auth route must never leak rendered body
|
|
103
|
+
* content into `dist/` (hard requirement). Matched against `path` OR `slug`.
|
|
104
|
+
*/
|
|
105
|
+
noindex?: string[];
|
|
106
|
+
/** `pages.yaml` top-level `excluded:` globs — matching routes are skipped. */
|
|
107
|
+
excludedGlobs?: string[];
|
|
108
|
+
debug?: boolean;
|
|
109
|
+
}
|
|
110
|
+
/** Outcome of a body-prerender pass. */
|
|
111
|
+
interface PrerenderBodiesResult {
|
|
112
|
+
/** Number of per-route files whose body was filled. */
|
|
113
|
+
prerendered: number;
|
|
114
|
+
/** Number of routes skipped (excluded / noindex / missing head file / empty). */
|
|
115
|
+
skipped: number;
|
|
116
|
+
/** The route paths that were prerendered, in order (for logging/tests). */
|
|
117
|
+
routes: string[];
|
|
118
|
+
}
|
|
231
119
|
/**
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
120
|
+
* For each emitted route: render its body and splice it into the per-route
|
|
121
|
+
* `dist/<path>/index.html` the head emitter already wrote. Reuses the exact same
|
|
122
|
+
* exclude/excluded-glob filters as `emitStaticSeoHtml`, and ADDITIONALLY skips
|
|
123
|
+
* every `noindex` route (auth-gated/private pages get a head-only file, never a
|
|
124
|
+
* prerendered body).
|
|
125
|
+
*
|
|
126
|
+
* FAILS LOUD (throws {@link BodyPrerenderError}) when a route's render reports
|
|
127
|
+
* fatal errors — better a red build than fabricated/broken crawler content.
|
|
128
|
+
* Pure aside from `fs` reads/writes; the browser lives behind `renderer`.
|
|
235
129
|
*/
|
|
236
|
-
declare function
|
|
130
|
+
declare function prerenderBodies(params: PrerenderBodiesParams): Promise<PrerenderBodiesResult>;
|
|
131
|
+
/** Inputs for {@link isBodyPrerenderEnabled}. */
|
|
132
|
+
interface BodyPrerenderEnableInput {
|
|
133
|
+
/** Whether the per-route static-HTML emitter is on (body prerender rides it). */
|
|
134
|
+
emitStaticHtml: boolean;
|
|
135
|
+
/** The plugin option `prerenderBody` (undefined ⇒ default ON when emitting). */
|
|
136
|
+
prerenderBodyOption?: boolean;
|
|
137
|
+
/** Preview/staging build — body prerender is skipped (kept minimal + fast). */
|
|
138
|
+
preview?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* The per-site escape hatch read from `.dcs/seo.yaml` (`prerenderBody: false`).
|
|
141
|
+
* Lets an operator disable body prerender for one site with NO code change and
|
|
142
|
+
* NO cms republish — the portal owns seo.yaml. `undefined`/`true` ⇒ enabled.
|
|
143
|
+
*/
|
|
144
|
+
seoPrerenderBody?: boolean;
|
|
145
|
+
}
|
|
237
146
|
/**
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
* unchanged (defensive — the emitter logs and no-ops in that case).
|
|
243
|
-
*
|
|
244
|
-
* Idempotent: applying twice produces identical output.
|
|
147
|
+
* Resolve whether body prerender runs for this build. ON by default whenever the
|
|
148
|
+
* static-HTML emitter is on; turned OFF by the plugin option `prerenderBody:
|
|
149
|
+
* false`, by the seo.yaml `prerenderBody: false` escape hatch, in preview mode,
|
|
150
|
+
* or when the emitter itself is off. Either OFF signal wins.
|
|
245
151
|
*/
|
|
246
|
-
declare function
|
|
152
|
+
declare function isBodyPrerenderEnabled(input: BodyPrerenderEnableInput): boolean;
|
|
247
153
|
|
|
248
|
-
export {
|
|
154
|
+
export { type BodyPrerenderEnableInput, BodyPrerenderError, PageRouteEntry, type PrerenderBodiesParams, type PrerenderBodiesResult, type RenderedRoute, type RouteRenderer, isBodyPrerenderEnabled, prerenderBodies, routeToOutputFile, spliceBodyHtml };
|
package/dist/seo/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import '../chunk-
|
|
2
|
-
export { AI_BOTS, absolutizeUrl, breadcrumbTrailFromRoute, buildBlogPosting, buildBreadcrumbList, buildFaqPage, buildGlobalGraph, buildHeadTags, buildLlmsTxt, buildReviewSchemaParts, buildRobotsTxt, buildSitemapXml, buildVitePressSeoHead, createSeoTransformPageData, defaultRelativePathToRoute, deriveSameAs, escapeJsonLd, filterRealFaq, filterRealReviews, findLocalBusinessSchema, findReviewItemsForPage, generateJsonLd, generateOpenGraphMeta, generateTwitterMeta, graphAbsorbs, graphIds, isLocalBusinessType, isRouteIndexable, loadPagesManifest, parsePagesManifest, renderHeadTags, resolvePageSeo, slugToTitle, spliceHeadHtml, stripManagedHeadTags } from '../chunk-
|
|
1
|
+
import '../chunk-DAYLLSEE.js';
|
|
2
|
+
export { AI_BOTS, BodyPrerenderError, absolutizeUrl, breadcrumbTrailFromRoute, buildBlogPosting, buildBreadcrumbList, buildFaqPage, buildGlobalGraph, buildHasCredential, buildHeadTags, buildLlmsTxt, buildReviewSchemaParts, buildRobotsTxt, buildSitemapXml, buildVitePressSeoHead, createSeoTransformPageData, defaultRelativePathToRoute, deriveSameAs, escapeJsonLd, filterRealFaq, filterRealReviews, findBusinessLicense, findLocalBusinessSchema, findReviewItemsForPage, generateJsonLd, generateOpenGraphMeta, generateTwitterMeta, graphAbsorbs, graphIds, isBodyPrerenderEnabled, isLocalBusinessType, isRouteIndexable, loadPagesManifest, parsePagesManifest, prerenderBodies, renderHeadTags, resolvePageSeo, routeToOutputFile, slugToTitle, spliceBodyHtml, spliceHeadHtml, stripManagedHeadTags } from '../chunk-F3EIWEZD.js';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { H as SeoConfiguration, x as BreadcrumbCrumb, z as ReviewSource, F as FaqSource, y as BlogMeta, T as ResolvedPageSeo, N as SeoSchemaConfig, G as GlobalSeoConfig, L as SeoOpenGraphConfig, M as SeoTwitterConfig } from './vitepressTransform-DfmABXmK.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Framework-agnostic SEO head-tag resolution.
|
|
5
|
+
*
|
|
6
|
+
* This module is the single source of truth for turning a
|
|
7
|
+
* (`pageSlug`, `pagePath`, `SeoConfiguration`) triple into a plain,
|
|
8
|
+
* serialisable description of the `<head>` tags a page should carry:
|
|
9
|
+
* resolved title, meta[], link[], and JSON-LD script[].
|
|
10
|
+
*
|
|
11
|
+
* It is consumed by:
|
|
12
|
+
* - `useSEO` (runtime, via `@unhead/vue`) — see `../composables/useSEO.ts`
|
|
13
|
+
* - `dcsSeoPlugin`'s build-time static-HTML emitter — see
|
|
14
|
+
* `../plugins/dcsSeoPlugin.ts`
|
|
15
|
+
*
|
|
16
|
+
* Keeping the resolution here (rather than inside the Vue composable) means
|
|
17
|
+
* the runtime and the build-time emitter produce byte-identical tags from the
|
|
18
|
+
* same `seo.yaml`, with no Vue/unhead dependency required at build time.
|
|
19
|
+
*
|
|
20
|
+
* Runtime behaviour is intentionally identical to the previous in-composable
|
|
21
|
+
* logic **except** for one corrected bug: `og:title` now falls back to the
|
|
22
|
+
* fully-resolved (template-applied) page title instead of the raw, untemplated
|
|
23
|
+
* `page.title`. Previously `og:title` could diverge from the `<title>` element
|
|
24
|
+
* (e.g. `<title>Iron Oak Contractors | Our Services</title>` but
|
|
25
|
+
* `og:title = "Our Services"`).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** A `<meta>` tag — either a `name=`/`content=` or `property=`/`content=` pair. */
|
|
29
|
+
interface HeadMetaTag {
|
|
30
|
+
name?: string;
|
|
31
|
+
property?: string;
|
|
32
|
+
content: string;
|
|
33
|
+
}
|
|
34
|
+
/** A `<link>` tag (canonical, alternate, etc.). */
|
|
35
|
+
interface HeadLinkTag {
|
|
36
|
+
rel: string;
|
|
37
|
+
href: string;
|
|
38
|
+
hreflang?: string;
|
|
39
|
+
}
|
|
40
|
+
/** A `<script type="application/ld+json">` tag carrying serialised JSON-LD. */
|
|
41
|
+
interface HeadScriptTag {
|
|
42
|
+
type: string;
|
|
43
|
+
/** Pre-serialised JSON-LD string (already `JSON.stringify`-ed). */
|
|
44
|
+
children: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The complete, framework-agnostic set of resolved `<head>` tags for a page.
|
|
48
|
+
*
|
|
49
|
+
* - `title` is the final, template-applied title (what goes in `<title>`).
|
|
50
|
+
* - `meta` covers description, keywords, robots, verification, OG, Twitter.
|
|
51
|
+
* - `link` covers canonical + hreflang alternates.
|
|
52
|
+
* - `script` covers JSON-LD (global + page schemas).
|
|
53
|
+
* - `jsonLd` is the same JSON-LD as parsed objects, for callers that want the
|
|
54
|
+
* structured form (e.g. `useSEO().getSchema()`).
|
|
55
|
+
*/
|
|
56
|
+
interface ResolvedHeadTags {
|
|
57
|
+
title: string;
|
|
58
|
+
meta: HeadMetaTag[];
|
|
59
|
+
link: HeadLinkTag[];
|
|
60
|
+
script: HeadScriptTag[];
|
|
61
|
+
jsonLd: object[];
|
|
62
|
+
/** The fully-resolved page SEO (merged global + page) used to build tags. */
|
|
63
|
+
resolved: ResolvedPageSeo;
|
|
64
|
+
}
|
|
65
|
+
/** Optional overrides applied on top of the resolved config when building tags. */
|
|
66
|
+
interface HeadTagOverrides {
|
|
67
|
+
/** Override the resolved `<title>`. */
|
|
68
|
+
title?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Page-specific title fallback used when `seo.yaml` has no `title` for this
|
|
71
|
+
* page (e.g. the route `title` from `pages.yaml`). Unlike `global.defaultTitle`
|
|
72
|
+
* this IS run through `titleTemplate`, so un-configured routes (blog posts,
|
|
73
|
+
* etc.) get unique titles rather than the global default.
|
|
74
|
+
*/
|
|
75
|
+
fallbackTitle?: string;
|
|
76
|
+
/** Override the resolved meta description. */
|
|
77
|
+
description?: string;
|
|
78
|
+
/** Override the meta keywords value. */
|
|
79
|
+
keywords?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Force the robots directive (e.g. `'noindex, nofollow'`). When supplied this
|
|
82
|
+
* wins over both page- and global-level robots.
|
|
83
|
+
*/
|
|
84
|
+
robots?: string;
|
|
85
|
+
/** Replace the JSON-LD schema objects entirely (already-built objects). */
|
|
86
|
+
schemas?: object[];
|
|
87
|
+
/** Extra meta tags appended after the generated ones. */
|
|
88
|
+
meta?: HeadMetaTag[];
|
|
89
|
+
/**
|
|
90
|
+
* Emit a `<meta name="keywords">` tag from the page's `keywords` field.
|
|
91
|
+
*
|
|
92
|
+
* Defaults to `false` so the `useSEO` runtime path stays byte-identical to
|
|
93
|
+
* its historical output (which never emitted keywords). The static-HTML
|
|
94
|
+
* emitter opts in (`true`) to surface page keywords in the baked `<head>`.
|
|
95
|
+
*/
|
|
96
|
+
includeKeywords?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Emit the global `@graph` spine (Organization + WebSite + the promoted
|
|
99
|
+
* LocalBusiness node, cross-linked by `@id`) PREPENDED before the per-schema
|
|
100
|
+
* JSON-LD. When true, the LocalBusiness subtype already present in
|
|
101
|
+
* `global.schemas` is ABSORBED into the graph (not emitted a second time).
|
|
102
|
+
* Default `false`.
|
|
103
|
+
*/
|
|
104
|
+
emitGraph?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Ordered Home → … → current breadcrumb trail (absolute item URLs). When it
|
|
107
|
+
* has more than one hop a `BreadcrumbList` is emitted; the home page (≤ 1 hop)
|
|
108
|
+
* emits none.
|
|
109
|
+
*/
|
|
110
|
+
breadcrumbTrail?: BreadcrumbCrumb[];
|
|
111
|
+
/**
|
|
112
|
+
* REAL review items (from `.dcs/content.yaml`) for the LocalBusiness node.
|
|
113
|
+
* Honest Review[] + aggregateRating are emitted ONLY for items with a numeric
|
|
114
|
+
* rating + non-empty text + authorName; empty/missing ⇒ nothing.
|
|
115
|
+
*/
|
|
116
|
+
reviews?: ReviewSource[];
|
|
117
|
+
/**
|
|
118
|
+
* Free-text trade/occupational license (from the `business.license`
|
|
119
|
+
* content.yaml key). When non-empty a `hasCredential`
|
|
120
|
+
* (`EducationalOccupationalCredential`, `credentialCategory: "license"`) is
|
|
121
|
+
* added to the LocalBusiness node in the `@graph`; empty/missing ⇒ nothing.
|
|
122
|
+
* Only consulted when `emitGraph` is true.
|
|
123
|
+
*/
|
|
124
|
+
license?: string;
|
|
125
|
+
/**
|
|
126
|
+
* Structured FAQ pairs (frontmatter `faq:` / `.dcs/faq.yaml`). A `FAQPage` is
|
|
127
|
+
* emitted ONLY when at least one entry has a non-empty question AND answer.
|
|
128
|
+
*/
|
|
129
|
+
faq?: FaqSource[];
|
|
130
|
+
/**
|
|
131
|
+
* Blog-post metadata. When `headline` is present a `BlogPosting` is emitted —
|
|
132
|
+
* UNLESS the page's own schemas already hand-author a `BlogPosting` (then the
|
|
133
|
+
* builder defers to the authored copy to avoid duplication).
|
|
134
|
+
*/
|
|
135
|
+
blogMeta?: BlogMeta;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Generate Open Graph meta tags from config.
|
|
139
|
+
*
|
|
140
|
+
* @param resolvedTitle - the final, template-applied page title. Used as the
|
|
141
|
+
* `og:title` fallback so OG stays consistent with `<title>`.
|
|
142
|
+
*/
|
|
143
|
+
declare function generateOpenGraphMeta(og: SeoOpenGraphConfig, global: GlobalSeoConfig, resolvedTitle: string, pageDescription: string, canonical: string): Array<{
|
|
144
|
+
property: string;
|
|
145
|
+
content: string;
|
|
146
|
+
}>;
|
|
147
|
+
/**
|
|
148
|
+
* Generate Twitter Card meta tags from config.
|
|
149
|
+
*
|
|
150
|
+
* @param resolvedTitle - the final, template-applied page title (Twitter title
|
|
151
|
+
* fallback), mirroring the OG behaviour.
|
|
152
|
+
*/
|
|
153
|
+
declare function generateTwitterMeta(twitter: SeoTwitterConfig, global: GlobalSeoConfig, resolvedTitle: string, pageDescription: string): Array<{
|
|
154
|
+
name: string;
|
|
155
|
+
content: string;
|
|
156
|
+
}>;
|
|
157
|
+
/**
|
|
158
|
+
* Generate JSON-LD schema objects from schema configs, auto-populating common
|
|
159
|
+
* WebSite properties from global config.
|
|
160
|
+
*/
|
|
161
|
+
declare function generateJsonLd(schemas: SeoSchemaConfig[], global: GlobalSeoConfig): object[];
|
|
162
|
+
/**
|
|
163
|
+
* Resolve page SEO by merging global defaults with page-specific config.
|
|
164
|
+
*
|
|
165
|
+
* Behavioural changes from the historical in-composable version (both bug
|
|
166
|
+
* fixes):
|
|
167
|
+
* 1. `openGraph.title` falls back to the **template-applied** page title, not
|
|
168
|
+
* the raw `page.title`, so `og:title` matches `<title>`.
|
|
169
|
+
* 2. The `titleTemplate` is applied **only** to a page-specific title
|
|
170
|
+
* (`page.title`, or the `fallbackTitle` arg). It is no longer applied to
|
|
171
|
+
* `global.defaultTitle`, which is already the complete brand title —
|
|
172
|
+
* templating it produced `"Brand | Default Title | Brand"` doubling on any
|
|
173
|
+
* page without its own `seo.yaml` entry.
|
|
174
|
+
*
|
|
175
|
+
* @param fallbackTitle - a page-specific title to use when `seo.yaml` has no
|
|
176
|
+
* `title` for this page (e.g. the route `title` from `pages.yaml`). It IS run
|
|
177
|
+
* through `titleTemplate`; `global.defaultTitle` is the last resort and is not.
|
|
178
|
+
*/
|
|
179
|
+
declare function resolvePageSeo(pageSlug: string, pagePath: string | undefined, seoConfig: SeoConfiguration | undefined, fallbackTitle?: string): ResolvedPageSeo;
|
|
180
|
+
/**
|
|
181
|
+
* Build the complete, framework-agnostic set of `<head>` tags for a page.
|
|
182
|
+
*
|
|
183
|
+
* This is the function both the `useSEO` runtime and the build-time emitter
|
|
184
|
+
* call, guaranteeing identical output. Pass `overrides` to mirror the
|
|
185
|
+
* composable's `applyHead(overrides)` behaviour, or to force `robots` (used by
|
|
186
|
+
* the emitter's `noindex` option).
|
|
187
|
+
*/
|
|
188
|
+
declare function buildHeadTags(pageSlug: string, pagePath: string | undefined, seoConfig: SeoConfiguration | undefined, overrides?: HeadTagOverrides): ResolvedHeadTags;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Pure, framework-free `<head>` splicing for the build-time SEO emitter.
|
|
192
|
+
*
|
|
193
|
+
* Given a built `index.html` shell and a set of resolved head tags (from
|
|
194
|
+
* `buildHeadTags`), this produces a new HTML string where the SEO-managed
|
|
195
|
+
* tags — `<title>`, `description`, `keywords`, `robots`, `canonical`,
|
|
196
|
+
* verification, all `og:*` / `article:*` properties, all `twitter:*` names,
|
|
197
|
+
* and `application/ld+json` scripts — have been **replaced** (not duplicated)
|
|
198
|
+
* with the resolved set.
|
|
199
|
+
*
|
|
200
|
+
* Design goals:
|
|
201
|
+
* - **Idempotent**: running it twice yields the same output (it strips the
|
|
202
|
+
* managed tags first, then re-inserts the canonical set).
|
|
203
|
+
* - **Deterministic**: tag order is fixed by `renderHeadTags`.
|
|
204
|
+
* - **Conservative**: only tags we own are touched. Charset, viewport, CSP,
|
|
205
|
+
* theme-color, favicons, stylesheets, and the app script are left intact.
|
|
206
|
+
*
|
|
207
|
+
* This is intentionally regex-based (no DOM dependency) to mirror the existing
|
|
208
|
+
* `dcsCdnImagePlugin` post-build HTML rewriting and to keep the emitter free of
|
|
209
|
+
* heavy parser deps at build time.
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Escape a JSON-LD string for safe inclusion inside an HTML `<script>` element.
|
|
214
|
+
*
|
|
215
|
+
* A `<script type="application/ld+json">` is a DATA block, but the HTML parser
|
|
216
|
+
* still scans its raw text for `</script` (and `<!--`) — so untrusted CMS free
|
|
217
|
+
* text (review bodies, FAQ Q&A, blog descriptions) carrying `</script>` could
|
|
218
|
+
* break out of the script and inject markup (stored XSS).
|
|
219
|
+
*
|
|
220
|
+
* Per OWASP "JSON in an HTML context", we escape the three HTML-significant
|
|
221
|
+
* characters as their `\uXXXX` JSON escapes. Because these characters only ever
|
|
222
|
+
* appear INSIDE JSON string literals (never as JSON structure), the result is
|
|
223
|
+
* still byte-for-byte valid JSON that `JSON.parse` round-trips:
|
|
224
|
+
* `<` → `<` (defeats `</script>` and `<!--` breakout)
|
|
225
|
+
* `>` → `>`
|
|
226
|
+
* `&` → `&`
|
|
227
|
+
*
|
|
228
|
+
* Exported so BOTH static-SEO sinks (the SPA `spliceHeadHtml` path AND the
|
|
229
|
+
* VitePress `transformPageData` head emit) share one guard.
|
|
230
|
+
*/
|
|
231
|
+
declare function escapeJsonLd(json: string): string;
|
|
232
|
+
/**
|
|
233
|
+
* Render the resolved head tags to a deterministic HTML fragment.
|
|
234
|
+
* Order: title, meta (in the order produced by buildHeadTags), link, script.
|
|
235
|
+
*/
|
|
236
|
+
declare function renderHeadTags(tags: ResolvedHeadTags, indent?: string): string;
|
|
237
|
+
/**
|
|
238
|
+
* Strip the SEO-managed tags from a `<head>` block so they can be re-inserted
|
|
239
|
+
* without duplication. Operates only within `<head>...</head>` to avoid
|
|
240
|
+
* touching body content.
|
|
241
|
+
*/
|
|
242
|
+
declare function stripManagedHeadTags(html: string): string;
|
|
243
|
+
/**
|
|
244
|
+
* Splice resolved SEO head tags into an HTML document.
|
|
245
|
+
*
|
|
246
|
+
* Strips the existing managed tags, then inserts the rendered canonical set
|
|
247
|
+
* immediately before `</head>`. If no `<head>` is present the HTML is returned
|
|
248
|
+
* unchanged (defensive — the emitter logs and no-ops in that case).
|
|
249
|
+
*
|
|
250
|
+
* Idempotent: applying twice produces identical output.
|
|
251
|
+
*/
|
|
252
|
+
declare function spliceHeadHtml(html: string, tags: ResolvedHeadTags): string;
|
|
253
|
+
|
|
254
|
+
export { type HeadMetaTag as H, type ResolvedHeadTags as R, generateOpenGraphMeta as a, buildHeadTags as b, generateTwitterMeta as c, renderHeadTags as d, stripManagedHeadTags as e, escapeJsonLd as f, generateJsonLd as g, type HeadLinkTag as h, type HeadScriptTag as i, type HeadTagOverrides as j, resolvePageSeo as r, spliceHeadHtml as s };
|