Merge branch 'master' into blog-3-14-1

This commit is contained in:
Ulf Gebhardt 2026-03-04 15:37:53 +01:00 committed by GitHub
commit 9b10ad9d4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 155 additions and 3 deletions

View File

@ -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."

1
.gitignore vendored
View File

@ -4,3 +4,4 @@
/docs/.vuepress/.cache/
/docs/.vuepress/.temp/
/.github/webhooks/hooks.json
**/README.stub.md

View File

@ -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(() =>

View File

@ -1,11 +1,78 @@
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: 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.
// ---------------------------------------------------------------------------
;(() => {
// 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)
}
}
}
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)
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 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.writeFileSync(path.resolve(targetDir, 'README.stub.md'), stubContent)
}
}
})()
export default defineUserConfig({
...meta,

View File

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

View File

@ -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) {