my-homelab-configs/apps/website/blog.php

602 lines
25 KiB
PHP

<?php
require_once __DIR__ . '/lang_helper.php';
$activityKeys = [
'blog_activity_1',
'blog_activity_2',
'blog_activity_3',
'blog_activity_4',
'blog_activity_5',
'blog_activity_6',
'blog_activity_7',
'blog_activity_8',
'blog_activity_9',
];
$todoKeys = [
'blog_todo_1',
'blog_todo_2',
'blog_todo_3',
'blog_todo_4',
'blog_todo_5',
'blog_todo_6',
'blog_todo_7',
'blog_todo_8',
'blog_todo_9',
'blog_todo_10',
'blog_todo_11',
'blog_todo_12',
];
$stackKeys = [
'blog_stack_1',
'blog_stack_2',
'blog_stack_3',
'blog_stack_4',
'blog_stack_5',
'blog_stack_6',
'blog_stack_7',
'blog_stack_8',
'blog_stack_9',
'blog_stack_10',
'blog_stack_11',
];
$treeHref = 'homelab-tree.php?lang=' . urlencode($lang);
$giteaSourceBase = 'https://lab2025.duckdns.org/git/jv/my-homelab-configs/src/branch/main/';
$stackSourceLinks = [
'blog_stack_1' => [
['label' => 'lab.sh', 'path' => 'lab.sh'],
['label' => 'cluster/main.tf', 'path' => 'bootstrap/cluster/main.tf'],
],
'blog_stack_2' => [
['label' => 'cluster/main.tf', 'path' => 'bootstrap/cluster/main.tf'],
],
'blog_stack_3' => [
['label' => 'lab.sh', 'path' => 'lab.sh'],
['label' => 'cluster', 'path' => 'bootstrap/cluster/main.tf'],
['label' => 'platform', 'path' => 'bootstrap/platform/main.tf'],
['label' => 'apps', 'path' => 'bootstrap/apps/main.tf'],
['label' => 'edge', 'path' => 'bootstrap/edge/main.tf'],
],
'blog_stack_4' => [
['label' => 'platform/main.tf', 'path' => 'bootstrap/platform/main.tf'],
],
'blog_stack_5' => [
['label' => 'apps/main.tf', 'path' => 'bootstrap/apps/main.tf'],
],
'blog_stack_6' => [
['label' => 'edge/main.tf', 'path' => 'bootstrap/edge/main.tf'],
['label' => 'nginx template', 'path' => 'bootstrap/edge/templates/default.conf.tftpl'],
['label' => 'HAProxy template', 'path' => 'bootstrap/edge/templates/haproxy.cfg.tftpl'],
],
'blog_stack_7' => [
['label' => 'cv-theme.js', 'path' => 'apps/website/cv-theme.js'],
['label' => 'cv.php', 'path' => 'apps/website/cv.php'],
['label' => 'styles.css', 'path' => 'apps/website/styles.css'],
],
'blog_stack_8' => [
['label' => 'media-cruncher.js', 'path' => 'apps/demos-static/public/media-cruncher/media-cruncher.js'],
['label' => 'media-cruncher page', 'path' => 'apps/demos-static/public/media-cruncher/index.html'],
],
'blog_stack_9' => [
['label' => 'demo catalog', 'path' => 'apps/demos-static/public/index.html'],
['label' => 'network-quality.js', 'path' => 'apps/demos-static/public/network-quality/network-quality.js'],
['label' => 'dev-toolbelt.js', 'path' => 'apps/demos-static/public/dev-toolbelt/dev-toolbelt.js'],
['label' => 'architecture-simulator.js', 'path' => 'apps/demos-static/public/architecture-simulator/architecture-simulator.js'],
],
'blog_stack_10' => [
['label' => 'sentiment-sandbox.js', 'path' => 'apps/demos-static/public/sentiment-sandbox/sentiment-sandbox.js'],
['label' => 'model-drift.js', 'path' => 'apps/demos-static/public/model-drift/model-drift.js'],
['label' => 'privacy-redactor.js', 'path' => 'apps/demos-static/public/privacy-redactor/privacy-redactor.js'],
],
'blog_stack_11' => [
['label' => 'demos Dockerfile', 'path' => 'apps/demos-static/Dockerfile'],
['label' => 'demos web-app.yaml', 'path' => 'apps/demos-static/web-app.yaml'],
['label' => 'website demos.php', 'path' => 'apps/website/demos.php'],
],
];
function renderStackSourceLinks(string $stackKey, array $sourceLinks, string $sourceBase): void {
if (!isset($sourceLinks[$stackKey])) {
return;
}
echo '<div class="source-links"><span>Source:</span>';
foreach ($sourceLinks[$stackKey] as $sourceLink) {
$href = $sourceBase . $sourceLink['path'];
echo '<a href="' . htmlspecialchars($href) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($sourceLink['label']) . '</a>';
}
echo '</div>';
}
?>
<!DOCTYPE html>
<html lang="<?php echo $lang; ?>">
<head>
<meta charset="UTF-8">
<title><?php echo $text['blog_title']; ?> - <?php echo $text['name']; ?></title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<nav class="top-nav">
<div class="nav-left">Juvenal Diaz</div>
<div class="nav-right">
<?php foreach ($availableLangs as $code): ?>
<a href="blog.php?lang=<?php echo $code; ?>"><?php echo strtoupper($code); ?></a>
<?php endforeach; ?>
|
<a href="index.php?lang=<?php echo $lang; ?>"
data-translate data-key="nav_home"
data-en="<?php echo htmlspecialchars($en['nav_home']); ?>">
<?php echo $text['nav_home']; ?>
</a>
<a href="cv.php?lang=<?php echo $lang; ?>"
data-translate data-key="nav_cv"
data-en="<?php echo htmlspecialchars($en['nav_cv']); ?>">
<?php echo $text['nav_cv']; ?>
</a>
<a href="blog.php?lang=<?php echo $lang; ?>"
data-translate data-key="nav_blog"
data-en="<?php echo htmlspecialchars($en['nav_blog']); ?>">
<?php echo $text['nav_blog']; ?>
</a>
<a href="demos.php?lang=<?php echo $lang; ?>"
data-translate data-key="nav_demos"
data-en="<?php echo htmlspecialchars($en['nav_demos']); ?>">
<?php echo $text['nav_demos']; ?>
</a>
</div>
</nav>
<main class="blog-page">
<section class="blog-hero">
<p class="blog-kicker"
data-translate data-key="blog_kicker"
data-en="<?php echo htmlspecialchars($en['blog_kicker']); ?>">
<?php echo $text['blog_kicker']; ?>
</p>
<h1 data-translate data-key="blog_title"
data-en="<?php echo htmlspecialchars($en['blog_title']); ?>">
<?php echo $text['blog_title']; ?>
</h1>
<p class="blog-subtitle"
data-translate data-key="blog_subtitle"
data-en="<?php echo htmlspecialchars($en['blog_subtitle']); ?>">
<?php echo $text['blog_subtitle']; ?>
</p>
</section>
<section class="architecture-section" aria-labelledby="architecture-title">
<div class="section-heading">
<p class="section-kicker"
data-translate data-key="blog_arch_kicker"
data-en="<?php echo htmlspecialchars($en['blog_arch_kicker']); ?>">
<?php echo $text['blog_arch_kicker']; ?>
</p>
<h2 id="architecture-title"
data-translate data-key="blog_arch_title"
data-en="<?php echo htmlspecialchars($en['blog_arch_title']); ?>">
<?php echo $text['blog_arch_title']; ?>
</h2>
<p data-translate data-key="blog_arch_intro"
data-en="<?php echo htmlspecialchars($en['blog_arch_intro']); ?>">
<?php echo $text['blog_arch_intro']; ?>
</p>
</div>
<div class="diagram-shell" aria-label="Professional homelab architecture diagram">
<svg class="homelab-map" viewBox="0 0 1120 720" role="img" aria-labelledby="homelab-map-title homelab-map-desc">
<title id="homelab-map-title">Homelab architecture map</title>
<desc id="homelab-map-desc">Git push enters Gitea, Gitea Actions validates and builds images, OpenTofu manages the cluster, Argo CD syncs manifests, and the OCI edge routes traffic over Tailscale into Kubernetes services.</desc>
<defs>
<marker id="map-arrow" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto">
<path d="M2,2 L10,6 L2,10 Z" fill="#2b6cb0"></path>
</marker>
</defs>
<rect class="diagram-zone diagram-zone-source" x="24" y="40" width="310" height="640" rx="12"></rect>
<text class="diagram-zone-title" x="46" y="74">Source, validation, and images</text>
<rect class="diagram-zone diagram-zone-platform" x="382" y="40" width="340" height="640" rx="12"></rect>
<text class="diagram-zone-title" x="404" y="74">Debian node 192.168.100.68</text>
<rect class="diagram-zone diagram-zone-runtime" x="770" y="40" width="326" height="640" rx="12"></rect>
<text class="diagram-zone-title" x="792" y="74">Edge access and workloads</text>
<g class="diagram-node node-accent-blue" transform="translate(54 110)">
<rect width="240" height="74" rx="8"></rect>
<text x="18" y="28">Developer laptop</text>
<text class="diagram-small" x="18" y="52">edit, test, push main</text>
</g>
<g class="diagram-node node-accent-blue" transform="translate(54 218)">
<rect width="240" height="82" rx="8"></rect>
<text x="18" y="27">Gitea repository</text>
<text class="diagram-small" x="18" y="50">https://lab2025.duckdns.org/git/</text>
<text class="diagram-small" x="18" y="68">main is the release branch</text>
</g>
<g class="diagram-node node-accent-teal" transform="translate(54 338)">
<rect width="240" height="82" rx="8"></rect>
<text x="18" y="27">Gitea Actions runner</text>
<text class="diagram-small" x="18" y="50">Debian hosted runner</text>
<text class="diagram-small" x="18" y="68">custom checkout for /git/ path</text>
</g>
<g class="diagram-node node-accent-red" transform="translate(54 458)">
<rect width="240" height="82" rx="8"></rect>
<text x="18" y="27">Validation gates</text>
<text class="diagram-small" x="18" y="50">Gitleaks secret scan</text>
<text class="diagram-small" x="18" y="68">Trivy IaC and image posture</text>
</g>
<g class="diagram-node node-accent-green" transform="translate(54 578)">
<rect width="240" height="70" rx="8"></rect>
<text x="18" y="27">Buildx image build</text>
<text class="diagram-small" x="18" y="50">linux/arm64 website + demos</text>
</g>
<g class="diagram-node node-accent-purple" transform="translate(412 108)">
<rect width="280" height="82" rx="8"></rect>
<text x="18" y="27">OpenTofu + lab.sh</text>
<text class="diagram-small" x="18" y="50">infra, platform, apps, edge</text>
<text class="diagram-small" x="18" y="68">repeatable apply path</text>
</g>
<g class="diagram-node node-accent-blue" transform="translate(412 222)">
<rect width="280" height="82" rx="8"></rect>
<text x="18" y="27">kubeadm control plane</text>
<text class="diagram-small" x="18" y="50">API server, scheduler, controller</text>
<text class="diagram-small" x="18" y="68">Calico pod networking</text>
</g>
<g class="diagram-node node-accent-teal" transform="translate(412 336)">
<rect width="280" height="82" rx="8"></rect>
<text x="18" y="27">GitOps mirror</text>
<text class="diagram-small" x="18" y="50">validated commit copied locally</text>
<text class="diagram-small" x="18" y="68">Argo CD reads deploy state</text>
</g>
<g class="diagram-node node-accent-green" transform="translate(412 450)">
<rect width="280" height="82" rx="8"></rect>
<text x="18" y="27">Argo CD</text>
<text class="diagram-small" x="18" y="50">container-registry, website</text>
<text class="diagram-small" x="18" y="68">gitea and demos-static apps</text>
</g>
<g class="diagram-node node-accent-orange" transform="translate(412 564)">
<rect width="280" height="82" rx="8"></rect>
<text x="18" y="27">Storage and backups</text>
<text class="diagram-small" x="18" y="50">OpenEBS retained hostpath PVs</text>
<text class="diagram-small" x="18" y="68">Gitea dumps and external SSD</text>
</g>
<g class="diagram-node node-accent-blue" transform="translate(800 106)">
<rect width="258" height="96" rx="8"></rect>
<text x="18" y="28">OCI edge host</text>
<text class="diagram-small" x="18" y="52">nginx, HAProxy, Varnish, Squid</text>
<text class="diagram-small" x="18" y="70">TLS, routing, caching</text>
<text class="diagram-small" x="18" y="88">public DNS entry point</text>
</g>
<g class="diagram-node node-accent-purple" transform="translate(800 246)">
<rect width="258" height="82" rx="8"></rect>
<text x="18" y="27">Tailscale + NodePorts</text>
<text class="diagram-small" x="18" y="50">30080 website, 30081 demos</text>
<text class="diagram-small" x="18" y="68">30300 Gitea service path</text>
</g>
<g class="diagram-node node-accent-green" transform="translate(800 366)">
<rect width="258" height="96" rx="8"></rect>
<text x="18" y="28">Raspberry Pi 192.168.100.89</text>
<text class="diagram-small" x="18" y="52">arm64 Kubernetes worker</text>
<text class="diagram-small" x="18" y="70">website-production pods</text>
<text class="diagram-small" x="18" y="88">demos-static and lab apps</text>
</g>
<g class="diagram-node node-accent-teal" transform="translate(800 506)">
<rect width="258" height="82" rx="8"></rect>
<text x="18" y="27">Local registry :30500</text>
<text class="diagram-small" x="18" y="50">php-website and demos-static</text>
<text class="diagram-small" x="18" y="68">pulled by arm64 workloads</text>
</g>
<path class="diagram-link" d="M174 184 L174 218"></path>
<path class="diagram-link" d="M174 300 L174 338"></path>
<path class="diagram-link" d="M174 420 L174 458"></path>
<path class="diagram-link" d="M174 540 L174 578"></path>
<path class="diagram-link" d="M294 616 C346 616 352 149 412 149"></path>
<path class="diagram-link" d="M294 616 C372 616 722 560 800 547"></path>
<path class="diagram-link" d="M294 379 C340 379 360 149 412 149"></path>
<path class="diagram-link" d="M294 259 C348 259 360 377 412 377"></path>
<path class="diagram-link" d="M552 304 L552 336"></path>
<path class="diagram-link" d="M552 418 L552 450"></path>
<path class="diagram-link" d="M692 491 C748 491 744 414 800 414"></path>
<path class="diagram-link" d="M929 202 L929 246"></path>
<path class="diagram-link" d="M929 328 L929 366"></path>
<path class="diagram-link" d="M929 506 L929 462"></path>
<text class="diagram-link-label" x="184" y="205">push</text>
<text class="diagram-link-label" x="196" y="327">workflow</text>
<text class="diagram-link-label" x="200" y="448">scan</text>
<text class="diagram-link-label" x="205" y="568">build</text>
<text class="diagram-link-label" x="320" y="124">apply</text>
<text class="diagram-link-label" x="346" y="346">validated Git</text>
<text class="diagram-link-label" x="710" y="462">sync apps</text>
<text class="diagram-link-label" x="934" y="232">secure tunnel</text>
<text class="diagram-link-label" x="934" y="352">service traffic</text>
<text class="diagram-link-label" x="946" y="492">image pulls</text>
</svg>
</div>
<div class="diagram-actions">
<p class="diagram-caption"
data-translate data-key="blog_arch_caption"
data-en="<?php echo htmlspecialchars($en['blog_arch_caption']); ?>">
<?php echo $text['blog_arch_caption']; ?>
</p>
<a class="diagram-fun-link" href="<?php echo htmlspecialchars($treeHref); ?>"
data-translate data-key="blog_arch_fun_link"
data-en="<?php echo htmlspecialchars($en['blog_arch_fun_link']); ?>">
<?php echo $text['blog_arch_fun_link']; ?>
</a>
</div>
</section>
<section class="conversation" aria-label="Homelab build conversation">
<article class="message question">
<div class="speaker"
data-translate data-key="blog_speaker_question"
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
<?php echo $text['blog_speaker_question']; ?>
</div>
<p data-translate data-key="blog_q1"
data-en="<?php echo htmlspecialchars($en['blog_q1']); ?>">
<?php echo $text['blog_q1']; ?>
</p>
</article>
<article class="message answer">
<div class="speaker"
data-translate data-key="blog_speaker_answer"
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
<?php echo $text['blog_speaker_answer']; ?>
</div>
<p data-translate data-key="blog_a1"
data-en="<?php echo htmlspecialchars($en['blog_a1']); ?>">
<?php echo $text['blog_a1']; ?>
</p>
</article>
<article class="message question">
<div class="speaker"
data-translate data-key="blog_speaker_question"
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
<?php echo $text['blog_speaker_question']; ?>
</div>
<p data-translate data-key="blog_q2"
data-en="<?php echo htmlspecialchars($en['blog_q2']); ?>">
<?php echo $text['blog_q2']; ?>
</p>
</article>
<article class="message answer">
<div class="speaker"
data-translate data-key="blog_speaker_answer"
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
<?php echo $text['blog_speaker_answer']; ?>
</div>
<p data-translate data-key="blog_a2"
data-en="<?php echo htmlspecialchars($en['blog_a2']); ?>">
<?php echo $text['blog_a2']; ?>
</p>
</article>
<article class="message question">
<div class="speaker"
data-translate data-key="blog_speaker_question"
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
<?php echo $text['blog_speaker_question']; ?>
</div>
<p data-translate data-key="blog_q3"
data-en="<?php echo htmlspecialchars($en['blog_q3']); ?>">
<?php echo $text['blog_q3']; ?>
</p>
</article>
<article class="message answer">
<div class="speaker"
data-translate data-key="blog_speaker_answer"
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
<?php echo $text['blog_speaker_answer']; ?>
</div>
<p data-translate data-key="blog_a3"
data-en="<?php echo htmlspecialchars($en['blog_a3']); ?>">
<?php echo $text['blog_a3']; ?>
</p>
</article>
<article class="message question">
<div class="speaker"
data-translate data-key="blog_speaker_question"
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
<?php echo $text['blog_speaker_question']; ?>
</div>
<p data-translate data-key="blog_q4"
data-en="<?php echo htmlspecialchars($en['blog_q4']); ?>">
<?php echo $text['blog_q4']; ?>
</p>
</article>
<article class="message answer">
<div class="speaker"
data-translate data-key="blog_speaker_answer"
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
<?php echo $text['blog_speaker_answer']; ?>
</div>
<p data-translate data-key="blog_a4"
data-en="<?php echo htmlspecialchars($en['blog_a4']); ?>">
<?php echo $text['blog_a4']; ?>
</p>
</article>
<article class="message question">
<div class="speaker"
data-translate data-key="blog_speaker_question"
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
<?php echo $text['blog_speaker_question']; ?>
</div>
<p data-translate data-key="blog_q5"
data-en="<?php echo htmlspecialchars($en['blog_q5']); ?>">
<?php echo $text['blog_q5']; ?>
</p>
</article>
<article class="message answer">
<div class="speaker"
data-translate data-key="blog_speaker_answer"
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
<?php echo $text['blog_speaker_answer']; ?>
</div>
<p data-translate data-key="blog_a5"
data-en="<?php echo htmlspecialchars($en['blog_a5']); ?>">
<?php echo $text['blog_a5']; ?>
</p>
</article>
<article class="message question">
<div class="speaker"
data-translate data-key="blog_speaker_question"
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
<?php echo $text['blog_speaker_question']; ?>
</div>
<p data-translate data-key="blog_q6"
data-en="<?php echo htmlspecialchars($en['blog_q6']); ?>">
<?php echo $text['blog_q6']; ?>
</p>
</article>
<article class="message answer">
<div class="speaker"
data-translate data-key="blog_speaker_answer"
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
<?php echo $text['blog_speaker_answer']; ?>
</div>
<p data-translate data-key="blog_a6"
data-en="<?php echo htmlspecialchars($en['blog_a6']); ?>">
<?php echo $text['blog_a6']; ?>
</p>
</article>
<article class="message question">
<div class="speaker"
data-translate data-key="blog_speaker_question"
data-en="<?php echo htmlspecialchars($en['blog_speaker_question']); ?>">
<?php echo $text['blog_speaker_question']; ?>
</div>
<p data-translate data-key="blog_q7"
data-en="<?php echo htmlspecialchars($en['blog_q7']); ?>">
<?php echo $text['blog_q7']; ?>
</p>
</article>
<article class="message answer">
<div class="speaker"
data-translate data-key="blog_speaker_answer"
data-en="<?php echo htmlspecialchars($en['blog_speaker_answer']); ?>">
<?php echo $text['blog_speaker_answer']; ?>
</div>
<p data-translate data-key="blog_a7"
data-en="<?php echo htmlspecialchars($en['blog_a7']); ?>">
<?php echo $text['blog_a7']; ?>
</p>
</article>
</section>
<section class="activity-log" aria-labelledby="activity-log-title">
<div class="section-heading">
<p class="section-kicker"
data-translate data-key="blog_activity_kicker"
data-en="<?php echo htmlspecialchars($en['blog_activity_kicker']); ?>">
<?php echo $text['blog_activity_kicker']; ?>
</p>
<h2 id="activity-log-title"
data-translate data-key="blog_activity_title"
data-en="<?php echo htmlspecialchars($en['blog_activity_title']); ?>">
<?php echo $text['blog_activity_title']; ?>
</h2>
<p data-translate data-key="blog_activity_intro"
data-en="<?php echo htmlspecialchars($en['blog_activity_intro']); ?>">
<?php echo $text['blog_activity_intro']; ?>
</p>
</div>
<ol class="activity-list">
<?php foreach ($activityKeys as $index => $activityKey): ?>
<li>
<span class="activity-number"><?php echo str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT); ?></span>
<span data-translate data-key="<?php echo htmlspecialchars($activityKey); ?>"
data-en="<?php echo htmlspecialchars($en[$activityKey]); ?>">
<?php echo $text[$activityKey]; ?>
</span>
</li>
<?php endforeach; ?>
</ol>
</section>
<section class="tech-notes">
<h2 data-translate data-key="blog_stack_title"
data-en="<?php echo htmlspecialchars($en['blog_stack_title']); ?>">
<?php echo $text['blog_stack_title']; ?>
</h2>
<ul>
<?php foreach ($stackKeys as $stackKey): ?>
<li>
<span data-translate data-key="<?php echo htmlspecialchars($stackKey); ?>"
data-en="<?php echo htmlspecialchars($en[$stackKey]); ?>">
<?php echo $text[$stackKey]; ?>
</span>
<?php renderStackSourceLinks($stackKey, $stackSourceLinks, $giteaSourceBase); ?>
</li>
<?php endforeach; ?>
</ul>
</section>
<section class="homelab-todo" aria-labelledby="homelab-todo-title">
<div class="section-heading">
<p class="section-kicker"
data-translate data-key="blog_todo_kicker"
data-en="<?php echo htmlspecialchars($en['blog_todo_kicker']); ?>">
<?php echo $text['blog_todo_kicker']; ?>
</p>
<h2 id="homelab-todo-title"
data-translate data-key="blog_todo_title"
data-en="<?php echo htmlspecialchars($en['blog_todo_title']); ?>">
<?php echo $text['blog_todo_title']; ?>
</h2>
<p data-translate data-key="blog_todo_intro"
data-en="<?php echo htmlspecialchars($en['blog_todo_intro']); ?>">
<?php echo $text['blog_todo_intro']; ?>
</p>
</div>
<ul class="todo-list">
<?php foreach ($todoKeys as $todoKey): ?>
<li>
<span class="todo-checkbox" aria-hidden="true"></span>
<span data-translate data-key="<?php echo htmlspecialchars($todoKey); ?>"
data-en="<?php echo htmlspecialchars($en[$todoKey]); ?>">
<?php echo $text[$todoKey]; ?>
</span>
</li>
<?php endforeach; ?>
</ul>
</section>
</main>
<script>
const OTHER_PAGES = ['/index.php', '/cv.php', '/homelab-tree.php'];
</script>
<?php require_once __DIR__ . '/partials/translation_ui.php'; ?>
</body>
</html>