Archived
Refactor UI assets and harden quality checks
This commit is contained in:
1 parent
23ab466c52
commit
4ecc8eb874
62 files changed
+3314
-3206
No files matched your search
+107
-65
@@ -1,10 +1,27 @@
|
||||
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 = ""
|
||||
@@ -22,8 +39,11 @@ def _top_nav(label: str | None = None, links: SafeHtml | None = None) -> SafeHtm
|
||||
<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">
|
||||
<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}
|
||||
@@ -68,60 +88,11 @@ def _site_footer() -> SafeHtml:
|
||||
def _page(
|
||||
title: str,
|
||||
body: SafeHtml,
|
||||
extra_styles: SafeHtml | None = None,
|
||||
extra_scripts: SafeHtml | None = None,
|
||||
description: str = "KarlDNS is a small self-hosted Dynamic DNS service for FRITZ!Box routers, Linux cron jobs, and home networks.",
|
||||
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,
|
||||
options: PageOptions | None = None,
|
||||
) -> str:
|
||||
if not isinstance(body, SafeHtml):
|
||||
raise TypeError("body must be a SafeHtml fragment built by a template helper")
|
||||
extra_styles_html = extra_styles or trusted("")
|
||||
extra_scripts_html = extra_scripts or trusted("")
|
||||
preload_logo_html = (
|
||||
trusted('<link rel="preload" as="image" href="/logo.webp?v=3" type="image/webp">')
|
||||
if preload_logo
|
||||
else trusted("")
|
||||
)
|
||||
canonical_html = trusted(f'<link rel="canonical" href="{attr(canonical_url)}">') if canonical_url else trusted("")
|
||||
social_url = canonical_url or ""
|
||||
social_image = image_url or ""
|
||||
social_html = trusted("")
|
||||
if social_url:
|
||||
social_card = "summary_large_image" if social_image else "summary"
|
||||
social_image_alt = image_alt or title
|
||||
social_image_html = (
|
||||
f"""
|
||||
<meta property="og:image" content="{attr(social_image)}">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta property="og:image:alt" content="{attr(social_image_alt)}">
|
||||
<meta name="twitter:image" content="{attr(social_image)}">
|
||||
<meta name="twitter:image:alt" content="{attr(social_image_alt)}">
|
||||
"""
|
||||
if social_image
|
||||
else ""
|
||||
)
|
||||
social_html = 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(social_url)}">
|
||||
{social_image_html}
|
||||
<meta name="twitter:card" content="{attr(social_card)}">
|
||||
<meta name="twitter:title" content="{attr(title)}">
|
||||
<meta name="twitter:description" content="{attr(description)}">
|
||||
""")
|
||||
structured_data_html = trusted("")
|
||||
if structured_data:
|
||||
structured_data_html = trusted(
|
||||
'<script type="application/ld+json">'
|
||||
+ escape(json.dumps(structured_data, ensure_ascii=False, separators=(",", ":")))
|
||||
+ "</script>"
|
||||
)
|
||||
options = options or PageOptions()
|
||||
return f"""
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -129,25 +100,96 @@ def _page(
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<meta name="description" content="{attr(description)}">
|
||||
{canonical_html}
|
||||
{social_html}
|
||||
<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">
|
||||
{preload_logo_html}
|
||||
<script src="/theme.js?v=2"></script>
|
||||
<script src="/clipboard.js?v=3" defer></script>
|
||||
<script src="/visitor-ip.js?v=1" defer></script>
|
||||
{extra_scripts_html}
|
||||
<link href="/bootstrap-subset.css?v=3" rel="stylesheet">
|
||||
<link href="/app.css?v=13" rel="stylesheet">
|
||||
{extra_styles_html}
|
||||
{structured_data_html}
|
||||
{_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>")
|
||||
Reference in new issue
Block a user