diff --git a/scripts/verify-build.mjs b/scripts/verify-build.mjs index 61e9e8e..afebb7a 100644 --- a/scripts/verify-build.mjs +++ b/scripts/verify-build.mjs @@ -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']; diff --git a/sites/www/content/services/dog-walking.md b/sites/www/content/services/dog-walking.md new file mode 100644 index 0000000..f83794e --- /dev/null +++ b/sites/www/content/services/dog-walking.md @@ -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. diff --git a/sites/www/content/services/grooming.md b/sites/www/content/services/grooming.md new file mode 100644 index 0000000..c3d13b1 --- /dev/null +++ b/sites/www/content/services/grooming.md @@ -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. diff --git a/sites/www/content/services/overnight-care.md b/sites/www/content/services/overnight-care.md new file mode 100644 index 0000000..4d9e4e3 --- /dev/null +++ b/sites/www/content/services/overnight-care.md @@ -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. diff --git a/sites/www/content/team/david-okonkwo.md b/sites/www/content/team/david-okonkwo.md new file mode 100644 index 0000000..b79bb0f --- /dev/null +++ b/sites/www/content/team/david-okonkwo.md @@ -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. diff --git a/sites/www/content/team/emily-chen.md b/sites/www/content/team/emily-chen.md new file mode 100644 index 0000000..4aa9b76 --- /dev/null +++ b/sites/www/content/team/emily-chen.md @@ -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. diff --git a/sites/www/content/team/marcus-rivera.md b/sites/www/content/team/marcus-rivera.md new file mode 100644 index 0000000..94ad4b6 --- /dev/null +++ b/sites/www/content/team/marcus-rivera.md @@ -0,0 +1,8 @@ +--- +name: Marcus Rivera +role: Lead Trainer +image: /images/marcus-rivera.jpg +order: 20 +--- + +Positive-reinforcement expert and champion pack leader. diff --git a/sites/www/content/team/sarah-mitchell.md b/sites/www/content/team/sarah-mitchell.md new file mode 100644 index 0000000..e418090 --- /dev/null +++ b/sites/www/content/team/sarah-mitchell.md @@ -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. diff --git a/sites/www/site.json b/sites/www/site.json index 098a649..49c87e7 100644 --- a/sites/www/site.json +++ b/sites/www/site.json @@ -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" } } diff --git a/src/content.config.ts b/src/content.config.ts index 57d7a08..aae28d5 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -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 }; diff --git a/src/pages/about.astro b/src/pages/about.astro index 00624b1..c9dd5a5 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -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 }))); --- -

How we work

{site.story.heading}

{site.story.copy}

Our team communicates clearly, keeps routines consistent, and makes safety a habit—not an afterthought.

{site.story.imageAlt}

Meet the team

Good people for good dogs.

{site.team.map((member) =>

{member.role}

{member.name}

{member.bio}

)}
+

How we work

{site.story.heading}

{site.story.copy}

Our team communicates clearly, keeps routines consistent, and makes safety a habit—not an afterthought.

{site.story.imageAlt}

Meet the team

Good people for good dogs.

{teamCards.map(({ member, Content }) =>

{member.data.role}

{member.data.name}

)}
diff --git a/src/pages/services.astro b/src/pages/services.astro index dea70b0..ecccb7f 100644 --- a/src/pages/services.astro +++ b/src/pages/services.astro @@ -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 }))); --- -
{site.services.map((service, index) => )}
+
{serviceCards.map(({ service, Content }) => )}