Let's Camp SEO strategy
TL;DR
HipCamp ranks everywhere for camping-related searches in the US. Their site has somewhere between 200,000 and 500,000 pages indexed and most of them rank.
The trick is permutations. They take three lists (places, types of camping, filters) and build a page for each combination that makes sense. For example:
/d/united-states/california/san-diego/camping/glamping/d/united-states/north-carolina/asheville/camping/cabins-and-pets/state-park/united-states/pennsylvania/chapman-park/all
We can mimic this with an emphasis on Canada, where we have the strongest data. We should build pages in this order:
-
Operator-side comparison pages. ~12 pages aimed at campground owners searching for software, not at campers searching for sites. (Not programmatic SEO but good groundwork.)
/software/campspot-alternative— "The best Campspot alternative for Canadian campgrounds"/software/firefly-reservations-alternative/software/free-campground-reservation-software
-
Named-park pages. E.g., Algonquin, Banff, Killarney, Jasper. One page per famous park, even if they're not a customer. The search happens for the park itself.
/camping/ontario/parks/algonquin-provincial-park/camping/alberta/parks/banff-national-park
-
Geo + filter pages. Province + city + type of camping. Start with Saskatchewan and Alberta only, because that's where we have enough campgrounds to fill out a page.
/camping/saskatchewan/saskatoon/rv-sites/camping/alberta/canmore/cabin-rentals
-
Long-tail questions. The kind of thing someone types into Google at 11pm before a trip.
/question/is-saskatchewan-good-for-camping/question/how-much-does-it-cost-to-camp-in-banff/question/can-you-camp-anywhere-in-algonquin
Our moat is proprietary data — booking facts that HipCamp and Campspot literally can't see, because we have the bookings and they don't (see Appendix A).
- Lead time: "July weekends at Algonquin sell out 47 days before check-in"
- Live demand: "16 campers are waitlisted for sites near you right now"
- Real price ranges: "RV sites in SK run $32-$48 in peak, $24-$36 in shoulder"
- Recent-booking ticker: "Just booked: 3 nights at Big Bend Campground, $135 total, 2 hours ago"
- Live events: "Live music Saturday at Big Bend Campground"
- Historical weather: "Driest July destinations in BC", "Warmest May camping in Ontario"
How this document is organized
- What HipCamp is doing and at what scale
- What changes when you swap their ICP for Let's Camp's
- Infrastructure
- The concrete keyword and URL plan
- Page templates
- Phased rollout plan
- Content generation pipeline
1. What HipCamp is actually doing
There are five page archetypes stacked into one big permutation engine:
1a. The geographic backbone
| Template | URL pattern | Scale |
|---|---|---|
| Country root | /d/<country>/camping/all | 4-5 |
| State / province | /d/<country>/<state>/camping/all | 104 (US+CA+AU+GB+FR) |
| County | /d/<country>/<state>/<county>-county/camping/all | 290 |
| Region / area | /d/<country>/<state>/<region>/camping/all | ~200 named geos (Yosemite, Big Sur, Lake Berryessa) |
| City | /d/<country>/<state>/<city>/camping/all | Tens of thousands (TX alone has 1,044) |
These "shell" pages each show a hero, intro paragraph mentioning the place by name, listing cards, FAQ block, and internal-link rails ("Top cities", "Top regions", "Top national parks", "Nearby states").
1b. The filter dimension (48 single + 15 multi)
Stacked on top of every geo, they layer 48 single-filter pages and 15 selected multi-filter combinations. Grouped:
Show all 48 single filters with frequencies
| Count | Slug | Category |
|---|---|---|
| 24,782 | rv | Accommodation (big-3) |
| 23,460 | glamping | Accommodation (big-3) |
| 20,938 | cabins | Accommodation (big-3) |
| 399 | pets | Amenity |
| 287 | farm-stays | Vibe |
| 286 | beach | Natural feature |
| 283 | glamping-pod | Accommodation |
| 270 | shower | Amenity |
| 264 | toilet | Amenity |
| 250 | wifi | Amenity |
| 246 | forest | Natural feature |
| 244 | campfires-allowed | Amenity |
| 243 | hiking | Activity |
| 240 | fishing | Activity |
| 237 | river | Natural feature |
| 234 | waterside | Natural feature |
| 231 | wildlife-watching | Activity |
| 214 | swimming | Activity |
| 209 | campervan | Accommodation |
| 209 | hot-tub | Amenity |
| 201 | horseback-riding | Activity |
| 191 | wheelchair-access | Amenity |
| 186 | lake | Natural feature |
| 169 | yurt | Accommodation |
| 164 | climbing | Activity |
| 163 | bell-tent | Accommodation |
| 156 | mountainous | Natural feature |
| 140 | shepherds-hut | Accommodation |
| 135 | great-views | Vibe |
| 123 | luxury | Vibe |
| 108 | surfing | Activity |
| 89 | tiny-home | Accommodation |
| 84 | waterfall | Natural feature |
| 80 | safari-tent | Accommodation |
| 79 | private | Vibe |
| 74 | family | Vibe |
| 68 | ranch | Vibe |
| 67 | vintage-trailer | Accommodation |
| 56 | big-rig-friendly | Amenity |
| 55 | dome | Accommodation |
| 53 | snow-sports | Activity |
| 45 | treehouse | Accommodation |
| 32 | cave | Natural feature |
| 22 | hot-spring | Natural feature |
| 22 | desert | Natural feature |
| 21 | airstream | Accommodation |
| 16 | a-frame | Accommodation |
| 5 | redwoods | Natural feature |
Show all 15 multi-filter combos
| Count | Combo | Notes |
|---|---|---|
| 271 | glamping-and-hot-tub | |
| 270 | glamping-and-pets | |
| 235 | glamping-and-wifi | |
| 225 | cabins-and-pets | |
| 221 | pets-and-rv | |
| 220 | forest-and-glamping | |
| 187 | glamping-and-river | |
| 172 | luxury-and-rv | |
| 145 | cabins-and-fishing | |
| 138 | river-and-rv | |
| 134 | cabins-and-river | |
| 131 | cabins-and-forest | |
| 127 | cabins-and-hot-tub | |
| 89 | family-and-glamping | |
| 50 | private-and-rv |
Every combo is (glamping | cabins | rv) + (popular feature). They picked cells where a known dual-keyword search exists, not (yurt × hot-tub) or (treehouse × pets).
1c. Specific-property pages (the listing leaves)
| Template | URL | Use |
|---|---|---|
| Private host land | /land/<slug>-<id> | Their actual marketplace listings (~1,000/state) |
| Public campground | /campground/<country>/<state>/<slug>-<id> | Govt-owned campgrounds, NOT bookable on HipCamp (1,325 in CA) |
| State park | /state-park/<country>/<state>/<slug>/all | 2,497 nationwide |
| National park | /national-park/<country>/<state>/<slug>/all | Every NPS site |
1d. Long-tail question pages
10,748 question URLs (4,994 unique slugs after dedup). Counted by leading pattern:
Show sub-pattern breakdown (what they're actually asking)
what-* dominated by activities/things to do:
what-are-some-popular-{X}-near-{place}(853)what-is-the-best-{X}-{verb}-{place}(441)what-outdoor-activities-{are/can-i}-{near/at}-{place}(219)
are-* dominated by amenity/permission about a specific place:
are-dogs-allowed-at-{place}(243)are-pets-allowed-at-{place}(157)are-there-camping-options-{near/in/at}-{place}(139)
can-* dominated by activity/permission:
can-i-bring-my-{pet/dog/rv}-{to/at}-{place}(327)can-i-go-fishing-{at/near}-{place}(169)can-i-camp-at-{place}(94)
how-* dominated by cost & access:
how-much-does-it-cost-to-camp-{at/in}-{place}(128)how-can-i-get-{to-X}(71)how-can-i-access-{place}(63)
Real corpus is ~30 master templates × thousands of entities. Each page links to 5 related questions for crawler recycling. ~50% URL duplication tolerated.
1e. Collections (editorial)
52 evergreen + event-driven landing pages: e.g., /collections/camping/memorial-day, /collections/camping/overland-expo-west, /collections/camping/field-van, etc. Low volume, high seasonal intent.
1f. The math of the index
Single-filter discover pages: ~92,500 URLs.
Multi-filter: 2,615 URLs.
City "all" pages: tens of thousands of URLs.
Public-campground pages: tens of thousands of URLs.
Question pages: 10,748 URLs.
HipCamp's PSEO index is somewhere in the 200K-500K page range.
{city, filter} page if they have ≥1 listing for that filter in that city. Most cities have 3 filter pages (rv/glamping/cabins). Largest cities have 30+. Empty filter pages would tank UX and SEO.
2. What changes for Let's Camp
Shift 1: Inventory is the bottleneck, not the keyword universe
HipCamp has tens of thousands of listings. Let's Camp has ~200 campgrounds right now.
| Province | Customers | Implication |
|---|---|---|
| Saskatchewan | ~84 (41.8%) | Dense enough to support 200+ city/filter pages |
| Alberta | ~38 (19.1%) | Supports 100+ pages |
| Ontario | ~28 (13.9%) | Geographically huge, ~28 grounds isn't dense |
| British Columbia | ~24 (11.9%) | Same problem |
| All others | ~26 (~13%) | Page-per-province at best |
Shift 2: There are two audiences, not one
- Operator SEO → drives B2B SaaS signups directly → grows the inventory → unblocks the consumer PSEO from the thin-inventory problem
- Consumer PSEO → drives camper bookings → makes the marketplace more valuable → makes our B2B pitch ("list with us and Google sends you free traffic") more credible → drives operator signups
Build both, but operator pages ship today regardless of inventory while consumer pages need inventory density + data first.
Shift 3: Canada-first taxonomy
A few of HipCamp's filters are Canada-irrelevant (redwoods, desert, surfing). The additions below are Canada-specific. They live on different axes of the permutation engine — calling out which is which because they're not all the same kind of thing:
Activity filters combine with geo, e.g., /camping/saskatchewan/ice-fishing
Camp-style filter Canadian phrasing for accommodation type
Outranks "cabin rentals" in ON/QC search behavior.
Access-mode filter niche, applies to the territories and far north
Operational characteristics about how the campground operates
Natural-feature labels can be filter slugs OR aggregate cross-province region pages
Named geographies PLACES, not filters — belong in the Geo dimension alongside Banff and Algonquin
3. Infrastructure
Let's Camp actually runs three separate web properties. The main site has structural SEO friction.
| Property | Stack | Indexable? | What's there today |
|---|---|---|---|
letscamp.ca |
Vite/React SPA | Yes, but slowly | A javascript-heavy site that Google struggles to index. |
blog.letscamp.ca |
WordPress + Yoast | Yes | 45 indexed posts. Camper-side content: tenting tips, monthly "campground booking open dates", environmental tips. Already has sitemap, schema, proper meta. |
join.letscamp.ca |
WordPress + Yoast | Yes | Operator-facing marketing site: Features, Pricing, Learning Center, Owners Blog, Case Studies. Has a custom campground_review-sitemap.xml. |
~1,000+ pages are indexed as of June 2026. But Google's crawler has to execute each page's JavaScript and index the rendered DOM. This is slow and expensive for Google; it's faster at indexing straight HTML. Indexing gets queued (days-to-weeks delay for new URLs), and pages can be invisible to non-Google crawlers (e.g., Bing, ChatGPT / Perplexity / Claude search, social cards).
The slowness is a real problem for a Programmatic SEO play that needs to ship hundreds of new pages quickly and earn AI-crawler citations.
The fix is adding Server-Side Rendering (SSR). There are common tools on the market to do this, such as Prerender.
Here are the benefits of SSR:
- Speed of indexing new pages. Rendering Javascript-heavy pages can take days to weeks for a new URL to be indexed. For a PSEO corpus shipping tens of new pages per week, that drag compounds. SSR pre-renders pages so Google's first pass can pick up all our content on the first pass.
- AI crawlers and non-Google search. ChatGPT, Perplexity, Claude search, and Bing don't typically render JS. AI search citations are a growing acquisition channel; SSR makes us visible to them.
- Deterministic reliability. Pass 2 can be silently delayed or fail. SSR is straightforward to verify.
Infra recommendations:
- Add Server-Side Rendering.
- Operator-side competitor pages — start on
join.letscamp.caimmediately. WordPress + Yoast is already there. Just add ~12 new pages. (No new infra required.) Subdirectories generally outrank subdomains by 15-40% in measured A/B migrations, so eventually we'll want operator content underletscamp.ca/software/..., but the existing subdomain has accumulated authority and backlinks, so we don't need to today. Plan a consolidation pass later, once the corpus is stable and someone can babysit the 301s + Search Console for ~90 days. - Consumer-side PSEO at scale → build at
letscamp.ca/explore/...as a static-rendered subdirectory. Google's official position is that subdomains and subdirectories rank equivalently, but practitioner consensus (Rand Fishkin, Aleyda Solis, multiple documented migrations) is that subdirectories outperform subdomains by 15-40% in practice. The probable mechanism is authority consolidation + entity perception + denser internal linking — not crawl budget or domain authority literally. Since this is brand-new content with no migration risk, we get the upside for free by going subdirectory from day one. - Noindex the 416
/camps/<slug>/terms-conditionspages. They're templated legal boilerplate with no search intent and currently sit in the sitemap. Five-minute fix, modest domain-quality lift.
Subdirectory architecture for the new PSEO surface is the consequential choice — get it right at the start.
Implementation options for letscamp.ca/explore/* — to decide with Vivek / Mike
The strategy (build /explore/ as a static-rendered subdirectory of letscamp.ca) is settled. The technical path to get there has three viable shapes. This is the kind of decision worth making jointly with whoever owns the Let's Camp tech stack:
Option A: Cloudflare Worker as a reverse proxy
A Cloudflare Worker sits in front of letscamp.ca and routes traffic by URL prefix:
/explore/*→ a new static-rendered SEO site, hosted separately (Vercel, Cloudflare Pages, etc.)- everything else → the existing Vite SPA, unchanged
Yes, this is a reverse proxy. The Worker accepts incoming requests and forwards them to different origins based on path rules. Workers run at the edge so the routing overhead is minimal (low milliseconds).
- Pros: Zero impact on the existing booking app. Independent deploy cycle for SEO content. Fast edge routing. Easy to roll back or A/B test. Two teams can work in parallel.
- Cons: Two codebases. Another moving piece in the system. Worker subrequest limits (50 per request) apply if a single page needs many origin fetches.
- Best when: We want to ship SEO content fast without touching the booking app. Cloudflare is already in the stack.
Option B: Build static rendering into the existing Vite app
Add SSR capability to the current Vite/React app, either by:
- Adopting Vite SSR via
Vike(formerlyvite-plugin-ssr) - Migrating to a framework that does SSR natively (Next.js, Astro, Remix)
The /explore/* routes would then be pre-rendered at build time as static HTML, shipped from the same codebase as the booking app.
- Pros: Single codebase. No proxy layer. Future-proofs the whole app for SEO, not just
/explore/. - Cons: Bigger engineering lift up front. Any bug in the migration risks the booking conversion flow. Ties SEO release cycle to app release cycle.
- Best when: The team is already considering a framework migration for other reasons (performance, DX), OR there's appetite for a single-codebase architecture.
Option C: Add Prerender.io (or equivalent) to the existing site
A drop-in service caches a server-rendered version of every SPA page. Cloudflare detects search-bot user-agents and routes them to the cached HTML; human users get the SPA.
- Pros: No code changes. Works for the entire site, including the existing 1,000+
/camps/*pages. Fastest path to "Google sees pre-rendered HTML." - Cons: External dependency. Costs scale with traffic ($90+/month at PSEO scale). Only helps SEO bots — humans still get the SPA, so no Core Web Vitals benefit. Doesn't make new PSEO content easier to author — we still need somewhere to build it.
- Best when: We want a quick fix for the existing SPA's indexing speed without building a separate static site.
A and C are complementary, not exclusive. A is the natural answer for the new PSEO surface (/explore/*). C is a separate question about the existing 1,000+ SPA pages (/camps/*) — whether to make those faster to index too. B is the long-term answer if there's appetite for an architecture consolidation.
Questions worth bringing to Vivek / Mike:
- Is there an existing roadmap to migrate the Vite SPA to a framework (Next.js, Astro)? If yes, Option B might be the natural place to absorb this work.
- How much eng capacity is available for new infrastructure work in the next quarter?
- Are there reasons to keep the existing Vite app intact (mobile webview compatibility, etc.) that would steer us toward A?
- What's the team's familiarity with Cloudflare Workers?
The strategy is sound regardless of implementation. The choice between A/B/C is about engineering trade-offs, not SEO outcomes.
4. Keyword storing — the explicit lists
The subsections below are the master taxonomy of keyword dimensions: geo, camp-style, amenity, activity, audience, and Canadian-specific niche. You generate a candidate page-set by combining one value from each relevant dimension — e.g., a province from the geo list plus a filter from the camp-style list produces a /camping/{province}/{filter} page. All values are picked with Canadian search behavior in mind, not US — that's why some of HipCamp's 48 filters from §1b don't appear here, and a few Canada-specific ones do.
3a. Geo
Country: Canada (1) · Provinces and territories (13): BC, AB, SK, MB, ON, QC, NB, NS, PE, NL, YT, NT, NU.
Tier-1 metro cities (~50 — start here)
- BC: Vancouver, Victoria, Kelowna, Nanaimo, Kamloops, Prince George, Whistler
- AB: Calgary, Edmonton, Banff, Canmore, Jasper, Lethbridge, Red Deer
- SK: Saskatoon, Regina, Prince Albert, Moose Jaw
- MB: Winnipeg, Brandon
- ON: Toronto, Ottawa, Sudbury, Thunder Bay, North Bay, Kingston, London, Niagara Falls, Barrie, Muskoka, Algonquin
- QC: Montreal, Quebec City, Gatineau, Sherbrooke, Trois-Rivieres, Saguenay, Mont-Tremblant
- NB: Moncton, Fredericton, Saint John
- NS: Halifax, Sydney, Cape Breton
- PE: Charlottetown
- NL: St. John's, Gros Morne
- Territories: Whitehorse, Yellowknife, Iqaluit
Regions (~40 — huge search volume because campers think in regions)
- BC: Okanagan, Vancouver Island, Sunshine Coast, Kootenays, Cariboo, Gulf Islands, Squamish-Whistler, Fraser Valley
- AB: Rocky Mountains, Kananaskis, Lakeland, Crowsnest Pass, Drumheller / Badlands
- SK: Lake Diefenbaker, Cypress Hills, Northern Saskatchewan, Qu'Appelle Valley
- MB: Whiteshell, Interlake, Riding Mountain, Lake Winnipeg
- ON: Muskoka, Algonquin, Bruce Peninsula, Georgian Bay, Kawartha Lakes, Thousand Islands, Northern Ontario, Prince Edward County, Haliburton
- QC: Charlevoix, Laurentides, Eastern Townships, Mauricie, Gaspé, Saguenay-Lac-Saint-Jean
- Atlantic: Cabot Trail, Bay of Fundy, Acadian Coast, Avalon Peninsula
Named parks (~200 — biggest organic-search magnets)
National parks: Banff, Jasper, Yoho, Kootenay, Glacier (BC), Mount Revelstoke, Pacific Rim, Gros Morne, Cape Breton Highlands, Fundy, Kejimkujik, La Mauricie, Forillon, Prince Edward Island, Riding Mountain, Wapusk, Wood Buffalo, Elk Island, Waterton Lakes, Grasslands, Prince Albert, Pukaskwa, Bruce Peninsula, Georgian Bay Islands, Point Pelee, Thousand Islands, Rouge, Mingan Archipelago, Sable Island, Torngat Mountains, Terra Nova, Kluane, Auyuittuq, and the northern parks.
Provincial park magnets: Algonquin, Killarney, Sandbanks, Bon Echo, Lake Superior, Killbear, Pinery, Awenda, Arrowhead (ON); Garibaldi, Strathcona, Manning, Cathedral, Mount Robson (BC); Cypress Hills Interprovincial, Greenwater, Duck Mountain, Meadow Lake, La Ronge (SK); Whiteshell, Hecla-Grindstone, Spruce Woods (MB); Mont-Tremblant, Gaspésie, Jacques-Cartier (QC).
3b. Camp-style — grounded in real product taxonomy
Use Let's Camp's own bookable-inventory taxonomy for filters, because every filter page can then ladder directly into the booking funnel without a category mismatch. From the homepage "What are you booking?" dropdown, the actual product categories are:
| Product category | URL slug | Captures |
|---|---|---|
| RV Site | rv-sites | Biggest category; "RV camping in X", "RV park near Y" |
| Tent Site | tent-sites | "Tent camping in X", "Tenting in X" |
| Lodging | lodging | Catch-all for cabin/yurt/glamping |
| Parking | parking | Day-use / overnight parking — niche |
| Moorage | moorage | Canada-specific niche — boat slips at waterfront campgrounds. HipCamp does NOT have this. |
| Rental RV | rental-rv | RV rentals on-site at the campground |
Search-intent additions (keyword-driven, not product-driven):
| Slug | Captures | Justification |
|---|---|---|
cabin-rentals | "Cabin rental in X" | Distinct keyword from "lodging"; ~10× volume in Canada |
cottage-rentals | "Cottage rental in X" | Uniquely Canadian; outranks "cabin" in Ontario |
glamping | "Glamping in X" | High-CTR keyword |
yurts | "Yurts in X" | Provincial parks in ON/SK have prominent yurts |
trailer-parks | "Trailer parks in X" | Canadian-specific phrasing for long-stay RV |
seasonal-sites | "Seasonal sites in X" | Big in MB/SK/ON |
group-camping | "Group camping in X" | Use-case driven |
backcountry | "Backcountry camping in X" | Maps to parks, not direct inventory |
boondocking | "Boondocking in X" | Camper intent; ranks adjacent |
3c. Amenity dimension
3d. Activity dimension
3e. Audience / use-case
3f. Canadian-specific niche
3g. B2B / operator-side (different intent, higher conversion)
Software comparison keywords
- "Campspot alternative Canada"
- "Firefly Reservations alternative"
- "ResNexus alternative"
- "RoverPass alternative"
- "Campground Master alternative"
- "RezExpert alternative"
Software intent by trait
- "Free campground reservation software"
- "Campground reservation software with no monthly fee"
- "Cloud campground reservation software"
- "Campground booking software with site map"
- "Bilingual campground software"
- "Campground software for small parks"
- "RV park reservation software Canada"
- "Campground software with marketplace"
- "Mobile campground reservation app"
- "Self-serve camper portal software"
Software by region & problem & how-to
By region: "Campground reservation software Ontario / Saskatchewan / Alberta / British Columbia"
By problem: "How to stop double bookings campground" · "Best way to take online campground reservations" · "Switching from Campspot to Let's Camp" · "How to refund a camper online"
How-to / glossary: "How to start a campground in Canada" · "How much does campground software cost" · "Best campground POS" · "How to price campground sites" · "Campground occupancy benchmarks"
5. Page templates
Three core templates. Each is a Mustache-ish template with slot fillers from inventory, scraped public data, and a small editor-curated per-province table.
Template A: Geo + filter (consumer)
URL: /camping/<province-slug>/<filter-slug> (province root)
/camping/<province-slug>/<city-slug>/<filter-slug> (city)
/camping/<province-slug>/<region-slug>/<filter-slug> (region)
H1: "The best {filter} camping in {place}, {province}"
Meta: "{filter} camping in {place}: {N}+ campgrounds | Let's Camp"
Above the fold:
- Hero image
- H1
- 2-3 sentence intro: sensory-specific opener about the place,
then a one-line inventory summary ("{N}+ {filter} sites starting
at $X/night"). NO em-dashes, NO dangling "the same".
- Filter chip rail (other available filters for this geo, with counts)
Body:
- Listing grid (cards): name, rating, # of sites, key amenity icons,
starting price in CAD, "Book direct" CTA
- If <5 listings: pad with "Public {province} provincial parks near
{place}" cards (info only — HipCamp's state-park trick)
Internal-link rails (this is where SEO compounds):
- "More {filter} camping in {province}" → 8 nearby cities/regions
- "Other ways to camp near {place}" → 6 other filter types
- "Top provincial parks near {place}" → 6 parks
- "Top campgrounds in {province}" → 6 most-booked listings
- Breadcrumb: Canada > {Province} > {Place} > {Filter}
Below the fold:
- FAQ (5-8 questions; LLM-generated against per-province fact sheet,
human-edited)
- Recent reviews (pull from listings if available)
- App download / "Become a host" CTA
Template B: Named park / public-land page (highest-leverage)
Highest-value template because it captures explicit search intent ("Algonquin Park camping") AND requires zero first-party inventory.
URL: /camping/{province-slug}/parks/{park-slug}
H1: "Camping at {Park Name}, {Province}"
Meta: "{Park Name} camping: {N} campgrounds nearby, booking guide,
and what to know before you go | Let's Camp"
Body:
- 4-paragraph guide:
1. What the park is, in one line
2. Best time to visit, peak/shoulder seasons
3. How camping inside the park works (fees, booking window) —
link OUT to the official source
4. "Where to camp nearby" — Let's Camp's actual inventory surfaces
- Cards: official park campgrounds (info only) + Let's Camp customer
listings within X km, sorted nearest
- "Things to do at {park}" — 6-10 activities with internal links
- FAQ block
- Map embed
- Internal-link rail: other top parks in this province
Template C: Operator / B2B comparison page
URL: /software/<comparison> e.g. /software/campspot-alternative
H1: "The best Campspot alternative for Canadian campgrounds"
Meta: "Campspot alternative: free marketplace, no monthly minimum,
Canadian support | Let's Camp"
Body:
- Hero with one-line value prop
- Side-by-side comparison table (Let's Camp vs Campspot) — cells
straight from the VoC bad-alternatives table
- "Why owners switched" — 3-4 named-customer quotes (Nancy G.,
Kassandra C., Michael S. — public, Capterra-sourced)
- "How the switch works" (the 48-hour migration value prop)
- FAQ: pricing, migration, training time, existing reservations
- "Book a demo" CTA throughout
A single "Campspot alternative Canada" page that ranks #1 could be worth more in pipeline than 500 thin consumer pages combined.
Template D: "When {Park} is sold out" — alternatives page (Tier A data-moat)
Tier A availability-driven, broken out as its own template because the SERP shape and page structure are distinct from Template A/B. The PAA box is reliably populated across famous parks — "Can you camp without a reservation?" / "How far in advance does {Park} sell out?" / "What's the closest park with availability?" — so the FAQ structure is templated for free.
Scope is smaller than initially scoped — only 3 of 18 famous parks have clean SERPs, 5 more are buildable with caution, and 10 are already taken by Hipcamp/Campnab. Full breakdown in VALIDATION_FINDINGS.md.
URL: /camping/{province-slug}/parks/{park-slug}/sold-out-alternatives
(also: /camping/alternatives-to-{park-slug})
H1: "Where to camp when {Park Name} is sold out"
Meta: "{Park Name} sold out? {N} nearby Canadian campgrounds with
availability right now, plus walk-up options inside the park."
Above the fold:
- H1
- 2-sentence intro using real proprietary data, e.g.:
"{Park} weekend sites sell out 47 days before check-in on average.
Here are {N} Let's Camp partner campgrounds with availability
within a 2-hour drive, plus walk-up options inside the park itself."
- "Available right now" badge or live count
Body — five blocks:
1. Live availability — nearby private campgrounds (Let's Camp inventory
within ~2hr drive, sorted by drive time). Card per camp: name,
distance, recent-booking ticker, starting price, "Book" CTA.
2. Walk-up campgrounds inside {Park} (info only, links to official
park site). Same trick as HipCamp's state-park pages — capture
intent we can't directly serve.
3. FAQ block (templated from Google's "People Also Ask" for this geo):
- "Can you camp in {Park} without a reservation?"
- "How far in advance does {Park} sell out?"
- "What's the closest park with availability?"
- "When does {Park} open for reservation?"
4. Closest similar parks — internal links to /camping/{province}/parks/{X}
for 4-6 nearby parks with similar character (Killarney for Algonquin,
Yoho for Banff, etc.).
5. Map: pins for nearby private campgrounds + the park itself.
Methodology block (mandatory): "Lead-time data based on {N} bookings
across {Y} Let's Camp campgrounds near {Park} between {date range}.
Refreshed weekly."
Build only for parks where the SERP is unowned — per the bulk validation in VALIDATION_FINDINGS.md.
5b. The quality bar — what makes a PSEO page worth shipping
The hard part of PSEO isn't generating pages, it's generating pages Google rewards. Empty templated pages don't just fail to rank — they degrade the perceived quality of the whole domain. Ship only if most of these are present:
- Proprietary data the reader can't find elsewhere. Best moat. For Let's Camp: actual lead-time data, real-time availability, real price ranges in that geo, occupancy patterns.
- Sub-breakdowns and ranges, not point estimates. Glassdoor's analog: price by site type with a "what's typical" callout. Always a range, never a single number.
- A real map. Embedded, with pins for every listed campground and key landmarks.
- Methodology block. "How we picked these" — what data, how ranked, when updated. Two sentences, signed off.
- Top-X lists with reasons. Each listing has a one-line reason it's on the list. Differentiation per listing, not uniform cards.
- Real reviews surfaced inline. The proof-of-life signal. Verbatim, with name, date, length of stay, snippet.
- Auto-generated FAQs templated against actual entity data;
FAQPageschema for rich snippets. - Internal links to two scales of context — one zoom out (region/province), one sibling (other places at same level).
- Tools where applicable. Campground pricing calculator, "how many nights can we afford" — earns external links.
- No padding. Thin-yet-padded pages are worse than short-but-dense ones.
5c. The content quality rubric (every page passes this before ship)
§5b lays out the principles. This is the operational checklist that enforces them. A page can only ship if it passes this rubric. Run through it as a gate before publishing each batch.
RUBRIC.md in this folder — copy it into a PR template, an Asana card, or a Linear sub-checklist for every batch.
Section 0 — SERP validation pre-flight
Before spending effort to build a page, do a quick SERP check on the target query. Some keywords look good on paper but die in the SERP for reasons that only show up when you look. If the query fails any of these, skip the page — don't generate it.
- Vocabulary match. Google's featured snippet / top 3 results use the same noun for the thing as our URL/H1. If the snippet says "lodge" and our URL says "campground," the market vocabulary doesn't match — rewrite the framing or skip. (Real example: "snowmobiling campgrounds in northern Ontario" → market vocabulary is "sled-friendly lodges," not campgrounds. Skip or pivot.)
- No dominant featured snippet from a strong competitor. Featured snippet from a weak source = opportunity. Featured snippet from a comparable commercial competitor = uphill.
- No marketplace / aggregator in the top 3. HipCamp / Campspot / RoverPass in the top 3 = expensive cell. Tourism boards, Reddit, and official park sites in the top 3 = open territory.
- Real Let's Camp inventory in the geo (≥3 customer campgrounds within the page's intent radius). Exception: Template B/D pages where the framing doesn't require inventory in the park itself.
- People Also Ask box exists. Positive signal — Google has identified adjacent intent that doubles as your FAQ structure.
Sample-validate, don't validate everything: 5-10 keywords per template. If the template passes, ship all instances of it. If it fails, kill or pivot the template.
Validation tooling: ~90% of these checks are fully automated via bulk_serp_validation.py in this folder. It hits ScrapingBee's structured Google API at ~1 credit per query (~$0.0002) — validating ~125 candidates costs under $0.05. The one signal it can't capture reliably is AI Overview content (Google often declines to render AI Overviews for bot sessions). For high-priority candidates where AI Overview matters, run serp_html_check.py (~$0.01 per query) or screenshot manually. Full pipeline notes in CLAUDE.md in this folder.
Section 1 — Data-moat blocks ≥2 required
A PSEO page without proprietary data is a thin templated page Google eventually deprioritizes. Ship only if at least two of these are present.
- Real price range broken out by site type, sourced from
pricemodels(10th-90th percentile, not a single average) - Lead-time stat ("Sites near here book on average X days out") computed from
bookings - Availability block — embedded live calendar OR "X% availability for {month}" badge from
availabilities. Cache 5-15 min to protect the read path. - Demand signal — waitlist count, "most-booked" or "in-demand" badge from
waitlistsor booking velocity - Recent-booking ticker — "Just booked: 3 nights at {Camp}, $135 total, 2 hours ago." Anonymized (camp + price + recency only). FOMO-style proof the page is live and prices are real. None of HipCamp's competitors can show this.
- Real reviews surfaced (mark N/A until the
reviewscollection ships) - External-data block — historical weather chart from ECCC, trail map from OSM, or attractions list
Section 2 — Structural elements 100% required
- Unique H1 with specific entity + clear intent
- Meta description ≤155 chars, includes entity name + key modifier
- First 50 characters of body mention the place by name
- Methodology block at the bottom — 2 sentences, what data + when refreshed
- Last-updated date visible on page
- Embedded map with pins for listings + key landmarks
- Breadcrumb nav (Canada → Province → Region → Filter)
- Three internal-link rails: one zoom-out (region/province) + one sibling (other places at same level) + one editorial (link to relevant
blog.letscamp.caguide or related-articles section). The editorial link is what passes WordPress-blog authority into the new corpus. - FAQ block with 5-8 questions answered against actual entity data
Section 3 — Schema markup 100% required
LodgingBusinessJSON-LD for each listingFAQPageJSON-LD for FAQ sectionBreadcrumbListJSON-LD- Validated in Google Rich Results Test before ship
Section 4 — Content quality ≥80% required
Editorial review. Reading-quality counts here as much as facts.
- Each listing has a one-line differentiator ("Highest-rated waterfront site in the region" / "Bilingual EN/FR front desk")
- No padding paragraphs added for word count
- No em-dashes (Let's Camp copy convention)
- No dangling references ("the same thing", "that issue", "making the switch")
- No internal-process language ("we'll cover that on the demo")
- Customer-facing throughout — reader's situation, reader's outcome
- Sentence rhythm varied (no stacked same-shape sentences)
- Numbers framed as "X+" not exact figures ("300+" not "309")
- LLM-generated facts cross-checked against per-province fact sheet (no hallucinations)
Section 5 — Technical health 100% required
- HTTP 200 on first crawl
- Page loads under 2.5s LCP
- All internal links resolve (no 404s in the batch)
- Image alt-text on every
<img>(LLM-generated against camp metadata is fine) - Mobile-responsive layout
- Included in
sitemap.xmlwithlastmodset
Section 6 — Honest pre-publish test All three yes
- Would this page be bookmark-worthy at 20 visits/month even if it never ranks?
- Would I be comfortable showing this to a customer campground owner?
- Could a competitor with only public data easily replicate this? (If yes, find data they can't access.)
Pass/fail gates
| Section | Threshold | What "fail" means |
|---|---|---|
| 0 — SERP validation | All checks pass | Don't generate the page; skip the template or pivot the framing |
| 1 — Data-moat | ≥2 blocks present | Don't ship; rebuild with proprietary data |
| 2 — Structural | 100% | Don't ship; fix |
| 3 — Schema | 100% | Don't ship; fix |
| 4 — Content quality | ≥80% | Editorial rewrite before ship |
| 5 — Technical | 100% | Don't ship; fix |
| 6 — Honest test | All three yes | Don't ship; rethink the page |
A batch only moves from staging → production when 100% of pages in the batch have passed the rubric and been human-signed-off. Pages that fail go back to the writer/template-author with the failing rows flagged. Don't try to fix them in production after they're indexed — pruning thin pages from a live corpus is much harder than not shipping them in the first place.
6. Phased rollout
Each phase has a clear gate. Headline principle: quality first, scale second. First batch (20-50 great pages across operator + consumer + park templates) is the proof point. Don't ramp until those are ranking. Then 5-10/week, not 500. Google's quality classifier penalizes sudden floods of templated pages.
Nothing matters until Googlebot sees HTML. Options:
- Static-rendered marketing surface at
letscamp.ca/explore/...(Next.js static export / Astro), fronted by a Cloudflare Worker that routes/explore/*to it and passes everything else to the existing Vite SPA. Subdirectory, not subdomain — see §3 callout. Easiest path that gets the SEO architecture right. ~2-4 eng weeks. - Prerender the SPA (Prerender.io, self-hosted Puppeteer). Cheapest in eng time, worst long-term.
- Move whole site to SSR. Best long-term, biggest project.
Pick (1) unless there's already a Next.js move in flight. Write content in parallel.
Static, hand-written, ~12 pages. No inventory dependency. Ship dollars fastest.
/software/campspot-alternative/software/firefly-reservations-alternative/software/resnexus-alternative/software/free-campground-reservation-software/software/bilingual-campground-software/software/cloud-campground-software/software/small-campground-software/software/rv-park-reservation-software-canada/software/how-much-does-campground-software-cost/software/how-to-start-a-campground-in-canada/software/switching-from-campspot/software/campground-software-comparison
Track sign-ups + demos per page. Campspot alternative should hit first — strongest commercial intent.
Start narrow: Saskatchewan, then Alberta. High inventory density.
/camping/saskatchewan/{filter}— 15 pages/camping/saskatchewan/{city}/all— top 15 SK cities with ≥2 listings/camping/saskatchewan/{city}/{filter}— only if ≥1 listing- Same for Alberta
Roughly 100-200 pages. Then ON, BC. Skip provinces with <5 listings.
Free internal-link amplification: every Template A page gets a "Top campgrounds" rail that links to the 416 existing /camps/<slug> leaf pages already in the sitemap. Both layers compound — the new aggregation pages inherit authority from the established leaf pages, and the leaf pages get fresh internal-link sources. Same trick HipCamp uses with their state-park pages.
Highest-leverage template — no inventory gate.
- All Banff/Jasper/Algonquin/Killarney national-park + major-provincial-park pages
- Public data (park websites, Parks Canada, Ontario Parks API) supplies structured fields
- Internal-link rail: "Private campgrounds near {Park}"
Target: 150-300 named-park pages.
Once core matrix is ranking. Same trick HipCamp uses. Patterns:
is-{province}-good-for-campingis-there-free-camping-in-{province}can-you-camp-anywhere-in-{place}cheapest-campgrounds-in-{place}dog-friendly-campgrounds-in-{place}{filter}-camping-near-{landmark}how-much-does-it-cost-to-camp-at-{park}
Aim for 500-2,000. Cheap traffic.
Only after single-filter pages have stable inventory. Pick 10-15 combos with real demand:
Don't permute everything by everything. Only generate cells with ≥1 listing.
Each is its own template. Tiered by data-moat strength — Tier A categories ONLY Let's Camp can do well (most defensible, ship first). Tier F is durable modifiers that compound across the corpus once data-moat templates establish ranking signal.
TIER AData-moat templates (most defensible — ship first)
Demand-revealed popularity (bookings + waitlists)
"Hardest-to-book campgrounds in {location}"— novel framing; high social-proof CTR"Most-booked campgrounds in {province} this summer""Campgrounds that sold out for {Canada Day | Civic Holiday | Labour Day}"— annual refresh hook"Quietest campgrounds in {region}"— inverse: peaceful framing"Hidden-gem campgrounds in {region}"— high-rating × low-booking-velocity- Recent-booking ticker — not a new page, but a block embedded on every geo/filter page. "Just booked: 3 nights at {Camp}, ${total}, 2h ago." Anonymized (camp + price + recency only). FOMO-driven proof-of-life signal. Refresh every 15 min from
bookings. Cache the rendered HTML to protect the read path. None of HipCamp's competitors can do this.
Availability-driven (live data from availabilities)
"Last-minute camping in {location} this weekend"— auto-refreshed"Available campgrounds for {month} in {location}""Available {filter} for {holiday} weekend in {province}""Where to camp when {Algonquin | Banff | Cape Breton Highlands} is sold out"— captures explicit substitution intent"Same-day booking campgrounds in {region}"
Lead-time intelligence (bookings.createdAt vs checkIn)
"How far in advance to book {Park} camping"— interactive widget + content, uniquely answerable from your data"When to book {province} campgrounds for July weekends""Last-chance booking windows for {holiday} in {region}"
Pricing-driven (pricemodels + dynamicprices + longweekendrules)
"Cheapest campgrounds in {location}"— with real range, not made-up"Campgrounds under $30 / $40 / $50 a night in {province}""Luxury / premium campgrounds in {region}""Best value RV parks in {province}"— quality-to-price index"Campgrounds that don't price-gouge on long weekends in {province}"— useslongweekendrules, unique angle"Monthly RV rates in {province}"— snowbird intent
Weather-driven (ECCC integration)
"Driest July campgrounds in {province}""Warmest May camping in {province}"— shoulder-season intent"Best September camping in {province}"— fall foliage"Coolest summer escapes in {region}"— heat-wave search spikes"Snow-free shoulder camping in {region}""Best stargazing campgrounds in {region}"— weather × dark-sky intersection
Site-spec granularity (sites collection)
HipCamp filters at camp granularity. Let's Camp can filter at site granularity.
"Campgrounds that fit 40-foot RVs in {location}""50-amp service campgrounds in {province}""Pull-through site campgrounds in {region}""Big-rig friendly RV parks in {province}""Class A motorhome campgrounds near {city}"
TIER BGeographic and travel-mode
Province → region → colloquial area
Canadian colloquial regions are real search behavior: GTA, Lower Mainland, NCR, Tri-Cities, Niagara Region, Cottage Country, Eastern Townships, Cariboo, Sea-to-Sky Corridor, Annapolis Valley.
Radius-from-city (new)
"Camping within 1 hour / 2 hours / 3 hours of {city}""Weekend camping from {Toronto | Vancouver | Calgary}""Day-trip campgrounds from {city}"
Highway-anchored (new)
"Campgrounds along the Trans-Canada Highway""Campgrounds along Highway 17 (north shore Superior)""Campgrounds along the Icefields Parkway / Cabot Trail"
Travel-mode (new)
"Drive-up campgrounds in {region}"vs walk-in / bike-in / boat-in"Boat-in / paddle-in campgrounds in {region}"
Named-landmark anchored
"Campgrounds near {landmark}" — lakes (Lake Winnipeg, Lake Louise), rivers, mountain ranges, falls.
TIER CItinerary, trail, and journey
- Backpacking-the-trail — West Coast Trail, La Cloche Silhouette, Gros Morne Long Range, Bruce Trail. Link-bait potential, earns external links.
- Drive-{A}-to-{B}-with-camping-stops (new) — explicit two-endpoint route planning ("Toronto to Halifax with camping stops"). Distinct from named scenic drives.
- Road-tripping the {scenic drive} — Cabot Trail, Icefields Parkway, Highway 17, Sea-to-Sky
- Trail-anchored basecamps (new) —
"Campgrounds near {trail name}", multi-day hike basecamps. AllTrails has the trails; doesn't book campgrounds. - N-day itineraries —
"7-day Maritimes camping itinerary","5-day Rockies camping trip"
TIER DAudience and use-case deep cuts
Use-case modifier
"{place} campgrounds for {weddings | family reunions | group retreats | corporate offsites}"
Audience deep cuts (new)
"Solo female-friendly campgrounds in {location}"— growing search trend, underserved"ADA / wheelchair-accessible campgrounds in {location}""Adults-only campgrounds in {province}""Senior-friendly RV parks in {province}""Toddler-friendly campgrounds in {location}""First-time camper friendly campgrounds in {location}""Quiet (no parties) campgrounds in {location}""Group campgrounds for {25+ | 50+ | 100+} people in {region}"
Connectivity / lifestyle (new — remote-work era)
"WiFi campgrounds in {location}""Workation campgrounds in {region}"— digital nomad"Off-grid campgrounds in {province}"
Length-of-stay (new — huge in Canada)
"Long-term RV parks in {province}"— snowbird angle"Monthly RV rates in {region}""Seasonal sites in {Ontario | Manitoba | Saskatchewan}"
Pet deep cuts
"Off-leash dog campgrounds in {location}", "Campgrounds with dog parks in {location}" — deeper than generic "pet-friendly".
TIER EDecision, comparison, and utility
Comparison pages (new — high commercial intent)
"{Campground A} vs {Campground B}"— Algonquin vs Killarney, Banff vs Jasper"Alternatives to {famous campground}"— captures priced-out / locked-out intent"Less crowded than {Algonquin | Banff} campgrounds""Hidden gems near {famous park}""Camping vs cottage rental in {region}"
Reservation-utility (new)
"When does {Park} open for booking"— Ontario Parks 5-month window, Parks Canada calendar"{Park} cancellation policy explained""Best campsites at {Park}"— site-pick guides, high evergreen traffic"{Park} site map and layout""Walk-up campgrounds in {province}"— no-reservation niche
Festival / event-anchored (new)
Calgary Stampede, Pemberton Music Festival, K-Days Edmonton, Tall Ships Halifax, Cavendish Beach Music Festival. Long-tail per event; spike-driven but evergreen across years.
Map-anchored (new — Let's Camp UI strength)
"Interactive campground map of {region}""Map of {filter} campgrounds in {province}"
Earns external links because it's genuinely useful, not a listicle.
TIER FDurable modifier templates (compound on top once A-E rank)
All strong but lower data-moat than Tier A — they layer on the geo/filter cells already built.
"Best campgrounds in {place} 2026"— year modifier, refresh annually"Camping guide for {place}"— long-form editorial"Campgrounds open in {month} in {place}"— seasonality"Year-round campgrounds in {place}"— snowbird / winter RV"Campgrounds open for {holiday}"— Canada Day, Victoria Day, Labour Day, Thanksgiving"{place} campgrounds that allow {tents | RVs | cabins | bonfires | dogs}""{place} campgrounds with {cabins | showers | full hookups | pool}""{place} campgrounds with {mountain | lake | river | ocean | prairie} views""Dark-sky / Aurora viewing campgrounds in {place}""First Nations / Indigenous-owned campgrounds in {place}"
Activity sub-niches (deeper than generic "{activity} in {location}")
"Surfing campgrounds in {BC | Nova Scotia | PEI}"— Tofino, Lawrencetown"Climbing campgrounds in {Squamish | Bon Echo | Kelowna}""Mountain biking campgrounds in {Whistler | Burke Mountain | Hardwood Hills}""Whitewater rafting campgrounds near {Ottawa | Slave River}""Pickleball-friendly campgrounds in {location}"— growing, underserved
DEFERCalled out as weak
- Trend-as-modifier (NFT-themed, AI-themed) — gimmicky, gets nuked in next quality update
- Best campgrounds at {postal code FSA} — ~1,500 FSAs × low individual search volume = lots of work, small payoff. The Tier B radius-from-city template captures the same "near me" intent with far fewer pages
- Camping near {airport code} — only YYZ / YVR / YYC / YEG have meaningful Canadian volume. Build 4-5, not all
- Bachelor parties / wild weekend framing — many customer campgrounds explicitly ban large stag parties. If you build this, lean it toward "campgrounds that welcome group celebrations" — the wild-weekend angle attracts guests the customer campgrounds don't want and damages the host relationship
Full long-tail surface: ~2,000-5,000 pages stacked. Don't try to ship them all. 5-10/week ramp; pick the next batch by where keyword research shows real volume AND where data-moat blocks are available.
7. Content generation pipeline
None of these are exotic — table stakes for any PSEO play.
- Master inventory table. Every campground, structured fields. Sourced from MongoDB
camps+sites+sitetypes(see Appendix A). - Proprietary-data extract (nightly from MongoDB): aggregations of
bookings,availabilities,pricemodels,waitlists,events,addons,attractions. - Per-province fact sheet (one short markdown each, edited twice a year): season dates, peak/shoulder, prices, top activities. Ground truth for the LLM.
- Public-park dataset (quarterly): name, province, slug, official URL, geo, key features, lat/lng.
- External data joins:
- Weather: Environment and Climate Change Canada open API. Per camp by lat/lng → nearest station → monthly historical normals. Powers historical weather graphs and entire pages like "Best camping months in {region}", "Driest July destinations in {province}", "Year-round warm-weather campgrounds." Refresh annually.
- Trails: OpenStreetMap. Per camp → trails within X km → name, length, difficulty. Powers "Trails near {camp}" and the parallel "Camping near the {trail name}" page set. AllTrails has the trails; Let's Camp can be the bridge.
- Optional: tides (DFO), ferry schedules (BC Ferries, Marine Atlantic), aurora forecast, dark-sky preserves (RASC).
- Templates (Mustache or Python f-strings): one per page type.
- LLM generation step: only state facts present in fact sheet or proprietary-data extract. Strict prompts.
- Human QA queue: 30-second human review before ship. 60-100 pages/hour at reasonable quality.
- Renderer: Next.js or Astro ingesting JSON per page.
- Sitemap generator: split by template (
sitemap-cities.xml,sitemap-parks.xml,sitemap-software.xml), submitted via Search Console.
7b. Measurement and monitoring
PSEO is a domain-level reputation game. Dashboard should be Google Search Console (free, authoritative for what Google sees), not just Ahrefs/SEMrush.
Pre-launch / per-batch
- New pages return HTTP 200 with proper title + meta description
- Internal links resolve (no 404s in the new batch)
- Schema validates (FAQPage, LodgingBusiness, BreadcrumbList — Google Rich Results Test)
- Sitemap updated + re-submitted via Search Console
Weekly alarms
- "Discovered – currently not indexed" growing = Google saw the URL but chose not to crawl. Almost always quality. Fix before shipping more.
- "Crawled – currently not indexed" growing = Google crawled AND read AND chose not to index. Same fix.
- 404s / soft-404s climbing = renderer issue or dead sitemap URLs. Same-day triage.
- Site-wide average position dropping after a batch = new pages diluting domain quality. Pull, fix, redeploy.
- Index coverage rate = indexed / submitted. Target ≥80%. <50% = quality problem, stop ramping.
Monthly editorial
- Stale content audit. Pages >12 months untouched: refresh or de-index.
- Better to remove than to keep up. Pages with no impressions after 6 months: fix intent match or noindex.
Lead-attribution (GTM-engineer angle)
- Track demo bookings + signups per page. Operator-driving pages are worth 100× camper-bouncing pages. UTM correctly. Surface per-page conversion in the funnel monitor.
7c. Data freshness architecture (how often pages refresh)
A PSEO page is only useful if its data is current. But re-rendering thousands of pages on every database change is wasteful and breaks the static-rendering model. The standard pattern at SEO scale is Incremental Static Regeneration (ISR) layered with on-demand revalidation and client-side hydration — three layers, picked by how fast each kind of data changes.
Layer 1 — Static pages with time-based revalidation (the bulk of the corpus)
Pages are pre-rendered as static HTML at build time. Each page has a revalidate interval — when a request comes in after the interval has elapsed, the page is regenerated in the background and the requester gets the stale version instantly. Next.js calls this ISR; Astro does it via SSG + adapter; Vercel and Cloudflare support it natively.
Recommended intervals for Let's Camp:
- 24 hours for camp descriptions, attractions, named-park guides, FAQs, comparison pages — most of the corpus
- 1 hour for price ranges, lead-time stats, "X% availability for {month}" badges — anything tied to
pricemodelsoravailabilitiesaggregates - 5–15 min is overkill for static rendering; if you need this frequency, the block belongs in Layer 3
Layer 2 — Webhook revalidation for material data changes
When something important changes in MongoDB (new camp listed, significant price update, new attraction added), fire a webhook from the booking app's API to the PSEO renderer. The renderer calls revalidatePath() (Next.js) or the equivalent on the specific affected URLs. The page rebuilds within seconds.
This is how you avoid the "stale until next 24h interval" gap for changes that matter. It's also why this pattern beats a database listener — events fire at the application layer when they're semantically meaningful, not on every individual database write.
Triggers worth firing:
- Camp goes live / pauses
- Price model changes materially
- New add-on offering launched
- Event added / updated
- Camp closes for the season
Layer 3 — Client-side fetch for live blocks
Some data needs to be near-real-time for humans but isn't useful for Google to index. The recent-booking ticker ("Just booked 3 nights at Big Bend, 2h ago"), live waitlist counts, "open this weekend" widgets — all of these render client-side via /api/... endpoints that hit MongoDB directly.
The static page shell (Layers 1+2) is what Google indexes. The live blocks (Layer 3) layer over top after page load. Google sees the proof-of-life value via the static layers; humans get the freshness on top.
What companies actually do
- Airbnb / Vrbo: static listing pages, ISR with hours-to-day revalidation, client-side data for prices and availability
- Zillow: static for the bulk, on-demand revalidation when listings change, client-side for "estimate updates"
- HipCamp: likely a similar pattern — page bones pre-rendered, hot blocks hydrate after
- Stripe / Linear / Vercel docs: pure SSG, rebuilt on every git push (editor-managed, not data-driven)
Anti-patterns to avoid
- Database listeners that trigger rebuilds on every write. Too noisy, too tightly coupled to the write path. Use application-event webhooks at semantic boundaries instead.
- Re-rendering the whole corpus every N minutes on a cron. Wasteful. ISR re-renders a page only when someone visits it after the interval has elapsed.
- Putting all the data client-side. Then you've built another SPA, with all the indexing trade-offs. The point of the layered pattern is to use Google's static-HTML preference where it helps and reserve client-side for blocks where it doesn't hurt.
Appendix A — Proprietary data → page features
Per-collection mapping: what's in each high-value MongoDB collection, what concrete page block it powers, query shape. Engineering scoping reference.
camps
Key fields: _id, name, slug, lat/lng, province, city, description, amenityFlags, tenantId, images[], season{open,close}
Page block(s): every listing card, breadcrumb, map pin, intro paragraph. Templates: all.
sites
Key fields: campId, siteTypeId, name, attributes{length, electrical, water, sewer, maxRigSize, allowsPets, ...}
Page block(s): site-level filter ("RV sites that fit 40-foot rigs in X"), amenity callouts on listing cards.
Why it matters: HipCamp filters at camp granularity. Let's Camp can filter at site granularity. "Campgrounds in {place} with a pull-through that fits 45 feet" is a real query and Let's Camp can answer it; competitors cannot.
sitetypes + sitetemplates
Type taxonomy used across all camps. Powers the filter chip rail. Cross-check against the homepage dropdown (RV Site, Tent Site, Lodging, Parking, Moorage, Rental RV) — that's the truth source.
lodging_query
Key fields: campName, campAddress, lodgingCount · 69 records
Use: Campground-level cache showing which camps have lodging-type inventory and how many. Use to size and gate the "Lodging" / "Cabin rentals" / "Glamping" filter pages — if a province has <3 camps in this cache, don't ship a lodging filter page for that province yet.
Caveat: NOT a log of camper search queries. For real search-intent data, instrument the booking app's search bar with server-side event logging.
images / tenants / clients
images needs CDN-cached responsive sizes + alt text (LLM-generated against camp metadata). tenants/clients drive the campground-owned brand on listing pages (white-label).
bookings
Key fields: campId, siteId, userId, createdAt, checkIn, checkOut, nights, partySize, totalCents, status
Page blocks:
- Lead-time intelligence — "{Place} sites book on average {X} days out; July weekends sell out by {date}."
(checkIn - createdAt)percentiles, grouped by(province, month-of-checkin). Refresh weekly. - "Books out by" calendar — overlay per camp. Powers the urgency CTA.
- Average stay length — median
nightsper(province, season). - Party-size hints — median
partySizeper camp/region.
Privacy: aggregate only. No individual-booking surfacing.
availabilities
Page blocks:
- "Open this weekend" dynamic listing — real-time, refreshed hourly.
- "X% availability for {month}" badge per camp.
- Seasonal availability heatmap — month × availability chart on individual camp pages.
Caveat: hot read path; cache 5-15 min.
waitlists
Page block: "In demand" badge — pages where N campers are currently waitlisted. Strongest social proof Let's Camp has access to — revealed preference, not stated. Threshold ≥3 → "In demand".
pricemodels + dynamicprices + pricethresholds + seasons
Page blocks:
- Nightly price ranges by site type by season — "RV sites in Saskatchewan: $32-$48 peak, $24-$36 shoulder." Headlining stat on every province/region/filter page.
- "Most affordable in {place}" sub-list.
- "Premium / luxury in {place}" sub-list.
Methodology block (mandatory): "Based on actual prices at {N} Let's Camp campgrounds in {place}, as of {month YYYY}. Range reflects 10th-90th percentile."
longweekendrules
Niche but defensible: "Campgrounds with predictable long-weekend pricing in {place}" — camps that DON'T spike on holidays. Inverse marketing angle nobody else can do.
attractions
Page blocks:
- "Things to do near {camp}" on every camp page.
- Reverse: "Campgrounds near {attraction}" — entirely new page set (100-500 pages keyed off major Canadian attractions: Royal Tyrrell Museum, Wascana Centre, Bay of Fundy).
- "Popular activities in {region}" rolled-up tags.
Caveat: if free-text entered by owners, expect inconsistency. Cleanup pass needed.
events + eventregistrations
Page blocks:
- Per-camp upcoming events strip.
- Cross-camp event roll-ups — "Live music weekends at SK campgrounds in July 2026", "Halloween at Ontario campgrounds", "Stargazing nights at Manitoba dark-sky parks." Inherently fresh content.
eventregistrationsconfirms which events draw — prioritize by signal not guess.
addons + addOnReport
Amenity-granular filter pages: "Campgrounds with on-site firewood in {place}", "Campgrounds with e-bike rentals in {place}", "Campgrounds with propane refill in {place}".
bookingStatsViewForChart
Pre-aggregated time-series. Probably feeds an internal dashboard. Reuse for "year-over-year booking trends in {region}" content.
salesReport + bookingReport
Proof stats for operator pages:
- "Campgrounds on Let's Camp processed avg {X} bookings/site in 2025."
- "Campgrounds that switched from Campspot saw {Y}% bookings growth in their first year" — do the analysis honestly first. Wrong stats kill the comparison page's credibility.
bookingFeeReport + detailedBookingFeeReport
Campspot savings calculator — "If your campground does $X in GMV/year, you'd save $Y vs Campspot's 10% commission." Interactive widget on /campspot-alternative.
clients + bookings (joined)
Automated case-study candidate generator. Query for customers ≥18 months on platform with growing booking volume. Surface 10 candidates per quarter; marketing picks ~3 to write up.
Environment and Climate Change Canada — historical climate
Free, government-maintained, no auth. Per camp → nearest station → 30-year monthly normals (avg/min/max temp, precipitation, snow days, frost-free days).
Page blocks: historical weather graph per camp / region / city; "best camping months in {region}" pages; "warmest July destinations in BC" modifier pages.
Refresh: yearly.
OpenStreetMap — trails
Open data. Per camp → trails within X km → name, length, surface, hiking/biking/skiing tag.
Page blocks: "Trails near {camp}", and a parallel page set "Camping near the {trail name}" (West Coast Trail, La Cloche Silhouette, Bruce Trail, Cabot Trail).
AllTrails has trails; AllTrails does NOT have campground booking. Let's Camp can be the bridge.
Optional layers (defer)
Tides (DFO) · Ferry schedules (BC Ferries, Marine Atlantic) · Aurora forecast · Dark-sky designations (RASC).
No reviews collection in the booking app
Reviews are the highest-impact PSEO block. If Let's Camp isn't capturing post-stay reviews systematically, file a product ticket: 1-question post-stay email ("How was your stay at {camp}?") with optional comment, stored as reviews (campId, userId, rating, comment, createdAt, allowPublic). Even 6 months of data unlocks the most credible content block in PSEO.
blogs collection is two abandoned posts (2020 + 2021)
noindex them or delete. If "Welcome to the New Let's Camp" has historical value, migrate to blog.letscamp.ca. Don't let stale internal-app blog pages drag domain quality.
Search-bar instrumentation
Add server-side event logging to the booking app's search bar — raw query, filters applied, results count, click-through. After 90 days you'll have the single best keyword-intent signal Let's Camp can have. Small eng task, very high downstream PSEO value.
Attraction data quality
Spot-check before relying on attractions for content. If free-text entered by owners, expect inconsistency.