Two URLs for the same file. One renders, one downloads.
| URL | Behavior |
|---|---|
1.foo/deck | Renders in browser (current behavior) |
1.foo/deck.html | Downloads the raw HTML file |
1.foo/text | Renders in browser |
1.foo/text.txt | Downloads the raw text file |
1.foo/leaf | Renders PDF in browser |
1.foo/leaf.pdf | Downloads the PDF file |
The extensionless URL is for reading. The explicit extension is for downloading the source. Same file, two access modes.
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.
cp /etc/nginx/sites-enabled/domains.conf /etc/nginx/sites-enabled/domains.conf.bak-$(date +%Y%m%d)
Content-Disposition location block to the 1.foo server block in nginx config.
nginx -t.
nginx -t passes, Daniel approves the diff.sudo nginx -s reload
curl -I https://1.foo/deck โ no Content-Disposition (renders in browser)
curl -I https://1.foo/deck.html โ Content-Disposition: attachment (downloads)
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.
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.