Every one of these paths checked a string that the browser reinterprets afterwards. isSafeUrl now hangs the value on an <a> and reads back probe.protocol, so the check sees what the browser will see: a pre-parse test reads "javascript:" as a relative path and passes it, and the HTML parser then decodes it to "javascript:". marked passes raw HTML through and emits hrefs unescaped. renderMarkdown escapes the angle brackets before parsing, parses into an inert DOMParser document where no script runs and no image loads, and drops anchors and images whose scheme is not http, https or mailto. Blockquotes and <autolinks> stop working as a result; neither appears in the configuration. modalTitle and the alternatives list interpolated subitem.name and icon.class into innerHTML. Both are built as nodes now. name is a translatable key, so it arrives from the machine-written catalogues. The link kept its click handler and its class across popups, because one anchor serves all of them: a later, unrelated click opened whatever an earlier popup pointed at, and addEventListener stacked one handler per open. Both are reset per popup and the handler is assigned, not added. openIframe guards its own argument. Removing the href alone left the handler passing the raw URL on, and ?iframe= in the query string reaches the same sink with no configuration involved at all. Verified in headless Chromium: decimal and hex character references, 	- and 
-split schemes, reference-style links, raw HTML as a link's text, and the two name sinks all executed before these changes. injection.spec.js keeps all fifteen payloads. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PortUI 🖥️✨
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 (demo)
🌐 Another live example: 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 simpleconfig.yaml. - CLI Control
Manage Docker containers via theportfoliocommand.
🌐 Quick Access
- Local Preview:
http://127.0.0.1:5000
🏁 Getting Started
🔧 Prerequisites
- Docker & Docker Compose
- Basic Python & YAML knowledge
🛠️ Installation via Git
-
Clone & enter repo
git clone <repository_url> cd <repository_directory> -
Configure Copy
config.sample.yaml→config.yaml& customize. -
Build & run
docker-compose up --build -
Browse Open http://localhost:5000
📦 Installation via Kevin’s Package Manager
pkgmgr install portui
Once installed, the portui CLI is available system-wide.
🖥️ CLI Commands
portui --help
buildBuild the Docker imageupStart containers (with build)downStop & remove containersrun-devDev mode (hot-reload)run-prodProduction modelogsView container logsdevDocker-Compose dev environmentprodDocker-Compose prod environmentcleanupPrune stopped containers
🔧 YAML Configuration Guide
Define your site’s structure in config.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
childrenenables multi-level menus.linkreferences 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
instance — set LIBRETRANSLATE_URL in .env, then:
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_HOSTSin.envto your public hostname(s), comma-separated. Left empty, the app reflects whateverHostheader arrives into its canonical,hreflangand 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 claimshttp://.X-Forwarded-Hostis deliberately not trusted; setHostto the public name instead.
📜 License
Licensed under GNU AGPLv3. See LICENSE for details.
✍️ Author
Created by Kevin Veen-Birkenbach
Enjoy building your portfolio! 🌟
