Move services and team content to Markdown

This commit is contained in:
Kevin
2026-09-02 10:43:45 -04:00
parent 8b8c541b3c
commit 1b25dc37a6
12 changed files with 103 additions and 6 deletions
+4
View File
@@ -19,6 +19,10 @@ const home = await readFile(resolve(root, 'index.html'), 'utf8');
if (key === 'www') {
if (!home.includes('What services do you offer?') || !home.includes('How do I book a visit?')) throw new Error('Primary FAQ Markdown entries are missing from the homepage.');
if (home.includes('What information should I bring to a meet and greet?')) throw new Error('Draft FAQ entry appeared in the homepage.');
const services = await readFile(resolve(root, 'services/index.html'), 'utf8');
for (const title of ['Dog walking', 'Grooming', 'Overnight care']) if (!services.includes(title)) throw new Error(`Service Markdown entry is missing: ${title}.`);
const about = await readFile(resolve(root, 'about/index.html'), 'utf8');
for (const name of ['Sarah Mitchell', 'Marcus Rivera', 'Emily Chen', 'David Okonkwo']) if (!about.includes(name)) throw new Error(`Team Markdown entry is missing: ${name}.`);
}
if (key === 'blog') {
const publishedPosts = ['first-walk-after-work', 'gentler-grooming-day', 'overnight-care-checklist'];
+11
View File
@@ -0,0 +1,11 @@
---
title: Dog walking
price: $25 / walk
items:
- GPS-tracked routes
- Flexible scheduling
- Post-walk notes
order: 10
---
One-on-one neighborhood walks with photo updates and water breaks.
+12
View File
@@ -0,0 +1,12 @@
---
title: Grooming
price: From $40
items:
- Bathing & blow-dry
- Nail and ear care
- Full groom available
order: 20
featured: true
---
A calmer, kinder grooming day for dogs of every size.
@@ -0,0 +1,11 @@
---
title: Overnight care
price: $65 / night
items:
- Evening & morning walks
- Feeding routine
- 24-hour supervision
order: 30
---
A familiar, cozy routine while you are away.
+8
View File
@@ -0,0 +1,8 @@
---
name: David Okonkwo
role: Overnight Care
image: /images/david-okonkwo.jpg
order: 40
---
The calm, caring presence behind cozy overnight stays.
+8
View File
@@ -0,0 +1,8 @@
---
name: Emily Chen
role: Pack Walker
image: /images/emily-chen.jpg
order: 30
---
A marathon runner who keeps every pup moving and smiling.
+8
View File
@@ -0,0 +1,8 @@
---
name: Marcus Rivera
role: Lead Trainer
image: /images/marcus-rivera.jpg
order: 20
---
Positive-reinforcement expert and champion pack leader.
+8
View File
@@ -0,0 +1,8 @@
---
name: Sarah Mitchell
role: Founder & Head Groomer
image: /images/sarah-mitchell.jpg
order: 10
---
A gentle grooming specialist with ten years of experience.
+1 -3
View File
@@ -9,8 +9,6 @@
"contact": { "email": "hello@3031023.xyz", "phone": "(555) 742-9283", "phoneHref": "+15557429283", "address": "742 Evergreen Terrace, Springfield, IL 62704", "hours": "Mon–Fri · 7am–6pm CST", "showMap": true },
"hero": { "eyebrow": "Springfield's neighborhood dog care", "headline": "Care that feels like family.", "copy": "Thoughtful walks, gentle grooming, and cozy overnight stays for the dog you love most.", "image": "/images/hero-dogs.jpg", "imageAlt": "Two happy dogs playing outdoors", "cta": "Meet the pack", "ctaHref": "/contact" },
"features": [{ "icon": "◎", "title": "Personalized care", "description": "Every visit follows your dog's pace, routine, and personality." }, { "icon": "↗", "title": "Updates you can trust", "description": "Photos and thoughtful notes keep you close to every adventure." }, { "icon": "✦", "title": "Experienced hands", "description": "A small, caring team with safety and comfort at the center." }],
"services": [{ "title": "Dog walking", "description": "One-on-one neighborhood walks with photo updates and water breaks.", "price": "$25 / walk", "items": ["GPS-tracked routes", "Flexible scheduling", "Post-walk notes"] }, { "title": "Grooming", "description": "A calmer, kinder grooming day for dogs of every size.", "price": "From $40", "items": ["Bathing & blow-dry", "Nail and ear care", "Full groom available"] }, { "title": "Overnight care", "description": "A familiar, cozy routine while you are away.", "price": "$65 / night", "items": ["Evening & morning walks", "Feeding routine", "24-hour supervision"] }],
"reviews": [{ "quote": "Marcus sends the best updates. Our dog recognizes his car and is ready at the door.", "author": "Tom N.", "rating": 5 }, { "quote": "The grooming team made bath time stress-free for the first time ever.", "author": "Jennifer W.", "rating": 5 }, { "quote": "We came home to a relaxed, happy puppy. That peace of mind is everything.", "author": "Lisa P.", "rating": 5 }],
"story": { "heading": "A smaller pack. A more personal kind of care.", "copy": "Happy Tails began with a few neighborhood walks and a belief that every dog deserves to be known—not just scheduled. Today, our close-knit team brings the same attentive care to every walk, groom, and sleepover.", "image": "/images/happy-tails-team.jpg", "imageAlt": "Happy Tails team with dogs in the park" },
"team": [{ "name": "Sarah Mitchell", "role": "Founder & Head Groomer", "bio": "A gentle grooming specialist with ten years of experience.", "image": "/images/sarah-mitchell.jpg" }, { "name": "Marcus Rivera", "role": "Lead Trainer", "bio": "Positive-reinforcement expert and champion pack leader.", "image": "/images/marcus-rivera.jpg" }, { "name": "Emily Chen", "role": "Pack Walker", "bio": "A marathon runner who keeps every pup moving and smiling.", "image": "/images/emily-chen.jpg" }, { "name": "David Okonkwo", "role": "Overnight Care", "bio": "The calm, caring presence behind cozy overnight stays.", "image": "/images/david-okonkwo.jpg" }]
"story": { "heading": "A smaller pack. A more personal kind of care.", "copy": "Happy Tails began with a few neighborhood walks and a belief that every dog deserves to be known—not just scheduled. Today, our close-knit team brings the same attentive care to every walk, groom, and sleepover.", "image": "/images/happy-tails-team.jpg", "imageAlt": "Happy Tails team with dogs in the park" }
}
+24 -1
View File
@@ -23,4 +23,27 @@ const faq = defineCollection({
}),
});
export const collections = { blog, faq };
const services = defineCollection({
loader: glob({ base: './sites/www/content/services', pattern: '**/*.md' }),
schema: z.object({
title: z.string().min(3).max(80),
price: z.string().min(2).max(48),
items: z.array(z.string().min(3).max(80)).min(1).max(8),
order: z.number().int().min(0).max(99).default(0),
featured: z.boolean().default(false),
draft: z.boolean().default(false),
}),
});
const team = defineCollection({
loader: glob({ base: './sites/www/content/team', pattern: '**/*.md' }),
schema: z.object({
name: z.string().min(3).max(80),
role: z.string().min(3).max(80),
image: z.string().startsWith('/images/'),
order: z.number().int().min(0).max(99).default(0),
draft: z.boolean().default(false),
}),
});
export const collections = { blog, faq, services, team };
+4 -1
View File
@@ -1,7 +1,10 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import PageIntro from '../components/PageIntro.astro';
import { getCollection, render } from 'astro:content';
import { isBlog, site } from '../lib/site-data.js';
if (isBlog) return Astro.redirect('/');
const team = (await getCollection('team', ({ data }) => !data.draft)).sort((a, b) => a.data.order - b.data.order);
const teamCards = await Promise.all(team.map(async (member) => ({ member, Content: (await render(member)).Content })));
---
<BaseLayout title={`About | ${site.name}`}><PageIntro eyebrow="Our pack" title="We know that great care starts with knowing your dog." lede="The Happy Tails team brings patience, playfulness, and real experience to every visit." image="/images/happy-tails-team.jpg" imageAlt="Dogs and the Happy Tails team enjoying the park" /><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"><div class="team-avatar" aria-hidden="true">{member.name.split(' ').map((part) => part[0]).join('')}</div><div><p class="eyebrow">{member.role}</p><h3>{member.name}</h3><p>{member.bio}</p></div></article>)}</div></div></section></BaseLayout>
<BaseLayout title={`About | ${site.name}`}><PageIntro eyebrow="Our pack" title="We know that great care starts with knowing your dog." lede="The Happy Tails team brings patience, playfulness, and real experience to every visit." image="/images/happy-tails-team.jpg" imageAlt="Dogs and the Happy Tails team enjoying the park" /><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">{teamCards.map(({ member, Content }) => <article class="team-card"><div class="team-avatar" aria-hidden="true">{member.data.name.split(' ').map((part) => part[0]).join('')}</div><div><p class="eyebrow">{member.data.role}</p><h3>{member.data.name}</h3><Content /></div></article>)}</div></div></section></BaseLayout>
+4 -1
View File
@@ -2,7 +2,10 @@
import BaseLayout from '../layouts/BaseLayout.astro';
import CallToAction from '../components/CallToAction.astro';
import PageIntro from '../components/PageIntro.astro';
import { getCollection, render } from 'astro:content';
import { isBlog, site } from '../lib/site-data.js';
if (isBlog) return Astro.redirect('/');
const services = (await getCollection('services', ({ data }) => !data.draft)).sort((a, b) => a.data.order - b.data.order);
const serviceCards = await Promise.all(services.map(async (service) => ({ service, Content: (await render(service)).Content })));
---
<BaseLayout title={`Services | ${site.name}`}><PageIntro eyebrow="Services" title="Everything your dog needs for a brighter day." lede="Flexible, personal care—with no one-size-fits-all routines." image="/images/dog-grooming.jpg" imageAlt="A dog receiving gentle grooming care" /><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><CallToAction eyebrow="A simple start" heading="Let's meet your dog first." copy="Tell us a little about their routine, personality, and what would make their day better." href="/contact" label="Book a meet and greet" /></BaseLayout>
<BaseLayout title={`Services | ${site.name}`}><PageIntro eyebrow="Services" title="Everything your dog needs for a brighter day." lede="Flexible, personal care—with no one-size-fits-all routines." image="/images/dog-grooming.jpg" imageAlt="A dog receiving gentle grooming care" /><section class="section"><div class="shell service-grid">{serviceCards.map(({ service, Content }) => <article class:list={['service-card', { featured: service.data.featured }]}><p class="eyebrow">{service.data.price}</p><h2>{service.data.title}</h2><Content /><ul>{service.data.items.map((item) => <li>{item}</li>)}</ul><a class="text-link" href="/contact">Ask about {service.data.title.toLowerCase()} →</a></article>)}</div></section><CallToAction eyebrow="A simple start" heading="Let's meet your dog first." copy="Tell us a little about their routine, personality, and what would make their day better." href="/contact" label="Book a meet and greet" /></BaseLayout>