Ortszeit is in development. Sign-up is not open yet; if you are interested, write to me.

ORTSZEIT · FORMAT · SCHEMA VERSION 1

The portable format

Files are canonical. A blog is a directory of markdown files with YAML front matter and the photographs beside them. The database is a derived index — it makes place lookups and search fast, and it can be rebuilt from the files.

That is the whole ownership claim, and it is only worth anything if the format is written down. So here it is, versioned, with a deprecation path attached. This page is the contract: if you are writing a theme, a converter or a tool that reads an Ortszeit archive, build against this and not against whatever a particular export happened to contain.

Version

ortszeit_schema: 1

The key appears in blog.yaml and in posts.json. Individual post files do not carry it — they belong to the blog they sit in.

Layout

blog/
  blog.yaml                          the blog itself
  posts.json                         every post again, as JSON
  trips/
    denmark-2026/
      trip.yaml                      one trip
      2026-06-12-aarhus-harbour.md   a post in that trip
      media/
        IMG_4021.jpg
  posts/                             posts that belong to no trip
    2026-05-01-a-note.md
    media/
      IMG_3980.jpg
  • Post files are named YYYY-MM-DD-<slug>.md, dated from the same value as the post’s date field, so the filename and the front matter always agree.
  • Every post directory has its own media/. Media filenames are unique within one such directory; a collision gets -2, -3 and so on appended to the stem.
  • Trips with no posts yet are still exported. An empty trip is still yours.

Post front matter

---
title: Aarhus harbour
date: 2026-06-12T14:20:00+02:00
draft: false
place:
  name: Aarhus Havn
  osm: node/240109189
  lat: 56.1572
  lon: 10.2107
visibility: public
media:
  - file: media/IMG_4021.jpg
    taken_at: 2026-06-12T14:18:00+02:00
    alt: Ferry coming in past the harbour cranes
---

Rain until four, then the harbour went gold.

Field by field:

Key Type Notes
title string or null Always present, often null. A post without a title is normal, not broken.
date timestamp Timezone-aware. published_at for a published post, the creation time while it is still a draft — so the value always matches the filename.
draft boolean Always present. Drafts are exported: they are your writing, and an archive that silently drops them is not an archive.
place mapping Omitted entirely when the post has no place. Never written as null.
place.name string The display name.
place.osm string or null <osm_type>/<osm_id>, e.g. node/240109189. Null when the place came from a source that had no OpenStreetMap reference — imports, mostly.
place.lat number Decimal degrees.
place.lon number Decimal degrees. Note lon, not lng.
visibility string public, unlisted or password. This is the effective visibility, already resolved against the trip — a reader never has to follow inheritance across two files.
media list Always present, possibly empty. Order is the display order.
media[].file string Path relative to the post’s own directory, so it reads the same for trip posts and standalone posts.
media[].taken_at timestamp or null From the photograph’s EXIF. Null when the source never stated one.
media[].alt string or null Alt text. Null when empty.

The body is everything after the closing ---, as plain markdown, with the subset described in Getting started.

blog.yaml

ortszeit_schema: 1
title: Ortszeit
bio: Notes from the road.
subdomain: alice
custom_domain: null
theme: ortszeit
custom_css: ''
exported_at: 2026-06-20T09:14:03+00:00
Key Type Notes
ortszeit_schema integer The schema version of this archive.
title string Blog title.
bio string Short description. May be empty.
subdomain string The blog’s name under ortszeit.app.
custom_domain string or null Null when there is none.
theme string ortszeit today. The field exists so a theme ecosystem has somewhere to name itself.
custom_css string May be empty.
exported_at timestamp When this archive was written. The one field that is about the export rather than about the blog.

trip.yaml

title: Denmark 2026
slug: denmark-2026
started_on: 2026-06-10
ended_on: 2026-06-24
visibility: public
co_authors:
  - Alice
Key Type Notes
title string
slug string Unique within the blog; it is also the directory name.
started_on date Date, not timestamp.
ended_on date or null Null while the trip is still open.
visibility string public, unlisted or password.
co_authors list of strings Display names.

A trip password is never exported, in any tier and in any form — not the password, not its hash. A visibility: password trip therefore arrives somewhere else without protection, and any importer must treat it as private rather than public. Ortszeit’s own importer downgrades it to unlisted and tells you to set a new password.

posts.json

The same content again, as one JSON document, for tools that would rather not parse front matter:

{
  "ortszeit_schema": 1,
  "blog": { "ortszeit_schema": 1, "title": "Ortszeit", "...": "..." },
  "posts": [
    {
      "path": "trips/denmark-2026/2026-06-12-aarhus-harbour.md",
      "slug": "aarhus-harbour",
      "trip": "denmark-2026",
      "title": "Aarhus harbour",
      "date": "2026-06-12T14:20:00+02:00",
      "draft": false,
      "place": { "name": "Aarhus Havn", "osm": "node/240109189", "lat": 56.1572, "lon": 10.2107 },
      "visibility": "public",
      "media": [
        { "file": "media/IMG_4021.jpg", "taken_at": "2026-06-12T14:18:00+02:00", "alt": null }
      ],
      "body": "Rain until four, then the harbour went gold."
    }
  ]
}

Each entry carries the post’s front matter verbatim, plus three keys the markdown file expresses through its location instead:

Key Type Notes
path string The post file’s path inside blog/.
slug string The post’s slug.
trip string or null The trip’s slug, or null for a standalone post.
body string The markdown body.

Dates and timestamps are ISO 8601 strings in JSON, where the YAML files use real date and timestamp values.

Reading the format

Three rules, so that a reader written today still works later:

  1. Ignore keys you do not recognise. New optional keys can appear within a schema version.
  2. Do not depend on key order. The writer emits keys in a stable order because it makes diffs readable, but YAML mappings are unordered and a conforming reader must treat them that way.
  3. Distinguish “absent” from “null”. place is absent when there is no place. title is present and null when there is no title. That difference is deliberate and stable.

Versioning and deprecation

ortszeit_schema is an integer. It is not a date and not a semantic version.

What does not bump the version: adding a new optional key, adding a new allowed value to a field that readers already have to handle generically, or writing a value that was previously always absent. Readers that follow rule 1 above are unaffected.

What bumps the version: removing a key, renaming a key, changing a key’s type, or changing the meaning of an existing value.

The promise attached to a bump:

  • The new version is documented on this page before anything writes it.
  • The previous version stays documented here, marked deprecated, with the date it stopped being written.
  • Ortszeit’s own importer accepts the previous version for at least twelve months after that date, and in practice indefinitely for version 1 — being able to restore an old archive is the entire point of the format.
  • A version bump is announced with the release that ships it, not afterwards.

What the format does not contain

Stated plainly, because a documented gap is a limitation and an undocumented one is a nasty surprise:

  • Trip passwords, as above.
  • Media not attached to any post. The layout has no place for it, so it is not exported. This is a known gap.
  • The per-trip PDF promised on paid exports. It is specified but not built yet, and it would not be part of this tree in any case — it is a rendering of the content, not the content.
  • Account, subscription and quota state. None of it is yours to move; all of it is derived.