How I Launched a Multilingual SaaS Frontend Using Vanilla JavaScript — No Framework, No Build Step
When the build command for a production SaaS is literally echo 'Static site - no build needed', it raises an obvious question: how does a tool-heavy, multi-language product with auth, payments, and trial gating actually hold together?
FastlyConvert is a file conversion platform shipping 40+ tools across 7 languages. Its entire frontend runs on plain HTML files, vanilla JavaScript modules, Tailwind loaded via CDN, and Vercel edge rewrites — no React, no Next.js, no bundler. The decision wasn't nostalgia. It was a deliberate tradeoff driven by SEO requirements, deployment simplicity, and the nature of the product itself: dozens of long-tail landing pages where predictable, fully-rendered HTML matters more than a reactive component tree.
How the stack is actually organized
Each tool page — pdf-to-word, meeting-transcription, text-to-speech — is a standalone HTML document. Shared behavior lives in a small set of global JS modules. Vercel handles clean URLs and language-aware routing at the edge.
The full picture looks like this:
| Layer | Tool | Role |
|---|---|---|
| Pages | Standalone HTML | Each tool owns its markup, schema, and SEO |
| Styling | Tailwind CDN | No build step, no PostCSS |
| Logic | Vanilla JS (IIFE modules) | Auth, payments, i18n, trial gating |
| Routing | vercel.json rewrites |
Clean URLs mapped to real files |
| SEO | Edge Middleware | Patches canonical, title, description per language |
| i18n | Custom window.i18n |
7 languages, attribute-based DOM hydration |
The package.json build script returns nothing useful on purpose. The actual product surface is index.html, pages/**/*.html, assets/js/*.js, and vercel.json.
Routing and SEO without a framework doing the heavy lifting
Clean URLs come from Vercel rewrites, not a client-side router. A path like /pdf-to-word maps directly to pages/pdf/pdf-to-word.html — no route resolution at runtime, no hydration mismatch, no SPA overhead.
{
"rewrites": [
{ "source": "/:lang(fr|ja|es|pt|zh-CN|zh-TW)/:path*", "destination": "/:path*?lang=:lang" },
{ "source": "/pdf-to-word", "destination": "/pages/pdf/pdf-to-word.html" },
{ "source": "/meeting-transcription", "destination": "/pages/audio/meeting-transcription.html" }
]
}
Crawlers receive real, fully-rendered HTML per route. Each page can carry its own schema markup or custom meta without touching anything else. The tradeoff is duplication — with many standalone pages, naming conventions and shared script discipline become load-bearing decisions rather than nice-to-haves.
The edge middleware layer is where the SEO story gets more interesting. middleware.js handles admin route protection, normalizes default-language query params, redirects ?lang=fr to /fr/..., and patches <title>, meta description, canonical, html lang, og:url, and og:locale for localized routes — all without maintaining seven physical copies of every page.
const seoData = SEO_META[pathKey]?.[lang];
if (seoData) {
html = html.replace(/<title>[^<]*<\/title>/i, `<title>${escapeHtml(seoData.title)}</title>`);
html = html.replace(
/<meta\s+name="description"\s+content="[^"]*"\s*\/?>/i,
`<meta name="description" content="${escapeHtml(seoData.desc)}" />`
);
}
The result sits between a purely static site and a full SSR app — English-first HTML files served at the edge, with metadata adjusted per language on the way out.
Localization and business logic as plain browser scripts
The i18n system follows the same philosophy: keep it simple, keep it inspectable. Two shared JS files expose a global window.i18n object. Pages bind translations through HTML attributes, so markup stays readable and the language layer stays centralized.
<!-- HTML stays clean and readable -->
<button data-i18n="converter.startConversion">Start Conversion</button>
<p data-i18n-html="converter.filesUpTo">Files up to {size}</p>
// Client-side hydration on page load
window.i18n.t('converter.startConversion')
// → "Start Conversion" (en)
// → "開始轉換" (zh-TW)
// → "Iniciar conversión" (es)
Covering en, fr, zh-CN, zh-TW, ja, es, and pt, the dictionary is large but the behavior is straightforward. There's also a clean architectural separation worth noting: UI strings are handled client-side, while search-facing metadata is patched at the edge. Each layer has one job.
The same principle applies to auth, Stripe payments, trial gating, and conversion-specific UX — all organized as plain browser scripts rather than framework components. A static frontend doesn't mean a passive one. It just means the complexity lives in well-scoped JS files instead of a component graph.
What this approach actually tells us about modern frontend choices
The FastlyConvert architecture won't suit every product, but it exposes something worth sitting with: a lot of framework adoption is driven by defaults and team familiarity rather than genuine product requirements. For a SaaS built around many discrete, SEO-sensitive tool pages — where each route benefits from owning its own markup, where deployment simplicity reduces operational risk, and where a JS-heavy runtime adds cost without adding value — plain HTML plus edge logic is a legitimate production choice, not a compromise.
The real cost shows up at scale. Standalone pages mean duplication. Duplication means conventions need to be enforced manually. There's no compiler catching inconsistencies, no component abstraction preventing drift. That's a real maintenance burden as the page count grows.
But the gains are concrete too: zero build pipeline to maintain, fully rendered HTML for every public route, per-page SEO control without server rendering infrastructure, and a codebase where any developer can open a file and immediately understand what it does. For a solo-built or small-team product optimizing for shipping speed and search visibility, that's a reasonable set of tradeoffs — and the fact that it handles auth, payments, and multi-language routing without a framework makes it a more instructive case study than most "I built it without React" posts tend to be.
The build command being a no-op is the punchline, but the actual story is about matching architecture to product shape — and in this case, the shape called for HTML files and edge middleware, not a component framework.
Three JavaScript modules totaling roughly 2,500 lines of code. That is what stands between a modern SaaS conversion platform and the framework-heavy frontend stacks that dominate most engineering discussions today. For FastlyConvert, a utility-focused file conversion service, that stripped-back approach is not a compromise — it is a deliberate architectural bet that pays off in ways worth examining closely.
How the Core Modules Divide Responsibility
The engineering here is organized around three distinct concerns, each encapsulated in its own module. An authentication modal module, running to approximately 1,200 lines, shoulders a surprisingly wide range of duties: login and registration state, Google OAuth callback handling, JWT token expiry checks, referral code capture, and CSRF-protected request management. That is a lot of surface area for one module, and the author acknowledges it carries more responsibility than is probably ideal.
The payment module, around 1,100 lines, manages Stripe checkout sessions, subscription tier logic, file-size limits — 100MB for free users, 500MB for pro — and toast notifications. Rounding things out is a conversion helper at roughly 170 lines, which detects the active tool from the URL path and coordinates trial-limit enforcement with the upgrade modal.
The gating logic that triggers when a user hits a usage limit is deliberately unflashy. When a locked response comes back from the server, the UI checks for the presence of TrialUI.showUpgradeModal on the window object and calls it with the relevant conversion type. It is global-variable-driven, loosely coupled, and honest about what it is doing. The same behavior then propagates across every tool page without duplication of the control logic itself.
The Real-World Tradeoffs of Going Framework-Free
Building production software on plain HTML and vanilla JavaScript in 2025 generates genuine advantages — and genuine costs. On the benefit side, the HTML that ships to the browser is predictable and auditable. Pages that need strong SEO performance, fast render times, and clean social sharing metadata benefit from knowing exactly what the document contains before it hits a CDN. Page-level ownership is also unusually clean: changing a hero layout, FAQ schema, or copy structure on a single page means editing that page directly, without negotiating with a component abstraction layer. Performance, too, tends to stay naturally restrained when there is no framework runtime to accidentally bloat.
Deployment fits this model well. Static hosting on Vercel handles routing and HTTP headers, while the application itself remains largely flat files — a setup that keeps infrastructure complexity low.
The costs are equally real. Duplication across many standalone HTML pages is unavoidable, and while shared modules reduce the damage, a maintenance tax still exists. Type safety is essentially social rather than technical: without a compiler enforcing naming conventions or catching loosely coupled globals, the burden falls on team discipline. Cross-page refactors require more manual effort than a component tree would demand, and without the natural boundaries that framework abstractions impose, modules can quietly accumulate too much responsibility — as at least one of these already has.
Why This Architecture Still Makes Sense for a Specific Class of Product
The case for plain HTML plus shared JavaScript plus edge middleware is not universal, and the author does not pretend otherwise. Where it holds up is a specific intersection of product characteristics: content- or tool-heavy sites where each page benefits from owning its own HTML and metadata, where SEO carries more weight than frontend novelty, where user interactions are meaningful but not complex enough to justify a full client-side framework, and where the team genuinely values directness over abstraction.
That description maps well onto a recognizable category of products — utility SaaS platforms, conversion tools, calculators, documentation-heavy products, and content-backed acquisition sites. For these products, a heavyweight frontend runtime often adds complexity without adding capability. The middleware layer at the edge handles what would otherwise require client-side gymnastics: localization, auth state, routing logic. The result is a stack whose tradeoffs are legible from the first read of the codebase, which is arguably its most underrated quality.
The broader provocation here is not that frameworks are problematic — it is that reaching for one by default, before asking what the product actually needs, is where engineering decisions quietly go wrong. FastlyConvert's architecture will not scale to every use case, but for the category of product it serves, it demonstrates that the most maintainable choice is often the one that is easiest to explain.