Content-Disposition for Vault

DRAFT ยท Walter ๐Ÿฆ‰ ยท March 16, 2026

The Idea

Two URLs for the same file. One renders, one downloads.

URLBehavior
1.foo/deckRenders in browser (current behavior)
1.foo/deck.htmlDownloads the raw HTML file
1.foo/textRenders in browser
1.foo/text.txtDownloads the raw text file
1.foo/leafRenders PDF in browser
1.foo/leaf.pdfDownloads the PDF file

The extensionless URL is for reading. The explicit extension is for downloading the source. Same file, two access modes.

How It Works (nginx)

Current nginx config uses try_files to resolve extensionless URLs:

try_files $uri $uri.html $uri.pdf $uri.txt $uri/ =404;

This makes /deck serve deck.html. But when you request /deck.html directly, nginx serves the same file โ€” also rendered in the browser. We need to add a Content-Disposition: attachment header when the request includes an explicit extension.

The nginx change:

# If the request has an explicit extension, force download
location ~* \.(html|pdf|txt)$ {
    # Only if the extensionless version also exists
    # (i.e., this is a "format file" not just any .html)
    add_header Content-Disposition 'attachment';
}

But this is too aggressive โ€” it would make ALL .html files download, including things like the clankers dashboard that don't have extensionless variants.

Cleaner approach โ€” use a map directive:

# In http block:
map $uri $content_disposition {
    default         "";
    ~*\.(html|pdf|txt)$  "attachment";
}

# In server block:
location / {
    try_files $uri $uri.html $uri.pdf $uri.txt $uri/ =404;
    
    # If requesting with explicit extension, download
    if ($content_disposition) {
        add_header Content-Disposition $content_disposition;
    }
}

Actually, even this is too broad. The cleanest approach is probably a named location or a specific regex for the format files. But a simpler and maybe better approach:

Simplest correct approach โ€” use the $request_uri to detect explicit extensions:

location / {
    try_files $uri $uri.html $uri.pdf $uri.txt $uri/ =404;
}

# Explicit .html/.pdf/.txt requests โ†’ download
location ~* ^/.+\.(html|pdf|txt)$ {
    add_header Content-Disposition "attachment; filename=$1";
    try_files $uri =404;
}

This catches any request that literally ends in .html, .pdf, or .txt and adds the download header. Requests without extensions go through the normal try_files and render in the browser.

The Plan

Step 1: Back up the current nginx config.
cp /etc/nginx/sites-enabled/domains.conf /etc/nginx/sites-enabled/domains.conf.bak-$(date +%Y%m%d)
Step 2: Add the Content-Disposition location block to the 1.foo server block in nginx config.
Deliverable: the exact nginx diff.
โธ STOP โ€” Show the diff. Test with nginx -t.
Why: nginx config errors take down ALL sites on Vault (1.foo, clankers.discount, flawless.engineering, etc.). Must verify syntax before reloading.
How to continue: nginx -t passes, Daniel approves the diff.
Step 3: Reload nginx.
sudo nginx -s reload
Step 4: Test both modes.
Verify: curl -I https://1.foo/deck โ†’ no Content-Disposition (renders in browser)
Verify: curl -I https://1.foo/deck.html โ†’ Content-Disposition: attachment (downloads)

Edge Cases

clankers.html โ€” currently accessed as clankers.discount (different domain, no extension). Requesting clankers.discount/clankers.html would trigger download. This is probably fine โ€” nobody does that.

Upload page โ€” 1.foo/upload is a special route. No extension, not affected.

Images, audio, etc. โ€” .png, .mp3, .json are not in the regex. They behave as before.

Backups โ€” files like deck-20260316-0852z.html would also trigger download when accessed with extension. This is correct โ€” if you're requesting a backup file explicitly, downloading makes sense.

Risks

Low risk. The change only affects requests with explicit extensions. The extensionless try_files path is unchanged. Worst case: if the regex is wrong, some files download instead of rendering. Easy to revert.