Here's the idea: take standard JavaScript modules (ESM) and turn them into direct endpoints for generating any text-based web assets — HTML files, CSS, SVG, or even JSON and Markdown — using a simple file naming convention with a default export as a string (JavaScript Template Literal). Sounds super simple and kinda like PHP, right? But what does this actually give us?
Let's dive into why JSDA (JavaScript Distributed Assets) might just be the thing that makes web development "great again" after a thousand wrong turns.
Basic Example
// Create a file called index.html.js
// Import a method to access data (optional):
import { getPageData } from './project-data.js';
// Fetch the necessary data (optional):
const pageData = await getPageData();
// Export the final HTML document:
export default /*html*/ `
<html>
<head>
<title>${pageData.title}</title>
</head>
<body>
<h1>${pageData.heading}</h1>
</body>
</html>
`;
This will automatically transform into plain HTML:
<html>
<head>
<title>Page Title</title>
</head>
<body>
<h1>Page Heading</h1>
</body>
</html>
Conventions
JSDA is all about following these simple conventions:
- To define the output file type, use the pattern
*.<type>.js, for examplemy-page.html.js,styles.css.js,image.svg.js, etc. - To define static generation entry points, use the pattern
index.<type>.js, for exampleindex.html.js,index.css.js,index.svg.js, etc. - A JSDA file must be a standard ESM module containing a default export as a string (
export default '...') - The directory structure of output files mirrors the source structure (we get file-system-based routing out of the box):
src/
├── index.html.js → dist/index.html
├── styles/
│ └── index.css.js → dist/styles/index.css
└── assets/
└── logo/
└── index.svg.js → dist/assets/logo/index.svg
JSDA as an Evolution of JAMStack
Simplifying things is actually pretty hard. When we strip away the excess, it feels like we might be missing something important. But in practice, it's often the opposite — extra elements in a system make it less stable and more vulnerable. Simplification is an art that might seem trivial in hindsight. But it really isn't.
When Netlify CEO Matt Biilmann proposed the JAMStack architectural concept back in 2015, that's exactly what he did — he simplified. Think about it: why do we need a CMS and a database, with all their vulnerabilities and server resource consumption, if the server ultimately just needs to serve static files? Why does a server need complex logic if we can generate the necessary assets at build time? Why not serve all static files as fast, efficiently, and securely as possible through a CDN, minimizing load and dramatically improving scalability? And most importantly—why overcomplicate things when we can achieve better results by simplifying? A pretty counterintuitive way of thinking at the time, but absolutely right.
However, in my opinion, JAMStack is too general a concept — a set of high-level recommendations that barely touch on implementation details and don't propose specific solutions to tasks that can have their own complexity. Many people still believe JAMStack has significant limitations, but that's not true — we can combine it with any other practices in any arbitrary combinations.
JSDA follows the same philosophy but with more technical substance. We take the native capabilities of the web platform (Node.js + browser) and existing standards, and without adding any new redundant entities, we solve problems that traditionally require much bulkier solutions — all while not limiting ourselves in terms of possibilities.
Hybrid Approach
If JAMStack is mostly about SSG (Static Site Generators), then JSDA plays on both fields — as an architecture applicable for generating both static and dynamic content. On top of that, JSDA doesn't restrict you from using any auxiliary technologies if needed.
Traditionally, we distinguish the following web application modes:
- SPA (Single Page Application) – everything is controlled by client-side JavaScript, the DOM tree is fully created dynamically on the client.
- SSR (Server Side Rendering) – the server pre-renders the page, which is then "hydrated" on the client with JavaScript.
- SSG (Static Site Generation) – generating HTML files at build time, which are then served as static content.
- Dynamic page generation – the server generates HTML files on request, and the result can be cached.
These approaches don't exclude each other. Each has its strengths and weaknesses, but they can be combined effectively. In complex scenarios, for instance, documentation pages or promo materials can be fully static, product pages can be partially pre-rendered on the server and partially contain dynamic widgets (f.e. shopping cart), while a user's dashboard can be fully implemented as an SPA.
And the JSDA stack is exactly suited for such complex scenarios, while remaining simple and minimalistic itself.
Async Operations (Top Level Await)
According to the ESM specifications, modules are asynchronous and support top-level async calls — Top level await. This means that when generating assets, we can make requests and fetch data across the entire chain of imports and dependencies. We can access databases, external APIs, the file system, and so on. And here's the cool part: we don't have to deal with async complexity ourselves — it's "masked" behind standard ESM mechanisms. By simply importing a dependency, we can be sure that all its data is fetched during module resolution. In my view, this is a very powerful yet underrated platform capability.
Caching
According to the standard, ESM modules are automatically cached in runtime memory upon resolution. This makes the generation process more efficient by avoiding redundant operations when reusing a module. If, on the other hand, you want to control caching and get fresh data from the import chain, you can use unique module identifiers (addresses) during import, like this:
const data = (await import(`./data.js?${Date.now()}`)).default;
You can also use query parameters accessible inside the module via import.meta, for example:
const userId = import.meta.url.split('?')[1];
const data = (await import(`./user-data.js?id=${userId}`)).default;
String Interpolation
Another cool thing we get "for free" is the ability to compose complex result strings. This native "templating engine" lets you build any complex markup or structure from components, reuse them, and inject any logic during generation. Against this backdrop, the trendy server components from the React and Next.js ecosystem look like overcomplicated nonsense.
SSR and Web Components
But how do we bring markup to life on the client? How do we bind DOM elements to data and handlers? We've got everything we need for this too — no need to invent anything extra. The solution is a standard group of specifications known as Web Components. Let me show you a simplified example of how this works.
On the server, we create the following markup:
import getUserData from './getUserData.js';
let userId = import.meta.url.split('?user-id=')[1];
const userData = await getUserData(userId);
export default /*html*/ `
<my-component user-id="${userId}">
<div id="name">${userData.name}</div>
<div id="age">${userData.age}</div>
</my-component>
`;
On the client, we register a CustomElement:
// getUserData function can be isomorphic
import getUserData from './getUserData.js';
class MyComponent extends HTMLElement {
connectedCallback() {
this.userNameEl = this.querySelector('#name');
this.userAgeEl = this.querySelector('#age');
this.userId = this.getAttribute('user-id');
// Refine and bind data if needed
getUserData(this.userId).then((data) => {
this.userNameEl.textContent = data.name;
this.userAgeEl.textContent = data.age;
});
}
}
window.customElements.define('my-component', MyComponent);
That's it! And no horrible __NEXT_DATA__ like in Next.js. The identifier for the "hydrated" node is our custom tag, which easily takes control of its DOM section. The browser handles everything through the CustomElements lifecycle. Remember — Web Components don't necessarily have to include Shadow DOM.
But what if there are lots of components with their own nested hierarchies? That's also simple—here's another example.
This time, a simple recursive function will draw us structured HTML:
import fs from 'fs';
/**
*
* @param {String} html source HTML
* @param {String} tplPathSchema component template path schema (e.g., './ref/wc/{tag-name}/tpl.html')
* @param {Object<string, string>} [data] rendering data
* @returns {Promise<String>} rendered HTML
*/
export async function wcSsr(html, tplPathSchema, data = {}) {
Object.keys(data).forEach((key) => {
html = html.replaceAll(`{[${key}]}`, data[key]);
});
const matches = html.matchAll(/<([a-z]+[\w-]+)(?:\s+[^>]*)?>/g);
for (const match of matches) {
const [fullMatch, tagName] = match;
let tplPath = tplPathSchema.replace('{tag-name}', tagName);
let tpl = '';
// Support component templates in different formats:
if (tplPath.endsWith('.html')) {
tpl = fs.existsSync(tplPath) ? fs.readFileSync(tplPath, 'utf8') : '';
} else if (tplPath.endsWith('.js') || tplPath.endsWith('.mjs')) {
try {
tpl = (await import(tplPath)).default;
} catch (e) {
console.warn('Template not found for ', tagName);
}
}
if (tpl) {
if (!tpl.includes(`<${tagName}`)) {
tpl = await wcSsr(tpl, tplPathSchema, data);
} else {
tpl = '';
console.warn(`Endless loop detected for component ${tagName}`);
}
}
html = html.replace(fullMatch, fullMatch + tpl);
}
return html;
}
For more fully-fledged work with Web Components, you can use any popular library. Personally, I use Symbiote.js, as it's adapted for working with execution-context-independent HTML templates (and more).
File Transformations
Converting JSDA files to text-based web assets is clear enough, but here's the next question: how do we represent regular text files in JSDA format?
Super easy:
import fs from 'fs';
export default fs.readFileSync('./index.html', 'utf8');
Before exporting the file content, you can perform any intermediate transformations, for example, adding the right color palette to SVG or even doing automatic document translation through an LLM call.
CDN, HTTPS Imports, and npm
Let's get back to the ESM standard and its wonderful capabilities—specifically, the ability to load modules directly from a CDN via HTTPS. This is fully supported in both Node.js and the browser. At the CDN level (or even your own endpoint), automatic intermediate bundling and minification of modules can happen. That's how many popular specialized code delivery CDNs work — jsDelivr, esm.run, esm.sh, cdnjs, and many others. This lets you efficiently manage external dependencies and separate deployment cycles for complex system components.
For managing such dependencies, a super useful tool is the native browser technology importmap. Forget about all those clunky things like Module Federation — the platform already gives us everything we need.
Also, for working with JSDA dependencies (especially in a server context), good old npm works perfectly, giving us version control out of the box.
Distributed Composition
Everything mentioned in the previous section explains the "Distributed" part in the JSDA acronym. Simple and clear composition models are super important at the ecosystem level, where solution providers may be loosely connected to each other.
In the JSDA ecosystem, creating integrable solutions is really easy because you can always rely on the most basic standards and specifications without reinventing the wheel.
Security
One of the key security mechanisms in the JSDA stack is SRI (Subresource Integrity) — integrity verification of JSDA dependencies through hashing.
Isomorphism
At the very beginning, I mentioned PHP, and you might be wondering: if JSDA works almost like PHP, why not just use PHP?
First off, PHP is a hypertext preprocessor. Working with output formats other than HTML (XML) isn't always as painless as you'd like. PHP simply doesn't have a full equivalent to template literals like JS does.
Second, JavaScript is — whether you like it or not — the only programming language on the web that's fully and natively supported on both server and client.
Third, and most importantly, by using one language you can reuse the same entities in server and client code, write so-called "isomorphic" code, and SIGNIFICANTLY save on all development-related processes, including the mental ones in your head. You don't have to switch between languages, you don't have to write tons of separate configs, and your project is easier to test and maintain with fewer resources.
Thanks to isomorphism and a unified language, JSDA, while primarily a server technology, can easily and almost seamlessly be applied on the client when needed.
TypeScript
You can't get by without TS in modern development. However, TypeScript itself, being an important ecosystem tool for static analysis, contains a bunch of complexities and controversial aspects that surface as soon as you try to do something more intricate. If you've ever developed your own libraries — you'll immediately know what I'm talking about.
The author of these lines has come to use type declarations directly in JS code in JSDoc format, along with additional *.d.ts files, as the most balanced TypeScript practice. And I'm not alone in this — I'm seeing more and more experienced developers doing the same.
Applied to JSDA, this approach gives the following benefits:
- No additional build steps: each module can be a standalone endpoint
- The code that runs is exactly the code you wrote — no messing with sourceMap bugs during debugging
- No issues with following standards (for example, ESM identifiers include file extensions — in TS this isn't mandatory)
- JSDoc is more convenient for commenting + allows automated documentation generation, which is what it was designed for anyway
But if that's not your thing — you can totally use TypeScript syntax, it's perfectly compatible with JSDA principles.
AI
When working with AI assistants in development, we face one problem that for some reason isn't talked about much yet. AI tends to reproduce popular approaches and patterns without deep analysis of their efficiency and applicability in a specific case. AI is still pretty bad at that simplification thing I mentioned earlier. React became too bloated and slow? Doesn't matter — AI will suggest using it simply because there are more code examples online. In this regard, AI behaves like a junior developer, unable to take responsibility for architectural decisions that require making choices. Meanwhile, AI itself globally needs token consumption optimization, and this could well be reflected in the technologies we use. The simplicity of our solutions, including reducing the entities we operate with, should positively influence this situation. That's why it's important to create new minimalistic patterns like JSDA, which should overall have a positive impact on the industry.
Tools
Until now, I've been talking about JSDA as a set of conventions and an architectural principle, without tying it to specific tools and not forcing anyone to use anything particular. Of course, for anyone to actually use all this, there needs to be an ecosystem of solutions that let you quickly spin up a project and get first results as fast as possible. That's exactly what I've been working on lately. I'd love to get the community involved and move forward — together.
Here's what we have at the moment:
- JSDA Manifest – concept description
- JSDA-Kit – isomorphic toolkit for working with the JSDA stack
- Symbiote.js – library for efficient work with Web Components, enabling very flexible HTML handling
- JSDA repository template for quick start
Of everything listed, Symbiote.js is the most "mature" project—the rest is in active development, but you can try it out right now.
Also, we are working on a specialized CDN that, in addition to preliminary bundling and minification of modules, will automatically issue ready files in the final format (HTML, CSS, SVG, Markdown, JSON and so on).
Top comments (0)