Icon

How to Create a File Download Link

Author imagePublished on Feb 11, 2026 by

Levi

Sometimes you want a link that downloads a file instead of opening it in the browser. You're sharing a PDF, an image, or a zip archive and you want your users to save it to their device. Here's how to do it.

Before You Start: Your File Must Be Hosted Online

A question that comes up a lot is whether you can create a download link that points to a file on your own computer, something like C:\Users\me\Documents\report.pdf or /home/me/files/report.pdf. You can't. A link on a website can only point to files that are accessible on the internet through a URL. Your visitors have no way to reach files on your local hard drive.

Before you can create a download link, your file needs to be hosted somewhere publicly accessible:

Once your file has a public URL, you're ready to create a download link.

Part 1: HTML Download Links

The download Attribute

HTML5 added the download attribute to anchor tags. It tells the browser to download the linked file instead of navigating to it:

<a href="/files/report.pdf" download>Download Report</a>

When a user clicks this link, the browser saves report.pdf to their downloads folder instead of opening it in a new tab.

Custom Filenames

You can suggest a filename by giving the download attribute a value:

<a href="/files/report-q4-2026-final-v3.pdf" download="Q4-Report.pdf"> Download Report </a>

The browser saves the file as Q4-Report.pdf regardless of what it's called on the server.

The Same-Origin Limitation

There's a catch: the download attribute only works for same-origin URLs. If your file is hosted on a different domain (like a CDN or cloud storage bucket), the browser ignores the download attribute and navigates to the file instead.

<!-- This WON'T force a download because it's a different origin --> <a href="https://cdn.example.com/files/report.pdf" download> Download Report </a>

This is a security restriction in browsers. For cross-origin files, you need a server-side solution, which is what Part 2 covers.

Using download with Blob URLs

If you're generating files on the client side (exporting data as CSV, for example), you can create a blob URL and use the download attribute on it:

const data = "Name,Email\nJohn,john@example.com"; const blob = new Blob([data], { type: "text/csv" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "contacts.csv"; a.click(); URL.revokeObjectURL(url);

This works because blob URLs are always same-origin.

Part 2: Forcing Downloads from S3 and R2

When your files live in Amazon S3 or Cloudflare R2, the download attribute won't help because the files are on a different origin. The fix is to set the Content-Disposition header on the file itself.

What is Content-Disposition?

Content-Disposition is an HTTP response header that tells the browser how to handle a file. It has two values:

Content-Disposition: attachment; filename="report.pdf"

When a browser sees this header, it triggers a download regardless of the file type or which domain is serving it.

Setting Content-Disposition on S3

You can set Content-Disposition when uploading a file to S3:

const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3"); const client = new S3Client({ region: "us-east-1" }); await client.send(new PutObjectCommand({ Bucket: "my-bucket", Key: "files/report.pdf", Body: fileBuffer, ContentType: "application/pdf", ContentDisposition: 'attachment; filename="report.pdf"', }));

Every request for this object will now trigger a download.

Setting Content-Disposition on Cloudflare R2

R2 uses the same S3-compatible API, so the code is the same:

const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3"); const client = new S3Client({ region: "auto", endpoint: "https://<ACCOUNT_ID>.r2.cloudflarestorage.com", credentials: { accessKeyId: R2_ACCESS_KEY, secretAccessKey: R2_SECRET_KEY, }, }); await client.send(new PutObjectCommand({ Bucket: "my-bucket", Key: "files/report.pdf", Body: fileBuffer, ContentType: "application/pdf", ContentDisposition: 'attachment; filename="report.pdf"', }));

Using Presigned URLs with Content-Disposition

You might not want to set Content-Disposition permanently on an object. Say you want the same file to display inline on a preview page but download when someone clicks a download button. Presigned URLs let you override the response headers per request:

const { GetObjectCommand } = require("@aws-sdk/client-s3"); const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); const command = new GetObjectCommand({ Bucket: "my-bucket", Key: "files/report.pdf", ResponseContentDisposition: 'attachment; filename="report.pdf"', }); const downloadUrl = await getSignedUrl(client, command, { expiresIn: 3600, });

This generates a temporary URL that forces a download. The same object can still be viewed inline through a regular URL or a presigned URL without the ResponseContentDisposition override.

This works the same way with both S3 and R2.

Using Content-Disposition with Cloudflare Workers

If you're serving files through a Cloudflare Worker (common with R2), you can set the header dynamically in the response:

export default { async fetch(request, env) { const url = new URL(request.url); const object = await env.MY_BUCKET.get(url.pathname.slice(1)); if (!object) { return new Response("Not Found", { status: 404 }); } const headers = new Headers(); headers.set("Content-Type", object.httpMetadata?.contentType || "application/octet-stream"); // Force download const filename = url.pathname.split("/").pop(); headers.set("Content-Disposition", `attachment; filename="${filename}"`); return new Response(object.body, { headers }); }, };

This gives you full control. You could check for a ?download query parameter and only set the attachment header when the user actually wants to download.

Summary

MethodWorks Cross-Origin?Server Required?
download attributeNoNo
Content-Disposition on uploadYesYes (at upload time)
Presigned URL with overrideYesYes (at URL generation)
Worker/proxy header injectionYesYes (at serve time)

For same-origin files, the download attribute is the simplest option. For files on S3 or R2, use Content-Disposition. You can set it at upload time, override it with presigned URLs, or inject it dynamically with a worker.

Related Articles