Host JavaScript on Webflow’s Own CDN: The .txt Asset Trick
Webflow won't let you upload .js files to the Assets panel, but you can upload a .txt file and load it with a script tag. Here's how it works, why it works, and where it breaks.

Webflow’s custom code fields are fine for a few lines of JavaScript. Once your script grows to a slider, a filter, a calculator or a small app, they get awkward. Each field has a character limit, you get no syntax highlighting, and the same code ends up pasted into several pages.
Usually you’d host the file somewhere else, like GitHub plus jsDelivr, Netlify or S3. There’s a shortcut, though. Webflow’s Assets panel accepts .txt files and serves them from Webflow’s global CDN. A browser will run a .txt file as JavaScript if you load it with a <script> tag.
This post covers how to do it, why it works, and the cases where you shouldn’t.
Step 1: Write your script and save it as .txt
Write your JavaScript as usual in your editor. When it’s ready to ship, save or copy it with a .txt extension:
// slider.txt (it's JavaScript, the extension is just .txt)
(function () {
function initSlider(root) {
const track = root.querySelector('[data-slider-track]');
const next = root.querySelector('[data-slider-next]');
const prev = root.querySelector('[data-slider-prev]');
let index = 0;
function go(i) {
const slides = track.children.length;
index = (i + slides) % slides;
track.style.transform = `translateX(-${index * 100}%)`;
}
next?.addEventListener('click', () => go(index + 1));
prev?.addEventListener('click', () => go(index - 1));
}
document.querySelectorAll('[data-slider]').forEach(initSlider);
})();
Wrap the whole file in an IIFE, as above, or put it in a block. Then its variables stay out of the global scope and can’t clash with Webflow’s webflow.js or other scripts.
Minify it first if you like (npx terser slider.js -o slider.txt -c -m). The file is served as-is, so the browser downloads whatever you upload.
Step 2: Upload it to the Assets panel
- Open the Designer and press J (or click the Assets icon).
- Drag
slider.txtinto the panel. Webflow accepts it as a document asset, the same as a PDF or CSV. - Open the asset’s details and copy its URL.
The URL will look something like this:
https://cdn.prod.website-files.com/64f1c0ffee1234567890abcd/6501deadbeef1234567890ab_slider.txt
Older projects may show uploads-ssl.webflow.com or assets-global.website-files.com. They’re all Webflow’s asset CDN.
Step 3: Load it with a script tag
Go to Site settings → Custom code → Footer code for the whole site, or Page settings → Custom code → Before </body> tag for one page, and add:
<script
src="https://cdn.prod.website-files.com/64f1c0ffee1234567890abcd/6501deadbeef1234567890ab_slider.txt"
type="text/javascript"
defer
></script>
Publish, and your script runs.
Why does a .txt file run as JavaScript?
People usually say the type="text/javascript" attribute makes the browser treat the file as JavaScript. That’s only part of the story.
- The
typeattribute tells the browser what kind of script block this is.text/javascriptis the default, so you could leave it out. It doesn’t change how the file is served. - The server still sends the file with its own
Content-Typeheader. For a.txtasset that’s normallytext/plain. - For classic (non-module) scripts, browsers are lenient about that header for historical reasons. They block a few types outright: Chrome refuses scripts served as
image/*,audio/*,video/*andtext/csv. Otherwise they run the response as JavaScript, unless the server also sendsX-Content-Type-Options: nosniff.
So the trick works because browsers tolerate text/plain for classic scripts, not because of the type attribute. The next post covers what that means for ES modules and CSS, and how to check the headers yourself.
Check it for your own project before you rely on it:
curl -sI "https://cdn.prod.website-files.com/.../slider.txt" | grep -i -E "content-type|nosniff|cache-control|access-control"
If you see x-content-type-options: nosniff, the browser will block the script. Look in the Console for a “refused to execute script… MIME type” error, and use one of the alternatives at the end of this post.
What you get
- Global CDN with long caching. Asset URLs are content-addressed, so they’re aggressively cached at the edge.
- Cache busting for free. A new upload gets a new URL with a new hash prefix, so visitors never get a stale copy. You do have to update the
<script src>yourself. - No external accounts. No GitHub repo, Netlify site or bucket policy. Everything stays in the Webflow project, which helps when you hand a site to a client.
- One place for shared code. Ten pages can load the same file and the browser caches it once.
The catches
1. You must update the URL every time. A new upload means a new URL. A simple fix is to keep the <script> tag in one place (site-wide footer code or a Global Embed symbol) so you only edit it once.
2. No ES modules. <script type="module" src="...txt"> will fail. Module scripts need a real JavaScript MIME type, and text/plain is rejected. Bundle your code into a single classic script (IIFE format) before uploading.
3. No import between files. For the same reason, one .txt file can’t import another. Bundle everything into one file.
4. Not a source of truth. The Assets panel isn’t version control. Keep the real source in Git and treat the .txt upload as a build output.
5. CSS doesn’t work the same way. Browsers in standards mode won’t apply a stylesheet served as text/plain, so <link rel="stylesheet" href="styles.txt"> will be ignored. The next post explains why.
6. Subresource Integrity needs CORS. To add an integrity="sha384-..." attribute you also need crossorigin="anonymous", and the CDN must send Access-Control-Allow-Origin. Check with the curl command above before you add it.
A tidy pattern: one loader in site settings
Instead of scattering asset URLs across pages, put one small block in site-wide footer code:
<script>
window.SITE_SCRIPTS = {
slider: 'https://cdn.prod.website-files.com/.../6501..._slider.txt',
pricing: 'https://cdn.prod.website-files.com/.../6502..._pricing-calc.txt'
};
// Load a script only if the page actually uses it
document.querySelectorAll('[data-needs]').forEach(function (el) {
var key = el.getAttribute('data-needs');
if (!window.SITE_SCRIPTS[key] || document.querySelector('script[data-key="' + key + '"]')) return;
var s = document.createElement('script');
s.src = window.SITE_SCRIPTS[key];
s.defer = true;
s.dataset.key = key;
document.body.appendChild(s);
});
</script>
Then, in the Designer, add the custom attribute data-needs="slider" to any element that needs the slider script. Pages without that attribute never download it.
When to use something else
The .txt trick is great for small-to-medium scripts on marketing sites. Use a real JavaScript host such as jsDelivr from GitHub, Cloudflare Pages or Netlify when:
- you need ES modules or code splitting,
- the curl check shows
nosniff, - several developers ship code and you need CI and versioned releases,
- or you want a dev server with hot reload (covered in post 6 of this series).
For a single-developer Webflow build that needs 15 KB of custom JavaScript, uploading a .txt file is hard to beat.