This repository has been archived on 2026-10-28. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
StasonJatham 5c46b47937 chore(repo): move current KarlDNS source to Gitea
Preserve the GitHub history and the newer deployed source from ct_134_karldns. Exclude runtime data, credentials, AppleDouble files and local backup copies. Make Gitea the canonical source for documentation, package metadata and project links.
2026-10-28 08:47:52 +01:00

200 lines
7.3 KiB
Python

from __future__ import annotations
import json
from dataclasses import dataclass, field
from .html_utils import SafeHtml, attr, escape, trusted
from .icons import _svg_icon
DEFAULT_DESCRIPTION = (
"KarlDNS is a small self-hosted Dynamic DNS service for FRITZ!Box routers, " "Linux cron jobs, and home networks."
)
@dataclass(frozen=True)
class PageOptions:
extra_styles: SafeHtml = field(default_factory=lambda: trusted(""))
extra_scripts: SafeHtml = field(default_factory=lambda: trusted(""))
description: str = DEFAULT_DESCRIPTION
preload_logo: bool = False
canonical_url: str | None = None
image_url: str | None = None
image_alt: str | None = None
structured_data: dict[str, object] | None = None
def _top_nav(label: str | None = None, links: SafeHtml | None = None) -> SafeHtml:
account_html = ""
if label:
account_html = f'<span class="navbar-text nav-label text-truncate">{escape(label)}</span>'
if links is None:
links = trusted(
'<a class="nav-link px-0" href="/docs">API</a>'
'<a class="nav-link px-0" href="/llms.txt" type="text/plain">llms.txt</a>'
)
theme_toggle = f"""
<button type="button" class="theme-toggle" data-theme-toggle aria-label="Switch to dark theme">
{_svg_icon("moon", "theme-icon theme-icon-moon")}
{_svg_icon("sun", "theme-icon theme-icon-sun")}
</button>
"""
return trusted(f"""
<a class="skip-link" href="#main">Skip to content</a>
<nav class="navbar navbar-expand-sm site-nav" aria-label="Main navigation">
<div class="container">
<a class="navbar-brand brand" href="/">
<img src="/logo-nav.webp?v=3" alt="" width="28" height="28" decoding="async">KarlDNS
</a>
<div class="navbar-nav site-nav-actions ms-auto flex-row align-items-center justify-content-end
gap-2 gap-sm-3">
{links}
{account_html}
{theme_toggle}
</div>
</div>
</nav>
""")
def _site_footer() -> SafeHtml:
return trusted(f"""
<footer class="site-footer border-top">
<div class="container footer-inner">
<div class="footer-made">Made with <span aria-label="love">❤️</span> by karl</div>
<nav class="footer-links" aria-label="Footer links">
<a class="footer-link" href="https://tea.karlcom.de/karl/karldns.de" rel="noopener">
{_svg_icon("github", "footer-icon")}
<span>Gitea</span>
</a>
<a class="footer-link" href="https://karl.fail" rel="noopener">
{_svg_icon("blog", "footer-icon")}
<span>karl.fail</span>
</a>
<a class="footer-link" href="https://karlcom.de" rel="noopener">
{_svg_icon("business", "footer-icon")}
<span>karlcom.de</span>
</a>
<a class="footer-link" href="/docs">
{_svg_icon("docs", "footer-icon")}
<span>API</span>
</a>
<a class="footer-link" href="/privacy">
{_svg_icon("privacy", "footer-icon")}
<span>Legal & privacy</span>
</a>
</nav>
</div>
</footer>
""")
def _page(
title: str,
body: SafeHtml,
options: PageOptions | None = None,
) -> str:
if not isinstance(body, SafeHtml):
raise TypeError("body must be a SafeHtml fragment built by a template helper")
options = options or PageOptions()
return f"""
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="describedby" href="/llms.txt" type="text/plain">
<meta name="description" content="{attr(options.description)}">
{_canonical_tag(options.canonical_url)}
{_social_meta_tags(
title=title,
description=options.description,
canonical_url=options.canonical_url,
image_url=options.image_url,
image_alt=options.image_alt,
)}
<title>{escape(title)}</title>
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
{_logo_preload_tag(options.preload_logo)}
{_base_script_tags()}
{options.extra_scripts}
{_base_style_tags()}
{options.extra_styles}
{_structured_data_tag(options.structured_data)}
</head>
<body>{body}{_site_footer()}</body>
</html>
"""
def _canonical_tag(canonical_url: str | None) -> SafeHtml:
return trusted(f'<link rel="canonical" href="{attr(canonical_url)}">') if canonical_url else trusted("")
def _logo_preload_tag(preload_logo: bool) -> SafeHtml:
if not preload_logo:
return trusted("")
return trusted('<link rel="preload" as="image" href="/logo.webp?v=3" type="image/webp">')
def _base_script_tags() -> SafeHtml:
return trusted("""
<script src="/theme.js?v=2"></script>
<script src="/clipboard.js?v=3" defer></script>
<script src="/visitor-ip.js?v=1" defer></script>
""")
def _base_style_tags() -> SafeHtml:
return trusted("""
<link href="/bootstrap-subset.css?v=3" rel="stylesheet">
<link href="/app.css?v=14" rel="stylesheet">
""")
def _social_meta_tags(
*,
title: str,
description: str,
canonical_url: str | None,
image_url: str | None,
image_alt: str | None,
) -> SafeHtml:
if not canonical_url:
return trusted("")
social_card = "summary_large_image" if image_url else "summary"
return trusted(f"""
<meta property="og:type" content="website">
<meta property="og:title" content="{attr(title)}">
<meta property="og:description" content="{attr(description)}">
<meta property="og:url" content="{attr(canonical_url)}">
{_social_image_tags(image_url, image_alt or title)}
<meta name="twitter:card" content="{attr(social_card)}">
<meta name="twitter:title" content="{attr(title)}">
<meta name="twitter:description" content="{attr(description)}">
""")
def _social_image_tags(image_url: str | None, image_alt: str) -> SafeHtml:
if not image_url:
return trusted("")
return trusted(f"""
<meta property="og:image" content="{attr(image_url)}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="{attr(image_alt)}">
<meta name="twitter:image" content="{attr(image_url)}">
<meta name="twitter:image:alt" content="{attr(image_alt)}">
""")
def _structured_data_tag(structured_data: dict[str, object] | None) -> SafeHtml:
if not structured_data:
return trusted("")
payload = json.dumps(structured_data, ensure_ascii=False, separators=(",", ":"))
return trusted('<script type="application/ld+json">' + escape(payload) + "</script>")