Publish
Search
A new site has no search box: give it one with an indexing tool you choose, run after each build, or with a list of your posts that the reader’s browser searches.
Which way?
| With an indexing tool | Without a tool | |
|---|---|---|
| What you add | An after_build command, and a snippet that loads the tool’s script |
A page and a template listing your posts, and a snippet holding a short script |
| What a reader’s browser fetches | The parts of the index a search needs | The whole list, when the reader first types |
Under statac serve |
Made once, after serve’s first build, then stale until serve is started again | Kept up to date on every save |
| Suits | Any site, and a large one most | A small site |
The list’s size decides it. Measured on three real sites, as sent to the reader:
| Posts | Each post’s whole text | Each post cut to 500 characters |
|---|---|---|
| 45 | 14 KB | not measured |
| 343 | 0.9 MB | 118 KB |
| 1,616 | 2.5 MB | 369 KB |
A post cut short is found by its title and opening, not by a phrase further in.
With an indexing tool
-
Run the tool after each build. In
statac.yaml:after_build: indexer --site "$STATAC_OUT" --output "$STATAC_OUT/search" after_build_makes: [search/]indexerstands for the tool you choose. It reads the built pages inSTATAC_OUTand writes its index, script and stylesheet intosearch/.after_build_makestells the link check thatsearch/will be there. -
Write the box as a snippet. In
snippets/search.html:{% set box = page.search_box() %} {% if box.first %} <link rel="stylesheet" href="{{ url("/search/search.css") }}"> <script src="{{ url("/search/search.js") }}"></script> {% endif %} <div id="{{ box.id }}"></div>search.cssandsearch.jsstand for the files the tool writes.page.search_box()gives the box’sid,statac-search, and says whether it is the page’sfirst, the one that loads the tool’s files.url()keeps each address right on a site under a path. -
Place one box on a page, with
{{ search() }}in a layout or@search()in a page’s writing. -
Build the site:
statac buildThe tool runs once the site is built with no errors.
Without a tool
Nothing runs after the build, and nothing outside your site is needed.
-
Make a page that lists the posts. In
content/search-list.md:--- permalink: /search.json layout: search.json ---A layout whose name ends
.jsonbuilds the page as JSON. -
Write its template. In
templates/search.json:[{% for entry in site.posts %} {"title": {{ headline(entry) }}, "url": {{ entry.url }}, "text": {{ entry.content | plain }}}{% if not loop.last %},{% endif %}{% endfor %} ]Statac writes each value as JSON, so the template puts no quotes round them.
headline(entry)names a post that has notitle:too. -
Write the box as a snippet. In
snippets/search.html:<search> <label for="search-words">Search</label> <input id="search-words" type="search" autocomplete="off"> <p id="search-count" role="status"></p> <ul id="search-found"></ul> </search> <script> (() => { const words = document.getElementById("search-words"); const count = document.getElementById("search-count"); const found = document.getElementById("search-found"); let pages; words.addEventListener("input", async () => { pages ??= await (await fetch("{{ site.languages[page.language].url }}search.json")).json(); const wanted = words.value.toLowerCase().split(/\s+/).filter(Boolean); const hits = pages.filter((page) => { const text = `${page.title} ${page.text}`.toLowerCase(); return wanted.length > 0 && wanted.every((word) => text.includes(word)); }); found.replaceChildren(...hits.slice(0, 20).map((page) => { const item = document.createElement("li"); const link = document.createElement("a"); link.href = page.url; link.textContent = page.title; item.append(link); return item; })); count.textContent = wanted.length > 0 ? `${hits.length} found` : ""; }); })(); </script>It shows the first 20 posts that hold every word typed, newest first, and counts them all. It fetches the list when a reader first types. A new site’s
assets/site.cssalready styles the box. -
Place one box on a page, with
@search()in a page’s writing or{{ search() }}in a layout. Its ids are written in the snippet, so a second box on the page would repeat them.
A larger site
To keep the list small, cut each post to its opening in
templates/search.json:
"text": {{ entry.content | plain | truncate(500, "") }}
Past a few hundred posts, an indexing tool is the better choice.
A site in several languages
content/
search-list.md
fr/
search-list.md
On a site in
several languages,
/search.json holds the main language’s posts alone, and Statac says
nothing of the others. A copy of the page in each other language’s
folder makes that language’s list: content/fr/search-list.md is
published at /fr/search.json.
Keep a page out of the index
In a new site’s templates/404.html, which extends base, add:
{% block body %} data-search-ignore{% endblock %}
data-search-ignore stands for the tool’s own mark for a page to leave
out, often an attribute on <body>. A template that extends base
writes on <body> through this block.
A new site’s 404 page asks search engines to leave it out, but an indexing tool goes by its own mark: until the page carries it, a search can find it.
A list made without a tool holds only site.posts, so pages that aren’t
posts are never in it.
Where next?
- Commands of your own: what an
after_buildcommand is handed, and when it runs - Snippets: calling a snippet from the writing and from a template
- Templates:
headline,url, and theplainandtruncatefilters - Addresses: a site
under a path, and
url()
Something unclear or wrong? Open an issue on GitHub.