# 05B Backend / Data / API / Cloudflare Plan — Subnautica2Maps Clean Rerun

- task_id: `t_03caa401`
- owner: 墨枢
- date: 2026-05-20
- project_slug: `subnautica2maps`
- selected_keyword: `subnautica 2 map`
- target_market: US/en
- tenant: `site-rerun-subnautica2maps-20260520-clean`
- clean_rerun: true
- output_artifact: `reports/site-pipeline/site-rerun-subnautica2maps-20260520-clean/subnautica2maps/05b-backend.md`

## 0. Conclusion

Backend verdict: `BACKEND_GO_WITH_CONSTRAINTS`.

P0 should remain mostly static/client-side on Cloudflare Pages:

- No login.
- No Stripe/payment/subscription.
- No user accounts.
- No community submissions.
- No D1 user-state dependency for map usage.
- Marker data ships as static JSON under versioned paths, generated only from provenance-backed clean-rerun data.
- Route calculation and local progress stay browser-side.
- Optional Pages Functions/Worker routes are limited to health/data-version/event-proxy/future admin ingestion, not required for core map usage.

Hard launch blockers carried forward:

1. Real marker JSON + provenance is still required before confirmed/indexable resource pages can ship.
2. Formal public domain was not present in parent metadata; domain email and live CF settings cannot be verified in this task.
3. `hello@domain` and `support@domain` must exist before public launch.
4. CF settings must be checked on the final Zone after domain confirmation: SSL Full Strict, Always HTTPS, Bot Fight/equivalent, Crawler Hints on, Browser Cache TTL, `/api` rate limit if APIs exist, static asset cache.
5. Analytics must be verified by ingestion evidence, not by script presence.
6. Code/deploy tasks must commit + push + deploy the same commit.

Status fields required by task:

```yaml
crawler_hints_status: planned_not_verified_domain_missing
cf_security_settings_status: planned_not_verified_domain_missing
domain_email_status: required_not_verified_domain_missing
```

## 1. Inputs read

Read clean-rerun parent artifacts only:

- PRD v1: `/root/.hermes/kanban/boards/site-factory/workspaces/t_43537dda/reports/site-pipeline/site-rerun-subnautica2maps-20260520-clean/subnautica2maps/03-prd-v1.md`
- Design Acceptance: `/root/.hermes/kanban/boards/site-factory/workspaces/t_ec99f0a1/reports/site-pipeline/site-rerun-subnautica2maps-20260520-clean/subnautica2maps/04b-design-acceptance.md`
- Design Handoff: `/root/.hermes/kanban/boards/site-factory/workspaces/t_f6e7a59a/reports/site-pipeline/site-rerun-subnautica2maps-20260520-clean/subnautica2maps/design/HANDOFF.md`
- Screen Index: `/root/.hermes/kanban/boards/site-factory/workspaces/t_f6e7a59a/reports/site-pipeline/site-rerun-subnautica2maps-20260520-clean/subnautica2maps/design/stitch/screen-index.json`
- Design Verification: `/root/.hermes/kanban/boards/site-factory/workspaces/t_f6e7a59a/reports/site-pipeline/site-rerun-subnautica2maps-20260520-clean/subnautica2maps/design/verification.json`

Not read/reused: old PRD, old design, old seed data, old dist, old repo, old D1/R2/KV/project IDs.

## 2. Site architecture

Recommended stack:

```text
Cloudflare Pages
├── Astro or Next.js static export
├── /public/data/markers/vYYYYMMDD.json
├── /public/data/manifest.json
├── /public/sitemap.xml
├── /public/robots.txt
└── optional Pages Functions
    ├── GET  /api/health
    ├── GET  /api/data-version
    ├── POST /api/events            # optional Plausible/custom proxy; privacy-bounded
    └── GET  /api/markers           # optional if static JSON becomes too large or needs cache headers
```

P0 storage decision:

| Layer | Decision | Reason |
|---|---|---|
| D1 | Not required for public P0 runtime | Core map/search/filter/detail/route/progress is static/client-side; no login/payment/UGC |
| R2 | Optional, only for large map tiles/assets or data archive | Static JSON and generated HUD assets can ship with Pages initially |
| KV | Optional for rate counters/event dedupe if `/api/events` is enabled | Not needed for static map usage |
| Queues | Not P0 | No background ingestion/moderation pipeline in P0 |
| Durable Objects | Not P0 | No realtime collaboration or stateful sessions |

If later P1 adds community submissions/admin ingestion, create new clean D1/R2/KV resources. Do not reuse previous tenant resources.

## 3. Data schema contract

### 3.1 Public marker JSON schema

Primary data artifact for P0:

`public/data/markers/subnautica2maps-markers.vYYYYMMDD.json`

```ts
export type MarkerType =
  | 'resource'
  | 'poi'
  | 'blueprint'
  | 'data_box'
  | 'biome'
  | 'wildlife'
  | 'plant'
  | 'location';

export type SourceType =
  | 'manual_gameplay'
  | 'official_update_note'
  | 'licensed_community_submission'
  | 'public_observation';

export type Confidence = 'confirmed' | 'needs_verification' | 'deprecated';

export interface Subnautica2Marker {
  marker_id: string;                     // stable slug-like id: res_silver_001
  name: string;
  type: MarkerType;
  subtype: string;
  slug: string;
  coordinates: {
    x: number;
    y: number;
    z: number;
  };
  depth: number | string;
  region: string;
  biome: string;
  description: string;                   // human-written; not copied
  route_note_from_lifepod: string;
  route_note_from_current_position_template: string;
  prerequisite_gear: string[];
  nearby_resources: string[];
  nearby_poi: string[];
  verified_game_version: string;
  source_type: SourceType;
  source_url: string | null;
  confidence: Confidence;
  updated_at: string;                    // ISO date
  reviewer: string;
  notes: string;
  index_policy: 'index' | 'noindex';
  sitemap_eligible: boolean;
}

export interface MarkerManifest {
  project_slug: 'subnautica2maps';
  data_version: string;
  generated_at: string;
  verified_game_version: string;
  marker_count: number;
  confirmed_count: number;
  needs_verification_count: number;
  deprecated_count: number;
  source_policy: 'self_made_manual_or_licensed_only';
  files: Array<{
    path: string;
    sha256: string;
    marker_count: number;
  }>;
}
```

Launch rule:

```text
confirmed marker = source_type present + confidence=confirmed + updated_at present + verified_game_version present + human-written description + no copied data.

Only confirmed markers can support indexable resource/location/biome pages.
needs_verification/deprecated markers may appear as in-tool clues only if visibly labeled and excluded from sitemap-backed detail pages.
```

### 3.2 Optional future D1 schema

Do not create this in P0 unless admin ingestion/community submissions become an approved task. If needed later, create a new D1 database, e.g. `subnautica2maps-clean-db`, and apply this migration after binding verification.

```sql
CREATE TABLE IF NOT EXISTS marker_sources (
  id TEXT PRIMARY KEY,
  source_type TEXT NOT NULL CHECK (source_type IN ('manual_gameplay','official_update_note','licensed_community_submission','public_observation')),
  source_url TEXT,
  source_owner TEXT,
  license_note TEXT NOT NULL,
  collected_by TEXT NOT NULL,
  collected_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  notes TEXT
);

CREATE TABLE IF NOT EXISTS markers (
  marker_id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  type TEXT NOT NULL CHECK (type IN ('resource','poi','blueprint','data_box','biome','wildlife','plant','location')),
  subtype TEXT NOT NULL,
  slug TEXT NOT NULL UNIQUE,
  coordinates_x REAL NOT NULL,
  coordinates_y REAL NOT NULL,
  coordinates_z REAL NOT NULL,
  depth TEXT NOT NULL,
  region TEXT NOT NULL,
  biome TEXT NOT NULL,
  description TEXT NOT NULL,
  route_note_from_lifepod TEXT NOT NULL,
  route_note_from_current_position_template TEXT NOT NULL,
  prerequisite_gear_json TEXT NOT NULL DEFAULT '[]',
  nearby_resources_json TEXT NOT NULL DEFAULT '[]',
  nearby_poi_json TEXT NOT NULL DEFAULT '[]',
  verified_game_version TEXT NOT NULL,
  source_id TEXT NOT NULL,
  confidence TEXT NOT NULL CHECK (confidence IN ('confirmed','needs_verification','deprecated')),
  index_policy TEXT NOT NULL DEFAULT 'noindex' CHECK (index_policy IN ('index','noindex')),
  sitemap_eligible INTEGER NOT NULL DEFAULT 0,
  updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  reviewer TEXT NOT NULL,
  notes TEXT,
  FOREIGN KEY (source_id) REFERENCES marker_sources(id)
);

CREATE INDEX IF NOT EXISTS idx_markers_type ON markers(type);
CREATE INDEX IF NOT EXISTS idx_markers_subtype ON markers(subtype);
CREATE INDEX IF NOT EXISTS idx_markers_biome ON markers(biome);
CREATE INDEX IF NOT EXISTS idx_markers_confidence ON markers(confidence);
CREATE INDEX IF NOT EXISTS idx_markers_sitemap ON markers(sitemap_eligible, index_policy);

CREATE TABLE IF NOT EXISTS data_releases (
  id TEXT PRIMARY KEY,
  data_version TEXT NOT NULL UNIQUE,
  marker_count INTEGER NOT NULL,
  confirmed_count INTEGER NOT NULL,
  needs_verification_count INTEGER NOT NULL,
  deprecated_count INTEGER NOT NULL,
  manifest_path TEXT NOT NULL,
  manifest_sha256 TEXT NOT NULL,
  generated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  released_by TEXT NOT NULL,
  notes TEXT
);

CREATE TABLE IF NOT EXISTS event_ingestion_checks (
  id TEXT PRIMARY KEY,
  event_name TEXT NOT NULL,
  page_slug TEXT,
  source TEXT NOT NULL,
  observed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  evidence_path TEXT,
  notes TEXT
);
```

D1 binding rule before any future migration:

```text
Verify wrangler.toml/jsonc database_name/database_id == package.json migrate script target == `wrangler d1 list` result.
Never copy database_id from old tenant/project.
```

## 4. API schema

### 4.1 Runtime API routes

P0 should work without API. If frontend wants runtime endpoints, use these minimal, cache-safe routes.

#### GET `/api/health`

Purpose: deployment/API liveness check.

Response:

```json
{
  "ok": true,
  "project_slug": "subnautica2maps",
  "tenant": "site-rerun-subnautica2maps-20260520-clean",
  "version": "2026-05-20-clean",
  "runtime": "cloudflare-pages-functions"
}
```

#### GET `/api/data-version`

Purpose: expose current marker data version and manifest path.

Response:

```json
{
  "project_slug": "subnautica2maps",
  "data_version": "YYYYMMDD-N",
  "manifest_url": "/data/manifest.json",
  "marker_url": "/data/markers/subnautica2maps-markers.vYYYYMMDD.json",
  "marker_count": 0,
  "confirmed_count": 0,
  "last_updated": "YYYY-MM-DDTHH:mm:ssZ",
  "source_policy": "self_made_manual_or_licensed_only"
}
```

#### GET `/api/markers`

Optional only. Prefer static JSON for P0. If implemented, route must be read-only and cacheable.

Query parameters:

```text
type?: resource|poi|blueprint|data_box|biome|wildlife|plant|location
subtype?: string
biome?: string
confidence?: confirmed|needs_verification|deprecated
q?: string
```

Response:

```json
{
  "data_version": "YYYYMMDD-N",
  "count": 12,
  "markers": []
}
```

Rules:

- Never return hidden admin-only/unpublished data.
- Never fabricate confirmed values.
- Set `Cache-Control: public, max-age=300, s-maxage=3600`.

#### POST `/api/events`

Optional. If Plausible direct event tracking is enough, skip this route. If implemented, use it only as a privacy-bounded proxy.

Request:

```json
{
  "event": "map_search",
  "page_slug": "/map",
  "properties": {
    "keyword_bucket": "resource",
    "result_count": 4
  }
}
```

Allowed events:

- `pageview`
- `hero_cta_click`
- `tool_start`
- `tool_result`
- `pricing_cta_click` or support-interest equivalent if shown
- `map_search`
- `marker_open`
- `filter_apply`
- `detail_open`
- `position_set`
- `route_calculate`
- `progress_toggle`
- `outbound_click`

Privacy guardrails:

- Reject raw `coordinates`, `x`, `y`, `z`, `lat`, `lng`, `email`, `name`, `user_id` fields.
- For `route_calculate`, accept `marker_id`, `distance_bucket`, `success`; never raw current coordinates.
- Do not set user-tracking cookies.
- Rate-limit if endpoint is public.

Response:

```json
{ "ok": true }
```

Error codes:

| Status | Meaning |
|---:|---|
| 400 | invalid event or disallowed properties |
| 413 | payload too large |
| 429 | rate limited |
| 500 | proxy failure |

### 4.2 Route hints for frontend

Core route map:

| URL | Runtime | Data source | Index policy | Notes |
|---|---|---|---|---|
| `/` | static | manifest + featured marker snippets | index | Hero map preview, fan-made notice, CTA |
| `/map` | static/client | marker JSON | index | Full map/search/filter/detail/route/progress |
| `/resources/silver` | static generated | confirmed silver markers only | index if data proof | 300+ unique words + ItemList |
| `/resources/copper` | static generated | confirmed copper markers only | index if data proof | same |
| `/resources/sulfur` | static generated | confirmed sulfur markers only | index if data proof | same |
| `/guides/does-subnautica-2-have-a-map` | static | editorial | index | answer-first + CTA |
| `/guides/how-to-find-coordinates` | static | editorial | index | HowTo + route CTA |
| `/privacy` | static | legal copy | index | disclose Cloudflare, analytics, localStorage |
| `/terms` | static | legal copy | index | fan-made, no affiliation, no warranty |
| `/legal` or `/contact` | static | legal/contact | index | DMCA/copyright contact path |
| `/cookie-policy` | static | legal | index only if non-essential trackers | required if GA/Clarity/ads/remarketing |
| query/filter/local/route result URLs | client state only | n/a | noindex, not in sitemap | canonical to `/map` when needed |

Frontend implementation hints:

```ts
const FILTERS = [
  'Resources',
  'Blueprints',
  'Data Boxes',
  'Wildlife',
  'Plants',
  'Biomes',
  'Locations',
] as const;
```

Route helper should be browser-side:

```ts
export function calculateRouteHint(current, target) {
  const dx = target.x - current.x;
  const dy = target.y - current.y;
  const dz = target.z - current.z;
  const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
  return {
    distance,
    distance_bucket: distance < 250 ? '0-250' : distance < 500 ? '250-500' : distance < 1000 ? '500-1000' : '1000+',
    depth_delta: target.y - current.y,
    direction_hint: `${dx >= 0 ? 'east' : 'west'} / ${dz >= 0 ? 'north' : 'south'}`,
  };
}
```

Do not send `current` coordinates to analytics.

## 5. Auth / payment decision

### 5.1 Auth

P0 decision: `NO_AUTH`.

Reason:

- PRD explicitly says no login in P0.
- Local progress uses `localStorage`.
- Route calculation stays client-side.
- No cross-device sync in P0.

If P1 adds sync/notes/export:

- Use Google OAuth with CSRF state.
- Cookie: HttpOnly + Secure + SameSite=Lax.
- JWT: user_id/email/plan, 7-day expiry.
- Every API route: `export const runtime = 'edge'`.
- D1 access: `getRequestContext().env.DB`.
- Do not add auth until PM/compliance approve P1.

### 5.2 Payment

P0 decision: `NO_PAYMENT`.

Reason:

- PRD says free-first, no Stripe, no subscription wall.
- Optional support interest CTA can be measured, but no checkout.

Allowed P0 conversion event:

```text
pricing_cta_click = support/pricing interest CTA click, not a checkout attempt.
```

Stripe gate for future P1:

- 30 days ≥ 300 UV/day.
- `pricing_cta_click / map_engaged_session` ≥ 1%.
- ≥30 supporter waitlist emails or ≥10 explicit paid-intent signals.
- Compliance approves trademark/supporter/refund/subscription language.

## 6. Analytics and event plan

P0 event taxonomy is mandatory. Implementation must prove ingestion.

| Event | Trigger | Required properties | Privacy rule |
|---|---|---|---|
| `pageview` | page load | page_slug, referrer, device_type | no raw IP in artifact |
| `hero_cta_click` | hero CTA click | page_slug, cta_id, placement | no PII |
| `tool_start` | first map/search/route interaction | tool_type, source_page | no raw coordinates |
| `tool_result` | search/route output | tool_type, success, result_count | aggregate only |
| `pricing_cta_click` | support CTA if shown | page_slug, placement, cta_variant | no checkout in P0 |
| `map_search` | search submit | keyword or keyword_bucket, result_count | avoid PII; prefer buckets |
| `marker_open` | marker panel open | marker_id, marker_type, source_context | marker_id only |
| `filter_apply` | filter selected | filter_type, filter_value, result_count | no PII |
| `detail_open` | detail page/panel | detail_type, slug | no PII |
| `position_set` | coordinate accepted | coordinate_format, success | no raw coordinate |
| `route_calculate` | route output | marker_id, distance_bucket, success | no raw coordinate |
| `progress_toggle` | found/collected toggle | marker_id, state | no user identity |
| `outbound_click` | external/legal/support click | destination, context | disclose sponsor/affiliate if any |

Required QA evidence later:

- Screenshot/path or API export showing at least one successful ingestion per required event.
- Debug console logs alone are not enough unless paired with dashboard/API evidence.
- Failure state tested for route/search.

## 7. Cloudflare infrastructure plan

### 7.1 Resource names for clean rerun only

Do not create or bind any old IDs. If a future implementation needs resources, use fresh names:

```toml
# wrangler.toml/jsonc example only — do not apply until implementation task
name = "subnautica2maps-clean"
pages_build_output_dir = "dist"

[vars]
PROJECT_SLUG = "subnautica2maps"
TENANT = "site-rerun-subnautica2maps-20260520-clean"
NODE_ENV = "production"

# Only if approved later
# [[d1_databases]]
# binding = "DB"
# database_name = "subnautica2maps-clean-db"
# database_id = "<fresh-clean-rerun-id>"

# [[r2_buckets]]
# binding = "R2"
# bucket_name = "subnautica2maps-clean-assets"

# [[kv_namespaces]]
# binding = "KV"
# id = "<fresh-clean-rerun-id>"
```

### 7.2 CF safety status

Current task did not modify CF settings because formal domain/zone was not provided as a clean-rerun input and destructive/production configuration changes require plan-before-execute.

Required verification once domain is confirmed:

| Setting | Required value | Status now | Evidence required later |
|---|---|---|---|
| Zone active | active | not_verified_domain_missing | CF zones API result |
| DNS root | CNAME/Pages custom domain target, proxied | not_verified_domain_missing | DNS records API + dig |
| DNS www | CNAME to root or Pages target, proxied | not_verified_domain_missing | DNS records API + dig |
| SSL mode | Full (Strict) | planned_not_verified_domain_missing | `/settings/ssl` value=`strict` |
| Always Use HTTPS | on | planned_not_verified_domain_missing | `/settings/always_use_https` value=`on` |
| Bot Fight/equivalent | on or documented equivalent | planned_not_verified_domain_missing | `/settings/bot_fight_mode` or WAF setting |
| Crawler Hints | on | planned_not_verified_domain_missing | `/settings/crawler_hints` value=`on` |
| Browser Cache TTL | configured, e.g. 14400 | planned_not_verified_domain_missing | `/settings/browser_cache_ttl` |
| `/api` rate limit | 100 req/min/IP if API routes exist | planned_not_verified_domain_missing | ruleset evidence |
| Static asset cache | `_astro/*`, `_next/static/*`, assets/data immutable cache | planned_not_verified_domain_missing | ruleset/page rule/header evidence |
| Security headers | nosniff/referrer/CSP/permissions | planned | live curl headers |

### 7.3 Plan-before-execute checklist for later CF changes

Before any CF write call:

1. Confirm final domain and zone id.
2. Read current zone status, DNS records, Pages custom domains, SSL, Always HTTPS, Bot Fight, Crawler Hints, Browser Cache TTL, rulesets.
3. Write a step-by-step plan with expected API responses.
4. Flag risk: DNS/SSL/ruleset changes can break production traffic.
5. Apply one API call at a time.
6. Verify after each call.
7. Record evidence paths in Kanban metadata.

Example read-only discovery commands for implementation worker:

```bash
source /etc/environment
DOMAIN="<confirmed-domain>"
ZONE_ID=$(curl -s "https://api.cloudflare.com/client/v4/zones?name=${DOMAIN}" \
  -H "Authorization: Bearer $CF_TOKEN" | jq -r '.result[0].id // empty')

curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/ssl" -H "Authorization: Bearer $CF_TOKEN" | jq '.result.value'
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/always_use_https" -H "Authorization: Bearer $CF_TOKEN" | jq '.result.value'
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/crawler_hints" -H "Authorization: Bearer $CF_TOKEN" | jq '.result.value'
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/browser_cache_ttl" -H "Authorization: Bearer $CF_TOKEN" | jq '.result.value'
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" -H "Authorization: Bearer $CF_TOKEN" | jq '.result[] | {type,name,content,proxied}'
```

## 8. Domain and email plan

Formal domain was not present in parent metadata. Do not assume the final domain from old runs.

Required before public launch:

| Address | Required | Purpose | Status |
|---|---:|---|---|
| `hello@domain` | yes | general contact / trust | required_not_verified_domain_missing |
| `support@domain` | yes | user support and correction reports | required_not_verified_domain_missing |
| `dmca@domain` or `copyright@domain` | recommended / effectively required for this game/IP site | DMCA/copyright takedown path | required_not_verified_domain_missing |
| `privacy@domain` | recommended | privacy requests | required_not_verified_domain_missing |

Recommended Cloudflare Email Routing aliases:

```text
hello@<confirmed-domain>   -> xjtumj@gmail.com or ops inbox
support@<confirmed-domain> -> xjtumj@gmail.com or support inbox
dmca@<confirmed-domain>    -> xjtumj@gmail.com or legal inbox
privacy@<confirmed-domain> -> xjtumj@gmail.com or legal inbox
```

Legal/contact pages must display at least `hello@domain` and `support@domain`; DMCA/copyright path should be explicit for a fan-made game map site.

## 9. Headers / cache / robots / sitemap handoff

### 9.1 Security headers

Recommended `_headers` for Cloudflare Pages:

```text
/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()
  Content-Security-Policy: default-src 'self'; img-src 'self' data: https:; script-src 'self' https://plausible.io; connect-src 'self' https://plausible.io; style-src 'self' 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; frame-src https://www.youtube.com https://www.youtube-nocookie.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests

/data/*
  Cache-Control: public, max-age=300, s-maxage=3600

/assets/*
  Cache-Control: public, max-age=31536000, immutable

/_astro/*
  Cache-Control: public, max-age=31536000, immutable

/_next/static/*
  Cache-Control: public, max-age=31536000, immutable
```

If frontend uses inline scripts or external Google Fonts, CSP must be adjusted deliberately. Avoid adding GA/Clarity/ads before compliance recheck.

### 9.2 robots policy

```text
User-agent: *
Allow: /
Disallow: /search
Disallow: /*?*

Sitemap: https://<confirmed-domain>/sitemap.xml
```

Do not block CSS/JS/assets/data JSON needed for rendering. Query URLs should be canonical/noindex by page logic and excluded from sitemap.

### 9.3 Sitemap allowlist

Only include pages that pass data/content proof:

- `/`
- `/map`
- `/resources/silver` if confirmed markers + 300+ unique support words exist
- `/resources/copper` if confirmed markers + 300+ unique support words exist
- `/resources/sulfur` if confirmed markers + 300+ unique support words exist
- `/guides/does-subnautica-2-have-a-map`
- `/guides/how-to-find-coordinates`
- `/privacy`
- `/terms`
- `/legal` or `/contact`
- `/cookie-policy` only if needed and complete

Exclude:

- query URLs
- local progress state
- route result URLs
- unverified marker detail pages
- placeholder/coming soon pages
- tag/filter/category pages without unique content

## 10. Build/test/deploy requirements for implementation worker

This task is a planning/backend contract artifact; it did not modify source code or deploy.

Future code/deploy task must provide:

```yaml
build: npm run build or equivalent
unit_or_schema_tests: marker schema validation + route helper tests + SEO audit
commit: conventional commit
push: origin branch
cloudflare_deploy: same commit only
metadata:
  commit_sha: <git rev-parse HEAD>
  pushed_branch: <branch>
  deploy_url: <production/staging URL>
  deployment_source_commit: <commit shown by CF Pages>
  git_status_after: clean
```

Do not deploy uncommitted/unpushed source. Do not deploy from a commit different from the source commit in metadata.

## 11. Acceptance checklist

| Check | Status | Evidence |
|---|---|---|
| Kanban source of truth used | PASS | `kanban_show(t_03caa401)` |
| START sent to Telegram | PASS | message sent to `telegram:-1003750190535:5318` |
| Clean-rerun parent PRD read | PASS | `03-prd-v1.md` path in §1 |
| Design acceptance read | PASS | `04b-design-acceptance.md` path in §1 |
| Design handoff/screen index read | PASS | HANDOFF/screen-index/verification paths in §1 |
| No old PRD/design/seed/dist/repo read | PASS | Work used only clean parent artifact paths |
| site_type carried through | PASS | §2, §4 route/API plan |
| SERP/competitive interaction baseline carried through | PASS | API/route hints preserve map/search/filter/detail/route/progress |
| core_user_tasks carried through | PASS | §4.2, §6 |
| competitive_minimum carried through | PASS | §4.2, §10 |
| asset/content inventory carried through | PASS | §3 marker manifest/source policy |
| index/noindex+sitemap policy carried through | PASS | §4.2, §9 |
| analytics/event plan carried through | PASS | §6 |
| legal/policy boundary carried through | PASS | §5, §8, §9 |
| GTM/review mouth carried through | PASS | Launch blockers and evidence requirements in §§0,10 |
| Auth/payment decision | PASS | P0 `NO_AUTH`, `NO_PAYMENT` in §5 |
| D1/R2/KV/project ID reuse avoided | PASS | No resources created; future names require fresh IDs |
| CF 7-step plan included | PASS | §7 |
| crawler_hints_status explicit | PASS | `planned_not_verified_domain_missing` |
| cf_security_settings_status explicit | PASS | `planned_not_verified_domain_missing` |
| domain_email_status explicit | PASS | `required_not_verified_domain_missing` |
| Output written under required path | PASS | this file |

## 12. skill_contract_check

| 合同项 | 必需输入 | 必需产出 | 硬约束 | 验收项 | Evidence |
|---|---|---|---|---|---|
| Kanban source of truth | task_id `t_03caa401`, parent metadata | `05b-backend.md` + Kanban metadata | Telegram only visibility; Kanban complete/block authoritative | START sent, DONE before complete | `kanban_show`; Telegram START sent |
| Clean rerun | clean tenant, PRD v1, design acceptance, design handoff | New backend/data/API/CF/email plan under clean output path | No old PRD/design/seed/dist/repo; no old D1/R2/KV/project id reuse | Inputs point only to clean workspaces | §1 paths |
| Bound skill: backend-site-build | PRD + design + task body | Data schema, API schema, auth/payment decision, CF/domain plan | Missing critical inputs must be surfaced; no destructive CF writes without plan-before-execute | This report includes all required backend phases scoped to P0 | §§2-10 |
| Data schema | PRD marker schema + asset inventory | Public marker JSON schema + optional future D1 migration | Confirmed/indexable data requires provenance/version/confidence | Schema includes all required marker fields | §3 |
| API schema | PRD core tasks + analytics taxonomy | `/api/health`, `/api/data-version`, optional `/api/markers`, `/api/events` | P0 core must work static/client-side; reject raw coordinate analytics | Routes have request/response/errors/privacy rules | §4, §6 |
| Route hints | PRD SEO matrix + design states | Route/runtime/index policy table | Query/local/unverified/thin pages noindex/out of sitemap | P0 route table complete | §4.2 |
| Auth/payment | PRD pricing/free-first | `NO_AUTH`, `NO_PAYMENT` P0 decision | No login/Stripe/subscription wall in P0 | Future gates defined | §5 |
| CF security 7 steps | Task CF P0 requirement | Verification plan/status table | No CF writes without confirmed domain/current-state lookup | SSL/HTTPS/Bot/Crawler/cache/rate/static/security covered | §7 |
| Domain email | PRD legal/domain policy | Required aliases and routing plan | hello/support required before launch | explicit `domain_email_status` | §8 |
| Analytics | PRD event taxonomy | Required events + privacy guardrails + evidence requirements | Real ingestion proof required; no raw current coordinates | P0 event table complete | §6 |
| SEO/index policy | PRD page matrix | robots/sitemap/canonical/noindex handoff | No thin/query/local/unverified pages in sitemap | allowlist/exclusion list present | §9 |
| Verification | output artifact | file exists + checklist + metadata | Must verify file before complete | wc/search verification after write | §11 + tool verification |

## 13. Handoff summary

For 墨界/frontend implementation:

- Use PRD v1 as product contract and design HTML as visual truth.
- Implement static/client-first map/search/filter/detail/route/progress.
- Include full filter taxonomy: Resources, Blueprints, Data Boxes, Wildlife, Plants, Biomes, Locations.
- Replace all mock marker values with provenance-backed clean-rerun marker JSON before claiming confirmed data.
- Add analytics hooks and collect ingestion proof for every P0 event.
- Keep route coordinates local; analytics uses distance_bucket only.
- Do not add login, Stripe, subscriptions, UGC, copied assets/data, or indexable placeholder pages.

For data/seed task:

- Produce marker JSON + manifest matching §3.
- Each indexable marker-backed page must have confirmed markers with source_type, confidence, verified_game_version, updated_at, reviewer, notes.
- Unverified markers stay labeled and out of sitemap-backed detail pages.

For compliance/SEO/QA:

- Block launch if formal domain emails are missing.
- Block launch if CF settings evidence is missing.
- Block launch if sitemap includes query/local/unverified/thin pages.
- Block launch if analytics ingestion evidence is missing.
- Block launch if source/deploy commit mismatch exists.
