The Simple Beauty Of A Static Website. Why index.html, styles.css and script.js might be all you need
Open the folder of a well-made company website and you might expect to find a framework, a database dump, a plugin directory and a build pipeline. Increasingly, the honest answer for a lot of businesses is three files: index.html, styles.css and script.js. Add a few more pages, an images folder and a favicon, and that is the whole site.
It sounds like a step backwards. It is closer to a correction. Most company sites are brochures: who we are, what we do, where we are, how to reach us. They change a few times a year. Yet a large share of them run on a content management system with a PHP runtime, a MySQL database, an admin login on a public URL and twenty plugins, each of which needs patching on its own schedule. That is a server attached to a brochure, and the server is where nearly all the cost and risk lives.
The case for the least powerful tool
In 2006 the W3C's Technical Architecture Group, chaired by Tim Berners-Lee, published a short finding called The Rule of Least Power. Its argument is that you should choose the least powerful language suitable for a given purpose, because less powerful languages are easier to analyse, reuse and keep working. HTML is the canonical example: a declarative document that any browser, crawler, screen reader or archive can understand without running your code.
The durability is not theoretical. The first web page, written at CERN in 1991, still loads in a modern browser today, unchanged. Try that with a WordPress install from 2012 that nobody has updated. Plain files age the way paper does; applications age the way milk does.
Engineers have a name for this instinct too. Dan McKinley's essay Choose Boring Technology argues that every company has a limited budget for novelty and should spend it where it creates an advantage. For a company information site, the advantage is almost never in the hosting stack. Boring, well-understood files let you spend your attention on the words and the offer instead.
What the three files actually do
Each file has one job, which is exactly why the arrangement stays easy to reason about:
- index.html holds the content and its structure: headings, paragraphs, images, the contact form, and the meta tags search engines and social networks read.
- styles.css holds the look: typography, colours, spacing, the layout at phone and desktop widths. Modern CSS handles grids, dark mode, animation and responsive images without a framework.
- script.js holds the small amount of behaviour a brochure site needs: a mobile menu, a form that submits without a page reload, perhaps a cookie notice.
More pages mean more HTML files (about.html, services.html, contact.html) sharing the same stylesheet and script. Once a site grows past a dozen pages and the repeated header and footer start to hurt, a static site generator such as Eleventy, Hugo or Astro can stitch them together at build time. The output is still plain files, so everything below still applies.
Speed comes free
A static page is a file sitting on a content delivery network, already close to the visitor. There is no PHP to execute, no database query to wait for and no page cache to warm or invalidate. The server's whole job is to hand over bytes.
The difference shows up in the field data. The HTTP Archive's 2025 Web Almanac, built on real Chrome user measurements, finds that WordPress "remains among the lowest-ranked" content management systems for Core Web Vitals on mobile, with a pass rate of 45%. Core Web Vitals are the loading, responsiveness and visual-stability measures Google uses as a ranking signal, so more than half of WordPress sites start with a handicap on mobile. A lean static page with optimised images passes them almost by default, because there is so little in the way.
Why static is more secure than WordPress
WordPress powers roughly four in ten websites, according to W3Techs, and its core team takes security seriously. The trouble is everything around the core. Patchstack's State of WordPress Security in 2026 counted 11,334 new vulnerabilities in the WordPress ecosystem during 2025, a 42% increase on 2024. Of those, 91% were in plugins and 9% in themes; WordPress core accounted for just six, all low priority. Two further findings matter more for a small business owner:
- 46% of the vulnerabilities had no patch available when they were disclosed, so "keep everything updated" was not enough.
- For the most heavily exploited flaws, the weighted median time from disclosure to the first mass exploitation was five hours.
Five hours is faster than most agencies answer a support ticket. Sucuri's 2023 Hacked Website and Malware Threat Report tells the same story from the clean-up side: WordPress made up 95.5% of the infected CMS sites it handled. That figure partly reflects WordPress's popularity, and Sucuri says so, but popularity is exactly what makes it worth an attacker's while to automate.
A static site removes whole categories of attack rather than defending against them:
- No code runs on request. There is no PHP interpreter for a malicious upload or a vulnerable plugin to hijack.
- No database. SQL injection needs a SQL database. There isn't one.
- No public login. There is no
/wp-adminor/wp-login.phpto brute-force, and no admin session to steal. - No plugins. The dependency list is whatever you deliberately wrote or included, and you can read all of it.
- Immutable deploys. Every version of the site is a commit in Git. If something is ever defaced, you redeploy the last good commit in a minute.
Static is not the same as invulnerable, and it is worth being precise about what remains. Your attack surface shrinks to the hosting account, the Git repository, your domain registrar and DNS, any third-party scripts you embed, and the endpoint that receives your forms. Protect the first three with strong passwords and two-factor authentication, keep third-party scripts to a minimum, and treat the form endpoint as the one real piece of backend you own. That is a short list you can actually check, rather than a moving target of plugin advisories.
You can also harden the response itself. Netlify and Cloudflare both let you ship a plain _headers file alongside your HTML to set security headers on every page (see the Netlify and Cloudflare docs):
/*
Content-Security-Policy: default-src 'self'; script-src 'self' https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com; connect-src 'self' https://n8n.example.com; img-src 'self' data:; style-src 'self'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
X-Frame-Options: DENYA content security policy that only allows your own scripts and your form endpoint is far easier to write for three files than for a theme and a page builder that load assets from a dozen places.
Hosting: GitHub, Cloudflare or Netlify
Because a static site is only files, hosting it is cheap, usually free, and portable. You can move between the three providers below in an afternoon, since none of them owns your content.
GitHub Pages
Push your files to a repository, enable Pages in the repository settings, and GitHub serves the site over HTTPS, with support for a custom domain. The published limits are generous for a brochure site: a site of up to 1 GB and a soft bandwidth limit of 100 GB a month. Two caveats. GitHub Pages does not let you set custom HTTP headers, so security headers are limited to what a <meta> tag can express. And GitHub's terms say Pages is not intended to be used as free hosting "to run your online business", an e-commerce site, or anything primarily directed at commercial transactions or SaaS. It is ideal for documentation, open-source projects and personal sites; for a company site, one of the next two is the cleaner choice.
Cloudflare
Cloudflare serves static files from its global network, and static asset requests are not metered on the free plan, so a traffic spike costs nothing. The Pages free plan allows 500 builds a month, up to 20,000 files per site at 25 MiB each, and 100 custom domains per project. Cloudflare now points new projects toward Workers with static assets, which serves the same files and lets you add a little server code later if you ever need it; existing Pages sites keep working. If your domain's DNS is already on Cloudflare, connecting it is a couple of clicks.
Netlify
Netlify is the gentlest on-ramp: you can literally drag a folder onto its dashboard and get a live URL, or connect a Git repository so every push deploys. Its distinctive feature for brochure sites is built-in form handling. Add a netlify attribute and a unique name to a plain HTML form and Netlify detects it at deploy time and stores the submissions, as its Forms documentation shows. From there, submission notifications can send each entry by email or as an HTTP POST to any URL, including an automation webhook. Netlify moved new accounts to credit-based plans in 2025, so check what your plan includes before relying on it for a high-volume form.
Forms without a backend: n8n as the glue
The usual objection to static sites is the contact form. "We need WordPress for Contact Form 7" is a sentence that has justified a great many servers. The modern answer is to post the form to a webhook in an automation tool, and let the tool deliver the data wherever it needs to go.
n8n is a good fit: it is open source, has a visual workflow editor, and can be self-hosted, which keeps personal data on infrastructure you control, a real advantage under GDPR. Its Webhook node gives you a URL that accepts a POST, with separate test and production URLs, a configurable response, an Allowed Origins setting for CORS and an optional IP allowlist. Make, Zapier, Pipedream and Activepieces do the same job in slightly different ways; the pattern below works with any of them.
On the page, the form is ordinary HTML. A hidden honeypot field catches the laziest bots, and a Cloudflare Turnstile widget handles the rest without making people click traffic lights:
<form id="contact" action="https://n8n.example.com/webhook/contact" method="POST">
<label>Name <input name="name" required></label>
<label>Email <input name="email" type="email" required></label>
<label>Message <textarea name="message" required></textarea></label>
<input name="company_website" tabindex="-1" autocomplete="off" hidden>
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Send</button>
<p id="contact-status" role="status"></p>
</form>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>In script.js, a few lines send the form in the background and show the result inline, following the approach described on MDN. Without JavaScript, the action attribute still submits the form the old-fashioned way:
const form = document.getElementById('contact');
const status = document.getElementById('contact-status');
form.addEventListener('submit', async (event) => {
event.preventDefault();
status.textContent = 'Sending...';
try {
const response = await fetch(form.action, {
method: 'POST',
body: new FormData(form),
});
if (!response.ok) throw new Error(response.status);
form.reset();
status.textContent = 'Thanks, we will be in touch shortly.';
} catch {
status.textContent = 'Something went wrong. Please email us instead.';
}
});In n8n, the workflow reads left to right:
- Webhook receives the POST. Set Allowed Origins to your own domain and the response mode to "Using Respond to Webhook node".
- IF drops the request if the honeypot field is filled in.
- HTTP Request posts the Turnstile token and your secret key to Cloudflare's siteverify endpoint; a second IF continues only if
successis true. - The destinations, in parallel: append a row to Google Sheets or Airtable, create a contact in HubSpot or Pipedrive, post to a Slack or Teams channel, send a confirmation email, or insert into your own Postgres database.
- Respond to Webhook returns a 200 so the page can thank the visitor.
One honest warning: anything in script.js is public, so a "secret" header placed there is not a secret. The protection comes from server-side checks inside the workflow (Turnstile verification, origin restriction, the honeypot, and n8n's IP allowlist when the sender is a known service such as Netlify's webhook), not from hiding the URL. Keep the Turnstile secret key in n8n's credentials, never in the page.
The pay-off is flexibility. Swapping your CRM, adding a second notification or routing enquiries by service line becomes a change to a workflow, not to the website. We run this exact pattern on our own site: the signup form on our field guide page posts straight to an n8n webhook.
When static is not enough: grow into Flask
Static is a default, not a religion. It stops being enough when:
- non-technical colleagues publish several times a week and need a proper editor with a login;
- the site is a shop, a membership area or a booking system, where real server logic and payments are the product;
- content is personalised per visitor or changes by the minute.
The usual reflex at that point is to reach for WordPress or a heavyweight JavaScript framework. Our recommendation is Flask, the small Python web framework, and the reason is continuity. A Flask app serves the very same plain HTML, CSS and JavaScript you already have: index.html moves into a templates/ folder, styles.css and script.js move into static/, and the shared header and footer become a single base template. Nothing about the front end has to be rewritten, there is no build step and no JavaScript bundle, and "view source" still shows the real page.
What you gain is exactly the part a static site lacks, added only where you need it: a route that saves form submissions to PostgreSQL, a small password-protected admin for editing pages, a Stripe checkout, a members' area. Each is a few dozen readable lines of Python and SQL rather than a plugin you have to trust. It also suits the way many sites are now built, with an AI coding assistant: a short, explicit stack with raw SQL and vanilla JavaScript is far easier for both you and the assistant to read, check and fix than a framework with layers of abstraction. That is the argument we make in more detail on Flask Vibe, with a case for Flask, a deployment guide and a project generator that produces a ready-made boilerplate.
The security reasoning from earlier still holds. A Flask app does run code on the server, so it has an attack surface, but it is one you wrote and can read end to end: your routes, your queries, your dependencies, with no third-party plugin ecosystem shipping vulnerabilities into it. Keep queries parameterised, keep secrets in environment variables, and turn on CSRF protection and rate limiting on any login.
For the brochure, the agency portfolio, the law firm, the clinic, the restaurant, the consultancy and the local trade, though, none of those triggers apply. A handful of files, a free CDN and one automation workflow cover everything the business actually needs, and if that ever changes, Flask lets the same files grow into an application without starting again.
Making the move
If you already have a WordPress site, the content is the hard part and it already exists. Our free WordPress to static site tool reads your public pages, scores how easily they convert, and writes a ready-made prompt that has a coding agent rebuild the site as plain files with your copy, images, colours and fonts. You can run the rebuild yourself, or ask us to handle it, forms, redirects and DNS included.
The end state is pleasantly dull: a folder you can open, read and understand; a site that loads before the visitor has finished blinking; nothing to patch on a Friday night. Sometimes index.html, styles.css and script.js really are all you need.
Sources
- W3C Technical Architecture Group, The Rule of Least Power (2006).
- CERN, the first website (1991).
- Dan McKinley, Choose Boring Technology.
- HTTP Archive, Web Almanac 2025: CMS chapter.
- W3Techs, Usage statistics of content management systems.
- Patchstack, State of WordPress Security in 2026.
- Sucuri, 2023 Hacked Website and Malware Threat Report (PDF).
- GitHub Docs, GitHub Pages limits.
- Cloudflare Docs, Pages limits, Workers static assets and headers.
- Netlify Docs, Forms setup, form notifications and custom headers.
- n8n Docs, Webhook node.
- Cloudflare Docs, Turnstile server-side validation.
- MDN Web Docs, Sending forms through JavaScript.
- Pallets, Flask documentation; Flask Vibe, Why Flask.