--- /dev/null
+module.exports = function(eleventyConfig) {
+ // Passthrough static files
+ eleventyConfig.addPassthroughCopy({ "site/public": "/" }); // favicon.svg → /favicon.svg
+ eleventyConfig.addPassthroughCopy({ "site/assets": "assets" });
+
+ return {
+ dir: {
+ input: "site",
+ includes: "_includes",
+ data: "_data",
+ output: "dist"
+ },
+ htmlTemplateEngine: "njk",
+ markdownTemplateEngine: "njk"
+ }
+}
--- /dev/null
+node_modules/
+dist/
+.cache/
+.DS_Store
--- /dev/null
+{
+ "name": "zndr-11ty",
+ "private": true,
+ "scripts": {
+ "dev": "eleventy --serve --input=site --output=dist",
+ "build": "eleventy --input=site --output=dist"
+ },
+ "devDependencies": {
+ "@11ty/eleventy": "^2.0.0"
+ }
+}
--- /dev/null
+[
+ {
+ "name": "Nextcloud",
+ "href": "https://nextcloud.zndr.dk",
+ "desc": "Files, calendars, contacts.",
+ "tag": "LAN/SSL"
+ },
+ {
+ "name": "Webmail",
+ "href": "https://mail.zndr.dk",
+ "desc": "Postfix \u00b7 Dovecot \u00b7 Roundcube.",
+ "tag": "IMAP/SMTP"
+ },
+ {
+ "name": "Pi-hole",
+ "href": "https://dns.zndr.dk/admin/",
+ "desc": "DNS-level ad/tracker blocking.",
+ "tag": "LAN-only"
+ },
+ {
+ "name": "Home Assistant",
+ "href": "https://ha.zndr.dk",
+ "desc": "Automation & dashboards.",
+ "tag": "Home"
+ },
+ {
+ "name": "Gitolite",
+ "href": "https://git.zndr.dk",
+ "desc": "Private repositories.",
+ "tag": "Git"
+ },
+ {
+ "name": "Status",
+ "href": "https://status.zndr.dk",
+ "desc": "Uptime & incidents.",
+ "tag": "Public"
+ },
+ {
+ "name": "Notes / Blog",
+ "href": "https://zndr.dk/blog/",
+ "desc": "Occasional write-ups.",
+ "tag": "Notes"
+ },
+ {
+ "name": "Contact",
+ "href": "mailto:post@zndr.dk",
+ "desc": "post@zndr.dk",
+ "tag": "Contact"
+ }
+]
--- /dev/null
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>{{ title or 'zndr.dk' }}</title>
+ <meta name="description" content="Minimal start page with quick links to my self‑hosted services." />
+ <meta name="color-scheme" content="light dark" />
+ <meta name="theme-color" content="#111111" />
+ <link rel="icon" href="/favicon.svg">
+ <link rel="stylesheet" href="/assets/styles.css">
+</head>
+<body>
+ <div class="container">
+ <header>
+ <a class="brand" href="/" aria-label="Home">
+ <span class="logo" aria-hidden="true">
+ <svg viewBox="0 0 24 24"><path d="M4 18h16L12 3 4 18zm2.5 3h11a1.5 1.5 0 0 0 0-3h-11a1.5 1.5 0 0 0 0 3z"/></svg>
+ </span>
+ <div>
+ <div class="title">zndr.dk</div>
+ <div class="subtitle">Self‑hosted services • Denmark</div>
+ </div>
+ </a>
+ </header>
+
+ {% block content %}{% endblock %}
+
+ <footer>
+ <div>© {{ now | date("yyyy") }} Jannik • Copenhagen</div>
+ <div class="foot-links">
+ <a href="/pgp.txt">PGP</a>
+ <a href="/keys.txt">SSH Keys</a>
+ <a href="/about.html">About</a>
+ </div>
+ </footer>
+ </div>
+
+ <script>
+ // Slash to focus any #filter found on page
+ window.addEventListener('keydown', (e) => {
+ const f = document.getElementById('filter');
+ if (!f) return;
+ if (e.key === '/' && document.activeElement !== f) { e.preventDefault(); f.focus(); }
+ });
+ </script>
+</body>
+</html>
--- /dev/null
+---
+layout: layouts/base.njk
+title: About — zndr.dk
+---
+
+# About
+
+Tiny landing page for links to self‑hosted services. Reach me at [post@zndr.dk](mailto:post@zndr.dk).
--- /dev/null
+:root { --bg: #f6f7fb; --card: #ffffff; --fg: #0f172a; --muted: #475569; --ring: #94a3b8; --accent: #2563eb; }
+@media (prefers-color-scheme: dark) { :root { --bg: #0b0f1a; --card: #0f172a; --fg: #e5e7eb; --muted: #94a3b8; --ring: #334155; } }
+*{box-sizing:border-box} html,body{height:100%}
+body{margin:0;font:16px/1.5 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Helvetica,Arial,"Apple Color Emoji","Segoe UI Emoji";color:var(--fg);background:radial-gradient(1200px 800px at 80% -10%, rgba(34,197,94,.08), transparent 50%),radial-gradient(800px 600px at -10% 10%, rgba(37,99,235,.08), transparent 50%),var(--bg)}
+.container{max-width:1100px;margin:0 auto;padding:32px 20px 48px}
+header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:28px}
+.brand{display:flex;align-items:center;gap:12px;text-decoration:none;color:inherit}
+.logo{width:36px;height:36px;border-radius:10px;background:#111;display:grid;place-items:center;box-shadow:0 8px 24px rgba(0,0,0,.25)}
+.logo svg{width:22px;height:22px;fill:#fff}
+.title{font-weight:700;letter-spacing:.2px}
+.subtitle{color:var(--muted);font-size:.95rem}
+.search{width:100%;margin:18px 0 24px}
+.search input{width:100%;padding:12px 14px;border-radius:12px;border:1px solid var(--ring);background:var(--card);color:var(--fg)}
+.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:14px}
+.card{position:relative;border-radius:14px;background:linear-gradient(180deg,rgba(255,255,255,.65),rgba(255,255,255,.35));background-color:var(--card);border:1px solid var(--ring);padding:16px 14px 14px;text-decoration:none;color:inherit;transition:transform .12s ease,box-shadow .12s ease,border-color .12s ease;box-shadow:0 6px 22px rgba(2,6,23,.18)}
+.card:hover{transform:translateY(-2px);border-color:var(--accent);box-shadow:0 10px 28px rgba(37,99,235,.22)}
+.card h3{margin:0 0 6px;font-size:1rem}
+.card p{margin:0;color:var(--muted);font-size:.92rem}
+.chip{position:absolute;top:10px;right:10px;font-size:.75rem;border:1px solid var(--ring);padding:.1rem .5rem;border-radius:999px;color:var(--muted)}
+footer{margin-top:36px;display:flex;gap:12px;flex-wrap:wrap;align-items:center;justify-content:space-between;color:var(--muted);font-size:.9rem}
+.foot-links{display:flex;gap:12px;flex-wrap:wrap}
+.foot-links a{color:inherit;text-decoration:none;border-bottom:1px dashed var(--ring)}
--- /dev/null
+---
+layout: layouts/base.njk
+title: zndr.dk — links
+---
+
+<div class="search" role="search">
+ <input id="filter" placeholder="Filter services… (press / to focus)" autocomplete="off" aria-label="Filter services" />
+</div>
+
+<main class="grid" id="grid" aria-live="polite">
+ {% for s in services %}
+ <a class="card" href="{{ s.href }}" rel="noopener">
+ {% if s.tag %}<span class="chip">{{ s.tag }}</span>{% endif %}
+ <h3>{{ s.name }}</h3>
+ <p>{{ s.desc }}</p>
+ </a>
+ {% endfor %}
+</main>
+
+<script>
+ (function(){
+ const f = document.getElementById('filter');
+ const cards = Array.from(document.querySelectorAll('.card'));
+ function apply(){
+ const q = f.value.toLowerCase().trim();
+ cards.forEach(c => { c.style.display = c.innerText.toLowerCase().includes(q) ? '' : 'none'; });
+ }
+ f.addEventListener('input', apply);
+ })();
+</script>
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" rx="20" fill="#000"/><path d="M22 70 L50 20 L78 70 Z" fill="#fff"/></svg>