@decocms/start 0.25.1 → 0.25.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": "0.25.1",
3
+ "version": "0.25.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",
@@ -22,7 +22,17 @@ const blocksDir = path.resolve(process.cwd(), arg("blocks-dir", ".deco/blocks"))
22
22
  const outFile = path.resolve(process.cwd(), arg("out-file", "src/server/cms/blocks.gen.ts"));
23
23
 
24
24
  function decodeBlockName(filename: string): string {
25
- return decodeURIComponent(decodeURIComponent(filename)).replace(/\.json$/, "");
25
+ let name = filename.replace(/\.json$/, "");
26
+ while (name.includes("%")) {
27
+ try {
28
+ const next = decodeURIComponent(name);
29
+ if (next === name) break;
30
+ name = next;
31
+ } catch {
32
+ break; // literal % in the decoded name — nothing left to decode
33
+ }
34
+ }
35
+ return name;
26
36
  }
27
37
 
28
38
  if (!fs.existsSync(blocksDir)) {
package/src/cms/index.ts CHANGED
@@ -3,6 +3,7 @@ export {
3
3
  findPageByPath,
4
4
  getAllPages,
5
5
  getRevision,
6
+ getSiteSeo,
6
7
  loadBlocks,
7
8
  onChange,
8
9
  setBlocks,
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 {
@@ -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
- if (!seoSection) return sectionSeo;
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
- // Apply title/description templates from the SEO block config.
287
- // SeoV2 blocks carry templates like "%s | CASA & VIDEO" that wrap
288
- // the computed title. Template "%s" is a no-op.
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 = rawProps.titleTemplate as string | undefined;
291
- const descTemplate = rawProps.descriptionTemplate as string | undefined;
292
- if (titleTemplate && titleTemplate !== "%s" && pageSeo.title) {
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 && descTemplate !== "%s" && pageSeo.description) {
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
  // ---------------------------------------------------------------------------