Statac

Drop-ins

Ready-made templates, snippets and a stylesheet to copy into a site.

Statac writes no templates or snippets into a site. These are ready-made ones to copy. Each block is a whole file, with the path to copy it to above it. Once you copy a file, it is yours to change.

They are written in English. To show their words in other languages, see the words your pages show. They replace the starter that statac new wrote before 0.7.0; a site made with it keeps its files.

A stylesheet

A plain stylesheet to start from: the system’s fonts, a line length that reads well, images that fit the page, and light or dark to match the reader’s device. It also styles the snippets below.

assets/site.css
/* A plain stylesheet to start from: the system's fonts, a line length
   that reads well, images that fit, and the reader's light or dark look. */
:root {
  color-scheme: light dark;
  font: 1.125rem/1.6 system-ui, sans-serif;
  --faint: color-mix(in srgb, currentColor 8%, transparent);
  --line: color-mix(in srgb, currentColor 30%, transparent);
}
body {
  max-width: 42rem;
  margin: 0 auto;
  padding: 1.5rem 1rem 4rem;
}
img, video, iframe {
  max-width: 100%;
  height: auto;
}
pre, code {
  font-family: ui-monospace, monospace;
  font-size: 0.9em;
}
pre {
  overflow-x: auto;
  padding: 1rem;
  background: var(--faint);
}
figure {
  margin: 2rem 0;
}
blockquote, .note, .callout {
  margin-left: 0;
  padding: 0.25rem 1rem;
  border-left: 3px solid var(--line);
}
.note, .callout {
  background: var(--faint);
}
.callout-title {
  font-weight: 600;
}
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
  gap: 0.5rem;
  padding: 0;
  list-style: none;
}
.video button {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  cursor: pointer;
}
.video iframe {
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
}

Link it from the <head> of your layout:

{{ css("site.css") }}

Images

@figure shows an image with a caption, and @gallery shows several, each linking to its largest copy. Images says what each call takes. Both call snippets/picture.html, so copy it too.

snippets/picture.html
{# takes image #}
{#
An image: its resized copies in a <picture> if Statac made them, or else
the file itself. The other image snippets call it.
#}
{% set text = alt if alt is defined else image.alt %}
{% if image.avif %}
<picture><source type="image/avif" srcset="{{ image.avif }}" sizes="{{ image.sizes }}"><img src="{{ image.src }}" srcset="{{ image.fallback }}" sizes="{{ image.sizes }}" width="{{ image.width }}" height="{{ image.height }}" alt="{{ text }}" loading="lazy" decoding="async"></picture>
{%- else %}
<img src="{{ image.src }}"{% if image.width %} width="{{ image.width }}" height="{{ image.height }}"{% endif %} alt="{{ text }}" loading="lazy" decoding="async">
{%- endif %}
snippets/figure.html
{# takes name #}
{#
An image with a caption, called as @figure("photo-1.jpg"). alt and
caption default to the ones in the image's .yaml file; wide=true adds the
class wide, and decorative=true marks the image as decorative.
#}
{% set image = page.image(name, decorative=decorative is defined and decorative) %}
{% set words = caption if caption is defined else image.caption %}
<figure{% if wide is defined and wide %} class="wide"{% endif %}>
  {{ picture(image, alt=alt) if alt is defined else picture(image) }}
  {% if words %}
  <figcaption>{{ words | markdown }}</figcaption>
  {% endif %}
</figure>
snippets/gallery.html
{# takes names... #}
{#
Several images, each linking to its largest copy, called as
@gallery("photo-1.jpg", "photo-2.jpg"), or as @gallery() for every
image beside the page that Statac makes resized copies of.
#}
<ul class="gallery">
  {% for image in page.images(names if names else none, sizes="(max-width: 30rem) 50vw, 15rem", full=true) %}
  <li><a href="{{ image.full }}"{% if not image.alt %} aria-label="{{ image.name }}"{% endif %}>{{ picture(image) }}</a></li>
  {% endfor %}
</ul>

Videos and recordings

@video and @audio play a file kept beside the page. @youtube and @vimeo show a video kept on another site as its thumbnail, and load nothing from that site until the reader presses play. Statac fetches the thumbnail when you run statac refresh: see fetching from other sites. Videos and files says what each call takes. @youtube and @vimeo call snippets/picture.html, from Images.

snippets/video.html
{# takes name #}
{#
A video kept in the site, with an optional caption, called as
@video("video-1.mp4"). poster="poster-1.jpg" shows an image until it plays.
#}
{% set video = page.video(name) %}
<figure>
  <video controls preload="metadata"{% if poster is defined %} poster="{{ page.image(poster).src }}"{% endif %}>
    <source src="{{ video.src }}" type="{{ video.type }}">
    <a href="{{ video.src }}">Download the video</a>
  </video>
  {% if caption is defined %}
  <figcaption>{{ caption | markdown }}</figcaption>
  {% endif %}
</figure>
snippets/audio.html
{# takes name #}
{#
A recording kept in the site, with an optional caption, called as
@audio("recording-1.m4a").
#}
{% set recording = page.audio(name) %}
<figure>
  <audio controls preload="metadata">
    <source src="{{ recording.src }}" type="{{ recording.type }}">
    <a href="{{ recording.src }}">Download the recording</a>
  </audio>
  {% if caption is defined %}
  <figcaption>{{ caption | markdown }}</figcaption>
  {% endif %}
</figure>
snippets/youtube.html
{# takes id #}
{#
A video kept on YouTube, called as @youtube("abcdefghijk") with its id or its URL,
and an optional caption. It shows the video's thumbnail, once
statac refresh has fetched it, and asks YouTube for nothing until the
reader presses play. Without JavaScript, it links to the player.
#}
{% set video = page.youtube(id) %}
{% set name = caption | markdown | plain if caption is defined else "YouTube video" %}
<figure class="video">
  <a href="{{ video.embed }}" data-title="{{ name }}">{% if video.thumbnail %}{{ picture(video.thumbnail, alt="Play: " ~ name) }}{% else %}Play: {{ name }}{% endif %}</a>
  <script>
    {
      const link = document.currentScript.previousElementSibling;
      const button = document.createElement("button");
      button.type = "button";
      button.append(...link.childNodes);
      button.addEventListener("click", () => {
        const player = document.createElement("iframe");
        player.src = link.href;
        player.title = link.dataset.title;
        player.allow = "autoplay; encrypted-media; fullscreen; picture-in-picture";
        button.replaceWith(player);
        player.focus();
      });
      link.replaceWith(button);
    }
  </script>
  {% if caption is defined %}
  <figcaption>{{ caption | markdown }}</figcaption>
  {% endif %}
</figure>
snippets/vimeo.html
{# takes id #}
{#
A video kept on Vimeo, called as @vimeo("123456789") with its id or its URL,
and an optional caption. It shows the video's thumbnail, once
statac refresh has fetched it, and asks Vimeo for nothing until the
reader presses play. Without JavaScript, it links to the player.
#}
{% set video = page.vimeo(id) %}
{% set name = caption | markdown | plain if caption is defined else "Vimeo video" %}
<figure class="video">
  <a href="{{ video.embed }}" data-title="{{ name }}">{% if video.thumbnail %}{{ picture(video.thumbnail, alt="Play: " ~ name) }}{% else %}Play: {{ name }}{% endif %}</a>
  <script>
    {
      const link = document.currentScript.previousElementSibling;
      const button = document.createElement("button");
      button.type = "button";
      button.append(...link.childNodes);
      button.addEventListener("click", () => {
        const player = document.createElement("iframe");
        player.src = link.href;
        player.title = link.dataset.title;
        player.allow = "autoplay; encrypted-media; fullscreen; picture-in-picture";
        button.replaceWith(player);
        player.focus();
      });
      link.replaceWith(button);
    }
  </script>
  {% if caption is defined %}
  <figcaption>{{ caption | markdown }}</figcaption>
  {% endif %}
</figure>

Files

@file shows a text file kept beside the page as code, with a link to download it, or links to any other file.

snippets/file.html
{# takes name #}
{#
A file kept beside the page, called as @file("example.css"): shown as
code with a link to download it if it is text, or else as the link alone.
#}
{% set file = page.file(name) %}
{% if file.code %}
<figure class="code">
  <figcaption><a href="{{ file.url }}" download>{{ file.name }}</a></figcaption>
  <pre><code{% if file.language %} class="language-{{ file.language }}"{% endif %}>{{ file.code }}</code></pre>
</figure>
{% else %}
<p><a href="{{ file.url }}" download>{{ file.name }}</a></p>
{% endif %}

Quotations and notes

Each wraps the lines between the call and @end:

@quote(by="Name 1", cite="Work 1")
The words quoted.
@end
snippets/quote.html
{#
A quotation, with who said it (by) and where (cite), each optional,
called as @quote(by="Name 1", cite="Work 1") and closed with @end.
#}
<figure class="quote">
  <blockquote>
    {{ body }}
  </blockquote>
  {% if by is defined or cite is defined %}
  <figcaption>{% if by is defined %}{{ by | markdown }}{% endif %}{% if by is defined and cite is defined %}, {% endif %}{% if cite is defined %}<cite>{{ cite | markdown }}</cite>{% endif %}</figcaption>
  {% endif %}
</figure>
snippets/note.html
{# A note set apart from the text around it, called as @note() and closed with @end. #}
<aside class="note">
  {{ body }}
</aside>

Callouts

Without this file, Statac builds a callout with no title. With it, each callout shows its kind as a title, such as “Tip”, in the page’s language.

snippets/markdown/callout.html
{#
A callout, written in Markdown as > [!TIP], with a title. Statac hands
the title in the page's language, such as Tip, or nothing for a language
whose words for callouts aren't in its languages/ file.
#}
<aside class="callout {{ kind }}">
  {% if title %}
  <p class="callout-title">{{ title }}</p>
  {% endif %}
  {{ body }}
</aside>

A search box and a page to hold it. The box’s script lists what Statac’s search index finds while the reader types. To put a box on every page, call {{ search() }} in your layout. Search says what the index finds, and how to offer search without JavaScript.

snippets/search.html
{#
A search box, called as {{ search() }} in a layout or @search() in a
page's content. assets/search.js lists what Statac's search index finds
as the reader types.
#}
{% set box = page.search_box() %}
<search id="{{ box.id }}" class="search" data-index="{{ box.index }}">
  <label for="{{ box.id }}-words">Search this site</label>
  <input id="{{ box.id }}-words" type="search" autocomplete="off">
  <noscript><p>Search needs JavaScript.</p></noscript>
  <p role="status"></p>
  <ol></ol>
</search>
{% if box.first %}
{{ js("search.js") }}
{% endif %}
assets/search.js
// Lists what Statac's search index finds for the words in each search
// box on the page, as the reader types. The index is a module at the
// URL in the box's data-index: see https://statac.dev/search/
for (const box of document.querySelectorAll(".search")) {
  const input = box.querySelector("input");
  const status = box.querySelector("[role=status]");
  const list = box.querySelector("ol");
  let index;
  let asked = 0;

  const result = (page) => {
    const item = document.createElement("li");
    const link = document.createElement("a");
    link.href = page.url;
    link.textContent = page.heading ? `${page.title}: ${page.heading}` : page.title;
    const excerpt = document.createElement("p");
    for (const piece of page.excerpt) {
      const text = piece.text.replaceAll("\n", " ");
      if (piece.marked) {
        const mark = document.createElement("mark");
        mark.textContent = text;
        excerpt.append(mark);
      } else {
        excerpt.append(text);
      }
    }
    item.append(link, excerpt);
    return item;
  };

  const said = (outcome) => {
    if (outcome.count === 0) {
      return outcome.suggestion ? `Nothing found. Did you mean ${outcome.suggestion}?` : "Nothing found.";
    }
    const pages = outcome.count === 1 ? "1 page" : `${outcome.count} pages`;
    return outcome.partial ? `${pages} with some of the words.` : `${pages} found.`;
  };

  input.addEventListener("input", async () => {
    const words = input.value.trim();
    const ask = ++asked;
    if (!words) {
      list.replaceChildren();
      status.textContent = "";
      return;
    }
    index ??= import(box.dataset.index);
    const { search } = await index;
    const outcome = await search(words, { typing: true });
    if (ask !== asked) return;
    list.replaceChildren(...outcome.results.map(result));
    status.textContent = said(outcome);
  });
}
content/search.md
---
title: Search
search: false
---
@search()

robots.txt

Tells search engines they may read every page, and where the sitemap is. Statac publishes it once the site has a url.

content/robots.md
---
permalink: /robots.txt
layout: robots.txt
needs: url
---
templates/robots.txt
User-agent: *
Disallow:

Sitemap: {{ url("/sitemap.xml") | absolute }}

On other pages

Something unclear or wrong? Open an issue.