From 7b358e442230215911b35755009409e2825d9396 Mon Sep 17 00:00:00 2001 From: Maksim Hodasevich Date: Tue, 27 Oct 2026 13:30:45 +0100 Subject: [PATCH 1/5] fix(cms): keep pages in sync with the global blocks they embed - GlobalBlock: add an afterChange hook that revalidates the cache of every published page that embeds the block, in every locale (locale fallback can render the block on pages of other locales), and re-indexes their search embeddings for the edited locale. - GlobalBlock: the delete guard now checks every locale and the latest page drafts, so a reference that exists only in `es` or only in a draft blocks the delete. - Page search: resolve published global blocks before text extraction, and extract the text of `globalSectionSlot` blocks. --- .../cms/src/collections/GlobalBlock/config.ts | 2 + .../GlobalBlock/findPagesUsingGlobalBlock.ts | 45 ++++++++++++++++ .../hooks/preventDeleteIfReferenced.ts | 22 +++----- .../hooks/revalidatePagesUsingGlobalBlock.ts | 52 +++++++++++++++++++ .../src/collections/Page/extractPageText.ts | 4 ++ .../collections/Page/hooks/indexEmbedding.ts | 37 ++++++++----- .../collections/Page/resolveGlobalBlocks.ts | 46 ++++++++++++++++ 7 files changed, 180 insertions(+), 28 deletions(-) create mode 100644 apps/cms/src/collections/GlobalBlock/findPagesUsingGlobalBlock.ts create mode 100644 apps/cms/src/collections/GlobalBlock/hooks/revalidatePagesUsingGlobalBlock.ts create mode 100644 apps/cms/src/collections/Page/resolveGlobalBlocks.ts diff --git a/apps/cms/src/collections/GlobalBlock/config.ts b/apps/cms/src/collections/GlobalBlock/config.ts index 1e2e2ad18..f5ca9d6fb 100644 --- a/apps/cms/src/collections/GlobalBlock/config.ts +++ b/apps/cms/src/collections/GlobalBlock/config.ts @@ -5,6 +5,7 @@ import { anyone, author, or, superAdmin, user } from "@/lib/access"; import { createLocalizedDefault } from "@/lib/utils/createLocalizedDefault"; import { preventDeleteIfReferenced } from "./hooks/preventDeleteIfReferenced"; +import { revalidatePagesUsingGlobalBlock } from "./hooks/revalidatePagesUsingGlobalBlock"; export const GlobalBlock: CollectionConfig<"globalBlock"> = { access: { @@ -54,6 +55,7 @@ export const GlobalBlock: CollectionConfig<"globalBlock"> = { }, ], hooks: { + afterChange: [revalidatePagesUsingGlobalBlock], beforeDelete: [preventDeleteIfReferenced], }, labels: { diff --git a/apps/cms/src/collections/GlobalBlock/findPagesUsingGlobalBlock.ts b/apps/cms/src/collections/GlobalBlock/findPagesUsingGlobalBlock.ts new file mode 100644 index 000000000..272cb6963 --- /dev/null +++ b/apps/cms/src/collections/GlobalBlock/findPagesUsingGlobalBlock.ts @@ -0,0 +1,45 @@ +import type { PayloadRequest } from "payload"; + +import { I18N_CONFIG } from "@/lib/config/i18n"; + +export interface PageUsingGlobalBlock { + id: number; + title: string; +} + +// `blocks` is localized and drafts live in the versions table, so each locale and the latest draft must be checked. +export async function findPagesUsingGlobalBlock({ + id, + includeDrafts = false, + req, +}: { + id: number | string; + includeDrafts?: boolean; + req: PayloadRequest; +}): Promise { + const pages = new Map(); + + for (const { code: locale } of I18N_CONFIG.locales) { + for (const draft of includeDrafts ? [false, true] : [false]) { + const { docs } = await req.payload.find({ + collection: "page", + depth: 0, + draft, + locale, + overrideAccess: true, + pagination: false, + req, + select: { title: true }, + where: { "blocks.reference": { equals: id } }, + }); + + for (const doc of docs) { + if (!pages.has(doc.id)) { + pages.set(doc.id, { id: doc.id, title: doc.title }); + } + } + } + } + + return [...pages.values()]; +} diff --git a/apps/cms/src/collections/GlobalBlock/hooks/preventDeleteIfReferenced.ts b/apps/cms/src/collections/GlobalBlock/hooks/preventDeleteIfReferenced.ts index a3ba5dfc4..c60181117 100644 --- a/apps/cms/src/collections/GlobalBlock/hooks/preventDeleteIfReferenced.ts +++ b/apps/cms/src/collections/GlobalBlock/hooks/preventDeleteIfReferenced.ts @@ -1,26 +1,16 @@ import { APIError } from "payload"; import type { CollectionBeforeDeleteHook } from "payload"; +import { findPagesUsingGlobalBlock } from "../findPagesUsingGlobalBlock"; + export const preventDeleteIfReferenced: CollectionBeforeDeleteHook = async ({ id, req }) => { - const pages = await req.payload.find({ - collection: "page", - depth: 0, - limit: 100, - overrideAccess: true, - pagination: false, - req, - where: { - "blocks.reference": { equals: id }, - }, - }); + const pages = await findPagesUsingGlobalBlock({ id, includeDrafts: true, req }); - if (pages.totalDocs > 0) { - const titles = pages.docs - .map((p) => (typeof p.title === "string" ? p.title : `#${p.id}`)) - .join(", "); + if (pages.length > 0) { + const titles = pages.map((p) => p.title || `#${p.id}`).join(", "); throw new APIError( - `Cannot delete this global block: it is still used by ${pages.totalDocs} page(s): ${titles}. Remove those references first.`, + `Cannot delete this global block: it is still used by ${pages.length} page(s): ${titles}. Remove those references first.`, 400, undefined, true diff --git a/apps/cms/src/collections/GlobalBlock/hooks/revalidatePagesUsingGlobalBlock.ts b/apps/cms/src/collections/GlobalBlock/hooks/revalidatePagesUsingGlobalBlock.ts new file mode 100644 index 000000000..d0e668394 --- /dev/null +++ b/apps/cms/src/collections/GlobalBlock/hooks/revalidatePagesUsingGlobalBlock.ts @@ -0,0 +1,52 @@ +import type { CollectionAfterChangeHook } from "payload"; + +import { upsertPageEmbedding } from "@/collections/Page/hooks/indexEmbedding"; +import { I18N_CONFIG } from "@/lib/config/i18n"; +import { getLocaleFromRequest } from "@/lib/utils/getLocaleFromRequest"; +import { revalidatePageCache } from "@/lib/utils/revalidatePageCache"; +import type { GlobalBlock } from "@/payload-types"; + +import { findPagesUsingGlobalBlock } from "../findPagesUsingGlobalBlock"; + +export const revalidatePagesUsingGlobalBlock: CollectionAfterChangeHook = async ({ + doc, + previousDoc, + req, +}) => { + const { context, payload } = req; + const affectsPublished = doc._status === "published" || previousDoc?._status === "published"; + + if (context.disableRevalidate || !affectsPublished) { + return doc; + } + + const pageIds = (await findPagesUsingGlobalBlock({ id: doc.id, req })).map((p) => p.id); + if (pageIds.length === 0) { + return doc; + } + + const editedLocale = getLocaleFromRequest(req); + + // Every locale is revalidated because locale fallback can render this block on pages of other locales. + for (const { code: locale } of I18N_CONFIG.locales) { + const { docs: pages } = await payload.find({ + collection: "page", + depth: 0, + locale, + overrideAccess: true, + pagination: false, + req, + where: { _status: { equals: "published" }, id: { in: pageIds } }, + }); + + for (const page of pages) { + revalidatePageCache({ doc: page, locale, payload }); + + if (locale === editedLocale && !context.skipEmbedding) { + await upsertPageEmbedding({ locale, page, req }); + } + } + } + + return doc; +}; diff --git a/apps/cms/src/collections/Page/extractPageText.ts b/apps/cms/src/collections/Page/extractPageText.ts index 37448ee25..9940d60fb 100644 --- a/apps/cms/src/collections/Page/extractPageText.ts +++ b/apps/cms/src/collections/Page/extractPageText.ts @@ -51,6 +51,10 @@ export function extractPageBlockText(block: Page["blocks"][number]): string { case "stats": { return extractStatsText(block); } + case "globalSectionSlot": { + const inner = typeof block.reference === "object" ? block.reference.block?.[0] : undefined; + return inner ? extractPageBlockText(inner) : ""; + } default: { return ""; } diff --git a/apps/cms/src/collections/Page/hooks/indexEmbedding.ts b/apps/cms/src/collections/Page/hooks/indexEmbedding.ts index 3e6830724..1f12d1116 100644 --- a/apps/cms/src/collections/Page/hooks/indexEmbedding.ts +++ b/apps/cms/src/collections/Page/hooks/indexEmbedding.ts @@ -1,29 +1,31 @@ -import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from "payload"; +import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, PayloadRequest } from "payload"; import type { Pool } from "pg"; import { extractPageText } from "@/collections/Page/extractPageText"; +import { resolveGlobalBlocks } from "@/collections/Page/resolveGlobalBlocks"; import { getLocaleFromRequest } from "@/lib/utils/getLocaleFromRequest"; +import type { Locale } from "@/lib/types"; import type { Page } from "@/payload-types"; import { upsertEmbedding, deleteEmbedding } from "@/lib/search/dbOperations"; import { generateEmbedding } from "@/lib/search/generateEmbedding"; -export const indexPageEmbedding: CollectionAfterChangeHook = async ({ doc, req }) => { - if (doc._status !== "published") { - return doc; - } - if (req.context?.skipEmbedding) { - return doc; - } - +export async function upsertPageEmbedding({ + locale, + page, + req, +}: { + locale: Locale; + page: Page; + req: PayloadRequest; +}): Promise { try { - const locale = getLocaleFromRequest(req); - const text = extractPageText(doc); + const text = extractPageText(await resolveGlobalBlocks({ locale, page, req })); const embedding = await generateEmbedding(text); const { pool } = req.payload.db as unknown as { pool: Pool }; await upsertEmbedding({ collection: "page", - documentId: String(doc.id), + documentId: String(page.id), embedding, locale, pool, @@ -31,6 +33,17 @@ export const indexPageEmbedding: CollectionAfterChangeHook = async ({ doc, } catch (error) { req.payload.logger.error({ error }, "Failed to index page embedding"); } +} + +export const indexPageEmbedding: CollectionAfterChangeHook = async ({ doc, req }) => { + if (doc._status !== "published") { + return doc; + } + if (req.context?.skipEmbedding) { + return doc; + } + + await upsertPageEmbedding({ locale: getLocaleFromRequest(req), page: doc, req }); return doc; }; diff --git a/apps/cms/src/collections/Page/resolveGlobalBlocks.ts b/apps/cms/src/collections/Page/resolveGlobalBlocks.ts new file mode 100644 index 000000000..5cbccc5b1 --- /dev/null +++ b/apps/cms/src/collections/Page/resolveGlobalBlocks.ts @@ -0,0 +1,46 @@ +import type { PayloadRequest } from "payload"; + +import type { Locale } from "@/lib/types"; +import type { Page } from "@/payload-types"; + +type Slot = Extract; + +const slotId = (slot: Slot) => + typeof slot.reference === "object" ? slot.reference.id : slot.reference; + +export async function resolveGlobalBlocks>({ + locale, + page, + req, +}: { + locale: Locale; + page: T; + req: PayloadRequest; +}): Promise { + const blocks = page.blocks ?? []; + const ids = blocks.flatMap((b) => (b.blockType === "globalSectionSlot" ? [slotId(b)] : [])); + + if (ids.length === 0) { + return page; + } + + const { docs } = await req.payload.find({ + collection: "globalBlock", + depth: 0, + locale, + overrideAccess: true, + pagination: false, + req, + where: { _status: { equals: "published" }, id: { in: ids } }, + }); + const published = new Map(docs.map((d) => [d.id, d])); + + return { + ...page, + blocks: blocks.map((b) => + b.blockType === "globalSectionSlot" + ? { ...b, reference: published.get(slotId(b)) ?? slotId(b) } + : b + ), + }; +} From b33f5dbc168a239c2cfd7288872305214ddab22f Mon Sep 17 00:00:00 2001 From: Maksim Hodasevich Date: Tue, 27 Oct 2026 13:52:09 +0100 Subject: [PATCH 2/5] fix(cms): share one global block visibility rule and skip draft-only saves - GlobalBlock: the revalidation hook now skips a draft save over a published block (published content is unchanged) but still runs on publish and unpublish, and re-indexes page embeddings in every locale, because locale fallback changes the text of other locales too. - GlobalBlock: new `slots` module with `collectGlobalBlockIds` and `isPublishedGlobalBlock`, used by the slot renderer, page search and the SEO extractor, so all three agree on which global block content is live. - Page search: a slot with a null reference no longer throws. - SEO extractor: unpublished global blocks no longer leak into SEO content. - Remove comments from the new global block code. --- .../blocks/GlobalSectionSlot/Component.tsx | 3 +- .../GlobalBlock/findPagesUsingGlobalBlock.ts | 1 - .../hooks/revalidatePagesUsingGlobalBlock.ts | 40 ++++++++++++++----- apps/cms/src/collections/GlobalBlock/slots.ts | 21 ++++++++++ .../collections/Page/extractPageContent.ts | 28 ++++--------- .../src/collections/Page/extractPageText.ts | 4 +- .../collections/Page/resolveGlobalBlocks.ts | 28 +++++++------ 7 files changed, 78 insertions(+), 47 deletions(-) create mode 100644 apps/cms/src/collections/GlobalBlock/slots.ts diff --git a/apps/cms/src/blocks/GlobalSectionSlot/Component.tsx b/apps/cms/src/blocks/GlobalSectionSlot/Component.tsx index 265e1b869..ad0a7a370 100644 --- a/apps/cms/src/blocks/GlobalSectionSlot/Component.tsx +++ b/apps/cms/src/blocks/GlobalSectionSlot/Component.tsx @@ -1,6 +1,7 @@ import { draftMode } from "next/headers"; import { renderContentBlock } from "@/blocks/contentBlockComponents"; +import { isPublishedGlobalBlock } from "@/collections/GlobalBlock/slots"; import { filterHiddenBlocks } from "@/lib/fields/section/filterHiddenBlocks"; import type { GlobalSectionSlotBlock } from "@/payload-types"; @@ -8,7 +9,7 @@ export const GlobalSectionSlotBlockComponent = async ({ reference }: GlobalSecti if (!reference || typeof reference === "number") return null; const { isEnabled: draft } = await draftMode(); - if (!draft && reference._status !== "published") return null; + if (!draft && !isPublishedGlobalBlock(reference)) return null; const [inner] = filterHiddenBlocks(reference.block); if (!inner) return null; diff --git a/apps/cms/src/collections/GlobalBlock/findPagesUsingGlobalBlock.ts b/apps/cms/src/collections/GlobalBlock/findPagesUsingGlobalBlock.ts index 272cb6963..bbc084481 100644 --- a/apps/cms/src/collections/GlobalBlock/findPagesUsingGlobalBlock.ts +++ b/apps/cms/src/collections/GlobalBlock/findPagesUsingGlobalBlock.ts @@ -7,7 +7,6 @@ export interface PageUsingGlobalBlock { title: string; } -// `blocks` is localized and drafts live in the versions table, so each locale and the latest draft must be checked. export async function findPagesUsingGlobalBlock({ id, includeDrafts = false, diff --git a/apps/cms/src/collections/GlobalBlock/hooks/revalidatePagesUsingGlobalBlock.ts b/apps/cms/src/collections/GlobalBlock/hooks/revalidatePagesUsingGlobalBlock.ts index d0e668394..14eb4f1ca 100644 --- a/apps/cms/src/collections/GlobalBlock/hooks/revalidatePagesUsingGlobalBlock.ts +++ b/apps/cms/src/collections/GlobalBlock/hooks/revalidatePagesUsingGlobalBlock.ts @@ -1,12 +1,36 @@ -import type { CollectionAfterChangeHook } from "payload"; +import type { CollectionAfterChangeHook, PayloadRequest } from "payload"; import { upsertPageEmbedding } from "@/collections/Page/hooks/indexEmbedding"; import { I18N_CONFIG } from "@/lib/config/i18n"; -import { getLocaleFromRequest } from "@/lib/utils/getLocaleFromRequest"; import { revalidatePageCache } from "@/lib/utils/revalidatePageCache"; import type { GlobalBlock } from "@/payload-types"; import { findPagesUsingGlobalBlock } from "../findPagesUsingGlobalBlock"; +import { isPublishedGlobalBlock } from "../slots"; + +async function changesPublishedContent({ + doc, + previousDoc, + req, +}: { + doc: GlobalBlock; + previousDoc?: GlobalBlock; + req: PayloadRequest; +}): Promise { + if (isPublishedGlobalBlock(doc)) return true; + if (!isPublishedGlobalBlock(previousDoc)) return false; + + const live = await req.payload.findByID({ + collection: "globalBlock", + depth: 0, + id: doc.id, + overrideAccess: true, + req, + select: { _status: true }, + }); + + return !isPublishedGlobalBlock(live); +} export const revalidatePagesUsingGlobalBlock: CollectionAfterChangeHook = async ({ doc, @@ -14,9 +38,8 @@ export const revalidatePagesUsingGlobalBlock: CollectionAfterChangeHook { const { context, payload } = req; - const affectsPublished = doc._status === "published" || previousDoc?._status === "published"; - if (context.disableRevalidate || !affectsPublished) { + if (context.disableRevalidate || !(await changesPublishedContent({ doc, previousDoc, req }))) { return doc; } @@ -25,11 +48,8 @@ export const revalidatePagesUsingGlobalBlock: CollectionAfterChangeHook(); + + for (const block of blocks) { + const id = block.blockType === "globalSectionSlot" ? relationId(block.reference) : null; + if (id != null) ids.add(id); + } + + return [...ids]; +} + +export const isPublishedGlobalBlock = (doc: Pick | null | undefined) => + doc?._status === "published"; diff --git a/apps/cms/src/collections/Page/extractPageContent.ts b/apps/cms/src/collections/Page/extractPageContent.ts index 8a3a879f4..9d8562017 100644 --- a/apps/cms/src/collections/Page/extractPageContent.ts +++ b/apps/cms/src/collections/Page/extractPageContent.ts @@ -6,6 +6,7 @@ import type { DocStore, } from "@focus-reactive/payload-plugin-seo/content"; +import { collectGlobalBlockIds, isPublishedGlobalBlock } from "@/collections/GlobalBlock/slots"; import { I18N_CONFIG } from "@/lib/config/i18n"; import { actionLinks, @@ -21,7 +22,6 @@ import type { ImageGroup, LinkResolveCtx, LinkValue, Upload } from "@/lib/conten import type { GlobalBlock, Page } from "@/payload-types"; type Block = Page["blocks"][number]; -type GlobalBlockContent = NonNullable[number]; export function extractPageBlockContent( block: Block, @@ -153,9 +153,9 @@ export function extractPageBlockContent( const gid = relationId(b.reference); const resolved = gid != null - ? (docs.get("globalBlock", gid) as { block?: GlobalBlockContent[] } | undefined) + ? (docs.get("globalBlock", gid) as Pick | undefined) : undefined; - const inner = resolved?.block?.[0]; + const inner = isPublishedGlobalBlock(resolved) ? resolved?.block?.[0] : undefined; return inner ? extractPageBlockContent(inner as unknown as Block, ctx, docs, helpers) : []; } @@ -164,19 +164,6 @@ export function extractPageBlockContent( } } -function collectGlobalBlockIds(blocks: Block[]): (string | number)[] { - const ids = new Set(); - - for (const block of blocks) { - if (block.blockType === "globalSectionSlot") { - const id = relationId((block as { reference?: unknown }).reference); - if (id != null) ids.add(id); - } - } - - return [...ids]; -} - function mergeStores(...stores: (DocStore | null)[]): DocStore { return { get: (collection, id) => { @@ -199,16 +186,15 @@ const extractPageContent: ContentExtractor = async (values, ctx, { resolveDocs, phase1Queries.push({ collection: "globalBlock", ids: globalIds, - select: ["block"], + select: ["block", "_status"], depth: 0, }); } const docs1: DocStore = await resolveDocs(phase1Queries); - const globalDocs = globalIds.map((id) => docs1.get("globalBlock", id)).filter(Boolean) as Record< - string, - unknown - >[]; + const globalDocs = globalIds + .map((id) => docs1.get("globalBlock", id) as Pick | undefined) + .filter(isPublishedGlobalBlock) as Record[]; const phase2Queries: DocQuery[] = globalDocs.flatMap((doc) => buildRefQueries(doc)); const docs2: DocStore | null = phase2Queries.length > 0 ? await resolveDocs(phase2Queries) : null; diff --git a/apps/cms/src/collections/Page/extractPageText.ts b/apps/cms/src/collections/Page/extractPageText.ts index 9940d60fb..8327ce0a1 100644 --- a/apps/cms/src/collections/Page/extractPageText.ts +++ b/apps/cms/src/collections/Page/extractPageText.ts @@ -8,6 +8,7 @@ import { extractFaqText } from "@/blocks/Faq/extractText"; import { extractHeroText } from "@/blocks/Hero/extractText"; import { extractLogosText } from "@/blocks/Logos/extractText"; import { extractTestimonialsText } from "@/blocks/TestimonialsList/extractText"; +import { isPublishedGlobalBlock } from "@/collections/GlobalBlock/slots"; import { extractLexicalText, joinText } from "@/lib/utils/text"; import type { Page } from "@/payload-types"; @@ -52,7 +53,8 @@ export function extractPageBlockText(block: Page["blocks"][number]): string { return extractStatsText(block); } case "globalSectionSlot": { - const inner = typeof block.reference === "object" ? block.reference.block?.[0] : undefined; + const reference = typeof block.reference === "object" ? block.reference : null; + const inner = isPublishedGlobalBlock(reference) ? reference?.block?.[0] : undefined; return inner ? extractPageBlockText(inner) : ""; } default: { diff --git a/apps/cms/src/collections/Page/resolveGlobalBlocks.ts b/apps/cms/src/collections/Page/resolveGlobalBlocks.ts index 5cbccc5b1..d14eaf791 100644 --- a/apps/cms/src/collections/Page/resolveGlobalBlocks.ts +++ b/apps/cms/src/collections/Page/resolveGlobalBlocks.ts @@ -1,13 +1,10 @@ import type { PayloadRequest } from "payload"; +import { collectGlobalBlockIds, isPublishedGlobalBlock } from "@/collections/GlobalBlock/slots"; +import { relationId } from "@/lib/contentExtraction"; import type { Locale } from "@/lib/types"; import type { Page } from "@/payload-types"; -type Slot = Extract; - -const slotId = (slot: Slot) => - typeof slot.reference === "object" ? slot.reference.id : slot.reference; - export async function resolveGlobalBlocks>({ locale, page, @@ -18,7 +15,7 @@ export async function resolveGlobalBlocks>({ req: PayloadRequest; }): Promise { const blocks = page.blocks ?? []; - const ids = blocks.flatMap((b) => (b.blockType === "globalSectionSlot" ? [slotId(b)] : [])); + const ids = collectGlobalBlockIds(blocks); if (ids.length === 0) { return page; @@ -31,16 +28,21 @@ export async function resolveGlobalBlocks>({ overrideAccess: true, pagination: false, req, - where: { _status: { equals: "published" }, id: { in: ids } }, + where: { id: { in: ids } }, }); - const published = new Map(docs.map((d) => [d.id, d])); + const publishedById = new Map( + docs.filter(isPublishedGlobalBlock).map((doc) => [String(doc.id), doc]) + ); return { ...page, - blocks: blocks.map((b) => - b.blockType === "globalSectionSlot" - ? { ...b, reference: published.get(slotId(b)) ?? slotId(b) } - : b - ), + blocks: blocks.map((block) => { + if (block.blockType !== "globalSectionSlot") return block; + + const id = relationId(block.reference); + const published = id == null ? undefined : publishedById.get(String(id)); + + return published ? { ...block, reference: published } : block; + }), }; } From 5d71bebccf703f4aae01f8ca69a608429848e49d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 12:55:43 +0000 Subject: [PATCH 3/5] feat(cms): translation, preview, depth and search for global blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the global blocks correctness fixes (#211). Addresses the remaining gaps from #210, parts 1, 2, 4, 5 and 6: - Wire `globalBlock` into the translator, scheduling and comments plugins so a shared block can be AI-translated, scheduled and commented on. - Add a standalone preview route (`/[locale]/preview/global-block/[id]`) and wire `admin.livePreview` / `admin.preview`, so an editor can see a global block before publishing it to every page that embeds it. - Re-populate the slot reference at the page render depth so a global block's inner relationships get the same population as page blocks. - Index `rawHtml` block text for semantic search, stripping tags and dropping `