Files
homepage.veen.world/README.md
Kevin Veen-Birkenbach 2a35b2910a feat(i18n): serve every page in 30 languages
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>
2026-08-22 01:18:29 +02:00

206 lines
6.4 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PortUI 🖥️✨
[![GitHub Sponsors](https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-blue?logo=github)](https://github.com/sponsors/kevinveenbirkenbach) [![Patreon](https://img.shields.io/badge/Support-Patreon-orange?logo=patreon)](https://www.patreon.com/c/kevinveenbirkenbach) [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20me%20a%20Coffee-Funding-yellow?logo=buymeacoffee)](https://buymeacoffee.com/kevinveenbirkenbach) [![PayPal](https://img.shields.io/badge/Donate-PayPal-blue?logo=paypal)](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.
![PortUI screenshot](assets/img/screenshot.png)
> 🚀 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/) (Kevins 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 Kevins 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 sites 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! 🌟