Files
verdande/site/index.html
T
Kristian WindandClaude Opus 5 1a6adfd9d3 docs: screenshots that regenerate themselves, and eleven of them
They were many rounds behind — no settings, no task detail, no themes, no groups,
no colours, no calendar, no admin pages. The reason is in how they were made:
`tools/shots` needs a running instance and a session cookie pasted in by hand, so
it ran about as often as somebody felt like doing that.

`node scripts/generate-screenshots.js` starts the real binary, creates its own
account, fills it with Danish demo data and photographs eleven surfaces into both
`site/screenshots` and `docs/screenshots`. Nothing to set up and nothing to
remember.

A script rather than a test, for the same reason `generate-icons.js` is one: CI
must never rewrite a tracked binary file.

`tools/shots` stays. It is still the way to photograph an instance that is
already running rather than a fresh one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 09:05:36 +02:00

610 lines
15 KiB
HTML

<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>verdande — self-hosted tasks, shared</title>
<meta
name="description"
content="Self-hosted tasks and projects, shared. One Go binary with SQLite inside it. CalDAV, mail-to-task and an MCP connector for Claude."
/>
<meta name="theme-color" content="#111312" />
<meta property="og:title" content="verdande" />
<meta property="og:description" content="Self-hosted tasks and projects, shared. One binary, SQLite inside it." />
<meta property="og:type" content="website" />
<link rel="icon" href="icon.svg" type="image/svg+xml" />
<style>
/*
* The same tokens as the app, so the site and the thing it describes
* look like one project rather than two.
*/
:root {
--ground: #111312;
--surface: #171a19;
--surface-raised: #1d211f;
--line: #262b29;
--line-strong: #343a37;
--ink: #e8ebe9;
--ink-muted: #9aa39e;
--ink-faint: #6b736f;
--accent: #5fbf8f;
--accent-ink: #0d1411;
--radius: 8px;
--radius-lg: 12px;
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, system-ui, sans-serif;
--font-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, monospace;
}
@media (prefers-color-scheme: light) {
:root:not([data-theme='dark']) {
--ground: #faf9f7;
--surface: #ffffff;
--surface-raised: #ffffff;
--line: #e5e3de;
--line-strong: #d3d0c9;
--ink: #1c201e;
--ink-muted: #5f6763;
--ink-faint: #8b918d;
--accent: #2f8f63;
--accent-ink: #ffffff;
}
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--ground);
color: var(--ink);
font-family: var(--font);
font-size: 16px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
.wrap {
max-width: 940px;
margin: 0 auto;
padding: 0 1.5rem;
}
/* --- hero --- */
header {
padding: 6rem 0 4rem;
text-align: center;
}
.mark {
font-size: 3rem;
color: var(--accent);
line-height: 1;
display: block;
margin-bottom: 1rem;
}
h1 {
margin: 0;
font-size: clamp(2.5rem, 7vw, 3.75rem);
font-weight: 560;
letter-spacing: -0.03em;
line-height: 1.05;
}
.lede {
margin: 1.25rem auto 0;
max-width: 30rem;
font-size: 1.125rem;
color: var(--ink-muted);
}
.norn {
margin: 2rem auto 0;
max-width: 34rem;
font-size: 0.9375rem;
color: var(--ink-faint);
}
.actions {
display: flex;
gap: 0.75rem;
justify-content: center;
flex-wrap: wrap;
margin-top: 2.5rem;
}
.button {
display: inline-block;
padding: 0.625rem 1.25rem;
border-radius: var(--radius);
text-decoration: none;
font-size: 0.9375rem;
font-weight: 550;
border: 1px solid var(--line-strong);
color: var(--ink);
transition: border-color 150ms ease, background 150ms ease;
}
.button:hover {
border-color: var(--accent);
}
.button.primary {
background: var(--accent);
color: var(--accent-ink);
border-color: var(--accent);
}
.button.primary:hover {
opacity: 0.9;
}
/* --- the one screenshot substitute: the thing it actually does --- */
.demo {
margin: 4rem 0;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-lg);
overflow: hidden;
}
.demo-bar {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--line);
font-size: 0.8125rem;
color: var(--ink-faint);
}
.demo-body {
padding: 1.5rem;
font-family: var(--font-mono);
font-size: 0.875rem;
line-height: 2;
overflow-x: auto;
}
.typed {
white-space: nowrap;
}
/* The tints match the app's own parse highlighting. */
mark {
background: color-mix(in srgb, var(--accent) 20%, transparent);
color: var(--ink);
border-radius: 3px;
padding: 1px 2px;
}
mark.p {
background: color-mix(in srgb, #cf6b6b 22%, transparent);
}
mark.proj {
background: color-mix(in srgb, #6b9ecf 22%, transparent);
}
mark.lbl {
background: color-mix(in srgb, #d69f5c 22%, transparent);
}
.arrow {
color: var(--ink-faint);
padding: 0 0.5rem;
}
.result {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
margin-top: 0.75rem;
font-family: var(--font);
font-size: 0.875rem;
}
.chip {
border: 1px solid var(--line-strong);
border-radius: 999px;
padding: 1px 0.625rem;
color: var(--ink-muted);
font-size: 0.75rem;
}
/* --- features --- */
section {
padding: 3rem 0;
}
h2 {
font-size: 1.5rem;
font-weight: 560;
letter-spacing: -0.02em;
margin: 0 0 2rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1px;
background: var(--line);
border: 1px solid var(--line);
border-radius: var(--radius-lg);
overflow: hidden;
}
.cell {
background: var(--ground);
padding: 1.5rem;
}
.cell h3 {
margin: 0 0 0.5rem;
font-size: 1rem;
font-weight: 560;
}
.cell p {
margin: 0;
font-size: 0.9375rem;
color: var(--ink-muted);
}
/* --- install --- */
pre {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1rem 1.25rem;
overflow-x: auto;
font-family: var(--font-mono);
font-size: 0.8125rem;
line-height: 1.7;
color: var(--ink);
}
.muted {
color: var(--ink-muted);
font-size: 0.9375rem;
}
ul.plain {
list-style: none;
padding: 0;
margin: 1rem 0 0;
color: var(--ink-muted);
font-size: 0.9375rem;
}
ul.plain li {
padding-left: 1.25rem;
position: relative;
margin-bottom: 0.375rem;
}
ul.plain li::before {
content: '·';
position: absolute;
left: 0.375rem;
color: var(--accent);
}
.rune-note {
margin: 1.25rem auto 0;
max-width: 32rem;
font-size: 0.9375rem;
color: var(--ink-muted);
}
.demo-note {
margin: 1rem 0 0;
font-family: var(--font);
font-size: 0.8125rem;
color: var(--ink-faint);
}
.demo-note code {
font-family: var(--font-mono);
color: var(--ink-muted);
}
.shot {
margin: 0 0 1.5rem;
}
.shot img {
width: 100%;
height: auto;
display: block;
border: 1px solid var(--line);
border-radius: var(--radius-lg);
}
.shot figcaption {
margin-top: 0.625rem;
font-size: 0.8125rem;
color: var(--ink-faint);
text-align: center;
}
.shots {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
h3.sub {
margin: 2rem 0 0.5rem;
font-size: 1rem;
font-weight: 560;
}
h3.sub:first-of-type {
margin-top: 0;
}
footer {
border-top: 1px solid var(--line);
margin-top: 4rem;
padding: 2rem 0 4rem;
text-align: center;
color: var(--ink-faint);
font-size: 0.875rem;
}
footer a {
color: var(--ink-muted);
}
a {
color: var(--accent);
}
</style>
</head>
<body>
<div class="wrap">
<header>
<span class="mark" aria-hidden="true">ᚹ</span>
<h1>verdande</h1>
<p class="lede">
Self-hosted tasks and projects, shared with the people you share the work with.
One binary, SQLite inside it.
</p>
<p class="rune-note">
Built to be deployed as a <strong>Rune</strong> in
<a href="https://github.com/kristianwind/yggdrasil">Yggdrasil Panel</a> — upload
the manifest, fill in two fields, press start. It also runs anywhere Docker does.
</p>
<p class="norn">
Verdande is one of the three Norns — the one who spins <em>that which is
becoming</em>. Not what has been, and not what is fated to be: the thing you are
in the middle of. Which is what a to-do list is.
</p>
<div class="actions">
<a class="button primary" href="https://kristianwind.github.io/verdande/self-hosting/">
Self-host it
</a>
<a class="button" href="https://github.com/kristianwind/verdande">Source</a>
<a class="button" href="https://kristianwind.github.io/verdande/">Documentation</a>
</div>
</header>
<!-- No screenshot: the quick-add parser is the thing worth showing, and it
shows better as the actual text than as a picture of it. -->
<div class="demo">
<div class="demo-bar">
<span aria-hidden="true">+</span>
<span>Add a task</span>
</div>
<div class="demo-body">
<div class="typed">
file the VAT return <mark>next friday</mark> <mark>at 10am</mark>
<mark class="p">p1</mark> <mark class="proj">#Accounts</mark>
<mark class="lbl">@tax</mark>
</div>
<div class="result">
<span class="arrow">→</span>
<strong>file the VAT return</strong>
<span class="chip">Fri 10:00</span>
<span class="chip">P1</span>
<span class="chip">Accounts</span>
<span class="chip">tax</span>
</div>
<p class="demo-note">
The same line works in Danish —
<code>betal moms på fredag kl 10 p1 #Firma @regnskab</code> — and the two
can be mixed mid-sentence.
</p>
</div>
</div>
<section>
<h2>What it looks like</h2>
<figure class="shot">
<img
src="screenshots/today.png"
alt="The Today view: a sidebar of projects, filters and labels beside a list of tasks due today, each with its priority ring, time and labels."
width="1400"
height="900"
loading="lazy"
/>
<figcaption>Today. Dark is the default; there is a light theme too.</figcaption>
</figure>
<div class="shots">
<figure class="shot">
<img
src="screenshots/board.png"
alt="A project in board view: one column per section, with task cards showing due dates, labels and repetition."
width="1400"
height="900"
loading="lazy"
/>
<figcaption>Board view — one column per section.</figcaption>
</figure>
<figure class="shot">
<img
src="screenshots/today-light.png"
alt="The same Today view in the light theme."
width="1400"
height="900"
loading="lazy"
/>
<figcaption>The light theme.</figcaption>
</figure>
<figure class="shot">
<img
src="screenshots/week.png"
alt="Upcoming as a single week: seven columns of days, each holding the tasks due that day."
width="1400"
height="900"
loading="lazy"
/>
<figcaption>A week at a time — so a task can be dragged across a month boundary.</figcaption>
</figure>
<figure class="shot">
<img
src="screenshots/detail.png"
alt="A task open in the detail drawer, with its description, due date, labels, sub-tasks and comments."
width="1400"
height="900"
loading="lazy"
/>
<figcaption>A task, opened.</figcaption>
</figure>
<figure class="shot">
<img
src="screenshots/themes.png"
alt="The settings page showing the five themes as swatches."
width="1400"
height="900"
loading="lazy"
/>
<figcaption>Five themes, all of them measured for contrast.</figcaption>
</figure>
<figure class="shot">
<img
src="screenshots/history.png"
alt="The instance-wide history page: who did what, in which project, filtered by person and event."
width="1400"
height="900"
loading="lazy"
/>
<figcaption>What has been done on the server, across every project.</figcaption>
</figure>
</div>
</section>
<section>
<h2>What it does</h2>
<div class="grid">
<div class="cell">
<h3>Reads what you type</h3>
<p>
Dates, times, priorities, projects, labels and repetition, parsed out of
one line — in Danish and English, mixed freely.
</p>
</div>
<div class="cell">
<h3>Shared, properly</h3>
<p>
Owner, editor and viewer roles. People outside your instance join by
invite link. Changes appear live.
</p>
</div>
<div class="cell">
<h3>A real CalDAV server</h3>
<p>
Two-way. Tick something off in Apple Reminders and it is ticked off
here. Not a read-only feed pretending.
</p>
</div>
<div class="cell">
<h3>Claude, connected</h3>
<p>
A built-in MCP server. Ask what is due this week, or have a task added,
in conversation.
</p>
</div>
<div class="cell">
<h3>Mail to task</h3>
<p>
Forward an email to your own address and it becomes a task, subject
parsed like anything else you type.
</p>
</div>
<div class="cell">
<h3>Yours to leave</h3>
<p>
Import from Todoist, export back to it, or take everything as JSON. The
round trip is tested, not promised.
</p>
</div>
</div>
</section>
<section>
<h2>Running it</h2>
<h3 class="sub">As a Rune in Yggdrasil Panel</h3>
<p class="muted">
This is what it was built for. <strong>Runes → Carve a rune</strong>, upload
<code>rune/verdande.yaml</code>, create a server from it, set the public address
and your mail server, and start it. No port forwarding — give it a subdomain and
the panel routes it through your Cloudflare Tunnel.
</p>
<h3 class="sub">Or with Docker, anywhere</h3>
<pre><code>docker run -d --name verdande \
-p 8080:8080 \
-v verdande-data:/data \
-e VERDANDE_BASE_URL=https://todo.example.dk \
ghcr.io/kristianwind/verdande:latest</code></pre>
<p class="muted" style="margin-top: 1rem">
Open the address and create the first account. That one is the administrator.
</p>
<ul class="plain">
<li>No database to provision — SQLite is inside the binary.</li>
<li>Distroless image: no shell, no package manager, one static file.</li>
<li>amd64 and arm64, so a Raspberry Pi is fine.</li>
<li>Nightly backups, fourteen kept, written with <code>VACUUM INTO</code>.</li>
</ul>
</section>
<section>
<h2>What it will not do</h2>
<p class="muted">
No karma or points. No team workspaces. No native apps. No mail integration
beyond Gmail and the forwarding address. No languages beyond Danish and English.
And no sync protocol of its own — CalDAV and the API <em>are</em> the sync.
</p>
</section>
<footer>
<p>
MIT licensed ·
<a href="https://github.com/kristianwind/verdande">github.com/kristianwind/verdande</a>
</p>
<p>
Built to run as a Rune in
<a href="https://github.com/kristianwind/yggdrasil">Yggdrasil Panel</a>.
</p>
</footer>
</div>
</body>
</html>