@decocms/start 1.4.2 → 1.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
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",
package/src/cms/loader.ts CHANGED
@@ -196,13 +196,13 @@ export function getSiteSeo(): {
196
196
 
197
197
  export function findPageByPath(
198
198
  targetPath: string,
199
- ): { page: DecoPage; params: Record<string, string> } | null {
199
+ ): { page: DecoPage; params: Record<string, string>; blockKey: string } | null {
200
200
  const allPages = getAllPages();
201
201
 
202
- for (const { page } of allPages) {
202
+ for (const { key, page } of allPages) {
203
203
  if (!page.path) continue;
204
204
  const params = matchPath(page.path, targetPath);
205
- if (params !== null) return { page, params };
205
+ if (params !== null) return { page, params, blockKey: key };
206
206
  }
207
207
 
208
208
  return null;
@@ -1262,6 +1262,8 @@ export interface DecoPageResult {
1262
1262
  name: string;
1263
1263
  path: string;
1264
1264
  params: Record<string, string>;
1265
+ /** CMS block key, e.g. "pages-Lista de cupons [APP]-397629" */
1266
+ blockKey?: string;
1265
1267
  resolvedSections: ResolvedSection[];
1266
1268
  deferredSections: DeferredSection[];
1267
1269
  /**
@@ -1285,7 +1287,7 @@ export async function resolveDecoPage(
1285
1287
  return null;
1286
1288
  }
1287
1289
 
1288
- const { page, params } = match;
1290
+ const { page, params, blockKey } = match;
1289
1291
  const ctx: MatcherContext = { ...matcherCtx, path: targetPath };
1290
1292
  const rctx: ResolveContext = { routeParams: params, matcherCtx: ctx, memo: new Map(), depth: 0 };
1291
1293
 
@@ -1403,6 +1405,7 @@ export async function resolveDecoPage(
1403
1405
  name: page.name,
1404
1406
  path: page.path || targetPath,
1405
1407
  params,
1408
+ blockKey,
1406
1409
  resolvedSections: allResults.flat(),
1407
1410
  deferredSections,
1408
1411
  seoSection,
@@ -39,9 +39,9 @@ import { getRenderShellConfig } from "../admin/setup";
39
39
  import { RequestContext } from "./requestContext";
40
40
  import { getAppMiddleware } from "./setupApps";
41
41
  import type { MatcherContext, ResolvedSection } from "../cms/resolve";
42
- import { resolveDecoPage, extractSeoFromProps, extractSeoFromSections } from "../cms/resolve";
42
+ import { resolveDecoPage } from "../cms/resolve";
43
43
  import { runSectionLoaders, runSingleSectionLoader } from "../cms/sectionLoaders";
44
- import { getSiteSeo } from "../cms/loader";
44
+ import { loadBlocks } from "../cms/loader";
45
45
 
46
46
  /**
47
47
  * Append Link preload headers for CSS and fonts so the browser starts
@@ -980,24 +980,52 @@ export function createDecoWorkerEntry(
980
980
  }
981
981
  const enrichedSections = await runSectionLoaders(page.resolvedSections, request);
982
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
- }));
983
+ // Run SEO section loader if registered
984
+ let seoResult = page.seoSection;
985
+ if (seoResult) {
986
+ try {
987
+ seoResult = await runSingleSectionLoader(seoResult, request);
988
+ } catch {
989
+ // use unloaded seoSection
990
+ }
991
+ }
992
+
993
+ // Merge site-wide SEO defaults into seo props
994
+ const blocks = loadBlocks();
995
+ const site = blocks["Site"] as Record<string, unknown> | undefined;
996
+ const fullSiteSeo = (site?.seo as Record<string, unknown>) ?? {};
997
+
998
+ // When SeoV2 loader ran, use its output as base (preserves key order)
999
+ // and only fill in missing fields from the site-wide SEO config.
1000
+ const loaderProps = seoResult?.props ?? {};
1001
+ const seoProps: Record<string, unknown> = { ...loaderProps };
1002
+ for (const [k, v] of Object.entries(fullSiteSeo)) {
1003
+ if (!(k in seoProps)) seoProps[k] = v;
1004
+ }
1005
+ // Strip internal template fields
1006
+ delete seoProps.titleTemplate;
1007
+ delete seoProps.descriptionTemplate;
1008
+
1009
+ // Build resolveChain statically to match legacy deco-cx/deco format.
1010
+ type FieldResolver = { type: string; value: string | number };
1011
+ const rawKey = page.blockKey ?? `pages-${page.name}`;
1012
+ const encodedKey = rawKey.replace(
1013
+ /^(pages-)(.+)$/,
1014
+ (_m, prefix, rest) => prefix + encodeURIComponent(rest),
1015
+ );
1016
+ const pageChain: FieldResolver[] = [
1017
+ { type: "resolver", value: "website/handlers/fresh.ts" },
1018
+ { type: "prop", value: "page" },
1019
+ { type: "resolver", value: "resolved" },
1020
+ { type: "resolvable", value: encodedKey },
1021
+ { type: "resolver", value: "website/pages/Page.tsx" },
1022
+ ];
1023
+
1024
+ const seoChain: FieldResolver[] = [
1025
+ ...pageChain,
1026
+ { type: "prop", value: "seo" },
1027
+ { type: "resolver", value: seoResult?.component ?? "website/sections/Seo/SeoV2.tsx" },
1028
+ ];
1001
1029
 
1002
1030
  const result = {
1003
1031
  props: {
@@ -1006,16 +1034,27 @@ export function createDecoWorkerEntry(
1006
1034
  seo: {
1007
1035
  props: seoProps,
1008
1036
  metadata: {
1009
- resolveChain: [],
1010
- component: seoComponent,
1037
+ resolveChain: seoChain,
1038
+ component: seoResult?.component ?? "website/sections/Seo/SeoV2.tsx",
1011
1039
  },
1012
1040
  },
1013
- sections,
1041
+ sections: enrichedSections.map((s, i) => ({
1042
+ props: s.props,
1043
+ metadata: {
1044
+ resolveChain: [
1045
+ ...pageChain,
1046
+ { type: "prop", value: "sections" },
1047
+ { type: "prop", value: String(i) },
1048
+ { type: "resolver", value: s.component },
1049
+ ],
1050
+ component: s.component,
1051
+ },
1052
+ })),
1014
1053
  devMode: false,
1015
1054
  unindexedDomain: false,
1016
1055
  },
1017
1056
  metadata: {
1018
- resolveChain: [],
1057
+ resolveChain: pageChain,
1019
1058
  component: "website/pages/Page.tsx",
1020
1059
  },
1021
1060
  };
@@ -1455,74 +1494,3 @@ export function createDecoWorkerEntry(
1455
1494
  }
1456
1495
  }
1457
1496
 
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
- }