@decocms/start 1.4.0 → 1.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
@@ -1551,7 +1551,7 @@ export async function resolveDeferredSectionFull(
1551
1551
  * rawProps for the section at the given index. Expensive but ensures correctness
1552
1552
  * when the in-memory cache has been evicted (different isolate, TTL expired).
1553
1553
  */
1554
- async function reExtractRawProps(
1554
+ export async function reExtractRawProps(
1555
1555
  pagePath: string,
1556
1556
  component: string,
1557
1557
  sectionIndex: number,
@@ -36,6 +36,7 @@ import {
36
36
  extractSeoFromProps,
37
37
  extractSeoFromSections,
38
38
  getDeferredRawProps,
39
+ reExtractRawProps,
39
40
  resolveDecoPage,
40
41
  resolveDeferredSection,
41
42
  resolveDeferredSectionFull,
@@ -220,9 +221,14 @@ export const loadDeferredSection = createServerFn({ method: "POST" })
220
221
  request: originRequest,
221
222
  };
222
223
 
223
- // Resolve rawProps: prefer client-provided (backward compat), then server cache
224
+ // Resolve rawProps: prefer client-provided (backward compat), then server cache,
225
+ // then re-extract from the page as a last resort (handles cross-isolate cache miss
226
+ // on Cloudflare Workers and TTL expiry for slow-scrolling users).
224
227
  const rawProps = clientRawProps
225
- ?? (index !== undefined ? getDeferredRawProps(pagePath, component, index) : null);
228
+ ?? (index !== undefined ? getDeferredRawProps(pagePath, component, index) : null)
229
+ ?? (index !== undefined
230
+ ? await reExtractRawProps(pagePath, component, index, matcherCtx)
231
+ : null);
226
232
 
227
233
  if (!rawProps) {
228
234
  console.warn(`[CMS] Deferred section cache miss: ${component} at index ${index} on ${pagePath}`);
@@ -38,9 +38,10 @@ import { isMobileUA } from "./useDevice";
38
38
  import { getRenderShellConfig } from "../admin/setup";
39
39
  import { RequestContext } from "./requestContext";
40
40
  import { getAppMiddleware } from "./setupApps";
41
- import type { MatcherContext } from "../cms/resolve";
42
- import { resolveDecoPage } from "../cms/resolve";
43
- import { runSectionLoaders } from "../cms/sectionLoaders";
41
+ import type { MatcherContext, ResolvedSection } from "../cms/resolve";
42
+ import { resolveDecoPage, extractSeoFromProps, extractSeoFromSections } from "../cms/resolve";
43
+ import { runSectionLoaders, runSingleSectionLoader } from "../cms/sectionLoaders";
44
+ import { getSiteSeo } from "../cms/loader";
44
45
 
45
46
  /**
46
47
  * Append Link preload headers for CSS and fonts so the browser starts
@@ -978,10 +979,45 @@ export function createDecoWorkerEntry(
978
979
  return Response.json(null, { status: 404, headers: { "Access-Control-Allow-Origin": "*" } });
979
980
  }
980
981
  const enrichedSections = await runSectionLoaders(page.resolvedSections, request);
981
- const { seoSection: _seo, ...pageData } = page;
982
+
983
+ // Build SEO props — same logic as buildPageSeo in cmsRoute.ts:
984
+ // 1. Run section loader on seoSection (e.g. SEOPDP)
985
+ // 2. Extract SEO fields from resolved props
986
+ // 3. Merge with site-wide SEO defaults from the "Site" app block
987
+ // 4. Extract section-contributed SEO from enriched sections
988
+ const seoProps = await buildAsJsonSeo(page.seoSection, enrichedSections, request);
989
+ const seoComponent = page.seoSection?.component ?? "website/sections/Seo/SeoV2.tsx";
990
+
991
+ // Build legacy deco-cx/deco (Fresh) compatible response shape.
992
+ // The old framework returns { props: { name, path, seo, sections, ... }, metadata }.
993
+ // Mobile apps and other consumers rely on this exact structure.
994
+ const sections = enrichedSections.map((s) => ({
995
+ props: s.props,
996
+ metadata: {
997
+ resolveChain: [],
998
+ component: s.component,
999
+ },
1000
+ }));
1001
+
982
1002
  const result = {
983
- ...pageData,
984
- resolvedSections: enrichedSections,
1003
+ props: {
1004
+ name: page.name,
1005
+ path: page.path,
1006
+ seo: {
1007
+ props: seoProps,
1008
+ metadata: {
1009
+ resolveChain: [],
1010
+ component: seoComponent,
1011
+ },
1012
+ },
1013
+ sections,
1014
+ devMode: false,
1015
+ unindexedDomain: false,
1016
+ },
1017
+ metadata: {
1018
+ resolveChain: [],
1019
+ component: "website/pages/Page.tsx",
1020
+ },
985
1021
  };
986
1022
  return Response.json(result, {
987
1023
  headers: {
@@ -1418,3 +1454,75 @@ export function createDecoWorkerEntry(
1418
1454
  return dressResponse(origin, "MISS");
1419
1455
  }
1420
1456
  }
1457
+
1458
+ // ---------------------------------------------------------------------------
1459
+ // ?asJson SEO builder — mirrors buildPageSeo() from cmsRoute.ts
1460
+ // ---------------------------------------------------------------------------
1461
+
1462
+ /**
1463
+ * Build SEO props for the ?asJson response, matching the legacy deco-cx/deco
1464
+ * format. Runs the SEO section loader, merges with site-wide SEO defaults,
1465
+ * and extracts section-contributed SEO.
1466
+ */
1467
+ async function buildAsJsonSeo(
1468
+ seoSection: ResolvedSection | null | undefined,
1469
+ enrichedSections: ResolvedSection[],
1470
+ request: Request,
1471
+ ): Promise<Record<string, unknown>> {
1472
+ const siteSeo = getSiteSeo();
1473
+ const sectionSeo = extractSeoFromSections(enrichedSections);
1474
+
1475
+ if (!seoSection) {
1476
+ const merged: Record<string, unknown> = { ...sectionSeo };
1477
+ if (siteSeo.title && !merged.title) merged.title = siteSeo.title;
1478
+ if (siteSeo.description && !merged.description) merged.description = siteSeo.description;
1479
+ if (siteSeo.image && !merged.image) merged.image = siteSeo.image;
1480
+ if (siteSeo.favicon) merged.favicon = siteSeo.favicon;
1481
+ if (!merged.jsonLDs) merged.jsonLDs = [];
1482
+ return merged;
1483
+ }
1484
+
1485
+ // Run the section loader if registered (e.g. SEOPDP)
1486
+ let enrichedProps = seoSection.props;
1487
+ try {
1488
+ const enriched = await runSingleSectionLoader(seoSection, request);
1489
+ if (enriched) enrichedProps = enriched.props;
1490
+ } catch {
1491
+ // Section loader failed — use raw resolved props
1492
+ }
1493
+
1494
+ const pageSeo = extractSeoFromProps(enrichedProps);
1495
+
1496
+ // Merge site-wide SEO defaults (same as cmsRoute.ts buildPageSeo)
1497
+ if (!pageSeo.title && siteSeo.title) pageSeo.title = siteSeo.title;
1498
+ if (!pageSeo.description && siteSeo.description) pageSeo.description = siteSeo.description;
1499
+ if (!pageSeo.image && siteSeo.image) pageSeo.image = siteSeo.image;
1500
+
1501
+ // Apply title/description templates (mirrors buildPageSeo in cmsRoute.ts).
1502
+ // Priority: page-level template → site-level template → no-op.
1503
+ const rawProps = seoSection.props;
1504
+ const titleTemplate =
1505
+ effectiveTemplate(rawProps.titleTemplate as string | undefined) ??
1506
+ effectiveTemplate(siteSeo.titleTemplate);
1507
+ const descTemplate =
1508
+ effectiveTemplate(rawProps.descriptionTemplate as string | undefined) ??
1509
+ effectiveTemplate(siteSeo.descriptionTemplate);
1510
+
1511
+ if (titleTemplate && pageSeo.title) {
1512
+ pageSeo.title = titleTemplate.replace("%s", pageSeo.title);
1513
+ }
1514
+ if (descTemplate && pageSeo.description) {
1515
+ pageSeo.description = descTemplate.replace("%s", pageSeo.description);
1516
+ }
1517
+
1518
+ const merged = { ...sectionSeo, ...pageSeo } as Record<string, unknown>;
1519
+ if (siteSeo.favicon) merged.favicon = siteSeo.favicon;
1520
+ if (!merged.jsonLDs) merged.jsonLDs = [];
1521
+ return merged;
1522
+ }
1523
+
1524
+ /** Returns a non-trivial template string, or undefined for "%s" / empty / blank. */
1525
+ function effectiveTemplate(tmpl: string | undefined): string | undefined {
1526
+ if (!tmpl || tmpl.trim() === "" || tmpl.trim() === "%s") return undefined;
1527
+ return tmpl;
1528
+ }