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.
astro.config.mjsThe 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.jsonBasic 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:
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, and generates dist/ALL_POSTS.txtnpm run build:web:translate – creates the web build, fills missing English translation cache entries through DeepL, and generates dist/ALL_POSTS.txtnpm run build:web:strict – creates the web build, fails if an English translation cache entry is missing, and generates dist/ALL_POSTS.txtnpm run preview – local preview of the buildnpm run build:usb – creates a portable file-based build, applies the English postprocess, rewrites paths for offline use, and generates dist/ALL_POSTS.txtnpm run build:usb:translate – creates a portable file-based build, fills missing English translation cache entries, and generates dist/ALL_POSTS.txtnpm run generate:all-posts – generates dist/ALL_POSTS.txt, a plain-text export of all article content from the finished build outputcontent.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:
imagepdftextUse cases:
The component also supports a bordered variant.
Embed.astroA generic wrapper for embedded iframe content. It is used, for example, for:
Supported parameters:
srcratiokindwidthalignMediaRowWhen 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.
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
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.
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
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.
build:usb logicThe 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:
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 buildastro.config.mjs does in this 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/, and /demos///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.
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.
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.
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:.
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:
translations/en/ forces translation generation from scratchThe cache is part of the project source, not a public runtime dependency. The published site uses the finished HTML in dist/.
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.
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 preview.dist/ directory.For USB/offline output:
npm run build:usb:translate.npm run build:usb:strict or node scripts/build-usb-strict.mjs.dist structureThe dist/ directory contains the finished build intended for publishing.
For the production website, the content corresponding to the standard web build is uploaded to the hosting server.
.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.
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. For this reason, the portable file-based build is supplemented with the usb-rewrite.mjs postprocessing step.
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:
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 filespublic/images/ and public/files/ should be checked for unintended embedded metadata before commitThis documentation describes the current architecture and operating model of the project in a form suitable for ongoing maintenance, handoff, or future migration.