Skip to content

feat(core): slug heading ids from Portable Text content - #2254

Open
scottbuscemi wants to merge 3 commits into
mainfrom
feat/heading-slug-ids
Open

feat(core): slug heading ids from Portable Text content#2254
scottbuscemi wants to merge 3 commits into
mainfrom
feat/heading-slug-ids

Conversation

@scottbuscemi

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds automatic slug id attributes on Portable Text headings from their text (e.g. "This is a new heading" → this-is-a-new-heading) so same-page #section links work without authors setting ids by hand.

Also keeps any existing id and the block _key as 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

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes (oxlint on touched files)
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: n/a (maintainer PR)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: grok-4.5 via opencode

Screenshots / test output

Unit + render tests:

  • packages/core/tests/unit/components/portable-text-heading-id.test.ts — 14 passed
  • packages/core/tests/repro/heading-slug-id.render.test.ts — 4 passed

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-bot

changeset-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1f2ef1f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
emdash Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Major
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Minor
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

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

@github-actions github-actions Bot added the review/needs-review No maintainer or bot review yet label Jul 27, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 27, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground 1f2ef1f Jul 27 2026, 06:47 PM

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This 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.

@pkg-pr-new

pkg-pr-new Bot commented Jul 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@2254

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@2254

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@2254

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@2254

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@2254

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@2254

emdash

npm i https://pkg.pr.new/emdash@2254

create-emdash

npm i https://pkg.pr.new/create-emdash@2254

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@2254

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2254

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2254

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2254

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2254

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2254

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2254

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@2254

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@2254

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@2254

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@2254

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@2254

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@2254

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2254

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@2254

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@2254

commit: 1f2ef1f

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 27, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache 1f2ef1f Jul 27 2026, 06:48 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 27, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-do 1f2ef1f Jul 27 2026, 06:48 PM

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ts helper, its integration into PortableText.astro, and the Block.astro rendering 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 slugify util and isSafeHtmlId helper.
  • 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.

Comment thread packages/core/src/components/portable-text-heading-id.ts
Comment thread packages/core/src/components/Block.astro Outdated
Comment thread packages/core/src/components/portable-text-heading-id.ts
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jul 27, 2026
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.
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jul 27, 2026
@scottbuscemi

Copy link
Copy Markdown
Collaborator Author

Pending discussion with the Cloudflare Blog team re: conflict of existing IDs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core cla: signed review/needs-rereview Author pushed changes since the last review size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant