160 lines
8.2 KiB
Markdown
160 lines
8.2 KiB
Markdown
# Region / City / Product URL Structure — Design
|
|
|
|
Date: 2026-10-29
|
|
Status: draft, awaiting approval
|
|
|
|
## Intent
|
|
|
|
The client wants the site organised by place, like hoponhopoffplus.com:
|
|
visitors pick a **region**, then a **city**, then a **product**. URLs should
|
|
read the same way:
|
|
|
|
| Page | URL | Example |
|
|
|---------|---------------------------------------|----------------------------------------------------------------|
|
|
| Region | `/{region-slug}/` | `/europe/` |
|
|
| City | `/{city-slug}/` | `/amsterdam/` |
|
|
| Product | `/{city-slug}/{product-slug}/` | `/amsterdam/city-sightseeing-hop-on-hop-off-amsterdam-bus-tour-24-hours/` |
|
|
|
|
The header menu lists the regions: **UK, Europe, Asia, Middle East and
|
|
Africa, Americas, Australasia**.
|
|
|
|
## Goals
|
|
|
|
- The client builds the region → city structure in the plugin settings.
|
|
- Products keep importing automatically from Viator. A city page shows
|
|
**every** Viator product in that city, not only hop-on hop-off tours.
|
|
- The settings screen makes bad data impossible to save (see Validation).
|
|
- Existing live URLs keep working through 301 redirects. The client sent
|
|
these, which are live today and likely the first Americas cities:
|
|
washington-dc, boston, chicago, new-york-city, new-orleans, miami,
|
|
las-vegas, los-angeles, san-diego, san-francisco, seattle, victoria,
|
|
vancouver, montreal, toronto (all under `/tour-destination/{slug}/`).
|
|
|
|
## Non-goals
|
|
|
|
- The new static pages from `new front/` (about, contact, faq, blog,
|
|
blog-post). That is a separate, later project.
|
|
- A hand-built sample product page. Products come from the Viator plugin.
|
|
- Redesigning the listing, card or single-tour templates.
|
|
- Region in the city URL (`/europe/amsterdam/`). The client's examples
|
|
rule this out.
|
|
|
|
## Anti-goals (would count as failure even if it "works")
|
|
|
|
- A city or region URL that hijacks an existing WordPress page, post or
|
|
system path (e.g. a city slugged `about` or `wp-admin`).
|
|
- The same product reachable at two indexable URLs (duplicate content).
|
|
- Any of the client's existing `/tour-destination/...` links returning 404.
|
|
- Admin able to type a city that does not exist in Viator.
|
|
|
|
## Decisions (agreed in conversation)
|
|
|
|
- Structure lives in plugin settings; client owns it.
|
|
- Cities are picked only from Viator's destination list (reuse the existing
|
|
`vc_search_destinations` AJAX lookup in `class-vc-settings.php`).
|
|
- A product that belongs to several configured cities **appears on every
|
|
one of those city pages**.
|
|
- Old `/tour-destination/{slug}/` URLs 301 to `/{slug}/`.
|
|
|
|
## Decisions made by Claude (user delegated URL choices)
|
|
|
|
1. **One canonical product URL.** City in the URL = the product's Viator
|
|
*primary* destination if that destination is a configured city;
|
|
otherwise the first configured city (in settings order) the product
|
|
belongs to. Requesting the product under any other city
|
|
(`/paris/same-tour/`) 301s to the canonical URL. `/tours/{slug}/`
|
|
also 301s to the canonical URL.
|
|
2. **Products in no configured city** (left over from older crawls) keep
|
|
`/tours/{slug}/` and do not appear on any region/city page.
|
|
3. **Data model: reuse the `viator_destination` taxonomy (hierarchical).**
|
|
- Region = top-level term with no Viator ID, marked by term meta
|
|
`_vc_region = 1`.
|
|
- City = term linked to a Viator destination ID (existing
|
|
`_vc_viator_destination_id` meta), with its parent set to its region.
|
|
- Existing city terms are reused by Viator ID, so current slugs such as
|
|
`washington-dc` are kept as-is.
|
|
- A region page queries its child terms with `include_children`, so it
|
|
lists every product in its cities without extra assignment.
|
|
- The structure itself is stored as one option (`vc_structure`): an
|
|
ordered list of regions, each with an ordered list of
|
|
`{viator_id, name, slug}` cities. Saving the settings syncs it into
|
|
terms (create/re-parent/rename), never deleting products.
|
|
4. **The six regions come preset** (slugs: `uk`, `europe`, `asia`,
|
|
`middle-east-and-africa`, `americas`, `australasia`). The client can
|
|
rename, reorder, add or remove regions.
|
|
5. **City slug** defaults to `sanitize_title( Viator name )`, or the
|
|
existing term's slug if one exists. The admin may edit it; edits go
|
|
through the same validation.
|
|
6. **Routing: explicit rewrite rules per configured slug, not catch-alls.**
|
|
For each region and city, add rules for `^{slug}/?$`; for each city also
|
|
`^{slug}/([^/]+)/?$` → product. Rules are rebuilt and flushed when the
|
|
structure is saved. Unconfigured paths fall through to normal WordPress
|
|
routing, so pages like `/contact/` are unaffected. Listing pagination
|
|
already uses `?paged=`, so no `/page/N/` rule is needed (the builder
|
|
should confirm this).
|
|
7. **Links.** `term_link` filter returns `/{slug}/` for region and city
|
|
terms; `post_type_link` filter returns the canonical product URL.
|
|
Unconfigured destination terms keep `/tour-destination/{slug}/`.
|
|
8. **Crawl.** When the structure has at least one city, the crawl queue is
|
|
exactly the configured cities' Viator IDs. The old
|
|
`crawl_destinations` allowlist is used only when the structure is empty,
|
|
and its field is labelled as such.
|
|
9. **Primary destination.** The importer stores the Viator primary
|
|
destination ref (the entry in `destinations` with `primary: true`) in
|
|
post meta `_vc_primary_destination`. Products imported before this change
|
|
get it on the next sync; until then the fallback rule in (1) applies.
|
|
10. **Menu.** Regions are added automatically to the "HoHo — Header Menu"
|
|
location as top-level items, each with a dropdown of its cities (the
|
|
existing nav walker already renders dropdowns). Items the client added
|
|
by hand in the menu stay, and the regions are appended after them.
|
|
11. **Region landing page.** Hero (region name and term image), a grid of
|
|
its city cards (reuse `card-destination.php`), then the standard
|
|
listing of all tours in the region. **City landing page** = the
|
|
existing taxonomy listing template, unchanged apart from the URLs.
|
|
12. **SEO.** Canonical tags follow the canonical URLs above. All redirects
|
|
are 301.
|
|
|
|
## Validation (settings save)
|
|
|
|
The whole save is rejected, with the previous structure kept, and the admin
|
|
sees a list of every problem, if any of these is true:
|
|
|
|
- A city's Viator ID is not in Viator's destination list.
|
|
- The same city (Viator ID) appears twice, in one region or across regions.
|
|
- Two items (regions or cities) share a slug.
|
|
- A region has an empty name.
|
|
- A slug is empty or contains anything other than `a-z`, `0-9` and `-`.
|
|
- A slug collides with an existing page, post, or other public post-type
|
|
slug or archive (`tours`), or with a reserved word: WordPress reserved
|
|
query vars, `wp-admin`, `wp-content`, `wp-includes`, `feed`, `page`,
|
|
`search`, `tag`, `category`, `author`, `tour-destination`, `tour-tag`,
|
|
`tour-category`, `blog`, `about`, `contact`, `faq`.
|
|
|
|
The editor UI also prevents most of these before submit: cities are added
|
|
only from search results, and already-used cities are greyed out in the
|
|
results.
|
|
|
|
Also, when a Page is created or renamed to a slug a region or city already
|
|
uses, show an admin notice warning about the clash. (WordPress itself does
|
|
not block this.)
|
|
|
|
## Constraints
|
|
|
|
- WordPress, Astra child theme `astra-child-hoponhopoff`, plugin
|
|
`viator-collector`. Match the existing plugin style (static classes,
|
|
`VC_` prefix, `vc_get_option`, `VC_Logger`).
|
|
- Viator Basic access: the catalogue is only enumerable per destination,
|
|
which suits per-city crawling.
|
|
- No changes to how products are fetched or priced.
|
|
|
|
## Left to the builder
|
|
|
|
- Settings editor UI details (drag-to-reorder or up/down buttons, etc.)
|
|
as long as the validation and pick-from-Viator rules hold.
|
|
- Whether the structure editor is a section of the existing settings page
|
|
or its own sub-page.
|
|
- Exact redirect hook (`template_redirect` is the expected place).
|
|
- What happens to a city removed from the structure: its term stays (it is
|
|
un-parented and loses its `/{slug}/` route, so it falls back to
|
|
`/tour-destination/{slug}/`). Products are not deleted.
|