Fixing the Plaintext Mess: A Unicode Formatter LinkedIn Posts

LinkedIn’s post editor is a nightmare for anyone trying to share technical ideas. No bold, no italics, no proper lists—just raw plaintext that often breaks on mobile. As developers who’d rather be debating on Reddit than wrestling with LinkedIn’s limitations, we feel the pain. Clear communication matters, whether it’s a system design breakdown or a debugging tip. So, on July 4th, while others were grilling, the Strike Labs team built a Unicode-based formatting tool to make LinkedIn posts actually readable. Using React, Tailwind, and Supabase, we tackled this for the greater good of the dev community. Here’s the problem, our solution, and the code-heavy details of how we pulled it off.

The Problem: LinkedIn’s Plaintext Trap

LinkedIn forces plaintext posts for practical reasons:

  • Consistency: Uniform styling keeps the feed clean.

  • Security: No HTML or CSS blocks sketchy scripts.

  • Cross-Platform: Plaintext renders reliably on web, iOS, Android, and email.

  • Accessibility: Minimal markup simplifies screen reader output.

But most people want more. You can’t highlight key terms in a microservices post. Structured lists? Nested bullets? Good luck—whitespace collapses on mobile, and numbered lists misalign across devices. This makes sharing technical insights or—importnat updates-harder than it should be, especially when you’re trying to help the community understand complex ideas.

Our mission: build a free, open tool to fake rich text formatting on LinkedIn, making technical posts clearer for everyone. We chose a modern stack—React, Tailwind, Supabase—to deliver a solution that’s both practical and extensible.

The Solution: Unicode to the Rescue

Since LinkedIn rejects HTML and Markdown, we used Unicode’s stylized character sets, which look like formatted text but are just different code points. Examples:

  • A → 𝐀 (U+1D400, Mathematical Bold Capital A)

  • a → 𝓪 (U+1D4B6, Script Small A)

  • 1 → 𝟏 (U+1D7D9, Mathematical Bold Digit One)

This character substitution trick creates a visual effect of bold, italic, or other styles in a plaintext field. It’s not semantic, but it gets the job done for LinkedIn’s constraints.Challenges

  • Glyph Gaps: Unicode stylized sets cover A–Z, a–z, and 0–9, but punctuation and special characters (e.g., @, #) often lack equivalents.

  • No Style Nesting: You can’t stack bold and italic on one character.

  • Rendering Quirks: Some characters vary across devices or locales.

  • Accessibility: Screen readers see stylized characters as unique, not formatted, which can confuse users.

These issues required robust error handling and cross-device testing to ensure the tool works for the community.

Implementation: A Code-Heavy Solution

We built a web-based formatter to transform text into Unicode-styled output, designed for developers who value function over flash. The stack—React 18, Tailwind CSS, Supabase—delivers a clean, scalable solution. Below are the key components with detailed code.Frontend: React + Tailwind CSSThe frontend uses React for modularity and Tailwind for rapid styling. Components include:

  • TextInput: A debounced <textarea> for user input.

  • LivePreview: A <div> showing real-time output in a monospace font, mimicking LinkedIn’s feed.

  • StyleSelector: A Headless UI popover for picking styles (bold, italic, script, monospace, etc.).

  • CopyButton: Copies output to the clipboard using navigator.clipboard.

Code: Formatter Component

import { useState, useCallback } from 'react';
import { debounce } from 'lodash';
import StyleSelector from './StyleSelector';
import LivePreview from './LivePreview';
import CopyButton from './CopyButton';
import { transformText } from '../utils/unicodeTransformer';

const Formatter = () => {
  const [input, setInput] = useState('');
  const [style, setStyle] = useState('bold');
  const [output, setOutput] = useState('');
  const [error, setError] = useState(null);

  const handleInputChange = useCallback(
    debounce((value) => {
      try {
        setOutput(transformText(value, style));
        setError(null);
      } catch (err) {
        setError('Invalid style or input');
      }
    }, 300),
    [style]
  );

  const onInput = (e) => {
    const value = e.target.value;
    setInput(value);
    handleInputChange(value);
  };

  return (
    <div className="flex flex-col gap-4 p-4 max-w-3xl mx-auto bg-gray-100 rounded-lg">
      {error && <div className="text-red-500">{error}</div>}
      <StyleSelector style={style} setStyle={setStyle} />
      <textarea
        className="w-full p-3 border rounded-lg font-mono text-sm resize-y"
        value={input}
        onChange={onInput}
        placeholder="Type your post here..."
        rows={6}
      />
      <LivePreview output={output} />
      <CopyButton text={output} />
    </div>
  );
};

export default Formatter;

Code: Style Selector

javascript

const styles = ['bold', 'italic', 'script', 'monospace', 'fraktur'];

const StyleSelector = ({ style, setStyle }) => {
  return (
    <div className="relative">
      <select
        className="p-2 border rounded-lg bg-white w-full"
        value={style}
        onChange={(e) => setStyle(e.target.value)}
      >
        {styles.map(s => (
          <option key={s} value={s}>
            {s.charAt(0).toUpperCase() + s.slice(1)}
          </option>
        ))}
      </select>
    </div>
  );
};

export default StyleSelector;

Code: Unicode Transformer

javascript

const unicodeMaps = {
  bold: { A: '𝐀', B: '𝐁', a: '𝐚', b: '𝐛', '1': '𝟏', /* ... */ },
  italic: { A: '𝐴', B: '𝐵', a: '𝑎', b: '𝑏', '1': '𝟷', /* ... */ },
  script: { A: '𝒜', B: 'ℬ', a: '𝓪', b: '𝓫', '1': '𝟷', /* ... */ },
  monospace: { A: '𝙰', B: '𝙱', a: '𝚊', b: '𝚋', '1': '𝟷', /* ... */ },
  fraktur: { A: '𝔸', B: '𝔹', a: '𝔸', b: '𝔹', '1': '𝟷', /* ... */ },
};

export function transformText(input, style) {
  if (!unicodeMaps[style]) throw new Error(`Unsupported style: ${style}`);
  return input
    .split('')
    .map(char => unicodeMaps[style][char] || char)
    .join('');
}

This transformer maps characters to Unicode equivalents, with fallbacks for unmapped characters. We used lodash.debounce to optimize real-time updates and added error handling for invalid styles.Backend: Supabase for Community FeaturesWe integrated Supabase to store user presets and handle heavy transformations, ensuring the tool scales for community use.Code: Save Preset

typescript

import { createClient } from '@supabase/supabase-js';

export async function savePreset(userId: string, style: string, name: string) {
  const supabase = createClient(
    process.env.SUPABASE_URL,
    process.env.SUPABASE_KEY
  );
  const { data, error } = await supabase
    .from('presets')
    .insert({ user_id: userId, style, name })
    .select();
  if (error) throw new Error(`Preset save failed: ${error.message}`);
  return data;
}

Code: Edge Function for Transformation

typescript

import { createClient } from '@supabase/supabase-js';
import { unicodeMaps } from './unicodeMaps';

export async function transformInput(
  req: { input: string; style: string },
  context: any
): Promise<string> {
  const supabase = createClient(
    process.env.SUPABASE_URL,
    process.env.SUPABASE_KEY
  );
  const { data: map } = await supabase
    .from('unicode_maps')
    .select(req.style)
    .single();

  const activeMap = map || unicodeMaps[req.style];
  if (!activeMap) throw new Error(`Style not found: ${req.style}`);

  return req.input
    .split('')
    .map(char => activeMap[char] || char)
    .join('');
}

This setup supports community contributions by allowing dynamic style map updates via Supabase.Structured Lists: Hacking IndentationLinkedIn’s lack of semantic lists was a pain. Bullets (•, ◦, ‣) are just glyphs, and whitespace-based indentation fails on mobile. Our solution:

  • Bullet Consistency: Normalize to reliable Unicode bullets.

  • Smart Indentation: Use non-breaking spaces (\u00A0) and en spaces (\u2002).

  • Mobile Testing: Validate with BrowserStack across devices.

Code: List Formatter

export function formatList(input, bullet = '•') {
  const lines = input Rieturn input.split('\n');
  const indentChar = '\u00A0\u00A0'; // Two non-breaking spaces
  return lines
    .map((line, index) => {
      const indentLevel = (line.match(/^\s+/)?.[0].length || 0) / 2;
      return `${indentChar.repeat(indentLevel)}${bullet} ${line.trim()}`;
    })
    .join('\n');
}

This ensures lists look structured and survive mobile rendering.

Outcomes and Trade-OffsOutcomes

  • Community Value: The tool is free and open, helping devs share clearer posts on LinkedIn.

  • Stack Efficiency: React and Tailwind enabled a fast, polished UI; Supabase added scalable backend features.

  • Technical Depth: Supporting multiple Unicode sets honed our data transformation skills.

  • Dev-Friendly UX: Live previews and clipboard integration make it a breeze to use.

Trade-Offs

  • Accessibility: Unicode characters can confuse screen readers; we added UI warnings to mitigate this.

  • Glyph Limitations: Some punctuation lacks Unicode equivalents, requiring fallbacks.

  • Maintenance: Keeping style maps updated requires monitoring Unicode and LinkedIn changes.

Why This Matters for Developers

We built this because the dev community deserves better tools for sharing knowledge. LinkedIn’s plaintext editor makes technical communication harder than it needs to be, and as a team of developers, we couldn’t let that slide. This project—built with React, Tailwind, and Supabase—shows how a small, focused solution can make a big impact. It’s not just about formatting; it’s about empowering devs to express complex ideas clearly, whether you’re posting about system design or debugging tips. Fork it, hack it, make it yours.

Next
Next

The Government’s PII Paradox