vojtamaur-web projectvojtamaur-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:
public/ for images, PDFs, demos, and other filesThis model makes it easier to version content, archive build outputs, and potentially migrate the project to another environment without relying on a database runtime.
The current project structure is divided into two main parts:
src/ – project source filespublic/ – static assets copied unchanged during the buildProvided structure:
public/
demos/
files/
images/
src/
components/
content/
content.config.ts
env.d.ts
layouts/
lib/
pages/
styles/
public/Contains static files that are simply copied into the output during the build:
public/images/ – article images, thumbnails, and other visual contentpublic/files/ – PDFs and other downloadable or embeddable filespublic/demos/ – standalone HTML/JS demos and legacy static pagessrc/content/Project content files. In the current configuration, the following are mainly used:
src/content/posts/ – articlessrc/content/videos/ – metadata for promotional videossrc/components/Reusable components for working with content and listings:
ImageFigure.astroMediaRow.astroEmbed.astroPostTileGrid.astroHeader.astrosrc/layouts/Layouts for individual page types:
BaseLayout.astroPostLayout.astroTravelLayout.astro and ExhibitionLayout.astrosrc/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.
astro.config.mjsThe 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:
A third deployment artifact is derived from the standard web build:
dist-arweave/ by copying the finished dist/ output and rewriting root-relative paths so that the site can work under an Arweave manifest transaction pathThe Arweave build is not a separate Astro BUILD_TARGET. It is a postprocessed version of the normal web build.
package.jsonBasic project workflow:
npm run dev
npm run build:web
npm run build:web:translate
npm run build:web:strict
npm run build:web:prune:dry
npm run build:web:prune
npm run preview
npm run build:usb
npm run build:usb:translate
npm run build:usb:strict
npm run build:usb:prune:dry
npm run build:usb:prune
npm run build:arweave
npm run generate:all-posts
npm run generate:source-bundle
npm run generate:integrity
npm run generate:integrity:arweave
npm run export:pdf
Meaning of the main scripts:
npm run dev – starts the Astro development servernpm 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 build integrity files for dist/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, and writes build integrity files for dist/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 build integrity files for dist/npm run build:web:prune:dry – runs the strict web build and prints unused English translation cache files that would be deleted, without deleting themnpm run build:web:prune – runs the strict web build and deletes unused, unprotected English translation cache filesnpm run preview – local preview of the buildnpm 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 build integrity files for dist/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 build integrity files for dist/npm run build:usb:strict – creates a strict portable file-based build and fails if an English translation cache entry is missingnpm run build:usb:prune:dry – runs the strict portable build and prints unused English translation cache files that would be deleted, without deleting themnpm run build:usb:prune – runs the strict portable build and deletes unused, unprotected English translation cache filesnpm 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 build integrity files for dist-arweave/npm run generate:all-posts – generates dist/ALL_POSTS.txt, a plain-text export of all article content from the finished build outputnpm run generate:source-bundle – generates dist/source/vojtamaur-web-source.zip, a small reconstructable source package with a media manifest and an asset downloadernpm run generate:integrity – generates SHA256SUMS.txt, BUILD_SHA256.txt, and integrity.json for the selected build output directorynpm run generate:integrity:arweave – generates fresh integrity files for dist-arweave/ after the Arweave copy-and-rewrite stepnpm 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 pipelinecontent.config.tsDefines content collections and metadata schemas using Zod validation. The project uses at least the following collections:
postsvideosposts collectionArticles are stored as .mdx files and validated against the schema in content.config.ts.
Required metadata:
titleslugsectiondatethumbnailthumbnailAltOptional metadata:
excerptdraftSection-specific metadata:
section: "vystavy"dateFromdateTocityvenueexhibitionsection: "cestovani"yearmediavideos collectionUsed for the Propagační videa section. Contains metadata for external YouTube videos. Typically:
titleurlthumbnailthumbnailAltdraftPostLayout.astroPostLayout.astro wraps article content in the main layout and creates a shared wrapper for article pages.
[slug].astroThe [slug].astro file is the central route for content from the posts collection. It handles:
getCollectionslugrender(post)Different sections have different meta blocks:
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.
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.
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.
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.
titleslugsectiondatethumbnailthumbnailAltexcerptdraftdateFromdateTocityvenueexhibitionyearmediaEach article uses:
thumbnail – path to the thumbnailthumbnailAlt – thumbnail alt textThumbnails are used in article listings, on the homepage, and in individual sections.
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:
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:
For normal publishing, the recommended workflow is:
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.
draftThe 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.
Besides standard Markdown, article content can also use the following components:
ImageFigureMediaRowEmbedThese components must be explicitly imported in the MDX file.
EntryPoints.astroThe 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.
---
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
---
---
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
---
---
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
---
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.
ImageFigure.astroA component for standalone images with support for the following parameters:
srcaltcaptionwidthalignborderedlinkSupported width variants:
fullhalfone-thirdSupported alignment options:
leftcenterrightDepending on the configuration, the component can also open the image when clicked.
MediaRow.astroA component for arranging multiple items in a row. Supports the following types:
imagepdftextemptyUse cases:
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:
<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:
<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.
Embed.astroA generic wrapper for embedded iframe content. It is used, for example, for:
Supported parameters:
srcratiokindwidthalignFor YouTube embeds, prefer the privacy-enhanced youtube-nocookie.com domain instead of the standard youtube.com embed URL:
<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.
MediaRowWhen the grid collapses, a PDF iframe may require special adjustment of height or aspect ratio. This was handled using CSS for .media-row__pdf.
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.
The homepage combines content from multiple parts of the website.
Each main section on the homepage displays the latest 9 items:
If a given section contains more items than the number displayed on the homepage, a “show all” tile is added.
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.
Section headings on the homepage are clickable and serve as quick navigation to the relevant categories or an external playlist.
The homepage also contains specialized content blocks outside the standard article system:
npm install
npm run dev
Astro normally starts the local server at http://localhost:4321/ and reacts continuously to changes in the project.
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
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.
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:
npm run build:web:translate
To verify that no English translation cache entry is missing, use:
npm run build:web:strict
To inspect unused English translation cache files without deleting them, use:
npm run build:web:prune:dry
To delete unused, unprotected English translation cache files after the dry run looks correct, use:
npm run build:web:prune
npm run preview
Used for local verification of the production build.
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:
npm run build:usb:translate
For a strict USB check, use:
npm run build:usb:strict
To inspect unused English translation cache files while producing the portable build, use:
npm run build:usb:prune:dry
To delete unused, unprotected English translation cache files after the dry run looks correct, use:
npm run build:usb:prune
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 and embedded content is represented by explicit placeholders instead of being silently removed. For example:
[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.
The generated dist/ALL_POSTS.txt can be filtered manually with:
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:
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:
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.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:
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:
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.
During the final post-build phase, the project generates integrity files for the completed static output:
/SHA256SUMS.txt
/BUILD_SHA256.txt
/integrity.json
These files are produced by:
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:
BUILD_SHA256.txt
Machine-readable metadata is stored in:
integrity.json
The following files are intentionally excluded from the checksum manifest:
SHA256SUMS.txt
BUILD_SHA256.txt
integrity.json
.DS_Store
Thumbs.db
This avoids a recursive checksum loop where the checksum files would change the checksum they are trying to describe.
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.
During the web and USB builds, the project generates a reconstructable source package:
/source/vojtamaur-web-source.zip
In the local build output this file is written to:
dist/source/vojtamaur-web-source.zip
The package is produced by:
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:
python download-assets.py
npm install
npm run build:web:strict
For a portable file-based reconstruction:
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.
The project includes a separate manual PDF export tool:
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 the first use, install the Python dependencies and the Playwright Chromium browser:
python -m pip install -r requirements-pdf-export.txt
python -m playwright install chromium
The standard command is:
npm run export:pdf
Equivalently, the script can be called directly:
python scripts/export-site-pdf.py
The default export:
pdf-export-manifest.json with article metadata, output file sizes, and SHA-256 hashesThe generated files are written to:
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:
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:
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:
--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:
python scripts/export-site-pdf.py --pdf-quality ebook --image-dpi 150 --jpeg-quality 75
The available quality presets are:
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:
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:
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.
The Arweave / Permaweb build is created with:
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:
node scripts/make-arweave-build.mjs
npm run generate:integrity:arweave
The script creates a separate output directory:
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:
/_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:
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.
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 && npm run generate:integrity
This implies six steps:
BUILD_TARGET=usb is setastro.config.mjsscripts/en-postprocess.mjs applies the English translation layer using the available cachescripts/usb-rewrite.mjs rewrites root-based URLs into relative file pathsscripts/generate-all-posts.mjs creates dist/ALL_POSTS.txt as a plain-text preservation export of the finished buildscripts/generate-integrity.mjs creates dist/SHA256SUMS.txt, dist/BUILD_SHA256.txt, and dist/integrity.json for the final static outputastro.config.mjs does in USB modeWhen BUILD_TARGET=usb, the following are used:
trailingSlash: "never"build.format: "file"This means that internal routes are generated as files of the form:
slug.html
instead of the directory form:
slug/index.html
scripts/usb-rewrite.mjs doesThe script:
.html files in the dist directorySpecific rewrites include:
/_astro//images/, /files/, /demos/, and /source///slug//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.
build:arweave logicThe 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:
npm run build:web:strict && node scripts/make-arweave-build.mjs && npm run generate:integrity:arweave
This implies five main steps:
dist/scripts/en-postprocess.mjs verifies or applies the English translation layer according to the strict build workflowscripts/generate-all-posts.mjs creates dist/ALL_POSTS.txt and scripts/generate-integrity.mjs creates integrity files for dist/scripts/make-arweave-build.mjs copies dist/ to dist-arweave/ and rewrites root-relative pathsscripts/generate-integrity.mjs dist-arweave creates fresh integrity files for the final Arweave outputThe 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:
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.
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:
src/lib/i18n.ts. This covers the header, section names, homepage text, and other short visible strings.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.
The translation configuration is stored in:
scripts/i18n-config.mjs
Important values:
CSEN-UStranslations/en[data-i18n="translate"]80_000 bytesThe 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.
Project-specific terminology used by the automatic translation layer is stored in:
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:
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:
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:
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:
translations/glossary-cs-en.state.json when possibleThe 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.
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.
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:
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:
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:
npm run build:web:prune:dry
To delete unused, unprotected English translation cache files, use:
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.
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
To inspect unused English translation cache files during the USB workflow, use:
npm run build:usb:prune:dry
To delete unused, unprotected English translation cache files during the USB workflow, use:
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:.
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, selector policy revision, and any glossary rows that apply to the fragment.
Practical consequences:
translations/en/ after content changes unless the cache is prunedtranslations/en/ forces translation generation from scratch and should not be used as routine cleanupThe cache is part of the project source, not a public runtime dependency. The published site uses the finished HTML in dist/.
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:
npm run build:web:prune:dry
npm run build:web:prune
Recommended USB workflow:
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:
manual: truelocked: trueedited: trueNew automatically generated cache entries are marked as not manually edited:
"manual": false,
"locked": false,
"edited": false,
"editedAt": null,
"editedBy": null
When a translation is manually corrected, the cache entry can be marked like this:
"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.
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
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:
notranslate inside MediaRowMediaRow.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.
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:
NoTranslate<div class="notranslate" translate="no">...</div> inside MediaRow text itemsThe size guardrail exists to avoid sending oversized and fragile HTML blobs to DeepL.
DEEPL_AUTH_KEY is not setThe 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 cacheStrict 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.
Remote DeepL glossary created from local TSVThis 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:
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 429DeepL 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:
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.
First run only the dry-run command:
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.
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.
dist/ is Czech after build:web:translateCheck 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.
dist/ is EnglishThe 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.
Before uploading the web build to FTP:
npm run build:web:translate.npm run build:web:strict.npm run build:web:prune:dry and check the summary.npm run build:web:prune.npm run preview.dist/SHA256SUMS.txt, dist/BUILD_SHA256.txt, and dist/integrity.json exist.dist/ directory, including the integrity files.For USB/offline output:
npm run build:usb:translate.npm run build:usb:strict.npm run build:usb:prune:dry and check the summary.npm run build:usb:prune.SHA256SUMS.txt, BUILD_SHA256.txt, and integrity.json are present in the generated output.For Arweave / Permaweb output:
python scripts/audit-public-metadata.py --exiftool "D:\Program Files\exiftool\exiftool.exe"npm run build:arweave.Arweave build prepared in dist-arweave.dist-arweave/SHA256SUMS.txt, dist-arweave/BUILD_SHA256.txt, and dist-arweave/integrity.json exist.dist-arweave/ to ArDrive.index.html is directly present.ALL_POSTS.txt, ARCHIVE.txt, and the integrity files.dist structureThe dist/ directory contains the finished build intended for publishing.
Important generated preservation and verification files include:
dist/ALL_POSTS.txt
dist/SHA256SUMS.txt
dist/BUILD_SHA256.txt
dist/integrity.json
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.
For the production website, the content corresponding to the standard web build is uploaded to the hosting server.
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:
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.
The Codeberg repository is named pages and is available at:
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 copiespages – the generated contents of dist/, used only by Codeberg PagesThe published website is available at:
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:
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:
G:\vojtamaur-web -> main
G:\vojtamaur-pages -> pages
The deployment helper is:
deploy-codeberg-pages.bat
It is intended to be stored in the project root. The script:
codeberg remotemain worktree contains uncommitted changesnpm run build in G:\vojtamaur-webpages worktreedist/ contents into G:\vojtamaur-pagespages branch to CodebergThe script does not commit, modify, or push main. The normal source publishing workflow therefore remains separate:
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:
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:
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.
.htaccessFor 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.
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:
./source/vojtamaur-web-source.zip
rather than:
/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.
For Arweave deployment, do not upload the normal dist/ directory directly. Use the generated Arweave-specific output:
dist-arweave/
Recommended workflow:
npm run build:arweavedist-arweave/SHA256SUMS.txt, dist-arweave/BUILD_SHA256.txt, and dist-arweave/integrity.json existdist-arweave/ locally under a fake manifest-like subdirectorydist-arweave/ to a public ArDrive driveindex.html is directly presentThe manifest must be created inside the uploaded build root, not above it. The correct manifest target level contains:
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.
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.
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.
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:
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.
If the listing or routes do not match after adding a new .mdx file, the recommended first step is to restart the development server.
Possible future directions for the project:
wiki.vojtamaur.cz)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:
.mdx filespages branch through the deploy-codeberg-pages.bat worktree workflow/source/vojtamaur-web-source.zippublic/images/ and public/files/ should be checked for unintended embedded metadata before commitmanual, locked, and edited entries are preservedThis documentation describes the current architecture and operating model of the project in a form suitable for ongoing maintenance, handoff, or future migration.