Tag: static-site

  • How to add full-text search to a static site

    How to add full-text search to a static site

    The first objection I hear is that static sites cannot have search because there is no server to query. That is wrong. You can build a search index when the site is generated and ship it as a plain file, then search it entirely in the browser. For anything up to a few thousand documents, it is fast enough that users assume there is a backend.

    Why client-side search works

    The trick is moving the work to build time. While my generator is already looping over every page to render HTML, it is trivial to also collect the title, URL, and body text of each one into a list. That list becomes a JSON file. The browser downloads it once, builds an in-memory index, and every keystroke after that is instant because nothing leaves the device. No database, no API, no per-query cost.

    Build the index at generation time

    During the build, I strip HTML tags from each page and push a small record into an array. Keep the body text trimmed; you do not need every word, and a leaner index downloads faster. Here is the gist of it:

    const index = pages.map(page => ({
      title: page.title,
      url: page.url,
      excerpt: page.excerpt,
      body: page.text.slice(0, 2000)
    }));
    
    fs.writeFileSync('dist/search-index.json', JSON.stringify(index));

    Writing this file is just one more step in the same pipeline that produces your pages, so it slots naturally into a build you may already run through CI/CD with GitHub Actions. The index ships alongside your HTML to the same edge network.

    Pick a search library, or write the dumb version

    For small sites you genuinely can write your own matcher in a dozen lines. Lowercase everything, split the query into words, and score documents by how many words they contain. It works. But the moment you want typo tolerance, prefix matching, or relevance ranking, reach for a library. I like Fuse.js for fuzzy matching and MiniSearch when I want proper full-text scoring. Both are tiny and run in the browser without a build step.

    My rule of thumb is to start with the hand-written version and only upgrade when users complain. Most of the time the simple matcher is more than enough, and you avoid shipping a dependency you do not need. When you do switch to a library, the index format stays the same, so the migration is a few lines, not a rewrite.

    import MiniSearch from 'minisearch';
    
    const res = await fetch('/search-index.json');
    const docs = await res.json();
    
    const mini = new MiniSearch({
      fields: ['title', 'body'],
      storeFields: ['title', 'url', 'excerpt']
    });
    mini.addAll(docs);
    
    const results = mini.search('cloudflare deploy', { fuzzy: 0.2 });

    Wire it to the input

    Hook a listener to your search box, but do not search on every single keystroke. Debounce it by 150 milliseconds or so, otherwise a fast typist fires a dozen searches for one word. On each debounced event, run the query, take the top handful of results, and render them as a list of links. Show the title and the excerpt so people can tell which result they want before clicking.

    • Debounce input so you search on a pause, not on every letter.
    • Limit to the top eight or ten results; nobody scrolls a search dropdown.
    • Highlight the matched term in the result so the relevance is obvious.
    • Handle the empty state and the no-results state explicitly.

    Load the index lazily

    Do not download the search index on page load. Most visitors never search, so paying that cost upfront is wasteful. I fetch the JSON the first time someone focuses the search box, cache it in a variable, and reuse it. The first search has a tiny delay while the file arrives, every search after is instant, and people who never search never pay a byte for it. This keeps your initial load lean, which matters for the same reasons I obsess over in optimizing images for the web.

    When to stop and use a service

    Client-side search has a ceiling. Past roughly ten thousand documents the index gets large enough that downloading and parsing it hurts, especially on phones. At that scale I switch to a hosted search service that exposes an API. But honestly, most blogs and docs sites never come close to that limit. Build the index, ship the JSON, search in the browser, and you get a feature that feels expensive for almost no cost and zero servers to maintain.

  • How to deploy a static site to Cloudflare Pages

    How to deploy a static site to Cloudflare Pages

    I have shipped a lot of sites onto Cloudflare Pages, including the one you are reading right now. The reason I keep coming back is boring in the best way: it is fast, the free tier is generous, and once it is set up I rarely think about it again. Here is how I do it.

    Connect the repository first

    Pages is happiest when it builds from a Git repo. Sign in to the Cloudflare dashboard, open Workers and Pages, and pick “Create application” then “Pages”. Authorize GitHub or GitLab, select your repository, and you land on the build configuration screen. This is the part people get wrong, so slow down here.

    You need three things: the framework preset (or “None” for a hand-rolled build), the build command, and the output directory. For my own generator the build command is node build.js and the output directory is dist. If you are using a known tool, the presets fill these in for you. If your “build” is just copying files, set the command to something harmless like echo done and point the output at your folder.

    Match the Node version to your local one

    A surprising number of failed first deploys come down to Node mismatches. Pages defaults to a recent Node, but your code might assume something else. I pin it explicitly with an environment variable so there are no surprises:

    NODE_VERSION = 20.11.0

    Add that under Settings, Environment variables, for both Production and Preview. While you are there, add any other secrets your build needs, like API tokens for pulling content. Anything sensitive belongs here, never in the repo.

    Trigger the first build

    Hit “Save and Deploy” and watch the log stream. The first build is the honest one. If a dependency is missing or your output directory is wrong, you will see it immediately. A clean build ends with Pages uploading your files to its edge network, and you get a *.pages.dev URL to test against. Open it, click around, and confirm assets actually load. Broken relative paths are the most common issue, usually from a site that assumed it lived at a subpath.

    Add your custom domain

    Once the preview looks right, attach a real domain. Go to the project’s Custom domains tab and add your hostname. If the domain is already on Cloudflare, the DNS record is created for you in one click. If it lives elsewhere, you will get a CNAME to add at your registrar. Propagation is usually minutes, not hours. Cloudflare provisions the TLS certificate automatically, so HTTPS just works without you touching certbot.

    Set up redirects and headers

    Static sites still need rules. Pages reads two special files from your output directory. A _redirects file handles URL rewrites and old links, and a _headers file lets you set caching and security headers. Here is a small example that locks down framing and caches assets hard:

    /*
      X-Frame-Options: DENY
      Referrer-Policy: strict-origin-when-cross-origin
    
    /assets/*
      Cache-Control: public, max-age=31536000, immutable

    Drop those in the folder you ship, not the project root, unless your build copies them across. Aggressive caching on hashed asset filenames is one of the cheapest performance wins you will ever get.

    Automate redeploys

    Every push to your production branch triggers a fresh build, and pull requests get their own preview URLs automatically. That alone covers most workflows. But if your content lives outside the repo, in a CMS for example, you will want a build hook. Create one under Settings, Builds and deployments, and you get a URL you can POST to from anywhere to kick off a deploy. I wire mine to a webhook so editors never touch Git.

    If you want more control over the build pipeline, you can skip the dashboard build entirely and run it yourself. I cover that approach in setting up CI/CD with GitHub Actions, which lets you deploy with the Wrangler CLI after your own test and lint steps pass.

    What I check before calling it done

    Before I trust a deploy, I run through a short list. Does the custom domain serve over HTTPS with no mixed-content warnings? Do the redirects actually fire? Are large images sensible, or am I shipping 4MB hero photos? That last one matters more than people expect, and I wrote up my whole approach in optimizing images for the web.

    That is genuinely it. Cloudflare Pages rewards a simple setup, and the edge network means your visitors in Sydney get the same snappy load as the ones next door. Once the pipeline is in place, deploying becomes a non-event, which is exactly what you want.