From a4356ada409a426cedee481ed88595827cbf7059 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 4 Mar 2026 14:35:11 +0100 Subject: [PATCH 1/6] redirect to english if page is missing --- docs/.vuepress/config/theme.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/.vuepress/config/theme.js b/docs/.vuepress/config/theme.js index 770d0fd..8eb254d 100644 --- a/docs/.vuepress/config/theme.js +++ b/docs/.vuepress/config/theme.js @@ -250,6 +250,26 @@ export default hopeTheme({ '/fr/': ['fr-FR', 'fr'], }, localeFallback: false, + + // Redirect to English for pages missing in other locales + config: (app) => { + const fallback = '/en/' + const locales = ['/de/', '/es/', '/fr/'] + const pagePaths = new Set(app.pages.map((p) => p.path)) + const enPages = app.pages.filter((p) => p.path.startsWith(fallback)).map((p) => p.path) + const redirects = {} + + for (const locale of locales) { + for (const enPath of enPages) { + const localePath = enPath.replace(fallback, locale) + if (!pagePaths.has(localePath)) { + redirects[localePath] = enPath + } + } + } + + return redirects + }, }, slimsearch: { indexContent: true, From f3cf13d572e23ddbb25b48be7c834ca6c7231940 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 4 Mar 2026 14:59:58 +0100 Subject: [PATCH 2/6] startpage all blog entries --- docs/.vuepress/components/MiniBlog.vue | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/.vuepress/components/MiniBlog.vue b/docs/.vuepress/components/MiniBlog.vue index 861ac93..f6965a6 100644 --- a/docs/.vuepress/components/MiniBlog.vue +++ b/docs/.vuepress/components/MiniBlog.vue @@ -74,11 +74,26 @@ const props = defineProps({ }, }); -// Nur Artikel des aktuellen Locales + Top X +const FALLBACK_LOCALE = '/en/'; + +// Slug = path without locale prefix (e.g. "news/2025-07-05-release/") +const slugOf = (a) => a.path.replace(a.locale, ''); + +// Articles for current locale + EN fallback for missing ones const items = computed(() => { const loc = locale.value || "/"; - const list = (articles || []).filter(a => a.locale === loc); - return list.slice(0, props.topArticlesCount); + const all = articles || []; + + const localeArticles = all.filter(a => a.locale === loc); + const localeSlugs = new Set(localeArticles.map(slugOf)); + + const fallbacks = loc === FALLBACK_LOCALE + ? [] + : all.filter(a => a.locale === FALLBACK_LOCALE && !localeSlugs.has(slugOf(a))); + + return [...localeArticles, ...fallbacks] + .sort((a, b) => (Date.parse(b.date) || 0) - (Date.parse(a.date) || 0)) + .slice(0, props.topArticlesCount); }); const articleIndex = computed(() => From d58a10ec725d6a15a881ad3db73512a05070aa76 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 4 Mar 2026 15:12:50 +0100 Subject: [PATCH 3/6] fallback english --- .gitignore | 1 + docs/.vuepress/config.js | 57 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/.gitignore b/.gitignore index c83b06a..d8fe03f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /docs/.vuepress/.cache/ /docs/.vuepress/.temp/ /.github/webhooks/hooks.json +/docs/.vuepress/.generated-fallbacks.json diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index b4d6954..380f625 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -1,11 +1,68 @@ import { getDirname, path } from "vuepress/utils" import { defineUserConfig } from 'vuepress' import { viteBundler } from '@vuepress/bundler-vite' +import fs from 'node:fs' import meta from './config/meta' import theme from './config/theme' const __dirname = getDirname(import.meta.url) +const docsDir = path.resolve(__dirname, '..') + +const FALLBACK_LOCALE = 'en' +const OTHER_LOCALES = ['de', 'es', 'fr'] + +// --------------------------------------------------------------------------- +// Before VuePress starts: create stub markdown files for EN articles that +// are missing in other locales. This ensures the blog plugin picks them up +// (it requires real files with filePathRelative). +// Generated stubs are tracked via a manifest and cleaned up on each run. +// --------------------------------------------------------------------------- +const manifestPath = path.resolve(__dirname, '.generated-fallbacks.json') + +;(() => { + // Clean up previously generated stubs + if (fs.existsSync(manifestPath)) { + const oldPaths = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) + for (const p of oldPaths) { + if (fs.existsSync(p)) fs.rmSync(p, { recursive: true }) + } + } + + const enNewsDir = path.resolve(docsDir, FALLBACK_LOCALE, 'news') + if (!fs.existsSync(enNewsDir)) return + + const enSlugs = fs.readdirSync(enNewsDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + + const generated = [] + + for (const locale of OTHER_LOCALES) { + const localeNewsDir = path.resolve(docsDir, locale, 'news') + if (!fs.existsSync(localeNewsDir)) fs.mkdirSync(localeNewsDir, { recursive: true }) + + const existingSlugs = new Set( + fs.readdirSync(localeNewsDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + ) + + for (const slug of enSlugs) { + if (existingSlugs.has(slug)) continue + + const enFile = path.resolve(enNewsDir, slug, 'README.md') + if (!fs.existsSync(enFile)) continue + + const targetDir = path.resolve(localeNewsDir, slug) + fs.mkdirSync(targetDir, { recursive: true }) + fs.copyFileSync(enFile, path.resolve(targetDir, 'README.md')) + generated.push(targetDir) + } + } + + fs.writeFileSync(manifestPath, JSON.stringify(generated, null, 2)) +})() export default defineUserConfig({ ...meta, From 0a60fc8f94dfa079d0595840426e5d2fe6891d0f Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 4 Mar 2026 15:18:51 +0100 Subject: [PATCH 4/6] padding top smaller distance --- docs/.vuepress/styles/palette.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/.vuepress/styles/palette.scss b/docs/.vuepress/styles/palette.scss index e4c1644..319db2b 100644 --- a/docs/.vuepress/styles/palette.scss +++ b/docs/.vuepress/styles/palette.scss @@ -52,6 +52,14 @@ h1, h2, h3, h4 { display: none; } +.vp-article-type-wrapper { + display: none; +} + +.blog-page-wrapper { + padding-top: 1rem; +} + // 2 columns from desktop width for article overviews @media (min-width: 1024px) { From 65d3bae7e3eb9bc3ac766bdec0a8e47016a5fb2e Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 4 Mar 2026 15:23:58 +0100 Subject: [PATCH 5/6] ignore stubs --- .gitignore | 2 +- docs/.vuepress/config.js | 44 ++++++++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index d8fe03f..a8b8e19 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ /docs/.vuepress/.cache/ /docs/.vuepress/.temp/ /.github/webhooks/hooks.json -/docs/.vuepress/.generated-fallbacks.json +**/README.stub.md diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 380f625..f9f0a4c 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -13,19 +13,24 @@ const FALLBACK_LOCALE = 'en' const OTHER_LOCALES = ['de', 'es', 'fr'] // --------------------------------------------------------------------------- -// Before VuePress starts: create stub markdown files for EN articles that -// are missing in other locales. This ensures the blog plugin picks them up -// (it requires real files with filePathRelative). -// Generated stubs are tracked via a manifest and cleaned up on each run. +// Before VuePress starts: for EN articles missing in other locales, create +// README.stub.md files with a permalink so VuePress treats them as pages. +// These .stub.md files are gitignored. // --------------------------------------------------------------------------- -const manifestPath = path.resolve(__dirname, '.generated-fallbacks.json') - ;(() => { - // Clean up previously generated stubs - if (fs.existsSync(manifestPath)) { - const oldPaths = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) - for (const p of oldPaths) { - if (fs.existsSync(p)) fs.rmSync(p, { recursive: true }) + // Clean up old stubs first + for (const locale of OTHER_LOCALES) { + const localeNewsDir = path.resolve(docsDir, locale, 'news') + if (!fs.existsSync(localeNewsDir)) continue + for (const entry of fs.readdirSync(localeNewsDir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue + const stubFile = path.resolve(localeNewsDir, entry.name, 'README.stub.md') + if (fs.existsSync(stubFile)) { + fs.unlinkSync(stubFile) + // Remove directory if now empty + const dir = path.resolve(localeNewsDir, entry.name) + if (fs.readdirSync(dir).length === 0) fs.rmdirSync(dir) + } } } @@ -36,8 +41,6 @@ const manifestPath = path.resolve(__dirname, '.generated-fallbacks.json') .filter((d) => d.isDirectory()) .map((d) => d.name) - const generated = [] - for (const locale of OTHER_LOCALES) { const localeNewsDir = path.resolve(docsDir, locale, 'news') if (!fs.existsSync(localeNewsDir)) fs.mkdirSync(localeNewsDir, { recursive: true }) @@ -54,14 +57,21 @@ const manifestPath = path.resolve(__dirname, '.generated-fallbacks.json') const enFile = path.resolve(enNewsDir, slug, 'README.md') if (!fs.existsSync(enFile)) continue + const enContent = fs.readFileSync(enFile, 'utf-8') + + // Inject permalink into frontmatter so VuePress maps the page + // to the correct locale path (e.g. /fr/news/slug/) + const permalink = `/${locale}/news/${slug}/` + const stubContent = enContent.replace( + /^---\n/, + `---\npermalink: ${permalink}\n` + ) + const targetDir = path.resolve(localeNewsDir, slug) fs.mkdirSync(targetDir, { recursive: true }) - fs.copyFileSync(enFile, path.resolve(targetDir, 'README.md')) - generated.push(targetDir) + fs.writeFileSync(path.resolve(targetDir, 'README.stub.md'), stubContent) } } - - fs.writeFileSync(manifestPath, JSON.stringify(generated, null, 2)) })() export default defineUserConfig({ From c7a5acd82aa9c1585b6e349e3b9e2facbad7dc37 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 4 Mar 2026 15:26:25 +0100 Subject: [PATCH 6/6] enforce engish articles ci --- .github/workflows/check-english-articles.yml | 41 ++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/check-english-articles.yml diff --git a/.github/workflows/check-english-articles.yml b/.github/workflows/check-english-articles.yml new file mode 100644 index 0000000..495d553 --- /dev/null +++ b/.github/workflows/check-english-articles.yml @@ -0,0 +1,41 @@ +name: Check English Article Availability + +on: [push, pull_request] + +jobs: + check-english-articles: + name: Every article must have an English version + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check for missing English articles + run: | + missing=0 + + for locale_dir in docs/de/news docs/es/news docs/fr/news; do + [ -d "$locale_dir" ] || continue + locale=$(echo "$locale_dir" | cut -d/ -f2) + + for slug_dir in "$locale_dir"/*/; do + [ -d "$slug_dir" ] || continue + slug=$(basename "$slug_dir") + en_file="docs/en/news/$slug/README.md" + + if [ ! -f "$en_file" ]; then + echo "::error::Missing English article: $en_file (exists in $locale)" + missing=$((missing + 1)) + fi + done + done + + if [ "$missing" -gt 0 ]; then + echo "" + echo "Found $missing article(s) without an English version." + echo "Every article must have an English translation in docs/en/news/." + exit 1 + fi + + echo "All articles have an English version."