SHA256
Separation as Tool
This commit is contained in:
+29
@@ -0,0 +1,29 @@
|
|||||||
|
# Visual Studio Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Visual Studio / MonoDevelop / Rider
|
||||||
|
.vs/
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.rsuser
|
||||||
|
*.sln.docstates
|
||||||
|
*.userprefs
|
||||||
|
*.pidb
|
||||||
|
|
||||||
|
# .NET / C# build outputs and caches
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
TestResult*/
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# Generated site output
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
# Machine-translation drafts (never commit)
|
||||||
|
.translation-drafts/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# GCBoK.WebStaticBuilder — Agent Skills
|
||||||
|
|
||||||
|
Quelle der Wahrheit für Agenten-Kollaboration an diesem Tool.
|
||||||
|
|
||||||
|
## Was das Tool ist
|
||||||
|
Eigenständiger Static-Site-Generator (C#/.NET 10, Assembly `webstatic`), MIT.
|
||||||
|
Kein Python. Suchindex via `npx pagefind` (optional). Früher Submodul der
|
||||||
|
GCBoK-Website (`www/build`), jetzt unter `/opt/GitCover/webstatic`.
|
||||||
|
|
||||||
|
## Kanonischer Build/Verify
|
||||||
|
```
|
||||||
|
dotnet build -c Release
|
||||||
|
dotnet bin/Release/net10.0/webstatic.dll build --env work --verbose
|
||||||
|
dotnet bin/Release/net10.0/webstatic.dll serve --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konventionen
|
||||||
|
- Namespace `GCBoK.WebStaticBuilder`; `Program.cs` braucht `using GCBoK.WebStaticBuilder;`.
|
||||||
|
- Package-Versionen explizit pinnen; `<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>` gesetzt.
|
||||||
|
- Pfade generisch über `Directories` + `BuildConfig.Paths` (nie `www/` hartkodieren).
|
||||||
|
- i18n via `{{ i18n.* }}` + `i18n.json` (Fallback EN).
|
||||||
|
- Config-Priorität: CLI > `webstatic.json` > Defaults.
|
||||||
|
|
||||||
|
## Bekannte Fallstricke
|
||||||
|
- NU1015 → Version pinnen. CS0103 in Program.cs → using. CS8361 → Ternär in Klammern.
|
||||||
|
- `BuildSectionsHtml` gibt `string` zurück (nicht `StringBuilder`) für `.Replace`.
|
||||||
|
|
||||||
|
## Struktur
|
||||||
|
plans/ docs/ diary/ src/ tests/ webstatic.example/ install.sh LICENSE
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# AGENTS — GCBoK.WebStaticBuilder
|
||||||
|
|
||||||
|
Guidelines für KI-Agenten, die an diesem Tool (nicht an der GCBoK-Website) arbeiten.
|
||||||
|
|
||||||
|
## Wesen des Projekts
|
||||||
|
- Eigenständiger **Static-Site-Generator** (C# / .NET 10, kompilierte Konsolen-App, Assembly `webstatic`).
|
||||||
|
- Früher: Submodul `OSS/build_webstatic` innerhalb der GCBoK-Website (`www/build/`).
|
||||||
|
Seit 2026-07 eigenständiges OSS-Tool unter `/opt/GitCover/webstatic` (KMU-fähig).
|
||||||
|
- **Kein Python.** Einzige externe Build-Abhängigkeit: `npx pagefind` (Suchindex, optional).
|
||||||
|
- Lizenz: **MIT** (`LICENSE`).
|
||||||
|
|
||||||
|
## Build & Verify (kanonisch)
|
||||||
|
```bash
|
||||||
|
dotnet build -c Release # kompiliert src/ -> bin/Release/net10.0/webstatic.dll
|
||||||
|
dotnet bin/Release/net10.0/webstatic.dll build --env work --verbose # Beispiel in webstatic.example/
|
||||||
|
dotnet bin/Release/net10.0/webstatic.dll serve --port 8000 # lokale Vorschau
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konventionen
|
||||||
|
- **Namespace:** `GCBoK.WebStaticBuilder` (jede `.cs` in `src/` deklariert ihn; `Program.cs` nutzt `using GCBoK.WebStaticBuilder;`).
|
||||||
|
- **Packages:** `Markdig`, `YamlDotNet` (Versionen **explizit** gepinnt — CPM ist deaktiviert via `<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>`).
|
||||||
|
- **Keine unversionierten `PackageReference`** (führt zu NU1015).
|
||||||
|
- **Config-Priorität:** CLI-Flags > `webstatic.json` > eingebaute Defaults (`BuildConfig.Defaults()`).
|
||||||
|
- **Pfade generisch:** nie hartkodiert auf `www/` — alles über `Directories` (aus `BuildConfig.Paths` relativ zum Site-Root).
|
||||||
|
- **i18n:** UI-Texte über `{{ i18n.<bereich>.<key> }}` in Templates; Daten in `i18n.json`; Substitution via `I18n.cs` (Fallback Englisch).
|
||||||
|
|
||||||
|
## Verzeichnisstruktur
|
||||||
|
`plans/` (Pläne), `docs/` (Anleitungen), `diary/` (Dev-Tagebuch), `src/` (Code),
|
||||||
|
`tests/` (Tests), `webstatic.example/` (KMU-Vorlage), `install.sh`, `LICENSE`.
|
||||||
|
|
||||||
|
## Guardrails
|
||||||
|
- Umlaute in Prosa verwenden (`ä ö ü ß`), nicht `ae/oe/ue`.
|
||||||
|
- `§` (nicht ausgeschrieben) für Gesetzesparagraphen.
|
||||||
|
- **Kein Commit ohne expliziten Wunsch.** Bei Release: signierte Commits.
|
||||||
|
- `dist/`, `bin/`, `obj/`, `.translation-drafts/` sind gitignored.
|
||||||
|
- Machine-Translation liefert nur **Entwürfe** (`.translation-drafts/`), nie Auto-Überschreibung.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>GCBoK.WebStaticBuilder</RootNamespace>
|
||||||
|
<AssemblyName>webstatic</AssemblyName>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
|
||||||
|
<Description>GCBoK.WebStaticBuilder — Static site generator for multilingual, SEO-friendly, compliance-friendly websites (OSS, MIT).</Description>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Markdig" Version="0.41.1" />
|
||||||
|
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 GitCover Commons gUG
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# MEMORY — GCBoK.WebStaticBuilder
|
||||||
|
|
||||||
|
Persistentes Gedächtnis für das Tool-Projekt (nicht die GCBoK-Website).
|
||||||
|
|
||||||
|
## Status (2026-07-11)
|
||||||
|
- Phase 0 + 1 abgeschlossen: Tool eigenständig unter `/mnt/brx5/work/OSS/build_webstatic`,
|
||||||
|
umbenannt zu `GCBoK.WebStaticBuilder`, Python/`build.csx` entfernt, Build verifiziert
|
||||||
|
(webstatic.example baut de/en/vi mit i18n, Pagefind, Mermaid).
|
||||||
|
- Phase 2–5 ausstehend (siehe plans/00-produkt-webstatic.md).
|
||||||
|
|
||||||
|
## Architektur-Entscheidungen
|
||||||
|
- Assembly-Name `webstatic`; CLI-Subcommands: `build`, `translate`, `init`, `serve`.
|
||||||
|
- Umgebungen: `work`/`review`/`staging`/`deploy` (Aliase `--preview`/`--staging`/`--deploy`).
|
||||||
|
- MT über `System.Net.Http` (Azure/DeepL REST), kein Extra-NuGet; Key via Env (`WEBSTATIC_TRANSLATOR_KEY`).
|
||||||
|
- i18n-Port von `i18n.py` → `I18n.cs` (JSON `i18n.json`).
|
||||||
|
|
||||||
|
## Wichtig
|
||||||
|
- CPM deaktiviert; Package-Versionen explizit pinnen (NU1015 sonst).
|
||||||
|
- `dist/`, `bin/`, `obj/`, `.translation-drafts/` gitignored.
|
||||||
|
- Submodul `www/build` der GCBoK-Website wird in Phase 3 entfernt; Website nutzt dann `/opt/GitCover/webstatic`.
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# GCBoK.WebStaticBuilder
|
||||||
|
|
||||||
|
Eigenständiger, quelloffener **Static-Site-Generator** der GitCover Commons für
|
||||||
|
mehrsprachige, SEO-optimierte, compliance-freundliche Websites. Ersetzt den
|
||||||
|
früheren C#-Builderskript der GCBoK-Website durch ein generisches, für KMU
|
||||||
|
einsetzbares Werkzeug.
|
||||||
|
|
||||||
|
- **Sprache:** C# / .NET 10 (kompilierte Konsole)
|
||||||
|
- **Lizenz:** MIT
|
||||||
|
- **Kein Python:** einzige externe Abhängigkeit ist `npx pagefind` (Suchindex, optional)
|
||||||
|
- **Funktionen:** Markdown + YAML-Frontmatter → statisches HTML pro Sprache,
|
||||||
|
Navigation, SEO-Kopf (OpenGraph/JSON-LD), `sitemap.xml`, `robots.txt`,
|
||||||
|
Mermaid-Diagramme, client-seitige Volltextsuche (Pagefind), UI-Lokalisierung
|
||||||
|
über `{{ i18n.* }}`, optionaler Machine-Translation-Helfer (Entwürfe).
|
||||||
|
|
||||||
|
## Schnellstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Tool bauen
|
||||||
|
dotnet build -c Release
|
||||||
|
|
||||||
|
# Beispiel-Seite erzeugen und bauen
|
||||||
|
dotnet bin/Release/net10.0/webstatic.dll init mysite
|
||||||
|
cd mysite
|
||||||
|
dotnet ../../bin/Release/net10.0/webstatic.dll build --env work
|
||||||
|
|
||||||
|
# Lokal ansehen
|
||||||
|
dotnet ../../bin/Release/net10.0/webstatic.dll serve --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Umgebungen
|
||||||
|
|
||||||
|
| `--env` | baseUrl (Beispiel) | Zweck |
|
||||||
|
|----------|------------------------------|------------------------|
|
||||||
|
| `work` | `http://localhost:8000` | Lokale Entwicklung |
|
||||||
|
| `review` | `https://review.gitcover.org`| Freigabe/Vorschau |
|
||||||
|
| `staging`| `https://gcbok.gitcover.org` | Abnahme (brx5) |
|
||||||
|
| `deploy` | `https://gcbok.org` | Production |
|
||||||
|
|
||||||
|
Aliase: `--preview` = `work`, `--staging` = `staging`, `--deploy` = `deploy`.
|
||||||
|
|
||||||
|
## Konfiguration (`webstatic.json`)
|
||||||
|
|
||||||
|
CLI-Flags haben Vorrang vor `webstatic.json`, dieses vor eingebauten Defaults.
|
||||||
|
Siehe `webstatic.example/webstatic.json` und `docs/getting-started.md`.
|
||||||
|
|
||||||
|
## Installation für KMU
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./install.sh # -> /opt/GitCover/webstatic, Symlink /usr/local/bin/webstatic
|
||||||
|
```
|
||||||
|
|
||||||
|
## Machine Translation (optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
webstatic translate --check # fehlende Übersetzungen melden
|
||||||
|
export WEBSTATIC_TRANSLATOR_KEY=... # API-Key (Azure/DeepL)
|
||||||
|
webstatic translate --draft # Entwürfe nach .translation-drafts/
|
||||||
|
```
|
||||||
|
|
||||||
|
Entwürfe überschreiben niemals Quelldateien.
|
||||||
|
|
||||||
|
## Verzeichnisstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
plans/ Produktplan & Folgepläne
|
||||||
|
docs/ Anleitungen (KMU, Admin)
|
||||||
|
diary/ Entwicklungs-Tagebuch
|
||||||
|
src/ C#-Quellcode (GCBoK.WebStaticBuilder)
|
||||||
|
tests/ (künftig) Unit/Integrationstests
|
||||||
|
webstatic.example/ Vorlage-Site für KMU
|
||||||
|
install.sh KMU-Installation
|
||||||
|
```
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# SKILLS — GCBoK.WebStaticBuilder
|
||||||
|
|
||||||
|
Fähigkeiten und wiederkehrende Arbeitsmuster für dieses Tool.
|
||||||
|
|
||||||
|
## Build & Test
|
||||||
|
- Immer `dotnet build -c Release` vor Verifikation.
|
||||||
|
- Smoke-Test: `webstatic build --env work` in `webstatic.example/`, dann `serve` prüfen.
|
||||||
|
- Pagefind-Index erwartet in `dist/pagefind/`; bei fehlendem Node nur Warnung.
|
||||||
|
|
||||||
|
## Häufige Fehler
|
||||||
|
- **NU1015** (keine Package-Version): CPM aktiv → Versionen in csproj pinnen + `<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>`.
|
||||||
|
- **CS0103** in `Program.cs`: Typen aus `GCBoK.WebStaticBuilder` nicht sichtbar → `using GCBoK.WebStaticBuilder;` oben in `Program.cs`.
|
||||||
|
- **CS8361** (Ternär in String-Interpolation): Tertiär-Ausdruck in `(...)` setzen oder vorab in Variable auslagern.
|
||||||
|
- **StringBuilder statt string** bei `Replace(...)`: Rückgabe von `BuildSectionsHtml` ist `string`.
|
||||||
|
|
||||||
|
## Dateien
|
||||||
|
- `src/BuildConfig.cs` — Config-Modell + Defaults + Merge + Load.
|
||||||
|
- `src/Directories.cs` — generische Pfade aus Config (Init vor Build).
|
||||||
|
- `src/Builder.cs` — Orchestrierung (Render, SEO, Sitemap, Pagefind).
|
||||||
|
- `src/I18n.cs` — `{{ i18n.* }}`-Substitution.
|
||||||
|
- `src/Translation/TranslationService.cs` — MT-Helfer (Azure/DeepL).
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Entwicklungs-Tagebuch — GCBoK.WebStaticBuilder
|
||||||
|
|
||||||
|
## 2026-07-11 — Phase 0/1: Eigenständiges Tool
|
||||||
|
- Dev-Repo `/mnt/brx5/work/OSS/build_webstatic` aus `www/build` (Submodul `OSS/build_webstatic`) angelegt.
|
||||||
|
- Python-Legacy (`build.py`, `i18n.py`, `sections.py`, `utils.py`, `simple_build.py`) und
|
||||||
|
`build.csx` entfernt; Verzeichnisstruktur `plans/ docs/ diary/ src/ tests/ webstatic.example/` erstellt.
|
||||||
|
- Umbenennung `GCBoK.Build` → `GCBoK.WebStaticBuilder` (csproj/Assembly `webstatic`).
|
||||||
|
- Config-Modell `BuildConfig` (CLI > webstatic.json > Defaults), generische Pfade (`Directories`),
|
||||||
|
Umgebungen `work/review/staging/deploy` (Aliase `--preview/--staging/--deploy`).
|
||||||
|
- i18n: `i18n.py` → `I18n.cs` (JSON `i18n.json`), `{{ i18n.* }}`-Substitution + `{{ lang_links }}`.
|
||||||
|
- MT-Helfer: `TranslationService` (Azure/DeepL via REST, kein Extra-NuGet), `translate --check/--draft`.
|
||||||
|
- Pagefind im Post-Build, per Config (`pagefind`) abschaltbar.
|
||||||
|
- `init`/`serve` (HttpListener) Subcommands; `webstatic.example/` KMU-Vorlage.
|
||||||
|
- `install.sh` (→ `/opt/GitCover/webstatic`), `LICENSE` (MIT), AGENTS/MEMORY/SKILLS/.hermes, plans/00.
|
||||||
|
- Verifiziert: `dotnet build -c Release` ✓; Beispiel baut de/en/vi mit i18n, Pagefind, Mermaid ✓.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Getting Started (KMU)
|
||||||
|
|
||||||
|
Diese Anleitung beschreibt, wie Sie mit **GCBoK.WebStaticBuilder** eine eigene,
|
||||||
|
mehrsprachige, statische Website erstellen — ohne Server, Datenbank oder
|
||||||
|
JavaScript-Framework.
|
||||||
|
|
||||||
|
## 1. Voraussetzungen
|
||||||
|
- **.NET 10 SDK** (`dotnet`) — https://dotnet.microsoft.com/download
|
||||||
|
- Optional: **Node.js** (für die Suchfunktion Pagefind). Ohne Node wird die Seite
|
||||||
|
gebaut, die Suche entfällt (nicht fatal).
|
||||||
|
|
||||||
|
## 2. Installation
|
||||||
|
```bash
|
||||||
|
git clone <repo> build_webstatic
|
||||||
|
cd build_webstatic
|
||||||
|
./install.sh # installiert nach /opt/GitCover/webstatic
|
||||||
|
```
|
||||||
|
Nach der Installation ist `webstatic` im PATH verfügbar.
|
||||||
|
|
||||||
|
## 3. Neue Website anlegen
|
||||||
|
```bash
|
||||||
|
webstatic init mysite
|
||||||
|
cd mysite
|
||||||
|
```
|
||||||
|
Das erzeugt eine Vorlage (`webstatic.example/`) mit:
|
||||||
|
- `webstatic.json` — Konfiguration (Sprachen, Umgebungen, Pfade)
|
||||||
|
- `i18n.json` — UI-Übersetzungen
|
||||||
|
- `content/` — Markdown-Inhalte
|
||||||
|
- `templates/layout.html` — HTML-Grundgerüst
|
||||||
|
- `assets/` — CSS, JS, Favicon
|
||||||
|
|
||||||
|
## 4. Inhalte pflegen
|
||||||
|
Jede Seite ist eine Markdown-Datei mit YAML-Frontmatter am Anfang:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
section: start
|
||||||
|
order: 1
|
||||||
|
eyebrow: "Startseite"
|
||||||
|
title: "Startseite"
|
||||||
|
description: "Die Landing-Page"
|
||||||
|
href: /
|
||||||
|
render: true
|
||||||
|
navbar: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Einführung
|
||||||
|
Ihr Inhalt …
|
||||||
|
```
|
||||||
|
|
||||||
|
- `order` steuert die Reihenfolge in Navigation und Sidebar.
|
||||||
|
- `order: 0` = Banner, der auf der Startseite (order 1) eingebettet wird.
|
||||||
|
- Sprachvarianten: `02-start.en.md`, `02-start.vi.md` (Fallback auf Basisdatei).
|
||||||
|
|
||||||
|
## 5. Bauen & Vorschau
|
||||||
|
```bash
|
||||||
|
webstatic build --env work # localhost:8000
|
||||||
|
webstatic serve --port 8000 # lokaler Server (Strg+C beendet)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Veröffentlichen
|
||||||
|
| Ziel | Befehl |
|
||||||
|
|------|--------|
|
||||||
|
| Review | `webstatic build --env review` |
|
||||||
|
| Staging | `webstatic build --env staging` |
|
||||||
|
| Production | `webstatic build --env deploy` |
|
||||||
|
|
||||||
|
Die erzeugte `dist/`-Aufgabe wird auf den jeweiligen Webspace kopiert.
|
||||||
|
|
||||||
|
## 7. UI übersetzen
|
||||||
|
Texte im Layout (`templates/layout.html`) nutzen Platzhalter `{{ i18n.<bereich>.<schluessel> }}`.
|
||||||
|
Die Werte stehen in `i18n.json` (pro Sprache). Fehlende Schlüssel bleiben als
|
||||||
|
Platzhalter sichtbar — so erkennen Sie Lücken sofort.
|
||||||
|
|
||||||
|
## 8. Machine Translation (optional)
|
||||||
|
```bash
|
||||||
|
webstatic translate --check # welche Sprachen fehlen?
|
||||||
|
export WEBSTATIC_TRANSLATOR_KEY=<ihr-key> # Azure oder DeepL
|
||||||
|
webstatic translate --draft # Entwürfe in .translation-drafts/
|
||||||
|
```
|
||||||
|
Entwürfe überschreiben niemals Ihre Quelldateien — sie sind zum Nacharbeiten gedacht.
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# GCBoK.WebStaticBuilder — KMU-Installationsskript
|
||||||
|
# Installiert das Tool nach /opt/GitCover/webstatic und legt einen Symlink
|
||||||
|
# `webstatic` nach /usr/local/bin. Idempotent.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
INSTALL_DIR="${WEBSTATIC_INSTALL_DIR:-/opt/GitCover/webstatic}"
|
||||||
|
BIN_LINK="/usr/local/bin/webstatic"
|
||||||
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
echo "== GCBoK.WebStaticBuilder Installer =="
|
||||||
|
echo "Installationsziel: $INSTALL_DIR"
|
||||||
|
|
||||||
|
# 1) .NET 10 pruefen
|
||||||
|
if ! command -v dotnet >/dev/null 2>&1; then
|
||||||
|
echo "FEHLER: .NET 10 (dotnet) ist nicht installiert." >&2
|
||||||
|
echo "Installiere .NET 10 von https://dotnet.microsoft.com/download" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
DOTNET_VER="$(dotnet --version 2>/dev/null || echo 0)"
|
||||||
|
echo "Gefunden: dotnet $DOTNET_VER"
|
||||||
|
|
||||||
|
# 2) Projekt bauen (Release)
|
||||||
|
echo "== Baue Release =="
|
||||||
|
dotnet build "$REPO_DIR/GCBoK.WebStaticBuilder.csproj" -c Release -v minimal
|
||||||
|
|
||||||
|
# 3) Nach /opt kopieren
|
||||||
|
echo "== Kopiere nach $INSTALL_DIR =="
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
rm -rf "$INSTALL_DIR/bin" "$INSTALL_DIR/webstatic.example" 2>/dev/null || true
|
||||||
|
cp -r "$REPO_DIR/bin/Release/net10.0/." "$INSTALL_DIR/"
|
||||||
|
cp -r "$REPO_DIR/webstatic.example" "$INSTALL_DIR/webstatic.example" 2>/dev/null || true
|
||||||
|
# Hilfsdateien auf oberster Ebene mitnehmen (LICENSE, README, docs)
|
||||||
|
for f in LICENSE README.md docs plans AGENTS.md MEMORY.md SKILLS.md; do
|
||||||
|
[ -e "$REPO_DIR/$f" ] && cp -r "$REPO_DIR/$f" "$INSTALL_DIR/" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
|
||||||
|
# 4) Symlink
|
||||||
|
echo "== Verknuepfe $BIN_LINK -> $INSTALL_DIR/webstatic.dll =="
|
||||||
|
if [ -w "$(dirname "$BIN_LINK")" ]; then
|
||||||
|
ln -sf "$INSTALL_DIR/webstatic.dll" "$BIN_LINK"
|
||||||
|
else
|
||||||
|
echo "Hinweis: $BIN_LINK benoetigt Schreibrechte; erstelle Link via sudo."
|
||||||
|
sudo ln -sf "$INSTALL_DIR/webstatic.dll" "$BIN_LINK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Fertig. Danach verfuegbar ueber: webstatic --help"
|
||||||
|
echo "Beispiel-Seite erzeugen: webstatic init mysite && cd mysite && webstatic build --env work"
|
||||||
|
echo "Umgebungsvariable (optional): WEBSTATIC_BUILDER=$INSTALL_DIR/webstatic.dll"
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# GCBoK.WebStaticBuilder — Produktbeschreibung & Umsetzungsplan
|
||||||
|
|
||||||
|
> Status: Implementierung (Phase 1 abgeschlossen, Build verifiziert). Eigenständiges OSS-Static-Site-Tool der GitCover Commons.
|
||||||
|
> Lizenz: MIT. Sprachen: C# / .NET 10 (keine Python-Abhängigkeiten).
|
||||||
|
|
||||||
|
## 1. Zweck
|
||||||
|
`GCBoK.WebStaticBuilder` (CLI-Befehl `webstatic`) ist ein schlanker, kompilierter Static-Site-Generator für mehrsprachige, SEO-optimierte, compliance-freundliche Websites. Er liest Markdown-Quellen mit YAML-Frontmatter, rendert sie pro Sprache zu statischem HTML, erzeugt Navigation, SEO-Kopf (OpenGraph/JSON-LD), Sitemap, robots.txt und einen client-seitigen Volltextindex (Pagefind) — ohne Server, Datenbank oder Build-Service.
|
||||||
|
|
||||||
|
Das Tool ist die Verallgemeinerung des Builders, der die GCBoK-Website erzeugt, und soll es insbesondere **KMU-Anwendern** ermöglichen, eigene Websites nach derselben Funktionsweise zu erstellen.
|
||||||
|
|
||||||
|
## 2. Zielgruppe
|
||||||
|
- **Primär:** KMU, die eine mehrsprachige, datenschutzfreundliche, revisionssichere Website/ Dokumentation betreiben wollen (kein JS-Framework, kein Tracking, statisch hostbar).
|
||||||
|
- **Sekundär:** OSS-Projekte, Behörden, gemeinnützige Träger mit BoK-/Compliance-Anspruch.
|
||||||
|
|
||||||
|
## 3. Architektur & Technik
|
||||||
|
- **C# / .NET 10**, kompilierte Konsolen-App (`OutputType Exe`), Ziel-Framework `net10.0`, Assembly `webstatic`.
|
||||||
|
- **NuGet (alle MIT/BSD-2, lizenzkonform):** `Markdig` (BSD-2), `YamlDotNet` (MIT). Machine-Translation nutzt `System.Net.Http` (kein Extra-Paket).
|
||||||
|
- **Kein Python.** Einziger externer Build-Schritt: `npx pagefind` (Node, best-effort, per Config abschaltbar).
|
||||||
|
- Modulare `src/`-Struktur: `BuildConfig`, `Directories`, `Model`, `ContentLoader`, `MarkdownProcessor`, `SeoHelper`, `I18n`, `Builder`, `Translation/*`.
|
||||||
|
|
||||||
|
## 4. Funktionsumfang
|
||||||
|
- Markdown + YAML-Frontmatter → statisches HTML pro Sprache (eigener Ordner je Sprache).
|
||||||
|
- Automatische Navigation (`.md`-Reihenfolge via `order`/`section`), Sidebar-TOC, Prev/Next.
|
||||||
|
- SEO: `<title>`, Description, Canonical, OpenGraph, Twitter-Card, **JSON-LD**; `sitemap.xml`, `robots.txt`.
|
||||||
|
- Mermaid-Diagramme (CDN, saubere einmalige Initialisierung), responsives Layout.
|
||||||
|
- **Client-Suche:** Pagefind-Index im Post-Build (Mehrsprachigkeit via `<html lang>`).
|
||||||
|
- **i18n:** UI-Chrome (Nav/Footer/ARIA/Sprachumschalter) mehrsprachig über `I18n.cs` + `{{ i18n.* }}`-Platzhalter.
|
||||||
|
- **MT-Helfer:** `translate --check` meldet fehlende Übersetzungen; `translate --draft` erzeugt Entwürfe (nie auto-überschreiben).
|
||||||
|
- **Umgebungen:** deklarativ in Config, wählbar via `--env`.
|
||||||
|
|
||||||
|
## 5. Mehrsprachigkeit
|
||||||
|
- Inhalte: pro Sprache eigene Datei (`<basis>.<lang>.md`), Fallback-Logik.
|
||||||
|
- UI-Strings: portiertes `i18n.py`-Wörterbuch nach `I18n.cs` (de/en/vi), zur Laufzeit pro Sprache injiziert.
|
||||||
|
- Kultur-korrekte Datums-/Zahlformate über `System.Globalization` (vorbereitet).
|
||||||
|
- Optional: MT-Provider (Azure/DeepL) erzeugt **Entwürfe** für fehlende Inhaltsübersetzungen (klar markiert, separater Ordner `.translation-drafts/`).
|
||||||
|
|
||||||
|
## 6. Umgebungen (Arbeitsumgebung / Review / Staging / Public Deployment)
|
||||||
|
| Umgebung | `--env` | baseUrl (Beispiel) | cdn | Zweck / Ort |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **Arbeitsumgebung** | `work` (`--preview`) | `http://localhost:8000` | `/assets` | Lokale Entwicklung; Build→lokales `dist/`, Vorschau via `serve` |
|
||||||
|
| **Review** | `review` | `https://review.gitcover.org` | CDN | Freigabe/Vorschau (Konzept) |
|
||||||
|
| **Staging** | `staging` | `https://gcbok.gitcover.org` | `https://gitcover.org/assets` | Abnahme (brx5 `/var/www/gcbok.gitcover.org`) |
|
||||||
|
| **Public Deployment** | `deploy` | `https://gcbok.org` | `https://gitcover.org/assets` | Production (manueller Upload WebFTP) |
|
||||||
|
|
||||||
|
## 7. CLI-Referenz
|
||||||
|
```
|
||||||
|
webstatic build --env <work|review|staging|deploy> [--content DIR] [--templates DIR]
|
||||||
|
[--assets DIR] [--output DIR] [--base-url URL] [--cdn PATH]
|
||||||
|
[--langs de,en,vi] [--default-lang de] [--config FILE] [--root DIR]
|
||||||
|
[--verbose] [--dry-run]
|
||||||
|
webstatic translate --check # fehlende Übersetzungen melden
|
||||||
|
webstatic translate --draft # Entwürfe erzeugen (MT, API-Key nötig)
|
||||||
|
webstatic init <sitename> # KMU-Onboarding: Site scaffolden
|
||||||
|
webstatic serve [--port 8000] # lokale Vorschau (.NET HttpListener)
|
||||||
|
```
|
||||||
|
Aliase `--preview`=`work`, `--staging`=`staging`, `--deploy`=`deploy` bleiben erhalten.
|
||||||
|
|
||||||
|
## 8. Konfiguration (`webstatic.json`)
|
||||||
|
- `site` (name/title/description je Sprache), `default_language`, `languages` (name/og_locale/href_lang/label), `paths` (content/templates/assets/output/legal/i18n), `glossary_section` + `glossary.href/label`, `pagefind` (bool), `translator` (provider/key_env/endpoint/region), `environments` (work/review/staging/deploy mit base_url/cdn).
|
||||||
|
- Priorität: CLI-Flags > `webstatic.json` > eingebaute Defaults.
|
||||||
|
|
||||||
|
## 9. Installation für KMU (`install.sh`)
|
||||||
|
- Bash-Skript (kein Python): prüft .NET 10, `dotnet build -c Release`, kopiert nach **`/opt/GitCover/webstatic`**, Symlink `webstatic`→App nach `/usr/local/bin`, setzt `WEBSTATIC_BUILDER`.
|
||||||
|
- Idempotent; meldet Voraussetzungen.
|
||||||
|
|
||||||
|
## 10. Onboarding-Flow für KMU
|
||||||
|
- `webstatic init <sitename>` scaffolder: kopiert `webstatic.example/` (content/templates/assets + Config), fragt Sprachen/Site-Titel.
|
||||||
|
- `webstatic serve` (lokale Vorschau) und `webstatic build --env staging` als Kern.
|
||||||
|
- Review-Gate/Konzept (Assistent, Freigabe) wird separat finalisiert.
|
||||||
|
|
||||||
|
## 11. Repository-Struktur (Ziel)
|
||||||
|
```
|
||||||
|
build_webstatic/
|
||||||
|
plans/ # dieser Plan + Folgepläne
|
||||||
|
docs/ # Anleitungen (KMU, Admin)
|
||||||
|
diary/ # Entwicklungs-Tagebuch / Changelog
|
||||||
|
src/ # C#-Quellcode (GCBoK.WebStaticBuilder)
|
||||||
|
tests/ # (künftig) Unit/Integrationstests
|
||||||
|
webstatic.example/ # Vorlage-Site für KMU
|
||||||
|
install.sh README.md LICENSE (MIT) AGENTS.md MEMORY.md SKILLS.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. Lizenz & Compliance
|
||||||
|
- Code: **MIT** (`LICENSE`). Kompatibel mit allen genutzten NuGet-Paketen.
|
||||||
|
- Inhalte der GCBoK-Site: separat (angestrebt CC BY-SA 4.0) — betrifft das Tool nicht.
|
||||||
|
|
||||||
|
## 13. Umsetzungsphasen
|
||||||
|
- **Phase 0 — Dev-Setup:** `build_webstatic` auf `/mnt/brx5/work/OSS/build_webstatic` (kopiert aus `www/build`, Python/`build.csx` entfernt, Verzeichnisstruktur plans/docs/diary/src/tests).
|
||||||
|
- **Phase 1 — Refactor (DONE):** Umbenennung `GCBoK.WebStaticBuilder`; Python entfernt; Config/Umgebungsmodell; i18n (`I18n.cs` + Platzhalter); MT-Helfer; Pagefind; `webstatic.example`; `install.sh`; Docs/LIZENZ. Build & Verify auf Beispielsite ✓.
|
||||||
|
- **Phase 2 — Install & Verify:** `install.sh` → `/opt/GitCover/webstatic`; Standalone-Smoke-Test.
|
||||||
|
- **Phase 3 — Website-Repo:** Submodul `www/build` entfernen; `www/build.sh` durch Bash ersetzen; alle Docs (AGENTS/MEMORY/SKILLS/CONTEXT/README/docs/DEPLOYMENT/oss/*/.hermes) auf neuen Pfad/Tool/Umgebungen/kein-Python aktualisieren; GCBoK-Preview-Build + Dead-Link-Check.
|
||||||
|
- **Phase 4 — Onboarding:** `init`/`serve`/`build` als KMU-Flow (Konzept finalisieren).
|
||||||
|
- **Phase 5 — Abschluss:** Zusammenfassung; Commit/Signierung nur auf Wunsch.
|
||||||
|
|
||||||
|
## 14. Offen / Risiken
|
||||||
|
- **Review-Umgebung:** Konzept (Host/Subdomain/Freigabe) noch offen.
|
||||||
|
- **MT-Sicherheit:** Entwürfe nur als Draft, nie auto-übernommen.
|
||||||
|
- **Rückwärtskompat GCBoK:** Config-Defaults spiegeln GCBoK-Struktur.
|
||||||
|
- **Portabilität:** `/opt` maschinenspezifisch → `WEBSTATIC_BUILDER` + `install.sh` dokumentiert.
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace GCBoK.WebStaticBuilder;
|
||||||
|
|
||||||
|
/// <summary>Per-language configuration.</summary>
|
||||||
|
public class LanguageConfig
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string OgLocale { get; set; } = "";
|
||||||
|
public string? HrefLang { get; set; }
|
||||||
|
public string? Label { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Filesystem paths, relative to the site root unless absolute.</summary>
|
||||||
|
public class PathsConfig
|
||||||
|
{
|
||||||
|
public string Content { get; set; } = "content";
|
||||||
|
public string Templates { get; set; } = "templates";
|
||||||
|
public string Assets { get; set; } = "assets";
|
||||||
|
public string Output { get; set; } = "dist";
|
||||||
|
public string Legal { get; set; } = "legal";
|
||||||
|
public string I18n { get; set; } = "i18n.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Build environment (work/review/staging/deploy) with base URL and CDN.</summary>
|
||||||
|
public class EnvironmentConfig
|
||||||
|
{
|
||||||
|
public string BaseUrl { get; set; } = "http://localhost:8000";
|
||||||
|
public string Cdn { get; set; } = "/assets";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Machine-translation helper configuration (optional, behind an API key).</summary>
|
||||||
|
public class TranslatorConfig
|
||||||
|
{
|
||||||
|
public string Provider { get; set; } = "azure"; // "azure" | "deepl"
|
||||||
|
public string? KeyEnv { get; set; } = "WEBSTATIC_TRANSLATOR_KEY";
|
||||||
|
public string? Endpoint { get; set; }
|
||||||
|
public string? Region { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Per-language glossary link (slug without extension).</summary>
|
||||||
|
public class GlossaryConfig
|
||||||
|
{
|
||||||
|
public Dictionary<string, string> Href { get; set; } = new();
|
||||||
|
public Dictionary<string, string> Label { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Site-level metadata.</summary>
|
||||||
|
public class SiteConfig
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "Site";
|
||||||
|
public Dictionary<string, string> Title { get; set; } = new();
|
||||||
|
public Dictionary<string, string> Description { get; set; } = new();
|
||||||
|
public string? Publisher { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Full build configuration. Loaded from <c>webstatic.json</c> (CLI > file > defaults).
|
||||||
|
/// </summary>
|
||||||
|
public class BuildConfig
|
||||||
|
{
|
||||||
|
public SiteConfig Site { get; set; } = new();
|
||||||
|
public string DefaultLanguage { get; set; } = "de";
|
||||||
|
public Dictionary<string, LanguageConfig> Languages { get; set; } = new();
|
||||||
|
public PathsConfig Paths { get; set; } = new();
|
||||||
|
public GlossaryConfig Glossary { get; set; } = new();
|
||||||
|
public string GlossarySection { get; set; } = "glossar";
|
||||||
|
public bool Pagefind { get; set; } = true;
|
||||||
|
public TranslatorConfig Translator { get; set; } = new();
|
||||||
|
public Dictionary<string, EnvironmentConfig> Environments { get; set; } = new();
|
||||||
|
|
||||||
|
public static BuildConfig Defaults()
|
||||||
|
{
|
||||||
|
var cfg = new BuildConfig
|
||||||
|
{
|
||||||
|
Languages = new()
|
||||||
|
{
|
||||||
|
["de"] = new LanguageConfig { Name = "Deutsch", OgLocale = "de_DE", HrefLang = "de", Label = "Deutsch" },
|
||||||
|
["en"] = new LanguageConfig { Name = "English", OgLocale = "en_US", HrefLang = "en", Label = "English" },
|
||||||
|
["vi"] = new LanguageConfig { Name = "Tiếng Việt", OgLocale = "vi_VN", HrefLang = "vi", Label = "Tiếng Việt" },
|
||||||
|
},
|
||||||
|
Environments = new()
|
||||||
|
{
|
||||||
|
["work"] = new EnvironmentConfig { BaseUrl = "http://localhost:8000", Cdn = "/assets" },
|
||||||
|
["review"] = new EnvironmentConfig { BaseUrl = "https://review.gitcover.org", Cdn = "https://gitcover.org/assets" },
|
||||||
|
["staging"] = new EnvironmentConfig { BaseUrl = "https://gcbok.gitcover.org", Cdn = "https://gitcover.org/assets" },
|
||||||
|
["deploy"] = new EnvironmentConfig { BaseUrl = "https://gcbok.org", Cdn = "https://gitcover.org/assets" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Load config from a JSON file if present, merged over the built-in defaults.</summary>
|
||||||
|
public static BuildConfig Load(string? configPath)
|
||||||
|
{
|
||||||
|
var cfg = Defaults();
|
||||||
|
if (string.IsNullOrEmpty(configPath)) return cfg;
|
||||||
|
if (!File.Exists(configPath)) return cfg;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(configPath);
|
||||||
|
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||||
|
var fromFile = JsonSerializer.Deserialize<BuildConfig>(json, opts);
|
||||||
|
if (fromFile != null)
|
||||||
|
{
|
||||||
|
cfg = Merge(cfg, fromFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"WARNING: could not read config '{configPath}': {ex.Message}");
|
||||||
|
}
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BuildConfig Merge(BuildConfig d, BuildConfig f)
|
||||||
|
{
|
||||||
|
if (f.Site != null) d.Site = f.Site;
|
||||||
|
if (!string.IsNullOrEmpty(f.DefaultLanguage)) d.DefaultLanguage = f.DefaultLanguage;
|
||||||
|
if (f.Languages != null && f.Languages.Count > 0) d.Languages = f.Languages;
|
||||||
|
if (f.Paths != null) d.Paths = f.Paths;
|
||||||
|
if (f.Glossary != null) d.Glossary = f.Glossary;
|
||||||
|
if (!string.IsNullOrEmpty(f.GlossarySection)) d.GlossarySection = f.GlossarySection;
|
||||||
|
d.Pagefind = f.Pagefind;
|
||||||
|
if (f.Translator != null) d.Translator = f.Translator;
|
||||||
|
if (f.Environments != null && f.Environments.Count > 0) d.Environments = f.Environments;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ordered list of language codes to render.</summary>
|
||||||
|
public List<string> LanguageCodes()
|
||||||
|
{
|
||||||
|
if (Languages.Count == 0) return new() { DefaultLanguage };
|
||||||
|
return Languages.Keys.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasLanguage(string lang) => Languages.ContainsKey(lang);
|
||||||
|
}
|
||||||
+503
@@ -0,0 +1,503 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace GCBoK.WebStaticBuilder;
|
||||||
|
|
||||||
|
/// <summary>Main build orchestrator.</summary>
|
||||||
|
public static class Builder
|
||||||
|
{
|
||||||
|
public static void Build(BuildConfig cfg, EnvironmentConfig env, string envName, bool verbose, bool dryRun)
|
||||||
|
{
|
||||||
|
File.WriteAllText(Directories.BuildLogPath,
|
||||||
|
$"=== Build Started: {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} ===\n");
|
||||||
|
File.AppendAllText(Directories.BuildLogPath,
|
||||||
|
$"ENV: {envName}\nBASE_URL: {env.BaseUrl}\nCDN_ASSETS: {env.Cdn}\nROOT: {Directories.Root}\n\n");
|
||||||
|
File.WriteAllText(Directories.RunLogPath,
|
||||||
|
$"\n=== Run Started: {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} ===\n");
|
||||||
|
|
||||||
|
if (verbose)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Build configuration:");
|
||||||
|
Console.WriteLine($" ENV: {envName}");
|
||||||
|
Console.WriteLine($" BASE_URL: {env.BaseUrl}");
|
||||||
|
Console.WriteLine($" CDN_ASSETS: {env.Cdn}");
|
||||||
|
Console.WriteLine($" Content: {Directories.ContentDir}");
|
||||||
|
Console.WriteLine($" Templates: {Directories.TemplateDir}");
|
||||||
|
Console.WriteLine($" Output: {Directories.DistDir}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load UI translations (best-effort; templates fall back to keys if absent)
|
||||||
|
I18n.Load(Directories.I18nFile);
|
||||||
|
if (verbose && I18n.Loaded) Console.WriteLine($" i18n: loaded {Directories.I18nFile}");
|
||||||
|
|
||||||
|
string layout = LoadLayout();
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Directories.DistDir);
|
||||||
|
CopyAssets(verbose);
|
||||||
|
|
||||||
|
var langLinksHtml = BuildLangLinksHtml();
|
||||||
|
|
||||||
|
foreach (var lang in cfg.LanguageCodes())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var contents = ContentLoader.LoadContent(Directories.ContentDir, lang);
|
||||||
|
if (verbose) Console.WriteLine($"Rendering {contents.Count} content files for language {lang}");
|
||||||
|
File.AppendAllText(Directories.RunLogPath, $"Language: {lang}, Files: {contents.Count}\n");
|
||||||
|
|
||||||
|
var sectionsHtml = BuildSectionsHtml(contents, lang);
|
||||||
|
var navLinksHtml = BuildNavLinksHtml(contents, lang);
|
||||||
|
var sidebarNavHtml = BuildSidebarNavHtml(contents, lang);
|
||||||
|
|
||||||
|
var bannerPage = contents.FirstOrDefault(p => p.Meta.Order == 0);
|
||||||
|
var bannerHtml = "";
|
||||||
|
if (bannerPage != null)
|
||||||
|
{
|
||||||
|
bannerHtml = MarkdownProcessor.RenderMarkdown(bannerPage.Content);
|
||||||
|
bannerHtml = MarkdownProcessor.MermaidPostprocess(bannerHtml);
|
||||||
|
bannerHtml = MarkdownProcessor.TablePostprocess(bannerHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var page in contents)
|
||||||
|
{
|
||||||
|
if (!page.Meta.Render) continue;
|
||||||
|
if (page.Meta.Order == 0) continue;
|
||||||
|
|
||||||
|
var title = page.Meta.LocalizedTitle(lang, page.Meta.Title ?? Path.GetFileNameWithoutExtension(page.Path));
|
||||||
|
var desc = page.Meta.LocalizedDescription(lang, "");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(title))
|
||||||
|
title = Path.GetFileNameWithoutExtension(page.Path);
|
||||||
|
|
||||||
|
var htmlContent = MarkdownProcessor.RenderMarkdown(page.Content);
|
||||||
|
htmlContent = MarkdownProcessor.MermaidPostprocess(htmlContent);
|
||||||
|
htmlContent = MarkdownProcessor.TablePostprocess(htmlContent);
|
||||||
|
|
||||||
|
var fullBody = page.Meta.Order == 1
|
||||||
|
? bannerHtml + htmlContent
|
||||||
|
: htmlContent;
|
||||||
|
|
||||||
|
var canonical = page.Href.StartsWith("/") ? page.Href : $"/{page.Href}";
|
||||||
|
if (!canonical.EndsWith("/")) canonical += "/";
|
||||||
|
|
||||||
|
var defaultTitle = cfg.Site.Title.TryGetValue(lang, out var dt) ? dt : cfg.Site.Name;
|
||||||
|
var defaultDesc = cfg.Site.Description.TryGetValue(lang, out var dd) ? dd : "";
|
||||||
|
|
||||||
|
var seoHead = SeoHelper.RenderSeoHead(lang, title, desc, $"{env.BaseUrl}{canonical}",
|
||||||
|
env.Cdn, defaultTitle, defaultDesc);
|
||||||
|
|
||||||
|
var sidebarTocHtml = BuildSidebarTocHtml(page.Content);
|
||||||
|
var pageNavHtml = (page.Meta.Order == 1) ? "" : BuildPageNavHtml(contents, page, lang);
|
||||||
|
|
||||||
|
var pageHtml = layout
|
||||||
|
.Replace("{{seo_head}}", seoHead).Replace("{{ seo_head }}", seoHead)
|
||||||
|
.Replace("{{ json_ld }}", "").Replace("{{json_ld}}", "")
|
||||||
|
.Replace("{{sections}}", sectionsHtml).Replace("{{ sections }}", sectionsHtml)
|
||||||
|
.Replace("{{nav_links}}", navLinksHtml).Replace("{{ nav_links }}", navLinksHtml)
|
||||||
|
.Replace("{{sidebar_nav}}", sidebarNavHtml).Replace("{{ sidebar_nav }}", sidebarNavHtml)
|
||||||
|
.Replace("{{sidebar_toc}}", sidebarTocHtml).Replace("{{ sidebar_toc }}", sidebarTocHtml)
|
||||||
|
.Replace("{{page_nav}}", pageNavHtml).Replace("{{ page_nav }}", pageNavHtml)
|
||||||
|
.Replace("{{lang}}", lang).Replace("{{ lang }}", lang)
|
||||||
|
.Replace("{{lang_links}}", langLinksHtml).Replace("{{ lang_links }}", langLinksHtml)
|
||||||
|
.Replace("{{glossary_href}}", GlossaryHref(lang)).Replace("{{ glossary_href }}", GlossaryHref(lang))
|
||||||
|
.Replace("{{glossary_label}}", GlossaryLabel(lang)).Replace("{{ glossary_label }}", GlossaryLabel(lang))
|
||||||
|
.Replace("{{content}}", fullBody).Replace("{{ content }}", fullBody)
|
||||||
|
.Replace("{{body}}", fullBody).Replace("{{ body }}", fullBody);
|
||||||
|
|
||||||
|
pageHtml = I18n.Substitute(pageHtml, lang);
|
||||||
|
|
||||||
|
if (dryRun)
|
||||||
|
{
|
||||||
|
if (verbose) Console.WriteLine($" [dry-run] Would write {lang}/{page.Href}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ldir = Path.Combine(Directories.DistDir, lang);
|
||||||
|
Directory.CreateDirectory(ldir);
|
||||||
|
|
||||||
|
var slug = page.Href.Trim('/');
|
||||||
|
var outputFile = string.IsNullOrEmpty(slug) ? "index.html" : $"{slug}.html";
|
||||||
|
var outputPath = Path.Combine(ldir, outputFile);
|
||||||
|
File.WriteAllText(outputPath, pageHtml);
|
||||||
|
|
||||||
|
if (page.Meta.Order == 1)
|
||||||
|
{
|
||||||
|
var heroIndexPath = Path.Combine(ldir, "index.html");
|
||||||
|
File.WriteAllText(heroIndexPath, pageHtml);
|
||||||
|
if (verbose) Console.WriteLine($" Written {heroIndexPath} (hero landing)");
|
||||||
|
File.AppendAllText(Directories.BuildLogPath, $" Written: {heroIndexPath} (hero landing)\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verbose) Console.WriteLine($" Written {outputPath}");
|
||||||
|
File.AppendAllText(Directories.BuildLogPath, $" Written: {outputPath}\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"ERROR rendering {lang}: {ex.Message}");
|
||||||
|
File.AppendAllText(Directories.BuildLogPath, $"ERROR rendering {lang}: {ex.Message}\n{ex.StackTrace}\n\n");
|
||||||
|
File.AppendAllText(Directories.RunLogPath, $"ERROR rendering {lang}: {ex.Message}\n{ex.StackTrace}\n\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildLegalPages(cfg, env, verbose);
|
||||||
|
File.WriteAllText(Path.Combine(Directories.DistDir, "index.html"), BuildRootIndex(cfg, env.BaseUrl));
|
||||||
|
if (verbose) Console.WriteLine(" Written root index.html");
|
||||||
|
|
||||||
|
File.WriteAllText(Path.Combine(Directories.DistDir, "robots.txt"),
|
||||||
|
$"User-agent: *\nAllow: /\nSitemap: {env.BaseUrl}/sitemap.xml");
|
||||||
|
|
||||||
|
BuildSitemap(cfg, env.BaseUrl, verbose);
|
||||||
|
|
||||||
|
if (cfg.Pagefind) BuildSearchIndex(verbose);
|
||||||
|
else if (verbose) Console.WriteLine(" Pagefind disabled (config).");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string LoadLayout()
|
||||||
|
{
|
||||||
|
var layoutPath = Path.Combine(Directories.TemplateDir, "layout.html");
|
||||||
|
if (File.Exists(layoutPath)) return File.ReadAllText(layoutPath);
|
||||||
|
Console.WriteLine($"WARNING: Layout file not found: {layoutPath}");
|
||||||
|
return "<html><body>{{body}}</body></html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CopyAssets(bool verbose)
|
||||||
|
{
|
||||||
|
var srcDir = Directories.AssetsDir;
|
||||||
|
var dstDir = Path.Combine(Directories.DistDir, "assets");
|
||||||
|
if (!Directory.Exists(srcDir))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"WARNING: Assets directory not found: {srcDir}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Directory.Exists(dstDir)) Directory.Delete(dstDir, recursive: true);
|
||||||
|
Directory.CreateDirectory(dstDir);
|
||||||
|
foreach (var file in Directory.GetFiles(srcDir, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
var rel = Path.GetRelativePath(srcDir, file);
|
||||||
|
var target = Path.Combine(dstDir, rel);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
|
||||||
|
File.Copy(file, target, overwrite: true);
|
||||||
|
}
|
||||||
|
if (verbose)
|
||||||
|
{
|
||||||
|
var count = Directory.GetFiles(dstDir, "*", SearchOption.AllDirectories).Length;
|
||||||
|
Console.WriteLine($" Copied {count} asset files to {dstDir}");
|
||||||
|
}
|
||||||
|
File.AppendAllText(Directories.BuildLogPath, $" Assets copied to: {dstDir}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildLangLinksHtml()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var kv in Directories.Config.Languages)
|
||||||
|
{
|
||||||
|
var lang = kv.Key;
|
||||||
|
var li = kv.Value;
|
||||||
|
var label = li.Label ?? li.Name;
|
||||||
|
var code = (li.HrefLang ?? lang).ToUpperInvariant();
|
||||||
|
sb.AppendLine($" <li><a href=\"/{lang}/\" hreflang=\"{li.HrefLang ?? lang}\"><span>{HtmlEncode(label)}</span> <span class=\"lang-dropdown__code\">{code}</span></a></li>");
|
||||||
|
}
|
||||||
|
return sb.ToString().Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildNavLinksHtml(List<PageInfo> contents, string lang)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var page in contents)
|
||||||
|
{
|
||||||
|
if (!page.Meta.Navbar) continue;
|
||||||
|
if (page.Meta.Order == 0 || page.Meta.Order == 1) continue;
|
||||||
|
var title = page.Meta.LocalizedTitle(lang, Path.GetFileNameWithoutExtension(page.Path));
|
||||||
|
var navLabel = page.Meta.Eyebrow ?? title;
|
||||||
|
if (navLabel.Contains(" — "))
|
||||||
|
navLabel = navLabel.Substring(navLabel.IndexOf(" — ") + 3);
|
||||||
|
var slug = page.Href.Trim('/');
|
||||||
|
var href = string.IsNullOrEmpty(slug) ? "index.html" : $"{slug}.html";
|
||||||
|
sb.AppendLine($" <a class=\"nav-gcbok__link\" href=\"{href}\">{HtmlEncode(navLabel)}</a>");
|
||||||
|
}
|
||||||
|
return sb.ToString().Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildSidebarNavHtml(List<PageInfo> contents, string lang)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var navPages = contents.Where(p => p.Meta.Navbar && p.Meta.Order != 0 && p.Meta.Order != 1)
|
||||||
|
.OrderBy(p => p.Meta.Order ?? 999).ToList();
|
||||||
|
var heroPage = contents.FirstOrDefault(p => p.Meta.Order == 1);
|
||||||
|
if (heroPage != null)
|
||||||
|
sb.AppendLine(" <li><a href=\"index.html\"><span class=\"nav-idx\">★</span> Startseite</a></li>");
|
||||||
|
|
||||||
|
foreach (var page in navPages)
|
||||||
|
{
|
||||||
|
var navLabel = page.Meta.Eyebrow ?? page.Meta.LocalizedTitle(lang, page.Meta.Title ?? "");
|
||||||
|
if (navLabel.Contains(" — "))
|
||||||
|
navLabel = navLabel.Substring(navLabel.IndexOf(" — ") + 3);
|
||||||
|
var slug = page.Href.Trim('/');
|
||||||
|
var href = string.IsNullOrEmpty(slug) ? "index.html" : $"{slug}.html";
|
||||||
|
var idx = page.Meta.Order?.ToString() ?? "·";
|
||||||
|
sb.AppendLine($" <li><a href=\"{href}\"><span class=\"nav-idx\">{idx}</span> {HtmlEncode(navLabel)}</a></li>");
|
||||||
|
}
|
||||||
|
return sb.ToString().Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildSidebarTocHtml(string markdownContent)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var lines = markdownContent.Split('\n');
|
||||||
|
var inCodeBlock = false;
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
if (line.TrimStart().StartsWith("```"))
|
||||||
|
{
|
||||||
|
inCodeBlock = !inCodeBlock;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inCodeBlock) continue;
|
||||||
|
var trimmed = line.TrimStart();
|
||||||
|
if (trimmed.StartsWith("## ") && !trimmed.StartsWith("### "))
|
||||||
|
{
|
||||||
|
var headingText = trimmed.Substring(3).Trim();
|
||||||
|
headingText = Regex.Replace(headingText, @"\*\*(.+?)\*\*", "$1");
|
||||||
|
headingText = Regex.Replace(headingText, @"\*(.+?)\*", "$1");
|
||||||
|
headingText = Regex.Replace(headingText, @"`(.+?)`", "$1");
|
||||||
|
var slug = Regex.Replace(headingText.ToLower(), @"[^a-z0-9äöüß\-]+", "-").Trim('-').TrimEnd(':');
|
||||||
|
if (!string.IsNullOrEmpty(slug))
|
||||||
|
sb.AppendLine($" <li><a href=\"#{slug}\">{HtmlEncode(headingText)}</a></li>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.ToString().Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildPageNavHtml(List<PageInfo> contents, PageInfo currentPage, string lang)
|
||||||
|
{
|
||||||
|
var navPages = contents.Where(p => p.Meta.Navbar && p.Meta.Order != 0 && p.Meta.Order != 1)
|
||||||
|
.OrderBy(p => p.Meta.Order ?? 999).ToList();
|
||||||
|
var currentIndex = navPages.FindIndex(p => p.Path == currentPage.Path);
|
||||||
|
if (currentIndex < 0) return "";
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("<nav class=\"page-nav\" aria-label=\"Seitennavigation\">");
|
||||||
|
if (currentIndex > 0)
|
||||||
|
{
|
||||||
|
var prev = navPages[currentIndex - 1];
|
||||||
|
var prevLabel = (prev.Meta.Eyebrow ?? prev.Meta.LocalizedTitle(lang, prev.Meta.Title ?? ""));
|
||||||
|
if (prevLabel.Contains(" — ")) prevLabel = prevLabel.Substring(prevLabel.IndexOf(" — ") + 3);
|
||||||
|
sb.AppendLine($" <a class=\"page-nav__link\" href=\"{prev.Href.Trim('/')}.html\">");
|
||||||
|
sb.AppendLine($" <span class=\"page-nav__label\">← Vorherige</span>");
|
||||||
|
sb.AppendLine($" <span class=\"page-nav__title\">{HtmlEncode(prevLabel)}</span>");
|
||||||
|
sb.AppendLine(" </a>");
|
||||||
|
}
|
||||||
|
else sb.AppendLine(" <span class=\"page-nav__link\"> </span>");
|
||||||
|
|
||||||
|
if (currentIndex < navPages.Count - 1)
|
||||||
|
{
|
||||||
|
var next = navPages[currentIndex + 1];
|
||||||
|
var nextLabel = (next.Meta.Eyebrow ?? next.Meta.LocalizedTitle(lang, next.Meta.Title ?? ""));
|
||||||
|
if (nextLabel.Contains(" — ")) nextLabel = nextLabel.Substring(nextLabel.IndexOf(" — ") + 3);
|
||||||
|
sb.AppendLine($" <a class=\"page-nav__link page-nav__link--next\" href=\"{next.Href.Trim('/')}.html\">");
|
||||||
|
sb.AppendLine($" <span class=\"page-nav__label\">Nächste →</span>");
|
||||||
|
sb.AppendLine($" <span class=\"page-nav__title\">{HtmlEncode(nextLabel)}</span>");
|
||||||
|
sb.AppendLine(" </a>");
|
||||||
|
}
|
||||||
|
else sb.AppendLine(" <span class=\"page-nav__link page-nav__link--next\"> </span>");
|
||||||
|
|
||||||
|
sb.AppendLine("</nav>");
|
||||||
|
return sb.ToString().Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildSectionsHtml(List<PageInfo> contents, string lang)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var page in contents)
|
||||||
|
{
|
||||||
|
if (!page.Meta.Navbar) continue;
|
||||||
|
var title = page.Meta.LocalizedTitle(lang, Path.GetFileNameWithoutExtension(page.Path));
|
||||||
|
sb.AppendLine($" <a href=\"{page.Href}\" data-lang=\"{lang}\">{HtmlEncode(title)}</a>");
|
||||||
|
}
|
||||||
|
return sb.ToString().Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildLegalPages(BuildConfig cfg, EnvironmentConfig env, bool verbose)
|
||||||
|
{
|
||||||
|
var legalDir = Directories.LegalDir;
|
||||||
|
if (!Directory.Exists(legalDir)) return;
|
||||||
|
var layout = LoadLayout();
|
||||||
|
var langLinksHtml = BuildLangLinksHtml();
|
||||||
|
|
||||||
|
foreach (var lang in cfg.LanguageCodes())
|
||||||
|
{
|
||||||
|
var lld = Path.Combine(Directories.DistDir, lang);
|
||||||
|
Directory.CreateDirectory(lld);
|
||||||
|
var contents = ContentLoader.LoadContent(Directories.ContentDir, lang);
|
||||||
|
var navLinksHtml = BuildNavLinksHtml(contents, lang);
|
||||||
|
var sidebarNavHtml = BuildSidebarNavHtml(contents, lang);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var lf in Directory.GetFiles(legalDir, "*.md"))
|
||||||
|
{
|
||||||
|
var bn = Path.GetFileName(lf);
|
||||||
|
if (bn.Contains("."))
|
||||||
|
{
|
||||||
|
var match = Regex.Match(bn, @".+\.(de|en|vi)\.md$");
|
||||||
|
if (match.Success && match.Groups[1].Value != lang) continue;
|
||||||
|
}
|
||||||
|
var text = File.ReadAllText(lf);
|
||||||
|
var (meta, content) = ContentLoader.ParseFrontmatter(text);
|
||||||
|
var title = meta.LocalizedTitle(lang, Path.GetFileNameWithoutExtension(bn));
|
||||||
|
var htmlContent = MarkdownProcessor.RenderMarkdown(content);
|
||||||
|
htmlContent = MarkdownProcessor.MermaidPostprocess(htmlContent);
|
||||||
|
htmlContent = MarkdownProcessor.TablePostprocess(htmlContent);
|
||||||
|
|
||||||
|
var baseName = Regex.Replace(bn, @"\.(de|en|vi)\.md$", ".html").Replace(".md", ".html");
|
||||||
|
var canonical = $"/{Path.GetFileNameWithoutExtension(baseName)}/";
|
||||||
|
var defaultTitle = cfg.Site.Title.TryGetValue(lang, out var dt) ? dt : cfg.Site.Name;
|
||||||
|
var defaultDesc = cfg.Site.Description.TryGetValue(lang, out var dd) ? dd : "";
|
||||||
|
var seoHead = SeoHelper.RenderSeoHead(lang, title, "", $"{env.BaseUrl}{canonical}",
|
||||||
|
env.Cdn, defaultTitle, defaultDesc);
|
||||||
|
var sidebarTocHtml = BuildSidebarTocHtml(content);
|
||||||
|
|
||||||
|
var pageHtml = layout
|
||||||
|
.Replace("{{seo_head}}", seoHead).Replace("{{ seo_head }}", seoHead)
|
||||||
|
.Replace("{{ json_ld }}", "").Replace("{{json_ld}}", "")
|
||||||
|
.Replace("{{sections}}", "").Replace("{{ sections }}", "")
|
||||||
|
.Replace("{{nav_links}}", navLinksHtml).Replace("{{ nav_links }}", navLinksHtml)
|
||||||
|
.Replace("{{sidebar_nav}}", sidebarNavHtml).Replace("{{ sidebar_nav }}", sidebarNavHtml)
|
||||||
|
.Replace("{{sidebar_toc}}", sidebarTocHtml).Replace("{{ sidebar_toc }}", sidebarTocHtml)
|
||||||
|
.Replace("{{page_nav}}", "").Replace("{{ page_nav }}", "")
|
||||||
|
.Replace("{{lang}}", lang).Replace("{{ lang }}", lang)
|
||||||
|
.Replace("{{lang_links}}", langLinksHtml).Replace("{{ lang_links }}", langLinksHtml)
|
||||||
|
.Replace("{{glossary_href}}", GlossaryHref(lang)).Replace("{{ glossary_href }}", GlossaryHref(lang))
|
||||||
|
.Replace("{{glossary_label}}", GlossaryLabel(lang)).Replace("{{ glossary_label }}", GlossaryLabel(lang))
|
||||||
|
.Replace("{{content}}", htmlContent).Replace("{{ content }}", htmlContent)
|
||||||
|
.Replace("{{body}}", htmlContent).Replace("{{ body }}", htmlContent);
|
||||||
|
pageHtml = I18n.Substitute(pageHtml, lang);
|
||||||
|
|
||||||
|
var outputPath = Path.Combine(lld, baseName);
|
||||||
|
File.WriteAllText(outputPath, pageHtml);
|
||||||
|
if (verbose) Console.WriteLine($" Written {outputPath}");
|
||||||
|
File.AppendAllText(Directories.BuildLogPath, $" Legal: {outputPath}\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"ERROR rendering legal {lang}: {ex.Message}");
|
||||||
|
File.AppendAllText(Directories.BuildLogPath, $"ERROR rendering legal {lang}: {ex.Message}\n{ex.StackTrace}\n\n");
|
||||||
|
File.AppendAllText(Directories.RunLogPath, $"ERROR rendering legal {lang}: {ex.Message}\n{ex.StackTrace}\n\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildSitemap(BuildConfig cfg, string baseUrl, bool verbose)
|
||||||
|
{
|
||||||
|
var urls = new List<string>();
|
||||||
|
foreach (var lang in cfg.LanguageCodes())
|
||||||
|
{
|
||||||
|
urls.Add($"{baseUrl}/{lang}/");
|
||||||
|
var contents = ContentLoader.LoadContent(Directories.ContentDir, lang);
|
||||||
|
foreach (var page in contents)
|
||||||
|
if (page.Meta.Render) urls.Add($"{baseUrl}{page.Href}");
|
||||||
|
}
|
||||||
|
var sitemap = @"<?xml version=""1.0"" encoding=""UTF-8""?>
|
||||||
|
<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">
|
||||||
|
{0}
|
||||||
|
</urlset>";
|
||||||
|
var urlEntries = string.Join("\n", urls.Select(u => $" <url><loc>{u}</loc></url>"));
|
||||||
|
File.WriteAllText(Path.Combine(Directories.DistDir, "sitemap.xml"), string.Format(sitemap, urlEntries));
|
||||||
|
if (verbose) Console.WriteLine(" Written sitemap.xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GlossaryHref(string lang)
|
||||||
|
{
|
||||||
|
if (Directories.Config.Glossary.Href.TryGetValue(lang, out var h))
|
||||||
|
return h.EndsWith(".html") ? h : h + ".html";
|
||||||
|
return lang == "de" ? "glossar.html" : $"{Directories.Config.GlossarySection}.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GlossaryLabel(string lang)
|
||||||
|
{
|
||||||
|
if (Directories.Config.Glossary.Label.TryGetValue(lang, out var l)) return l;
|
||||||
|
return lang == "de" ? "Glossar" : lang == "en" ? "Glossary" : "Bảng thuật ngữ";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildSearchIndex(bool verbose)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "npx",
|
||||||
|
Arguments = $"pagefind --site \"{Directories.DistDir}\"",
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
using var proc = Process.Start(psi)
|
||||||
|
?? throw new InvalidOperationException("Could not start npx.");
|
||||||
|
var sbOut = new StringBuilder();
|
||||||
|
var sbErr = new StringBuilder();
|
||||||
|
proc.OutputDataReceived += (_, e) => { if (e.Data != null) sbOut.AppendLine(e.Data); };
|
||||||
|
proc.ErrorDataReceived += (_, e) => { if (e.Data != null) sbErr.AppendLine(e.Data); };
|
||||||
|
proc.BeginOutputReadLine();
|
||||||
|
proc.BeginErrorReadLine();
|
||||||
|
proc.WaitForExit();
|
||||||
|
File.AppendAllText(Directories.BuildLogPath, $" Pagefind (exit {proc.ExitCode}): {sbOut.ToString().Trim()}\n");
|
||||||
|
if (proc.ExitCode != 0)
|
||||||
|
File.AppendAllText(Directories.BuildLogPath, $" Pagefind stderr: {sbErr.ToString().Trim()}\n");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
File.AppendAllText(Directories.BuildLogPath, $" Pagefind skipped (search index not built): {ex.Message}\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string HtmlEncode(string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text)) return "";
|
||||||
|
return text.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildRootIndex(BuildConfig cfg, string baseUrl)
|
||||||
|
{
|
||||||
|
var supportedArray = string.Join(",", cfg.LanguageCodes().Select(l => $"\"{l}\""));
|
||||||
|
var defaultLang = cfg.DefaultLanguage;
|
||||||
|
var links = string.Join(" | ", cfg.LanguageCodes().Select(l =>
|
||||||
|
{
|
||||||
|
var label = cfg.Languages.TryGetValue(l, out var li) ? (li.Label ?? li.Name) : l;
|
||||||
|
return $"<a href=\"/{l}/\">{label}</a>";
|
||||||
|
}));
|
||||||
|
return $@"<!DOCTYPE html>
|
||||||
|
<html lang=""{defaultLang}"">
|
||||||
|
<head>
|
||||||
|
<meta charset=""UTF-8"">
|
||||||
|
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
|
||||||
|
<title>{HtmlEncode(cfg.Site.Name)}</title>
|
||||||
|
<script>
|
||||||
|
(function() {{
|
||||||
|
var supported = [{supportedArray}];
|
||||||
|
var defaultLang = ""{defaultLang}"";
|
||||||
|
var lang = defaultLang;
|
||||||
|
if (navigator.languages && navigator.languages.length) {{
|
||||||
|
for (var i = 0; i < navigator.languages.length; i++) {{
|
||||||
|
var pref = navigator.languages[i].toLowerCase().split(""-"")[0];
|
||||||
|
if (supported.indexOf(pref) !== -1) {{ lang = pref; break; }}
|
||||||
|
}}
|
||||||
|
}} else if (navigator.language) {{
|
||||||
|
var pref = navigator.language.toLowerCase().split(""-"")[0];
|
||||||
|
if (supported.indexOf(pref) !== -1) lang = pref;
|
||||||
|
}}
|
||||||
|
window.location.replace(""/"" + lang + ""/"");
|
||||||
|
}})();
|
||||||
|
</script>
|
||||||
|
<meta http-equiv=""refresh"" content=""0; url=/{defaultLang}/"">
|
||||||
|
<link rel=""canonical"" href=""{baseUrl}/{defaultLang}/"">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Redirecting to {links}</p>
|
||||||
|
</body>
|
||||||
|
</html>";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
using YamlDotNet.Serialization.NamingConventions;
|
||||||
|
|
||||||
|
namespace GCBoK.WebStaticBuilder;
|
||||||
|
|
||||||
|
/// <summary>Content loading and YAML frontmatter parsing.</summary>
|
||||||
|
public static class ContentLoader
|
||||||
|
{
|
||||||
|
public static List<PageInfo> LoadContent(string contentDir, string lang)
|
||||||
|
{
|
||||||
|
var results = new List<PageInfo>();
|
||||||
|
|
||||||
|
foreach (var filepath in Directory.GetFiles(contentDir, "*.md", SearchOption.TopDirectoryOnly))
|
||||||
|
{
|
||||||
|
var nameWithoutExt = Path.GetFileNameWithoutExtension(filepath);
|
||||||
|
|
||||||
|
string? fileLang = null;
|
||||||
|
if (nameWithoutExt.Contains("."))
|
||||||
|
{
|
||||||
|
var parts = nameWithoutExt.Split('.');
|
||||||
|
if (parts.Length >= 2)
|
||||||
|
{
|
||||||
|
var lastPart = parts[^1].ToLowerInvariant();
|
||||||
|
if (lastPart == "de" || lastPart == "en" || lastPart == "vi")
|
||||||
|
{
|
||||||
|
fileLang = lastPart;
|
||||||
|
if (fileLang != lang) continue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = File.ReadAllText(filepath);
|
||||||
|
var (meta, content) = ParseFrontmatter(text);
|
||||||
|
|
||||||
|
var langs = meta.Langs ?? "";
|
||||||
|
if (!string.IsNullOrEmpty(langs) && !langs.Split(',').Select(l => l.Trim()).Contains(lang))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (fileLang == null && lang != "de")
|
||||||
|
{
|
||||||
|
var baseName = nameWithoutExt;
|
||||||
|
var langSpecificPath = Path.Combine(contentDir, $"{baseName}.{lang}.md");
|
||||||
|
if (File.Exists(langSpecificPath))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.Add(new PageInfo(filepath, meta, GetHref(meta), content));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.OrderBy(x => x.Meta.Order ?? 999).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (PageMeta Meta, string Content) ParseFrontmatter(string text)
|
||||||
|
{
|
||||||
|
var lines = text.Split('\n').ToList();
|
||||||
|
|
||||||
|
if (lines.Count < 2 || !lines[0].Trim().StartsWith("---"))
|
||||||
|
return (new PageMeta(), text);
|
||||||
|
|
||||||
|
var yamlLines = new List<string>();
|
||||||
|
int i = 1;
|
||||||
|
while (i < lines.Count && lines[i].Trim() != "---")
|
||||||
|
{
|
||||||
|
yamlLines.Add(lines[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < lines.Count) i++;
|
||||||
|
|
||||||
|
var yamlText = string.Join("\n", yamlLines);
|
||||||
|
var content = string.Join("\n", lines.Skip(i)).Trim();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var deserializer = new DeserializerBuilder()
|
||||||
|
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||||
|
.IgnoreUnmatchedProperties()
|
||||||
|
.Build();
|
||||||
|
var meta = deserializer.Deserialize<PageMeta>(yamlText) ?? new PageMeta();
|
||||||
|
return (meta, content);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to parse frontmatter: {ex.Message}");
|
||||||
|
return (new PageMeta(), text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetHref(PageMeta meta)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(meta.Href))
|
||||||
|
return meta.Href;
|
||||||
|
|
||||||
|
var title = meta.Title ?? "";
|
||||||
|
var slug = Regex.Replace(
|
||||||
|
title.ToLower(), @"[^a-z0-9äöüß\-]+", "-").Trim('-');
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(slug))
|
||||||
|
slug = "page";
|
||||||
|
|
||||||
|
return $"/{slug}/";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
namespace GCBoK.WebStaticBuilder;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolved filesystem paths for a build, derived from <see cref="BuildConfig.Paths"/>
|
||||||
|
/// relative to the site root. Generic — no longer tied to the GCBoK <c>www/</c> layout.
|
||||||
|
/// </summary>
|
||||||
|
public static class Directories
|
||||||
|
{
|
||||||
|
public static string Root { get; private set; } = AppContext.BaseDirectory;
|
||||||
|
public static string BuildDir { get; private set; } = AppContext.BaseDirectory;
|
||||||
|
|
||||||
|
public static string ContentDir => Resolve(Config.Paths.Content);
|
||||||
|
public static string TemplateDir => Resolve(Config.Paths.Templates);
|
||||||
|
public static string AssetsDir => Resolve(Config.Paths.Assets);
|
||||||
|
public static string DistDir => Resolve(Config.Paths.Output);
|
||||||
|
public static string LegalDir => Resolve(Path.Combine(Config.Paths.Content, Config.Paths.Legal));
|
||||||
|
public static string I18nFile => Resolve(Config.Paths.I18n);
|
||||||
|
|
||||||
|
public static BuildConfig Config { get; private set; } = BuildConfig.Defaults();
|
||||||
|
|
||||||
|
private static string Resolve(string p)
|
||||||
|
{
|
||||||
|
if (Path.IsPathRooted(p)) return p;
|
||||||
|
return Path.GetFullPath(Path.Combine(Root, p));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Initialise paths for a build.</summary>
|
||||||
|
public static void Init(BuildConfig config, string rootDir)
|
||||||
|
{
|
||||||
|
Config = config;
|
||||||
|
Root = Path.GetFullPath(rootDir);
|
||||||
|
BuildDir = Root;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string BuildLogPath => Path.Combine(BuildDir, "build_log.txt");
|
||||||
|
public static string RunLogPath => Path.Combine(BuildDir, "run_log.txt");
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace GCBoK.WebStaticBuilder;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UI localization. Loads an <c>i18n.json</c> dictionary and substitutes
|
||||||
|
/// <c>{{ i18n.<namespace>.<key> }}</c> placeholders in templates.
|
||||||
|
/// Fallback language is English. Ported from the legacy <c>i18n.py</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static class I18n
|
||||||
|
{
|
||||||
|
private static JsonNode? _root;
|
||||||
|
private static readonly Regex Placeholder =
|
||||||
|
new(@"\{\{\s*i18n\.([\w.\-]+)\s*\}\}", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public static void Load(string file)
|
||||||
|
{
|
||||||
|
_root = null;
|
||||||
|
if (!File.Exists(file)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(file);
|
||||||
|
_root = JsonNode.Parse(json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"WARNING: could not load i18n file '{file}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Loaded => _root != null;
|
||||||
|
|
||||||
|
/// <summary>Resolve a dotted key path for a language (fallback: English, then the key itself).</summary>
|
||||||
|
public static string Get(string lang, string keyPath)
|
||||||
|
{
|
||||||
|
var value = Resolve(lang, keyPath);
|
||||||
|
if (value != null) return value;
|
||||||
|
if (lang != "en")
|
||||||
|
{
|
||||||
|
value = Resolve("en", keyPath);
|
||||||
|
if (value != null) return value;
|
||||||
|
}
|
||||||
|
return keyPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Resolve(string lang, string keyPath)
|
||||||
|
{
|
||||||
|
if (_root == null) return null;
|
||||||
|
JsonNode? node = _root[lang];
|
||||||
|
if (node == null) return null;
|
||||||
|
foreach (var part in keyPath.Split('.'))
|
||||||
|
{
|
||||||
|
node = node[part];
|
||||||
|
if (node == null) return null;
|
||||||
|
}
|
||||||
|
return node.GetValueKind() == JsonValueKind.String ? node.GetValue<string>() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Replace all <c>{{ i18n.* }}</c> placeholders in a template for the given language.</summary>
|
||||||
|
public static string Substitute(string template, string lang)
|
||||||
|
{
|
||||||
|
if (_root == null) return template;
|
||||||
|
return Placeholder.Replace(template, m => Get(lang, m.Groups[1].Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using Markdig;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace GCBoK.WebStaticBuilder;
|
||||||
|
|
||||||
|
/// <summary>Markdown processing with Markdig and post-processing.</summary>
|
||||||
|
public static class MarkdownProcessor
|
||||||
|
{
|
||||||
|
private static readonly Lazy<MarkdownPipeline> Pipeline = new(() =>
|
||||||
|
new MarkdownPipelineBuilder().UseAdvancedExtensions().Build());
|
||||||
|
|
||||||
|
public static string RenderMarkdown(string content)
|
||||||
|
{
|
||||||
|
var pipeline = Pipeline.Value;
|
||||||
|
return Markdown.ToHtml(content, pipeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string MermaidPostprocess(string html)
|
||||||
|
{
|
||||||
|
var pattern1 = @"<pre><code class=""language-mermaid"">(.*?)</code></pre>";
|
||||||
|
var regex1 = new Regex(pattern1, RegexOptions.Singleline);
|
||||||
|
html = regex1.Replace(html, m =>
|
||||||
|
{
|
||||||
|
var code = m.Groups[1].Value
|
||||||
|
.Replace("<", "<").Replace(">", ">").Replace("&", "&");
|
||||||
|
return $"<div class=\"mermaid\">{code}</div>";
|
||||||
|
});
|
||||||
|
|
||||||
|
var pattern2 = @"<pre class=""mermaid"">(.*?)</pre>";
|
||||||
|
var regex2 = new Regex(pattern2, RegexOptions.Singleline);
|
||||||
|
html = regex2.Replace(html, m =>
|
||||||
|
{
|
||||||
|
var code = m.Groups[1].Value
|
||||||
|
.Replace("<", "<").Replace(">", ">").Replace("&", "&");
|
||||||
|
return $"<div class=\"mermaid\">{code}</div>";
|
||||||
|
});
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string TablePostprocess(string html)
|
||||||
|
{
|
||||||
|
return html.Replace("<table>", "<table class=\"gc-table\">");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
|
namespace GCBoK.WebStaticBuilder;
|
||||||
|
|
||||||
|
/// <summary>Metadata parsed from a Markdown file's YAML frontmatter.</summary>
|
||||||
|
public class PageMeta
|
||||||
|
{
|
||||||
|
public string? Section { get; set; }
|
||||||
|
public int? Order { get; set; }
|
||||||
|
public string? Eyebrow { get; set; }
|
||||||
|
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public string? TitleDe { get; set; }
|
||||||
|
public string? TitleEn { get; set; }
|
||||||
|
public string? TitleVi { get; set; }
|
||||||
|
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string? DescriptionDe { get; set; }
|
||||||
|
public string? DescriptionEn { get; set; }
|
||||||
|
public string? DescriptionVi { get; set; }
|
||||||
|
|
||||||
|
public string? Href { get; set; }
|
||||||
|
public string? Langs { get; set; }
|
||||||
|
public bool Render { get; set; } = true;
|
||||||
|
public bool Navbar { get; set; } = true;
|
||||||
|
public string? Intro { get; set; }
|
||||||
|
public string? Positioning { get; set; }
|
||||||
|
public string? Publisher { get; set; }
|
||||||
|
public string? Anchor { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A loaded content page.</summary>
|
||||||
|
public record PageInfo(string Path, PageMeta Meta, string Href, string Content);
|
||||||
|
|
||||||
|
/// <summary>Helpers to resolve language-specific metadata values.</summary>
|
||||||
|
public static class PageMetaExtensions
|
||||||
|
{
|
||||||
|
public static string LocalizedTitle(this PageMeta meta, string lang, string fallback)
|
||||||
|
{
|
||||||
|
var v = lang.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"de" => meta.TitleDe ?? meta.Title,
|
||||||
|
"en" => meta.TitleEn ?? meta.Title,
|
||||||
|
"vi" => meta.TitleVi ?? meta.Title,
|
||||||
|
_ => meta.Title
|
||||||
|
};
|
||||||
|
return string.IsNullOrEmpty(v) ? fallback : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string LocalizedDescription(this PageMeta meta, string lang, string fallback)
|
||||||
|
{
|
||||||
|
var v = lang.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"de" => meta.DescriptionDe ?? meta.Description,
|
||||||
|
"en" => meta.DescriptionEn ?? meta.Description,
|
||||||
|
"vi" => meta.DescriptionVi ?? meta.Description,
|
||||||
|
_ => meta.Description
|
||||||
|
};
|
||||||
|
return string.IsNullOrEmpty(v) ? fallback : v;
|
||||||
|
}
|
||||||
|
}
|
||||||
+210
@@ -0,0 +1,210 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using GCBoK.WebStaticBuilder;
|
||||||
|
|
||||||
|
var argsList = args.ToList();
|
||||||
|
|
||||||
|
bool Has(string f) => argsList.Contains(f) || argsList.Contains(f switch { "--verbose" => "-v", "--dry-run" => "-n", _ => f });
|
||||||
|
string? Val(string f)
|
||||||
|
{
|
||||||
|
var i = argsList.FindIndex(a => a == f);
|
||||||
|
return i >= 0 && i + 1 < argsList.Count ? argsList[i + 1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var verbose = Has("--verbose") || Has("-v");
|
||||||
|
var dryRun = Has("--dry-run") || Has("-n");
|
||||||
|
|
||||||
|
// Resolve config file + site root
|
||||||
|
var configPath = Val("--config");
|
||||||
|
var root = Val("--root") ?? Directory.GetCurrentDirectory();
|
||||||
|
if (!string.IsNullOrEmpty(configPath))
|
||||||
|
root = Path.GetFullPath(Path.GetDirectoryName(Path.GetFullPath(configPath))!);
|
||||||
|
|
||||||
|
var cfg = BuildConfig.Load(string.IsNullOrEmpty(configPath) ? Path.Combine(root, "webstatic.json") : configPath);
|
||||||
|
|
||||||
|
// CLI overrides
|
||||||
|
if (Val("--content") is { } c) cfg.Paths.Content = c;
|
||||||
|
if (Val("--templates") is { } t) cfg.Paths.Templates = t;
|
||||||
|
if (Val("--assets") is { } a) cfg.Paths.Assets = a;
|
||||||
|
if (Val("--output") is { } o) cfg.Paths.Output = o;
|
||||||
|
if (Val("--default-lang") is { } dl) cfg.DefaultLanguage = dl;
|
||||||
|
if (Val("--langs") is { } langs)
|
||||||
|
{
|
||||||
|
var wanted = langs.Split(',').Select(x => x.Trim()).Where(x => x.Length > 0).ToList();
|
||||||
|
if (wanted.Count > 0)
|
||||||
|
{
|
||||||
|
var filtered = new Dictionary<string, LanguageConfig>();
|
||||||
|
foreach (var l in wanted)
|
||||||
|
if (cfg.Languages.TryGetValue(l, out var lc)) filtered[l] = lc;
|
||||||
|
else filtered[l] = new LanguageConfig { Name = l, OgLocale = $"{l}_{l.ToUpperInvariant()}" };
|
||||||
|
cfg.Languages = filtered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Val("--base-url") is { } bu) { /* applied to chosen env below */ }
|
||||||
|
if (Val("--cdn") is { } cdn) { /* applied to chosen env below */ }
|
||||||
|
|
||||||
|
Directories.Init(cfg, root);
|
||||||
|
|
||||||
|
// Subcommand: translate
|
||||||
|
if (argsList.Contains("translate"))
|
||||||
|
{
|
||||||
|
if (Has("--draft"))
|
||||||
|
await TranslationService.GenerateDrafts(cfg);
|
||||||
|
else
|
||||||
|
await TranslationService.CheckMissing(cfg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subcommand: serve
|
||||||
|
if (argsList.Contains("serve"))
|
||||||
|
{
|
||||||
|
var port = int.TryParse(Val("--port"), out var p) ? p : 8000;
|
||||||
|
await ServeAsync(Directories.DistDir, port);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subcommand: init <name>
|
||||||
|
if (argsList.Contains("init"))
|
||||||
|
{
|
||||||
|
var nameIdx = argsList.IndexOf("init") + 1;
|
||||||
|
var siteName = nameIdx < argsList.Count ? argsList[nameIdx] : "mysite";
|
||||||
|
InitSite(siteName, root);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build
|
||||||
|
var envName = "work";
|
||||||
|
if (Has("--preview")) envName = "work";
|
||||||
|
if (Has("--work")) envName = "work";
|
||||||
|
if (Has("--review")) envName = "review";
|
||||||
|
if (Has("--staging")) envName = "staging";
|
||||||
|
if (Has("--deploy")) envName = "deploy";
|
||||||
|
if (Val("--env") is { } ev) envName = ev;
|
||||||
|
|
||||||
|
var env = cfg.Environments.TryGetValue(envName, out var e)
|
||||||
|
? e
|
||||||
|
: new EnvironmentConfig { BaseUrl = "http://localhost:8000", Cdn = "/assets" };
|
||||||
|
|
||||||
|
if (Val("--base-url") is { } bu2) env.BaseUrl = bu2;
|
||||||
|
if (Val("--cdn") is { } cdn2) env.Cdn = cdn2;
|
||||||
|
|
||||||
|
if (verbose) Console.WriteLine($"GCBoK.WebStaticBuilder — building (env={envName})");
|
||||||
|
Builder.Build(cfg, env, envName, verbose, dryRun);
|
||||||
|
Console.WriteLine("Build completed successfully.");
|
||||||
|
|
||||||
|
static string FindExampleDir()
|
||||||
|
{
|
||||||
|
var candidates = new[]
|
||||||
|
{
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "webstatic.example"),
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "webstatic.example"),
|
||||||
|
"/opt/GitCover/webstatic/webstatic.example",
|
||||||
|
};
|
||||||
|
foreach (var c in candidates)
|
||||||
|
if (Directory.Exists(Path.GetFullPath(c))) return Path.GetFullPath(c);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void InitSite(string name, string rootDir)
|
||||||
|
{
|
||||||
|
var example = FindExampleDir();
|
||||||
|
if (string.IsNullOrEmpty(example))
|
||||||
|
{
|
||||||
|
Console.WriteLine("ERROR: webstatic.example template not found (expected next to the tool).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var target = Path.Combine(rootDir, name);
|
||||||
|
if (Directory.Exists(target))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"ERROR: target '{target}' already exists.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CopyDir(example, target);
|
||||||
|
Console.WriteLine($"Initialized site scaffold at {target}");
|
||||||
|
Console.WriteLine($"Next: cd {name} && webstatic build --env work");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CopyDir(string src, string dst)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(dst);
|
||||||
|
foreach (var dir in Directory.GetDirectories(src, "*", SearchOption.AllDirectories))
|
||||||
|
Directory.CreateDirectory(dir.Replace(src, dst));
|
||||||
|
foreach (var file in Directory.GetFiles(src, "*", SearchOption.AllDirectories))
|
||||||
|
File.Copy(file, file.Replace(src, dst), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task ServeAsync(string distDir, int port)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(distDir))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"ERROR: output directory '{distDir}' not found. Run a build first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var prefix = $"http://localhost:{port}/";
|
||||||
|
using var listener = new HttpListener();
|
||||||
|
listener.Prefixes.Add(prefix);
|
||||||
|
listener.Start();
|
||||||
|
Console.WriteLine($"Serving {distDir} at {prefix} (Ctrl+C to stop)");
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
|
||||||
|
|
||||||
|
while (!cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var ctx = await listener.GetContextAsync();
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var reqPath = Uri.UnescapeDataString(ctx.Request.Url!.LocalPath);
|
||||||
|
if (reqPath.EndsWith("/")) reqPath += "index.html";
|
||||||
|
var file = Path.Combine(distDir, reqPath.TrimStart('/'));
|
||||||
|
if (!File.Exists(file))
|
||||||
|
{
|
||||||
|
file = Path.Combine(distDir, reqPath.TrimStart('/') + "/index.html");
|
||||||
|
if (!File.Exists(file)) file = Path.Combine(distDir, "404.html");
|
||||||
|
}
|
||||||
|
if (File.Exists(file))
|
||||||
|
{
|
||||||
|
var bytes = await File.ReadAllBytesAsync(file);
|
||||||
|
ctx.Response.ContentType = MimeType(file);
|
||||||
|
await ctx.Response.OutputStream.WriteAsync(bytes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 500;
|
||||||
|
var msg = Encoding.UTF8.GetBytes(ex.Message);
|
||||||
|
await ctx.Response.OutputStream.WriteAsync(msg);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ctx.Response.Close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
listener.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
static string MimeType(string file)
|
||||||
|
{
|
||||||
|
var ext = Path.GetExtension(file).ToLowerInvariant();
|
||||||
|
return ext switch
|
||||||
|
{
|
||||||
|
".html" => "text/html; charset=utf-8",
|
||||||
|
".css" => "text/css; charset=utf-8",
|
||||||
|
".js" => "text/javascript",
|
||||||
|
".json" => "application/json",
|
||||||
|
".svg" => "image/svg+xml",
|
||||||
|
".png" => "image/png",
|
||||||
|
".jpg" or ".jpeg" => "image/jpeg",
|
||||||
|
".ico" => "image/x-icon",
|
||||||
|
".webmanifest" => "application/manifest+json",
|
||||||
|
".xml" => "application/xml",
|
||||||
|
".txt" => "text/plain; charset=utf-8",
|
||||||
|
_ => "application/octet-stream",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace GCBoK.WebStaticBuilder;
|
||||||
|
|
||||||
|
/// <summary>SEO header generation (title, meta, OpenGraph, Twitter, canonical, favicons).</summary>
|
||||||
|
public static class SeoHelper
|
||||||
|
{
|
||||||
|
public static string RenderSeoHead(string lang, string title, string description,
|
||||||
|
string canonicalUrl, string cdnAssets, string defaultTitle, string defaultDescription)
|
||||||
|
{
|
||||||
|
var ogTitle = Localized(title, defaultTitle);
|
||||||
|
var ogDescription = Localized(description, defaultDescription);
|
||||||
|
|
||||||
|
var ogLocale = Directories.Config.Languages.TryGetValue(lang, out var li)
|
||||||
|
? li.OgLocale : "de_DE";
|
||||||
|
|
||||||
|
var cdn = cdnAssets.TrimEnd('/');
|
||||||
|
|
||||||
|
return $@"<meta charset=""UTF-8"">
|
||||||
|
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
|
||||||
|
<title>{HtmlEncode(ogTitle)}</title>
|
||||||
|
<meta name=""description"" content=""{HtmlEncode(ogDescription)}"">
|
||||||
|
<link rel=""icon"" type=""image/svg+xml"" href=""{cdn}/favicon/favicon.svg"">
|
||||||
|
<link rel=""icon"" type=""image/png"" sizes=""96x96"" href=""{cdn}/favicon/favicon-96x96.png"">
|
||||||
|
<link rel=""icon"" href=""{cdn}/favicon/favicon.ico"">
|
||||||
|
<link rel=""apple-touch-icon"" sizes=""180x180"" href=""{cdn}/favicon/apple-touch-icon.png"">
|
||||||
|
<link rel=""manifest"" href=""{cdn}/favicon/site.webmanifest"">
|
||||||
|
<meta name=""msapplication-TileColor"" content=""#da532c"">
|
||||||
|
<meta name=""theme-color"" content=""#ffffff"">
|
||||||
|
<meta property=""og:title"" content=""{HtmlEncode(ogTitle)}"">
|
||||||
|
<meta property=""og:description"" content=""{HtmlEncode(ogDescription)}"">
|
||||||
|
<meta property=""og:type"" content=""website"">
|
||||||
|
<meta property=""og:url"" content=""{canonicalUrl}"">
|
||||||
|
<meta property=""og:locale"" content=""{ogLocale}"">
|
||||||
|
<meta property=""og:image"" content=""{cdn}/favicon/web-app-manifest-512x512.png"">
|
||||||
|
<meta name=""twitter:card"" content=""summary_large_image"">
|
||||||
|
<meta name=""twitter:title"" content=""{HtmlEncode(ogTitle)}"">
|
||||||
|
<meta name=""twitter:description"" content=""{HtmlEncode(ogDescription)}"">
|
||||||
|
<link rel=""canonical"" href=""{canonicalUrl}"">";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Localized(string value, string fallback)
|
||||||
|
=> string.IsNullOrEmpty(value) ? fallback : value;
|
||||||
|
|
||||||
|
public static string HtmlEncode(string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text)) return "";
|
||||||
|
return text.Replace("&", "&")
|
||||||
|
.Replace("<", "<")
|
||||||
|
.Replace(">", ">")
|
||||||
|
.Replace("\"", """)
|
||||||
|
.Replace("'", "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace GCBoK.WebStaticBuilder;
|
||||||
|
|
||||||
|
/// <summary>Machine-translation provider (optional, requires an API key via environment).</summary>
|
||||||
|
public interface ITranslator
|
||||||
|
{
|
||||||
|
/// <summary>Translate text from the site default language into <paramref name="targetLang"/>.</summary>
|
||||||
|
Task<string> TranslateAsync(string text, string targetLang);
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace GCBoK.WebStaticBuilder;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Machine-translation helper. Talks to Azure Translator or DeepL over REST
|
||||||
|
/// (no extra NuGet packages). Keys are read from an environment variable.
|
||||||
|
/// Drafts are written to <c>.translation-drafts/</c> and never overwrite source.
|
||||||
|
/// </summary>
|
||||||
|
public static class TranslationService
|
||||||
|
{
|
||||||
|
public static ITranslator? Create(BuildConfig cfg)
|
||||||
|
{
|
||||||
|
var t = cfg.Translator;
|
||||||
|
var key = t.KeyEnv != null ? Environment.GetEnvironmentVariable(t.KeyEnv) : null;
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"No translator key found in environment '{t.KeyEnv}' — translation skipped.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Provider.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"deepl" => new DeeplTranslator(key, t.Endpoint),
|
||||||
|
_ => new AzureTranslator(key, t.Endpoint, t.Region),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task CheckMissing(BuildConfig cfg)
|
||||||
|
{
|
||||||
|
var root = Directories.ContentDir;
|
||||||
|
var langs = cfg.LanguageCodes().Where(l => l != cfg.DefaultLanguage).ToList();
|
||||||
|
var files = Directory.GetFiles(root, "*.md", SearchOption.TopDirectoryOnly);
|
||||||
|
var missing = new List<(string File, string Lang)>();
|
||||||
|
foreach (var f in files)
|
||||||
|
{
|
||||||
|
var name = Path.GetFileNameWithoutExtension(f);
|
||||||
|
if (name.Contains('.')) continue; // skip already language-specific
|
||||||
|
foreach (var lang in langs)
|
||||||
|
{
|
||||||
|
if (!File.Exists(Path.Combine(root, $"{name}.{lang}.md")))
|
||||||
|
missing.Add((Path.GetFileName(f), lang));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (missing.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("All languages have content files. Nothing missing.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Console.WriteLine($"Missing translations ({missing.Count}):");
|
||||||
|
foreach (var (file, lang) in missing)
|
||||||
|
Console.WriteLine($" [{lang}] {file}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task GenerateDrafts(BuildConfig cfg)
|
||||||
|
{
|
||||||
|
var translator = Create(cfg);
|
||||||
|
if (translator == null) return;
|
||||||
|
|
||||||
|
var root = Directories.ContentDir;
|
||||||
|
var draftRoot = Path.Combine(Directories.Root, ".translation-drafts");
|
||||||
|
var langs = cfg.LanguageCodes().Where(l => l != cfg.DefaultLanguage).ToList();
|
||||||
|
var files = Directory.GetFiles(root, "*.md", SearchOption.TopDirectoryOnly);
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
foreach (var f in files)
|
||||||
|
{
|
||||||
|
var name = Path.GetFileNameWithoutExtension(f);
|
||||||
|
if (name.Contains('.')) continue;
|
||||||
|
var (meta, body) = ContentLoader.ParseFrontmatter(File.ReadAllText(f));
|
||||||
|
foreach (var lang in langs)
|
||||||
|
{
|
||||||
|
var targetFile = Path.Combine(draftRoot, lang, $"{name}.{lang}.md");
|
||||||
|
if (File.Exists(Path.Combine(root, $"{name}.{lang}.md"))) continue; // already exists
|
||||||
|
if (File.Exists(targetFile)) continue; // draft already there
|
||||||
|
|
||||||
|
var tTitle = await translator.TranslateAsync(meta.Title ?? "", lang);
|
||||||
|
var tDesc = await translator.TranslateAsync(meta.Description ?? "", lang);
|
||||||
|
var tEyebrow = await translator.TranslateAsync(meta.Eyebrow ?? "", lang);
|
||||||
|
var tBody = await translator.TranslateAsync(body, lang);
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("---");
|
||||||
|
sb.AppendLine($"section: {meta.Section}");
|
||||||
|
sb.AppendLine($"order: {meta.Order}");
|
||||||
|
sb.AppendLine($"eyebrow: \"{tEyebrow}\"");
|
||||||
|
sb.AppendLine($"title: \"{tTitle}\"");
|
||||||
|
sb.AppendLine($"description: \"{tDesc}\"");
|
||||||
|
sb.AppendLine($"href: {meta.Href}");
|
||||||
|
sb.AppendLine($"render: {meta.Render.ToString().ToLowerInvariant()}");
|
||||||
|
sb.AppendLine($"navbar: {meta.Navbar.ToString().ToLowerInvariant()}");
|
||||||
|
sb.AppendLine("---");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"<!-- DRAFT (machine-translated via {cfg.Translator.Provider}). Review before publishing. -->");
|
||||||
|
sb.AppendLine(tBody);
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(targetFile)!);
|
||||||
|
File.WriteAllText(targetFile, sb.ToString());
|
||||||
|
count++;
|
||||||
|
Console.WriteLine($" Draft -> {Path.GetRelativePath(Directories.Root, targetFile)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Console.WriteLine($"Generated {count} translation draft(s) in {draftRoot}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Azure Translator (Text Translation API) REST client.</summary>
|
||||||
|
public class AzureTranslator : ITranslator
|
||||||
|
{
|
||||||
|
private readonly HttpClient _http = new();
|
||||||
|
private readonly string _key;
|
||||||
|
private readonly string? _endpoint;
|
||||||
|
private readonly string? _region;
|
||||||
|
|
||||||
|
public AzureTranslator(string key, string? endpoint, string? region)
|
||||||
|
{
|
||||||
|
_key = key;
|
||||||
|
_endpoint = string.IsNullOrEmpty(endpoint) ? "https://api.cognitive.microsofttranslator.com" : endpoint;
|
||||||
|
_region = region;
|
||||||
|
_http.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> TranslateAsync(string text, string targetLang)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text)) return text;
|
||||||
|
var url = $"{_endpoint!.TrimEnd('/')}/translate?api-version=3.0&to={targetLang}";
|
||||||
|
using var req = new HttpRequestMessage(HttpMethod.Post, url);
|
||||||
|
req.Headers.Add("Ocp-Apim-Subscription-Key", _key);
|
||||||
|
if (!string.IsNullOrEmpty(_region)) req.Headers.Add("Ocp-Apim-Subscription-Region", _region);
|
||||||
|
req.Content = new StringContent(JsonSerializer.Serialize(new[] { new { Text = text } }),
|
||||||
|
Encoding.UTF8, "application/json");
|
||||||
|
using var resp = await _http.SendAsync(req);
|
||||||
|
resp.EnsureSuccessStatusCode();
|
||||||
|
var doc = await resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var translated = doc[0].GetProperty("translations")[0].GetProperty("text").GetString();
|
||||||
|
return translated ?? text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>DeepL REST client.</summary>
|
||||||
|
public class DeeplTranslator : ITranslator
|
||||||
|
{
|
||||||
|
private readonly HttpClient _http = new();
|
||||||
|
private readonly string _key;
|
||||||
|
private readonly string _endpoint;
|
||||||
|
|
||||||
|
public DeeplTranslator(string key, string? endpoint)
|
||||||
|
{
|
||||||
|
_key = key;
|
||||||
|
_endpoint = string.IsNullOrEmpty(endpoint) ? "https://api-free.deepl.com/v2" : endpoint.TrimEnd('/');
|
||||||
|
_http.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> TranslateAsync(string text, string targetLang)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text)) return text;
|
||||||
|
var target = targetLang.ToUpperInvariant();
|
||||||
|
var content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["text"] = text,
|
||||||
|
["target_lang"] = target,
|
||||||
|
});
|
||||||
|
using var req = new HttpRequestMessage(HttpMethod.Post, $"{_endpoint}/translate") { Content = content };
|
||||||
|
req.Headers.Authorization = new AuthenticationHeaderValue("DeepL-Auth-Key", _key);
|
||||||
|
using var resp = await _http.SendAsync(req);
|
||||||
|
resp.EnsureSuccessStatusCode();
|
||||||
|
var doc = await resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var translated = doc.GetProperty("translations")[0].GetProperty("text").GetString();
|
||||||
|
return translated ?? text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
:root { color-scheme: light dark; }
|
||||||
|
body { font-family: system-ui, sans-serif; margin: 0; color: #1a1a1a; }
|
||||||
|
.container { max-width: 760px; margin: 0 auto; padding: 1.5rem; }
|
||||||
|
.skip-link { position: absolute; left: -999px; }
|
||||||
|
.skip-link:focus { left: 1rem; top: 1rem; background: #0b5fff; color: #fff; padding: .5rem; }
|
||||||
|
.site-header { display: flex; gap: 1rem; align-items: center; padding: .75rem 1.5rem; border-bottom: 1px solid #ddd; }
|
||||||
|
.site-nav { display: flex; gap: 1rem; }
|
||||||
|
.brand { font-weight: 700; text-decoration: none; color: #0b5fff; }
|
||||||
|
.lang-dropdown__menu { list-style: none; display: flex; gap: .5rem; }
|
||||||
|
.site-footer { border-top: 1px solid #ddd; padding: 1.5rem; margin-top: 2rem; color: #555; }
|
||||||
|
.muted { color: #777; }
|
||||||
|
.page-nav { display: flex; justify-content: space-between; margin-top: 2rem; }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#0b5fff"/><text x="16" y="22" font-size="16" text-anchor="middle" fill="#fff" font-family="sans-serif">G</text></svg>
|
||||||
|
After Width: | Height: | Size: 220 B |
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "Meine Website",
|
||||||
|
"short_name": "Website",
|
||||||
|
"icons": [
|
||||||
|
{ "src": "/assets/favicon/favicon.svg", "sizes": "any", "type": "image/svg+xml" }
|
||||||
|
],
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// GCBoK.WebStaticBuilder example theme script.
|
||||||
|
(function () {
|
||||||
|
var t = localStorage.getItem('gc-theme') || 'light';
|
||||||
|
document.documentElement.setAttribute('data-theme', t);
|
||||||
|
var btn = document.getElementById('themeToggle');
|
||||||
|
if (btn) btn.addEventListener('click', function () {
|
||||||
|
var cur = document.documentElement.getAttribute('data-theme');
|
||||||
|
var next = cur === 'light' ? 'dark' : 'light';
|
||||||
|
document.documentElement.setAttribute('data-theme', next);
|
||||||
|
localStorage.setItem('gc-theme', next);
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
=== Build Started: 2026-07-11 12:00:07.845 ===
|
||||||
|
ENV: work
|
||||||
|
BASE_URL: http://localhost:8000
|
||||||
|
CDN_ASSETS: /assets
|
||||||
|
ROOT: /mnt/brx5/work/OSS/build_webstatic/webstatic.example
|
||||||
|
|
||||||
|
Assets copied to: /mnt/brx5/work/OSS/build_webstatic/webstatic.example/dist/assets
|
||||||
|
Legal: /mnt/brx5/work/OSS/build_webstatic/webstatic.example/dist/de/impressum.html
|
||||||
|
Legal: /mnt/brx5/work/OSS/build_webstatic/webstatic.example/dist/en/impressum.html
|
||||||
|
Legal: /mnt/brx5/work/OSS/build_webstatic/webstatic.example/dist/vi/impressum.html
|
||||||
|
Pagefind (exit 0): Running Pagefind v1.5.2 (Extended)
|
||||||
|
Running from: "/mnt/brx5/work/OSS/build_webstatic/webstatic.example"
|
||||||
|
Source: "dist"
|
||||||
|
Output: "dist/pagefind"
|
||||||
|
|
||||||
|
[Walking source directory]
|
||||||
|
Found 10 files matching **/*.{html}
|
||||||
|
|
||||||
|
[Parsing files]
|
||||||
|
Found a data-pagefind-body element on the site.
|
||||||
|
↳ Ignoring pages without this tag.
|
||||||
|
|
||||||
|
[Reading languages]
|
||||||
|
Discovered 3 languages: vi, en, de
|
||||||
|
|
||||||
|
[Building search indexes]
|
||||||
|
Total:
|
||||||
|
Indexed 3 languages
|
||||||
|
Indexed 9 pages
|
||||||
|
Indexed 337 words
|
||||||
|
Indexed 0 filters
|
||||||
|
Indexed 0 sorts
|
||||||
|
|
||||||
|
Finished in 0.036 seconds
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
section: hero
|
||||||
|
order: 0
|
||||||
|
title: "Banner"
|
||||||
|
eyebrow: "Beispiel"
|
||||||
|
description: "Banner-Block für die Startseite"
|
||||||
|
href: /
|
||||||
|
render: true
|
||||||
|
navbar: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Willkommen bei GCBoK.WebStaticBuilder
|
||||||
|
|
||||||
|
Dies ist der Banner-Block (order 0), der auf der Startseite (Hero/Landing) eingebettet wird.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
section: start
|
||||||
|
order: 1
|
||||||
|
eyebrow: "Startseite"
|
||||||
|
title: "Startseite"
|
||||||
|
description: "Die Landing-Page dieser Beispiel-Website"
|
||||||
|
href: /
|
||||||
|
render: true
|
||||||
|
navbar: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Einführung
|
||||||
|
|
||||||
|
Diese Seite wurde mit **GCBoK.WebStaticBuilder** erzeugt — einem statischen, mehrsprachigen
|
||||||
|
Generator für compliance-freundliche Websites (MIT, OSS).
|
||||||
|
|
||||||
|
## Funktionen
|
||||||
|
|
||||||
|
- Markdown mit YAML-Frontmatter
|
||||||
|
- Mehrsprachigkeit (de / en / vi)
|
||||||
|
- SEO-Kopf, Sitemap, robots.txt
|
||||||
|
- Client-Suche via Pagefind
|
||||||
|
- UI-Lokalisierung über `{{ i18n.* }}`
|
||||||
|
|
||||||
|
## Nächste Schritte
|
||||||
|
|
||||||
|
Siehe `docs/` im Repository für die Einrichtung.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
section: about
|
||||||
|
order: 2
|
||||||
|
eyebrow: "Über"
|
||||||
|
title: "Über das Projekt"
|
||||||
|
description: "Hintergrund zum Beispiel-Projekt"
|
||||||
|
href: /ueber/
|
||||||
|
render: true
|
||||||
|
navbar: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Über
|
||||||
|
|
||||||
|
GCBoK.WebStaticBuilder ist ein eigenständiges OSS-Tool der GitCover Commons.
|
||||||
|
Es erzeugt statische Websites ohne Server, Datenbank oder Build-Service.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[Markdown] --> B(Builder)
|
||||||
|
B --> C[HTML]
|
||||||
|
C --> D[Pagefind Index]
|
||||||
|
```
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
section: legal
|
||||||
|
order: 10
|
||||||
|
eyebrow: "Rechtliches"
|
||||||
|
title: "Impressum"
|
||||||
|
description: "Impressum der Beispiel-Website"
|
||||||
|
href: /impressum/
|
||||||
|
render: true
|
||||||
|
navbar: false
|
||||||
|
---
|
||||||
|
|
||||||
|
## Impressum
|
||||||
|
|
||||||
|
Beispiel-Website · Ansprechpartner: webmaster@example.org
|
||||||
|
|
||||||
|
© 2026 Beispiel-Website. Erstellt mit GCBoK.WebStaticBuilder (MIT).
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
:root { color-scheme: light dark; }
|
||||||
|
body { font-family: system-ui, sans-serif; margin: 0; color: #1a1a1a; }
|
||||||
|
.container { max-width: 760px; margin: 0 auto; padding: 1.5rem; }
|
||||||
|
.skip-link { position: absolute; left: -999px; }
|
||||||
|
.skip-link:focus { left: 1rem; top: 1rem; background: #0b5fff; color: #fff; padding: .5rem; }
|
||||||
|
.site-header { display: flex; gap: 1rem; align-items: center; padding: .75rem 1.5rem; border-bottom: 1px solid #ddd; }
|
||||||
|
.site-nav { display: flex; gap: 1rem; }
|
||||||
|
.brand { font-weight: 700; text-decoration: none; color: #0b5fff; }
|
||||||
|
.lang-dropdown__menu { list-style: none; display: flex; gap: .5rem; }
|
||||||
|
.site-footer { border-top: 1px solid #ddd; padding: 1.5rem; margin-top: 2rem; color: #555; }
|
||||||
|
.muted { color: #777; }
|
||||||
|
.page-nav { display: flex; justify-content: space-between; margin-top: 2rem; }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#0b5fff"/><text x="16" y="22" font-size="16" text-anchor="middle" fill="#fff" font-family="sans-serif">G</text></svg>
|
||||||
|
After Width: | Height: | Size: 220 B |
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "Meine Website",
|
||||||
|
"short_name": "Website",
|
||||||
|
"icons": [
|
||||||
|
{ "src": "/assets/favicon/favicon.svg", "sizes": "any", "type": "image/svg+xml" }
|
||||||
|
],
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// GCBoK.WebStaticBuilder example theme script.
|
||||||
|
(function () {
|
||||||
|
var t = localStorage.getItem('gc-theme') || 'light';
|
||||||
|
document.documentElement.setAttribute('data-theme', t);
|
||||||
|
var btn = document.getElementById('themeToggle');
|
||||||
|
if (btn) btn.addEventListener('click', function () {
|
||||||
|
var cur = document.documentElement.getAttribute('data-theme');
|
||||||
|
var next = cur === 'light' ? 'dark' : 'light';
|
||||||
|
document.documentElement.setAttribute('data-theme', next);
|
||||||
|
localStorage.setItem('gc-theme', next);
|
||||||
|
});
|
||||||
|
})();
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Impressum</title>
|
||||||
|
<meta name="description" content="Eine mehrsprachige, statische Website.">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="/assets/favicon/favicon-96x96.png">
|
||||||
|
<link rel="icon" href="/assets/favicon/favicon.ico">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<meta property="og:title" content="Impressum">
|
||||||
|
<meta property="og:description" content="Eine mehrsprachige, statische Website.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="http://localhost:8000/impressum/">
|
||||||
|
<meta property="og:locale" content="">
|
||||||
|
<meta property="og:image" content="/assets/favicon/web-app-manifest-512x512.png">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="Impressum">
|
||||||
|
<meta name="twitter:description" content="Eine mehrsprachige, statische Website.">
|
||||||
|
<link rel="canonical" href="http://localhost:8000/impressum/">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="#main" class="skip-link">Zum Hauptinhalt springen</a>
|
||||||
|
|
||||||
|
<header class="site-header">
|
||||||
|
<a href="index.html" class="brand" aria-label="Zur Startseite">Meine Website</a>
|
||||||
|
<nav class="site-nav" aria-label="Hauptnavigation">
|
||||||
|
<a class="nav-gcbok__link" href="ueber.html">Über</a>
|
||||||
|
</nav>
|
||||||
|
<div class="lang-dropdown" aria-label="Sprachumschalter">
|
||||||
|
<ul class="lang-dropdown__menu">
|
||||||
|
<li><a href="/de/" hreflang="de"><span>Deutsch</span> <span class="lang-dropdown__code">DE</span></a></li>
|
||||||
|
<li><a href="/en/" hreflang="en"><span>English</span> <span class="lang-dropdown__code">EN</span></a></li>
|
||||||
|
<li><a href="/vi/" hreflang="vi"><span>Tiếng Việt</span> <span class="lang-dropdown__code">VI</span></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main" tabindex="-1" data-pagefind-body>
|
||||||
|
<div class="container">
|
||||||
|
<h2 id="impressum">Impressum</h2>
|
||||||
|
<p>Beispiel-Website · Ansprechpartner: webmaster@example.org</p>
|
||||||
|
<p>© 2026 Beispiel-Website. Erstellt mit GCBoK.WebStaticBuilder (MIT).</p>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container">
|
||||||
|
<h4>MEINE WEBSITE</h4>
|
||||||
|
<p class="muted">Ein Beispiel für GCBoK.WebStaticBuilder</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="glossar.html">Glossar</a></li>
|
||||||
|
<li><a href="impressum.html">Impressum</a></li>
|
||||||
|
</ul>
|
||||||
|
<p>© 2026 Meine Website</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/assets/js/gcbok-theme.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Vendored
+85
@@ -0,0 +1,85 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Startseite</title>
|
||||||
|
<meta name="description" content="Die Landing-Page dieser Beispiel-Website">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="/assets/favicon/favicon-96x96.png">
|
||||||
|
<link rel="icon" href="/assets/favicon/favicon.ico">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<meta property="og:title" content="Startseite">
|
||||||
|
<meta property="og:description" content="Die Landing-Page dieser Beispiel-Website">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="http://localhost:8000/">
|
||||||
|
<meta property="og:locale" content="">
|
||||||
|
<meta property="og:image" content="/assets/favicon/web-app-manifest-512x512.png">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="Startseite">
|
||||||
|
<meta name="twitter:description" content="Die Landing-Page dieser Beispiel-Website">
|
||||||
|
<link rel="canonical" href="http://localhost:8000/">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="#main" class="skip-link">Zum Hauptinhalt springen</a>
|
||||||
|
|
||||||
|
<header class="site-header">
|
||||||
|
<a href="index.html" class="brand" aria-label="Zur Startseite">Meine Website</a>
|
||||||
|
<nav class="site-nav" aria-label="Hauptnavigation">
|
||||||
|
<a class="nav-gcbok__link" href="ueber.html">Über</a>
|
||||||
|
</nav>
|
||||||
|
<div class="lang-dropdown" aria-label="Sprachumschalter">
|
||||||
|
<ul class="lang-dropdown__menu">
|
||||||
|
<li><a href="/de/" hreflang="de"><span>Deutsch</span> <span class="lang-dropdown__code">DE</span></a></li>
|
||||||
|
<li><a href="/en/" hreflang="en"><span>English</span> <span class="lang-dropdown__code">EN</span></a></li>
|
||||||
|
<li><a href="/vi/" hreflang="vi"><span>Tiếng Việt</span> <span class="lang-dropdown__code">VI</span></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main" tabindex="-1" data-pagefind-body>
|
||||||
|
<div class="container">
|
||||||
|
<h1 id="willkommen-bei-gcbok.webstaticbuilder">Willkommen bei GCBoK.WebStaticBuilder</h1>
|
||||||
|
<p>Dies ist der Banner-Block (order 0), der auf der Startseite (Hero/Landing) eingebettet wird.</p>
|
||||||
|
<h2 id="einfuhrung">Einführung</h2>
|
||||||
|
<p>Diese Seite wurde mit <strong>GCBoK.WebStaticBuilder</strong> erzeugt — einem statischen, mehrsprachigen
|
||||||
|
Generator für compliance-freundliche Websites (MIT, OSS).</p>
|
||||||
|
<h2 id="funktionen">Funktionen</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Markdown mit YAML-Frontmatter</li>
|
||||||
|
<li>Mehrsprachigkeit (de / en / vi)</li>
|
||||||
|
<li>SEO-Kopf, Sitemap, robots.txt</li>
|
||||||
|
<li>Client-Suche via Pagefind</li>
|
||||||
|
<li>UI-Lokalisierung über <code>{{ i18n.* }}</code></li>
|
||||||
|
</ul>
|
||||||
|
<h2 id="nachste-schritte">Nächste Schritte</h2>
|
||||||
|
<p>Siehe <code>docs/</code> im Repository für die Einrichtung.</p>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container">
|
||||||
|
<h4>MEINE WEBSITE</h4>
|
||||||
|
<p class="muted">Ein Beispiel für GCBoK.WebStaticBuilder</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="glossar.html">Glossar</a></li>
|
||||||
|
<li><a href="impressum.html">Impressum</a></li>
|
||||||
|
</ul>
|
||||||
|
<p>© 2026 Meine Website</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/assets/js/gcbok-theme.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Vendored
+81
@@ -0,0 +1,81 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Über das Projekt</title>
|
||||||
|
<meta name="description" content="Hintergrund zum Beispiel-Projekt">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="/assets/favicon/favicon-96x96.png">
|
||||||
|
<link rel="icon" href="/assets/favicon/favicon.ico">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<meta property="og:title" content="Über das Projekt">
|
||||||
|
<meta property="og:description" content="Hintergrund zum Beispiel-Projekt">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="http://localhost:8000/ueber/">
|
||||||
|
<meta property="og:locale" content="">
|
||||||
|
<meta property="og:image" content="/assets/favicon/web-app-manifest-512x512.png">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="Über das Projekt">
|
||||||
|
<meta name="twitter:description" content="Hintergrund zum Beispiel-Projekt">
|
||||||
|
<link rel="canonical" href="http://localhost:8000/ueber/">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="#main" class="skip-link">Zum Hauptinhalt springen</a>
|
||||||
|
|
||||||
|
<header class="site-header">
|
||||||
|
<a href="index.html" class="brand" aria-label="Zur Startseite">Meine Website</a>
|
||||||
|
<nav class="site-nav" aria-label="Hauptnavigation">
|
||||||
|
<a class="nav-gcbok__link" href="ueber.html">Über</a>
|
||||||
|
</nav>
|
||||||
|
<div class="lang-dropdown" aria-label="Sprachumschalter">
|
||||||
|
<ul class="lang-dropdown__menu">
|
||||||
|
<li><a href="/de/" hreflang="de"><span>Deutsch</span> <span class="lang-dropdown__code">DE</span></a></li>
|
||||||
|
<li><a href="/en/" hreflang="en"><span>English</span> <span class="lang-dropdown__code">EN</span></a></li>
|
||||||
|
<li><a href="/vi/" hreflang="vi"><span>Tiếng Việt</span> <span class="lang-dropdown__code">VI</span></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main" tabindex="-1" data-pagefind-body>
|
||||||
|
<div class="container">
|
||||||
|
<h2 id="uber">Über</h2>
|
||||||
|
<p>GCBoK.WebStaticBuilder ist ein eigenständiges OSS-Tool der GitCover Commons.
|
||||||
|
Es erzeugt statische Websites ohne Server, Datenbank oder Build-Service.</p>
|
||||||
|
<div class="mermaid">graph TD
|
||||||
|
A[Markdown] --> B(Builder)
|
||||||
|
B --> C[HTML]
|
||||||
|
C --> D[Pagefind Index]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="page-nav" aria-label="Seitennavigation">
|
||||||
|
<span class="page-nav__link"> </span>
|
||||||
|
<span class="page-nav__link page-nav__link--next"> </span>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container">
|
||||||
|
<h4>MEINE WEBSITE</h4>
|
||||||
|
<p class="muted">Ein Beispiel für GCBoK.WebStaticBuilder</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="glossar.html">Glossar</a></li>
|
||||||
|
<li><a href="impressum.html">Impressum</a></li>
|
||||||
|
</ul>
|
||||||
|
<p>© 2026 Meine Website</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/assets/js/gcbok-theme.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Impressum</title>
|
||||||
|
<meta name="description" content="A multilingual, static website.">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="/assets/favicon/favicon-96x96.png">
|
||||||
|
<link rel="icon" href="/assets/favicon/favicon.ico">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<meta property="og:title" content="Impressum">
|
||||||
|
<meta property="og:description" content="A multilingual, static website.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="http://localhost:8000/impressum/">
|
||||||
|
<meta property="og:locale" content="">
|
||||||
|
<meta property="og:image" content="/assets/favicon/web-app-manifest-512x512.png">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="Impressum">
|
||||||
|
<meta name="twitter:description" content="A multilingual, static website.">
|
||||||
|
<link rel="canonical" href="http://localhost:8000/impressum/">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="#main" class="skip-link">Skip to main content</a>
|
||||||
|
|
||||||
|
<header class="site-header">
|
||||||
|
<a href="index.html" class="brand" aria-label="Back to home">My Website</a>
|
||||||
|
<nav class="site-nav" aria-label="Main navigation">
|
||||||
|
<a class="nav-gcbok__link" href="ueber.html">Über</a>
|
||||||
|
</nav>
|
||||||
|
<div class="lang-dropdown" aria-label="Language switcher">
|
||||||
|
<ul class="lang-dropdown__menu">
|
||||||
|
<li><a href="/de/" hreflang="de"><span>Deutsch</span> <span class="lang-dropdown__code">DE</span></a></li>
|
||||||
|
<li><a href="/en/" hreflang="en"><span>English</span> <span class="lang-dropdown__code">EN</span></a></li>
|
||||||
|
<li><a href="/vi/" hreflang="vi"><span>Tiếng Việt</span> <span class="lang-dropdown__code">VI</span></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main" tabindex="-1" data-pagefind-body>
|
||||||
|
<div class="container">
|
||||||
|
<h2 id="impressum">Impressum</h2>
|
||||||
|
<p>Beispiel-Website · Ansprechpartner: webmaster@example.org</p>
|
||||||
|
<p>© 2026 Beispiel-Website. Erstellt mit GCBoK.WebStaticBuilder (MIT).</p>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container">
|
||||||
|
<h4>MY WEBSITE</h4>
|
||||||
|
<p class="muted">An example for GCBoK.WebStaticBuilder</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="glossary.html">Glossary</a></li>
|
||||||
|
<li><a href="impressum.html">Imprint</a></li>
|
||||||
|
</ul>
|
||||||
|
<p>© 2026 My Website</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/assets/js/gcbok-theme.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Vendored
+85
@@ -0,0 +1,85 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Startseite</title>
|
||||||
|
<meta name="description" content="Die Landing-Page dieser Beispiel-Website">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="/assets/favicon/favicon-96x96.png">
|
||||||
|
<link rel="icon" href="/assets/favicon/favicon.ico">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<meta property="og:title" content="Startseite">
|
||||||
|
<meta property="og:description" content="Die Landing-Page dieser Beispiel-Website">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="http://localhost:8000/">
|
||||||
|
<meta property="og:locale" content="">
|
||||||
|
<meta property="og:image" content="/assets/favicon/web-app-manifest-512x512.png">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="Startseite">
|
||||||
|
<meta name="twitter:description" content="Die Landing-Page dieser Beispiel-Website">
|
||||||
|
<link rel="canonical" href="http://localhost:8000/">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="#main" class="skip-link">Skip to main content</a>
|
||||||
|
|
||||||
|
<header class="site-header">
|
||||||
|
<a href="index.html" class="brand" aria-label="Back to home">My Website</a>
|
||||||
|
<nav class="site-nav" aria-label="Main navigation">
|
||||||
|
<a class="nav-gcbok__link" href="ueber.html">Über</a>
|
||||||
|
</nav>
|
||||||
|
<div class="lang-dropdown" aria-label="Language switcher">
|
||||||
|
<ul class="lang-dropdown__menu">
|
||||||
|
<li><a href="/de/" hreflang="de"><span>Deutsch</span> <span class="lang-dropdown__code">DE</span></a></li>
|
||||||
|
<li><a href="/en/" hreflang="en"><span>English</span> <span class="lang-dropdown__code">EN</span></a></li>
|
||||||
|
<li><a href="/vi/" hreflang="vi"><span>Tiếng Việt</span> <span class="lang-dropdown__code">VI</span></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main" tabindex="-1" data-pagefind-body>
|
||||||
|
<div class="container">
|
||||||
|
<h1 id="willkommen-bei-gcbok.webstaticbuilder">Willkommen bei GCBoK.WebStaticBuilder</h1>
|
||||||
|
<p>Dies ist der Banner-Block (order 0), der auf der Startseite (Hero/Landing) eingebettet wird.</p>
|
||||||
|
<h2 id="einfuhrung">Einführung</h2>
|
||||||
|
<p>Diese Seite wurde mit <strong>GCBoK.WebStaticBuilder</strong> erzeugt — einem statischen, mehrsprachigen
|
||||||
|
Generator für compliance-freundliche Websites (MIT, OSS).</p>
|
||||||
|
<h2 id="funktionen">Funktionen</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Markdown mit YAML-Frontmatter</li>
|
||||||
|
<li>Mehrsprachigkeit (de / en / vi)</li>
|
||||||
|
<li>SEO-Kopf, Sitemap, robots.txt</li>
|
||||||
|
<li>Client-Suche via Pagefind</li>
|
||||||
|
<li>UI-Lokalisierung über <code>{{ i18n.* }}</code></li>
|
||||||
|
</ul>
|
||||||
|
<h2 id="nachste-schritte">Nächste Schritte</h2>
|
||||||
|
<p>Siehe <code>docs/</code> im Repository für die Einrichtung.</p>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container">
|
||||||
|
<h4>MY WEBSITE</h4>
|
||||||
|
<p class="muted">An example for GCBoK.WebStaticBuilder</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="glossary.html">Glossary</a></li>
|
||||||
|
<li><a href="impressum.html">Imprint</a></li>
|
||||||
|
</ul>
|
||||||
|
<p>© 2026 My Website</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/assets/js/gcbok-theme.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Vendored
+81
@@ -0,0 +1,81 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Über das Projekt</title>
|
||||||
|
<meta name="description" content="Hintergrund zum Beispiel-Projekt">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="/assets/favicon/favicon-96x96.png">
|
||||||
|
<link rel="icon" href="/assets/favicon/favicon.ico">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<meta property="og:title" content="Über das Projekt">
|
||||||
|
<meta property="og:description" content="Hintergrund zum Beispiel-Projekt">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="http://localhost:8000/ueber/">
|
||||||
|
<meta property="og:locale" content="">
|
||||||
|
<meta property="og:image" content="/assets/favicon/web-app-manifest-512x512.png">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="Über das Projekt">
|
||||||
|
<meta name="twitter:description" content="Hintergrund zum Beispiel-Projekt">
|
||||||
|
<link rel="canonical" href="http://localhost:8000/ueber/">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="#main" class="skip-link">Skip to main content</a>
|
||||||
|
|
||||||
|
<header class="site-header">
|
||||||
|
<a href="index.html" class="brand" aria-label="Back to home">My Website</a>
|
||||||
|
<nav class="site-nav" aria-label="Main navigation">
|
||||||
|
<a class="nav-gcbok__link" href="ueber.html">Über</a>
|
||||||
|
</nav>
|
||||||
|
<div class="lang-dropdown" aria-label="Language switcher">
|
||||||
|
<ul class="lang-dropdown__menu">
|
||||||
|
<li><a href="/de/" hreflang="de"><span>Deutsch</span> <span class="lang-dropdown__code">DE</span></a></li>
|
||||||
|
<li><a href="/en/" hreflang="en"><span>English</span> <span class="lang-dropdown__code">EN</span></a></li>
|
||||||
|
<li><a href="/vi/" hreflang="vi"><span>Tiếng Việt</span> <span class="lang-dropdown__code">VI</span></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main" tabindex="-1" data-pagefind-body>
|
||||||
|
<div class="container">
|
||||||
|
<h2 id="uber">Über</h2>
|
||||||
|
<p>GCBoK.WebStaticBuilder ist ein eigenständiges OSS-Tool der GitCover Commons.
|
||||||
|
Es erzeugt statische Websites ohne Server, Datenbank oder Build-Service.</p>
|
||||||
|
<div class="mermaid">graph TD
|
||||||
|
A[Markdown] --> B(Builder)
|
||||||
|
B --> C[HTML]
|
||||||
|
C --> D[Pagefind Index]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="page-nav" aria-label="Seitennavigation">
|
||||||
|
<span class="page-nav__link"> </span>
|
||||||
|
<span class="page-nav__link page-nav__link--next"> </span>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container">
|
||||||
|
<h4>MY WEBSITE</h4>
|
||||||
|
<p class="muted">An example for GCBoK.WebStaticBuilder</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="glossary.html">Glossary</a></li>
|
||||||
|
<li><a href="impressum.html">Imprint</a></li>
|
||||||
|
</ul>
|
||||||
|
<p>© 2026 My Website</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/assets/js/gcbok-theme.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Vendored
+30
@@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Meine Website</title>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var supported = ["de","en","vi"];
|
||||||
|
var defaultLang = "de";
|
||||||
|
var lang = defaultLang;
|
||||||
|
if (navigator.languages && navigator.languages.length) {
|
||||||
|
for (var i = 0; i < navigator.languages.length; i++) {
|
||||||
|
var pref = navigator.languages[i].toLowerCase().split("-")[0];
|
||||||
|
if (supported.indexOf(pref) !== -1) { lang = pref; break; }
|
||||||
|
}
|
||||||
|
} else if (navigator.language) {
|
||||||
|
var pref = navigator.language.toLowerCase().split("-")[0];
|
||||||
|
if (supported.indexOf(pref) !== -1) lang = pref;
|
||||||
|
}
|
||||||
|
window.location.replace("/" + lang + "/");
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<meta http-equiv="refresh" content="0; url=/de/">
|
||||||
|
<link rel="canonical" href="http://localhost:8000/de/">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Redirecting to <a href="/de/">Deutsch</a> | <a href="/en/">English</a> | <a href="/vi/">Tiếng Việt</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
{"version":"1.5.2","languages":{"de":{"hash":"de_8987bfa9ae","wasm":"de","page_count":3},"vi":{"hash":"vi_2270a45bad","wasm":null,"page_count":3},"en":{"hash":"en_9b2654cc0d","wasm":"en","page_count":3}},"include_characters":["_","‿","⁀","⁔","︳","︴","﹍","﹎","﹏","_"]}
|
||||||
+1070
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,214 @@
|
|||||||
|
:root {
|
||||||
|
--pagefind-ui-scale: 0.8;
|
||||||
|
--pagefind-ui-primary: #034AD8;
|
||||||
|
--pagefind-ui-fade: #707070;
|
||||||
|
--pagefind-ui-text: #393939;
|
||||||
|
--pagefind-ui-background: #ffffff;
|
||||||
|
--pagefind-ui-border: #eeeeee;
|
||||||
|
--pagefind-ui-tag: #eeeeee;
|
||||||
|
--pagefind-ui-border-width: 2px;
|
||||||
|
--pagefind-ui-border-radius: 8px;
|
||||||
|
--pagefind-ui-image-border-radius: 8px;
|
||||||
|
--pagefind-ui-image-box-ratio: 3 / 2;
|
||||||
|
--pagefind-ui-font: system, -apple-system, ".SFNSText-Regular",
|
||||||
|
"San Francisco", "Roboto", "Segoe UI", "Helvetica Neue",
|
||||||
|
"Lucida Grande", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-pfmod-hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-pfmod-suppressed] {
|
||||||
|
opacity: 0 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-pfmod-sr-hidden] {
|
||||||
|
-webkit-clip: rect(0 0 0 0) !important;
|
||||||
|
clip: rect(0 0 0 0) !important;
|
||||||
|
-webkit-clip-path: inset(100%) !important;
|
||||||
|
clip-path: inset(100%) !important;
|
||||||
|
height: 1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
overflow: clip !important;
|
||||||
|
position: absolute !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
width: 1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-pfmod-loading] {
|
||||||
|
color: var(--pagefind-ui-text);
|
||||||
|
background-color: var(--pagefind-ui-text);
|
||||||
|
border-radius: var(--pagefind-ui-border-radius);
|
||||||
|
opacity: 0.1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input */
|
||||||
|
|
||||||
|
.pagefind-modular-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-input-wrapper::before {
|
||||||
|
background-color: var(--pagefind-ui-text);
|
||||||
|
width: calc(18px * var(--pagefind-ui-scale));
|
||||||
|
height: calc(18px * var(--pagefind-ui-scale));
|
||||||
|
top: calc(23px * var(--pagefind-ui-scale));
|
||||||
|
left: calc(20px * var(--pagefind-ui-scale));
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
opacity: 0.7;
|
||||||
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.7549 11.255H11.9649L11.6849 10.985C12.6649 9.845 13.2549 8.365 13.2549 6.755C13.2549 3.165 10.3449 0.255005 6.75488 0.255005C3.16488 0.255005 0.254883 3.165 0.254883 6.755C0.254883 10.345 3.16488 13.255 6.75488 13.255C8.36488 13.255 9.84488 12.665 10.9849 11.685L11.2549 11.965V12.755L16.2549 17.745L17.7449 16.255L12.7549 11.255ZM6.75488 11.255C4.26488 11.255 2.25488 9.245 2.25488 6.755C2.25488 4.26501 4.26488 2.255 6.75488 2.255C9.24488 2.255 11.2549 4.26501 11.2549 6.755C11.2549 9.245 9.24488 11.255 6.75488 11.255Z' fill='%23000000'/%3E%3C/svg%3E%0A");
|
||||||
|
mask-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.7549 11.255H11.9649L11.6849 10.985C12.6649 9.845 13.2549 8.365 13.2549 6.755C13.2549 3.165 10.3449 0.255005 6.75488 0.255005C3.16488 0.255005 0.254883 3.165 0.254883 6.755C0.254883 10.345 3.16488 13.255 6.75488 13.255C8.36488 13.255 9.84488 12.665 10.9849 11.685L11.2549 11.965V12.755L16.2549 17.745L17.7449 16.255L12.7549 11.255ZM6.75488 11.255C4.26488 11.255 2.25488 9.245 2.25488 6.755C2.25488 4.26501 4.26488 2.255 6.75488 2.255C9.24488 2.255 11.2549 4.26501 11.2549 6.755C11.2549 9.245 9.24488 11.255 6.75488 11.255Z' fill='%23000000'/%3E%3C/svg%3E%0A");
|
||||||
|
-webkit-mask-size: 100%;
|
||||||
|
mask-size: 100%;
|
||||||
|
z-index: 9;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-input {
|
||||||
|
height: calc(64px * var(--pagefind-ui-scale));
|
||||||
|
padding: 0 calc(70px * var(--pagefind-ui-scale)) 0 calc(54px * var(--pagefind-ui-scale));
|
||||||
|
background-color: var(--pagefind-ui-background);
|
||||||
|
border: var(--pagefind-ui-border-width) solid var(--pagefind-ui-border);
|
||||||
|
border-radius: var(--pagefind-ui-border-radius);
|
||||||
|
font-size: calc(21px * var(--pagefind-ui-scale));
|
||||||
|
position: relative;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-input::placeholder {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-input-clear {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(2px * var(--pagefind-ui-scale));
|
||||||
|
right: calc(2px * var(--pagefind-ui-scale));
|
||||||
|
height: calc(60px * var(--pagefind-ui-scale));
|
||||||
|
border-radius: var(--pagefind-ui-border-radius);
|
||||||
|
padding: 0 calc(15px * var(--pagefind-ui-scale)) 0 calc(2px * var(--pagefind-ui-scale));
|
||||||
|
color: var(--pagefind-ui-text);
|
||||||
|
font-size: calc(14px * var(--pagefind-ui-scale));
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--pagefind-ui-background);
|
||||||
|
border: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ResultList */
|
||||||
|
|
||||||
|
.pagefind-modular-list-result {
|
||||||
|
list-style-type: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: min(calc(40px * var(--pagefind-ui-scale)), 3%);
|
||||||
|
padding: calc(30px * var(--pagefind-ui-scale)) 0 calc(40px * var(--pagefind-ui-scale));
|
||||||
|
border-top: solid var(--pagefind-ui-border-width) var(--pagefind-ui-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-list-result:last-of-type {
|
||||||
|
border-bottom: solid var(--pagefind-ui-border-width) var(--pagefind-ui-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-list-thumb {
|
||||||
|
width: min(30%,
|
||||||
|
calc((30% - (100px * var(--pagefind-ui-scale))) * 100000));
|
||||||
|
max-width: calc(120px * var(--pagefind-ui-scale));
|
||||||
|
margin-top: calc(10px * var(--pagefind-ui-scale));
|
||||||
|
aspect-ratio: var(--pagefind-ui-image-box-ratio);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-list-image {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 0;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
border-radius: var(--pagefind-ui-image-border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-list-inner {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: calc(10px * var(--pagefind-ui-scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-list-title {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: calc(21px * var(--pagefind-ui-scale));
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-list-link {
|
||||||
|
color: var(--pagefind-ui-text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-list-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-list-excerpt {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: calc(16px * var(--pagefind-ui-scale));
|
||||||
|
margin-top: calc(4px * var(--pagefind-ui-scale));
|
||||||
|
margin-bottom: 0;
|
||||||
|
min-width: calc(250px * var(--pagefind-ui-scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FilterPills */
|
||||||
|
|
||||||
|
.pagefind-modular-filter-pills-wrapper {
|
||||||
|
overflow-x: scroll;
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-filter-pills {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-filter-pill {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border: none;
|
||||||
|
appearance: none;
|
||||||
|
padding: 0 calc(24px * var(--pagefind-ui-scale));
|
||||||
|
background-color: var(--pagefind-ui-background);
|
||||||
|
color: var(--pagefind-ui-fade);
|
||||||
|
border: var(--pagefind-ui-border-width) solid var(--pagefind-ui-border);
|
||||||
|
border-radius: calc(25px * var(--pagefind-ui-scale));
|
||||||
|
font-size: calc(18px * var(--pagefind-ui-scale));
|
||||||
|
height: calc(50px * var(--pagefind-ui-scale));
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-filter-pill:hover {
|
||||||
|
border-color: var(--pagefind-ui-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagefind-modular-filter-pill[aria-pressed="true"] {
|
||||||
|
border-color: var(--pagefind-ui-primary);
|
||||||
|
color: var(--pagefind-ui-primary);
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
+6
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
Sitemap: http://localhost:8000/sitemap.xml
|
||||||
Vendored
+15
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url><loc>http://localhost:8000/de/</loc></url>
|
||||||
|
<url><loc>http://localhost:8000/</loc></url>
|
||||||
|
<url><loc>http://localhost:8000/</loc></url>
|
||||||
|
<url><loc>http://localhost:8000/ueber/</loc></url>
|
||||||
|
<url><loc>http://localhost:8000/en/</loc></url>
|
||||||
|
<url><loc>http://localhost:8000/</loc></url>
|
||||||
|
<url><loc>http://localhost:8000/</loc></url>
|
||||||
|
<url><loc>http://localhost:8000/ueber/</loc></url>
|
||||||
|
<url><loc>http://localhost:8000/vi/</loc></url>
|
||||||
|
<url><loc>http://localhost:8000/</loc></url>
|
||||||
|
<url><loc>http://localhost:8000/</loc></url>
|
||||||
|
<url><loc>http://localhost:8000/ueber/</loc></url>
|
||||||
|
</urlset>
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Impressum</title>
|
||||||
|
<meta name="description" content="Một trang web tĩnh, đa ngôn ngữ.">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="/assets/favicon/favicon-96x96.png">
|
||||||
|
<link rel="icon" href="/assets/favicon/favicon.ico">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<meta property="og:title" content="Impressum">
|
||||||
|
<meta property="og:description" content="Một trang web tĩnh, đa ngôn ngữ.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="http://localhost:8000/impressum/">
|
||||||
|
<meta property="og:locale" content="">
|
||||||
|
<meta property="og:image" content="/assets/favicon/web-app-manifest-512x512.png">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="Impressum">
|
||||||
|
<meta name="twitter:description" content="Một trang web tĩnh, đa ngôn ngữ.">
|
||||||
|
<link rel="canonical" href="http://localhost:8000/impressum/">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="#main" class="skip-link">Đến nội dung chính</a>
|
||||||
|
|
||||||
|
<header class="site-header">
|
||||||
|
<a href="index.html" class="brand" aria-label="Về trang chủ">Trang web của tôi</a>
|
||||||
|
<nav class="site-nav" aria-label="Điều hướng chính">
|
||||||
|
<a class="nav-gcbok__link" href="ueber.html">Über</a>
|
||||||
|
</nav>
|
||||||
|
<div class="lang-dropdown" aria-label="Chuyển ngôn ngữ">
|
||||||
|
<ul class="lang-dropdown__menu">
|
||||||
|
<li><a href="/de/" hreflang="de"><span>Deutsch</span> <span class="lang-dropdown__code">DE</span></a></li>
|
||||||
|
<li><a href="/en/" hreflang="en"><span>English</span> <span class="lang-dropdown__code">EN</span></a></li>
|
||||||
|
<li><a href="/vi/" hreflang="vi"><span>Tiếng Việt</span> <span class="lang-dropdown__code">VI</span></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main" tabindex="-1" data-pagefind-body>
|
||||||
|
<div class="container">
|
||||||
|
<h2 id="impressum">Impressum</h2>
|
||||||
|
<p>Beispiel-Website · Ansprechpartner: webmaster@example.org</p>
|
||||||
|
<p>© 2026 Beispiel-Website. Erstellt mit GCBoK.WebStaticBuilder (MIT).</p>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container">
|
||||||
|
<h4>TRANG WEB CỦA TÔI</h4>
|
||||||
|
<p class="muted">Một ví dụ cho GCBoK.WebStaticBuilder</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="b-ng-thu-t-ng.html">Bảng thuật ngữ</a></li>
|
||||||
|
<li><a href="impressum.html">Thông tin pháp lý</a></li>
|
||||||
|
</ul>
|
||||||
|
<p>© 2026 Trang web của tôi</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/assets/js/gcbok-theme.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Vendored
+85
@@ -0,0 +1,85 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Startseite</title>
|
||||||
|
<meta name="description" content="Die Landing-Page dieser Beispiel-Website">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="/assets/favicon/favicon-96x96.png">
|
||||||
|
<link rel="icon" href="/assets/favicon/favicon.ico">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<meta property="og:title" content="Startseite">
|
||||||
|
<meta property="og:description" content="Die Landing-Page dieser Beispiel-Website">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="http://localhost:8000/">
|
||||||
|
<meta property="og:locale" content="">
|
||||||
|
<meta property="og:image" content="/assets/favicon/web-app-manifest-512x512.png">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="Startseite">
|
||||||
|
<meta name="twitter:description" content="Die Landing-Page dieser Beispiel-Website">
|
||||||
|
<link rel="canonical" href="http://localhost:8000/">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="#main" class="skip-link">Đến nội dung chính</a>
|
||||||
|
|
||||||
|
<header class="site-header">
|
||||||
|
<a href="index.html" class="brand" aria-label="Về trang chủ">Trang web của tôi</a>
|
||||||
|
<nav class="site-nav" aria-label="Điều hướng chính">
|
||||||
|
<a class="nav-gcbok__link" href="ueber.html">Über</a>
|
||||||
|
</nav>
|
||||||
|
<div class="lang-dropdown" aria-label="Chuyển ngôn ngữ">
|
||||||
|
<ul class="lang-dropdown__menu">
|
||||||
|
<li><a href="/de/" hreflang="de"><span>Deutsch</span> <span class="lang-dropdown__code">DE</span></a></li>
|
||||||
|
<li><a href="/en/" hreflang="en"><span>English</span> <span class="lang-dropdown__code">EN</span></a></li>
|
||||||
|
<li><a href="/vi/" hreflang="vi"><span>Tiếng Việt</span> <span class="lang-dropdown__code">VI</span></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main" tabindex="-1" data-pagefind-body>
|
||||||
|
<div class="container">
|
||||||
|
<h1 id="willkommen-bei-gcbok.webstaticbuilder">Willkommen bei GCBoK.WebStaticBuilder</h1>
|
||||||
|
<p>Dies ist der Banner-Block (order 0), der auf der Startseite (Hero/Landing) eingebettet wird.</p>
|
||||||
|
<h2 id="einfuhrung">Einführung</h2>
|
||||||
|
<p>Diese Seite wurde mit <strong>GCBoK.WebStaticBuilder</strong> erzeugt — einem statischen, mehrsprachigen
|
||||||
|
Generator für compliance-freundliche Websites (MIT, OSS).</p>
|
||||||
|
<h2 id="funktionen">Funktionen</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Markdown mit YAML-Frontmatter</li>
|
||||||
|
<li>Mehrsprachigkeit (de / en / vi)</li>
|
||||||
|
<li>SEO-Kopf, Sitemap, robots.txt</li>
|
||||||
|
<li>Client-Suche via Pagefind</li>
|
||||||
|
<li>UI-Lokalisierung über <code>{{ i18n.* }}</code></li>
|
||||||
|
</ul>
|
||||||
|
<h2 id="nachste-schritte">Nächste Schritte</h2>
|
||||||
|
<p>Siehe <code>docs/</code> im Repository für die Einrichtung.</p>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container">
|
||||||
|
<h4>TRANG WEB CỦA TÔI</h4>
|
||||||
|
<p class="muted">Một ví dụ cho GCBoK.WebStaticBuilder</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="b-ng-thu-t-ng.html">Bảng thuật ngữ</a></li>
|
||||||
|
<li><a href="impressum.html">Thông tin pháp lý</a></li>
|
||||||
|
</ul>
|
||||||
|
<p>© 2026 Trang web của tôi</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/assets/js/gcbok-theme.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Vendored
+81
@@ -0,0 +1,81 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Über das Projekt</title>
|
||||||
|
<meta name="description" content="Hintergrund zum Beispiel-Projekt">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="/assets/favicon/favicon-96x96.png">
|
||||||
|
<link rel="icon" href="/assets/favicon/favicon.ico">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<meta property="og:title" content="Über das Projekt">
|
||||||
|
<meta property="og:description" content="Hintergrund zum Beispiel-Projekt">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="http://localhost:8000/ueber/">
|
||||||
|
<meta property="og:locale" content="">
|
||||||
|
<meta property="og:image" content="/assets/favicon/web-app-manifest-512x512.png">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="Über das Projekt">
|
||||||
|
<meta name="twitter:description" content="Hintergrund zum Beispiel-Projekt">
|
||||||
|
<link rel="canonical" href="http://localhost:8000/ueber/">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="#main" class="skip-link">Đến nội dung chính</a>
|
||||||
|
|
||||||
|
<header class="site-header">
|
||||||
|
<a href="index.html" class="brand" aria-label="Về trang chủ">Trang web của tôi</a>
|
||||||
|
<nav class="site-nav" aria-label="Điều hướng chính">
|
||||||
|
<a class="nav-gcbok__link" href="ueber.html">Über</a>
|
||||||
|
</nav>
|
||||||
|
<div class="lang-dropdown" aria-label="Chuyển ngôn ngữ">
|
||||||
|
<ul class="lang-dropdown__menu">
|
||||||
|
<li><a href="/de/" hreflang="de"><span>Deutsch</span> <span class="lang-dropdown__code">DE</span></a></li>
|
||||||
|
<li><a href="/en/" hreflang="en"><span>English</span> <span class="lang-dropdown__code">EN</span></a></li>
|
||||||
|
<li><a href="/vi/" hreflang="vi"><span>Tiếng Việt</span> <span class="lang-dropdown__code">VI</span></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main" tabindex="-1" data-pagefind-body>
|
||||||
|
<div class="container">
|
||||||
|
<h2 id="uber">Über</h2>
|
||||||
|
<p>GCBoK.WebStaticBuilder ist ein eigenständiges OSS-Tool der GitCover Commons.
|
||||||
|
Es erzeugt statische Websites ohne Server, Datenbank oder Build-Service.</p>
|
||||||
|
<div class="mermaid">graph TD
|
||||||
|
A[Markdown] --> B(Builder)
|
||||||
|
B --> C[HTML]
|
||||||
|
C --> D[Pagefind Index]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="page-nav" aria-label="Seitennavigation">
|
||||||
|
<span class="page-nav__link"> </span>
|
||||||
|
<span class="page-nav__link page-nav__link--next"> </span>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container">
|
||||||
|
<h4>TRANG WEB CỦA TÔI</h4>
|
||||||
|
<p class="muted">Một ví dụ cho GCBoK.WebStaticBuilder</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="b-ng-thu-t-ng.html">Bảng thuật ngữ</a></li>
|
||||||
|
<li><a href="impressum.html">Thông tin pháp lý</a></li>
|
||||||
|
</ul>
|
||||||
|
<p>© 2026 Trang web của tôi</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/assets/js/gcbok-theme.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"de": {
|
||||||
|
"aria": {
|
||||||
|
"skip": "Zum Hauptinhalt springen",
|
||||||
|
"brand": "Zur Startseite",
|
||||||
|
"main_nav": "Hauptnavigation",
|
||||||
|
"lang_switcher": "Sprachumschalter"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"brand": "Meine Website"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"col1_heading": "MEINE WEBSITE",
|
||||||
|
"col1_line2": "Ein Beispiel für GCBoK.WebStaticBuilder",
|
||||||
|
"col4_imprint": "Impressum",
|
||||||
|
"bottom_copyright": "© 2026 Meine Website"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"aria": {
|
||||||
|
"skip": "Skip to main content",
|
||||||
|
"brand": "Back to home",
|
||||||
|
"main_nav": "Main navigation",
|
||||||
|
"lang_switcher": "Language switcher"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"brand": "My Website"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"col1_heading": "MY WEBSITE",
|
||||||
|
"col1_line2": "An example for GCBoK.WebStaticBuilder",
|
||||||
|
"col4_imprint": "Imprint",
|
||||||
|
"bottom_copyright": "© 2026 My Website"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vi": {
|
||||||
|
"aria": {
|
||||||
|
"skip": "Đến nội dung chính",
|
||||||
|
"brand": "Về trang chủ",
|
||||||
|
"main_nav": "Điều hướng chính",
|
||||||
|
"lang_switcher": "Chuyển ngôn ngữ"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"brand": "Trang web của tôi"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"col1_heading": "TRANG WEB CỦA TÔI",
|
||||||
|
"col1_line2": "Một ví dụ cho GCBoK.WebStaticBuilder",
|
||||||
|
"col4_imprint": "Thông tin pháp lý",
|
||||||
|
"bottom_copyright": "© 2026 Trang web của tôi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
=== Run Started: 2026-07-11 12:00:07.880 ===
|
||||||
|
Language: de, Files: 3
|
||||||
|
Language: en, Files: 3
|
||||||
|
Language: vi, Files: 3
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="{{ lang }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
{{ seo_head }}
|
||||||
|
{{ json_ld }}
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="#main" class="skip-link">{{ i18n.aria.skip }}</a>
|
||||||
|
|
||||||
|
<header class="site-header">
|
||||||
|
<a href="index.html" class="brand" aria-label="{{ i18n.aria.brand }}">{{ i18n.nav.brand }}</a>
|
||||||
|
<nav class="site-nav" aria-label="{{ i18n.aria.main_nav }}">
|
||||||
|
{{ nav_links }}
|
||||||
|
</nav>
|
||||||
|
<div class="lang-dropdown" aria-label="{{ i18n.aria.lang_switcher }}">
|
||||||
|
<ul class="lang-dropdown__menu">
|
||||||
|
{{ lang_links }}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main" tabindex="-1" data-pagefind-body>
|
||||||
|
<div class="container">
|
||||||
|
{{ body }}
|
||||||
|
{{ page_nav }}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container">
|
||||||
|
<h4>{{ i18n.footer.col1_heading }}</h4>
|
||||||
|
<p class="muted">{{ i18n.footer.col1_line2 }}</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="{{ glossary_href }}">{{ glossary_label }}</a></li>
|
||||||
|
<li><a href="impressum.html">{{ i18n.footer.col4_imprint }}</a></li>
|
||||||
|
</ul>
|
||||||
|
<p>{{ i18n.footer.bottom_copyright }}</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/assets/js/gcbok-theme.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"site": {
|
||||||
|
"name": "Meine Website",
|
||||||
|
"title": { "de": "Meine Website", "en": "My Website", "vi": "Trang web của tôi" },
|
||||||
|
"description": {
|
||||||
|
"de": "Eine mehrsprachige, statische Website.",
|
||||||
|
"en": "A multilingual, static website.",
|
||||||
|
"vi": "Một trang web tĩnh, đa ngôn ngữ."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default_language": "de",
|
||||||
|
"languages": {
|
||||||
|
"de": { "name": "Deutsch", "og_locale": "de_DE", "href_lang": "de", "label": "Deutsch" },
|
||||||
|
"en": { "name": "English", "og_locale": "en_US", "href_lang": "en", "label": "English" },
|
||||||
|
"vi": { "name": "Tiếng Việt", "og_locale": "vi_VN", "href_lang": "vi", "label": "Tiếng Việt" }
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"content": "content",
|
||||||
|
"templates": "templates",
|
||||||
|
"assets": "assets",
|
||||||
|
"output": "dist",
|
||||||
|
"legal": "legal",
|
||||||
|
"i18n": "i18n.json"
|
||||||
|
},
|
||||||
|
"glossary_section": "glossar",
|
||||||
|
"glossary": {
|
||||||
|
"href": { "de": "glossar.html", "en": "glossary.html", "vi": "b-ng-thu-t-ng.html" },
|
||||||
|
"label": { "de": "Glossar", "en": "Glossary", "vi": "Bảng thuật ngữ" }
|
||||||
|
},
|
||||||
|
"pagefind": true,
|
||||||
|
"translator": { "provider": "azure", "key_env": "WEBSTATIC_TRANSLATOR_KEY" },
|
||||||
|
"environments": {
|
||||||
|
"work": { "base_url": "http://localhost:8000", "cdn": "/assets" },
|
||||||
|
"review": { "base_url": "https://review.gitcover.org", "cdn": "https://gitcover.org/assets" },
|
||||||
|
"staging": { "base_url": "https://gcbok.gitcover.org", "cdn": "https://gitcover.org/assets" },
|
||||||
|
"deploy": { "base_url": "https://gcbok.org", "cdn": "https://gitcover.org/assets" }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user