feat(core): slug heading ids from Portable Text content - #2254
feat(core): slug heading ids from Portable Text content#2254scottbuscemi wants to merge 3 commits into
Conversation
Render h1–h6 with an id derived from heading text so same-page anchors work without manual ids. Keep any existing id and the block _key as additional fragment targets so links survive heading renames.
🦋 Changeset detectedLatest commit: 1f2ef1f The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | 1f2ef1f | Jul 27 2026, 06:47 PM |
Scope checkThis PR changes 585 lines across 7 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/registry-verification
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | 1f2ef1f | Jul 27 2026, 06:48 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-do | 1f2ef1f | Jul 27 2026, 06:48 PM |
There was a problem hiding this comment.
This PR adds deterministic slug ids to Portable Text headings and keeps existing block ids/_keys as extra fragment targets. That approach fits EmDash’s public-rendering layer: it’s done in the component render path, adds no DB queries, and preserves backwards-compatible anchors.
What I checked:
- The new
portable-text-heading-id.tshelper, its integration intoPortableText.astro, and theBlock.astrorendering path. - Call sites (
BlockquoteGroup.astro, component index) to confirm document-wide allocation only happens in the wrapper. - The slugification/id-safety contract against the existing
slugifyutil andisSafeHtmlIdhelper. - The new unit and render tests.
Headline conclusion: the implementation is close, but a real contract bug exists for headings whose slug starts with a digit (isSafeHtmlId rejects leading digits, but allocateHeadingId does not enforce that on generated slugs). In the full PortableText.astro path this creates mismatched per-heading re-allocation in Block.astro, which can emit duplicate ids. I also flagged two smaller, non-blocking items: Block.astro now strips id from all block styles (not just headings), and the new module-level comment reads like PR prose rather than a code explanation.
Prefix slugified heading text that starts with a digit so stamped ids pass isSafeHtmlId and document-wide uniqueness is preserved. Only strip attrs.id on heading styles, and trim the helper module comment.
|
Pending discussion with the Cloudflare Blog team re: conflict of existing IDs |
What does this PR do?
Adds automatic slug
idattributes on Portable Text headings from their text (e.g. "This is a new heading" →this-is-a-new-heading) so same-page#sectionlinks work without authors setting ids by hand.Also keeps any existing id and the block
_keyas additional fragment targets (nested empty spans) so anchors survive heading renames and imported/custom ids are not dropped. Duplicate slugs in one document are disambiguated (overview,overview-2, …).Type of change
Checklist
pnpm typecheckpassespnpm lintpasses (oxlint on touched files)pnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.AI-generated code disclosure
Screenshots / test output
Unit + render tests:
packages/core/tests/unit/components/portable-text-heading-id.test.ts— 14 passedpackages/core/tests/repro/heading-slug-id.render.test.ts— 4 passed