Skip to content

Paginate city_scores to return all 1000+ cities#3

Merged
papasoft23 merged 1 commit into
mainfrom
fix-scores-pagination
Apr 17, 2026
Merged

Paginate city_scores to return all 1000+ cities#3
papasoft23 merged 1 commit into
mainfrom
fix-scores-pagination

Conversation

@papasoft23
Copy link
Copy Markdown
Contributor

PostgREST caps results at 1000 rows per request. The city_scores MV has 1057 cities today (growing), so /api/scores and the City Ranks page were silently truncating 57 cities. Loop with .range() until the last page returns short.

🤖 Generated with Claude Code

PostgREST caps results at 1000 rows per request; the MV currently has
1057 cities (growing). Loop with .range() until the last page returns
short. City Ranks now includes every scored city instead of truncating
at 1000.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
plantspack Ready Ready Preview, Comment Apr 17, 2026 3:48pm

Request Review

@papasoft23 papasoft23 merged commit aaa10f1 into main Apr 17, 2026
2 checks passed
@papasoft23 papasoft23 deleted the fix-scores-pagination branch April 17, 2026 15:49
papasoft23 added a commit that referenced this pull request Apr 29, 2026
…evalidate

Ships items 1, 3, 4, 5 from the homepage DB-call audit.

#1 - Middleware now short-circuits on public read-only paths
(/, /vegan-places/*, /place/*, /city-ranks, /map, /blog/*, /recipe/*,
/about, /legal/*, /sitemap, /robots.txt, plus the cached /api/home,
/api/scores, /api/places/directory, /api/cities/*, /api/stats, /api/health
read endpoints). Removes one supabase.auth.getUser() round-trip per
request on the busiest part of the site (guests, bots, RSC prefetches).
JWT is 1h with refresh-token rotation, so logged-in users keep their
session - the refresh just happens on their first non-public nav.

#3 - getRecentPosts / getRecentReviews now use PostgREST count
aggregates (post_reactions(count), comments(count),
place_review_reactions(count)) instead of selecting every joined row's
id. Re-shaped to the same array-of-{id} the client expects so
HomeClient is unchanged. Cuts join payload 10-50x for active posts.

#4 - New platform_stats materialized view (single row) with
total_places, fully_vegan, restaurants, stores, stays, sanctuaries,
countries, cities. Replaces 3 queries on every /api/home cache miss
(177-row directory_countries scan + directory_cities head count +
sanctuaries count) with one MV lookup. Wired into existing
refresh_directory_views() so the nightly cron keeps it fresh.

#5 - Homepage revalidate 60s -> 300s, aligned with /api/home edge
cache. Cuts homepage SSR rate ~5x; recent posts/reviews can be up to
5min stale but mutations call revalidatePath('/') when needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
papasoft23 added a commit that referenced this pull request Apr 29, 2026
…, edge cache, prefetch trim

Ships items 1-5 from the post-homepage audit.

#3 - migration 20260429220000: partial composite indexes on places
for the archived_at IS NULL hot path
(idx_places_active_country_city, idx_places_active_category,
idx_places_active_vegan_level, idx_places_active_country_city_rating).
The previous idx_places_archived_at was filtered the wrong way
(WHERE archived_at IS NOT NULL), so directory and admin queries
fell back to filter-after-index. Speeds nearly every read query.

#1 - GET /api/cities/followed no longer fires a write on every
read. Side-effect was breaking CDN ETag/304 caching and hammering
Supabase. Added POST /api/cities/followed/seen as the explicit
mark-seen endpoint, ready to wire from the city-page render path
when delta tracking is brought back.

#2 - admin/data-quality PUT actions:
  - verify_vegan: DB writes parallelised with Promise.all instead
    of awaiting each row. ~50x faster RTT on a 50-place batch.
  - dismiss/removeTag: replaced the per-row update loop with a
    single bulk_remove_place_tag RPC (migration 20260429230000).
    One atomic update, no half-applied dismisses on partial failure.

#4 - prefetch={false} on the dense card grids that drove RSC
prefetch fan-out: HomeClient nearby places, top-cities tiles,
activity feed items, page.tsx Featured Places, and CityRanksTable's
popular-cities row, sortable table, and paginated card list.
Hover-walks no longer blow through Vercel function quota.

#5 - /api/home now sets a public, s-maxage=300, swr=3600 header on
the cookieless payload (identical for every guest, safe to dedupe
at the CDN) and keeps the per-user header private. Prevents
cold-cache stampedes on traffic spikes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
papasoft23 added a commit that referenced this pull request Apr 30, 2026
… overrides

#1 Czechia -> Czech Republic merge: 326 active rows renamed via
scripts/_merge-czechia.ts so the upcoming OSM CZ import lands under
one canonical country name. Side discovery: refresh_directory_views
was failing because platform_stats can't be REFRESHed CONCURRENTLY
through a constant-expression unique index. Migration
20260430000000 switches it to plain REFRESH (1-row MV, lock is
microseconds). MV state verified: Czech Republic = 326, Czechia = 0.

#2 Policy block: BLOCKED_COUNTRIES = {RU, BY} exported from
scripts/lib/place-pipeline.ts (the tracked layer). Used by
import-osm-countries.ts (gitignored runner) for an explicit
fail-loud check. Defense-in-depth - neither code is in COUNTRY_NAMES
today, but if added later the policy block still fires.

#3 Denmark CITY_OVERRIDES added so OSM addr:city -> canonical:
- 'kobenhavn'/'koebenhavn'/'kobenhavn' -> Copenhagen (was producing
  the diacritic-stripped Danish form on first pass)
- 'tonder kommune'/'tonder kommune' -> Tonder (strips municipality
  suffix the way the existing Belgian and Dutch entries do)

Coverage audit scripts retained for future reuse:
- _audit-coverage.ts: bucket countries by current place_count
- _audit-osm-gap.ts + _audit-osm-gap-2.ts: probe Overpass per-ISO
  for the OSM-vs-ours gap
- _audit-denmark-detail.ts: full per-city dedup preview for one
  country
- _merge-czechia.ts + _refresh-mvs-individually.ts: pre-flight tools

Denmark dry-run after all overrides: 290 OSM raw -> 247 net new
after source_id dedup (36 already imported) + chain filter (6).
Top cities: Copenhagen 146, Aarhus 16, Aalborg 15, Vejle 9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
papasoft23 added a commit that referenced this pull request May 6, 2026
#1 Duplicate-slug dedup
- scripts/seo-1-find-dupes.ts groups places by name+city+country and
  flags location duplicates (coords <=80m). Keeper picked by vegan-tier
  > richness > unsuffixed-slug > age. Never archives a higher vegan-tier
  in favor of a lower one.
- scripts/seo-1-archive-dupes.ts moves reviews/favorites/pack_places
  from loser to keeper, inserts the loser's slug into
  place_slug_aliases for a 301 redirect, and archives the loser with
  archived_reason=duplicate_of:slug.
- Applied: 118 archived, 118 redirects, 0 user-content losses.

#2 City-page descriptions
- vegan-scene-descriptions.ts grows the generator with 4 new
  deterministic signals: verified vs community-tracked split,
  mostly-vegan callout, notable spots sentence, hours-coverage pct.
  No paid LLM. Per-city wording varies because the underlying numbers
  vary.
- City page now passes verification_level, mostly_vegan count, website
  and hours coverage, and a quality-ranked top-picks list.

#3 Indexation hygiene
- Place page: robots noindex,follow when vegan_level NOT in
  (fully_vegan, mostly_vegan) AND no description (>=50ch) AND no image
  AND no website AND no hours. Audit: 3,657 of 52,627 active places
  (~7%) qualify; all fully/mostly-vegan rows preserved.
- City page: robots noindex,follow when fewer than 5 places. Audit:
  8,663 of 10,007 cities (86.6%).
- Sitemap aligned with both predicates: thin.xml emits zero place URLs,
  priority.xml skips cities with <5 places, placeTier classifier
  promotes any vegan-tier or has-website row above 'thin'.

OSM backfill (initial pass)
- scripts/seo-4-osm-backfill.ts re-queries Overpass for places where
  source LIKE 'osm%' and source_id parses as osm-{node|way|relation}-N.
  Only writes empty fields. Self-rate-limits.
- Initial 2K-candidate pass yielded 4 updates (2 desc, 1 web, 1 phone)
  - typical OSM nodes for vegan venues have minimal tag coverage.
papasoft23 added a commit that referenced this pull request May 13, 2026
docs/SEO-PLAN-2026-05.md — top 5 SEO improvements ranked by leverage:
  1. Auto-generated city-page intros for every city with >=5 places
  2. Programmatic "Best vegan [category] in [city]" listicle pages
  3. Country-page editorial intros (matches summer-hub treatment)
  4. FAQ + PAA schema on top-N city pages
  5. Internal linking pass: place -> city category -> country
  Each item: leverage analysis, implementation steps, effort estimate,
  expected impact. Ordered for execution sequence.

docs/blog-drafts/ — 5 blog post drafts, 60-80% written, with YAML
frontmatter (slug, target queries, title tag, meta description,
internal-link plan):
  01 Germany tier-2 cities comparison (gated on audit)
  02 Lisbon vs top 5 European vegan cities
  03 How we verify "fully vegan" (methodology / trust play)
  04 24 accidentally vegan Mediterranean staples (evergreen, no blocker)
  05 Why we don't take paid listings

docs/blog-drafts/README.md — priority + publish cadence:
  this week: #4 (no blockers, highest evergreen value)
  week 2: #5 (trust-build, supporter conversion)
  week 3: #3 (methodology, authority signal)
  week 4: #2 (Lisbon comparison, needs live numbers)
  month 2: #1 (after Germany audit lands)

Note on GSC: the GSC/ folder was empty (.DS_Store only), so the SEO
plan is grounded in platform knowledge + the summer-hub audit from
the same day, not real query data. The plan notes this and can be
sharpened once a CSV is added to GSC/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
papasoft23 added a commit that referenced this pull request May 14, 2026
Two pieces, both shipping now.

1. New endpoint: GET /api/export/summer-hub

Mirrors the shape of /api/export/places/<country>: returns every
vegan place across all 29 summer-hub destinations in one JSON
response. Useful for offline analysis, downstream tooling, or
caching as a static JSON.

Response shape:
  { exported_at, hub, totals, by_city: [...], places: [...] }

Each by_city entry includes count, fully_vegan_count, and
verified_fully_vegan_count alongside the per-city places list.
Cache: s-maxage=300, swr=3600.

2. Place page: "Other vegan cities in {country}" SSR block

GSC plan item #5 from docs/GSC-ACTIONS-2026-05-14.md. The
indexed/discovered ratio is currently 3.9% (3,847 / 98,399); the
bottleneck is crawl-budget, not content. Densifying internal
linking on the highest-volume route in the site (place pages)
gives Google more reason to crawl deeper.

Each place page now SSR-renders 3 sibling city links from the
same country (top 3 by place count, min 5 places per city). Pairs
with the existing "More places in {city}" sibling block: every
place page is now a 3-way crawl hub linking to its city, 3 sibling
cities, and the country page.

Audit findings recorded for visibility:
- Thin place pages already have noindex meta + are excluded from
  the sitemap via placeTier(). GSC plan #1 is structurally
  satisfied — no further sitemap slim needed.
- Strict-thin places (no description, no image, no review, no
  vegan_level signal, no website) = 30 rows total across 52,574
  alive places. Already non-indexable.

Remaining GSC plan items (#3 canonical, #4 404 redirects) need
the explicit URL lists from GSC to act on safely; they're not
in the export, so deferred until the next sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant