mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2026-08-24 05:04:33 +00:00
The interface ships translated; page content stays English until a LibreTranslate instance fills app/i18n/content/ through make i18n. A string without a catalogue entry falls back to its English source, so a half-filled catalogue degrades instead of breaking. Translation runs after ConfigurationResolver.resolve_links(), on a copy. resolve_links matches by the `name` field, so translating it beforehand would break every `link:` reference in the configuration. negotiate() normalises to the primary subtag itself. Werkzeug's best_match returns an exact match before it considers a primary-tag fallback, so the Chrome default `de-DE,en;q=0.8` resolves to English there. "/" carries Vary: Accept-Language, without which a shared cache pins the first visitor's language for everyone. The route rule lists the known codes as a converter argument. A bare "/<lang>/" answers /robots.txt and /favicon.ico with a permanently cacheable 308 to their trailing-slash form. Templates gain lang, dir, the RTL stylesheet, a canonical URL and 30 hreflang alternates. Those are the first external URLs in this app: ProxyFix takes the scheme from X-Forwarded-Proto so they do not claim http:// behind a TLS-terminating proxy, X-Forwarded-Host stays untrusted because nginx passes a client-supplied one through, and TRUSTED_HOSTS lets Flask reject a forged Host outright. Flask only autoescapes .html/.htm/.xml/.xhtml/.svg, so every *.html.j2 template interpolated configuration raw. Enabling it changes two lines of the shipped page, both an apostrophe. read_catalog degrades an unreadable catalogue to English rather than serving a 500, and drops non-string entries that would otherwise render as "42". i18n_sync writes atomically, never overwrites an existing entry, refuses to touch a catalogue it could not parse, and leaves the file alone when a run translated nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
206 lines
6.4 KiB
Markdown
206 lines
6.4 KiB
Markdown
# PortUI 🖥️✨
|
||
|
||
[](https://github.com/sponsors/kevinveenbirkenbach) [](https://www.patreon.com/c/kevinveenbirkenbach) [](https://buymeacoffee.com/kevinveenbirkenbach) [](https://s.veen.world/paypaldonate)
|
||
|
||
A lightweight, Docker-powered portfolio/landing-page generator—fully customizable via YAML! Showcase your projects, skills, and online presence in minutes.
|
||
|
||

|
||
|
||
> 🚀 You can also pair PortUI with JavaScript for sleek, web-based desktop-style interfaces.
|
||
> 💻 Example in action: [CyMaIS.Cloud](https://cymais.cloud/) (demo)
|
||
> 🌐 Another live example: [veen.world](https://www.veen.world/) (Kevin’s personal site)
|
||
|
||
---
|
||
|
||
## ✨ Key Features
|
||
|
||
- **Dynamic Navigation**
|
||
Create dropdowns & nested menus with ease.
|
||
- **Customizable Cards**
|
||
Highlight skills, projects, or services—with icons, titles, and links.
|
||
- **Smart Cache Management**
|
||
Auto-cache assets for lightning-fast loading.
|
||
- **Responsive Design**
|
||
Built on Bootstrap; looks great on desktop, tablet & mobile.
|
||
- **30 Languages**
|
||
Browser-negotiated, RTL-aware, with machine translation for your own content.
|
||
- **YAML-Driven**
|
||
All content & structure defined in a simple `config.yaml`.
|
||
- **CLI Control**
|
||
Manage Docker containers via the `portfolio` command.
|
||
|
||
---
|
||
|
||
## 🌐 Quick Access
|
||
|
||
- **Local Preview:**
|
||
[http://127.0.0.1:5000](http://127.0.0.1:5000)
|
||
|
||
---
|
||
|
||
## 🏁 Getting Started
|
||
|
||
### 🔧 Prerequisites
|
||
|
||
- Docker & Docker Compose
|
||
- Basic Python & YAML knowledge
|
||
|
||
### 🛠️ Installation via Git
|
||
|
||
1. **Clone & enter repo**
|
||
```bash
|
||
git clone <repository_url>
|
||
cd <repository_directory>
|
||
```
|
||
|
||
2. **Configure**
|
||
Copy `config.sample.yaml` → `config.yaml` & customize.
|
||
3. **Build & run**
|
||
|
||
```bash
|
||
docker-compose up --build
|
||
```
|
||
4. **Browse**
|
||
Open [http://localhost:5000](http://localhost:5000)
|
||
|
||
### 📦 Installation via Kevin’s Package Manager
|
||
|
||
```bash
|
||
pkgmgr install portui
|
||
```
|
||
|
||
Once installed, the `portui` CLI is available system-wide.
|
||
|
||
---
|
||
|
||
## 🖥️ CLI Commands
|
||
|
||
```bash
|
||
portui --help
|
||
```
|
||
|
||
* `build` Build the Docker image
|
||
* `up` Start containers (with build)
|
||
* `down` Stop & remove containers
|
||
* `run-dev` Dev mode (hot-reload)
|
||
* `run-prod` Production mode
|
||
* `logs` View container logs
|
||
* `dev` Docker-Compose dev environment
|
||
* `prod` Docker-Compose prod environment
|
||
* `cleanup` Prune stopped containers
|
||
|
||
---
|
||
|
||
## 🔧 YAML Configuration Guide
|
||
|
||
Define your site’s structure in `config.yaml`:
|
||
|
||
```yaml
|
||
accounts:
|
||
name: Online Accounts
|
||
description: Discover my online presence.
|
||
icon:
|
||
class: fa-solid fa-users
|
||
children:
|
||
- name: Channels
|
||
description: Platforms where I share content.
|
||
icon:
|
||
class: fas fa-newspaper
|
||
children:
|
||
- name: Mastodon
|
||
description: Follow me on Mastodon.
|
||
icon:
|
||
class: fa-brands fa-mastodon
|
||
url: https://microblog.veen.world/@kevinveenbirkenbach
|
||
identifier: "@kevinveenbirkenbach@microblog.veen.world"
|
||
cards:
|
||
- icon:
|
||
source: https://cloud.veen.world/s/logo_agile_coach_512x512/download
|
||
title: Agile Coach
|
||
text: I lead agile transformations and improve team dynamics through Scrum and Agile Coaching.
|
||
url: https://www.agile-coach.world
|
||
link_text: www.agile-coach.world
|
||
|
||
company:
|
||
title: Kevin Veen-Birkenbach
|
||
subtitle: Consulting & Coaching Solutions
|
||
logo:
|
||
source: https://cloud.veen.world/s/logo_face_512x512/download
|
||
favicon:
|
||
source: https://cloud.veen.world/s/veen_world_favicon/download
|
||
address:
|
||
street: Afrikanische Straße 43
|
||
postal_code: DE-13351
|
||
city: Berlin
|
||
country: Germany
|
||
imprint_url: https://s.veen.world/imprint
|
||
```
|
||
|
||
* **`children`** enables multi-level menus.
|
||
* **`link`** references other YAML paths to avoid duplication.
|
||
|
||
---
|
||
|
||
## 🌍 Languages
|
||
|
||
The interface ships in 30 languages. `/` serves the best match for the visitor's
|
||
`Accept-Language` header, `/<code>/` forces one, and a switcher in the navbar
|
||
lists them all. Right-to-left languages (`ar`, `fa`, `he`, `ur`) get
|
||
`dir="rtl"` and Bootstrap's RTL stylesheet automatically.
|
||
|
||
Translations live in two catalogues, both keyed by the English source string:
|
||
|
||
| Path | Tracked | Holds |
|
||
| --- | --- | --- |
|
||
| `app/i18n/ui/<code>.yaml` | yes | Interface strings, shipped complete for all 29 non-English languages. English is the source and has no file. |
|
||
| `app/i18n/content/<code>.yaml` | no | Your `config.yaml` prose, generated per deployment. |
|
||
|
||
A string with no catalogue entry falls back to English, so a half-filled
|
||
catalogue degrades instead of breaking.
|
||
|
||
Fill the content catalogues from a [LibreTranslate](https://libretranslate.com/)
|
||
instance — set `LIBRETRANSLATE_URL` in `.env`, then:
|
||
|
||
```bash
|
||
make i18n
|
||
```
|
||
|
||
Existing entries are never overwritten, so corrections you make by hand survive
|
||
later runs. Only prose (`description`, `text`, `warning`, `info`, `subtitel`) is
|
||
filled automatically; `name` and `title` are left to you, because a machine
|
||
cannot tell the menu label "Pictures" from the brand "Mastodon". Write those
|
||
into the content catalogue yourself when you want them translated.
|
||
|
||
---
|
||
|
||
## 🚢 Production Deployment
|
||
|
||
* Use a reverse proxy (NGINX/Apache).
|
||
* Secure with SSL/TLS.
|
||
* Swap to a production database if needed.
|
||
|
||
Because every page carries a canonical URL and 30 `hreflang` alternates, two
|
||
details of the proxy setup now matter:
|
||
|
||
* **Set `TRUSTED_HOSTS`** in `.env` to your public hostname(s), comma-separated.
|
||
Left empty, the app reflects whatever `Host` header arrives into its canonical,
|
||
`hreflang` and redirect URLs — so a shared cache in front of it can be made to
|
||
store a redirect pointing somewhere else.
|
||
* **Have the proxy send `X-Forwarded-Proto`.** Without it the app cannot know TLS
|
||
terminated upstream and every canonical URL claims `http://`. `X-Forwarded-Host`
|
||
is deliberately *not* trusted; set `Host` to the public name instead.
|
||
|
||
---
|
||
|
||
## 📜 License
|
||
|
||
Licensed under **GNU AGPLv3**. See [LICENSE](./LICENSE) for details.
|
||
|
||
---
|
||
|
||
## ✍️ Author
|
||
|
||
Created by [Kevin Veen-Birkenbach](https://www.veen.world/)
|
||
|
||
Enjoy building your portfolio! 🌟
|