← Work

Solo · open source

ProseEdge — Accessible Post Formatter

Role
Solo — design and engineering
Outcome
Live and open source under AGPL-3.0 at v0.1 — the formatter ships first; the accessibility study and the on-device ranking model stay off until their evaluation exists
Built with
Next.js · React 19 · TypeScript · Tailwind CSS · Property-based testing · Accessibility
ProseEdge, cover image

A local-first editor for social posts that use Unicode “bold” and “italic” letters, built to show what that styling costs before the post goes out. LinkedIn, X and Threads have no rich text, so writers substitute mathematical symbols that look like letters — and a screen reader reads them aloud as “mathematical sans-serif bold capital W”, one symbol at a time. ProseEdge styles the post across serif, sans, script, fraktur, double-struck and monospace alphabets, then sets the cost directly beside the choice: what a screen reader announces, how many extra characters the platform counts, and which letters a style cannot cover. Nothing typed leaves the browser. Built solo, end to end.

Problem

Every free “LinkedIn bold” tool performs the same swap — letters for Mathematical Alphanumeric Symbols — and none of them mention what it does to the people reading. The styled words are no longer searchable, each one costs two code units against the platform’s limit, and assistive technology either spells them out symbol by symbol or skips them. A second class of failure is quieter: most tools convert one way, so once a post is styled the plain words are gone, and a character the target alphabet does not cover is silently replaced with a lookalike rather than reported.

Approach

ProseEdge keeps the plain text as the single source of truth. The document is a tree of spans holding canonical ASCII, with styling held as metadata and applied only at render — so normalize(render(doc)) === sourceText(doc) for every document and every style combination. That invariant is property-tested over generated documents rather than asserted, and it is what makes every transform reversible and the plain-text copy always one click away. Pasted styled text is parsed back into the same tree, so styles are recovered rather than discarded. The screen-reader transcript is generated from the Unicode character names of all 726 styled code points, tested against UnicodeData.txt, and per-platform length budgets show the extra cost of each styled letter, with rules that are assumed rather than verified labelled as such. Privacy is enforced, not promised: an end-to-end test fails the build if anything typed leaves the page. The research features — an accessibility score and a compressed ranking model for openings that runs in the browser — are designed but kept switched off until they have a measurement behind them.

Screens

Launch film · 21 seconds, with sound.
The words 'We're hiring' in bold, above the screen-reader transcript of the same words: 'mathematical sans-serif bold capital w, mathematical sans-serif bold small e' and so on
The problem in one frame. Two words of Unicode bold, and what a screen reader may say in their place.
The ProseEdge landing page in dark mode, with the headline 'Unicode bold is not rich text' beside a specimen card showing 'We're hiring' in bold, italic, script, fraktur, double-struck and underline
The landing page. It leads with the trade-off rather than the styles, because the trade-off is what no other formatter shows.
The formatter: a toolbar of alphabet, emphasis, list and case controls over a draft post, a LinkedIn character budget showing 196 of 3,000 with 'styling adds 23', and a Checks panel reading the styled words out as a screen reader may
The formatter. Styling happens at the top; its cost in characters and in speech is shown directly beneath it, not in a separate panel.
Architecture diagram: typing and pasting both feed one document tree of ASCII spans with styles as metadata; render() produces the styled post, sourceText() the plain text, and normalize() maps the first back to the second
The document model. Styling is metadata over canonical text, so normalising the rendered post always returns the source — the invariant the test suite checks.