A teammate sends you an architecture diagram. You can read it, but the next request is to rename a service and change a route. A screenshot has already discarded the useful part: the source.
Here is the result worth preserving: a rendered API gateway diagram with services, databases, and relationships. A share link can carry the Mermaid source so the receiving editor can render—and edit—it again.
Actual Mermaid Online rendering of the separate visual example below. The codec preserved its 715 UTF-8 source bytes exactly; rendering is the editor’s job. The small Unicode test later in this article is a different, 57-byte input.
Open the visual example in the editor. Its source is included here, so the picture does not have to be taken on trust.
Open the Mermaid source used for the rendered architecture
flowchart LR
Client["Web / Mobile"] -->|HTTPS| Gateway["API Gateway"]
Gateway --> Auth["Auth Check"]
Gateway --> Limit["Rate Limit"]
Auth --> User["User Service"]
Auth --> Order["Order Service"]
Auth --> Billing["Billing Service"]
User --> UserDB[("User DB")]
Order --> OrderDB[("Order DB")]
Billing --> Provider["Payment Provider"]
Limit --> Logs["Observability"]
classDef entry fill:#dbeafe,stroke:#2563eb,color:#172554
classDef core fill:#ede9fe,stroke:#7c3aed,color:#2e1065
classDef data fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef failure fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
class Client,Gateway entry
class Auth,User,Order,Billing core
class UserDB,OrderDB data
The smaller executable test below then concentrates on the failure-prone details: Chinese text, an emoji, CRLF line endings, configuration, malformed input, and a decoded-size limit.
What the fragment contains
For the pako: format covered here, the pipeline is:
state object
→ JSON
→ UTF-8 bytes
→ zlib-wrapped DEFLATE
→ Base64URL
→ #pako:...
The compressed data is a document, not an image. A minimal state might look like this:
{
"code": "flowchart LR\n A-->B",
"mermaid": "{\"theme\":\"dark\"}"
}
Notice that mermaid is a JSON string, rather than a nested configuration object. That detail follows the recorded Mermaid Live Editor serializer used as a compatibility reference.
The pako: prefix names the format. An implementation can use a different compatible compression library.
There is also an uncompressed base64: variant. A cloud document ID, a site-specific #code= parameter, and an arbitrary Base64 string are different inputs; a decoder should not silently guess that they follow this format.
Each boundary has a different job. The diagram travels as serialized data, without loading a Mermaid renderer.
Run the example from a source checkout
The example uses mermaid-share-codec, an independent MIT project. npm publication is pending, so these instructions use a recorded Git commit rather than a registry install.
Use Node.js 22 or newer. The run reported below used Node.js 24.13.1 on Windows.
git clone https://github.com/j-river/mermaid-share-codec.git
cd mermaid-share-codec
git checkout d3d7c90eab4e5e7cb0fa22dca184e937b3f71d28
npm ci --ignore-scripts
npm run build
Save the following as roundtrip.mjs in that repository's root, then run node roundtrip.mjs.
Open the complete runnable round-trip and failure checks
import assert from 'node:assert/strict';
import { deflateSync } from 'node:zlib';
import {
createShareUrl, extractCode, decodeState, encodeState,
} from './dist/index.js';
const code = 'flowchart LR\r\n A["开始 🚀"] --> B["Review & ship"]\r\n';
const mermaid = JSON.stringify({ theme: 'dark' });
const baseUrl = 'https://mermaid.live/edit';
const url = createShareUrl({ code, mermaid }, { baseUrl });
const recovered = extractCode(url);
assert.equal(recovered.ok, true);
assert.equal(recovered.value, code); // Includes CRLF and the trailing newline.
const state = decodeState(url);
assert.equal(state.ok, true);
assert.equal(state.value.mermaid, mermaid);
assert.equal(new URL(url).origin, new URL(baseUrl).origin);
assert.equal(new URL(url).pathname, '/edit');
// A second decoder path: use Node's compressor to produce an input token.
const independent = 'pako:' + deflateSync(
Buffer.from(JSON.stringify({ code, mermaid }), 'utf8'),
).toString('base64url');
assert.equal(extractCode(independent).value, code);
const malformed = decodeState('pako:!!!!');
assert.equal(malformed.ok, false);
assert.equal(malformed.error.code, 'INVALID_BASE64');
// A small compressed input can expand beyond the application's limit.
const oversized = 'pako:' + deflateSync(Buffer.from(JSON.stringify({
code: 'A'.repeat(20_000),
}))).toString('base64url');
const bounded = decodeState(oversized, { maxDecodedBytes: 1_024 });
assert.equal(bounded.ok, false);
assert.equal(bounded.error.code, 'OUTPUT_TOO_LARGE');
assert.throws(
() => encodeState({ code, mermaid: { theme: 'dark' } }),
error => error.code === 'INVALID_STATE',
);
console.log(JSON.stringify({
roundTrip: recovered.value === code,
configRoundTrip: state.value.mermaid === mermaid,
independentlyCompressedInput: true,
malformedError: malformed.error.code,
boundedError: bounded.error.code,
urlCharacters: url.length,
sourceUtf8Bytes: Buffer.byteLength(code, 'utf8'),
}, null, 2));
The first assertions compare the recovered source string, including its CRLF line endings and final newline. A screenshot comparison would not catch those differences.
The configuration is checked separately. The URL's origin and path are also asserted, so the test verifies the selected destination as well as the payload.
The independently compressed token uses Node's zlib, rather than the codec's encoder. This checks one external input path instead of relying exclusively on an encoder and decoder agreeing with each other.
What actually passed
The example was executed on October 23, 2026. Its output was:
{
"roundTrip": true,
"configRoundTrip": true,
"independentlyCompressedInput": true,
"malformedError": "INVALID_BASE64",
"boundedError": "OUTPUT_TOO_LARGE",
"urlCharacters": 225,
"sourceUtf8Bytes": 57
}
Those size values describe this particular source and encoded state. A tiny diagram can produce a URL longer than its source because JSON fields, compression headers, the prefix, and the destination add overhead. Compression is not a promise that every input becomes smaller.
Do not snapshot the entire compressed URL as the primary compatibility test. Two compressors can emit different streams that recover the same state. Compare the decoded fields, and keep a separately sourced editor fixture or serializer test as an interoperability check.
This experiment verifies source and configuration recovery. It does not test whether the Mermaid syntax renders, or whether every target editor accepts every configuration field.
These are the error outcomes from the runnable example, not estimated robustness scores.
Make decoder limits part of the feature
The malformed input returns INVALID_BASE64. The second failure is more interesting: the token represents 20,000 repeated characters, but decoding is restricted to 1,024 uncompressed bytes.
Checking only the pasted URL length would miss the expansion. The decoder returns OUTPUT_TOO_LARGE while enforcing its output bound.
A sensible boundary has several parts:
| Boundary | What it controls |
|---|---|
| Encoded input length | How much data a caller can submit |
| Uncompressed byte limit | How much decoded JSON can be produced |
| JSON depth | How deeply the resulting data can nest |
| Application policy | Which fields the editor actually accepts |
For this codec, callers can lower the built-in ceilings. They cannot raise them arbitrarily. The defaults and error codes are documented in the repository.
These limits reduce resource use. Synchronous decompression still consumes CPU; it is not a general-purpose isolation boundary. Keep that distinction when adapting the example to an application that handles untrusted links.
Recovering data does not authorize applying it
A decoded configuration can contain settings your application should not accept.
For a source-recovery feature, extracting only code is often enough. If the application also supports configuration import, define an allowlist of supported options rather than merging arbitrary decoded fields into the renderer.
Likewise, display recovered code with a textarea or textContent. A successful JSON decode says nothing about whether text is safe to insert as HTML.
In an interactive editor, source edits should also invalidate any previously generated link. Otherwise, a user can change a label and copy a link that still contains the old diagram.
A fragment keeps the payload out of one request, not out of reach
The browser does not include the fragment in the HTTP request for the document. It processes that part of the URL on the client. MDN's fragment reference describes this behavior.
The destination page's scripts can still read the fragment. So can anyone who receives the complete link. Compression does not encrypt the source, and a self-contained link has no central record to revoke.
For confidential diagrams, choose a sharing mechanism with the access and revocation behavior the project requires. Also test the real delivery path: a browser, chat application, or receiving editor can impose practical URL-length limits below the codec's own limit.
Actual browser editor capture. The visual example was edited in the source panel and rendered successfully. The independent share-codec playground linked below exposes recovered JSON; it does not render diagrams.
Try a changed input
The browser playground exposes the source, generated link, and recovered JSON without a local setup. It performs encoding and decoding in browser memory; it does not render the diagram.
A useful next experiment is to alter only the final newline, or replace an English label with Chinese and an emoji. Check source equality after decoding. Then lower the decoded-byte limit and check that failure remains an explicit error rather than an empty diagram.
For editors that already import shared state: which settings do you deliberately refuse to import, and how do you explain that choice in the UI?
Project context: this standalone codec is maintained alongside Mermaid Online. It is independent of the official Mermaid project, and the extracted package's production integration is pending.
AI disclosure: this article was drafted with AI. The included executable example was run against the recorded source commit; the measured output above is from that run.




Top comments (0)