---
layout: ../layouts/BaseLayout.astro
title: Technical Documentation
---

# Technical documentation for the `vojtamaur-web` project

## 1. Project overview

`vojtamaur-web` is a static website built with Astro. Content is managed as files in the repository and converted into static output during the build process. The project does not use a CMS or a database. The source of truth is the repository containing the source code, content, and static assets.

The project is divided into the following main content sections:

- **Volná tvorba**
- **Výstavy**
- **Cestování**
- **Propagační videa**
- **O mně**
- **Kontakt**

The architecture is based on the following components:

- **Astro** as the static site generator
- **MDX** as the authoring format for content
- **Content Collections** for metadata validation and typing
- **Components** for reusable content blocks and layouts
- **Static assets in `public/`** for images, PDFs, demos, and other files
- **A generated Gemini capsule in `dist-gemini/`** as a text-oriented hypertext edition of the finished website

This model makes it easier to version content, archive build outputs, and potentially migrate the project to another environment without relying on a database runtime.

---

## 2. Project structure

### 2.1 Directory structure

The current project structure is divided into two main parts:

- `src/` – project source files
- `public/` – static assets copied unchanged during the build

Provided structure:

```text
public/
  demos/
  files/
  images/
  keys/

src/
  components/
  content/
  content.config.ts
  env.d.ts
  layouts/
  lib/
  pages/
  styles/
```

Generated output directories are kept outside the source tree:

```text
dist/           # normal web or portable build output
dist-arweave/   # derived Arweave / Permaweb deployment output
dist-gemini/    # generated Gemini capsule
```

These directories are build artifacts and should not be edited as source content.

### 2.2 Meaning of the main directories

#### `public/`
Contains static files that are simply copied into the output during the build:

- `public/images/` – article images, thumbnails, and other visual content
- `public/files/` – PDFs and other downloadable or embeddable files
- `public/demos/` – standalone HTML/JS demos and legacy static pages
- `public/keys/` – public OpenPGP material copied unchanged into the build, including the public key and its fingerprint

#### `src/content/`
Project content files. In the current configuration, the following are mainly used:

- `src/content/posts/` – articles
- `src/content/videos/` – metadata for promotional videos

#### `src/components/`
Reusable components for working with content and listings:

- `ImageFigure.astro`
- `MediaRow.astro`
- `Embed.astro`
- `PostTileGrid.astro`
- `OpenPgpContact.astro`
- `OpenPgpFingerprint.astro`
- `Header.astro`

#### `src/layouts/`
Layouts for individual page types:

- `BaseLayout.astro`
- `PostLayout.astro`
- potentially specialized layouts such as `TravelLayout.astro` and `ExhibitionLayout.astro`

#### `src/pages/`
Application routes. Includes the homepage, category pages, and dynamic article routing.

#### `src/styles/`
Global and optionally other style files.

#### `src/lib/`
Helper utilities and shared logic used across the project.

#### `source-bundle/`
Templates used by `scripts/generate-source-bundle.mjs` when creating the reconstructable source package. This directory contains the Python asset downloader and the reconstruction README that are copied into the generated source ZIP.

---

## 3. Key configuration files

### `astro.config.mjs`

The project uses two Astro build modes. The configuration switches behavior according to the `BUILD_TARGET` variable. In the standard web build it uses `trailingSlash: "always"` and `build.format: "directory"`. In the portable file-based build it uses `trailingSlash: "never"` and `build.format: "file"`.

This produces two primary Astro output types:

- **web build** – suitable for standard hosting
- **portable file-based build** – suitable, for example, for offline use, archiving, or transfer on external media

A third deployment artifact is derived from the standard web build:

- **Arweave / Permaweb build** – generated into `dist-arweave/` by copying the finished `dist/` output and rewriting root-relative paths so that the site can work under an Arweave manifest transaction path

The Arweave build is not a separate Astro `BUILD_TARGET`. It is a postprocessed version of the normal web build.

A fourth publication artifact is also derived from the finished standard web build:

- **Gemini capsule** – generated into `dist-gemini/` by `scripts/generate-gemini-capsule.mjs`

The Gemini capsule is not an Astro `BUILD_TARGET` and is not placed inside `dist/`. It reads the finished HTML in `dist/` so that the English postprocess is already reflected in the exported content, while article frontmatter remains available for section membership, dates, slugs, and ordering.

### `package.json`

Basic project workflow:

```bash
npm run dev
npm run build:web
npm run build:web:signed
npm run build:web:translate
npm run build:web:translate:signed
npm run build:translate:signed
npm run build:web:strict
npm run build:web:strict:signed
npm run build:web:prune:dry
npm run build:web:prune
npm run preview
npm run build:usb
npm run build:usb:signed
npm run build:usb:translate
npm run build:usb:strict
npm run build:usb:strict:signed
npm run build:usb:prune:dry
npm run build:usb:prune
npm run build:arweave
npm run build:arweave:signed
npm run generate:all-posts
npm run generate:gemini
npm run build:gemini
npm run generate:source-bundle
npm run generate:integrity
npm run generate:integrity:arweave
npm run sign:build
npm run sign:build:arweave
npm run export:pdf
```

Meaning of the main scripts:

- `npm run dev` – starts the Astro development server
- `npm run build:web` – creates the standard production web build, applies the English postprocess using existing cache only, generates `dist/ALL_POSTS.txt`, generates the reconstructable source package at `dist/source/vojtamaur-web-source.zip`, and writes unsigned build integrity metadata for `dist/`
- `npm run build:web:signed` – runs the standard web build and then creates and verifies the detached OpenPGP signature for `dist/SHA256SUMS.txt`
- `npm run build:signed` – alias for `npm run build:web:signed`
- `npm run build:web:translate` – creates the web build, fills missing English translation cache entries through DeepL, generates `dist/ALL_POSTS.txt`, generates the reconstructable source package, writes unsigned build integrity metadata for `dist/`, and then generates the Gemini capsule in `dist-gemini/`
- `npm run build:web:translate:signed` – runs the translating web build, including Gemini capsule generation, and then signs and verifies the checksum manifest for `dist/`; the detached signature does not cover the separate `dist-gemini/` directory
- `npm run build:translate:signed` – alias for `npm run build:web:translate:signed`
- `npm run build:web:strict` – creates the web build, fails if an English translation cache entry is missing, generates `dist/ALL_POSTS.txt`, generates the reconstructable source package, and writes unsigned build integrity metadata for `dist/`
- `npm run build:web:strict:signed` – runs the strict web build and then signs and verifies its checksum manifest
- `npm run build:web:prune:dry` – runs the strict web build and prints unused English translation cache files that would be deleted, without deleting them
- `npm run build:web:prune` – runs the strict web build and deletes unused, unprotected English translation cache files
- `npm run preview` – local preview of the build
- `npm run build:usb` – creates a portable file-based build, applies the English postprocess, generates `dist/ALL_POSTS.txt`, generates the reconstructable source package, rewrites paths for offline use, and writes unsigned build integrity metadata for `dist/`
- `npm run build:usb:signed` – runs the portable build and then signs and verifies its checksum manifest
- `npm run build:usb:translate` – creates a portable file-based build, fills missing English translation cache entries, generates `dist/ALL_POSTS.txt`, generates the reconstructable source package, rewrites paths for offline use, and writes unsigned build integrity metadata for `dist/`
- `npm run build:usb:strict` – creates a strict portable file-based build and fails if an English translation cache entry is missing
- `npm run build:usb:strict:signed` – runs the strict portable build and then signs and verifies its checksum manifest
- `npm run build:usb:prune:dry` – runs the strict portable build and prints unused English translation cache files that would be deleted, without deleting them
- `npm run build:usb:prune` – runs the strict portable build and deletes unused, unprotected English translation cache files
- `npm run build:arweave` – creates the strict standard web build, creates `dist-arweave/` as a Permaweb-compatible deployment copy with rewritten paths, and then writes separate unsigned build integrity metadata for `dist-arweave/`
- `npm run build:arweave:signed` – runs the Arweave build and then signs and verifies `dist-arweave/SHA256SUMS.txt`
- `npm run generate:all-posts` – generates `dist/ALL_POSTS.txt`, a plain-text export of all article content from the finished build output
- `npm run generate:gemini` – replaces `dist-gemini/` with a freshly generated bilingual Gemini capsule based on the existing finished `dist/` build and the post frontmatter
- `npm run build:gemini` – runs the strict standard web build and then generates `dist-gemini/`; this is the standalone complete Gemini build command
- `npm run generate:source-bundle` – generates `dist/source/vojtamaur-web-source.zip`, a small reconstructable source package with a media manifest and an asset downloader
- `npm run generate:integrity` – generates `SHA256SUMS.txt`, `BUILD_SHA256.txt`, `integrity.json`, and an unsigned `SIGNING_STATUS.txt` for `dist/`; any stale detached signature is removed
- `npm run generate:integrity:arweave` – performs the same integrity pass for `dist-arweave/` after the Arweave copy-and-rewrite step
- `npm run sign:build` – signs the existing `dist/SHA256SUMS.txt`, verifies the detached signature immediately, and updates the informational signing status and JSON metadata
- `npm run sign:build:arweave` – performs the same explicit local signing step for `dist-arweave/`
- `npm run export:pdf` – manually exports article pages from the finished `dist/` build to archival PDF files; this command is not part of the normal build pipeline



### `content.config.ts`

Defines content collections and metadata schemas using Zod validation. The project uses at least the following collections:

- `posts`
- `videos`

---

## 4. Content Collections

### 4.1 `posts` collection

Articles are stored as `.mdx` files and validated against the schema in `content.config.ts`.

Required metadata:

- `title`
- `slug`
- `section`
- `date`
- `thumbnail`
- `thumbnailAlt`

Optional metadata:

- `excerpt`
- `draft`

Section-specific metadata:

#### For `section: "vystavy"`
- `dateFrom`
- `dateTo`
- `city`
- `venue`
- `exhibition`

#### For `section: "cestovani"`
- `year`
- `media`

### 4.2 `videos` collection

Used for the Propagační videa section. Contains metadata for external YouTube videos. Typically:

- `title`
- `url`
- `thumbnail`
- `thumbnailAlt`
- `draft`

---

## 5. Layout logic

### 5.1 `PostLayout.astro`

`PostLayout.astro` wraps article content in the main layout and creates a shared wrapper for article pages.

### 5.2 Dynamic routing through `[slug].astro`

The `[slug].astro` file is the central route for content from the `posts` collection. It handles:

- loading articles via `getCollection`
- generating static paths based on `slug`
- rendering specific content via `render(post)`

### 5.3 Conditional rendering by section

Different sections have different meta blocks:

- **Volná tvorba** – title + formatted month and year
- **Výstavy** – title + event date + city + venue + exhibition title
- **Cestování** – title + supplementary metadata such as medium

### 5.4 Date formatting

In the Volná tvorba section, the date is displayed as month and year, for example:

```text
duben 2026
```

Internally, the standard `date` field is still used for sorting.

### 5.5 Sorting

Articles are sorted by `date`. This also applies in cases where the UI does not display the exact day, but only the month and year.

### 5.5.1 Legacy date migration note

This project was created as a replacement for an older website with fragile infrastructure (outdated PHP, WordPress, unmaintained plugins, and dependence on third-party systems).

That legacy site displayed only the month and year for many articles in the Volná tvorba section (for example `duben 2020`). During migration, the exact original day was often no longer recoverable. In such cases, the `date` field was normalized to the first day of the given month (for example `2020-04-01`) in order to preserve sorting behavior.

This means that for part of the legacy content, the stored day may be approximate and should be understood as a technical migration value rather than an exact historical publication date.

Articles added after April 2026 use the real day in the `date` field whenever that information is available.

---

## 6. Adding and managing content

### 6.1 Adding a new article

A new article is added by creating a new `.mdx` file in:

```text
src/content/posts/
```

The file must contain valid frontmatter according to the `posts` schema.

### 6.2 Required and optional metadata

#### Shared required metadata
- `title`
- `slug`
- `section`
- `date`
- `thumbnail`
- `thumbnailAlt`

#### Shared optional metadata
- `excerpt`
- `draft`

#### Metadata for Výstavy
- `dateFrom`
- `dateTo`
- `city`
- `venue`
- `exhibition`

#### Metadata for Cestování
- `year`
- `media`

### 6.3 Thumbnails

Each article uses:

- `thumbnail` – path to the thumbnail
- `thumbnailAlt` – thumbnail alt text

Thumbnails are used in article listings, on the homepage, and in individual sections.  

### 6.3.1 Public asset metadata and privacy

Files stored in `public/` are copied into the build output unchanged. This includes images in `public/images/` and downloadable or embeddable files in `public/files/`. Embedded metadata in these files must therefore be treated as public data once the files are committed and published.

This applies especially to:

- EXIF camera metadata
- GPS coordinates
- XMP and IPTC metadata
- software and editing history
- PDF author, creator, producer, and title fields
- internal document IDs and document lineage metadata
- embedded comments, captions, keywords, or other hidden text fields

The project uses a separate metadata audit script for checking public assets:

```bash
python scripts/audit-public-metadata.py --exiftool "D:\Program Files\exiftool\exiftool.exe"
```

ExifTool path may need to be adjusted depending on the local installation.

The script checks:

- `public/images/`
- `public/files/`

It reports privacy-relevant metadata such as camera model, GPS data, author fields, software history, PDF metadata, document IDs, and embedded comments.

Default rule:

- files committed to the public repository should not contain unintended metadata
- original files with full metadata should be kept outside the public repository, if they need to be preserved
- metadata should only be kept when it is intentionally part of the work itself
- intentional metadata exceptions should be handled through the script allowlist

For normal publishing, the recommended workflow is:

1. keep the original file in a private archive, if needed
2. export or copy a public version of the file
3. run the metadata audit
4. strip unintended metadata from the public version
5. run the metadata audit again
6. commit only the cleaned public version into `public/`

The audit and stripping process is not part of the Astro build. It is a separate maintenance step. This is intentional: metadata should be removed before commit, not only from the generated `dist/` output, because the source repository, mirrors, releases, and archival snapshots may preserve the original files.  

### 6.4 `draft`

The `draft: true` field excludes the article from the public listing and generated paths. It is used for content that is in progress or temporarily hidden.

### 6.5 Components used in content

Besides standard Markdown, article content can also use the following components:

- `ImageFigure`
- `MediaRow`
- `Embed`

These components must be explicitly imported in the MDX file.

### 6.5.1 `EntryPoints.astro`

The `EntryPoints.astro` component is used in the meta article to render the main archive entry points.

It reads the `public/ARCHIVE.txt` file, extracts the section between the markers `=== ENTRY POINTS START ===` and `=== ENTRY POINTS END ===`, and displays it inside a `<pre>` block.

This ensures that the list of primary locations and snapshots is maintained in a single source of truth and does not need to be manually duplicated in the article content.

### 6.6 Example frontmatter

#### Volná tvorba

```mdx
---
title: "Název článku"
slug: "nazev-clanku"
section: "volna-tvorba"
date: 2026-04-19
thumbnail: "/images/nazev-clanku-nahled.jpg"
thumbnailAlt: "Náhled článku"
excerpt: ""
draft: false
---
```

#### Výstavy

```mdx
---
title: "Recamánova struktura"
slug: "vystavy-recamanova-struktura"
section: "vystavy"
date: 2024-01-01
thumbnail: "/images/vystavy-recamanova-struktura-nahled.jpg"
thumbnailAlt: "Recamánova struktura"
dateFrom: "1. 1. 2024"
dateTo: "31. 1. 2024"
city: "Jindřichův Hradec"
venue: "Muzeum fotografie a moderních obrazových médií"
exhibition: "Obrazy nad čísly"
draft: false
---
```

#### Cestování

```mdx
---
title: "Itálie - Benátky 2019"
slug: "cestovani-italie-benatky-2019"
section: "cestovani"
date: 2019-01-01
thumbnail: "/images/cestovani-italie-benatky-2019-nahled.jpg"
thumbnailAlt: "Itálie - Benátky 2019"
year: "2019"
media: "Fotografie"
draft: false
---
```

### 6.7 Representative example of content

The following file was provided as a representative example of an MDX article:

```text
recamanova-posloupnost-zelvi-grafice.mdx
```

This file is suitable as a reference example of content structure, frontmatter, and component usage.

---

## 7. Media components

### 7.1 `ImageFigure.astro`

A component for standalone images with support for the following parameters:

- `src`
- `alt`
- `caption`
- `width`
- `align`
- `bordered`
- `link`

Supported width variants:

- `full`
- `half`
- `one-third`

Supported alignment options:

- `left`
- `center`
- `right`

Depending on the configuration, the component can also open the image when clicked.

### 7.2 `MediaRow.astro`

A component for arranging multiple items in a row. Supports the following types:

- `image`
- `pdf`
- `text`
- `empty`

Use cases:

- galleries with multiple images
- combinations of images and PDFs
- combinations of visual and text blocks in a single grid
- intentional empty cells used to keep a row layout without creating fake media

The component also supports a bordered variant.

Image items support `alt` text. The value is rendered directly into the image `alt` attribute. If `alt` is omitted, the component renders an empty `alt` attribute.

Example:

```mdx
<MediaRow
  bordered
  items={[
    {
      type: "image",
      src: "/images/example-1.jpg",
      alt: "Popis prvního obrázku"
    },
    {
      type: "image",
      src: "/images/example-2.jpg",
      alt: "Popis druhého obrázku"
    },
    {
      type: "pdf",
      src: "/files/example.pdf"
    },
    {
      type: "text",
      content: "Textový blok v řádku médií."
    }
  ]}
/>
```

The `alt` value applies only to `type: "image"` items. It is not automatically translated by the English postprocess.

Use `type: "empty"` when a row should keep an intentionally blank cell. Do not create placeholder image items with an empty `src`, because that produces broken media in the rendered HTML and can also leak into preservation exports such as `ALL_POSTS.txt`.

Example with one image and two empty cells:

```mdx
<MediaRow
  items={[
    {
      type: "image",
      src: "/images/nahodna-cisla-zvolena-clovekem-obr-1.jpg",
      alt: "Graf náhodných čísel zvolených počítačem"
    },
    { type: "empty" },
    { type: "empty" }
  ]}
/>
```

Empty items render only an empty `.media-row__item` cell marked with `aria-hidden="true"`. They do not render an image, link, iframe, or text block.

### 7.3 `Embed.astro`

A generic wrapper for embedded iframe content. It is used, for example, for:

- YouTube
- Google Maps
- Sketchfab
- local HTML demos

Supported parameters:

- `src`
- `ratio`
- `kind`
- `width`
- `align`

For YouTube embeds, prefer the privacy-enhanced `youtube-nocookie.com` domain instead of the standard `youtube.com` embed URL:

```mdx
<Embed
  src="https://www.youtube-nocookie.com/embed/6wDN62Xq3pA"
  kind="youtube"
/>
```

This reduces unnecessary YouTube cookie use, although the iframe still loads content from a third-party service.

### 7.4 Edge cases

#### PDF in `MediaRow`
When the grid collapses, a PDF iframe may require special adjustment of height or aspect ratio. This was handled using CSS for `.media-row__pdf`.

#### Responsive grid collapse
Listings and media layouts have multiple states depending on screen width. Some elements, such as the “show all” button or PDF embeds, required separate behavior adjustments for 3, 2, and 1 column layouts.

---

## 8. Homepage architecture

The homepage combines content from multiple parts of the website.

### 8.1 Dynamic content loading
Each main section on the homepage displays the latest 9 items:

- Volná tvorba
- Výstavy
- Cestování

### 8.2 “Show all” button
If a given section contains more items than the number displayed on the homepage, a “show all” tile is added.

### 8.3 Propagační videa
The Propagační videa section uses a visual model similar to article listings, but the items link to external YouTube URLs. The listing is based on the `videos` collection.

### 8.4 Clickable section headings
Section headings on the homepage are clickable and serve as quick navigation to the relevant categories or an external playlist.

### 8.5 O mně and Kontakt
The homepage also contains specialized content blocks outside the standard article system:

- an O mně block with text and a Sketchfab iframe
- a Kontakt block with phone number, email, and the public OpenPGP identity

### 8.6 OpenPGP contact block

Both homepage variants use the shared component:

```astro
<OpenPgpContact lang={lang} />
```

The component is rendered from both `src/pages/index.astro` and `src/pages/en/index.astro`. It provides short Czech or English labels while keeping the cryptographic material shared between both language versions.

The component reads these files during the Astro build:

```text
public/keys/vojta-maur-openpgp.asc
public/keys/vojta-maur-openpgp-fingerprint.txt
```

`OpenPgpContact.astro` therefore does not duplicate the armored public key or fingerprint inside either homepage file. The public key remains directly visible in the rendered HTML, while the fingerprint is normalized to a single space-separated line. Both values are marked with `translate="no"` so that the translation postprocess cannot modify them.

### 8.7 Gemini homepage parity

The generated Gemini homepage intentionally follows the content logic of the HTML homepage rather than reducing the capsule to a single archive dump.

For **Volná tvorba / Personal Work**, **Výstavy / Exhibitions**, and **Cestování / Travel**, the capsule shows the latest nine entries in the same order as the built homepage. If the HTML homepage contains its final “show all” tile, the Gemini homepage appends an uppercase `ZOBRAZIT VŠE` or `SHOW ALL` link after those entries.

The remaining sections are handled differently:

- **Propagační videa / Promotional Videos** lists the individual external video links directly on the homepage and ends with `ZOBRAZIT VŠE` or `SHOW ALL` pointing directly to the YouTube playlist.
- **O mně / About Me** is rendered directly on the Gemini homepage, including the external link to the Sketchfab model.
- **Kontakt / Contact** is rendered directly on the Gemini homepage, including active `tel:`, `mailto:`, and OpenPGP material links or blocks.

No separate Gemini section pages are created for Promotional Videos, About Me, or Contact. English is the capsule root language at `/index.gmi`; Czech is available under `/cs/index.gmi`.

---

## 9. Development and normal workflow

### 9.1 Local development

```bash
npm install
npm run dev
```

Astro normally starts the local server at `http://localhost:4321/` and reacts continuously to changes in the project.

### 9.2 Practical note about the dev server

In this specific project, it sometimes happens that after adding a new `.mdx` file, the new article does not appear correctly in dev mode, or temporarily replaces another article in the listing. Restarting the development server usually fixes it immediately.

Recommended troubleshooting step:

```bash
Ctrl + C
npm run dev
```

### 9.2.1 Public asset metadata audit

Before publishing new or changed public assets, run the metadata audit:

```bash
python scripts/audit-public-metadata.py --exiftool "D:\Program Files\exiftool\exiftool.exe"
```

The script scans `public/images/` and `public/files/` and reports files containing privacy-relevant embedded metadata.

To preview metadata stripping without modifying files:

```bash
python scripts/audit-public-metadata.py --exiftool "D:\Program Files\exiftool\exiftool.exe" --strip --dry-run
```

To strip unintended metadata from supported image files:

```bash
python scripts/audit-public-metadata.py --exiftool "D:\Program Files\exiftool\exiftool.exe" --strip
```

PDF files are treated more conservatively and are not stripped by default. If PDF metadata needs to be removed, create a checked public copy and verify the output afterwards.

After stripping metadata, run the audit again and check the changed files before committing.

Files with intentional embedded metadata can be kept through the script allowlist. These exceptions should remain explicit, because otherwise hidden metadata becomes indistinguishable from accidental leakage.


### 9.3 Web build

```bash
npm run build:web
```

Creates the standard build intended for normal deployment to web hosting. This command uses existing English translation cache entries, but it does not create new DeepL translations.

The web build also creates the reconstructable source package at `dist/source/vojtamaur-web-source.zip`. The link to this package is root-relative in the source content (`/source/vojtamaur-web-source.zip`), which is correct for normal web hosting.

To create missing English translations, use:

```bash
npm run build:web:translate
```

To verify that no English translation cache entry is missing, use:

```bash
npm run build:web:strict
```

To inspect unused English translation cache files without deleting them, use:

```bash
npm run build:web:prune:dry
```

To delete unused, unprotected English translation cache files after the dry run looks correct, use:

```bash
npm run build:web:prune
```

### 9.4 Build preview

```bash
npm run preview
```

Used for local verification of the production build.

### 9.5 Portable file-based build

```bash
npm run build:usb
```

This build is suitable, for example, for offline use, archiving, or transfer as a set of files.

The USB build also includes the reconstructable source package. In USB mode, `scripts/usb-rewrite.mjs` must run after the source package has been generated so that `/source/vojtamaur-web-source.zip` is rewritten to a relative `file://`-safe link.

To create missing English translations in the portable build, use:

```bash
npm run build:usb:translate
```

For a strict USB check, use:

```bash
npm run build:usb:strict
```

To inspect unused English translation cache files while producing the portable build, use:

```bash
npm run build:usb:prune:dry
```

To delete unused, unprotected English translation cache files after the dry run looks correct, use:

```bash
npm run build:usb:prune
```

### 9.6 Plain-text export of all posts

During the build process, the project also generates a plain-text export of all article content:

```text
/ALL_POSTS.txt
```

The export is produced by:

```text
scripts/generate-all-posts.mjs
```

The script reads the finished static HTML from `dist/`, extracts the main article content, converts it into plain text, and writes the result to `dist/ALL_POSTS.txt`.

This is intentionally generated from the built output rather than directly from the source `.mdx` files. The English version is created as a post-build static artifact by `scripts/en-postprocess.mjs`, so reading from `dist/` allows the export to include both Czech and English article versions.

The file is intended as a minimal preservation layer for:

- long-term archiving
- indexing
- full-text search
- offline reading
- recovery of textual content even if the original HTML structure, CSS, JavaScript, or build system becomes unusable

The export includes metadata for each article, such as title, slug, canonical URL, language, section, date, source file, and built HTML path.

Non-textual and embedded content is represented by explicit placeholders instead of being silently removed. For example:

```text
[MEDIA: image]
[VIDEO EMBED]
[INTERACTIVE EMBED]
[PDF EMBED]
[SVG CONTENT OMITTED]
```

HTML tables are converted into readable plain-text table blocks marked with `[TABLE]` and `[/TABLE]`. Table rows and cells are preserved in a Markdown-like form so that tabular data remains legible in the linear text export.

Code blocks and generated output blocks are preserved as `[CODE BLOCK]` sections. Very large code or generated output blocks are truncated when they exceed the configured size limits. The export keeps the beginning of the block up to the configured line or character limit, then adds an explicit truncation note with the original size and the amount of omitted content.

This prevents one unusually large generated block from making the entire text export difficult to read or process while still preserving a readable sample of the original block. The full version remains available in the rendered website, source repository, or static snapshots.

The output file is written as UTF-8 with BOM to improve encoding detection in text editors and archival systems.

#### Manual filtered and compact exports

The generated `dist/ALL_POSTS.txt` can be filtered manually with:

```text
scripts/filter-all-posts.py
```

This Python script is a separate post-processing tool. It is not called by `npm run build:web`, `npm run build:usb`, `npm run build:arweave`, `npm run generate:all-posts`, or any other normal build command. It reads an already generated `dist/ALL_POSTS.txt` and does not modify the source export.

The script has no external Python dependencies. When it is stored in `scripts/`, the default input is resolved automatically as `dist/ALL_POSTS.txt`. Available languages, sections, metadata fields, and current article counts can be inspected with:

```bash
python scripts/filter-all-posts.py --list-values
```

Article selection can be restricted by language, section, or an inclusive date range. Multiple languages or sections can be supplied by repeating an option or by separating values with commas. For example:

```bash
python scripts/filter-all-posts.py --language cs --section volna-tvorba,vystavy --format structured --metadata full
python scripts/filter-all-posts.py --language en --section volna-tvorba --format compact
python scripts/filter-all-posts.py --from-date 2020-01-01 --to-date 2026-12-31 --format structured
```

Two output formats are available:

- `--format structured` is the default. It retains the delimited article-block structure and can preserve all metadata or use the `full`, `archive`, `minimal`, or `none` metadata profiles. Exact metadata fields can also be selected with `--metadata-fields`, for example `--metadata-fields TITLE,URL,DATE`.
- `--format compact` creates a compact reading and transfer copy. Long separator lines and repeated article metadata blocks are removed. Each article starts with [rendered title|YYYY-MM]; image references are reduced to records such as [img:filename|ALT: ...|CAPTION: ...]; and video, PDF, and interactive embeds use equivalent one-line records. Redundant blank lines and generic embed notes are omitted.

Compact output and structured output with `--metadata none` are intentionally not intended as inputs for another filtering pass because the metadata required for reliable filtering has been removed. Use structured output with sufficient metadata when another filtering step may be needed later.

The default output directory is:

```text
exports/
```

Output filenames describe the selected language, section, date range, format, and metadata profile. A different path can be supplied with `--output`. The `exports/` directory is generated and excluded through `.gitignore`, while `scripts/filter-all-posts.py` remains a versioned source file.

Filtered exports are written outside `dist/` by default so that they do not silently change the contents described by the existing build integrity manifests. If an explicit `--output` path is placed inside `dist/`, the script prints a warning because the existing `SHA256SUMS.txt`, `BUILD_SHA256.txt`, and `integrity.json` do not cover the newly created file.

A selection can be fully parsed and validated without writing a file:

```bash
python scripts/filter-all-posts.py --language cs --section volna-tvorba --format structured --metadata full --dry-run
```

The script refuses to overwrite its input, rejects unknown language or section values, and rejects an empty selection unless `--allow-empty` is supplied explicitly. It reports the selected article count, output path, and SHA-256 hash after processing. Like the source export, written output uses UTF-8 with BOM.

### 9.6.1 Build integrity and OpenPGP signatures

During the final post-build phase, the project generates integrity metadata for the completed static output:

```text
/SHA256SUMS.txt
/BUILD_SHA256.txt
/integrity.json
/SIGNING_STATUS.txt
```

These files are produced by:

```text
scripts/generate-integrity.mjs
```

The script walks through the selected output directory, calculates a SHA-256 checksum for each included file, writes a sorted per-file manifest to `SHA256SUMS.txt`, and then calculates a global build hash from that manifest.

The global build hash is stored in:

```text
BUILD_SHA256.txt
```

Machine-readable informational metadata is stored in:

```text
integrity.json
```

Every ordinary integrity pass starts in an unsigned state. It removes any stale `SHA256SUMS.txt.asc`, writes `SIGNING_STATUS.txt` as unsigned, and records `openPgp.present: false` in `integrity.json`. This prevents a detached signature from an older build from surviving after the manifest has changed.

The following files are intentionally excluded from the checksum manifest:

```text
SHA256SUMS.txt
SHA256SUMS
BUILD_SHA256.txt
integrity.json
SHA256SUMS.txt.asc
SIGNING_STATUS.txt
.DS_Store
Thumbs.db
```

The checksum and signing metadata files cannot describe themselves without creating a recursive dependency. In particular, `integrity.json` and `SIGNING_STATUS.txt` are informational and are not cryptographic proof that a build is signed.

The manifest uses the `.txt` extension intentionally. A bare file named `SHA256SUMS` can be interpreted badly by local preview or static hosting setups that use directory-style routes and `trailingSlash: "always"`. `SHA256SUMS.txt` behaves as a normal downloadable text file.

The integrity files are generated from the final build output, not from the source files. They are intended to verify the published static artifact after Astro build, English postprocessing, generated text exports, path rewriting for portable builds, and any other post-build changes that happen before `scripts/generate-integrity.mjs` runs.

For the normal web build and USB build, the integrity files describe the final contents of `dist/`.

For the Arweave / Permaweb build, integrity is generated again after `dist-arweave/` is created, so the integrity files inside `dist-arweave/` describe the final Arweave deployment artifact rather than the original `dist/` directory.

The public OpenPGP identity used for signed build artifacts is published separately in the static build:

```text
/keys/vojta-maur-openpgp.asc
/keys/vojta-maur-openpgp-fingerprint.txt
```

SHA-256 verifies whether the files match a particular manifest. The detached OpenPGP signature authenticates that exact manifest as one signed by the holder of the corresponding private key. Publishing the public key alone does not authenticate a deployment.

The explicit signing step is implemented by:

```text
scripts/sign-build.mjs
```

It reads the expected primary-key fingerprint from `public/keys/vojta-maur-openpgp-fingerprint.txt`, locates the private key through `GNUPGHOME`, creates the armored detached signature:

```text
/SHA256SUMS.txt.asc
```

and immediately verifies the new signature against `SHA256SUMS.txt`. Only after successful verification does it write a signed `SIGNING_STATUS.txt` and update the informational `openPgp` object in `integrity.json`.

If signing fails, the script removes `SHA256SUMS.txt.asc`, writes a failed unsigned status where possible, records `openPgp.present: false`, and exits with a non-zero status. A build is therefore signed only when the detached signature is present and verifies successfully.

When `GNUPGHOME` is already set, the script validates that directory and confirms that it contains the requested secret key. When it is not set and an interactive terminal is available, the script asks for the GnuPG home directory. In a non-interactive environment, `GNUPGHOME` must be set explicitly.

A Windows CMD example is:

```bat
set "GNUPGHOME=C:\path\to\gnupg"
npm run build:web:signed
```

The private signing key is never stored in the repository, copied into `public/`, included in the build, or provided to third-party CI systems.

The available signed build commands are:

```bash
npm run build:web:signed
npm run build:translate:signed
npm run build:web:strict:signed
npm run build:usb:signed
npm run build:usb:strict:signed
npm run build:arweave:signed
```

The signing step can also be applied to an already generated output:

```bash
npm run sign:build
npm run sign:build:arweave
```

#### Verifying a signed build

Download the public key, checksum manifest, and detached signature into the same directory:

```text
vojta-maur-openpgp.asc
SHA256SUMS.txt
SHA256SUMS.txt.asc
```

Import the public key:

```bash
gpg --import vojta-maur-openpgp.asc
```

Display its fingerprint:

```bash
gpg --fingerprint C5F5B3905220BE59
```

The primary-key fingerprint must match exactly:

```text
57E9 D455 FB10 A228 F66E 18AE C5F5 B390 5220 BE59
```

The fingerprint should also be compared with a copy obtained from an independent trusted source or archive. Downloading the public key, manifest, and signature from the same compromised mirror would not by itself establish the author's identity.

Verify the detached signature:

```bash
gpg --verify SHA256SUMS.txt.asc SHA256SUMS.txt
```

A valid result includes a message similar to:

```text
Good signature from "Vojta Maur vojtamaur.cz"
```

A trust label such as `[unknown]` concerns the local GnuPG trust assigned to the identity. It does not mean that the mathematical signature verification failed. The important results are a good signature and an independently confirmed primary-key fingerprint.

Verifying the OpenPGP signature authenticates `SHA256SUMS.txt`. To check the complete downloaded build against that authenticated manifest, run this command from the build root:

```bash
sha256sum -c SHA256SUMS.txt
```

The last command requires `sha256sum`. It is normally available on Linux, through GNU coreutils on macOS, and on Windows through environments such as Git Bash or WSL.

### 9.6.2 Reconstructable source package

During the web and USB builds, the project generates a reconstructable source package:

```text
/source/vojtamaur-web-source.zip
```

In the local build output this file is written to:

```text
dist/source/vojtamaur-web-source.zip
```

The package is produced by:

```text
scripts/generate-source-bundle.mjs
```

This source package is not a full copy of the generated website and it is not intended to duplicate all media files inside the ZIP. It is a compact reconstruction layer: it contains the project source, content, build scripts, `package.json`, `package-lock.json` when available, Astro configuration, selected small files from `public/`, and the files required to reconstruct omitted media.

Large or externally useful media assets from `public/images/` and `public/files/` are omitted from the ZIP and described in `MEDIA_MANIFEST.json` and `MEDIA_SHA256SUMS.txt`. The generated `download-assets.py` script can then restore them from the configured mirrors and verify them using SHA-256.

The `public/demos/` directory is bundled directly into the source ZIP rather than restored through the asset downloader. The demo files are small enough to include, and their HTML can differ between source, web output, USB output, and static mirrors after path rewriting or deployment-specific postprocessing. Treating them as downloadable checksum assets would create false hash mismatches.

The generated package contains the downloader and reconstruction notes both at the ZIP root and inside `source-bundle/`. This allows a reconstructed copy of the project to build the website and then generate a new source package again. In other words, the source package is recursively reconstructable: a source ZIP can produce a rebuilt website, and that rebuilt website can produce another source ZIP.

The default reconstruction flow is:

```bash
python download-assets.py
npm install
npm run build:web:strict
```

For a portable file-based reconstruction:

```bash
python download-assets.py
npm install
npm run build:usb
```

The downloader first checks local candidate paths when possible, then uses the configured public mirrors. Files are accepted only when their SHA-256 hash matches the manifest. Hash mismatches are reported and rejected by default rather than silently written into `public/`.

The source package is generated before the final integrity pass. Therefore `SHA256SUMS.txt` includes the source ZIP as part of the final published or portable build artifact.

### 9.6.3 Manual PDF export

The project includes a separate manual PDF export tool:

```text
scripts/export-site-pdf.py
```

The exporter is intentionally not run by any normal web, usb, or Arweave build. PDF generation is comparatively slow and can create large output files, so it is started only when an archival or reading copy is needed.

The script reads the finished static HTML from `dist/`, not the source `.mdx` files. This is necessary because English article pages are finalized by `scripts/en-postprocess.mjs` after the Astro build. The PDF export therefore reflects the same rendered Czech and English pages that exist in the finished build.

Before running the PDF exporter, create a standard web build (`npm run build:web` or another web-build variant), not a portable USB build. USB-rewritten paths in `dist/` may produce invalid hyperlinks in the exported PDF.

Before the first use, install the Python dependencies and the Playwright Chromium browser:

```bash
python -m pip install -r requirements-pdf-export.txt
python -m playwright install chromium
```

The standard command is:

```bash
npm run export:pdf
```

Equivalently, the script can be called directly:

```bash
python scripts/export-site-pdf.py
```

The default export:

- includes all non-draft articles from all article sections
- includes both Czech and English routes
- creates one combined A4 PDF
- adds a generated cover and article index
- adds PDF outline entries for the included pages
- writes `pdf-export-manifest.json` with article metadata, output file sizes, and SHA-256 hashes

The generated files are written to:

```text
exports/
```

This is the same generated output directory used by `scripts/filter-all-posts.py`, so manually created text and PDF exports are kept together outside `dist/`. The `exports/` directory is excluded through `.gitignore`. The exporter script and `requirements-pdf-export.txt` remain versioned source files.

Useful selection options include:

```bash
python scripts/export-site-pdf.py --lang cs
python scripts/export-site-pdf.py --lang en
python scripts/export-site-pdf.py --lang both
python scripts/export-site-pdf.py --section cestovani
python scripts/export-site-pdf.py --section volna-tvorba,vystavy
python scripts/export-site-pdf.py --separate
```

`--section` can be repeated or supplied as a comma-separated list. `--separate` creates one PDF for each article/language page instead of one combined file.

The script normally expects an existing `dist/` directory. A build can be requested explicitly for a particular run:

```bash
python scripts/export-site-pdf.py --build-command "npm run build:web:strict"
```

This does not change the normal project build scripts; it only runs the supplied command before that individual PDF export.

By default, the exporter keeps Chromium's original PDF output unchanged:

```text
--pdf-quality archive
```

This mode does not require Ghostscript. Optional PDF compression and image downsampling use Ghostscript as a second post-processing stage. A practical medium-quality export is:

```bash
python scripts/export-site-pdf.py --pdf-quality ebook --image-dpi 150 --jpeg-quality 75
```

The available quality presets are:

```text
archive
printer
ebook
screen
```

The `archive` preset preserves the original output. The other presets require Ghostscript and may be further adjusted with `--image-dpi` and `--jpeg-quality`. If Ghostscript is not detected automatically, its executable can be supplied explicitly:

```bash
python scripts/export-site-pdf.py --pdf-quality ebook --image-dpi 150 --jpeg-quality 75 --ghostscript "C:\Program Files\gs\gs10.07.1\bin\gswin64c.exe"
```

`--keep-uncompressed` preserves the original uncompressed PDF alongside the compressed result.

The exporter renders pages through a temporary local server, but hyperlinks written into the PDF are converted to their public equivalents under `https://vojtamaur.cz/`. This prevents relative article, image, PDF, download, and other media links from pointing to a temporary address such as `http://127.0.0.1:54321/`. Loopback preview aliases such as `localhost` and `127.0.0.1` on the export server port are treated as the same temporary server. Existing external links remain external. Article images and other directly rendered media that do not already have a link are linked to their public source file, using the original HTML source attributes where possible rather than browser-resolved local preview URLs. The generated cover index also links each built route to the corresponding public article page.

If the same exporter is used for another deployed domain, the public root can be overridden:

```bash
python scripts/export-site-pdf.py --site-url https://example.com
```

In the Ghostscript-compressed modes (`printer`, `ebook`, and `screen`), iframes are replaced before printing by PNG snapshots. Each snapshot links to the original public embed target. YouTube embed URLs are converted to normal YouTube watch URLs; local PDF, scan, map, and other iframe sources link to their corresponding public file or page. The `archive` mode keeps Chromium's original iframe rendering unchanged while still correcting normal page and media hyperlinks.

For English pages, the index title is read from the rendered HTML rather than copied from Czech MDX frontmatter. If the English postprocess marked a page as incomplete, the cover index and manifest label it as `Incomplete / Czech fallback`. An `/en/` route can therefore exist even when some or all of its article content remains Czech.

The PDF is a paginated visual snapshot, not a lossless replacement for the website or source repository. Wide tables, long source-code lines, and other horizontally overflowing content can be clipped, wrapped differently, or extend beyond the printable area. The generated cover includes this limitation explicitly. The archived HTML, source files, repository, `ALL_POSTS.txt`, and other preservation layers remain the authoritative complete versions.

### 9.6.4 Ultra-compact PDF export

The project also includes a second PDF exporter designed for high-density archival copies:

```text
scripts/export-site-pdf-ultra.py
```

Unlike `scripts/export-site-pdf.py`, this exporter does not reproduce the visual design of each web page. It reads the existing `dist/ALL_POSTS.txt`, reuses the compact serialization implemented by `scripts/filter-all-posts.py`, and lays the selected content out as a dense A4 document. The tool is intended for cases where page count and storage size matter, while images and links should still remain present and usable.

The ultra-compact export:

- uses four text columns and 4 pt body text by default
- starts every article title on a new line
- includes aggressively resized image thumbnails while preserving their natural aspect ratios
- keeps consecutive image groups in regular rows of up to three images
- uses incomplete image rows efficiently by allowing a complete following paragraph to occupy the remaining width, rather than leaving an isolated sentence fragment beside the images
- writes video, PDF, and interactive-media targets as compact clickable text URLs instead of large embed cards
- keeps links clickable without visually underlining them
- omits unrecoverable image placeholders from the PDF and reports their count on the command line
- omits duplicate media by default when the same media occurs in both language versions
- writes an adjacent JSON manifest containing the selection, layout settings, media statistics, output size, page count, link count, and SHA-256 hash

The exporter is manual and is not called by the normal web, USB, Arweave, Gemini, or integrity build workflows. It expects a current standard web build containing:

```text
dist/ALL_POSTS.txt
dist/images/
dist/files/
dist/demos/
```

Create or refresh that input before exporting, for example with:

```bash
npm run build:web:strict
```

The Python packages used by the exporter are Playwright, pypdf, and Pillow. Playwright also requires its Chromium browser:

```bash
python -m pip install playwright pypdf Pillow
python -m playwright install chromium
```

The recommended high-resolution Czech export is:

```bash
python scripts/export-site-pdf-ultra.py --lang cs --image-dpi 400
```

The default output is:

```text
exports/vojtamaur-web-export-ultra.pdf
```

The adjacent manifest is written as:

```text
exports/vojtamaur-web-export-ultra.manifest.json
```

Language and section selection work in the same style as the normal PDF exporter:

```bash
python scripts/export-site-pdf-ultra.py --lang cs
python scripts/export-site-pdf-ultra.py --lang en
python scripts/export-site-pdf-ultra.py --lang both
python scripts/export-site-pdf-ultra.py --section cestovani
python scripts/export-site-pdf-ultra.py --section volna-tvorba,vystavy
python scripts/export-site-pdf-ultra.py --lang cs --section cestovani
python scripts/export-site-pdf-ultra.py --separate
```

`--section` can be repeated or supplied as a comma-separated list. Date selection is available through `--from-date YYYY-MM-DD` and `--to-date YYYY-MM-DD`. `--separate` creates one compact PDF for each selected article and language under `exports/ultra-media-separate/` instead of creating one combined document.

Image quality can be adjusted independently from the physical thumbnail size:

```bash
python scripts/export-site-pdf-ultra.py --lang cs --image-dpi 300 --jpeg-quality 80
```

The default thumbnail raster resolution is 240 DPI and the default JPEG quality is 70. Higher values generally produce sharper images and a larger PDF. `--thumbnail-width-mm` and `--thumbnail-height-mm` control the maximum physical thumbnail box; images are fitted inside that box without changing their original proportions.

The page format, column count, text size, spacing, margins, and page numbers can also be adjusted. All available options and their current defaults can be displayed with:

```bash
python scripts/export-site-pdf-ultra.py --help
```

The script detects the project root from its own location, so it can be launched from the repository root or directly from the `scripts/` directory. `--project-root` remains available when the script is stored or invoked from another location.

This output is a compact preservation and reading layer, not a lossless replacement for the website, source repository, original media, or `ALL_POSTS.txt`. Text is deliberately very small, images are downsampled, interactive media is represented by links, and missing built assets cannot be embedded. The source files and normal build artifacts remain the authoritative complete versions.

### 9.6.5 Gemini capsule generation

The project generates a separate bilingual Gemini capsule with:

```bash
npm run generate:gemini
```

The generator is:

```text
scripts/generate-gemini-capsule.mjs
```

The output directory is a sibling of the normal web build:

```text
dist/
dist-gemini/
```

`generate:gemini` requires an existing finished `dist/` directory. It does not run Astro or the English translation postprocess by itself. For a complete build from source, use:

```bash
npm run build:gemini
```

This command runs the strict standard web build first and then generates the capsule.

The translating production workflow also generates the capsule automatically. Therefore:

```bash
npm run build:web:translate:signed
```

builds and translates the normal website, generates `dist-gemini/`, and then signs the checksum manifest belonging to `dist/`.

The Gemini capsule is deliberately generated from the finished HTML rather than directly from the MDX body. This preserves the actual postprocessed English output and the rendered behavior of shared Astro components. The generator also reads article frontmatter from `src/content/posts/` for stable slugs, section membership, dates, draft filtering, and ordering.

The default output structure is:

```text
dist-gemini/
  index.gmi
  favicon.txt
  keys/
  personal-work/
    index.gmi
    article-slug.gmi
  exhibitions/
    index.gmi
    article-slug.gmi
  travel/
    index.gmi
    article-slug.gmi
  cs/
    index.gmi
    volna-tvorba/
      index.gmi
      article-slug.gmi
    vystavy/
      index.gmi
      article-slug.gmi
    cestovani/
      index.gmi
      article-slug.gmi
```

English is intentionally the default language at the capsule root. Czech pages are stored below `/cs/`. Each article page includes navigation back to the homepage and section, a link to the counterpart language when available, and a link to the full HTTPS web version.

The Gemini homepage contains:

- the site title and motto
- a language switch
- a short description of the capsule
- a thank-you to Adële for hosting the capsule
- the latest nine items for Personal Work, Exhibitions, and Travel
- `SHOW ALL` / `ZOBRAZIT VŠE` after the previews where the HTML homepage exposes that action
- promotional video links followed by a direct link to the YouTube playlist
- the full About Me and Contact content directly on the homepage

The generator converts the built HTML into Gemtext using the following rules:

- headings become Gemtext headings, limited to the three heading levels supported by Gemtext
- paragraphs remain paragraphs
- explicit HTML `<br>` elements remain line breaks instead of being collapsed into spaces
- unordered and ordered lists become Gemtext list items
- blockquotes become `>` lines
- `<pre>` and code blocks become Gemtext preformatted blocks
- HTML tables become tab-separated preformatted blocks
- horizontal rules become plain separator lines
- images become active links to the canonical HTTPS asset URL
- image ALT text or captions are retained when useful, and the visible link label also includes the target URL so that the destination remains explicit
- PDF, video, map, Sketchfab, and interactive-demo embeds become external links
- local demo paths such as `/demos/example/example.html` become complete links under `https://vojtamaur.cz/`
- internal article and section links are rewritten to local Gemini routes when a corresponding generated page exists
- unknown internal routes remain HTTPS links to the main website instead of becoming broken Gemini paths
- `mailto:` and `tel:` references remain active Gemtext links without duplicated plain-text copies
- generic embeds without a useful title use their URL as the visible label rather than the meaningless text “Embedded content”

The output is written as UTF-8. One client-specific workaround is applied during final writing: a literal Unicode replacement character `U+FFFD` (`�`) is serialized as the visible text `\uFFFD`. The original MDX and HTML remain unchanged. This exists because the tested Lagrange renderer stops drawing the remainder of a line immediately after a literal `U+FFFD`, even though the character is valid UTF-8.

The generator copies `favicon.txt` and the public `keys/` directory from `dist/`, or falls back to `public/` when necessary. Binary article media are not copied into `dist-gemini/`; they remain available through stable HTTPS links to the main website.

Before generation, the existing output directory is removed and recreated. The script contains safety checks that refuse to delete the project root, the normal `dist/` directory, the filesystem root, or an output directory whose name does not contain `gemini`.

Available command-line overrides are:

```text
--dist <dir>
--output <dir>
--content <dir>
--site-url <url>
```

Because `dist-gemini/` is outside `dist/`, the existing web integrity and signing commands do not cover it. Even when the capsule is produced during `build:web:translate:signed`, the resulting detached signature authenticates only `dist/SHA256SUMS.txt`. A separate checksum or signing workflow for `dist-gemini/` has not been implemented.

### 9.7 Arweave / Permaweb build

The Arweave / Permaweb build is created with:

```bash
npm run build:arweave
```

This command first runs the strict standard web build, then runs the Arweave postprocess, and finally regenerates integrity files for the finished Arweave output:

```bash
node scripts/make-arweave-build.mjs
npm run generate:integrity:arweave
```

The script creates a separate output directory:

```text
dist-arweave/
```

This directory is intended for upload to ArDrive or another Arweave-compatible upload tool.

The Arweave build exists because a normal web build uses root-relative paths such as:

```text
/_astro/...
/images/...
/files/...
/volna-tvorba/
```

These paths work on a normal domain root such as `https://vojtamaur.cz/`, but they do not work automatically when the site is served below an Arweave manifest transaction URL.

The Arweave postprocess copies the finished `dist/` output into `dist-arweave/` and rewrites root-relative references into relative paths appropriate for the location of each HTML or CSS file.

After this copy-and-rewrite step, the build runs the integrity generator again for `dist-arweave/`. This is necessary because the Arweave postprocess can modify files after the normal web build has already created integrity files for `dist/`.

The integrity files inside `dist-arweave/` belong to the Arweave deployment output itself:

```text
dist-arweave/SHA256SUMS.txt
dist-arweave/BUILD_SHA256.txt
dist-arweave/integrity.json
```

The source of truth remains the normal project source. `dist-arweave/` is a generated deployment artifact and should not be edited manually.

---

## 10. Exact special build logic

The `build:usb` script is defined as a USB-targeted Astro build followed by postprocessing. In the current translation-aware workflow, the effective order is:

```text
set "BUILD_TARGET=usb" && astro build && node scripts/en-postprocess.mjs && node scripts/usb-rewrite.mjs && npm run generate:all-posts && npm run generate:integrity
```

This implies six steps:

1. `BUILD_TARGET=usb` is set
2. the Astro build is run in the mode defined in `astro.config.mjs`
3. `scripts/en-postprocess.mjs` applies the English translation layer using the available cache
4. `scripts/usb-rewrite.mjs` rewrites root-based URLs into relative file paths
5. `scripts/generate-all-posts.mjs` creates `dist/ALL_POSTS.txt` as a plain-text preservation export of the finished build
6. `scripts/generate-integrity.mjs` creates `dist/SHA256SUMS.txt`, `dist/BUILD_SHA256.txt`, and `dist/integrity.json` for the final static output

### 10.1 What `astro.config.mjs` does in USB mode

When `BUILD_TARGET=usb`, the following are used:

- `trailingSlash: "never"`
- `build.format: "file"`

This means that internal routes are generated as files of the form:

```text
slug.html
```

instead of the directory form:

```text
slug/index.html
```

### 10.2 What `scripts/usb-rewrite.mjs` does

The script:

1. goes through all `.html` files in the `dist` directory
2. reads their content
3. rewrites selected root-based paths to relative paths
4. saves the files again

Specific rewrites include:

- generated Astro assets in `/_astro/`
- public assets in `/images/`, `/files/`, `/demos/`, and `/source/`
- homepage links to `/`
- internal links in the form `/slug/`
- nested English paths such as `/en/slug/`

The script calculates the relative path from each HTML file back to the `dist/` root. This matters because a root-level file such as `dist/about.html` needs `./_astro/...`, while a nested file such as `dist/en/about.html` needs `../_astro/...`.

The purpose of this step is to adjust the HTML so that the output works even outside a standard web server with root-relative URLs.

### 10.3 Exact `build:arweave` logic

The `build:arweave` script is defined as a strict standard web build followed by Arweave-specific postprocessing and a second integrity pass for the final Arweave output. The effective order is:

```text
npm run build:web:strict && node scripts/make-arweave-build.mjs && npm run generate:integrity:arweave
```

This implies five main steps:

1. the standard Astro web build is created in `dist/`
2. `scripts/en-postprocess.mjs` verifies or applies the English translation layer according to the strict build workflow
3. `scripts/generate-all-posts.mjs` creates `dist/ALL_POSTS.txt` and `scripts/generate-integrity.mjs` creates integrity files for `dist/`
4. `scripts/make-arweave-build.mjs` copies `dist/` to `dist-arweave/` and rewrites root-relative paths
5. `scripts/generate-integrity.mjs dist-arweave` creates fresh integrity files for the final Arweave output

The second integrity pass matters. The checksum files created during `build:web:strict` describe `dist/`. After `scripts/make-arweave-build.mjs` rewrites files in `dist-arweave/`, those original checksums would no longer be sufficient for the Arweave output. Therefore `dist-arweave/` gets its own `SHA256SUMS.txt`, `BUILD_SHA256.txt`, and `integrity.json`.

The Arweave output keeps the directory-style route model of the normal web build, for example:

```text
fotogrammetrie/index.html
en/fotogrammetrie/index.html
```

This is different from the USB build, which uses a file-based output model.

The purpose of `dist-arweave/` is to create a static folder that can be uploaded to ArDrive and then exposed through an Arweave manifest. The manifest should be created at the level where `index.html`, `_astro/`, `images/`, `files/`, `ALL_POSTS.txt`, and `ARCHIVE.txt` are directly present.

### 10.4 Exact Gemini build logic

The standalone Gemini build is effectively:

```text
npm run build:web:strict && npm run generate:gemini
```

This creates or verifies the strict bilingual HTML build in `dist/` and then replaces `dist-gemini/` with the generated capsule.

For rapid iteration on only the converter, the existing HTML build can be reused:

```bash
npm run generate:gemini
```

This avoids rebuilding Astro or rerunning translation checks after every parser change.

The translating signed production command reaches the Gemini generator through the underlying translating web build. Its relevant order is:

```text
Astro build
-> English postprocess with missing translations enabled
-> ALL_POSTS and source-bundle generation
-> integrity generation for dist/
-> Gemini capsule generation in dist-gemini/
-> detached OpenPGP signing of dist/SHA256SUMS.txt
```

The final signing step remains scoped to the normal web build. `dist-gemini/` is generated in the same workflow but is a separate unsigned artifact.

---

## 11. English translation workflow

The project has an English version generated at build time. The Czech MDX files remain the source of truth. The English version is a derived static artifact produced from the rendered HTML output.

There are two separate translation layers:

- **Manual UI translation** – stable interface text is kept in `src/lib/i18n.ts`. This covers the header, section names, homepage text, and other short visible strings. The small Czech/English labels inside `OpenPgpContact.astro` are a deliberate local exception because the component shares one cryptographic payload between both homepage variants.
- **Automatic content translation** – article body regions are translated after `astro build` by `scripts/en-postprocess.mjs`. The translated fragments are cached in `translations/en/`.

This separation is intentional. UI text is small and highly visible, so it is translated manually. Article content is larger and less convenient to maintain twice, so it is translated automatically.

### 11.1 Translation configuration

The translation configuration is stored in:

```text
scripts/i18n-config.mjs
```

Important values:

- source language: `CS`
- target language: `EN-US`
- cache directory: `translations/en`
- translated regions: `[data-i18n="translate"]`
- maximum translated fragment size: `80_000` bytes

The translation postprocess protects code, embeds, scripts, styles, SVG, canvas, iframes, and anything marked as `notranslate` or `translate="no"`. Image `alt` text and `thumbnailAlt` metadata are not automatically translated. Captions are translated when they are normal HTML text inside a translated region.

### 11.1.1 Translation glossary and terminology preservation

Project-specific terminology used by the automatic translation layer is stored in:

```text
translations/glossary-cs-en.tsv
```

The TSV file is the canonical source of the glossary. Each non-empty line contains exactly one Czech source term, one tab character, and one English target term:

```tsv
Volná tvorba	Personal Work
```

The tab must be a real tab character, not a sequence of spaces. Duplicate Czech source terms, empty values, extra columns, and leading or trailing whitespace cause the translation postprocess to fail instead of silently accepting an ambiguous glossary.

The glossary serves two related purposes:

- **translation consistency** – project-specific names and concepts are translated according to the intended terminology rather than according to a generic literal interpretation
- **semantic and archival documentation** – the repository preserves how the author intended a Czech term to be understood in English

For example, `Volná tvorba` is mapped to `Personal Work`, not the superficially literal `Free Creation`. The TSV file therefore documents part of the conceptual structure of the project, not merely a temporary setting for an external translation service. Because it is stored and versioned with the source code, this intended terminology remains available in archived repository copies even if DeepL or the current build environment is no longer available.

The glossary integration is configured in `scripts/i18n-config.mjs`:

```js
glossary: {
  enabled: true,
  name: "vojtamaur.cz CS-EN",
  sourceFile: "translations/glossary-cs-en.tsv",
  stateFile: "translations/glossary-cs-en.state.json"
}
```

During a `build:*:translate` run, `scripts/en-postprocess.mjs`:

1. loads and validates the local TSV file
2. reuses the remote DeepL glossary ID recorded in `translations/glossary-cs-en.state.json` when possible
3. otherwise searches the DeepL account for a glossary with the configured name
4. creates the remote glossary if no matching glossary exists
5. updates the remote glossary when its entries differ from the local TSV source
6. stores the resulting remote ID and local revision in the state file

The remote glossary ID does not need to be set manually as an environment variable. Only `DEEPL_AUTH_KEY` is required. The state JSON file is operational metadata and a reusable pointer to the remote DeepL object; it is not the authoritative glossary content. If the state file is missing, it can be recreated from the TSV and the DeepL account. If the TSV is missing, the intended terminology is no longer reliably reconstructable from the project source.

Glossary-aware cache invalidation is selective. For each translated fragment, only glossary rows whose Czech source phrase occurs in that fragment contribute to its glossary revision. Changing one glossary entry therefore invalidates translations that use that term without forcing unrelated pages to be translated again.

Translation requests are paced and retried with exponential backoff to reduce DeepL rate-limit failures. If DeepL still returns HTTP `429`, translations completed before the failure remain cached and the same translate command can be run again.

### 11.2 DeepL API key

The DeepL key is not stored in the repository. It is provided through the `DEEPL_AUTH_KEY` environment variable.

In Windows CMD:

```bat
cd F:\vojtamaur-web
set "DEEPL_AUTH_KEY=YOUR_DEEPL_KEY"
npm run build:web:translate
```

One-line CMD version:

```bat
set "DEEPL_AUTH_KEY=YOUR_DEEPL_KEY" && npm run build:web:translate
```

In PowerShell:

```powershell
cd F:\vojtamaur-web
$env:DEEPL_AUTH_KEY = "YOUR_DEEPL_KEY"
npm run build:web:translate
```

The key must never be committed to the repository, embedded in client-side JavaScript, or uploaded as a public file.

### 11.3 Web translation commands

Recommended workflow after changing content:

```bat
cd F:\vojtamaur-web
set "DEEPL_AUTH_KEY=YOUR_DEEPL_KEY"
npm run build:web:translate
npm run build:web:strict
npm run preview
```

Meaning:

- `npm run build:web:translate` fills missing translation cache entries and writes translated EN HTML into `dist/`.
- `npm run build:web:strict` checks that no translated EN fragment is missing.
- `npm run preview` shows the finished build output.

For normal rebuilds with an already complete cache, `npm run build:web` can be enough. Before publishing, `npm run build:web:strict` is the safer check.

To force a full retranslation, use:

```bat
npm run build:web:refresh
```

This should be used carefully because it can change existing English output even if the Czech source text did not change. It is not a cache cleanup command.

To inspect unused English translation cache files without deleting them, use:

```bat
npm run build:web:prune:dry
```

To delete unused, unprotected English translation cache files, use:

```bat
npm run build:web:prune
```

The prune commands run the strict workflow, so they should only be used when the translation cache is already complete.

### 11.4 USB translation commands

For the portable build with missing translation generation:

```bat
cd F:\vojtamaur-web
set "DEEPL_AUTH_KEY=YOUR_DEEPL_KEY"
npm run build:usb:translate
```

For a strict USB check, use:

```bat
npm run build:usb:strict
```

To inspect unused English translation cache files during the USB workflow, use:

```bat
npm run build:usb:prune:dry
```

To delete unused, unprotected English translation cache files during the USB workflow, use:

```bat
npm run build:usb:prune
```

The USB wrapper scripts run `usb-rewrite.mjs` even if the translation postprocess fails. This prevents `dist/` from being left with broken relative CSS and image paths. A rewritten `dist/` is not proof of a successful translation build; the log must still be checked for `[i18n] Postprocess failed:`.

### 11.5 Translation cache

Translation cache files are stored in:

```text
translations/en/
```

Each cache entry contains the original source fragment, the translated fragment, and metadata about the translation configuration. The cache key is derived from the route, purpose, source fragment, language direction, DeepL options, selector policy revision, and any glossary rows that apply to the fragment.

Practical consequences:

- unchanged content is not sent to DeepL again
- changed Czech content gets a new cache key
- changing a glossary entry invalidates fragments that contain its Czech source term
- changing an unrelated glossary entry does not invalidate every translated page
- changing other translation policy can invalidate old cache entries
- old cache entries can remain in `translations/en/` after content changes unless the cache is pruned
- deleting `translations/en/` forces translation generation from scratch and should not be used as routine cleanup

The cache is part of the project source, not a public runtime dependency. The published site uses the finished HTML in `dist/`.

### 11.6 Translation cache pruning

Translation cache pruning is handled by `scripts/en-postprocess.mjs` when the `EN_PRUNE_CACHE` environment variable is enabled. The script records every cache hash that is actually used while processing the current English build output. After that, it can remove cache files that are no longer used by the current site.

Recommended web workflow:

```bat
npm run build:web:prune:dry
npm run build:web:prune
```

Recommended USB workflow:

```bat
npm run build:usb:prune:dry
npm run build:usb:prune
```

The dry-run command must be checked first. It prints files that would be deleted and reports a summary such as `cache-kept`, `cache-would-delete`, `cache-locked-kept`, and `cache-invalid-kept`. The non-dry command deletes only unused cache files that are not protected.

A cache file is kept during pruning when at least one of these is true:

- its hash is used by the current build
- the JSON file cannot be read safely
- the cache entry has `manual: true`
- the cache entry has `locked: true`
- the cache entry has `edited: true`

New automatically generated cache entries are marked as not manually edited:

```json
"manual": false,
"locked": false,
"edited": false,
"editedAt": null,
"editedBy": null
```

When a translation is manually corrected, the cache entry can be marked like this:

```json
"edited": true,
"editedAt": "2026-07-06",
"editedBy": "Vojta"
```

This protects the entry from future pruning even if the current source text changes and the hash is no longer used. `manual: true` and `locked: true` are also respected as stronger preservation markers.

Pruning is meant to keep `translations/en/` searchable and maintainable. It should not change the published HTML except through the normal strict build process. If the goal is only to remove unused cache files, do not use `build:web:refresh`; refresh can retranslate existing content and change manually checked output.

### 11.7 Development server versus final build

`npm run dev` is useful for editing layout and content. The final publishing artifact is still the build output in `dist/`.

In some cases, `npm run dev` may correctly display manually translated UI elements and metadata, while article bodies remain untranslated in English routes. This usually means the DeepL postprocess has not been applied to the current output yet.

To verify the real final translated output, use:

    npm run build:web:translate
    npm run preview
	
### 11.8 Marking content as not translatable

Use `NoTranslate.astro` for MDX content that must remain unchanged.

Import it from an MDX file in `src/content/posts/` like this:

```mdx
import NoTranslate from "../../components/NoTranslate.astro";
```

Inline use:

```mdx
The term <NoTranslate>anti-language</NoTranslate> should stay unchanged.
```

Block use:

```mdx
<NoTranslate as="div">
This text should not be sent to DeepL.
It will remain exactly as written.
</NoTranslate>
```

Use `as="div"` for block content. The default element is `span`, which is better suited for inline text.

Typical uses:

- already-English quotations
- code-like output that is not fenced as code
- generated text that should remain in its original form
- names, labels, or conceptual phrases that should not be normalized by DeepL
- very large fragments that would otherwise exceed the translation size limit

### 11.9 `notranslate` inside `MediaRow`

`MediaRow.astro` is a special case because `type: "text"` items are rendered through `set:html`. That means the `content` value is an HTML string, not an Astro component.

This does not work:

```mdx
<MediaRow
  items={[
    {
      type: "text",
      content: "<NoTranslate>This will not run as an Astro component.</NoTranslate>"
    }
  ]}
/>
```

Use a normal HTML marker instead:

```mdx
<MediaRow
  bordered
  items={[
    {
      type: "text",
      content: "28. prosince 2014 jsem v Mombase vyfotil starou popsanou zeď."
    },
    {
      type: "text",
      content: `
<div class="notranslate" translate="no">
It doesn't matter which religion you claim you are.
It doesn't matter which country you're coming from.
It doesn't matter whether you're poor or rich.
It doesn't matter if you're black or white.
We are all the same in the eyes of God.
</div>
      `.trim()
    },
    {
      type: "text",
      content: "Nezáleží, jakého jsi vyznání. Nezáleží, z jaké země pocházíš."
    }
  ]}
/>
```

The important part is:

```html
<div class="notranslate" translate="no">
  ...
</div>
```

The postprocess recognizes this marker, removes the protected block before sending the fragment to DeepL, and restores it afterward.

### 11.10 Large fragments

The current maximum translated fragment size is `80_000` bytes. If a translated region is larger, the build fails with an error similar to:

```text
EN fragment for /en/example/ is 166405 bytes, above configured 80000.
```

Preferred solutions:

- mark non-translatable output with `NoTranslate`
- use `<div class="notranslate" translate="no">...</div>` inside `MediaRow` text items
- split unusually large pages into smaller translated regions if needed

The size guardrail exists to avoid sending oversized and fragile HTML blobs to DeepL.

### 11.11 Troubleshooting

#### `DEEPL_AUTH_KEY is not set`

The build tried to create a new translation but no DeepL key was available. Set the key and run the translate command again:

```bat
set "DEEPL_AUTH_KEY=YOUR_DEEPL_KEY"
npm run build:web:translate
```

#### `Missing EN translation cache`

Strict mode found an EN page that needs a translation cache entry that does not exist yet. Run:

```bat
npm run build:web:translate
```

or, for USB:

```bat
npm run build:usb:translate
```

Then run the strict build again.

#### `Remote DeepL glossary created from local TSV`

This is an informational message, not an error. The TSV file in the repository is the local source of glossary entries, while DeepL requires a separate remote glossary object in the account. The message means that no reusable remote object was found through the saved state ID or configured glossary name, so the script created one and stored its ID in:

```text
translations/glossary-cs-en.state.json
```

If this message appears on every translate build, check whether the state file is being deleted, whether a different DeepL account or API key is being used, or whether the remote glossary was removed.

#### `DeepL HTTP 429`

DeepL is rate-limiting translation requests. The script spaces requests apart, retries temporary failures with exponential backoff, and respects the `Retry-After` response header when provided. If all retries still fail, run the same translate command again:

```bat
npm run build:web:translate
```

Translations completed before the failure are already stored in `translations/en/`. Do not delete the translation cache or glossary state file merely because a `429` occurred; that would discard useful progress and create more requests.

#### Cache pruning would delete a manually edited translation

First run only the dry-run command:

```bat
npm run build:web:prune:dry
```

If an unused cache entry still needs to be preserved, open the corresponding JSON file and add `edited: true`, `manual: true`, or `locked: true`. Then run the dry-run command again and check that the file is counted as `cache-locked-kept` instead of `cache-would-delete`.

#### Header and UI are English, but the article body is Czech

The manual UI dictionary is working, but the automatic content translation did not run or did not have a cache entry. Check the build log and run `build:web:translate`.

#### Local `dist/` is Czech after `build:web:translate`

Check the end of the build log. If it contains `DEEPL_AUTH_KEY is not set` or another `Postprocess failed` message, the Astro build completed but the translation postprocess failed.

#### Production is Czech, but local `dist/` is English

The problem is upload or caching, not translation. Upload the entire `dist/` directory again and force overwrite existing files. Avoid “skip if same size” and similar FTP shortcuts. Then test with a cache-busting URL parameter.

### 11.12 Publishing checklist

Before uploading the web build to FTP:

1. Run `npm run build:web:translate`.
2. Run `npm run build:web:strict`.
3. If translation cache cleanup is needed, run `npm run build:web:prune:dry` and check the summary.
4. If the dry-run looks correct, run `npm run build:web:prune`.
5. Run `npm run preview`.
6. Check that `dist/SHA256SUMS.txt`, `dist/BUILD_SHA256.txt`, and `dist/integrity.json` exist.
7. Open at least one EN article locally.
8. Check that the article body is actually English, not only the header and metadata.
9. Upload the complete `dist/` directory, including the integrity files.
10. Overwrite existing files on the server.

For USB/offline output:

1. Run `npm run build:usb:translate`.
2. Run `npm run build:usb:strict`.
3. If translation cache cleanup is needed, run `npm run build:usb:prune:dry` and check the summary.
4. If the dry-run looks correct, run `npm run build:usb:prune`.
5. Check that `SHA256SUMS.txt`, `BUILD_SHA256.txt`, and `integrity.json` are present in the generated output.
6. Open the generated HTML from disk.
7. Check CSS, images, internal links, and EN article content.

For Arweave / Permaweb output:

1. Run the metadata audit:
   `python scripts/audit-public-metadata.py --exiftool "D:\Program Files\exiftool\exiftool.exe"`
2. Resolve unintended metadata findings before publishing.
3. Run `npm run build:arweave`.
4. Verify that the log ends with `Arweave build prepared in dist-arweave`.
5. Verify that `dist-arweave/SHA256SUMS.txt`, `dist-arweave/BUILD_SHA256.txt`, and `dist-arweave/integrity.json` exist.
6. Test the build locally under a fake manifest-like subdirectory.
7. Upload only `dist-arweave/` to ArDrive.
8. Create the manifest at the level where `index.html` is directly present.
9. Test the manifest gateway URL.
10. Check several deep routes, English routes, `ALL_POSTS.txt`, `ARCHIVE.txt`, and the integrity files.

---

## 12. Deploy

### 12.1 `dist` structure
The `dist/` directory contains the finished build intended for publishing.

Important generated preservation and verification files include:

```text
dist/ALL_POSTS.txt
dist/SHA256SUMS.txt
dist/BUILD_SHA256.txt
dist/integrity.json
dist/SIGNING_STATUS.txt
dist/SHA256SUMS.txt.asc       # only after an explicit signed build
dist/keys/vojta-maur-openpgp.asc
dist/keys/vojta-maur-openpgp-fingerprint.txt
dist/source/vojtamaur-web-source.zip
```

The `source/` directory in `dist/` is a generated output directory. It should not be treated as source input or committed as a hand-maintained project directory.

The Gemini capsule is not part of this tree. It is generated separately as the sibling directory `dist-gemini/`, and it is not included in the checksum manifest or detached signature stored in `dist/`.

### 12.2 Deployment of the standard web build
For the production website, the content corresponding to the standard web build is uploaded to the hosting server.

#### 12.2.1 Neocities mirror limitation

The current Neocities mirror is subject to Neocities file-type restrictions, which reject `.zip` uploads. The following generated file is therefore not present on that mirror:

```text
dist/source/vojtamaur-web-source.zip
```

Because the source-package link in the published content is root-relative, the link resolves on the Neocities mirror to `/source/vojtamaur-web-source.zip`. That URL is a known dead link on the Neocities deployment.

The source package remains part of the complete local `dist/` output and other deployments that accept ZIP files. The Neocities deployment must therefore be treated as a partial hosting mirror rather than a byte-for-byte copy of `dist/`. Integrity files generated from the complete build may still list the source ZIP even though Neocities rejected it; this is a known deployment-specific omission, not evidence that the local build is corrupted.

#### 12.2.2 Codeberg Pages deployment

The Codeberg repository is named `pages` and is available at:

```text
https://codeberg.org/vojta_maur/pages
```

It contains two different branches with separate purposes:

- `main` – the normal project source, pushed together with the GitHub and GitLab copies
- `pages` – the generated contents of `dist/`, used only by Codeberg Pages

The published website is available at:

```text
https://vojta_maur.codeberg.page/
```

The repository name is intentionally `pages`. The normal web build contains root-relative URLs such as `/_astro/...`, `/images/...`, and `/slug/`. If the same build were served from a repository subpath such as `/vojtamaur-web/`, CSS, images, the web manifest, and internal links would resolve against the domain root and return `404`. Using the special `pages` repository exposes the build at the user-domain root and preserves the same URL model as the production website and the other root-hosted mirrors.

Codeberg Pages is triggered by a repository webhook with the following relevant settings:

```text
Target URL: https://vojta_maur.codeberg.page/
Event: push
Branch filter: pages
Content type: application/json
```

The local setup uses two Git worktrees so that the source branch and generated deployment branch can remain checked out at the same time:

```text
G:\vojtamaur-web    -> main
G:\vojtamaur-pages  -> pages
```

The deployment helper is:

```text
deploy-codeberg-pages.bat
```

It is intended to be stored in the project root. The script:

1. verifies the expected directories, branches, and the `codeberg` remote
2. refuses to deploy while the `main` worktree contains uncommitted changes
3. runs `npm run build` in `G:\vojtamaur-web`
4. resets and cleans only the `pages` worktree
5. copies the finished `dist/` contents into `G:\vojtamaur-pages`
6. creates a deployment commit only when the generated output changed
7. pushes only the `pages` branch to Codeberg

The script does not commit, modify, or push `main`. The normal source publishing workflow therefore remains separate:

```bat
git status
git add .
git commit -m "Commit message"

git push origin main
git push gitlab main
git push codeberg main

deploy-codeberg-pages.bat
```

An optional deployment commit message can be passed as an argument:

```bat
deploy-codeberg-pages.bat "Deploy updated website"
```

On a fresh workstation, the `pages` worktree can be recreated after configuring the `codeberg` remote and fetching the remote deployment branch:

```bat
git remote add codeberg https://codeberg.org/vojta_maur/pages.git
git fetch codeberg pages
git worktree add -b pages G:\vojtamaur-pages codeberg/pages
```

If a local `pages` branch already exists, omit `-b pages` and attach the worktree to that existing branch instead. The deployment branch is generated output and should not be merged into `main`.

#### 12.2.3 OpenPGP signing policy

Only build artifacts explicitly produced and signed in the author's controlled local environment are OpenPGP-signed. The private signing key is kept on the author's local storage and is never committed to the repository, copied into the build, or uploaded as a CI secret.

Automated or provider-side deployments that do not run the explicit local signing step are intentionally unsigned. They may still publish the public key and fingerprint because those are public identity material, but their presence does not authenticate that particular build.

A valid signature belongs only to the exact `SHA256SUMS.txt` that was signed. A provider-side rebuild is a different artifact even when it was produced from the same source revision. Generated timestamps and deployment-specific rewriting can also make otherwise equivalent builds byte-wise different.

A mirror that receives an already completed locally signed build without rebuilding or modifying it can preserve the same signature. The absence of `SHA256SUMS.txt.asc` on an automated deployment therefore does not indicate failed checksum generation; it indicates that the explicit private-key operation was not performed.

### 12.3 `.htaccess`
For a static Astro website, a minimalist configuration is appropriate. A typical WordPress rewrite rule to `index.php` is not relevant for this type of project.

### 12.4 Portable build
The portable build can be used as a file-based snapshot or offline copy. However, it is not identical to normal web hosting, and some external services may behave differently.

In the portable build, root-relative paths must be rewritten so that the site works from `file://` URLs. This includes the source package link. A correct portable build must link to the source package relatively, for example:

```text
./source/vojtamaur-web-source.zip
```

rather than:

```text
/source/vojtamaur-web-source.zip
```

The latter would point to the root of the local drive, such as `C:\source\vojtamaur-web-source.zip` on Windows.

### 12.5 Arweave / Permaweb deployment
For Arweave deployment, do not upload the normal `dist/` directory directly. Use the generated Arweave-specific output:

```text
dist-arweave/
```

Recommended workflow:

1. run the public asset metadata audit
2. run `npm run build:arweave`
3. verify that `dist-arweave/SHA256SUMS.txt`, `dist-arweave/BUILD_SHA256.txt`, and `dist-arweave/integrity.json` exist
4. optionally test `dist-arweave/` locally under a fake manifest-like subdirectory
5. upload only `dist-arweave/` to a public ArDrive drive
6. create an Arweave manifest at the level where `index.html` is directly present
7. copy the manifest Data TX ID
8. test the site through an Arweave or ArDrive gateway URL

The manifest must be created inside the uploaded build root, not above it. The correct manifest target level contains:

```text
index.html
_astro/
images/
files/
ALL_POSTS.txt
ARCHIVE.txt
SHA256SUMS.txt
BUILD_SHA256.txt
integrity.json
```

The uploaded Arweave version is immutable. If a mistake is uploaded, the correction must be published as a new upload and a new manifest transaction. The old version remains available.

### 12.6 Gemini capsule deployment

For Gemini deployment, use the generated directory:

```text
dist-gemini/
```

The Gemini server document root should correspond to the contents of this directory so that `index.gmi` is the English capsule homepage. The Czech homepage remains available at:

```text
/cs/index.gmi
```

A typical update consists of:

1. generating a fresh capsule with `npm run build:gemini`, or generating it through the translating production workflow
2. previewing representative `.gmi` files locally in a Gemini client
3. uploading the contents of `dist-gemini/` through the hosting account's supported transfer method, such as SFTP or FTPS
4. verifying the homepage, language switch, section indexes, several article pages, the YouTube playlist link, direct media links, demo links, and the OpenPGP block

The capsule itself contains text and links. Images, PDFs, videos, maps, 3D models, and interactive HTML demonstrations remain on the normal HTTPS website and are opened through absolute URLs. The Gemini deployment therefore depends on the continued availability of `https://vojtamaur.cz/` for non-text media, while article text and capsule navigation remain native Gemtext.

The capsule can be updated independently by rerunning `npm run generate:gemini` against an existing current `dist/`. However, publishing from stale HTML would also publish stale translated content, so the complete build command should be preferred for normal releases.

---

## 13. Known issues and solutions

### 13.1 YouTube embed in local or file-based mode
A YouTube iframe may fail in local or file-based mode with error 153. In that case, it is recommended to account for a fallback opening of the video via an external link.

### 13.2 Sketchfab warnings in the console
The Sketchfab iframe may generate console warnings such as:

- permissions policy violation
- deviceorientation blocked
- accelerometer is not allowed

If the viewer works, this is not a project error, but a limitation or behavior of a third party.

### 13.3 Broken CSS or assets with the wrong base model
If the build uses root-relative paths in an environment where no standard server root is available, styles, images, and internal links may break.

This affects more than one special output model:

- USB / file-based output
- Arweave / Permaweb output served below a manifest transaction path

For this reason, the portable file-based build is supplemented with `scripts/usb-rewrite.mjs`, and the Arweave build is supplemented with `scripts/make-arweave-build.mjs`.

If CSS, JavaScript, images, source package links, or internal links fail in the Arweave deployment, check whether any root-relative path such as `/_astro/...`, `/images/...`, `/files/...`, `/source/...`, or `/slug/` remained in the generated `dist-arweave/` output.

### 13.4 Dev server and new articles
If the listing or routes do not match after adding a new `.mdx` file, the recommended first step is to restart the development server.

### 13.5 Lagrange and the Unicode replacement character

A tested Lagrange client stops rendering the remainder of a normal text line immediately after a literal Unicode replacement character `U+FFFD` (`�`). The source `.gmi` remains valid UTF-8, and the same character is allowed by Unicode; this is treated as a client rendering defect rather than malformed Gemtext.

The Gemini generator works around the issue only in the generated capsule by converting each literal `U+FFFD` to the visible ASCII sequence:

```text
\uFFFD
```

This preserves the information that the replacement character occurred while preventing the client from truncating the rest of the line. The original MDX, rendered HTML, and other Unicode characters are not modified.

### 13.6 Lagrange indentation after intentional line breaks

When several normal Gemtext lines originate from intentional `<br>` line breaks inside one HTML paragraph (the equivalent of Shift+Enter), Lagrange may render the later lines with paragraph-like indentation. Kristall renders the same `.gmi` content without this indentation. This is a client-specific rendering difference, not an error in the Gemini generator.

Where the source text genuinely contains separate paragraphs, replacing `<br>` with separate MDX paragraphs improves both HTML and Gemini output. Intentional hard line breaks should remain unchanged if converting them to paragraphs would alter the intended HTML structure.

---

## 14. Future extensions

Possible future directions for the project:

- extending the documentation with content workflow instructions for another editor
- translation of image alt texts
- automatic translation of PDF exports, with automatic compression of the translated PDFs to reduce file size
- a wiki with interconnected articles (e.g. `wiki.vojtamaur.cz`)

---

## 15. Archival operations

### 15.1 Preservation priority under constraints

When storage capacity or snapshot quotas make it impossible to preserve the complete site, archive these artifacts in the following order:

1. `ALL_POSTS.txt` – the smallest practical content-preservation layer
2. `ARCHIVE.txt` – the map of repositories, mirrors, snapshots, and archive entry points
3. this technical documentation (`/documentation/` or its source file, `src/pages/documentation.mdx`)

This priority is especially useful for storage-constrained deposits such as Memory of Mankind and for services such as Perma.cc where the number of available snapshots may be limited.

### 15.2 Manual mirror updates

The ChatGPT Sites deployment does not update automatically when the canonical repository changes. To refresh it, ask ChatGPT in the Sites chat to load the current state of the canonical repository, save or build a new version, and deploy that version explicitly.

Codeberg Pages is updated separately through `deploy-codeberg-pages.bat`; the complete worktree-based procedure is documented in section 12.2.2.

### 15.3 External web archives

As an operating assumption, newly published article URLs and mirror URLs should be submitted to the Internet Archive. Seeding the archive with these URLs is expected to improve discovery and may lead to later automatic recrawling, but it is not a guarantee that every URL or later version will be captured.

The Czech Webarchiv likely captured `https://vojtamaur.cz/` after a manual preservation request. Searching for the root URL currently returns the following access notice:

> Tuto stránku Webarchiv nemůže zobrazit. Z důvodu autorského zákona nemůžeme tuto stránku zpřístupnit online. Archivované verze této stránky jsou dostupné pouze z Referenčního centra NK ČR.

This indicates restricted archived holdings rather than public online access; it does not by itself establish the completeness or capture dates of those holdings.

### 15.4 YouTube video archiving

YouTube videos are downloaded from Windows Command Prompt with `yt-dlp`. Put the video URLs in `urls.txt`, one URL per line, then run:

```bat
yt-dlp --write-info-json --write-thumbnail --write-description --write-subs --write-auto-subs --sub-langs "cs,en" --embed-metadata --download-archive downloaded.txt -f "bv*+ba/b" -o "%(title).200B [%(id)s]/%(title).200B [%(id)s].%(ext)s" -a urls.txt
```

The `downloaded.txt` archive prevents already recorded videos from being downloaded again.

---

## 16. Summary

The project is designed as a file-oriented static website. Content is versioned directly in the repository, and the final published form is produced by the build process. This model makes it easier to archive, restore, and migrate the project without relying on a database runtime.

From a maintenance perspective, the following points are especially important:

- content is managed as `.mdx` files
- metadata is validated through content collections
- specialized content is encapsulated in several reusable components
- the project supports a standard web build, a portable file-based build, a derived Arweave / Permaweb deployment build, and a generated bilingual Gemini capsule in `dist-gemini/`
- the Gemini capsule uses English at the root, Czech under `/cs/`, native Gemtext navigation, and HTTPS links for binary media and interactive content
- `build:web:translate:signed` generates the Gemini capsule but signs only the normal `dist/` checksum manifest; `dist-gemini/` is currently a separate unsigned artifact
- Codeberg Pages is deployed separately from the generated `pages` branch through the `deploy-codeberg-pages.bat` worktree workflow
- web and USB builds generate a recursively reconstructable source package at `/source/vojtamaur-web-source.zip`
- when working with new content, it is useful to expect the occasional need to restart the development server
- public assets in `public/images/` and `public/files/` should be checked for unintended embedded metadata before commit
- unused English translation cache entries can be pruned with a dry-run-first workflow, while `manual`, `locked`, and `edited` entries are preserved

This documentation describes the current architecture and operating model of the project in a form suitable for ongoing maintenance, handoff, or future migration.
