Remote-friendly cafe directory with Hugo
Static Hugo site listing cafes rated for wifi speed, plugs and noise, with moderated visitor submissions and no required JavaScript.
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 remote-friendly-cafe-directory-with-hugo-0cf30bCLAUDE.md# Cafe directory
## Purpose
Static directory of remote-friendly cafes, rated for wifi speed, plugs and noise.
Data is visitor-submitted and moderated before publishing. Nothing goes live
without a maintainer review.
## Stack
- Hugo extended. Pin the version here: `HUGO_VERSION=<set-me>` (keep in sync with hosting).
- Plain HTML and CSS templates in `layouts/`. No JS framework, no theme, no database.
- Zero required JavaScript to read the directory. Filters work through GET query params.
- Product spec: `SPEC.md`. Build plan: `PLAN.md`. Review rules: `REVIEW.md`.
## Commands
- Local preview: `hugo server -D` (port 1313).
- Build: `hugo --gc --minify --printPathWarnings`.
- Validate content: `python3 scripts/check_cafes.py`.
- Full proof of work: `/verify` skill. Run it before saying work is done.
- Long build output: delegate to the `build-runner` subagent.
## Content layout
- One Markdown file per cafe: `content/cafes/<city>/<slug>.md`.
- City index pages are generated from front matter. Never write city lists by hand.
- Slugs: lowercase, hyphenated, unique within the city.
- Dates: ISO format, `YYYY-MM-DD`.
## Front matter schema
Required fields:
- `name`, `city`, `address`
- `wifi_mbps`: number, 0 to 1000
- `wifi_source`: `self-reported` or `measured`
- `plugs`: `none`, `few` (1-5 seats), `many` (more than 5)
- `noise`: `quiet`, `moderate`, `loud` (scale defined in SPEC.md; do not add values)
- `laptop_friendly`: `true` or `false`
- `hours`, `website`, `lat`, `lng`
- `last_verified`: date
- `submitted_via`: `form` or `manual`
## Rules
- Never publish visitor names, emails, phone numbers or IP data, in any field.
`submitted_via` is the only submission metadata allowed in content.
- Never edit `public/` or `resources/` by hand. They are generated.
- Never invent addresses, hours or wifi numbers. Ask the user for missing facts.
- A wifi number without `wifi_source: measured` is shown with a self-reported label.
- Use `wifi_source: measured` only with a speed test date and method on record.
## Forms
- The submission endpoint is the placeholder `FORM_ENDPOINT_ID` in
`layouts/partials/submit-form.html`.
- Never commit a real endpoint key, account token or secret.
- Real values live in the hosting dashboard or environment variables.
## Gotchas
- Hugo path warnings fail the build. Fix them, do not suppress them.
- `hugo --gc` removes unused generated resources. Expect them to disappear after
template changes.
- Keep `scripts/` on the Python standard library. No new packages without asking.
## Workflow
- Explore, then plan, then code, then run `/verify`.
- Changes to content go through `/add-cafe` when they come from a submission.
- Second-pass review of larger diffs: `/code-review`.
@REVIEW.md
.claude/agents/build-runner.md---
name: build-runner
description: Runs the Hugo build and content check scripts for the cafe directory and returns only failures or a short pass summary. Use when build or check output is long and the main session should stay small.
model: haiku
maxTurns: 12
tools: Bash, Read
---
You run build and validation commands for the cafe directory and report results. You do not fix anything.
Commands you may run, in this order unless the parent asks for a subset:
1. `hugo --gc --minify --printPathWarnings`
2. `python3 scripts/check_cafes.py`
Rules:
- Do not edit, create or delete any file in the project. Your only write is capturing output to a temp file made with `mktemp` outside the repo.
- For each command, redirect stdout and stderr to a temp file, record the exit code, then read the file.
- Treat any line starting with WARN as a failure, even if the exit code is 0.
- Read only the lines needed to locate each failure. Do not dump full logs.
Return format:
- One line per command: `<command>: PASS (exit 0)` or `<command>: FAIL (exit N)`.
- For each failure, one line: `<file path>:<line number>: <one-line message>`. If no file or line is given, write `<command>: <one-line message>`.
- If everything passes, return the pass lines and nothing else.
Never return more than 30 lines to the parent.
.claude/settings.json{
"permissions": {
"defaultMode": "acceptEdits",
"allow": [
"Bash(hugo *)",
"Bash(python3 scripts/*)",
"Bash(git status)",
"Bash(git diff *)",
"Bash(git log *)",
"Bash(ls *)",
"Bash(npx --yes broken-link-checker *)"
],
"ask": [
"Bash(git commit *)",
"Bash(git push *)",
"Bash(npm install *)",
"Bash(npx *)"
],
"deny": [
"Bash(rm -rf *)",
"Bash(git push --force *)",
"Bash(git reset --hard *)",
"Read(./.env)",
"Read(./.env.*)",
"Read(./**/secrets*)"
]
}
}
.claude/skills/add-cafe/SKILL.md---
name: add-cafe
description: Turns one approved visitor submission into a valid cafe content file, asking for missing facts instead of guessing. Run only when the maintainer types /add-cafe.
disable-model-invocation: true
argument-hint: pasted submission or file path
---
# Add cafe
Convert one approved submission into `content/cafes/<city>/<slug>.md`. The maintainer has already reviewed the submission. Your job is to map it accurately and write nothing you cannot source from it.
## Steps
1. **Read the submission.** Use the pasted text or the file path in the argument. If neither is given, ask for it and stop.
2. **Map fields to the schema.** The front matter schema is in CLAUDE.md. Fields are: name, city, address, wifi_mbps, wifi_source, plugs, noise, laptop_friendly, hours, website, lat, lng, last_verified, submitted_via. Use only the exact enum values from SPEC.md:
- `plugs`: none, few, many
- `noise`: quiet, moderate, loud
- `wifi_source`: self-reported or measured
- `laptop_friendly`: true or false
3. **Ask for missing required fields.** List every required field the submission does not provide. Ask the user for each one and wait for the answers before writing. Do not guess or fill a placeholder.
4. **Never invent facts.** Do not make up hours, addresses, coordinates or wifi numbers. If a value is unclear, such as a vague noise description, ask the user to choose one enum value.
5. **Set the source fields.**
- `wifi_source`: set to `self-reported` unless the submission includes a speed test with a date and method. Only then set `measured`.
- `submitted_via`: set to `form`.
- `last_verified`: set to today's date in `YYYY-MM-DD`.
6. **Remove personal data.** Strip names, email addresses, phone numbers and IP data from every free-text field before writing. Keep the business name and public business details only. If you removed something, tell the user what kind of data was removed, without repeating it.
7. **Check the slug.** Build the slug from the cafe name: lowercase, hyphenated, ASCII only. Check whether `content/cafes/<city>/<slug>.md` already exists. If it does, stop and report the conflict. Do not overwrite it.
8. **Write the file.** Save to `content/cafes/<city>/<slug>.md` with the front matter fields in the order listed in step 2. Use a short body only if the submission gives a useful public description. Do not add visitor notes or reviews.
9. **Run verify.** Invoke the `verify` skill. Report its table and the overall result. If verify fails, show the first failing line and fix only the file you just wrote, with the user's approval.
## Output
Report in this order:
- The file path written, or the reason nothing was written
- Fields still missing, if any
- Personal data removed, described by type only
- The verify result (`VERIFY PASSED` or `VERIFY FAILED`)
Do not run `git commit` or `git push`. The maintainer does that after review.
.claude/skills/verify/SKILL.md---
name: verify
description: Use after any change to content, layouts, scripts or config, and before saying work is done. Runs the build and content checks and reports each result with its exit code.
allowed-tools: Bash(hugo *), Bash(python3 scripts/*), Bash(git status), Bash(git diff *), Read
---
# Verify
This skill is the proof of work for the cafe directory. Do not report a change as done until every step below has run and passed.
## Rules
- Run each command yourself in this session. Do not rely on earlier output.
- Show the exit code for every check. Never mark a check as passed without it.
- Delegate long build output to the `build-runner` subagent so the main context stays small. Ask it to run the build and check commands and return only pass or fail with failing lines.
- Do not edit any file during verification. If a check fails, report it and stop.
## Step 1: Hugo build
Run:
```
hugo --gc --minify --printPathWarnings
```
Pass only if the command exits 0 and prints no line starting with `WARN`. Record the first `WARN` or `ERROR` line if it fails.
## Step 2: Content check
Run:
```
python3 scripts/check_cafes.py
```
Pass only if the command exits 0. Record the first failing line if it fails. The script checks required front matter fields, enum values (plugs, noise, wifi_source), `wifi_mbps` between 0 and 1000, and ISO dates in `last_verified`.
## Step 3: Output files
Confirm that both files exist and are non-empty:
- `public/index.html`
- `public/sitemap.xml`
Use `ls -l` or Read to check size. Pass only if both are present with a size greater than 0.
## Step 4: Link check (optional)
Run this only when the Hugo site is already running on port 1313 (`hugo server -D`). If it is not running, mark the step as skipped and say so.
```
npx --yes broken-link-checker http://localhost:1313 --recursive --ordered
```
This command needs approval under the project permissions. Report broken internal links only. Ignore external links that time out, and say that you ignored them.
## Step 5: Report
Finish with a table in this form:
| Check | Command | Result | Exit code | First failing line |
|---|---|---|---|---|
| Hugo build | `hugo --gc --minify --printPathWarnings` | pass or fail | 0 or other | none or the line |
| Content check | `python3 scripts/check_cafes.py` | pass or fail | 0 or other | none or the line |
| Output files | ls on public/index.html and public/sitemap.xml | pass or fail | n/a | none or the missing path |
| Link check | broken-link-checker | pass, fail or skipped | 0 or other | none or the broken link |
Then give one overall line: `VERIFY PASSED` only when every required step passed. The link check counts as required only when it ran. Otherwise write `VERIFY FAILED` and list the steps to fix first.
INSTALL.md# Install and run the cafe directory
This guide sets up the project for a solo builder with some experience. It covers local setup, the first build, the phase workflow, deploy and daily-use tips.
## Prerequisites
- Hugo extended. Use the version pinned in CLAUDE.md (`HUGO_VERSION`). Check with `hugo version`.
- Git.
- Python 3. Check with `python3 --version`. The validation script uses the standard library only.
- Node.js, only for the link checker run by `/verify` (`npx --yes broken-link-checker`). Skip it if you do not need that step.
- Claude Code, installed and logged in. Check with `claude --version`.
## Create the project
```
hugo new site cafe-directory
cd cafe-directory
git init
```
Copy these into the project root: `CLAUDE.md`, `SPEC.md`, `PLAN.md`, `REVIEW.md`, `INSTALL.md` and the `.claude/` folder.
Add a theme or leave it out. The plan builds plain layouts in `layouts/`, so no theme is required.
Make a first commit of the scaffolding before you start Phase 1. This gives you a clean diff to review later.
## Install plugins and skills
Start Claude Code in the project folder:
```
claude
```
Inside Claude Code, run `/plugin` and install:
- `code-review@claude-plugins-official`
- `commit-commands@claude-plugins-official`
Both come from the official marketplace, which Claude Code adds automatically.
In a separate terminal, add the skills marketplace and install the frontend skill:
```
claude plugin marketplace add anthropics/skills
```
Then install `frontend-design@anthropic-agent-skills` from inside Claude Code.
Restart Claude Code so the plugins and skills load.
## Confirm the empty site passes
Run this once inside Claude Code:
```
/verify
```
On the empty project, `/verify` should report the tooling works. It may report that there is no site content yet. That is expected before Phase 1.
## Start a phase
Each phase in PLAN.md ends with a `/goal` prompt. To start a phase:
1. Run `/clear` so the new phase starts with a small context.
2. Paste the phase `/goal` prompt from PLAN.md, or the Phase 1 prompt from the kickoff section.
3. Let Claude work until the goal is met or the turn bound is reached.
4. Read the summary. If it reports blocked items, fix them before moving on.
Run `/clear` between phases, not in the middle of one. Commit after each phase passes, and review the diff with `/code-review` for larger changes.
## Add and moderate a cafe
Visitor submissions arrive through the form service inbox. For each one:
1. Review it against the moderation checklist in REVIEW.md.
2. Paste the approved submission into Claude Code and run `/add-cafe` with the pasted text or a file path.
3. Answer any questions the skill asks for missing facts. Do not let it guess hours, addresses or wifi numbers.
4. Review the new file in `content/cafes/<city>/`, then let `/verify` run.
5. Commit and push when you are happy with it.
## Deploy
Use GitHub Pages or Netlify. Both work with the same build settings:
- Build command: `hugo --gc --minify`
- Publish folder: `public`
- Hugo version: match the version pinned in CLAUDE.md. Set it in the hosting dashboard if the host lets you choose it.
Form endpoint: the submission form has a `FORM_ENDPOINT_ID` placeholder in `layouts/partials/submit-form.html`. Replace it with your form service endpoint only in the hosting environment or at build time, not in a committed file. If you cannot make that work for your host, leave the placeholder in place and note it in the launch checklist from Phase 5.
Before the first public deploy, run `/verify` on the final build and check the Phase 5 deploy checklist.
## Daily use and cost tips
- Run `/usage` to see where tokens went. Check it after long sessions.
- Run `/compact` at natural breaks, such as after a finished task.
- Run `/clear` between unrelated tasks.
- Keep `/goal` turn bounds low. A bound of 25 turns is enough for a phase. Lower it for small tasks.
- Change effort with `/effort` when you need more reasoning mid-task. Changing models breaks the prompt cache, so avoid it mid-task.
- Use the build-runner subagent for long build output. It keeps the main session small.
## Safety
- Never paste a real form key, account token or endpoint into the chat or into any file in the repo.
- Keep secrets in environment variables or the hosting dashboard.
- `.claude/settings.json` denies reads of `.env` files and `secrets*` files, and asks before `git commit` and `git push`. Keep those rules.
- Check `git status` before each commit to confirm no secret or generated file is staged.
PLAN.md# Build plan
Five phases. Run them in order. Run `/clear` before pasting each phase's `/goal` prompt.
Each `/goal` is judged by a small model that sees only what appears in the conversation. Every prompt therefore asks Claude to run the check command and print its exit code and last lines of output. Use `/goal clear` to stop a goal early.
## Phase 1: Scaffold
Tasks:
- Create the Hugo site at the repo root with `hugo.toml`.
- Add a base template and a cafe list template under `layouts/`.
- Add one sample cafe with clearly marked sample values.
- Add `scripts/check_cafes.py` with the required-field rules.
Files touched: `hugo.toml`, `layouts/`, `content/cafes/lisbon/sample-cafe.md`, `scripts/check_cafes.py`.
```
/goal Phase 1 (Scaffold) is complete when all of these are true:
(1) a Hugo site exists at the repo root with hugo.toml, a base template and a cafe list template under layouts/;
(2) content/cafes/lisbon/sample-cafe.md exists with every required front matter field from CLAUDE.md filled with clearly marked sample values;
(3) scripts/check_cafes.py exits 0 on the sample file and exits 1 when a required field is removed from a temporary copy;
(4) `hugo --gc --minify --printPathWarnings` exits 0 with no WARN lines;
(5) public/cafes/sample-cafe/index.html and public/sitemap.xml exist.
Check: run each command in this conversation and show its exit code and last 5 lines of output before concluding.
Constraints: edit only hugo.toml, layouts/, content/, scripts/ and .claude/; use only Hugo and the Python standard library; no git commit or git push; no secrets or real endpoints; keep each new file under 150 lines; if Hugo is not installed, stop and report what to install.
Turn bound: stop after 25 turns and write a short summary of what passes, what fails and what is blocked.
```
## Phase 2: Data model
Tasks:
- Finalize the front matter schema in CLAUDE.md to match SPEC.md.
- Write the rules in `check_cafes.py`: required fields, enum values, wifi_mbps from 0 to 1000, ISO dates.
- Add 6 cafes across Lisbon and Berlin (3 each). Use real cafes only if their facts are verified; otherwise mark them clearly as samples.
Files touched: `CLAUDE.md`, `scripts/check_cafes.py`, `content/cafes/lisbon/`, `content/cafes/berlin/`.
```
/goal Phase 2 (Data model) is complete when all of these are true:
(1) the front matter schema in CLAUDE.md matches SPEC.md field names and enums;
(2) scripts/check_cafes.py rejects a file with a bad enum, wifi_mbps above 1000, or a non-ISO date (show each rejection once);
(3) exactly 6 cafe files exist, 3 under content/cafes/lisbon/ and 3 under content/cafes/berlin/, each marked as sample or verified;
(4) `python3 scripts/check_cafes.py` exits 0 on all files under content/cafes/.
Check: run each command in this conversation and show its exit code and last 5 lines of output before concluding.
Constraints: edit only CLAUDE.md, scripts/, content/ and .claude/; no invented addresses, hours or wifi numbers; no git commit or git push; keep each file under 150 lines.
Turn bound: stop after 25 turns and write a short summary of what passes, what fails and what is blocked.
```
## Phase 3: Directory pages
Tasks:
- Build city index pages generated from front matter. Never hand-write cafe lists.
- Add filters. Pre-render one page per single filter value (plugs, noise, laptop_friendly, min wifi_mbps bands) so they work with JavaScript off.
- Add GET parameters for the same filters, read by a small progressive script.
- Build the cafe detail page with a labelled wifi source.
- Add `scripts/test_filters.py`, which builds the site and checks the filtered pages list the expected cafes.
Files touched: `layouts/`, `content/cities/`, `static/js/filters.js`, `scripts/test_filters.py`.
```
/goal Phase 3 (Directory pages) is complete when all of these are true:
(1) city index pages for Lisbon and Berlin are generated from front matter and list all their sample cafes;
(2) pre-rendered single-filter pages exist for each plugs and noise value in each city;
(3) cafe detail pages show the wifi figure with its source label (measured or self-reported);
(4) `python3 scripts/test_filters.py` exits 0 and prints one PASS line per filter page checked;
(5) `hugo --gc --minify --printPathWarnings` exits 0 with no WARN lines.
Check: run each command in this conversation and show its exit code and last 5 lines of output before concluding.
Constraints: no JavaScript needed to read the directory; the optional script is under 60 lines; no framework; edit only layouts/, content/, static/, scripts/ and .claude/; no git commit or git push.
Turn bound: stop after 25 turns and write a short summary of what passes, what fails and what is blocked.
```
## Phase 4: Submission
Tasks:
- Build the submit page with a labelled form, a honeypot field and the placeholder `FORM_ENDPOINT_ID` in `layouts/partials/submit-form.html`.
- Add the moderation checklist to REVIEW.md (it is already imported by CLAUDE.md).
- Create the `/add-cafe` skill and test it on one fake submission in a temp file.
- Run `/verify` after the dry run.
Files touched: `layouts/partials/submit-form.html`, `layouts/submit/`, `REVIEW.md`, `.claude/skills/add-cafe/SKILL.md`.
```
/goal Phase 4 (Submission) is complete when all of these are true:
(1) the submit page builds and contains a labelled form with a honeypot field and the literal text FORM_ENDPOINT_ID;
(2) `npx --yes html-validate public/submit/index.html` exits 0 with zero errors;
(3) /add-cafe, run on one fake submission in a temp file, writes a valid content file, and `python3 scripts/check_cafes.py` exits 0 on it; the temp file is then deleted;
(4) the /verify skill runs and reports all steps passing, with exit codes shown.
Check: run each command in this conversation and show its exit code and last 5 lines of output before concluding.
Constraints: never write a real key, token or endpoint; no git commit or git push; no personal data in the fake submission beyond clearly fake values; edit only layouts/, content/, REVIEW.md, scripts/ and .claude/.
Turn bound: stop after 25 turns and write a short summary of what passes, what fails and what is blocked.
```
## Phase 5: Launch readiness
Tasks:
- Confirm sitemap.xml, canonical tags, meta descriptions and JSON-LD on cafe pages.
- Run the link check on the local server.
- Write a Lighthouse-style checklist as a manual list in INSTALL.md.
- Write deploy notes in INSTALL.md (GitHub Pages, build command, publish folder `public`, endpoint set in the dashboard).
Files touched: `layouts/`, `INSTALL.md`, `scripts/` (only if a check script is added).
```
/goal Phase 5 (Launch readiness) is complete when all of these are true:
(1) every page in public/ has a canonical link and a meta description, and cafe pages have valid LocalBusiness JSON-LD;
(2) public/sitemap.xml lists every city and cafe page;
(3) with `hugo server -D` running, `npx --yes broken-link-checker http://localhost:1313 --recursive --ordered` reports no broken internal links;
(4) INSTALL.md contains the deploy checklist and `grep -c '\[ \]' INSTALL.md` prints 0;
(5) the /verify skill runs and reports all steps passing, with exit codes shown.
Check: run each command in this conversation and show its exit code and last 5 lines of output before concluding.
Constraints: no real endpoint or key in INSTALL.md; no git commit or git push; no deploy; edit only layouts/, INSTALL.md and scripts/.
Turn bound: stop after 25 turns and write a short summary of what passes, what fails and what is blocked.
```
REVIEW.md# Review rules
These rules apply to every code and content change. CLAUDE.md imports this file, so `/code-review` and manual reviews follow the same standard.
## Blocking
- Any secret, API key, token or real form endpoint committed to the repo. The endpoint must stay the `FORM_ENDPOINT_ID` placeholder in `layouts/partials/submit-form.html`.
- Visitor personal data in content: names, emails, phone numbers, IP data, or anything beyond `submitted_via` as submission metadata.
- Any warning from `hugo --gc --minify --printPathWarnings`.
- `python3 scripts/check_cafes.py` failing.
- Files under `public/` or `resources/` edited by hand.
- A cafe with `wifi_mbps` set but no `wifi_source`.
- A `noise` or `plugs` value outside the SPEC enums (noise: quiet, moderate, loud; plugs: none, few, many).
- A cafe page without `last_verified`, or a date that is not `YYYY-MM-DD`.
## Blocking for accessibility
- A form input without a visible label.
- An image without alt text.
- A filter or form control that works only with JavaScript. Filters must work from GET query parameters.
- A keyboard trap or a control that cannot be reached with Tab.
## Non-blocking
- Template duplication that a partial or shared block would remove.
- Long front matter or a body that repeats front matter fields.
- Unclear slugs, such as `cafe1` or names with spaces or capitals.
- A new cafe that is missing from its city index. Index pages are generated, so check the data, not a hand-written list.
- Missing meta description or canonical tag on a new page.
## Content review
- Claims about wifi, plugs or noise need a source or must be labelled self-reported.
- Measured wifi needs a speed test at off-peak time, with date and method noted in the submission or the cafe file.
- No invented addresses, hours, website or coordinates. If a field is unknown, ask the submitter or leave it out, do not guess.
- Noise and plugs follow the SPEC definitions. Do not add new values.
## Moderation review for submissions
Check each visitor submission before `/add-cafe` converts it:
- Spam, link farms or promotional text unrelated to the cafe.
- Duplicate of an existing slug or an existing cafe at the same address.
- Closed or relocated business, or no evidence it exists.
- Unclear location: city missing, address not findable, or coordinates far from the address.
- Personal data inside free text, such as a staff member's name or a reviewer's contact details. Remove it before publishing.
Reject a submission rather than edit a claim you cannot verify.
## Output format
List findings in three groups, in this order: Blocking, Non-blocking, Question. Each finding has the file and line:
- `Blocking: content/cafes/lisbon/example.md:12 wifi_mbps is set but wifi_source is missing.`
If there are no findings in a group, write "None."
## Larger diffs
For larger diffs, run `/code-review` as a second pass after the manual review. Note in the review that the second pass ran.
SPEC.md# Cafe directory spec
## Goal
Help remote workers find cafes in their city with usable wifi, enough plugs and tolerable noise.
## Visitor pages (v1)
- Home: list of cities with cafe counts.
- City index: cafe list with filters for plugs, noise, laptop_friendly and minimum wifi_mbps.
- Cafe detail: name, address, hours, website, wifi speed with its source label, plugs, noise, last_verified date, and a map link from lat and lng.
- Submit a cafe: form for a new cafe (see submission flow).
- About and methodology: how data is collected and what each label means.
## Filters
- Filters are shown on every city index page.
- Filter values appear in the URL as GET parameters, for example `?plugs=many&noise=quiet&min_wifi=50`, so a filtered view can be shared.
- With JavaScript off, each single filter value has its own pre-rendered page, linked from the city index (for example `/lisbon/plugs/many/`). Combined filters need the small script.
- Filters never hide the wifi source label.
## Data definitions
- wifi_mbps: download speed from a speed test run at an off-peak time (weekday mid-morning or mid-afternoon), recorded with date and tool name.
- wifi_source: `measured` when a speed test with date and method is on record, otherwise `self-reported`. Self-reported figures always show a self-reported label.
- plugs: `none` (no seats with power), `few` (1 to 5 seats with power), `many` (more than 5).
- noise: `quiet` (conversation is easy), `moderate` (a raised voice is sometimes needed), `loud` (hard to talk).
- laptop_friendly: `true` or `false`, from the maintainer's visit or a verified submission.
- last_verified: date of the last check, ISO format (YYYY-MM-DD).
## Submission flow
1. Visitor fills the submit form (cafe name, city, address, wifi, plugs, noise, optional note).
2. The form service sends the submission to the maintainer's inbox. No submission data goes into the repo.
3. Maintainer reviews it against the moderation checklist in REVIEW.md.
4. Approved submissions go through `/add-cafe`, which writes one content file.
5. `/verify` runs. Then the change is committed and deployed.
6. Rejected submissions are deleted from the inbox. Nothing is published.
## Non-goals for v1
- User accounts.
- Written reviews.
- Map API keys.
- Payments.
- Server-side code.
- Personal tracking.
## SEO
- Unique title and meta description on each city page and cafe page.
- sitemap.xml generated by Hugo.
- Canonical URL on every page.
- Optional LocalBusiness JSON-LD on cafe pages, using only verified fields.
## Accessibility
- Target: WCAG 2.1 AA.
- Filters and the submit form work with the keyboard alone.
- Every input has a visible label.
- Every image has alt text.
## Performance
- The directory is fully readable with JavaScript off.
- Cafe pages stay under 150 KB total page weight, counting HTML, CSS, images and any script.
## Privacy
- No third-party trackers by default.
- No analytics in v1. If analytics is added, it must be privacy-friendly and listed in this file first.
- No visitor names, emails, phone numbers or IP data in content or in the repo.
## Open questions
- Which cities at launch? Default: Lisbon and Berlin, with sample cafes in both.
- Deployment host? Default: GitHub Pages, build command `hugo --gc --minify`, publish folder `public`.
- Form service? Default: a hosted form service with an inbox. The endpoint lives in a placeholder (`FORM_ENDPOINT_ID`) and the real key is set in the hosting dashboard.
- Analytics? Default: none.
- Who moderates? Default: the maintainer alone.
- Speed test method? Default: a browser speed test on a laptop, recorded with date and tool name.