Word-by-word Aligner has a Share button and no database behind it. The link you copy carries the whole diagram: the text of every line, the links between words, the colors, the fonts, and the visual settings. Open it on another machine and the exact alignment comes back. Here is how the state fits into a URL, and how one is read back without trusting it.
Why not raw JSON
The app state is a plain object, so the first instinct is to JSON-encode it and drop it in a query parameter. That produces a long, fragile link. Every default value rides along, every key is spelled out in full, and a modest alignment turns into a wall of percent-encoded text. Three steps bring it down to size: trim the object, compress the result, and encode it for a URL.
Trim: short keys and no defaults
The compact form renames keys to one or two letters and, more importantly, drops anything still sitting at its default. A two-line alignment with default styling carries almost nothing beyond its words and links:
function settingsToCompact(rounded: VisualSettingsV2): CompactSettings3 | undefined {
const def = roundVisualSettings(defaultVisualSettingsV2());
const o: CompactSettings3 = {};
if (rounded.lineThickness !== def.lineThickness) o.lt = rounded.lineThickness;
if (rounded.lineOpacity !== def.lineOpacity) o.lo = rounded.lineOpacity;
if (rounded.lineStyle !== def.lineStyle) o.ls = rounded.lineStyle === 'straight' ? 0 : 1;
if (rounded.palette !== def.palette) o.pl = rounded.palette;
// ... booleans encoded as 0 / 1, the rest dropped when default
return Object.keys(o).length > 0 ? sortKeys(o) : undefined;
}
Two choices mattered here beyond the renaming. Booleans become 0 or 1. And purely local state stays out of the link: the site theme and whether the preview toolbar is collapsed are one person’s UI preferences, so putting them in a shared link would push that person’s view onto everyone who opens it. Keys are sorted so the same state always serializes to the same string.
Lines and connections use a flat text format rather than nested JSON, with tabs between fields and pipes between lines:
function encodeLines(lines: LineV2[]): string {
return lines
.map((l) => {
const cols = [
l.id, encodeURIComponent(l.rawText), l.font.family,
l.font.source === 'custom' ? 1 : 0,
l.font.customName ? encodeURIComponent(l.font.customName) : '',
String(l.textSizePx), String(l.gapWordPx)
];
if (l.rtl) cols.push('1');
return cols.join('\t');
})
.join('|');
}
Compress and encode
The trimmed object is JSON-stringified, deflated, and turned into URL-safe base64 with no padding. fflate handles the compression; base64url swaps the characters that a URL would otherwise escape:
import { deflateSync, inflateSync, strFromU8, strToU8 } from 'fflate';
export function deflateBase64url(s: string): string {
const bytes = deflateSync(strToU8(s), { level: 9 });
return toBase64url(bytes);
}
The whole encode path is two calls deep:
export function encodeState(state: AppStateV2): string {
return deflateBase64url(toCompactJSON(state));
}
A short alignment lands around a few hundred characters, well inside what browsers and chat apps accept.
Read it back without trusting it
A link is user-editable input, so the decoder assumes nothing about it. Decompression is capped, so a hand-built string cannot force a huge allocation:
const MAX_DECOMPRESSED_BYTES = 2 * 1024 * 1024; // 2 MB
export function inflateBase64url(s: string): string | null {
try {
const bytes = fromBase64url(s);
const decompressed = strFromU8(inflateSync(bytes));
if (decompressed.length > MAX_DECOMPRESSED_BYTES) return null;
return decompressed;
} catch {
return null;
}
}
The format also carries a version number, which keeps old links working after the schema changes. The decoder tries the current format first, then the previous one, then falls back to an empty document:
export function decodeState(data: string | null | undefined): AppStateV2 {
if (!data || typeof data !== 'string') return migrate({});
const v3 = tryDecodeV3(data);
if (v3) return v3;
const v2 = tryDecodeV2(data);
if (v2) return v2;
return migrate({});
}
Each attempt checks the version tag before trusting the shape, and any parse error drops to the next attempt rather than throwing. After a successful decode, the app also prunes links that point at words that no longer exist or run between lines that are no longer adjacent, so an edited or truncated URL never reopens into a broken diagram.
Where this fits
State-in-a-URL suits anything a user makes and wants to share without an account: diagrams, editors, calculators, dashboards with saved filters. The pieces are the same each time: drop defaults so the common cases stay short, compress what is left, version the format from day one, and treat every incoming link as untrusted. The result is a share feature with no storage, no auth, and no links that expire.
Word-by-word Aligner is free and runs in the browser at aligner.tinygods.dev. The code is open source (MIT) at github.com/tinygodsdev/bitext-word-alignment; the serialization lives in src/lib/serialization/.