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:

The architecture is based on the following components:

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:

Provided structure:

public/
  demos/
  files/
  images/

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

2.2 Meaning of the main directories

public/

Contains static files that are simply copied into the output during the build:

src/content/

Project content files. In the current configuration, the following are mainly used:

src/components/

Reusable components for working with content and listings:

src/layouts/

Layouts for individual page types:

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.


3. Key configuration files

astro.config.mjs

The project uses two 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 different output types:

package.json

Basic project workflow:

npm run dev
npm run build:web
npm run build:web:translate
npm run build:web:strict
npm run preview
npm run build:usb
npm run build:usb:translate

Meaning of the main scripts:

content.config.ts

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


4. Content Collections

4.1 posts collection

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

Required metadata:

Optional metadata:

Section-specific metadata:

For section: "vystavy"

For section: "cestovani"

4.2 videos collection

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


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:

5.3 Conditional rendering by section

Different sections have different meta blocks:

5.4 Date formatting

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

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:

src/content/posts/

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

6.2 Required and optional metadata

Shared required metadata

Shared optional metadata

Metadata for Výstavy

Metadata for Cestování

6.3 Thumbnails

Each article uses:

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:

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

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:

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

Default rule:

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:

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

---
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

---
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í

---
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:

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:

Supported width variants:

Supported alignment options:

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:

Use cases:

The component also supports a bordered variant.

7.3 Embed.astro

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

Supported parameters:

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:

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:


9. Development and normal workflow

9.1 Local development

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:

Ctrl + C
npm run dev

9.2.1 Public asset metadata audit

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

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:

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

To strip unintended metadata from supported image files:

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

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.

To create missing English translations, use:

npm run build:web:translate

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

npm run build:web:strict

9.4 Build preview

npm run preview

Used for local verification of the production build.

9.5 Portable file-based build

npm run build:usb

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

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

npm run build:usb:translate

For a strict USB check, use npm run build:usb:strict if the alias exists in package.json. If it is not registered, run the wrapper directly:

node scripts/build-usb-strict.mjs

9.6 Plain-text export of all posts

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

/ALL_POSTS.txt

The export is produced by:

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:

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

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

[MEDIA: image]
[VIDEO EMBED]
[INTERACTIVE EMBED]
[PDF EMBED]

Very large code or generated output blocks are omitted when they exceed the configured size limits. This prevents one unusually large generated block from making the entire text export difficult to read or process. Omitted blocks are replaced with an explicit placeholder explaining that the full version is available in the rendered website or source repository.

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


10. Exact build:usb 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:

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

This implies five 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

10.1 What astro.config.mjs does in this mode

When BUILD_TARGET=usb, the following are used:

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

slug.html

instead of the directory form:

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:

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.


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:

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:

scripts/i18n-config.mjs

Important values:

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.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:

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

One-line CMD version:

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

In 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:

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:

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:

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.

11.4 USB translation commands

For the portable build with missing translation generation:

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

For a strict USB check, use:

npm run build:usb:strict

If the build:usb:strict alias is not present in package.json, run the wrapper directly:

node scripts/build-usb-strict.mjs

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:

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, and selector policy revision.

Practical consequences:

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

11.6 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.7 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:

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

Inline use:

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

Block use:

<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:

11.8 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:

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

Use a normal HTML marker instead:

<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:

<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.9 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:

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

Preferred solutions:

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

11.10 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:

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:

npm run build:web:translate

or, for USB:

npm run build:usb:translate

Then run the strict build again.

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.11 Publishing checklist

Before uploading the web build to FTP:

  1. Run npm run build:web:translate.
  2. Run npm run build:web:strict.
  3. Run npm run preview.
  4. Open at least one EN article locally.
  5. Check that the article body is actually English, not only the header and metadata.
  6. Upload the complete dist/ directory.
  7. Overwrite existing files on the server.

For USB/offline output:

  1. Run npm run build:usb:translate.
  2. Run npm run build:usb:strict or node scripts/build-usb-strict.mjs.
  3. Open the generated HTML from disk.
  4. Check CSS, images, internal links, and EN article content.

12. Deploy

11.1 dist structure

The dist/ directory contains the finished build intended for publishing.

11.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.

11.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.

11.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.


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:

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. For this reason, the portable file-based build is supplemented with the usb-rewrite.mjs postprocessing step.

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.


14. Future extensions

Possible future directions for the project:


15. 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:

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