@decocms/start 0.25.0 → 0.25.2
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/package.json +1 -1
- package/src/cms/index.ts +1 -0
- package/src/cms/loader.ts +25 -0
- package/src/routes/cmsRoute.ts +39 -8
- package/src/sdk/workerEntry.ts +23 -0
package/package.json
CHANGED
package/src/cms/index.ts
CHANGED
package/src/cms/loader.ts
CHANGED
|
@@ -161,6 +161,31 @@ function matchPath(pattern: string, urlPath: string): Record<string, string> | n
|
|
|
161
161
|
return params;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Extract the site-wide SEO config from the "Site" app block.
|
|
166
|
+
*
|
|
167
|
+
* In the original deco-cx/deco framework this is `ctx.seo` — the app-level
|
|
168
|
+
* SEO configuration that provides fallback title, description, and templates
|
|
169
|
+
* when page-level seo blocks don't supply them.
|
|
170
|
+
*/
|
|
171
|
+
export function getSiteSeo(): {
|
|
172
|
+
title?: string;
|
|
173
|
+
description?: string;
|
|
174
|
+
titleTemplate?: string;
|
|
175
|
+
descriptionTemplate?: string;
|
|
176
|
+
image?: string;
|
|
177
|
+
favicon?: string;
|
|
178
|
+
themeColor?: string;
|
|
179
|
+
noIndexing?: boolean;
|
|
180
|
+
} {
|
|
181
|
+
const blocks = loadBlocks();
|
|
182
|
+
const site = blocks["Site"] as Record<string, unknown> | undefined;
|
|
183
|
+
if (!site) return {};
|
|
184
|
+
const seo = site.seo as Record<string, unknown> | undefined;
|
|
185
|
+
if (!seo) return {};
|
|
186
|
+
return seo as ReturnType<typeof getSiteSeo>;
|
|
187
|
+
}
|
|
188
|
+
|
|
164
189
|
export function findPageByPath(
|
|
165
190
|
targetPath: string,
|
|
166
191
|
): { page: DecoPage; params: Record<string, string> } | null {
|
package/src/routes/cmsRoute.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
resolveDecoPage,
|
|
38
38
|
resolveDeferredSection,
|
|
39
39
|
} from "../cms/resolve";
|
|
40
|
+
import { getSiteSeo } from "../cms/loader";
|
|
40
41
|
import { runSectionLoaders, runSingleSectionLoader } from "../cms/sectionLoaders";
|
|
41
42
|
import {
|
|
42
43
|
type CacheProfile,
|
|
@@ -269,7 +270,19 @@ async function buildPageSeo(
|
|
|
269
270
|
// Secondary source: SEO sections embedded in the sections array
|
|
270
271
|
const sectionSeo = extractSeoFromSections(enrichedSections);
|
|
271
272
|
|
|
272
|
-
|
|
273
|
+
// Site-wide SEO config from the "Site" app block — mirrors ctx.seo in
|
|
274
|
+
// the original deco-cx/deco framework. Provides fallback title,
|
|
275
|
+
// description, and templates when page-level seo doesn't supply them.
|
|
276
|
+
const siteSeo = getSiteSeo();
|
|
277
|
+
|
|
278
|
+
if (!seoSection) {
|
|
279
|
+
// No page.seo block — use site-wide SEO as primary, section-contributed as secondary
|
|
280
|
+
const merged: PageSeo = { ...sectionSeo };
|
|
281
|
+
if (siteSeo.title && !merged.title) merged.title = siteSeo.title;
|
|
282
|
+
if (siteSeo.description && !merged.description) merged.description = siteSeo.description;
|
|
283
|
+
if (siteSeo.image && !merged.image) merged.image = siteSeo.image;
|
|
284
|
+
return merged;
|
|
285
|
+
}
|
|
273
286
|
|
|
274
287
|
// Run the section loader on the seo section if one is registered
|
|
275
288
|
// (e.g., SEOPDP loader transforms {jsonLD: ProductDetailsPage} → {title, description, ...})
|
|
@@ -283,16 +296,28 @@ async function buildPageSeo(
|
|
|
283
296
|
|
|
284
297
|
const pageSeo = extractSeoFromProps(enrichedProps);
|
|
285
298
|
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
299
|
+
// Replicate original SeoV2 loader logic: `_title ?? appTitle`
|
|
300
|
+
// When the page's seo block doesn't have a title/description,
|
|
301
|
+
// fall back to the Site app's seo config.
|
|
302
|
+
if (!pageSeo.title && siteSeo.title) pageSeo.title = siteSeo.title;
|
|
303
|
+
if (!pageSeo.description && siteSeo.description) pageSeo.description = siteSeo.description;
|
|
304
|
+
if (!pageSeo.image && siteSeo.image) pageSeo.image = siteSeo.image;
|
|
305
|
+
|
|
306
|
+
// Apply title/description templates.
|
|
307
|
+
// Priority: page-level template → site-level template → no-op.
|
|
308
|
+
// This mirrors the original: `(titleTemplate ?? "").trim().length === 0 ? "%s" : titleTemplate`
|
|
289
309
|
const rawProps = seoSection.props;
|
|
290
|
-
const titleTemplate =
|
|
291
|
-
|
|
292
|
-
|
|
310
|
+
const titleTemplate =
|
|
311
|
+
effectiveTemplate(rawProps.titleTemplate as string | undefined) ??
|
|
312
|
+
effectiveTemplate(siteSeo.titleTemplate);
|
|
313
|
+
const descTemplate =
|
|
314
|
+
effectiveTemplate(rawProps.descriptionTemplate as string | undefined) ??
|
|
315
|
+
effectiveTemplate(siteSeo.descriptionTemplate);
|
|
316
|
+
|
|
317
|
+
if (titleTemplate && pageSeo.title) {
|
|
293
318
|
pageSeo.title = titleTemplate.replace("%s", pageSeo.title);
|
|
294
319
|
}
|
|
295
|
-
if (descTemplate &&
|
|
320
|
+
if (descTemplate && pageSeo.description) {
|
|
296
321
|
pageSeo.description = descTemplate.replace("%s", pageSeo.description);
|
|
297
322
|
}
|
|
298
323
|
|
|
@@ -301,6 +326,12 @@ async function buildPageSeo(
|
|
|
301
326
|
return { ...sectionSeo, ...pageSeo };
|
|
302
327
|
}
|
|
303
328
|
|
|
329
|
+
/** Returns a non-trivial template string, or undefined for "%s" / empty / blank. */
|
|
330
|
+
function effectiveTemplate(tmpl: string | undefined): string | undefined {
|
|
331
|
+
if (!tmpl || tmpl.trim() === "" || tmpl.trim() === "%s") return undefined;
|
|
332
|
+
return tmpl;
|
|
333
|
+
}
|
|
334
|
+
|
|
304
335
|
// ---------------------------------------------------------------------------
|
|
305
336
|
// Head metadata builder
|
|
306
337
|
// ---------------------------------------------------------------------------
|
package/src/sdk/workerEntry.ts
CHANGED
|
@@ -722,6 +722,20 @@ export function createDecoWorkerEntry(
|
|
|
722
722
|
const v = (env[cacheVersionEnv] as string) || "";
|
|
723
723
|
if (v) hit.headers.set("X-Cache-Version", v);
|
|
724
724
|
}
|
|
725
|
+
// Restore client-facing Cache-Control (the stored version uses sMaxAge
|
|
726
|
+
// as max-age for Cache API TTL, which would leak to the CDN auto-cache
|
|
727
|
+
// and cause stale HTML after deploys — the CDN caches under the raw URL,
|
|
728
|
+
// bypassing BUILD_HASH versioned keys).
|
|
729
|
+
const hitProfile = getProfile(url);
|
|
730
|
+
const hitHeaders = cacheHeaders(hitProfile);
|
|
731
|
+
for (const [k, v] of Object.entries(hitHeaders)) {
|
|
732
|
+
hit.headers.set(k, v);
|
|
733
|
+
}
|
|
734
|
+
// Prevent CDN from auto-caching this response under the raw URL.
|
|
735
|
+
// The Worker manages edge caching via caches.default with versioned keys;
|
|
736
|
+
// CDN auto-caching would bypass that versioning and serve stale HTML
|
|
737
|
+
// after deploys (referencing old CSS/JS fingerprinted filenames).
|
|
738
|
+
hit.headers.set("CDN-Cache-Control", "no-store");
|
|
725
739
|
return hit;
|
|
726
740
|
}
|
|
727
741
|
} catch {
|
|
@@ -785,12 +799,21 @@ export function createDecoWorkerEntry(
|
|
|
785
799
|
if (v) toReturn.headers.set("X-Cache-Version", v);
|
|
786
800
|
}
|
|
787
801
|
|
|
802
|
+
// Prevent CDN from auto-caching this response under the raw URL.
|
|
803
|
+
// Edge caching is managed by the Worker via caches.default with
|
|
804
|
+
// BUILD_HASH-versioned keys; CDN auto-caching would bypass versioning
|
|
805
|
+
// and serve stale HTML after deploys.
|
|
806
|
+
toReturn.headers.set("CDN-Cache-Control", "no-store");
|
|
807
|
+
|
|
788
808
|
// For Cache API storage, use sMaxAge as max-age since the Cache API
|
|
789
809
|
// ignores s-maxage and only respects max-age for TTL decisions.
|
|
790
810
|
if (cache) {
|
|
791
811
|
try {
|
|
792
812
|
const toStore = toReturn.clone();
|
|
793
813
|
toStore.headers.set("Cache-Control", `public, max-age=${profileConfig.sMaxAge}`);
|
|
814
|
+
// Remove CDN-Cache-Control from stored version — it's only needed
|
|
815
|
+
// on the response to the client to prevent CDN auto-caching.
|
|
816
|
+
toStore.headers.delete("CDN-Cache-Control");
|
|
794
817
|
ctx.waitUntil(cache.put(cacheKey, toStore));
|
|
795
818
|
} catch {
|
|
796
819
|
// Cache API unavailable — skip storing
|