Microblogging
A microblog is a collection of short, titleless entries, shown in full on a timeline, each with its own permalink, tags, photos, an RSS feed, h-entry microformats and SocialMediaPosting structured data. There is no database and no server: the site stays plain static HTML.
How it works
A note is a Markdown file, plus its images, in content/notes/. Anything that can add a file to your repository can post: the static note command, an iOS Shortcut, the GitHub web editor, Working Copy, Obsidian. A CI job then rebuilds and deploys the site on every push (see Deploying & CI). The git repository is the database.
Set up
A collection named notes is a microblog by default. The blog template already includes one. To add it to another project, create content/notes/ and, to choose the title or URL, configure it in data/site.json:
{
"timezone": "Europe/London",
"collections": {
"notes": { "title": "Notes", "path": "notes", "style": "note", "perPage": 10 }
},
"navigation": [{ "slug": "notes", "title": "Notes" }]
}
timezone is an IANA time zone name and decides the date and time shown on each note (UTC when omitted). See Blog & collections for all collection options.
Posting from the terminal
static note "Shipped version 1.2 today"
static note "Sunset" --image=sunset.jpg --alt="Orange sky over the harbour" --tags=photo
static note "Two photos" --image=a.jpg --alt="First" --image=b.jpg --alt="Second"
echo "from a pipe" | static note
static note # no text: opens $EDITOR
Inside a project folder the project name is optional; otherwise pass it first: static note my-site "text".
| flag | meaning |
|---|---|
--image=<file> | attach a photo. Repeat for several. |
--alt="..." | alt text for the image in the same position. Always set it; the build warns when it is missing. |
--tags=a,b | extra tags, shown as chips on the note |
--collection=<name> | post to a collection other than notes |
--push | git add, commit and push the note, which triggers CI |
A note needs text or at least one image. Accepted image formats are JPEG, PNG, WebP, AVIF and GIF; convert HEIC photos first.
The command writes content/notes/<date>-<hhmm>.md and copies images to content/notes/media/.
The note format
---
date: 2026-10-01T14:32:00.000Z
slug: 2026-10-01-1432
tags: ["photo", "harbour"]
images:
- src: media/2026-10-01-1432-1.jpg
alt: "Orange sky over the harbour"
---
Sunset tonight. Markdown works, **including links**.
You can write these files by hand or generate them from any tool. That is the whole interface.
dateis an ISO timestamp. It is also read from aYYYY-MM-DD-file name prefix.images[].srcis relative to the collection folder.in the body works too.- No title is needed. The start of the text (or the first image's alt text) is used for
<title>, the feed and search results. - The first image is used as the Open Graph / Twitter image, so a shared note shows the photo.
- Images use the same pipeline as
images/: AVIF and WebP at several widths,srcset, intrinsic dimensions and lazy loading.
Text, links and hashtags
Notes are Markdown with microblog-style handling:
- A single newline is a line break, and every extra blank line adds visible spacing. Code blocks are untouched.
- Bare URLs (
https://...,www....) and email addresses become links. External links open in a new tab (target="_blank"withrel="noopener noreferrer"). - Hashtags (
#swift,#ios-dev) become links to a tag page such as/notes/tag/swift/, listing every note with that tag. They count as tags automatically, are ignored inside code and existing links, and need at least one letter (#1is left alone). Tags from--tagsthat are not in the text appear as chips under the note.
Pagination
The timeline is paginated by perPage (10 by default), with numbered links available to your layout, and tag pages paginate too.
Photos, times and sharing in your layout
The default BlogList and BlogPost show a simple note timeline. To make it look like a social feed, override them. Notes expose these fields:
| field | example |
|---|---|
html | rendered note body |
images | [{ src, url, alt, width, height }] |
tags, inlineTags | all tags / tags written as hashtags in the text |
dateFormatted, timeFormatted | 1 October 2026, 14:32 |
date | ISO timestamp for <time datetime> |
url, title | permalink and a text title |
A compact timeline entry as components/Note.vue (any key in site.json is available as site, so a profile object is a convenient place for your name and avatar):
<script setup>
import { useStatic } from '@static';
defineProps({ note: Object, collection: Object });
const { site } = useStatic();
const profile = site.profile || {};
</script>
<template>
<article class="h-entry flex gap-3 border-b border-zinc-200 py-4 dark:border-zinc-800">
<Image v-if="profile.avatar" :src="profile.avatar" :alt="profile.name" width="48" height="48"
class="size-12 rounded-full object-cover" />
<div class="min-w-0 flex-1">
<p class="text-sm">
<strong>{{ profile.name }}</strong>
<span class="text-zinc-500">
<a :href="note.url"><time class="dt-published" :datetime="note.date">{{ note.dateFormatted }}, {{ note.timeFormatted }}</time></a>
</span>
</p>
<div class="prose max-w-none dark:prose-invert e-content" v-html="note.html"></div>
<NoteImages :images="note.images" />
</div>
</article>
</template>
Then use it from components/BlogList.vue (<Note v-for="item in items" :key="item.url" :note="item" :collection="collection" />) and components/BlogPost.vue (<Note :note="post" :collection="collection" />). BlogList receives items, heading, pagination (page, pages, prevUrl, nextUrl, urls), tag and collection; BlogPost receives post (with prev and next) and collection.
Share links need no framework. Plain links to a post intent URL work without JavaScript, and a <script client> block can add copy-link and the native share sheet:
<a :href="`https://twitter.com/intent/tweet?url=${encodeURIComponent(site.url + note.url)}`" target="_blank" rel="noopener noreferrer">Share on X</a>
<button type="button" data-copy :data-url="site.url + note.url" hidden>Copy link</button>
<script client>
document.querySelectorAll('[data-copy]').forEach(function (b) {
if (!navigator.clipboard) return;
b.hidden = false;
b.addEventListener('click', function () { navigator.clipboard.writeText(b.dataset.url); b.textContent = 'Copied'; });
});
</script>
A "latest notes" section on any page:
<Note v-for="n in collections.notes.slice(0, 3)" :key="n.url" :note="n" :collection="{ config: site.collections.notes }" />
Posting from a phone
The simplest way is a static posting page. Run static remote in your project and it writes public/post/index.html; deploy it with the rest of the site and open /post/ on your phone. The page is plain HTML and JavaScript with no server and no login. The first time you open it you enter your repository (owner/name) and a GitHub token, which is kept only in that browser. After that you can write a note, add photos (resized on the device), set tags, post, and edit or delete recent notes. Each post is one commit to your repository, so your CI rebuilds and deploys the site. Add the page to your Home Screen to use it like an app.
Create the token as a fine-grained personal access token limited to that one repository, with Contents set to read and write and nothing else. The page is marked noindex, and anyone who finds it still cannot post without a token.
Alternatively, an iOS Shortcut can create the note file through the GitHub contents API, with no server of your own:
- Ask for Input (text); optionally Select Photos and Convert Image to JPEG.
- Format Date as ISO 8601 for a timestamp.
- Get Contents of URL: method
PUT, tohttps://api.github.com/repos/<owner>/<repo>/contents/content/notes/<timestamp>.md, headersAuthorization: Bearer <token>and a JSON body{ "message": "note", "content": "<base64 of the Markdown file>" }. - For each photo, repeat the request for
content/notes/media/<timestamp>-1.jpgwith the base64 image.
Use a fine-grained personal access token limited to that one repository (Contents: read and write). It lives in the Shortcut on your device and never in the website. The GitHub mobile app and Working Copy can create the same files by hand.
On your own computer, static webui gives you a timeline-style editor for writing, editing and deleting notes (see Web UI).
Why the site cannot accept posts itself
A static site is public files. .env values never reach the HTML (only PUBLIC_* variables do), which keeps secrets safe but also means the page cannot hold a posting secret. Posting credentials therefore live where the poster is: your terminal's git login, a token in your Shortcut, or a server you run. Deploy tokens live in .env.local locally and in your CI provider's secrets.
Static does not include a Micropub endpoint. If you want apps such as Micro.blog or Quill to post to your site, run a small Micropub service that commits note files to your repository in the format above.