Files
chicken_babies_site/app/templates/public/base.html
Phillip Tarrant f9f90d408e chore: phase 6 hardening — CSP/HSTS, access log, docker, backup, CI
Ships the cross-cutting hardening set:

- SecurityHeadersMiddleware: per-request nonce-based CSP, HSTS
  (production only), Referrer-Policy, Permissions-Policy,
  X-Content-Type-Options, frame-ancestors 'none', form-action 'self'.
- AccessLogMiddleware: one http_request INFO event per request
  (method/path/status/duration_ms/ip/ua). Skips /healthz, redacts
  /admin/auth/consume/<token> paths, logs 500 + re-raises on
  downstream exceptions.
- Public base.html inline nav-toggle script gets a nonce so it
  passes strict CSP without relaxing to 'unsafe-inline'.
- Dockerfile: non-root app user (uid/gid 10001) + stdlib-only
  HEALTHCHECK against /healthz.
- scripts/backup.sh: sqlite3 .backup + tar data/media with
  14-entry retention; host-side cron install documented.
- .gitea/workflows/build-image.yml: on push to master /
  workflow_dispatch, builds and publishes
  git.sneakygeek.net/ptarrant/chicken_babies_site:latest +
  sha-<short>, with GIT_COMMIT_SHA threaded as a build-arg so
  /healthz keeps reporting the right commit in deployed images.
- 8 new tests (security headers + access log).

Pre-existing dev failures (logo asset rename + RESEND env
pollution) remain unchanged; verified not Phase 6 regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 07:38:23 -05:00

125 lines
5.2 KiB
HTML

{#
Base layout for every public page.
Child templates override the following blocks:
- title : the contents of <title>
- meta_description : contents of <meta name="description">
- content : the page body inside <main>
Design notes:
- Semantic landmarks (<header>, <nav>, <main>, <footer>) for a11y.
- Skip-link is the first focusable element so keyboard users can jump
past the header.
- aria-current="page" is applied to the active nav link by comparing
the `active_nav` context variable the route passed us.
- The mobile nav toggle uses addEventListener only — no inline event
handlers — so we stay CSP-nonce-compatible when Phase 6 adds the
strict CSP middleware.
#}<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Chicken Babies R Us{% endblock %}</title>
<meta name="description" content="{% block meta_description %}Small-farm fresh eggs and happy birds, raised in Morrison, Tennessee.{% endblock %}">
{# Self-hosted favicon + apple touch icon — no third-party CDNs. #}
<link rel="icon" href="{{ url_for('static', path='img/favicon.ico') }}" sizes="any">
<link rel="apple-touch-icon" href="{{ url_for('static', path='img/apple-touch-icon.png') }}">
<link rel="stylesheet" href="{{ url_for('static', path='css/site.css') }}">
</head>
<body>
{# Skip link: hidden until focused. First focusable element on the page. #}
<a class="skip-link" href="#main-content">Skip to main content</a>
<header class="site-header">
<div class="wrap site-header__wrap">
<a class="site-header__brand" href="/" aria-label="Chicken Babies R Us home">
{# Chick-only mark paired with the site title as styled text.
Decoupling the mark from the wordmark lets the header colors
change freely without the multi-colored logo text clashing. #}
<picture>
<source srcset="{{ url_for('static', path='img/logo-mark.webp') }}" type="image/webp">
<img src="{{ url_for('static', path='img/logo-mark.png') }}"
alt=""
height="56"
class="site-header__mark">
</picture>
<span class="site-header__title">Chicken Babies R Us</span>
</a>
{# The mobile toggle button — script below attaches a click handler
that flips aria-expanded and toggles .is-open on the nav. #}
<button type="button"
class="site-nav__toggle"
id="nav-toggle"
aria-controls="primary-nav"
aria-expanded="false">
<span class="visually-hidden">Toggle navigation</span>
<span class="site-nav__toggle-bar" aria-hidden="true"></span>
<span class="site-nav__toggle-bar" aria-hidden="true"></span>
<span class="site-nav__toggle-bar" aria-hidden="true"></span>
</button>
<nav class="site-nav" id="primary-nav" aria-label="Primary">
<ul class="site-nav__list">
<li class="site-nav__item">
<a href="/"
class="site-nav__link{% if active_nav == 'home' %} is-active{% endif %}"
{% if active_nav == 'home' %}aria-current="page"{% endif %}>Home</a>
</li>
<li class="site-nav__item">
<a href="/about"
class="site-nav__link{% if active_nav == 'about' %} is-active{% endif %}"
{% if active_nav == 'about' %}aria-current="page"{% endif %}>About</a>
</li>
<li class="site-nav__item">
<a href="/contact"
class="site-nav__link{% if active_nav == 'contact' %} is-active{% endif %}"
{% if active_nav == 'contact' %}aria-current="page"{% endif %}>Contact</a>
</li>
<li class="site-nav__item">
<a href="/shop"
class="site-nav__link nav--muted{% if active_nav == 'shop' %} is-active{% endif %}"
{% if active_nav == 'shop' %}aria-current="page"{% endif %}>Shop</a>
</li>
</ul>
</nav>
</div>
</header>
<main id="main-content" class="site-main" tabindex="-1">
<div class="wrap">
{% block content %}{% endblock %}
</div>
</main>
<footer class="site-footer">
<div class="wrap site-footer__wrap">
<p class="site-footer__tag">
Chicken Babies R Us &middot; Morrison, Tennessee
</p>
<p class="site-footer__legal">
&copy; {{ now_year or 2026 }} Chicken Babies R Us. All rights reserved.
</p>
</div>
</footer>
{# Mobile nav toggle. Tiny and CSP-friendly: no inline handlers, no JS
framework. Phase 6's CSP will be compatible with moving this into an
external file + nonce if we grow; for now the inline block stays. #}
<script nonce="{{ request.state.csp_nonce }}">
(function () {
"use strict";
var toggle = document.getElementById("nav-toggle");
var nav = document.getElementById("primary-nav");
if (!toggle || !nav) { return; }
toggle.addEventListener("click", function () {
var expanded = toggle.getAttribute("aria-expanded") === "true";
toggle.setAttribute("aria-expanded", expanded ? "false" : "true");
nav.classList.toggle("is-open");
});
})();
</script>
</body>
</html>