Build static Astro pilot for Happy Tails

This commit is contained in:
Kevin
2026-09-02 00:54:33 -04:00
parent cfe0a7d98d
commit 56a80951b7
42 changed files with 7171 additions and 134 deletions
+11
View File
@@ -0,0 +1,11 @@
---
import { isBlog, site } from '../lib/site-data.js';
---
<footer class="site-footer">
<div class="shell footer-grid">
<div><a class="brand" href="/"><span class="brand-mark" aria-hidden="true">✦</span><span>{isBlog ? 'Happy Tails Journal' : 'Happy Tails'}</span></a><p>{site.description}</p></div>
<div><p class="eyebrow">Explore</p>{site.nav.map((item) => <a class="footer-link" href={item.href}>{item.label}</a>)}</div>
{!isBlog && <address><p class="eyebrow">Say hello</p><a class="footer-link" href={`tel:${site.contact.phoneHref}`}>{site.contact.phone}</a><a class="footer-link" href={`mailto:${site.contact.email}`}>{site.contact.email}</a></address>}
</div>
<div class="shell footer-bottom">© {new Date().getFullYear()} {site.name} <span>Built with care by SloanOps</span></div>
</footer>
+22
View File
@@ -0,0 +1,22 @@
---
import { isBlog, site } from '../lib/site-data.js';
const path = Astro.url.pathname;
---
<header class="site-header">
<div class="shell nav-wrap">
<a class="brand" href="/" aria-label={`${site.name} home`}><span class="brand-mark" aria-hidden="true">✦</span><span>{isBlog ? 'Happy Tails Journal' : 'Happy Tails'}</span></a>
<button class="menu-button" aria-expanded="false" aria-controls="main-menu">Menu <span aria-hidden="true">+</span></button>
<nav id="main-menu" aria-label="Main navigation">
{site.nav.map((item) => <a href={item.href} aria-current={path === item.href ? 'page' : undefined}>{item.label}</a>)}
</nav>
</div>
</header>
<script>
const button = document.querySelector('.menu-button');
const menu = document.querySelector('#main-menu');
button?.addEventListener('click', () => {
const open = button.getAttribute('aria-expanded') !== 'true';
button.setAttribute('aria-expanded', String(open));
menu?.classList.toggle('is-open', open);
});
</script>
+9
View File
@@ -0,0 +1,9 @@
---
import { site } from '../lib/site-data.js';
---
<section class="hero">
<div class="shell hero-grid">
<div class="hero-copy"><p class="eyebrow">{site.hero.eyebrow}</p><h1>{site.hero.headline}</h1><p class="lede">{site.hero.copy}</p><a class="button" href={site.hero.ctaHref}>{site.hero.cta} <span aria-hidden="true">→</span></a></div>
<div class="hero-image"><img src={site.hero.image} alt={site.hero.imageAlt} fetchpriority="high" /><div class="image-note">Kind care. Every day.</div></div>
</div>
</section>
+31
View File
@@ -0,0 +1,31 @@
---
import '../styles/global.css';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import { deployment, isProduction, site } from '../lib/site-data.js';
const { title = site.name, description = site.description } = Astro.props;
const canonical = `https://${deployment.hostname}${Astro.url.pathname}`;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<title>{title}</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=DM+Serif+Display:ital@0;1&display=swap" rel="stylesheet" />
{isProduction && site.analyticsId && <script async src={`https://www.googletagmanager.com/gtag/js?id=${site.analyticsId}`}></script>}
{isProduction && site.analyticsId && <script define:vars={{ analyticsId: site.analyticsId }}>window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag('js', new Date()); gtag('config', analyticsId);</script>}
</head>
<body style={`--accent:${site.theme.accent};--ink:${site.theme.ink};--surface:${site.theme.surface}`}>
<Header />
<main><slot /></main>
<Footer />
</body>
</html>
+16
View File
@@ -0,0 +1,16 @@
import manifest from '../../sites.json';
import www from '../../sites/www/site.json';
import blog from '../../sites/blog/site.json';
const content = { www, blog };
const key = process.env.SITE_KEY || 'www';
if (!Object.hasOwn(manifest.sites, key) || !Object.hasOwn(content, key)) {
throw new Error(`Unknown SITE_KEY: ${key}`);
}
export const siteKey = key;
export const deployment = manifest.sites[key];
export const site = content[key];
export const isBlog = key === 'blog';
export const isProduction = process.env.DEPLOY_ENV === 'production';
+4
View File
@@ -0,0 +1,4 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="Page not found"><section class="page-intro not-found"><div class="shell"><p class="eyebrow">404</p><h1>That trail ends here.</h1><p class="lede">The page you are looking for is not available.</p><a class="button" href="/">Return home →</a></div></section></BaseLayout>
+6
View File
@@ -0,0 +1,6 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { isBlog, site } from '../lib/site-data.js';
if (isBlog) return Astro.redirect('/');
---
<BaseLayout title={`About | ${site.name}`}><section class="page-intro"><div class="shell"><p class="eyebrow">Our pack</p><h1>We know that great care starts with knowing your dog.</h1><p class="lede">The Happy Tails team brings patience, playfulness, and real experience to every visit.</p></div></section><section class="section"><div class="shell story-grid"><div><p class="eyebrow">How we work</p><h2>{site.story.heading}</h2><p class="lede">{site.story.copy}</p><p>Our team communicates clearly, keeps routines consistent, and makes safety a habit—not an afterthought.</p></div><img src={site.story.image} alt={site.story.imageAlt} loading="lazy" /></div></section><section class="section surface"><div class="shell"><div class="section-heading"><p class="eyebrow">Meet the team</p><h2>Good people for good dogs.</h2></div><div class="team-grid">{site.team.map((member) => <article class="team-card"><img src={member.image} alt={member.name} loading="lazy" /><div><p class="eyebrow">{member.role}</p><h3>{member.name}</h3><p>{member.bio}</p></div></article>)}</div></div></section></BaseLayout>
+6
View File
@@ -0,0 +1,6 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { isBlog, site } from '../lib/site-data.js';
if (isBlog) return Astro.redirect('/');
---
<BaseLayout title={`Contact | ${site.name}`}><section class="page-intro"><div class="shell"><p class="eyebrow">Get in touch</p><h1>Let's make your dog's day.</h1><p class="lede">Reach out to ask a question or start planning the right care routine.</p></div></section><section class="section"><div class="shell contact-grid"><div><p class="eyebrow">Happy Tails Pet Care</p><h2>We would love to hear from you.</h2><address class="contact-details"><p>{site.contact.address}</p><p><a href={`tel:${site.contact.phoneHref}`}>{site.contact.phone}</a></p><p><a href={`mailto:${site.contact.email}`}>{site.contact.email}</a></p><p>{site.contact.hours}</p></address></div><form class="contact-form"><label>Name<input type="text" autocomplete="name" disabled /></label><label>Email<input type="email" autocomplete="email" disabled /></label><label>How can we help?<textarea rows="5" disabled></textarea></label><button class="button" type="button" disabled>Contact route coming soon</button><p>This form is intentionally disabled until the approved server-mapped n8n contact route is connected. It does not claim to send messages.</p></form></div></section></BaseLayout>
+13
View File
@@ -0,0 +1,13 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import Hero from '../components/Hero.astro';
import { isBlog, site } from '../lib/site-data.js';
---
<BaseLayout>
<Hero />
{isBlog ? <section class="section"><div class="shell"><div class="section-heading"><p class="eyebrow">The journal</p><h2>Small notes for happier dog days.</h2></div><div class="post-grid">{site.posts.map((post) => <article class="post-card"><p class="tag">{post.tag}</p><h3>{post.title}</h3><p>{post.excerpt}</p><footer>{post.date}</footer></article>)}</div><p class="phase-note">This static sample deliberately uses JSON. Markdown publishing starts only in the later, separately approved content-update phase.</p></div></section> : <>
<section class="section"><div class="shell"><div class="section-heading split"><div><p class="eyebrow">The Happy Tails difference</p><h2>A familiar face, a thoughtful routine, a very happy dog.</h2></div><p>We keep our pack intentionally small, so every dog gets the calm attention and consistent care they deserve.</p></div><div class="feature-grid">{site.features.map((feature) => <article class="feature"><span>{feature.icon}</span><h3>{feature.title}</h3><p>{feature.description}</p></article>)}</div></div></section>
<section class="section surface"><div class="shell story-grid"><img src={site.story.image} alt={site.story.imageAlt} loading="lazy" /><div><p class="eyebrow">Our story</p><h2>{site.story.heading}</h2><p class="lede">{site.story.copy}</p><a class="text-link" href="/about">Meet the team →</a></div></div></section>
<section class="section"><div class="shell"><div class="section-heading"><p class="eyebrow">Loved by local families</p><h2>Five-star care, shared in their own words.</h2></div><div class="review-grid">{site.reviews.map((review) => <blockquote class="review"><div aria-label={`${review.rating} out of 5 stars`} class="stars">★★★★★</div><p>“{review.quote}”</p><footer>{review.author} <span>Google review</span></footer></blockquote>)}</div><p class="review-disclosure">Review cards are a presentation pattern. Before launch, display only reviews approved for reuse and preserve their source, date, rating, and attribution under the business’s Google review policy.</p></div></section>
</>}
</BaseLayout>
+6
View File
@@ -0,0 +1,6 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { isBlog, site } from '../lib/site-data.js';
if (isBlog) return Astro.redirect('/');
---
<BaseLayout title={`Services | ${site.name}`}><section class="page-intro"><div class="shell"><p class="eyebrow">Services</p><h1>Everything your dog needs for a brighter day.</h1><p class="lede">Flexible, personal care—with no one-size-fits-all routines.</p></div></section><section class="section"><div class="shell service-grid">{site.services.map((service, index) => <article class:list={['service-card', { featured: index === 1 }]}><p class="eyebrow">{service.price}</p><h2>{service.title}</h2><p>{service.description}</p><ul>{service.items.map((item) => <li>{item}</li>)}</ul><a class="text-link" href="/contact">Ask about {service.title.toLowerCase()} →</a></article>)}</div></section><section class="section surface"><div class="shell centered"><p class="eyebrow">A simple start</p><h2>Let's meet your dog first.</h2><p class="lede">Tell us a little about their routine, personality, and what would make their day better.</p><a class="button" href="/contact">Book a meet and greet →</a></div></section></BaseLayout>
+53
View File
@@ -0,0 +1,53 @@
:root { color-scheme: light; font-family: 'DM Sans', sans-serif; color: var(--ink); background: #fff; }
* { box-sizing: border-box; }
body { margin: 0; line-height: 1.55; }
a { color: inherit; }
img { display: block; max-width: 100%; }
h1,h2,h3,p { margin-top: 0; }
h1,h2 { font-family: 'DM Serif Display', Georgia, serif; font-weight: 400; line-height: 1.03; letter-spacing: -.035em; }
h1 { font-size: clamp(3.1rem, 7vw, 6.7rem); max-width: 10ch; margin-bottom: 1.5rem; }
h2 { font-size: clamp(2.25rem, 4vw, 4.1rem); margin-bottom: 1.25rem; }
h3 { font-size: 1.2rem; margin-bottom: .5rem; }
.shell { width: min(1120px, calc(100% - 3rem)); margin-inline: auto; }
.site-header { position: relative; z-index: 5; background: #fff; border-bottom: 1px solid #e8ecea; }
.nav-wrap { min-height: 82px; display: flex; align-items: center; justify-content: space-between; gap: 2rem; }
.brand { display: inline-flex; align-items: center; gap: .5rem; text-decoration: none; font-size: 1.15rem; font-weight: 700; letter-spacing: -.03em; }
.brand-mark { display: grid; place-items: center; width: 28px; height: 28px; color: #fff; background: var(--accent); border-radius: 50%; font-size: .85rem; }
nav { display: flex; align-items: center; gap: 1.5rem; }
nav a { color: #52616b; text-decoration: none; font-size: .92rem; font-weight: 600; }
nav a[aria-current='page'], nav a:hover { color: var(--ink); }
.menu-button { display: none; font: inherit; font-weight: 600; color: var(--ink); background: transparent; border: 0; }
.hero { padding: clamp(4rem, 9vw, 8rem) 0; overflow: hidden; background: linear-gradient(110deg, var(--surface) 0%, var(--surface) 68%, #fff 68%); }
.hero-grid,.story-grid,.contact-grid { display: grid; grid-template-columns: 1.02fr .98fr; align-items: center; gap: clamp(3rem, 7vw, 8rem); }
.hero-copy { padding: 1rem 0; }
.eyebrow,.tag { color: color-mix(in srgb, var(--accent), var(--ink) 25%); text-transform: uppercase; letter-spacing: .13em; font-size: .73rem; font-weight: 700; margin-bottom: 1rem; }
.lede { color: #52616b; font-size: clamp(1.1rem, 2vw, 1.32rem); max-width: 48ch; }
.button { display: inline-flex; gap: .65rem; align-items: center; margin-top: 1.4rem; padding: .9rem 1.2rem; border-radius: 999px; border: 1px solid var(--ink); background: var(--ink); color: #fff; text-decoration: none; font-weight: 700; transition: transform .2s, background .2s; }
.button:hover { transform: translateY(-2px); background: var(--accent); border-color: var(--accent); }
.hero-image { position: relative; }
.hero-image img { aspect-ratio: 4 / 4.6; width: 100%; object-fit: cover; border-radius: 1.25rem; }
.image-note { position: absolute; right: -1.5rem; bottom: 1.5rem; padding: .8rem 1rem; background: var(--accent); color: #102a43; font-weight: 700; transform: rotate(-4deg); }
.section { padding: clamp(4rem, 8vw, 7.5rem) 0; }
.surface { background: var(--surface); }
.section-heading { max-width: 720px; margin-bottom: 3rem; }
.section-heading.split { max-width: none; display: grid; grid-template-columns: 1.25fr .75fr; gap: 2rem; }
.section-heading.split > p { color: #52616b; font-size: 1.06rem; align-self: end; }
.feature-grid,.review-grid,.service-grid,.post-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.25rem; }
.feature { padding: 2rem; background: #fff; border: 1px solid #e8ecea; border-radius: .85rem; }
.feature > span { display: inline-grid; place-items: center; width: 2.75rem; height: 2.75rem; margin-bottom: 1.5rem; border-radius: 50%; background: var(--surface); color: var(--accent); font-size: 1.4rem; }
.feature p,.review p,.team-card p,.post-card p,.service-card > p { color: #52616b; }
.story-grid img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border-radius: 1.1rem; }
.text-link { display: inline-block; margin-top: .7rem; color: var(--ink); font-weight: 700; text-decoration-thickness: 2px; text-underline-offset: 4px; }
.review { margin: 0; padding: 1.75rem; border-radius: .8rem; background: #fff; border: 1px solid #e8ecea; }
.review .stars { color: var(--accent); font-size: .85rem; letter-spacing: .12em; }
.review blockquote { margin: 0; }
.review footer { font-weight: 700; }.review footer span { display: block; margin-top: .15rem; color: #75828c; font-size: .82rem; font-weight: 400; }
.review-disclosure,.phase-note { margin: 1.8rem auto 0; color: #66747d; font-size: .86rem; max-width: 75ch; }
.page-intro { padding: clamp(4rem, 8vw, 7rem) 0 2rem; background: var(--surface); }.page-intro .lede { max-width: 55ch; }
.team-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.25rem; }.team-card { display: grid; grid-template-columns: 150px 1fr; overflow: hidden; background: #fff; border: 1px solid #e8ecea; border-radius: .85rem; }.team-card img { width: 100%; height: 100%; object-fit: cover; }.team-card div { padding: 1.5rem; }
.service-card { padding: 2rem; border: 1px solid #dce4e1; border-radius: .85rem; background: #fff; }.service-card.featured { border: 2px solid var(--accent); transform: translateY(-.5rem); }.service-card h2 { font-size: 2.2rem; }.service-card ul { padding: 0; list-style: none; }.service-card li { padding: .4rem 0; border-top: 1px solid #e8ecea; }.service-card li::before { content: '✓'; color: var(--accent); font-weight: 700; margin-right: .55rem; }
.centered { text-align: center; }.centered .lede { margin-inline: auto; }
.contact-grid { align-items: start; }.contact-details { margin-top: 2rem; font-style: normal; color: #52616b; }.contact-details p { margin-bottom: .8rem; }.contact-details a { color: var(--ink); font-weight: 700; }.contact-form { display: grid; gap: 1rem; padding: 2rem; background: var(--surface); border-radius: .85rem; }.contact-form label { display: grid; gap: .4rem; font-size: .9rem; font-weight: 700; }.contact-form input,.contact-form textarea { width: 100%; padding: .8rem; font: inherit; border: 1px solid #cbd5d1; border-radius: .35rem; background: #fff; }.contact-form button:disabled { cursor: not-allowed; opacity: .6; }.contact-form p { margin-bottom: 0; color: #66747d; font-size: .85rem; }
.post-card { padding: 2rem; border-radius: .85rem; background: #fff; border: 1px solid #dce4e1; }.post-card h3 { font-family: 'DM Serif Display', Georgia, serif; font-size: 1.75rem; font-weight: 400; line-height: 1.1; }.post-card footer { color: #75828c; font-size: .85rem; }.not-found { min-height: 55vh; display: grid; place-items: center; }
.site-footer { padding: 4rem 0 1.25rem; background: var(--ink); color: #fff; }.site-footer .brand { color: #fff; }.site-footer p { color: #c5d0d6; max-width: 34ch; }.footer-grid { display: grid; grid-template-columns: 2fr 1fr 1fr; gap: 3rem; }.footer-link { display: block; margin: .6rem 0; color: #fff; text-decoration: none; }.footer-bottom { display: flex; justify-content: space-between; gap: 1rem; margin-top: 3rem; padding-top: 1.25rem; border-top: 1px solid #35505f; color: #c5d0d6; font-size: .85rem; }
@media (max-width: 760px) { .shell { width: min(100% - 2rem, 1120px); }.menu-button { display: inline-flex; gap: .5rem; } .nav-wrap { min-height: 68px; } nav { display: none; position: absolute; top: 68px; right: 1rem; left: 1rem; padding: 1rem; background: #fff; border: 1px solid #e8ecea; border-radius: .6rem; box-shadow: 0 1rem 2rem #102a4320; } nav.is-open { display: grid; } .hero { background: var(--surface); }.hero-grid,.story-grid,.contact-grid,.section-heading.split { grid-template-columns: 1fr; gap: 2.5rem; }.hero-image { max-width: 520px; }.image-note { right: -.5rem; }.feature-grid,.review-grid,.service-grid,.post-grid { grid-template-columns: 1fr; }.service-card.featured { transform: none; }.team-grid { grid-template-columns: 1fr; }.footer-grid { grid-template-columns: 1fr; gap: 1.5rem; }.footer-bottom { display: block; }.footer-bottom span { display: block; margin-top: .5rem; } }