Archived
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.
200 lines
7.3 KiB
Python
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>")
|