Content types and rendering

Anchorify renders shares with one of ten content types:

Type What it does
markdown GitHub-flavored markdown via marked.
code Syntax-highlighted source via highlight.js.
json Pretty-printed JSON in a code-style frame.
yaml YAML in a code-style frame.
csv Comma-separated values as a Notion-style HTML table.
tsv Tab-separated values as a Notion-style HTML table.
html Raw HTML, sanitized via DOMPurify before rendering.
image Inline <img> from R2 storage (24h signed URL).
pdf Inline <embed> from R2 storage (24h signed URL).
binary "Can't preview" page with a Download button.

The default for text uploads is markdown. Binary uploads classify themselves at upload time based on MIME hint + filename extension — every publish surface detects binary content via a NUL-byte heuristic and routes the bytes to R2 storage instead of inlining them in the share row.

How the type is picked#

Precedence, highest to lowest:

  1. An explicit type override on the request (form field, CLI flag, or API field).
  2. The filename extension.
  3. Default to markdown.

The extension is the part after the final . in the filename, lowercased.

Extension map#

Extension Type
.md, .markdown markdown
.json json
.yaml, .yml yaml
.csv csv
.tsv tsv
.html, .htm html
.js, .ts, .tsx, .jsx, .py, .rb, .go, .rs, .java, code
.c, .cc, .cpp, .cxx, .h, .hpp, .cs, .swift, .kt,
.kts, .scala, .php, .pl, .lua, .r, .sh, .bash,
.zsh, .fish, .ps1, .sql, .dockerfile, .toml, .xml,
.svg

Anything not in the map falls through to markdown.

Binary classification#

When a publish surface detects binary content (a NUL byte in the first 1KB, or anywhere in the decoded text), it picks the content type from the MIME hint + filename:

Hint Type
image/* MIME, or .png/.jpg/.gif/.webp/.avif/.svg image
application/pdf MIME, or .pdf pdf
Anything else binary binary

The share's content column holds the sentinel "@@blob"; the actual bytes live in R2 under shares/<share_id>/<filename> and are fetched via a 24-hour signed URL on each render. The stored MIME is sniffed from the bytes, never taken from the request, so a PNG declared as text/html is still served as image/png and an HTML payload is served as an attachment.

Every surface publishes binaries ([F-CLI-BINARY]): the web /new form and the batch upload page, anchorify <file> and anchorify upload-folder, POST /api/v1/shares/upload, and the anchorify_publish_file MCP tool. See uploads. A type override doesn't apply to a binary — the tier comes from the bytes.

A markdown share with H2/H3 headings gets a Contents index listing them in document order and nested by level. H1 is left out — it is the document's title, and there is normally one of it.

Where the index sits depends on how much room the window has. On a wide screen it is pinned in the left margin beside the document, so it stays reachable wherever the reader has scrolled to — the section currently being read is marked, and a ↑ Top entry returns to the start. On a narrower window there is no margin to spare, so the index appears inline just under the title instead: a short one open, a long one collapsed with the section count visible, so it states the document's shape without burying it.

Every H2 and H3 also carries a # control, visible on hover or keyboard focus. Clicking it copies the full URL with the fragment — it deliberately does not jump, since you are already looking at the heading you clicked. Paste it to send someone one section rather than the top of a long page. Following such a link scrolls the heading clear of the page header rather than tucking it underneath.

These documentation pages carry the same index and the same # controls — try the API reference, which has over a hundred sections. They keep the inline placement at every width, because the left margin here already belongs to the docs navigation.

Non-markdown shares (code, CSV, JSON, slides, PDF) get neither: there are no headings to index.

Overriding the type#

If the extension is wrong (or there is no filename, e.g. paste-without-filename), set the type explicitly.

Web#

In the /new form, set Render as to a specific type. For an existing share, open its Share settings page (/<org>/<project>/<slug>/settings) and use the Render as picker — that surface is reachable by anyone who can edit the share, not just admins of the owning org. A row's actions menu (Render as…) is the quicker path. Both offer the same ten types.

Neither offers it on a file-backed share (image, PDF, other binary). Those aren't render choices — they describe a share whose content is the uploaded file, the API rejects them as content_type values, and switching such a share to a text type would render its internal storage sentinel as the document. Use Replace with a file instead.

CLI#

At publish time:

anchorify path/to/data.txt --type csv

On an existing share:

anchorify type my-share json

API#

In POST /api/v1/shares, pass "type": "json". To change an existing share, PATCH /api/v1/shares/<id> with {"type":"json"}. See the API reference.

HTML sanitization#

HTML shares are rendered with DOMPurify before they reach the browser. The sanitization strips:

  • All <script> tags.
  • All inline event handlers (onclick, onload, etc.).
  • javascript: URLs in href and src.
  • CSS values that look like script injection — expression(...), javascript: inside url(), and similar.

The result is HTML that renders as authored visually but cannot execute code in the visitor's browser. This is enforced; you cannot turn it off.

If your HTML relies on JavaScript to render correctly, it will not work as an HTML share. Convert it to markdown, or host it somewhere else.

CSV and TSV rendering#

CSV and TSV shares render as Notion-style HTML tables:

  • The first row is the header (<thead>).
  • Subsequent rows are the body (<tbody>).
  • Zebra striping on even rows.
  • Sticky header on vertical scroll.
  • Cells word-wrap on long values.
  • Numeric cells right-align. Any cell matching ^-?\d+(\.\d+)?$ (signed integers and simple decimals) gets right-aligned with tabular figures so numeric columns line up. Currency strings ($10), comma-thousands (1,234), dates (2024-01-02), and scientific notation (1e5) stay left-aligned.

There is no setting to disable header detection. If your file does not have a header row, the first row will still render as one. Add a header row at the top of the file before publishing.

Sorting and column resizing#

The rendered table is interactive in the browser, no setup required:

  • Click a column header to sort by that column. Each click cycles ascending → descending → original order. The arrow next to the header shows the current direction. Columns whose values are all numbers (after stripping thousands separators, currency symbols, and %) sort numerically; everything else sorts as text with natural-number ordering (item2 before item10). Empty cells always sort to the bottom. You can also focus a header with the keyboard and press Enter or Space to sort.
  • Drag the right edge of a column header to resize it. The first resize switches the table to a fixed layout so each column resizes independently; wide tables scroll horizontally inside their own box rather than stretching the page.

Sorting and resizing happen entirely in the recipient's browser and do not change the published file. Sorting operates on the rows currently rendered, so on a file large enough to trip the row cap (below) it sorts the visible preview, not the full dataset — download the full file for whole-dataset analysis.

Supported source formats#

Source Stored content type Delimiter
.csv filename csv ,
.tsv filename tsv \t
Explicit --type csv / --type tsv csv / tsv as above

The parser is RFC 4180-tolerant: quoted commas, doubled quotes ("""), and embedded newlines all parse correctly. Inside-quote newlines render as <br> so multi-line cells stay readable.

Caps and truncation#

For practicality the table renderer caps at 5,000 rows × 50 columns. When either cap trips, the share renders the first 5,000 rows × 50 columns and shows a banner above the table:

Showing first 5,000 of N rows · 50 of M columns · [Download full file]

The Download full file link points at ?raw=1 on the same share URL — a small endpoint that serves the original content with Content-Type: text/csv (or text/tab-separated-values) and Content-Disposition: attachment. Recipients can save the file locally and open it in Excel / Numbers / a spreadsheet of their choice.

If your data is bigger than the caps, the truncated preview still works as a quick-look. For full analysis the recipient downloads.

Out of scope#

  • Filtering and editing. The table sorts and resizes in the browser (above) but is otherwise read-only — there is no in-place filter box or cell editing. Edit the source file and re-publish to change the data.
  • Inferring header rows. If the file has no header, add one or live with the first data row appearing as a header.

Code rendering#

Code shares use highlight.js. The language is inferred from the filename extension via the same extension map. If the extension is not bundled in the highlight.js common subset, the share falls back to a plain <pre> with no highlighting.

The render uses the GitHub light theme.

Common pitfalls per type#

Anchorify silently fixes some authoring mistakes (UTF-8 BOM, JSON trailing commas, semicolon-delimited CSVs, leading HTML comments before slide frontmatter). For everything else there's a lint API and an owner-only warning banner on the share page. The list below covers what's worth knowing.

Markdown#

  • YAML frontmatter shows as visible text — happens when the frontmatter block is malformed (no closing ---, embedded mid-document, contains an unrecognized key). Anchorify hides frontmatter only when the block opens with ---, closes with ---, and contains at least one recognized key (title, author, date, marp, reveal, theme, tags, etc.). Strict --- divider in the middle of your document is fine — it renders as a thematic break.
  • First <h1> ate a BOM — UTF-8 byte-order marks at the top of the file. Now stripped silently.
  • Want slide-deck rendering? See Slide decks.

Code#

  • No syntax highlighting — the filename's extension isn't in our mapping (see the table above) or the language isn't bundled in highlight.js's common subset. The file still renders inside a <pre><code> block; just no token coloring.
  • No filename at all — the lint flags this. Set --type code and use a filename like script.py for highlighting.

JSON#

  • Trailing commas[1, 2, 3,] parses fine. Anchorify recovers via a second-chance parse so the rendered output is still pretty-printed.
  • Comments (// foo) / single-quoted keys — JSON5 features that we don't try to fix. The render falls back to raw highlight; the lint flags it as an error.
  • UTF-8 BOM — stripped silently before parsing.

YAML#

  • Tabs — YAML forbids tabs for indentation. The file still highlights but anyone consuming it programmatically will choke. Convert to spaces.

CSV / TSV#

  • European Excel saves with ; — Anchorify detects this on .csv files when the first row has no commas but multiple semicolons. The render is correct; the lint flags it as info so you know the file isn't strictly portable.
  • BOM in the first cell header — Excel adds one. Stripped silently.
  • No header row — first row will render as a header regardless. Add a real header before publishing.
  • Caps: 5000 rows × 50 columns. Beyond that, the recipient gets a "Download full file" link to grab the raw CSV.

HTML#

  • JavaScript stripped — DOMPurify removes all <script> tags, inline event handlers, javascript: URLs, and dangerous CSS. If your HTML needs JS to function, host it somewhere else.
  • <iframe>, <object>, <embed> also stripped — same sanitizer policy.

Image#

  • SVG with embedded <script> — rendered as inline <img> so embedded scripts won't execute when the page loads. However, if you reference the same SVG directly (?raw=1), the browser may execute the script. Treat user-supplied SVGs as untrusted.
  • Mismatched MIME vs extension — the upload form picks the type from MIME first, then extension; the renderer trusts that.

PDF#

  • Corrupt magic bytes — browser shows a blank embed. Re-export the PDF.
  • Encrypted PDFs — browser shows a password prompt. Same UX as opening the PDF locally.

Slides (Marp + reveal.js)#

  • See the dedicated Slide decks guide for activation, frontmatter shape, and the most common pitfalls (frontmatter not detected, missing slide separators, ignored directives).

Next steps#