Wedding photography website with galleries, prices, contact
Static Astro portfolio for a wedding photographer with gallery pages, a price list, and a contact form that can deploy free on Netlify.
Built 1 time, downloaded 0 times.
Install from your terminal
Run this in your project folder. It signs you in, shows what changes and asks before it overwrites a file.
npx claude-lab add wedding-photography-website-with-galleries-prices-contact-e19feaCLAUDE.md# Wedding photography website
This is a static website for a wedding photographer. Its visitors are engaged couples looking for a photographer. The tone is warm, calm, and simple. The site shows galleries of past weddings, a price list, and a contact form that leads to an inquiry email.
## Stack
- Astro (latest stable) for the static site.
- Plain CSS, kept in one small stylesheet.
- No CMS, no database, no backend code.
## Commands
- `npm install`: install dependencies.
- `npm run dev`: start the dev server at localhost:4321.
- `npm run build`: build the site into `dist/`.
- `npm run check`: run the Astro type and content check.
- `npm run preview`: preview the built site locally.
## Structure
- `src/pages/`: index, galleries, prices, about, contact, thank-you, privacy.
- `src/content/galleries/`: one markdown file per wedding.
- `src/data/prices.json`: all prices, in one place.
- `src/data/site.ts`: site settings, including the placeholder `FORM_ENDPOINT_ID`.
- `public/galleries/<slug>/`: web-sized images for each gallery.
- `src/components/`: Nav, Footer, GalleryGrid, PriceCard.
## Rules
- Prices live only in `src/data/prices.json`. Never hardcode a price in a page or component.
- Every image needs descriptive alt text.
- Resize images to 2000px wide or less, and under 400 KB, before they go into `public/`.
- Never put client names or couple names in filenames. Never put private wedding details in public files unless the gallery file says `published: true`.
- The contact form posts to the endpoint in `src/data/site.ts`, which is the placeholder `FORM_ENDPOINT_ID`. Never invent or commit a real endpoint key or email password.
- Do not add analytics or tracking scripts unless the owner asks for them in a plan. If one is proposed, explain the privacy impact first.
- Before saying a task is done, run the verify skill.
## Gotchas
- The owner's real name, email, phone, and location are placeholders marked `YOUR_NAME`, `YOUR_EMAIL`, and `YOUR_PHONE`. They stay as placeholders until the owner replaces them.
- Leave placeholders in place rather than guessing real details.
@REVIEW.md
.claude/settings.json{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"model": "sonnet",
"effort": "medium",
"permissions": {
"allow": [
"Bash(npm run:*)",
"Bash(npm install:*)",
"Bash(npx astro:*)",
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git add:*)",
"Bash(git commit:*)"
],
"deny": [
"Bash(rm -rf:*)",
"Bash(git push --force:*)",
"Bash(git reset --hard:*)",
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Read(./keys/**)"
],
"ask": [
"Bash(git push:*)",
"Bash(npm publish:*)",
"Bash(rm -r src/*)",
"Bash(rm -r public/*)"
]
}
}
.claude/skills/add-gallery/SKILL.md---
name: add-gallery
description: Use when the owner adds a new wedding gallery to the site. Creates the image folder and an unpublished gallery file, then runs the verify skill. Runs only when the owner types the command.
argument-hint: couple-slug year
disable-model-invocation: true
---
# Add a wedding gallery
This skill runs only when the owner types `/add-gallery`. Follow the steps in order. Do not publish a gallery without the owner's confirmation.
## Step 1: Collect the details
Use the arguments if they were given (couple-slug and year). Ask for anything missing:
- **Slug**: lowercase, hyphens only. Use first names or an initial plus year, for example `emma-jo-2027`. Never use real surnames in the slug.
- **Year**: four digits, the year of the wedding.
- **First name**: the couple's first names as they should appear on the page. The gallery page shows first names and year only.
- **Story**: one short paragraph about the day. Ask for it if the owner did not give it.
Confirm the slug with the owner before creating any files.
## Step 2: Create the image folder
Create the folder:
```
public/galleries/<slug>/
```
Tell the owner to copy their photos into this folder after resizing them.
## Step 3: Remind the owner about image size
Before the owner copies photos, remind them:
- Resize every photo to a maximum of 2000px wide.
- Keep every photo under 400 KB.
- Remove any file names that include client surnames or private details.
Wait for the owner to confirm the photos are ready before you go on to Step 5.
## Step 4: Create the gallery file
Create this file:
```
src/content/galleries/<slug>.md
```
Use this frontmatter and body, filling in the values:
```
---
title: "<First name> and <First name>"
year: <year>
published: false
cover: /galleries/<slug>/<cover-file-name>
coverAlt: "<short description of the cover photo>"
---
<story paragraph>
```
Rules:
- `published` must be `false` when the file is created.
- `coverAlt` is required. Describe what is in the photo, not just the couple's names.
- Write the cover file name only if the owner has named the cover photo. Otherwise use a placeholder `cover.jpg` and tell the owner to replace it.
- Do not add other personal details (venue names, addresses, guest names) unless the owner asks.
## Step 5: Publish only after confirmation
Change `published: false` to `published: true` only when the owner confirms, in chat, that the photos are in place and the story is correct. If the owner has not confirmed, leave the file unpublished and say so in the report.
## Step 6: Run verify
Run the verify skill. Report its result in full, including any FAIL lines and exact error text.
## Step 7: Report
Finish with a short summary:
- Slug created
- Folder path created
- Gallery file path
- Published state (true or false)
- Verify result (PASS or FAIL)
- Any steps still waiting on the owner (for example, photos not yet copied)
Do not run `git commit` or `git push` in this skill. Commits need the owner's approval in chat.
.claude/skills/verify/SKILL.md---
name: verify
description: Use before reporting any change to this wedding photography website as done. Runs the real build, type check, and output checks, then reports pass or fail.
---
# Verify the wedding site
Run these steps in order from the project root. Report only. Do not fix anything in this skill's run, even if the fix looks obvious. If a step fails, record the exact error text and continue with the remaining steps so the report is complete.
## Step 1: Type and content check
Run:
```
npm run check
```
- PASS: exit code 0.
- FAIL: exit code not 0. Copy the exact error lines.
## Step 2: Build
Run:
```
npm run build
```
- PASS: exit code 0 and dist/ exists.
- FAIL: exit code not 0. Copy the exact error lines.
If Step 2 fails, mark Steps 3 to 7 as SKIPPED (build failed) and go to the report.
## Step 3: Required output files
Confirm each of these exists under dist/:
- index.html
- galleries/index.html
- prices/index.html
- contact/index.html
- thank-you/index.html
- privacy/index.html
- sitemap.xml
Use `ls` or a file-existence check. List every missing path in the failure text.
## Step 4: Internal links
Scan every HTML file in dist/ for `href` values that start with `/` and point to a page that does not exist in dist/. Ignore links to files under /galleries/ that are image or asset paths. Example check:
```
grep -rhoE 'href="/[^"#?]*"' dist/ | sort -u
```
For each path, confirm a matching file or folder with index.html exists in dist/. List each broken link with the page it came from.
## Step 5: Image alt text
Scan dist/ for `<img` tags that have no `alt` attribute. Example check:
```
grep -rnoE '<img[^>]*>' dist/ | grep -v 'alt='
```
Any match is a FAIL. Report the file and the full img tag.
## Step 6: Unpublished galleries stay out of the build
1. Find every file in src/content/galleries/ whose frontmatter has `published: false`. Take each file name without `.md` as the slug.
2. For each slug, run:
```
grep -rl "<slug>" dist/
```
Any file returned is a FAIL. This includes public image copies under dist/galleries/<slug>/.
If there are no unpublished galleries, mark this step PASS with the note "no unpublished galleries".
## Step 7: No hardcoded prices
Run:
```
grep -rnE '\$[0-9]' src/pages src/components
```
Any match is a FAIL. Prices must come from src/data/prices.json only. Report the file, line number, and matched text.
## Report format
Print one line per step, in this exact shape:
```
Step 1 Type and content check: PASS
Step 2 Build: FAIL
error: <exact error text>
Step 3 Required output files: SKIPPED (build failed)
...
Overall: FAIL (2 steps failed)
```
End with `Overall: PASS` only when all seven steps pass. Do not add suggestions for fixes unless the owner asks for them.
INSTALL.md# Install and run the wedding photography site
## What Claude Code is and what this folder is for
Claude Code is an AI assistant that works in your terminal. It can create files, edit them, run commands, and check its own work.
This folder holds the plan and rules that tell Claude how to build your wedding photography website. The site has a home page, galleries, prices, an about page, and a contact form.
You review the work and decide what goes live.
## What you need
- A computer with a terminal app
- Internet access
- A Claude account
- About an hour for setup
## Step 1: Install Node.js
1. Go to nodejs.org and download the LTS version.
2. Run the installer and accept the defaults.
3. Open a new terminal window and type:
```
node --version
```
You should see a version number that starts with `v`.
## Step 2: Install Claude Code
Follow the official installer instructions for Claude Code.
Then type:
```
claude --version
```
You should see a version number. Use version 2.1.296 or newer.
## Step 3: Create the project folder
Type these in your terminal:
```
mkdir wedding-site
cd wedding-site
git init
```
You should see a line that says the Git repository was initialized.
## Step 4: Copy the project files
Copy these into the `wedding-site` folder:
- CLAUDE.md
- SPEC.md
- PLAN.md
- REVIEW.md
- INSTALL.md
- The `.claude` folder
To check, list the files:
- Mac or Linux: `ls -a`
- Windows PowerShell: `dir -Force`
You should see `.claude` in the list.
## Step 5: Start Claude Code and log in
In the `wedding-site` folder, type:
```
claude
```
Follow the login prompts. A browser window may open.
When you see the Claude prompt, you are ready.
This project is set to use Sonnet at medium effort. Sonnet costs less than Opus for routine website work.
## Step 6: Add the two plugins
Type these in your terminal, in the `wedding-site` folder, one at a time:
```
claude plugin marketplace add anthropics/skills
claude plugin install frontend-design@anthropic-agent-skills
claude plugin install code-review@claude-plugins-official
```
What they do:
- **frontend-design** gives Claude guidance on layout and typography, so the site looks professional.
- **code-review** runs a second look at changes before you accept them.
## Step 7: Replace the placeholders (when you are ready)
The placeholders are:
- YOUR_NAME, YOUR_EMAIL, and YOUR_PHONE in CLAUDE.md and SPEC.md
- FORM_ENDPOINT_ID in `src/data/site.ts`, which Phase 1 and Phase 4 create
You can replace them now or later. Do not put passwords or API keys in these files.
## Step 8: Start Phase 1
In Claude Code, paste the Phase 1 prompt from PLAN.md as your first message. It starts with `/goal`.
Claude will create the project and run checks. Approve permission prompts as they appear. Claude will not push code or commit without your approval.
To stop a goal at any time, type:
```
/goal clear
```
## Step 9: Look at the result
Phase 1 ends with a working development server. In a second terminal window, in the `wedding-site` folder, type:
```
npm run dev
```
Open http://localhost:4321 in your browser.
You should see the home page with a headline and a placeholder hero area.
To stop the server, press Ctrl+C in that terminal.
## Step 10: Run the remaining phases
Run one phase per session:
1. Finish a phase and check that its goal passed.
2. Type `/clear` in Claude Code.
3. Paste the next phase prompt from PLAN.md.
Read the "check by hand" notes in PLAN.md for each phase, such as reading the copy.
## Daily habits
- Type `/clear` between unrelated tasks.
- Type `/compact` at natural breaks in a long task.
- Type `/usage` to see where your tokens went.
- Ask Claude to explain a plan before big changes.
## Cost note
Sonnet is the default model in this project, so routine work costs less than it would on Opus.
Hard problems may need more effort. Use `/effort` to change it, ideally in the middle of a task rather than at the start of a new one.
## Safety notes
- Never paste passwords or API keys into the chat.
- Use placeholders until you set real values in your hosting dashboard.
- Claude asks before pushing, publishing, or deleting folders under `src/` or `public/`.
- Read the REVIEW.md checklist before accepting any change.
## Deploy the site
1. Push the project to a GitHub or GitLab repository that you own. You do this step yourself.
2. Sign in to Netlify or Cloudflare Pages.
3. Choose the option to import a project from Git, then select your repository.
4. Set the build command to:
```
npm run build
```
5. Set the publish directory to:
```
dist
```
6. Add the real form endpoint value in the hosting dashboard, not in any file.
7. Deploy the site.
8. Open the live address and check every page on your phone.
Before you deploy, run the verify skill and complete the REVIEW.md checklist by hand.
## If something goes wrong
- **"command not found: node"**: Close and reopen the terminal. If it still fails, reinstall Node.js LTS.
- **Build fails**: Copy the error text and paste it into Claude. Ask it to run the verify skill.
- **Claude gets stuck**: Type `/goal clear`, then `/clear`, then describe the problem again.
PLAN.md# Build plan
This plan takes the project from an empty folder to a live site in five phases. Run one phase per Claude Code session. Use `/clear` between phases.
## How to use each phase
1. Read the phase goal and the files it touches.
2. Paste the `/goal` prompt into Claude Code.
3. Let it run. To stop early, type `/goal clear`.
4. Do the checks marked **Owner checks by hand**.
5. Only start the next phase when the current one passes.
Every `/goal` prompt has an end state, exact check commands, constraints, and a 30-turn limit.
---
## Phase 1: Scaffold
**Goal:** Create the Astro project, base layout, navigation, footer, and a Home page with a placeholder hero.
**Files likely touched:**
- `package.json`, `astro.config.mjs`
- `src/layouts/`, `src/components/Nav.astro`, `src/components/Footer.astro`
- `src/pages/index.astro` and stub pages for galleries, prices, about, contact, thank-you, privacy
- `src/styles/` (one plain CSS file)
**`/goal` prompt:**
```
/goal Phase 1 of wedding-site is complete. End state: an Astro project exists in this folder with a base layout (Nav and Footer components), a Home page with a placeholder hero image area and a headline, and stub pages for galleries, prices, about, contact, thank-you, and privacy that each render a title and the text "Coming soon". Check: run `npm run build` and `npm run check`; both must exit with code 0, and `npm run build` must create dist/index.html. Also run `npm run dev` in the background and confirm localhost:4321 returns HTTP 200 for / and /galleries. Constraints: add only Astro and no other dependencies without asking; do not use real names, emails, or phone numbers (keep YOUR_NAME, YOUR_EMAIL, YOUR_PHONE placeholders); do not commit or push; do not delete files outside the new project; do not touch .claude/settings.json; use plain CSS in one stylesheet. Stop and report the failing command and error if a check cannot pass after three attempts. Turn bound: stop after 30 turns and report what is done and what is not.
```
**Owner checks by hand:**
- Open localhost:4321 and confirm the header, footer, and placeholder hero appear.
- Confirm the placeholders YOUR_NAME, YOUR_EMAIL, and YOUR_PHONE are still in place.
---
## Phase 2: Galleries
**Goal:** Add the gallery content collection, the Galleries index, the Gallery detail template, and two sample galleries with placeholder images.
**Files likely touched:**
- `src/content.config.ts` (collection schema)
- `src/content/galleries/` (two sample files)
- `src/pages/galleries/index.astro`, `src/pages/galleries/[slug].astro`
- `src/components/GalleryGrid.astro`
- `public/galleries/<slug>/` (placeholder images)
**`/goal` prompt:**
```
/goal Phase 2 of wedding-site is complete. End state: a content collection for galleries exists with a schema requiring title, year, published (boolean), cover image path, cover alt text, and story. Two sample galleries exist in src/content/galleries/ using placeholder slugs (sample-one, sample-two), both with published: true, and one unpublished sample (sample-draft) with published: false. The Galleries index lists only published galleries and shows the first name and year only. Each published gallery has a detail page with a photo grid using lazy loading. Check: run `npm run build` and `npm run check`; both must exit with code 0. Confirm dist/galleries/sample-one/index.html and dist/galleries/sample-two/index.html exist. Confirm dist/galleries/sample-draft/ does not exist and the string "sample-draft" does not appear in dist/. Constraints: add no new dependencies without asking; use placeholder images only (no real wedding photos); no real names or surnames; do not commit or push; do not touch .claude/settings.json. Stop and report the failing command and error if a check cannot pass after three attempts. Turn bound: stop after 30 turns and report what is done and what is not.
```
**Owner checks by hand:**
- Open the Galleries index and confirm only the two published samples appear.
- Open one gallery detail page and confirm the photo grid loads.
---
## Phase 3: Prices and About
**Goal:** Add the price data file with three packages, the Prices page using the PriceCard component, and the About page with a bio placeholder.
**Files likely touched:**
- `src/data/prices.json` (three packages)
- `src/components/PriceCard.astro`
- `src/pages/prices.astro`, `src/pages/about.astro`
**`/goal` prompt:**
```
/goal Phase 3 of wedding-site is complete. End state: src/data/prices.json holds exactly three packages, each with name, price, and a list of included items. The Prices page renders one PriceCard per package, reading only from prices.json. The About page shows a bio placeholder paragraph, a portrait placeholder image with alt text, and a short approach paragraph. Check: run `npm run build` and `npm run check`; both must exit with code 0. Run a search of src/pages and src/components for a dollar sign followed by digits; it must find nothing. Confirm dist/prices/index.html contains all three package names. Constraints: no prices typed into any .astro file; no new dependencies without asking; use placeholder copy only (no lorem ipsum, mark any unknown facts as YOUR_ placeholders); no real personal details; do not commit or push; do not touch .claude/settings.json. Stop and report the failing command and error if a check cannot pass after three attempts. Turn bound: stop after 30 turns and report what is done and what is not.
```
**Owner checks by hand:**
- Open the Prices page and read each package. Replace the example prices in prices.json with your real ones.
- Read the About bio and replace the placeholder with your own words.
---
## Phase 4: Contact and Privacy
**Goal:** Build the contact form with validation, a honeypot field, error and success states, the Thank-you page, and the Privacy page.
**Files likely touched:**
- `src/pages/contact.astro`, `src/pages/thank-you.astro`, `src/pages/privacy.astro`
- `src/data/site.ts` (contains the placeholder `FORM_ENDPOINT_ID`)
- `src/components/Footer.astro` (privacy link)
**`/goal` prompt:**
```
/goal Phase 4 of wedding-site is complete. End state: the Contact page has a form with name (required), email (required), wedding date, venue, message (required), and a hidden honeypot field. Required fields are checked in the browser and the endpoint is read from src/data/site.ts, where it stays as the placeholder FORM_ENDPOINT_ID. The form shows a plain error message and keeps typed values on failure, and redirects to /thank-you on success. The Thank-you page and the Privacy page exist. The Privacy page explains what the form collects, why, and that no tracking scripts are used. The footer and the Contact page both link to Privacy. Check: run `npm run build` and `npm run check`; both must exit with code 0. Confirm dist/contact/index.html contains a honeypot input and dist/thank-you/index.html and dist/privacy/index.html exist. Search src/ for any real-looking endpoint key or email address; it must find only placeholders. Constraints: do not invent or commit a real endpoint key or email password; no new dependencies without asking; no analytics or tracking scripts; do not commit or push; do not touch .claude/settings.json. Stop and report the failing command and error if a check cannot pass after three attempts. Turn bound: stop after 30 turns and report what is done and what is not.
```
**Owner checks by hand:**
- Submit the form with an empty required field and confirm the error message and kept text.
- Read the Privacy page and confirm it matches what you actually do with inquiries.
- Replace FORM_ENDPOINT_ID in src/data/site.ts yourself when your form service is ready.
---
## Phase 5: Launch readiness
**Goal:** Add the sitemap, meta tags, the Open Graph image, an image size check, a Lighthouse check, and deploy notes for Netlify.
**Files likely touched:**
- `astro.config.mjs` (sitemap integration, only if already available; see constraints)
- `src/components/Head.astro` or the base layout (meta tags)
- `public/og/home.jpg` (Open Graph image)
- `DEPLOY.md` (deploy notes)
**`/goal` prompt:**
```
/goal Phase 5 of wedding-site is complete. End state: every page has a unique title and meta description. The Home page has an Open Graph image tag pointing to a file in public/. Images in public/ are each under 400 KB and at most 2000px wide. A sitemap.xml exists in dist/. A DEPLOY.md file exists with plain steps for Netlify: connect the repo, build command npm run build, publish folder dist, and set any secrets in the hosting dashboard only. Check: run `npm run build` and `npm run check`; both must exit with code 0. Run the verify skill and it must pass. Confirm dist/sitemap.xml exists and that each HTML file in dist/ has a non-empty title and meta description. Lighthouse: if the Lighthouse CLI is already available, run it on the Home page and the Galleries index and record the mobile performance scores. If it is not available, do not install it; write NOT MEASURED for those scores and tell the owner to run Lighthouse in Chrome DevTools. Constraints: add a sitemap integration only if it is listed in package.json or the owner approves it in chat; no other new dependencies without asking; no real personal data; no secrets committed; do not commit or push; do not touch .claude/settings.json. Stop and report the failing command and error if a check cannot pass after three attempts. Turn bound: stop after 30 turns and report what is done and what is not.
```
**Owner checks by hand:**
- Run Lighthouse in Chrome DevTools on the Home page and the Galleries index. Confirm mobile performance is 85 or higher.
- Share the Home page link in a chat app and confirm the preview image appears.
- Read DEPLOY.md and follow it once on Netlify or Cloudflare Pages.
- Complete the REVIEW.md checklist by hand before you accept the launch.
REVIEW.md# Review checklist
Use this list before accepting any change to the site. Claude and the owner both work through it.
## Run first
- Run the verify skill first. Any failure blocks approval.
- Then go through the items below by hand.
## Prices
- Every price shown matches `src/data/prices.json`.
- No price appears in any `.astro` file.
## Images
- Images are web-sized: 2000px wide or less, under 400 KB.
- Every image has descriptive alt text.
- No client names or couple names appear in filenames.
- Unpublished galleries do not appear in the built output (`dist/`).
## Forms
- Required fields are enforced on the client and the server.
- The honeypot field is present.
- Success and error states both exist. Check by reading the code path.
## Copy
- No lorem ipsum.
- No placeholder text except the YOUR_NAME, YOUR_EMAIL, and YOUR_PHONE markers.
## Privacy
- The privacy page is linked from the footer and from the contact page.
## Mobile
- The layout reads correctly at 375px width.
- Write down what you checked: pages, nav, form, and gallery grid.
## Secrets
- No API keys, passwords, or real endpoints are committed.
- FORM_ENDPOINT_ID stays a placeholder in `src/data/site.ts`.
## Scope
- No tracking, analytics, or new third-party scripts were added.
- Exception: the owner approved it in writing in the conversation.
## Output format
Start with Pass or Fail.
If Fail, add one bullet per failed item:
- `path/to/file` line N: what failed and why
SPEC.md# Website spec
This document describes what the wedding photography website must do, page by page. You can check each item by opening the site in a browser.
## Goal
Turn visitors into inquiry emails. Every page has one clear call to action: **Check availability**. The button links to the Contact page.
## Pages
### Home
- Hero photo across the top, with a short headline.
- One intro line about the photographer.
- Three featured galleries, chosen from published galleries.
- Call to action: Check availability.
### Galleries index
- A grid of published weddings.
- Each item shows the cover photo, the couple's first name, and the wedding year.
- Only galleries with `published: true` appear.
### Gallery detail (one page per wedding)
- A photo grid for the wedding.
- A short story paragraph.
- Shows the couple's first name and wedding year only. No surnames, venues, or addresses.
- Call to action: Check availability.
### Prices
- Package cards built from `src/data/prices.json`.
- Each card shows the package name, the price, and what is included.
- No prices are typed into the page itself.
- Call to action: Check availability.
### About
- Photographer bio, a portrait photo, and a short paragraph on approach.
- Call to action: Check availability.
### Contact
- A form with these fields:
- Name (required)
- Email (required)
- Wedding date (optional)
- Venue (optional)
- Message (required)
- Honeypot spam field, hidden from people and filled in only by bots
- On success, the visitor is redirected to the Thank-you page.
- On failure, the page shows a plain message and keeps everything the visitor typed.
### Thank-you
- Confirms the inquiry was sent.
- Says when the photographer usually replies.
- Links back to Galleries and Prices.
### Privacy
- Explains what the contact form collects (name, email, wedding date, venue, message).
- Explains why it is collected (to reply to the inquiry).
- States that no analytics or tracking scripts are used.
- Linked from the footer and from the Contact page.
## Contact form rules
- Required fields are checked in the browser.
- Required fields are checked again on the server endpoint.
- The endpoint is set in `src/data/site.ts` as the placeholder `FORM_ENDPOINT_ID` until the owner sets a real one.
- The honeypot field must stay empty for a valid submission.
## Galleries rules
- Only files with `published: true` appear on the index and get pages in the built site.
- Unpublished galleries must not appear anywhere in `dist/`.
- Gallery pages show the couple's first name and wedding year only.
## SEO basics
- Each page has a unique title and a unique meta description.
- Each page has exactly one `h1`.
- A sitemap is generated at `sitemap.xml`.
- The Home page has an Open Graph image.
## Performance
- Lighthouse performance score of 85 or higher on mobile for the Home page and the Galleries index.
- Gallery images are lazy-loaded.
- Images are resized to a maximum width of 2000px and kept under 400 KB.
## Accessibility
- All pages work with the keyboard alone.
- Text and background colors pass WCAG AA contrast.
- Every image has descriptive alt text.
- Form fields have visible labels.
## Out of scope for v1
- Online booking calendar
- Payments
- Client login
- Blog
- Email newsletter
- Multiple languages
- Analytics or tracking scripts (add only if the owner asks in a plan)
## Acceptance checklist
- [ ] `npm run build` succeeds.
- [ ] `npm run check` passes.
- [ ] Every page listed above renders.
- [ ] The contact form shows a success state and an error state.
- [ ] The verify skill passes.