@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 +1 -1
- package/scripts/generate-blocks.ts +11 -1
- package/src/cms/index.ts +1 -0
- package/src/cms/loader.ts +25 -0
- package/src/routes/cmsRoute.ts +39 -8
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
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
|
// ---------------------------------------------------------------------------
|