Skip to content

fix(core): make editor text alignment render on public pages - #2410

Merged
ascorbic merged 2 commits into
emdash-cms:mainfrom
danielmlr:fix/portable-text-align-css
Aug 12, 2026
Merged

ascorbic merged 2 commits into
emdash-cms:mainfrom
danielmlr:fix/portable-text-align-css

Conversation

@danielmlr

@danielmlr danielmlr commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Text that you center or right-align in the editor shows with the default alignment on every public page.

The cause: #1396 made the Portable Text Block override emit WordPress-style classes like has-text-align-center, but no stylesheet ever shipped rules for them. (rg has-text-align across the repo finds the class-name helper, comments, and tests — no stylesheet.)

The fix adds the three missing rules as a scoped <style> in packages/core/src/components/Block.astro. The other public components in that directory (Code.astro, Table.astro, Columns.astro, …) follow the same pattern. Left-aligned text keeps no class and no rule, so existing content renders byte-for-byte unchanged (the #1201 convention).

The editor itself was never broken: the TipTap text-align extension styles the editing view inline. Only public rendering was affected.

Tests. tests/repro/portable-text-text-align.render.test.ts renders PortableText through the container API and pins the class that the Block override emits for each textAlign value. A fourth case pins that default-aligned blocks get no alignment class. e2e/tests/portable-text-alignment.spec.ts loads a fixture page and reads the computed text-align in the browser. If you restore Block.astro to its state on main, the e2e spec fails 3 of 4 cases.

Verified end-to-end with a build of templates/blog — the client CSS asset now contains:

.has-text-align-center[data-astro-cid-oexraql5]{text-align:center}
.has-text-align-right[data-astro-cid-oexraql5]{text-align:right}
.has-text-align-justify[data-astro-cid-oexraql5]{text-align:justify}

Closes #2285

Details

  • Size cost: ~120 bytes of CSS on pages that import emdash/ui. That touches the Comments/CommentForm CSS bleeds into all pages via emdash/ui barrel import #2039 concern about barrel CSS, but at this size a new import path did not seem worth it.
  • Scope of the new rules: they are scoped styles, so they only match blocks this component renders. A site's own has-text-align-* markup elsewhere on a page is untouched. Within the component, Astro's scoping raises specificity to .has-text-align-center[data-astro-cid-…], which outranks a plain .has-text-align-center rule a site may have added while the classes shipped unstyled. Such a rule almost certainly sets the same alignment. A site that needs a different value can raise the specificity of its own rule.
  • Commands: pnpm --filter emdash exec vitest run --config vitest.repro.config.ts (26/26); pnpm exec playwright test portable-text-alignment on both the Node and the Cloudflare target (4/4); pnpm typecheck clean; pnpm lint:json 0 diagnostics against a clean baseline; formatted with pnpm format.

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
  • 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. (n/a: no admin strings)
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/... (n/a: bug fix)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Fable 5 (Claude Code). Drafted and reviewed with Claude Fable 5 (separate sessions).

Screenshots / test output

✓ tests/repro/portable-text-text-align.render.test.ts (4 tests) 31ms

Test Files  1 passed (1)
     Tests  4 passed (4)

Full repro suite: 5 files, 26 tests passed.

@changeset-bot

changeset-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 45e8bf4

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

This PR includes changesets to release 17 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Patch
@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 Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@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 Aug 10, 2026
@pkg-pr-new

pkg-pr-new Bot commented Aug 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-verification

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 45e8bf4

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

The approach is sound: the Block override already emits has-text-align-* classes, so shipping scoped CSS for them is the right, minimal fix for the public rendering gap. Keeping the left alignment class-less and using Astro's scoped <style> matches the existing component patterns (Code.astro, Table.astro, Columns.astro) and avoids restyling arbitrary user markup. The changeset targets the correct package (emdash).

What I checked: the diff, Block.astro, the textAlignClassName helper, PortableText.astro/component exports, sibling components' scoped-style pattern, the new test, and the existing repro tests.

Headline conclusion: the fix is correct, but there are three AGENTS.md/quality issues to address before merge:

  1. Comment discipline in Block.astro — the new JSX comment justifies the scoping choice and explains why left has no rule. That context belongs in the commit message/PR description, not in the component.
  2. Comment discipline in the new test — a large JSDoc block summarises the test strategy and Astro internals, and the describe string references #2285.
  3. The test asserts an implementation proxy, not the behavior — it checks that Astro's data-astro-cid-* scoping attribute exists, which only proves a scoped <style> tag exists, not that the style rules themselves are present or correct. A regression that deletes or mistypes the actual CSS could still pass.

Fixing these keeps the diff clean and the test actually meaningful.

Comment on lines +73 to +75
{/* Converting this to `is:global` would restyle any `has-text-align-*`
markup the site renders outside Portable Text. `left` maps to no class and
needs no rule. */}

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.

[needs fixing] This JSX comment justifies the decision to use scoped styles instead of is:global and explains why left has no rule. Per AGENTS.md, comments should not justify decisions, narrate rejected alternatives, or restate what the code plainly shows (left is visibly absent from the selectors). Remove the comment.

Suggested change
{/* Converting this to `is:global` would restyle any `has-text-align-*`
markup the site renders outside Portable Text. `left` maps to no class and
needs no rule. */}
}
<style>
.has-text-align-center {

Comment on lines +6 to +13
/**
* Renders PortableText through the container API and pins that the
* `has-text-align-{value}` classes emitted by the Block override come with
* the CSS to back them. Astro stamps its scoping attribute
* (`data-astro-cid-*`) on template elements only when the component
* carries a scoped `<style>`, so the attribute next to the alignment class
* is the signal that the rules ship and target the aligned element.
*/

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.

[needs fixing] This JSDoc is a summary of the test strategy and Astro internals — exactly the kind of narrative AGENTS.md says comments should not contain. The test name and assertions should speak for themselves. Delete the block comment.

Suggested change
/**
* Renders PortableText through the container API and pins that the
* `has-text-align-{value}` classes emitted by the Block override come with
* the CSS to back them. Astro stamps its scoping attribute
* (`data-astro-cid-*`) on template elements only when the component
* carries a scoped `<style>`, so the attribute next to the alignment class
* is the signal that the rules ship and target the aligned element.
*/
import PortableText from "../../src/components/PortableText.astro";
function alignedBlock(key: string, textAlign: string | undefined, text: string) {

return html.match(/<p\b[^>]*>/g) ?? [];
}

describe("PortableText text-align CSS (#2285)", () => {

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.

[needs fixing] AGENTS.md forbids issue/PR references in code comments. A describe label is effectively reader-facing text in the suite; the issue link belongs in the PR description and commit message, not the test output. Drop #2285.

Suggested change
describe("PortableText text-align CSS (#2285)", () => {
describe("PortableText text-align CSS", () => {

const [p] = paragraphTags(html);

expect(p).toContain(`has-text-align-${align}`);
expect(p).toMatch(/data-astro-cid-/);

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.

[needs fixing] This assertion tests an Astro implementation detail (the scoping attribute exists) as a proxy for "CSS ships and targets the element." It cannot catch regressions that remove or mistype the actual .has-text-align-{align} { text-align: ... } rules while leaving an empty scoped <style> in place. Replace this with an assertion on the emitted CSS itself if renderToString returns the style tag, or mount the output and assert getComputedStyle(paragraph).textAlign via a lightweight DOM fixture.

Suggested change
expect(p).toMatch(/data-astro-cid-/);
const styleBlock = html.match(/<style[^>]*>([\s\S]*?)<\/style>/)?.[1] ?? "";
expect(styleBlock).toContain(`.has-text-align-${align}`);
expect(styleBlock).toContain(`text-align: ${align}`);

(Adjust if your Astro container setup extracts styles rather than inlining them.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed on the substance, and I moved the claim to a level that can carry it. Both mechanics you suggested turned out to be unavailable here, so this took a different route.

The container API emits no CSS at all. renderToString on this component returns only the paragraph markup:

<p class="has-text-align-center" data-astro-cid-…>Aligned</p>

There is no <style> tag to match, so a regex over the output would assert against an empty string and pass unconditionally. Importing the compiled style module directly (Block.astro?astro&type=style&index=0&lang.css) resolves to "" under vitest.repro.config.ts, and getComputedStyle needs styles the container never produces.

So the repro test now only claims what it can prove — that the Block override emits has-text-align-{align} for a block's textAlign — and the CSS claim moved to an e2e spec that loads a page and reads the computed alignment from the browser. That covers the mistyped-rule case you raised, and also the case neither test saw before: whether the scoped style reaches a rendered page at all.

The spec renders its own Portable Text value from a fixture page rather than a seeded entry, to keep the suite's shared content — and the admin visual-regression baselines — untouched.

Verified on both e2e targets: 4/4 green, and 3/4 red with Block.astro restored to its state on main, failing on the alignment assertion itself.

The repro test asserted Astro's `data-astro-cid-*` scoping attribute as a
stand-in for "the alignment rules ship and apply". That proxy cannot fail on
the regression it guards: the attribute is stamped whenever the component
carries any scoped style, so deleting or mistyping the rules keeps it green.
The container API cannot do better — `renderToString` emits markup only, and
the compiled style module resolves to an empty string under the repro config.

Move the CSS claim to the level that can see it. A fixture page renders a
Portable Text value through the public component tree, and an e2e spec reads
the computed alignment from the browser. The repro test keeps the part it can
prove on its own: the class the Block override emits per `textAlign`.

The fixture page carries its own markup instead of a seeded entry so the
suite's shared content stays untouched, which also keeps the admin
visual-regression baselines unchanged.

Drop the comments flagged in review and lower the changeset to the effect a
reader upgrading the package can observe.
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/needs-review No maintainer or bot review yet labels Aug 10, 2026
@MA2153

MA2153 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

FYI the bot doesn't read PR comments or replies to its reviews it only reads PR body and possibly other reviews (alongside commit messages).

@MA2153 MA2153 added the bot:review Trigger an emdashbot code review on this PR label Aug 10, 2026

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

Re-review of PR #2410.

Approach judgment: This is the right, minimal fix. A prior change taught the Portable Text Block override to emit has-text-align-* classes, but it never shipped the corresponding CSS, so editor alignment was lost on public pages. Adding scoped <style> rules in Block.astro matches how sibling components in packages/core/src/components/ ship their own component styles, and keeping left class-less preserves byte-for-byte rendering for existing content.

What I checked: the diff, Block.astro, the textAlignClassName helper, sibling component style patterns, the new repro render test, the new e2e test and its fixtures, and the changeset. No SQL, auth, admin-UI, or logged-out-route query changes are involved.

Status of prior findings: All three issues from the previous review are addressed in this revision:

  1. Block.astro has no new JSX comment justifying the scoping/left choice — only the existing pre-PR file-level docstring remains, which I’m not re-flagging because it predates this change.
  2. The new repro test no longer contains the JSDoc block, Astro-internals narrative, or #2285 reference — it is now a straightforward container render test.
  3. The test no longer asserts the data-astro-cid-* implementation proxy. Instead, the e2e spec asserts the actual observable behavior (computed text-align), and the unit/repro test asserts the documented class-name contract between textAlign and the Block override output.

Headline conclusion: The fix is clean, focused, and consistent with EmDash conventions. No further changes needed.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Aug 10, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Aug 11, 2026

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks!

@ascorbic
ascorbic merged commit 5a5adb7 into emdash-cms:main Aug 12, 2026
57 checks passed
@emdashbot emdashbot Bot mentioned this pull request Aug 12, 2026
@danielmlr
danielmlr deleted the fix/portable-text-align-css branch August 13, 2026 21:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core review/approved Approved; no new commits since size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

has-text-align-* classes emitted but no CSS shipped — text alignment has no visual effect

3 participants