Skip to content

feat: auto text direction (RTL) in composer editor and previews - #1993

Open
giladresisi wants to merge 5 commits into
stagingfrom
rtl-auto-direction
Open

giladresisi wants to merge 5 commits into
stagingfrom
rtl-auto-direction

Conversation

@giladresisi

@giladresisi giladresisi commented Aug 30, 2026 •

Copy link
Copy Markdown
Collaborator

What kind of change does this PR introduce?

Feature (frontend, composer + previews). Adds automatic text direction (dir="auto") to the post composer: the TipTap editor emits dir="auto" on every paragraph and heading (new-launch/editor.tsx), and the text wrappers of the default preview (general.preview.component.tsx) and the Facebook, LinkedIn, Instagram, TikTok, YouTube, Pinterest and Reddit previews carry dir="auto" as well. Supporting changes the new attribute required: 'dir' added to the sanitizer whitelist (sanitize.post.content.ts), the literal indexOf('<p>') checks in strip.html.validation.ts and add.edit.modal.tsx relaxed to indexOf('<p') so paragraphs with attributes are still recognized (otherwise stripHtmlValidation returned raw HTML at publish time), and the Instagram preview caption is wrapped in its own dir="auto" span so the prepended Latin username doesn't force the caption LTR.

Why was this change needed?

A customer writing posts in Hebrew reported that Postiz has no RTL support: with the UI in English, Hebrew text in the editor and preview renders left-to-right with misplaced punctuation. Direction was only ever inherited from the document (it flips only when the whole UI language is Hebrew/Arabic). dir="auto" resolves direction from the first strong directional character, which is exactly how X, Facebook and Threads render post text (verified in their live DOM), so the editor and previews now match what gets published. The editor resolves per paragraph; previews resolve whole-post, same as the platforms.

Other information:

The first strong character decides: lines starting with neutral characters (numbers, emoji, "@") follow the first strong letter after them, matching platform behavior. The indexOf('<p') fix matters for correctness beyond previews - without it, content saved with the new attribute would have been published to platforms with literal HTML tags.

QA

  1. Open the composer (Create Post) with the UI language set to English.
  2. Type a Hebrew line, an English line, and a line starting with a number followed by Hebrew (e.g. "1. שלום"), each as its own paragraph.
  3. In the editor, the Hebrew and number-first lines are right-aligned (RTL) while the English line stays left-aligned.
  4. The global preview shows the whole post RTL (post starts with Hebrew); the Instagram preview shows the username inline followed by correctly ordered Hebrew.
  5. Schedule the post to a channel and verify the published text is plain (no literal HTML tags) and renders RTL on the platform.
  6. Edit an existing post and confirm the content loads without duplicated/double-wrapped paragraphs.
  7. Type an English-only post and confirm nothing changed for LTR content.

Checklist:

  • I have read the CONTRIBUTING guide.
  • I have signed the Contributor License Agreement (CLA) (ICLA for individuals, CCLA for entities).
  • I confirm I have not used AI to submit this PR or generate code for it.
  • I checked that there were no similar issues or PRs already open for this.
  • This PR fixes just ONE issue

@postiz-contribution postiz-contribution Bot added the contribution:approved Approved contributor label Aug 30, 2026
@strix-security

strix-security Bot commented Aug 30, 2026 •

Copy link
Copy Markdown

Strix Security Review

No security issues found.

Updated for e2860a6.


Reviewed by Strix
Re-run review · Configure security review settings

@postiz-agent

postiz-agent Bot commented Aug 30, 2026 •

Copy link
Copy Markdown

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues
✅ Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Comment on lines 907 to 917
const editor = useEditor({
extensions: [
Document,
Paragraph,
Paragraph.configure({
HTMLAttributes: {
dir: 'auto',
},
}),
Text,
Underline,
Bold,

This comment was marked as outdated.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, confirmed: sanitizePostContent stripped the per paragraph dir attribute before storage. The impact was partial (the editor re applies dir=auto from the extension config and the preview wrappers carry their own dir=auto), but stored content lost per paragraph granularity and the public share page rendered without it. Fixed in 179ba4d by adding dir to ALLOWED_ATTR.

Comment on lines 82 to 88
/>
)}
<div
dir="auto"
className="text-[14px] font-[400] whitespace-pre-line"
dangerouslySetInnerHTML={{
__html: renderContent?.[0]?.text,

This comment was marked as outdated.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed with a minimal repro: the Latin username as first strong character forced the whole caption block LTR. Fixed in fd487c8 by wrapping the caption content in its own span with dir=auto, so the username stays inline (like on Instagram itself) while the content resolves its own direction. Verifying this also surfaced that the literal indexOf('

') checks in strip.html.validation.ts and add.edit.modal.tsx missed paragraphs carrying the new dir attribute, which made stripHtmlValidation return raw HTML at publish time; fixed in the same commit by matching '<p' instead.

Comment on lines 150 to 156
existingData.posts.map((post) => ({
delay: post.delay,
content:
post.content.indexOf('<p>') > -1
post.content.indexOf('<p') > -1
? post.content
: post.content
.split('\n')

This comment was marked as outdated.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fair point, tightened to a tag-shaped check: /<p[\s>]/i in both add.edit.modal.tsx and the strip.html.validation.ts guard. It matches

and

but ignores a stray <p followed by other characters in plain text. Note the same false positive class already existed with the original indexOf('

') check for text containing a literal

, so this is now stricter than the original code as well.

Comment on lines 217 to 223
.replace(/&lt;/gi, '<');
}

if (value.indexOf('<p>') === -1 && !none) {
if (!/<p[\s>]/i.test(value) && !none) {
return value;
}

This comment was marked as outdated.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed and reproduced: markdown mode returned the paragraphs concatenated with no newlines, and the headings also lost their # prefixes since h1-h3 now carry dir=auto too. Fixed in the markdown branch by making the p and h1/h2/h3 regexes attribute-tolerant ([^>]*). Verified the output is now identical for content with and without the dir attribute.

@postiz-contribution
postiz-contribution Bot changed the base branch from staging to main August 30, 2026 12:55
@postiz-contribution
postiz-contribution Bot changed the base branch from main to staging August 30, 2026 12:56

This branch has not been deployed

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

Labels

contribution:approved Approved contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants