@teamscala/service-boot 0.23.377 → 0.23.379

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
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "name": "@teamscala/service-boot",
8
8
  "type": "module",
9
- "version": "0.23.377",
9
+ "version": "0.23.379",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -86,3 +86,65 @@ describe("composePageTitle", () => {
86
86
  expect(composePageTitle({ websiteName: "" })).toBe("Untitled");
87
87
  });
88
88
  });
89
+
90
+ describe("composePageTitle with orgPrefix (feature flag page-title-org-prefix)", () => {
91
+ test("prefix is the owning org's name, never the internal website label", () => {
92
+ // The defect: client-facing document tabs read "Public Pages: <subject>".
93
+ expect(
94
+ composePageTitle(
95
+ {
96
+ websiteName: "Public Pages",
97
+ organisationName: "GBG Indo",
98
+ entityTitle: "Launcha Blueprint",
99
+ },
100
+ { orgPrefix: true },
101
+ ),
102
+ ).toBe("GBG Indo: Launcha Blueprint");
103
+ });
104
+
105
+ test("org with no display name gets NO prefix — never the website name", () => {
106
+ expect(
107
+ composePageTitle(
108
+ { websiteName: "Public Pages", organisationName: "", entityTitle: "Launcha Blueprint" },
109
+ { orgPrefix: true },
110
+ ),
111
+ ).toBe("Launcha Blueprint");
112
+ expect(
113
+ composePageTitle({ websiteName: "Public Pages", entityTitle: "Launcha Blueprint" }, { orgPrefix: true }),
114
+ ).toBe("Launcha Blueprint");
115
+ });
116
+
117
+ test("per-page override and curated template still win over the org prefix", () => {
118
+ expect(
119
+ composePageTitle(
120
+ { pageMetaTitle: "Override", organisationName: "GBG Indo", pageName: "P" },
121
+ { orgPrefix: true },
122
+ ),
123
+ ).toBe("Override");
124
+ expect(
125
+ composePageTitle(
126
+ { templateTitle: "T", organisationName: "GBG Indo", pageName: "P" },
127
+ { orgPrefix: true },
128
+ ),
129
+ ).toBe("T");
130
+ });
131
+
132
+ test("no subject renders the org name alone; nothing at all is 'Untitled'", () => {
133
+ expect(composePageTitle({ organisationName: "GBG Indo" }, { orgPrefix: true })).toBe(
134
+ "GBG Indo",
135
+ );
136
+ expect(composePageTitle({}, { orgPrefix: true })).toBe("Untitled");
137
+ });
138
+
139
+ test("flag off keeps the legacy website-name derivation", () => {
140
+ expect(
141
+ composePageTitle(
142
+ { websiteName: "Public Pages", organisationName: "GBG Indo", pageName: "Dashboard" },
143
+ { orgPrefix: false },
144
+ ),
145
+ ).toBe("Public Pages: Dashboard");
146
+ expect(
147
+ composePageTitle({ websiteName: "Public Pages", organisationName: "GBG Indo", pageName: "Dashboard" }),
148
+ ).toBe("Public Pages: Dashboard");
149
+ });
150
+ });
@@ -32,8 +32,13 @@ export interface TitleContext {
32
32
  pageMetaTitle?: string | null;
33
33
  /** Resolved resource-template string (forms/quiz/gpt). Curated per-surface shape. */
34
34
  templateTitle?: string | null;
35
- /** builda_websites.name — the surface, the primary site context. */
35
+ /** builda_websites.name — the surface, the legacy site context (orgPrefix off). */
36
36
  websiteName?: string | null;
37
+ /** The OWNING ORG's display name (brand.companyName, resolved from the
38
+ * org the page's brand_org_path names). The title prefix when orgPrefix is
39
+ * on — operator ruling 2026-08-17: a client-facing document's tab carries
40
+ * the org's name, never the internal website label ("Public Pages"). */
41
+ organisationName?: string | null;
37
42
  /** builda_pages.name — the page (e.g. "Dashboard"). */
38
43
  pageName?: string | null;
39
44
  /** Resource entity title (proposal/form/quiz/agent name) — preferred subject. */
@@ -46,20 +51,35 @@ function trimToDefined(value: unknown): string | undefined {
46
51
  return trimmed ? trimmed : undefined;
47
52
  }
48
53
 
49
- export function composePageTitle(ctx: TitleContext): string {
54
+ export function composePageTitle(
55
+ ctx: TitleContext,
56
+ opts?: { orgPrefix?: boolean },
57
+ ): string {
50
58
  // 1. Explicit per-page override wins outright — the operator's escape
51
- // hatch for a custom SEO/social title. It is NEVER the default source.
59
+ // hatch for a custom SEO/social title. It is NEVER the default source.
52
60
  const override = trimToDefined(ctx.pageMetaTitle);
53
61
  if (override) return override;
54
62
 
55
63
  // 2. Curated resource template (forms/quiz/gpt). Already contextual
56
- // (entity + org), so it beats the generic derivation for those surfaces.
64
+ // (entity + org), so it beats the generic derivation for those surfaces.
57
65
  const template = trimToDefined(ctx.templateTitle);
58
66
  if (template) return template;
59
67
 
60
- // 3. Default derivation: <website.name>: <subject>. Site-first so the
61
- // stable surface precedes the varying page — how a tab is scanned.
68
+ // 3. Default derivation. Site-first so the stable surface precedes the
69
+ // varying page — how a tab is scanned. With orgPrefix on (feature flag
70
+ // page-title-org-prefix), the prefix is the OWNING ORG's name, never the
71
+ // website label: an internal site name ("Public Pages") on a
72
+ // client-facing document is the defect the flag exists to remove. An org
73
+ // with no display name gets NO prefix — falling back to the website name
74
+ // would reintroduce the defect the mode exists to fix.
62
75
  const subject = trimToDefined(ctx.entityTitle) ?? trimToDefined(ctx.pageName);
76
+ if (opts?.orgPrefix) {
77
+ const orgName = trimToDefined(ctx.organisationName);
78
+ if (orgName && subject) return `${orgName}: ${subject}`;
79
+ if (orgName) return orgName;
80
+ if (subject) return subject;
81
+ return "Untitled";
82
+ }
63
83
  const websiteName = trimToDefined(ctx.websiteName);
64
84
  if (websiteName && subject) return `${websiteName}: ${subject}`;
65
85
  if (websiteName) return websiteName;
@@ -4,11 +4,16 @@
4
4
  * @edit edit directly
5
5
  *
6
6
  * Meta/title helpers for the route registrar: interpolate the route's
7
- * meta_context against the resolved graph, and resolve a curated
8
- * page_meta_template title. Extracted from register-registry-page-route.ts.
7
+ * meta_context against the resolved graph. Extracted from
8
+ * register-registry-page-route.ts.
9
+ *
10
+ * resolveTemplateTitle (the page_meta_template title reader) was DELETED
11
+ * 2026-08-17: it only ever applied when NO resource entity resolved, and on a
12
+ * resource page an entity ALWAYS resolves — the curated template was discarded
13
+ * by construction, dead on exactly the pages the type exists for (Valencia's
14
+ * finding). composePageTitle (+ the org-prefix flag) owns titles end to end.
9
15
  */
10
16
  import { interpolateMeta } from "../resolve-from-loader-config.ts";
11
- import { getEntriesByType } from "@teamscala/db/registry/accessors";
12
17
 
13
18
  export function buildMetaFromContext(
14
19
  metaContext: Record<string, Record<string, string>> | undefined,
@@ -18,70 +23,44 @@ export function buildMetaFromContext(
18
23
  return interpolateMeta(metaContext as Record<string, unknown>, resolved);
19
24
  }
20
25
 
21
- export function resolveTemplateTitle(
22
- service: string,
23
- resolved: Record<string, unknown>,
24
- ): string | undefined {
25
- const rows = getEntriesByType("page_meta_template") as Array<{
26
- slug: string;
27
- config: Record<string, unknown>;
28
- }>;
29
- const bySlug = new Map<string, Record<string, unknown>>();
30
- for (const row of rows) {
31
- bySlug.set(row.slug, row.config);
32
- }
33
- const merged = (bySlug.get("default") ?? null) as Record<string, unknown> | null;
34
- const serviceTmpl = (bySlug.get(service) ?? null) as Record<string, unknown> | null;
35
- const tmpl = serviceTmpl ?? merged;
36
- if (!tmpl) return undefined;
37
- const titleTemplate = tmpl.titleTemplate as string | undefined;
38
- if (!titleTemplate) return undefined;
39
- const brand = (resolved.brand as Record<string, unknown>) ?? {};
40
- const website = (resolved.website as Record<string, unknown>) ?? {};
41
- const page = (resolved.page as Record<string, unknown>) ?? {};
42
- // The full placeholder set the curated resource templates use:
43
- // {org.name}, {website.name}, {page.name}, {form.name}, {quiz.name},
44
- // {proposal.name}, {agent.name}. Previously only {org.name} resolved, so
45
- // every resource template ("{agent.name} — {org.name}", "{form.name} by
46
- // {org.name}") shipped with a blank subject. Resolving the graph here is
47
- // the executor half of composePageTitle (business-logic-is-rows-plus-
48
- // executors): the template ROWS carry the shape, this carries the values.
49
- const context: Record<string, unknown> = {
50
- org: { name: (brand.companyName as string) || undefined },
51
- website: { name: (website.name as string) || undefined },
52
- page: { name: (page.name as string) || undefined },
53
- form: { name: ((resolved.form as Record<string, unknown>)?.name as string) || undefined },
54
- // An assessment has no `name` column — it carries `title`, and a report
55
- // surface carries `report_title` ("Personalised Authority Blueprint")
56
- // which is the document's own name and the right subject for its tab.
57
- // Reading `.name` here resolved to "", so the template produced " by
58
- // <org>", was discarded as empty, and EVERY quiz page silently fell back
59
- // to the "<website>: <subject>" derivation — surfacing the internal site
60
- // name ("Public Pages") on a client-facing document. The failure was
61
- // invisible because the fallback is itself a plausible title.
62
- quiz: {
63
- name:
64
- ((resolved.quiz as Record<string, unknown>)?.report_title as string) ||
65
- ((resolved.quiz as Record<string, unknown>)?.title as string) ||
66
- ((resolved.quiz as Record<string, unknown>)?.name as string) ||
67
- undefined,
68
- },
69
- proposal: {
70
- name: ((resolved.proposal as Record<string, unknown>)?.name as string) || undefined,
71
- },
72
- agent: { name: ((resolved.agent as Record<string, unknown>)?.name as string) || undefined },
73
- };
74
- const result = titleTemplate.replace(/\{([^}]+)\}/g, (_m, path: string) => {
75
- const keys = path.trim().split(".");
76
- let cur: unknown = context;
77
- for (const k of keys) {
78
- if (cur && typeof cur === "object") {
79
- cur = (cur as Record<string, unknown>)[k];
80
- } else {
81
- return "";
82
- }
26
+ /**
27
+ * THE ONE SUBJECT DERIVATION. What a page is ABOUT — the entity whose name the
28
+ * browser tab should carry.
29
+ *
30
+ * This existed as THREE hand-written `||` chains, one per render branch in
31
+ * resolve-page-data, and they had already diverged: two ordered form before
32
+ * proposal, the third ordered proposal first. So which resource named the page
33
+ * depended on which branch happened to render it, silently.
34
+ *
35
+ * The divergence is the cheap half of the cost. The expensive half is that a
36
+ * correction has to be applied three times to work, and applying it twice looks
37
+ * exactly like applying it correctly — measured 2026-08-17, when a fix to the
38
+ * quiz subject was shipped, installed and verified present in the bundle while
39
+ * the page kept its old title, because the branch that rendered it held the
40
+ * second copy.
41
+ *
42
+ * One chain, one precedence, one place to change. Adding a resource type is an
43
+ * edit here rather than a fourth branch in three files.
44
+ */
45
+ export function resolveEntityTitle(resolved: Record<string, unknown>): string | undefined {
46
+ const pick = (key: string, ...fields: string[]): string | undefined => {
47
+ const row = resolved[key] as Record<string, unknown> | undefined;
48
+ if (!row) return undefined;
49
+ for (const field of fields) {
50
+ const value = row[field];
51
+ if (typeof value === "string" && value.trim()) return value.trim();
83
52
  }
84
- return cur === null || cur === undefined ? "" : String(cur);
85
- });
86
- return result || undefined;
53
+ return undefined;
54
+ };
55
+ return (
56
+ pick("form", "name") ||
57
+ pick("proposal", "name") ||
58
+ // A report surface carries its own document name in `report_title`
59
+ // ("Personalised Authority Blueprint"); `title` names the DIAGNOSTIC
60
+ // ("The Launcha Authority Index"), which is the wrong subject for the tab
61
+ // of a document sent to a client. The landing page has no report_title
62
+ // and correctly falls through to the quiz title.
63
+ pick("quiz", "report_title", "title") ||
64
+ pick("agent", "name")
65
+ );
87
66
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @system ui-service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Locks the single subject derivation that replaced three divergent copies.
7
+ */
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+ import { resolveEntityTitle } from "./meta-title.ts";
11
+
12
+ describe("resolveEntityTitle", () => {
13
+ test("a report is named by its own document name, not the diagnostic", () => {
14
+ // The Launcha Blueprint: `title` names the QUIZ, `report_title` names the
15
+ // DOCUMENT the client receives. The tab must carry the document.
16
+ expect(
17
+ resolveEntityTitle({
18
+ quiz: {
19
+ title: "The Launcha Authority Index",
20
+ report_title: "Personalised Authority Blueprint",
21
+ },
22
+ }),
23
+ ).toBe("Personalised Authority Blueprint");
24
+ });
25
+
26
+ test("a quiz with no report name falls through to its title", () => {
27
+ expect(resolveEntityTitle({ quiz: { title: "The Launcha Authority Index" } })).toBe(
28
+ "The Launcha Authority Index",
29
+ );
30
+ });
31
+
32
+ test("precedence is single and fixed across every caller", () => {
33
+ // The three former copies disagreed: two put form first, one put proposal
34
+ // first, so the subject depended on which render branch ran. One order now.
35
+ const all = {
36
+ form: { name: "Form" },
37
+ proposal: { name: "Proposal" },
38
+ quiz: { report_title: "Report" },
39
+ agent: { name: "Agent" },
40
+ };
41
+ expect(resolveEntityTitle(all)).toBe("Form");
42
+ expect(resolveEntityTitle({ ...all, form: undefined })).toBe("Proposal");
43
+ expect(resolveEntityTitle({ ...all, form: undefined, proposal: undefined })).toBe("Report");
44
+ });
45
+
46
+ test("blank and whitespace-only names do not win", () => {
47
+ expect(resolveEntityTitle({ form: { name: " " }, agent: { name: "Agent" } })).toBe("Agent");
48
+ });
49
+
50
+ test("undefined when nothing names the page", () => {
51
+ expect(resolveEntityTitle({})).toBeUndefined();
52
+ expect(resolveEntityTitle({ quiz: {} })).toBeUndefined();
53
+ });
54
+ });
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { resolveThemeMode } from "../resolve-theme-mode.ts";
12
12
  import { composePageTitle } from "../compose-page-title.ts";
13
+ import { resolveEntityTitle } from "./meta-title.ts";
13
14
  import { getAppLogger } from "@teamscala/logger/app-loggers";
14
15
 
15
16
  /**
@@ -90,6 +91,9 @@ export function resolvedToPageData(
90
91
  // a page framed inside another shell (e.g. a form in a GPT tool panel)
91
92
  // matches the host's light/dark scheme so an embedded page doesn't flash.
92
93
  themeOverride?: "light" | "dark",
94
+ // feature flag page-title-org-prefix (read live by the caller): title
95
+ // prefixes carry the OWNING ORG's name instead of the website label.
96
+ orgPrefix?: boolean,
93
97
  ): Record<string, unknown> {
94
98
  const brand = (resolved.brand as Record<string, unknown>) ?? {};
95
99
  const primaryColor = (brand.primaryColor as string) ?? "";
@@ -124,11 +128,7 @@ export function resolvedToPageData(
124
128
  // name in <title>/OG, not the generic "Portal". Falls back to the website
125
129
  // name, then "Portal". Without this, the region-slots branch returned
126
130
  // early with a hardcoded "Portal" title (gpt-test: SSR/DOM/OG title FAIL).
127
- const entityTitle =
128
- (resolved.form as { name?: string })?.name ||
129
- (resolved.quiz as { title?: string })?.title ||
130
- (resolved.proposal as { name?: string })?.name ||
131
- (resolved.agent as { name?: string })?.name;
131
+ const entityTitle = resolveEntityTitle(resolved);
132
132
 
133
133
  const regions: RegionEntry[] = [];
134
134
  for (const slot of regionSlots) {
@@ -161,11 +161,15 @@ export function resolvedToPageData(
161
161
  favicon: (brand.favicon as string) ?? null,
162
162
  },
163
163
  meta: {
164
- title: composePageTitle({
165
- templateTitle: (meta.title as string) || undefined,
166
- websiteName: (website.name as string) || undefined,
167
- entityTitle: entityTitle || undefined,
168
- }),
164
+ title: composePageTitle(
165
+ {
166
+ templateTitle: (meta.title as string) || undefined,
167
+ websiteName: (website.name as string) || undefined,
168
+ organisationName: (brand.companyName as string) || undefined,
169
+ entityTitle: entityTitle || undefined,
170
+ },
171
+ { orgPrefix },
172
+ ),
169
173
  },
170
174
  };
171
175
  }
@@ -229,12 +233,16 @@ export function resolvedToPageData(
229
233
  },
230
234
  theme,
231
235
  meta: {
232
- title: composePageTitle({
236
+ title: composePageTitle(
237
+ {
233
238
  templateTitle: (meta.title as string) || undefined,
234
239
  websiteName: (website.name as string) || undefined,
235
- pageName: contentPageName,
236
- entityTitle: entityTitle || undefined,
237
- }),
240
+ organisationName: (brand.companyName as string) || undefined,
241
+ pageName: contentPageName,
242
+ entityTitle: entityTitle || undefined,
243
+ },
244
+ { orgPrefix },
245
+ ),
238
246
  },
239
247
  };
240
248
  }
@@ -306,11 +314,7 @@ export function resolvedToPageData(
306
314
  const overrideBody = pickRootProp<string>(regions, "bodyFont");
307
315
 
308
316
  const pageName = (page.name as string) || null;
309
- const entityTitle =
310
- (resolved.proposal as { name?: string })?.name ||
311
- (resolved.form as { name?: string })?.name ||
312
- (resolved.quiz as { title?: string })?.title ||
313
- (resolved.agent as { name?: string })?.name;
317
+ const entityTitle = resolveEntityTitle(resolved);
314
318
  const bodyMetaTitle = regions.find((r) => r.slot === "body")?.metaTitle;
315
319
  const pageRootTitle = pickRootProp<string>(regions, "title");
316
320
  const pageDescription =
@@ -366,13 +370,17 @@ export function resolvedToPageData(
366
370
  },
367
371
  theme,
368
372
  meta: {
369
- title: composePageTitle({
370
- pageMetaTitle: bodyMetaTitle,
371
- templateTitle: (meta.title as string) || undefined,
372
- websiteName: (website.name as string) || undefined,
373
- pageName: pageName ?? pageRootTitle,
374
- entityTitle: entityTitle || undefined,
375
- }),
373
+ title: composePageTitle(
374
+ {
375
+ pageMetaTitle: bodyMetaTitle,
376
+ templateTitle: (meta.title as string) || undefined,
377
+ websiteName: (website.name as string) || undefined,
378
+ organisationName: (brand.companyName as string) || undefined,
379
+ pageName: pageName ?? pageRootTitle,
380
+ entityTitle: entityTitle || undefined,
381
+ },
382
+ { orgPrefix },
383
+ ),
376
384
  ...(pageDescription ? { description: pageDescription } : {}),
377
385
  },
378
386
  };
@@ -438,17 +446,7 @@ export function resolvedToPageData(
438
446
  const screenMode = pageRoot?.screenMode as string | undefined;
439
447
 
440
448
  const pageName = (page?.name as string) || null;
441
- const entityTitle =
442
- (resolved.form as { name?: string })?.name ||
443
- // A report surface carries its own document name in `report_title`
444
- // ("Personalised Authority Blueprint"); the quiz `title` names the
445
- // DIAGNOSTIC ("The Launcha Authority Index"), which is the wrong subject
446
- // for the tab of a document sent to a client. Prefer the document's name
447
- // and fall back to the quiz title for the landing page, which has none.
448
- (resolved.quiz as { report_title?: string })?.report_title ||
449
- (resolved.quiz as { title?: string })?.title ||
450
- (resolved.proposal as { name?: string })?.name ||
451
- (resolved.agent as { name?: string })?.name;
449
+ const entityTitle = resolveEntityTitle(resolved);
452
450
  const entityDescription =
453
451
  ((resolved.form as Record<string, unknown>)?.description as string) ||
454
452
  ((resolved.quiz as Record<string, unknown>)?.description as string) ||
@@ -479,13 +477,17 @@ export function resolvedToPageData(
479
477
  ...(pageDescription ? { description: pageDescription } : {}),
480
478
  ...meta,
481
479
  };
482
- mergedMeta.title = composePageTitle({
483
- pageMetaTitle,
484
- templateTitle: (meta.title as string) || undefined,
485
- websiteName: (website.name as string) || undefined,
486
- pageName: pageName ?? ((pageRoot?.title as string) || undefined),
487
- entityTitle: entityTitle || undefined,
488
- });
480
+ mergedMeta.title = composePageTitle(
481
+ {
482
+ pageMetaTitle,
483
+ templateTitle: (meta.title as string) || undefined,
484
+ websiteName: (website.name as string) || undefined,
485
+ organisationName: (brand.companyName as string) || undefined,
486
+ pageName: pageName ?? ((pageRoot?.title as string) || undefined),
487
+ entityTitle: entityTitle || undefined,
488
+ },
489
+ { orgPrefix },
490
+ );
489
491
 
490
492
  return {
491
493
  puckData,
@@ -15,6 +15,7 @@
15
15
  import type { RenderModeLiteral } from "./page-route-shell-types.ts";
16
16
  import { PageRouteConfigSchema } from "@teamscala/db-validation/registry-schemas/page-route";
17
17
  import { ResourceLoaderConfigSchema } from "@teamscala/db-validation/registry-schemas/resource-loader";
18
+ import { FeatureFlagConfigSchema } from "@teamscala/db-validation/registry-schemas/feature-flag";
18
19
  import {
19
20
  loadRegistryConfig,
20
21
  loadRegistryConfigsByType,
@@ -43,7 +44,6 @@ import {
43
44
  import { resolvedToPageData } from "./register-registry-page-route/resolve-page-data.ts";
44
45
  import {
45
46
  buildMetaFromContext,
46
- resolveTemplateTitle,
47
47
  } from "./register-registry-page-route/meta-title.ts";
48
48
  import { prefetchAiChatSession } from "./register-registry-page-route/ai-chat-prefetch.ts";
49
49
  import {
@@ -479,16 +479,24 @@ export async function registerRegistryPageRoute(
479
479
  ) {
480
480
  meta.title = pageMetaTitle.trim();
481
481
  }
482
- const templateTitle = resolveTemplateTitle(serviceSlug, resolved);
483
- if (templateTitle && !meta.title) {
484
- const resolvedEntity =
485
- (resolved.agent as { name?: string } | undefined)?.name ||
486
- (resolved.proposal as { name?: string } | undefined)?.name ||
487
- (resolved.form as { name?: string } | undefined)?.name ||
488
- (resolved.quiz as { title?: string } | undefined)?.title;
489
- if (!resolvedEntity) {
490
- meta.title = templateTitle;
491
- }
482
+ // Org-prefixed page titles (feature flag page-title-org-prefix, read
483
+ // LIVE per request — feature flags are never cached). The title prefix
484
+ // is the OWNING ORG's name, never the internal website label
485
+ // ("Public Pages: <subject>" on a client-facing document was the
486
+ // defect; operator ruling 2026-08-17). Absent or unreadable flag ⇒
487
+ // legacy <website.name>: <subject> derivation — the switch fails toward
488
+ // the pre-flag behaviour, and flipping it back is one registry edit.
489
+ let orgTitlePrefix = false;
490
+ try {
491
+ orgTitlePrefix = (
492
+ await loadRegistryConfig(
493
+ "feature_flag",
494
+ "page-title-org-prefix",
495
+ FeatureFlagConfigSchema,
496
+ )
497
+ ).enabled;
498
+ } catch {
499
+ // Flag row absent — legacy derivation.
492
500
  }
493
501
  const regionSlots = (
494
502
  loaderConfig as v.InferOutput<typeof ResourceLoaderConfigSchema> & {
@@ -515,6 +523,7 @@ export async function registerRegistryPageRoute(
515
523
  routeProviders !== baseProviders ? routeProviders : undefined,
516
524
  regionSlots,
517
525
  themeOverride,
526
+ orgTitlePrefix,
518
527
  );
519
528
  const panelSlug = new URL(request.url).searchParams.get("panel");
520
529
  if (panelSlug) {