---
name: content-types
description: How Anchorify renders markdown, code, JSON, YAML, CSV, TSV, and HTML.
---

# 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 —
the V3 web upload page 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 the web upload page detects binary content (a NUL byte in the
first 1KB), 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. CLI `anchorify
upload-folder` rejects binaries today — use the web upload page for
images, PDFs, and other binaries.

## 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 the dashboard, open the actions menu (`⋯`), pick **Render as…**, choose a type, submit.

### CLI

At publish time:

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

On an existing share:

```bash
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](/docs/api).

## HTML sanitization

HTML shares are rendered with [DOMPurify](https://github.com/cure53/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.

### 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

- **Column sort, filter, edit.** The table is read-only; clicking a header does nothing. This is deliberate to keep the table render simple + server-side. Heavier interactive views are a V4 conversation.
- **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](/docs/guide-lint) 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](/docs/guide-slides).

### 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](/docs/guide-slides) guide for activation, frontmatter shape, and the most common pitfalls (frontmatter not detected, missing slide separators, ignored directives).

## Next steps

- [Slide decks](/docs/guide-slides) — Marp + reveal.js.
- [Lint API](/docs/guide-lint) — pre-publish format check.
- [Web dashboard](/docs/guide-web) — the **Render as** picker.
- [Command-line](/docs/guide-cli) — `--type` and `anchorify type`.
- [REST API](/docs/api) — the `type` field on `POST` / `PATCH`.
