How to host a static website on your own domain

A static site is files, nothing more: HTML, CSS, JavaScript and images, sent to the browser as they are. Hosting one comes down to uploading the right folder and pointing your domain at it. Here is how, and the parts that catch people out.

6 min readIntermediateUpdated

Is your site static?

It is if every page is a file you could open in a browser: plain HTML, or what a static site generator builds. It is not if it needs code running on the server for each visit, such as logins checked on the server, a contact form that sends email, or pages read from a database. That is an app; see Flask, Django or Express instead.

A static site can still talk to APIs from the browser, and can still be built with React or Vue, as long as the build step gives you plain files.

1. Build it, and find the folder to upload

If you use a framework or generator, run its build and upload what it produces, not your source folder:

ToolBuild commandFolder to upload
Vite (React, Vue, Svelte)npm run builddist
Astronpm run builddist
Hugohugopublic
Jekyllbundle exec jekyll build_site
Eleventynpx @11ty/eleventy_site
Next.js, with output: "export"npm run buildout

Upload the contents of that folder, so index.html ends up at the top of your site, not inside a dist folder of its own.

2. Check it from a server, not by double-clicking

A page opened straight from your disk (file://) behaves differently from one served over HTTP: links starting with / point at the root of your disk, and some scripts refuse to load. Serve the folder locally before uploading:

Terminal
cd dist
python -m http.server 8000

Then open http://localhost:8000 and click through. If your site will live in a subfolder rather than at the root of the domain, tell the build: Vite has base, Astro base, Hugo baseURL.

3. Clean URLs

On a plain file host, about.html answers at /about.html. For /about/, make a folder with an index.html in it: about/index.html. Hugo, Astro and Eleventy build pages this way by default; in Jekyll, set permalink: pretty. Link to /about/ with the slash, which is the address that file really has.

4. Single-page apps and their routes

A React or Vue app with its own router changes the address without loading a new page. That works until someone opens /settings directly or presses refresh: the host looks for a settings file, finds none, and answers 404. There are two ways round it:

  • Hash routes (/#/settings): the part after # never reaches the server, so any host works. Switch the router to hash mode (createHashRouter in React Router, createWebHashHistory in Vue Router).
  • A fallback to index.html for paths that are not files, which needs a host or a small server that does it. With Node.js, the serve package does: serve -s dist -l tcp://0.0.0.0:8000. -s sends index.html for unknown paths.

5. Your domain, and HTTPS

A host gives you DNS records to add where your domain's DNS is managed, usually your registrar or Cloudflare:

  • A subdomain such as www.example.com usually takes a CNAME record pointing at a name the host gives you.
  • The bare domain, example.com, cannot have a CNAME at most DNS providers, so it takes an A record with the host's IP address instead (Cloudflare and a few others flatten a CNAME there for you).

The certificate for HTTPS can only be issued once the records point at the host, so it follows a few minutes after DNS is in place. If you use both example.com and www.example.com, add both, and pick one as the address you share.

Hosting it on SnowServers

  1. Choose a plan on the website plans page and pick A website at checkout, with Python or Node.js: both come with the starter site. A static site is light: Flurry, at 1 GB, is plenty.
  2. The server comes with a starter site that serves the public folder. In Files, open public and upload your built site there, with index.html at the top. For a big site, SFTP is quicker than the browser; the panel shows its details.
  3. Visit the address and port on the Network tab to check it. New files are live as soon as they are uploaded, with no restart.
  4. On the Website tab, add your domain and the two records it shows: a CNAME to sites.snowservers.com and a TXT record that proves the domain is yours. For the bare domain, the tab gives you an A record instead. HTTPS follows on its own.

The starter serves files as they are: no file listings, nothing whose name starts with a dot (so a stray .env stays private), and a plain 404 for anything missing. It has no fallback for single-page-app routes. Use hash routes, or, on a Node.js server, let serve do it with this package.json, which installs and runs on the next restart:

JSON
{
  "private": true,
  "scripts": {
    "start": "serve -s public -l tcp://0.0.0.0:$SERVER_PORT"
  },
  "dependencies": {
    "serve": "^14.2.0"
  }
}

The website docs have the rest.

Common problems

  • The page loads with no styling: the CSS path is wrong once the site is served. Check the browser's developer tools (Network tab) for the file that answered 404, and the build's base path.
  • The home page is a list of files, or a 404: index.html is inside a folder. Move the contents of dist (or _site, or out) up a level.
  • Refreshing a page of a React or Vue app gives a 404: section 4.
  • The old version still shows: your browser cached it. A hard refresh (Ctrl+Shift+R, or Cmd+Shift+R) shows what is really there. Build tools that put a hash in file names avoid this for CSS and JavaScript.
  • HTTPS does not arrive: the DNS records are missing or still point elsewhere. If the domain is on Cloudflare, set the records to DNS only (grey cloud) until the certificate is issued.

Press / or Ctrl K on any page to search.