Skip to content

@W-22849705 feat(content): support Page Designer content blocks (fragments) - #500

Open
clavery wants to merge 5 commits into
mainfrom
work/W-22849705
Open

@W-22849705 feat(content): support Page Designer content blocks (fragments)#500
clavery wants to merge 5 commits into
mainfrom
work/W-22849705

Conversation

@clavery

@clavery clavery commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds first-class support for Page Designer content blocks — reusable, shared fragment.*-typed content — across the SDK, CLI, and VS Code extension. Previously the SDK bucketed these as components, so they were invisible as a distinct, shared concept.

This work item was a title-only stub ("Support 'Content Blocks' in Content Topic and Page Designer tree views"); the data model was established by exploring a live instance (zzpq-023 MarketStreet site library).

What a content block is (verified against the instance)

  • A <content> element typed fragment.* (e.g. fragment.Content.contentCard, fragment.Layout.grid) with a <display-name>.
  • A shared singleton: one definition, N incoming <content-link>s. The same block — and its identical children — appears everywhere it's linked; there is no per-placement override.
  • A Layout block keeps its regions; its region children stay plain component.* (converting a block does not convert its children). Blocks can nest inside other blocks.
  • "Convert to Content Block" rewrites an element's <type> from component.* to fragment.* and adds a <display-name> on the same content object — same content-id, no link rewiring.
  • No XSD change needed: the instance's library.xsd (26.6) is byte-identical to the bundled copy and fragment.* already validates.

Changes

SDK (@salesforce/b2c-tooling-sdk)

  • Add FRAGMENT to LibraryNodeType; classify fragment.* content as FRAGMENT.
  • Parse and expose LibraryNode.displayName.
  • Add Library.getContentBlocks() — a deduplicated catalog of a library's blocks, including unlinked ones (scans the full content set, not just the linked tree).
  • Render blocks distinctly in getTreeString ((CONTENT BLOCK: <typeId>), magenta); promote and count fragments in exportContent (new fragmentCount).

CLI (@salesforce/b2c-cli)

  • content export/content list render blocks distinctly and count them separately.
  • content list --type fragment lists the deduplicated catalog.

VS Code extension (b2c-vs-extension)

  • Per-library Content Blocks group is the single source of truth for a block (full child subtree). Because blocks are shared singletons, every page/component that links a block shows a non-expanding reference (↗) that reveals the canonical block in the group — so a block is only ever edited in one place.
  • Right-click Convert to Content Block on a component (rewrites <type> + adds display-name, re-imports the element).
  • Extract a reusable importLibraryXML helper; remove a leftover debug temp-file write in content-fs-provider.ts.

Docs/skills — updated the CLI content reference, the VS Code extension overview, and the b2c-content skill.

Notes

  • The extension tree/convert UI builds and typechecks and reuses the proven siteArchiveImport path, but could not be exercised in the Extension Dev Host headlessly — confirm the tree rendering and convert round-trip interactively before release.
  • convertToBlock is a prototype write-action; it mirrors the platform's observed mutation.

…ments)

Content blocks are reusable, shared `fragment.*`-typed Page Designer content.
Previously the SDK bucketed them as components, so they were invisible as a
distinct, shared concept across the SDK, CLI, and VS Code extension.

SDK (@salesforce/b2c-tooling-sdk):
- Add `FRAGMENT` to `LibraryNodeType`; classify `fragment.*` content as FRAGMENT
- Parse and expose `LibraryNode.displayName` (content blocks always have one)
- Add `Library.getContentBlocks()`: deduplicated catalog of a library's blocks,
  including unlinked ones (scans full content set, not just the linked tree)
- Render blocks distinctly in `getTreeString` ("(CONTENT BLOCK: <typeId>)", magenta)
- Promote and count fragments in `exportContent` (new `fragmentCount`)

CLI (@salesforce/b2c-cli):
- `content export`/`list` render blocks distinctly and count them separately
- `content list --type fragment` lists the dedup catalog via getContentBlocks

VS Code extension (b2c-vs-extension):
- Per-library "Content Blocks" group is the single source of truth for a block
  (full child subtree). Because blocks are shared singletons, every page/component
  that links a block shows a non-expanding reference (↗) that reveals the canonical
  block in the group — so a block is only ever edited in one place
- Right-click "Convert to Content Block" rewrites a component's <type> to
  fragment.* + adds a display-name and re-imports the element
- Extract reusable importLibraryXML helper; remove leftover debug temp-file write

Verified end-to-end against the zzpq-023 MarketStreet site library (leaf, Layout,
shared, nested, and unlinked content blocks). The bundled library.xsd is
byte-identical to the instance's 26.6 schema (fragment.* already validates), so no
schema change was needed.
@clavery
clavery requested a review from wei-liu-sf as a code owner June 15, 2026 00:08
clavery added 3 commits June 15, 2026 11:33
…te+recreate+relink

The initial convert prototype rewrote an element's <type> and re-imported it,
but live testing against instances proved a site-archive MERGE import silently
ignores <type> changes on an existing content element (verified by importing a
real fragment from one instance into another — the element stayed a component).

Empirically characterized the platform's actual contract (zzpq-019/013/023):
- IMPEX can CREATE a net-new fragment element fine.
- IMPEX merge will NOT change an existing element's type.
- `mode="delete"` on a content element DEEP-deletes its whole subtree AND strips
  every incoming content-link.
- So a faithful in-place conversion (matching Page Designer's "Convert to Content
  Block") requires, in ONE archive: delete the target, recreate it as fragment.*,
  recreate all descendants (cascade-deleted), and re-import every referrer so
  incoming content-links survive.

Implements that as `Library.buildContentBlockConversionXML(contentId, displayName)`:
- delete marker + recreated fragment (display-name first for XSD order; own region
  content-link types flipped component.<base>.<region> -> fragment.<base>.<region>)
- all descendants recreated; all referrers re-imported
The extension convert command now builds this archive via the SDK and imports it.

Verified end-to-end: the SDK-built archive imported into zzpq-013 produces an
element BYTE-IDENTICAL to a manual BM/PD conversion, with the parent's
content-link preserved. Added SDK tests (archive structure: delete/recreate/
descendants/referrers, region-link rename) including xmllint validation against
the bundled library.xsd.
… plain merge import

Re-tested the platform on the same instances (zzpq-019 fragment / zzpq-013
component, neither the default): a plain site-archive MERGE import of a re-typed
element now applies the component.* -> fragment.* change IN PLACE and preserves
incoming content-links. This is the behavior that was broken when the feature was
built; it has since been fixed on the platform.

Verified live: merging just the convertToFragment()-transformed element into
zzpq-013 flips the type (byte-identical to the manual conversion on zzpq-019) with
the parent carousel's content-link intact. (Also observed the merge is one-way:
it promotes component->fragment but won't demote fragment->component.)

The delete+recreate+recreate-descendants+re-import-referrers machinery built to
work around the old immutability is therefore no longer needed — and was heavier
and riskier (deep-deletes subtrees). Removed it:
- SDK: drop Library.buildContentBlockConversionXML() and the childLinkIds helper;
  keep LibraryNode.convertToFragment() (type rewrite + display-name in schema
  order + own region-link type flip).
- Extension: convertToBlock now calls convertToFragment() + generateContentXML()
  + a normal importLibraryXML() merge (the same single-element import path used
  elsewhere).
- Tests: drop the conversion-archive suite; convertToFragment tests (incl. xmllint
  schema validation) remain.
# Conflicts:
#	packages/b2c-vs-extension/src/content-tree/content-fs-provider.ts
@clavery
clavery deployed to b2c-docs-preview August 12, 2026 20:59 — with GitHub Actions Active
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

📘 Docs preview

Your documentation changes are published at:

https://d3uhw92m2zac57.cloudfront.net/pr-500/

Commit eee4a35 · ✅ Published

This preview updates on every push and is removed when the PR is closed.

…ps region links

Re-tested convert on a Layout grid WITH children (zzpq-019 MarketStreet-SharedLibrary).
The simple single-element merge — which I switched to in d20c0ad after finding the
platform now applies component.*->fragment.* type changes in a merge — turns out to
DROP the converted element's own region content-links. A converted grid loses its
column_N links and its children are orphaned (they still exist, but the block no
longer references them). Reproduced by importing an XSD-valid archive that DID include
the flipped fragment.Layout.grid.column_N links; the platform discarded them on the
in-place type change.

Delete+recreate preserves the region links (verified live: the grid re-exports as
fragment.Layout.grid with both column links intact and children attached). So the
machinery removed in d20c0ad is required after all — this reverts that simplification
and restores Library.buildContentBlockConversionXML() + the extension wiring + tests.

Updated the rationale in comments/docstrings: the reason delete+recreate is needed is
region-link preservation (merge drops them), not type immutability (merge now handles
the type change).

This reverts commit d20c0ad.
@clavery
clavery deployed to b2c-docs-preview August 13, 2026 00:53 — with GitHub Actions Active
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant