← All playbooks · Raw API
task-location-jobposting-schema
# Capability: Location JobPosting schema (shared template + location meta)
## Intent phrases
- JobPosting schema
- job posting schema location
- Google for Jobs drive page
- CareDriver schema
- optimize location schema
- hiring cycle schema
- datePosted location
- roll out JobPosting to cities
- update JobPosting identifier
- location meta
- next drive location schema
- refresh JobPosting dates
- Q4 schema
- new hiring wave
- update all location identifiers
## Requires capabilities
`pages` (location entries are CMS content; not a normal marketing `page`)
## Prerequisites
- Read `cms-edit://customer/location-pages`
- Target is a **`location`** entry under `/drive/{slug}/`, not the `/drive` hub
- County URLs (`/drive/frederick-county/` etc.) redirect to `/drive/maryland/` — not location entries
- No DC location. DMV is `maryland` + `northern-va`
- **Schema-only.** Do not change visible page body
- **Do not clone a JobPosting.** One shared schema + per-location `meta`
- **Never publish**
## Which job is this?
| User said | Do this section |
|-----------|-----------------|
| New hiring wave / Q4 / refresh dates / update all identifiers | **Periodic wave update** (first) |
| One city, or “Houston like Denver” | **Add one market** |
| Roll out / these cities / the rest | **Pre-screen**, then add each **ready** market |
Do not start writing until the user has confirmed the wave dates (periodic) or the city list (roll-out).
## Reference IDs
| Item | ID |
|------|----|
| Shared default (short) JobPosting | `5YCdby8QnTkiweZuw2y43c` |
| Shared **optimized** JobPosting | `7g1YmIvX07P75e5xXGFVPA` |
| Location page template | `6giibBl8cPHV6rRbTSis2F` |
Control city without optimized schema: any **active** location that does **not** link `7g1YmIvX07P75e5xXGFVPA`. Do not hardcode Nashville. If every active location is optimized, say so — there is no control city.
**Live inventory (do not maintain a static city table):**
```
cms_edit ["list", "--type", "location", "-n", "80"]
```
Optimized = `structuredData` contains `7g1YmIvX07P75e5xXGFVPA`. Identifiers live on each location’s `meta.identifier` — that is the collision check. Re-list; do not trust an old playbook table.
## How it works
1. Template `meta` holds the **wave**: `datePosted`, `validThrough`, `hiringWave`, and default `vehicleMaxAgeYears` (15).
2. Shared optimized markup is already Mustache (`{{meta.identifier}}`, `{{meta.serviceAreas}}`, `{{meta.datePosted}}`, `{{meta.validThrough}}`, `{{meta.vehicleMaxAgeYears}}`, optional `backgroundCheckExtra`). **Do not edit it.**
3. Location `meta` merges **over** template `meta`. Invalid JSON is ignored (the whole bag vanishes).
4. Location `structuredData` = **only** the shared optimized id. That JobPosting replaces the template one.
`set … meta` **replaces the entire JSON**. Peek first, change only the keys you mean, write the **full** object back. Never identifier-only.
## Identifier
Pattern: `HSD-{METRO}-{hiringWave}`
`hiringWave` from **template** `meta.hiringWave` (currently `2026-Q3`).
Build `{METRO}` from location **`city`**, not slug:
1. Start with `city`.
2. Drop `.` (`St. Louis` → `St Louis`).
3. Uppercase.
4. Spaces and remaining punctuation → `-`.
5. Collapse repeated `-`.
Do **not** strip a trailing state token if two locations share the same base city — keep it so identifiers stay unique.
| `city` | `state` | `{METRO}` |
|--------|---------|-----------|
| Los Angeles | California | `LOS-ANGELES` |
| Minneapolis-Saint Paul | Minnesota | `MINNEAPOLIS-SAINT-PAUL` |
| St. Louis | Missouri | `ST-LOUIS` |
| Kansas City MO | Missouri | `KANSAS-CITY-MO` |
| Kansas City | Kansas | `KANSAS-CITY` |
If two built identifiers would collide, stop and ask.
## Vehicle age
Footnote: DC, MD, PA, Minneapolis, Seattle → **10**. Else omit (inherit 15).
| 10 | 15 |
|----|-----|
| PA entries (Philadelphia, Pittsburgh) | Spokane (WA ≠ Seattle) |
| `maryland` entry only | Northern Virginia |
| Seattle | County redirect slugs |
| Minneapolis | |
## `serviceAreas`
`servingCopy` is rich text — use **plaintext** only.
Source, first hit:
1. `servingCopy` plaintext (strip `We serve:`)
2. `areas[]` (Maryland)
3. `city` as one metro label — **only** if the user approved city-fallback (Minneapolis; inactive stubs). Do not invent neighborhoods.
Clean mechanically (do not eyeball):
1. Split on `|`
2. Trim; strip ` ` / `\u00a0`
3. Drop empties
4. Drop 2–4 letter all-caps junk (`PRS`)
5. Dedupe case-insensitive, keep first (Phoenix Peoria, Milwaukee South Milwaukee)
6. Join `A, B, C, and D`
Do not merge FAQ extras. Keep odd-but-real tokens (`Nevada` in Dallas, `Benicia Vallejo`). Do not truncate.
## Special-case keys (do not invent new ones)
| Key | When |
|-----|------|
| `vehicleMaxAgeYears`: `10` | Footnote table only |
| `backgroundCheckExtra` | Denver only today |
| `datePosted` / `validThrough` / `hiringWave` | **Template only** unless one market is on a different wave |
## Periodic wave update (every 60–90 days)
This is the recurring job. It does **not** re-parse service areas or re-link schemas.
1. User confirms `datePosted`, `validThrough`, and new suffix (e.g. `2026-Q4`) **before any write**.
2. Live inventory: every location whose `structuredData` includes `7g1YmIvX07P75e5xXGFVPA`. Show slug + current `identifier`. Get approval.
3. Peek template `meta`. Change only `datePosted`, `validThrough`, `hiringWave`. Write the **full** template object. `diff` → `save`.
4. For each approved location: peek `meta`. Replace only the wave suffix on `identifier` (`-2026-Q3` → `-2026-Q4`). Keep every other key. Write the **full** object.
5. **N ≥ 4:** after peeks, use `batch set` for those `id:meta=…` values, then `batch diff` / `batch save`. Still never identifier-only JSON.
6. Never change dates on the template while leaving location identifiers on the old suffix.
7. Human publishes **template + all those locations together**.
Do not copy wave dates onto locations — that blocks the next cycle.
## Pre-screen (multi-city / “the rest”)
Before the first `set`, one `list --type location` pass. Classify every named city:
| Bucket | Rule | Action |
|--------|------|--------|
| **Ready** | `active` and (`servingCopy` or `areas[]`) | Eligible |
| **Broken** | Optimized schema `7g1YmIvX07P75e5xXGFVPA` linked and `meta` missing/invalid | Fill full `meta`. Do **not** unlink the schema |
| **Fallback** | Active but no list (Oklahoma City) or user already approved city-fallback | Ask once, then treat as ready |
| **Stub** | `active` is false and no list | Do **not** write unless the user explicitly says city-fallback is OK |
| **Skip** | No slug, untitled, or county redirect | Skip |
Show the four lists plus **Broken**. Wait. Then write **Ready** (plus approved Fallback) and always repair **Broken**.
If the user says the list is already clean, still skip county slugs, but you may skip repeating stub narration.
## Add one market (or each Ready city)
1. Confirm id / slug / city / state.
2. Peek template `meta` for `hiringWave`.
3. Peek shared schema only if you have not this session — if tokens are present, do not edit it.
4. Peek existing location `meta`.
5. Build identifier + `serviceAreas` with the rules above.
6. Unless the user said “known-clean, skip narration”: show identifier, vehicle (10 or inherit 15), and `serviceAreas`. Then `save`.
7. Always `diff` before `save` (even if you do not narrate it).
8. Write **full** `meta` + `structuredData` → `7g1YmIvX07P75e5xXGFVPA` only.
**N ≥ 4 Ready cities** (after pre-screen + confirmation): `batch set` full `meta` JSON per id, then set `structuredData` on each (batch `set` does not do `--links`). `batch diff` → `batch save`.
```
cms_edit ["open", "--id", "<locationId>"]
cms_edit ["read", "@root", "meta"]
cms_edit ["set", "@root", "meta", "{ ...full json... }"]
cms_edit ["set", "@root", "structuredData", "7g1YmIvX07P75e5xXGFVPA", "--links"]
cms_edit ["diff"]
cms_edit ["save"]
```
Location/schema `open` may show `emptyContent`. `set @root` still works.
## Shared schema
Already tokenized. Peek once per session. If you still see another city’s hardcoded areas in markup: **stop**, backfill **every** linked location `meta` first, tokenize only documented keys, get explicit approval. Do not clone.
## Verify
**Hosted (default):** peek the draft — one shared schema id, `meta` matches what you saved. www / Vercel use **published** content; a new draft still shows the short JobPosting. Do not “fix” that by cloning.
**Local optional:** `http://localhost:3023/drive/{slug}/` with `DRAFT_ONLY=true`. Refetch once if the first HTML is still short.
**After human publish:** check JSON-LD on the live host.
## Confirmation gates
1. Periodic: user confirmed wave dates + suffix before template write
2. Roll-out: user approved the pre-screen buckets
3. One-city: user named the metro
4. `diff` before every `save`
5. Full-object `meta` merge
6. Publish is human-owned
## Out of scope
- Publish
- Page body / pay / Apply
- Editing the short default JobPosting
- `baseSalary` without pay on the page and product sign-off
- Inventing legal extras or neighborhoods
- Creating county or DC locations
- Production git branches
- Creating a market (`task-create-location`)
## Related
- `cms-edit://customer/location-pages`
- `cms-edit://customer/task-location-pages`
- `cms-edit://customer/task-create-location`
- `cms-edit://customer/task-publish-handoff`