feat: phase 1 public site skeleton — layout, routes, CSS, logo pipeline

Ship base Jinja layout (header/nav/main/footer with skip link and aria-current),
mobile-first single-file CSS using the ROADMAP palette tokens, and four public
routes: /, /about, /contact, /shop. Blog index renders via a stable
PostService.list_published() stub returning [] — Phase 2 only swaps the body.
About is static placeholder copy, /contact ships an inert form plus a mailto:
link driven by ADMIN_CONTACT_EMAIL, /shop shows a "Coming soon" card.

Adds a Pillow-based scripts/generate_static_assets.py producing resized logo
PNG + WebP, multi-size favicon.ico, and a 180x180 apple-touch-icon on a cream
background. Outputs committed for a reproducible build.

Also ship docs/MANUAL_TESTING.md with per-route / responsive / a11y / static-
asset checklists, and mark Phase 1 complete in docs/ROADMAP.md.
This commit is contained in:
2026-04-21 15:21:21 -05:00
parent e830e5da50
commit f77da87eaa
21 changed files with 1533 additions and 7 deletions

View File

@@ -0,0 +1,42 @@
{#
About page — static placeholder copy. Head Hen will replace this via
the Phase 4 admin CMS, so the prose below is deliberately generic and
free of lorem ipsum. Per CLAUDE.md, the physical address is not shown
anywhere on the site — only the town name.
#}
{% extends "public/base.html" %}
{% block title %}About — Chicken Babies R Us{% endblock %}
{% block meta_description %}About Chicken Babies R Us — a small family farm in Morrison, Tennessee raising chickens, ducks, and geese.{% endblock %}
{% block content %}
<article class="page-article">
<header class="page-article__header">
<h1 class="page-article__title">About the farm</h1>
</header>
<p>
Chicken Babies R Us is a small family farm tucked into the rolling
hills of Morrison, Tennessee. What started as a handful of chicks
in a backyard brooder has grown into a flock of chickens, ducks, and
geese that keep us busy (and entertained) year round.
</p>
<p>
The operation is run by Head Hen &mdash; the chief wrangler, egg
gatherer, waterfowl-whisperer, and unofficial chicken photographer.
She handles the day-to-day care of the birds and does most of the
writing you'll find on this site. Expect updates on hatching plans,
new arrivals, the occasional coop mishap, and whatever the geese
decided to get into this week.
</p>
<p>
We're a hobby farm at heart, not a commercial one, which means we
can take the time to know our birds and raise them the way we think
they ought to be raised. If you're curious about what we've got
going on &mdash; or just want to say hello &mdash; pop over to the
contact page.
</p>
</article>
{% endblock %}

View File

@@ -0,0 +1,121 @@
{#
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">
{# WebP with PNG fallback — generated by scripts/generate_static_assets.py. #}
<picture>
<source srcset="{{ url_for('static', path='img/logo.webp') }}" type="image/webp">
<img src="{{ url_for('static', path='img/logo.png') }}"
alt="Chicken Babies R Us"
height="48"
class="site-header__logo">
</picture>
</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>
(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>

View File

@@ -0,0 +1,85 @@
{#
Contact page — Phase 1 version.
The form is deliberately inert: no `method`, no `action`, all inputs
and the submit button carry the `disabled` attribute. A muted note
explains the form is coming soon; if `ADMIN_CONTACT_EMAIL` is set in
the environment we render a `mailto:` link above the form so visitors
still have a way to reach the farm.
Phase 5 replaces this template with a working POST handler, hCaptcha,
honeypot, and rate limiting.
Context:
- contact_email : str | None (from settings.admin_contact_email)
- active_nav : "contact"
#}
{% extends "public/base.html" %}
{% block title %}Contact &mdash; Chicken Babies R Us{% endblock %}
{% block meta_description %}Get in touch with Chicken Babies R Us.{% endblock %}
{% block content %}
<article class="page-article">
<header class="page-article__header">
<h1 class="page-article__title">Get in touch</h1>
</header>
<p>
We'd love to hear from you &mdash; questions about the birds,
availability, or just to say hi.
</p>
{% if contact_email %}
<p class="contact-mailto">
The easiest way to reach us right now is email:
<a href="mailto:{{ contact_email }}">{{ contact_email }}</a>.
</p>
{% else %}
<p class="contact-mailto contact-mailto--muted">
A direct email address will be posted here soon.
</p>
{% endif %}
<p class="contact-form__note" role="note">
Secure contact form coming soon.
</p>
{# action="" and no method = form cannot submit. Every input is
disabled so screen readers and the keyboard both respect the
"not-yet-available" state. #}
<form class="contact-form" action="" aria-describedby="contact-form-note" novalidate>
<div class="contact-form__field">
<label for="contact-name">Name</label>
<input type="text"
id="contact-name"
name="name"
autocomplete="name"
disabled>
</div>
<div class="contact-form__field">
<label for="contact-email">Email</label>
<input type="email"
id="contact-email"
name="email"
autocomplete="email"
disabled>
</div>
<div class="contact-form__field">
<label for="contact-message">Message</label>
<textarea id="contact-message"
name="message"
rows="6"
disabled></textarea>
</div>
<div class="contact-form__actions">
<button type="submit" class="btn btn--primary" disabled>
Send message
</button>
</div>
</form>
</article>
{% endblock %}

View File

@@ -0,0 +1,35 @@
{#
Home page / blog index.
Receives:
- posts : list[PostSummary] (empty in Phase 1)
- active_nav : str "home"
#}
{% extends "public/base.html" %}
{% block title %}Chicken Babies R Us &mdash; Home{% endblock %}
{% block meta_description %}Updates from Chicken Babies R Us &mdash; a small family farm in Morrison, Tennessee.{% endblock %}
{% block content %}
<section class="page-intro">
<h1 class="page-intro__title">Welcome to Chicken Babies R Us</h1>
<p class="page-intro__lede">
A tiny family farm in Morrison, Tennessee. Follow along for updates
on our flock, hatching plans, and whatever Head Hen is up to this week.
</p>
</section>
<section class="post-list" aria-label="Latest posts">
{% if posts %}
{% for post in posts %}
{% include "public/partials/_post_card.html" %}
{% endfor %}
{% else %}
{# Empty-state copy. Phase 2 seeds a welcome post so this state only
ever shows up in unseeded dev databases and tests. #}
<div class="post-list__empty">
<p>No posts yet &mdash; check back soon!</p>
</div>
{% endif %}
</section>
{% endblock %}

View File

@@ -0,0 +1,26 @@
{#
Single blog card. Rendered once per PostSummary in the home-page loop.
Expects the loop variable `post` in scope with:
- post.slug (str)
- post.title (str)
- post.published_at (datetime)
- post.excerpt (str)
The post detail page does not exist yet (Phase 2 adds it), but we link
to /posts/<slug> anyway so the card markup is final. Phase 2 will
register the route; until then the link 404s, which is acceptable
because the post list itself is empty in Phase 1.
#}
<article class="post-card">
<header class="post-card__header">
<h2 class="post-card__title">
<a href="/posts/{{ post.slug }}">{{ post.title }}</a>
</h2>
<time class="post-card__date"
datetime="{{ post.published_at.isoformat() }}">
{{ post.published_at.strftime("%B %-d, %Y") }}
</time>
</header>
<p class="post-card__excerpt">{{ post.excerpt }}</p>
</article>

View File

@@ -0,0 +1,32 @@
{#
Shop placeholder. Phase 7 replaces this with a real Stripe-backed
catalog. For now the page itself is the "disabled" UI; the nav link
uses the `nav--muted` class to hint that it isn't fully live.
#}
{% extends "public/base.html" %}
{% block title %}Shop &mdash; Chicken Babies R Us{% endblock %}
{% block meta_description %}Our farm shop is coming soon &mdash; eggs, chicks, and waterfowl.{% endblock %}
{% block content %}
<article class="page-article">
<header class="page-article__header">
<h1 class="page-article__title">Shop</h1>
</header>
<section class="shop-card" aria-label="Shop status">
<h2 class="shop-card__title">Coming soon</h2>
<p class="shop-card__body">
We're getting the farm shop ready. Soon you'll be able to order
eating eggs, fertile hatching eggs, day-old chicks, and a small
selection of waterfowl (ducks and geese) when available. Pickup
will be local to Morrison; we'll share details here when the
shop goes live.
</p>
<p class="shop-card__body">
In the meantime, if you're looking for something specific, the
contact page is the best way to reach us.
</p>
</section>
</article>
{% endblock %}