10 Things to Check on Your Static Website Before Launch

Though it’s taken me longer than expected, sigseis.dev is mostly done - with only a few remaining missing features and polish work.

With this site being mostly finished, I thought it would be fun to write up a little article about some things you should check on your static sites! At least while it’s still fresh in my own mind.

It’s important to ensure that your website functions both on desktop and mobile, and that the experience is good on either.

This is usually accomplished with media queries.

The modern practice is to reorganize the content so that it displays in a slightly different way as the screen gets smaller. The killer CSS features to learn are CSS Flexbox, and CSS Grid.

On the home page, I reorganize the article image to be on top instead of to the side by changing the flexbox flow direction (if you need to change more than just the flow, consider a grid instead).

/* The normal direction is `row`, which puts the image to the left. */
/* But if the screen is too small, put it on top by changing flow. */
@media screen and (max-width: 550px) {
  .article-entry {
    flex-direction: column;
  }
}

And for non-critical elements, I hide them if the screen gets too small.

/* If the screen is <= 420px in size - hide the "view more" button. */
/* This button appears in two places, so it's fine to hide one. */
@media screen and (max-width: 420px) {
  .section-view-more {
    display: none;
  }
}

You can also selectively enable things based on screen size, too. The most common of which would be different kinds of menus.

/* By default hide the inline navbar label, but show it when small enough. */
@media screen and (max-width: 550px) {
  #header-static-navbar {
    display: none;
  }
}
@media screen and (min-width: 551px) {
  #header-inline-navbar {
    height: 0 !important;
  }
}

When you intend on sharing a website broadly, you should take a closer look at accessibility. This is where aria attributes come into play.

The main thing to remember about aria is that - if you’re practicing modern web fundamentals - these special labels are often unnecessary. The goal of the aria attributes is that you should rarely (if ever) need them.

You should aim to have a flat list of div-equivalent elements on your page, using the modern semantic element names.

<body>
  <header>
    <nav>...</nav>
  </header>
  <main>
    <article>
      <section>...</section>
      <section>...</section>
      <section>...</section>
    </article>
  </main>
  <footer></footer>
</body>

This actually says a lot about the structure of the page, all without even using the special aria attributes.

NOTE: Something maybe slightly less obvious is how headings work. You should only have one h1 heading (which is reflective of whatever the page title is) and all other headings must be in a sensible order (meaning h2 which may contain h3 and so-on). You may have multiple of the non-h1 headings.

If you cannot represent an element with the proper element for some reason (like making a div act like a button), you should apply the role="..." attribute the element’s intent.

If text description is available on the page for an element, use aria-labelledby="{id}" to suggest the id containing the text that labels a <section> or <div>. As a last resort, you can use aria-label (on interactive elements, or sections that contain interactive elements) to suggest the intent of certain controls.

The last important aria thing to be aware of is aria-hidden="true". If you have some elements that are purely decorative, you should mark them as such using this attribute. This removes the element (and all children) from the accessibility tree. That can be important to not cause confusion and noise for those who need the accessibility view.

You can read about the various attributes here: ARIA states and properties

However, perhaps a better website is this one, which gives good examples of how to use them, and discusses anti-patterns to watch out for and gives implementation advice: ARIA Authoring Practices Guide (APG)

As a quick summary of the most important points:

  • Use modern semantic elements like header, footer, nav, article, section, etc.
  • Mark decorative-only, non-interactive elements using aria-hidden="true".
  • Use alt tags on images if they’re non-decorative.
  • Prefer aria-labelledby="{id}" for labeling elements.
    • If the information is important enough to label, it usually is important enough to have text visually labelling it on the website anyways.
  • To add context that would normally only be visually conveyed (as in, without text), you can use aria-label="{label}" as a last resort.
    • Usually this is only used to label things like control regions - for example “Primary” and “Secondary” nav bars, etc.
  • Don’t over-use aria attributes.
    • Using them too much will cause noise and - counter to expectations - make the site more difficult to browse for those who actually use the accessibility features.
  • There’s a nifty “accessibility” tab in your web developer tools - use it to view and debug the accessibility tree!

NOTE: Truth be told, my website is probably not setting a great example. I learned a lot, and would definitely do some of my accessibility design differently if I had the time to do a second pass on it.

This is a simple one, but also it’s really easy to miss.

It’s also a bit of a holy war. It doesn’t matter which way you land here, you just need to be sure to be consistent.

You need to decide between the following path formats (note the trailing slash):

  • https://example.com/my/path/to/resource
  • https://example.com/my/path/to/resource/

If you don’t care which, just use the nginx index command, it will do “the right thing” for you (opting towards the latter format, with a trailing slash).

I decided on no slash, personally.

My take on it is that me accessing a webpage is like accessing a resource, and as such the trailing slash doesn’t make much sense. But again let me state - there’s no right answer here. Let your heart guide you. <3

server {
    # Canonicalize trailing `/`, `/index.html`, and `*/index.html`.
    # This will return a 301 (Moved Permanently) for these paths.
    rewrite ^(.+)/+$ $1 permanent;
    rewrite ^(.+)/index.html$ $1 permanent;
    rewrite ^/index.html$ / permanent;

    location / {
        # Try the file directly, then try `index.html`
        try_files $uri $uri/index.html =404;
    }
}

This is really important, read this before making any changes!

If you use trailing slashes, then href and src paths work as you might expect - href="baz/", when on the /foo/bar page, will link to /foo/bar/baz/. You should have a trailing slash on your href, to avoid an unnecessary redirect from your server.

If you DON’T use trailing slashes, things may work slightly counter to your expectation - href="baz", when on /foo/bar page, will link to /foo/baz. So you must state either the complete path, or the relative from the parent (so, in this case /foo/bar/baz or bar/baz).

NOTE: You can see if you have unnecessary redirects by looking at the Network tool in your browser, and seeing if the first request to your site rewrote the file. It will look like a 301 for path followed by either a 200 or 304 to the final destination path/.

For assets that don’t change very often, you should suggest to the client to cache them on the their machine. This will ensure that the client is talking to you only as much as is necessary, and hopefully reduce the amount of data you send.

If you’re unfamiliar with the concept, cache busting is putting some data in the resource path that changes only when the asset itself changes - and then in the response headers tells the client that resource never expires.

You can see it on this site, actually - in the header, and in any of the images, you can see part of a hash in the path.

EXAMPLE: /articles/2026/08/03/cover.ef5e04e9b7.picture/cover.jpg (at time of writing, may no longer be an accurate path)

You DON’T want to get tricky with this - don’t mix the data into a query parameter, for instance.

BAD: /articles/2026/08/03/cover.picture/cover.jpg?fp=ef5e04e9b7 (don’t do this)

This doesn’t work very well, as some browsers ignore the query parameters for caching. I also recommend AGAINST using a file timestamp for this.

BAD: /articles/2026/08/03/cover.1781640330.picture/cover.jpg (don’t do this)

It’s too easy to accidentally change this value.

Really, your path should only update when the source file’s contents change. So basing this off some hash is best. I just took the first N characters of an md5 hash - it’s okay, we aren’t using this for security purposes.

The file name is only half of the work - the other half is that you should configure your headers to set certain cache control values (expiration is mostly for legacy).

map $sent_http_content_type $expires {
    default                    off;
    text/html                  epoch;
    text/css                   max;
    application/javascript     max;
    ~image/                    max;
    ~font/                     max;
    ~video/                    max;
}

map $sent_http_content_type $cache_control {
    default                    "public";
    text/html                  "public";
    text/css                   "public, immutable";
    application/javascript     "public, immutable";
    ~image/                    "public, immutable";
    ~font/                     "public, immutable";
    ~video/                    "public, immutable";
}

server {
    expires $expires;
    add_header Cache-Control $cache_control;
}

These configuration options use a map of the content type to change how caching and expiration is configured. Seems easier to do it based on the MIME type.

NOTE: You can validate if your cache busting is working properly by browsing around your website normally with the Network tool open. You should see the assets report “cached” under “Transferred”. If you reload the page, caching may be ignored, so be careful to browse not to refresh.

If you can automate saving pictures as different formats, use the <picture> element for more efficient content delivery.

These are really good for delivering the best format of image possible. My site currently supports the following formats.

The simplest form of this looks like this.

<picture>
  <source srcset="picture.avif" type="image/avif">
  <source srcset="picture.jxl" type="image/jxl">
  <source srcset="picture.webp" type="image/webp">
  <img src="picture.jpg" alt="Descriptive Alt Text Here" width="975" height="512" loading="lazy">
</picture>

This is a really simple implementation that doesn’t select a proper size depending on the viewport, but it should serve as a good starting point for you.

The way this works is it will load the first image in the list that the browser claims to support (so order matters), ultimately falling back to the source provided in the <img> element if no other <source> was usable (which should be a very commonly supported format, like regular jpeg here).

The way I sort mine is like so:

  1. Smallest between avif and jxl; either are good quality.
  2. Largest between avif and jxl; either are good quality.
  3. webp as a fallback - but usually worse quality and higher size than avif and jxl.
  4. Fallback is always a simple jpg (widely supported, but worst quality, and largest size).

If you don’t need any special features like transparency or lossless, I think AVIF creates the best quality:size trade-off. The one issue is that AVIF tends to take forever to encode.

JXL is good if you need the other features, and tends to fare better than WebP in my experience. Though, it’s very bleeding edge (I just wanted to say I supported it).

I’ve been pretty underwhelmed with WebP. There’s a high chance I remove it sometime in the near future.

Additionally, you’ll notice that I use the loading="lazy" attribute. This is an easy way to make the page not immediately load all images. I definitely recommend this on your non-UI elements (incidental pictures, article images, or thumbnail images).

This is necessary for SEO (if you care about that). Luckily, it’s really, really easy to generate.

You can find a description of the protocol here: Sitemaps XML Format

The simplest form of it looks something like this.

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://www.example.com</loc>
    <lastmod>2026-07-10T17:17:27+00:00</lastmod>
    <changefreq>weekly</changefreq>
  </url>
  <url>
    <loc>https://www.example.com/about</loc>
    <lastmod>2026-06-13T03:09:29+00:00</lastmod>
  </url>
  <url>
    <loc>https://www.example.com/articles</loc>
    <lastmod>2026-06-26T21:20:41+00:00</lastmod>
    <changefreq>monthly</changefreq>
  </url>
  <url>
    <loc>https://www.example.com/articles/2026/06/26</loc>
    <lastmod>2026-06-26T21:20:41+00:00</lastmod>
    <priority>0.7</priority>
  </url>
</urlset>

If you have a lot of content, you may instead wish to chunk things into separate indexes, and instead have the main sitemap list those indexes.

When separating into different indexes, it should be chunked up based on when the elements have been published - those usually won’t update very regularly in subsequent years, allowing indexers to skip large sections of your site.

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://www.example.com/sitemap/2026.xml.gz</loc>
    <lastmod>2026-06-26T21:20:41+00:00</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://www.example.com/sitemap/2025.xml.gz</loc>
    <lastmod>2025-12-30</lastmod>
  </sitemap>
</sitemapindex>

The main things to note:

  • Only provide a changefreq if you do have a planned update frequency when you intend to update these pages in a way in which the lastmod would change.
    • If you don’t know, just don’t provide it. (In fact, most indexers ignore this, so you really don’t even need to provide it - I do, but I don’t know if it’s doing anything for me.)
  • Only change lastmod if the actual contents of the page change in a meaningful way.
    • This can be tricky - the page being styled differently does not count. So you cannot just use a file timestamp. For my site, I track a separate piece of “updated” metadata (separate from modified time of the HTML), and that’s what is populated here.
  • Use your canonical addresses for the loc.
    • As noted above, there should be one canonical address to a resource. So here, you should use that canonical address. I’ve standardized around there being no subdomain, and paths not ending with a trailing slash. Do what you wish here.
  • There is a priority field, but it’s unclear if anyone really pays any attention to it.
    • This allows you to prioritize relatively between your sites (e.g. content X is more important to index than content Y). This is a value between 0.0 and 1.0 (default is 0.5) - so for my site, I say that articles are important content to prioritize indexing relative to other content on the site.

Even if you don’t plan on serving your content via social media, you’re going to want to have meta tags for OpenGraph.

You can find easy-to-read descriptions of the different meta tags here: The Open Graph protocol

Looking at this page, you can see a number of these elements.

<meta property="og:url" content="https://sigseis.dev/articles/2026/08/03">
<meta property="og:site_name" content="Sigseis">
<meta property="og:title" content="Static Site Generator Retrospective">
<meta property="og:description" name="description" content="TODO">
<meta property="og:image" content="https://sigseis.dev/articles/2026/08/03/cover.ef5e04e9b7.picture/full.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="An Osaka toy from Azumanga Daioh sitting under a kotatsu with a strange cat at a desk next to her.">
<meta property="og:image:type" content="image/jpeg">
<meta property="og:type" content="article">
<meta property="article:section" content="Article">
<meta property="article:tag" content="programming">
<meta property="article:tag" content="rust">
<meta property="article:publisher" content="https://sigseis.dev/">
<meta property="og:site_name" content="Sigseis">
<meta property="og:locale" content="en_US">
<meta name="twitter:creator" content="@sigseis">
<meta name="twitter:label1" content="Written by">
<meta name="twitter:data1" content="Sigseis">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@sigseis">
<meta name="twitter:creator" content="@sigseis">

NOTE: The order can matter in some cases! Some of these entries represent fields in a list of objects. So the order can matter for cases where you want to provide multiple images for instance (I don’t do that for simplicity, but you could).

If you set the correct OpenGraph values, you can get really nice presentation on things like Bluesky, Twitter/X, and Mastodon. But the reason I recommend it generally is that other apps respect this as well (like chat apps, for instance).

Uploading some cover image is usually a good idea (at least for articles). Minimally you should aim for 1200x630, as that covers what most sites want to see (with the sole exception to my understanding being LinkedIn, who annoyingly wants 1200x627).

This shows up slightly differently on each social media site, however.

screenshot of the OpenGraph integration working on Bluesky
Bluesky

screenshot of the OpenGraph integration working on Mastodon
Mastodon

screenshot of the OpenGraph integration working on Twitter/X
Twitter/X

Personally, I like the format of Bluesky’s the most. My least favorite is how it shows up on Twitter/X. Regardless, it looks so much nicer to have an image associated with your article rather than just some text and a link.

If you plan on having regular content (like blog posts, or articles, or whatnot), you should consider having an RSS feed.

It’s a bit niche, but I like it - you don’t need to manage some other service for sending emails. You leave the subscription and polling in the hands of the clients.

The downsides are pretty much only for the website owner - you can’t really “track” your subscribers. However, if you value privacy and don’t want to maintain state, it’s a great idea to publish a feed.

This does mean that you have to ensure your feed is published properly.

It’s not too hard; most readers accept either RSS or ATOM, I’ve found ATOM to be a slightly simpler feed schema, so I went with that, and it’s been pretty easy to automate.

You can view the feed I’ve created as an example: Sigseis

And if you need to find reference documentation about ATOM feeds, you can here: Introduction to Atom

NOTE: Your feed doesn’t need to be “complete” - anything not presented will act as if the reader had missed them if they subscribe later. I aim to only present the last N important updates to the site.

It’s actually really simple to support dark and light themes. All it takes is a little CSS.

:root {
    --page-background: #f5f5f5;
    --text-color: #212121;
}

@media (prefers-color-scheme: dark) {
    :root {
        --page-background: #212121;
        --text-color: #f5f5f5;
    }
}

/* Example of using it */
.some-class {
    background: var(--page-background);
    color: var(--text-color);
}

This is strong enough to work for most sites - it will default to a light theme (provided via :root variables), but if the user prefers a dark theme it will override it.

For this site, I wanted to add a theme switcher button. Which is most commonly accomplished using attribute selectors and a data-* attribute.

body[data-theme="dark"] {
    --page-background: #212121;
    --text-color: #f5f5f5;
}

body[data-theme="light"] {
    --page-background: #f5f5f5;
    --text-color: #212121;
}

Then, you can set the theme from JavaScript.

function set_theme(theme) {
  if (theme != "light" && theme != "dark") {
    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
      theme = "dark"
    } else {
      theme = "light";
    }
  }
  document.body.setAttribute("data-theme", theme);
}

function onload() {
  set_theme(localStorage.getItem("theme"));
}

I could not find a way to make a theme switcher work well without JavaScript. So if JavaScript is disabled, I just hide the button and stick with the inferred preferences from the media query (using the noscript head element).

On this site, I made the colors and backgrounds all transition to allow for nice fading between the themes. You don’t need to do this, and in fact it’s kind of a pain to manage (also it’s very inefficient for such a niche polish feature).

I’ll keep it on my site, but I won’t recommend it to others.

You probably notice these while reading this article.

screenshot of the mobile version of this website showing the sticky heading sections
Example of the sticky heading sections.

Sticky headings (little sections of HTML that “stick” to the top of the visible div) are really easy to add to a site, and really improve the usability of it.

The way you accomplish this is just by placing the div that you want to stick within some surrounding div that you want it to stick within.

<section>
    <div class="sticky-heading">
        <h2>Example Heading</h2>
    </div>
</section>

Then, in your CSS:

.sticky-heading {
  position: sticky;
  top: <some value>;
}

Usually 0 works perfectly for <some value>, but if you have some other styles that are fighting you (like margins or padding), you may need to input some other value.

This is an easy quick win for just about any website - I find that it makes reading articles a much more pleasing experience.

If you nest your sections, you can even achieve stacking sticky headings. Usually it requires a little finagling with the top values and CSS, but it can look pretty nice.

That’s a lot of things to check up on - some of them are less important than others, but I think all of them are at least a little important, or greatly aide in the usability or presentation of your site.

To summarize, we focused on the following 10 things to check:

  • Responsive Design
  • Accessibility Support
  • Canonicalizing HTML Paths
  • Cache Busting Assets
  • Picture Elements
  • Create a Sitemap
  • OpenGraph Support
  • RSS Feed Support
  • Dark and Light Themes
  • Sticky Headings

I hope you enjoyed reading this! Please share with others if you did - or send me a message on my socials if you have any questions or comments.

← Previous Article
How to Organize Rust Modules for Maintainability
A Chiyo toy from Azumanga Daioh standing at the front of a classroom.