Pond Routing & Views Overhaul — Draft Plan (v0.1)
Goal: tame chaos, make URLs predictably map to a scope (geo level) + a view (feature), with safe defaults, hard guardrails, and modular implementations that don’t step on each other.
1) Core concept
URL = [Scope] + [View]
- Scope (pre‑path):
/ca/{province}/{city?}/{community?} - View (post‑path):
/{view}whereview ∈ ALLOWED_VIEWSand defaults to/indexif omitted.
Examples:
/ca/ab/calgary/sunnyside/forums→ forums at community scope/ca/ab/calgary/projects→ projects at city scope/ca/ab/businesses→ businesses at province scope (guarded + paginated + filterable)/ca/ab/calgary/sunnyside→ same as/ca/ab/calgary/sunnyside/index
Why this helps: Clean separation of concerns, simpler mental model, views become independent modules; cross‑module impact is reduced to routing + contracts.
2) View registry
Define a central, strict registry of allowed view keys with metadata for access, caching, and required filters.
Initial set (your list, normalized):
index(default hub) — overview, quick links, stats, map thumbnailforums— forum split‑view (D2/D3) loaderflightplan— projects/initiatives with timeline (city/community relevant)projects— alias toflightplanat city/province scopebusinesses— business directory (requires filters at ≥city scope; hard guards at province)nonprofits— nonprofit directoryreligious_societies— faith orgs directory (respect current taxonomy name)education— schools + related education entitiesqr— QR assets and community/business shareableslist— generic list/indexer (aliasindexif omitted)
Nice‑to‑add (future):
events(calendar)services(municipal services index)alerts(notices/outages)map(full interactive map with layers)stats(public metrics; paid tiers unlock more detail)news(ingest + curated local feed)people(community association reps / public officer directory)about(static info; owner/association profile)
All views must declare:
id, label, allowed_scopes, min_filters, max_page_size, default_sort, cache_ttl, feature_flags.
3) Technical feasibility & routing design (Drupal 11)
Route pattern
/ca/{province}/{city?}/{community?}/{view?}
viewdefaults toindex- Parameter converters for
{province, city, community}map to canonical records (slug → ID). 404 if invalid. - A single Front Controller dispatches to a
ViewHandlerInterfacebased onview.
Module layout
cdk_geo_routing— front controller + param converters + access checks + scope context servicecdk_view_registry— static map or config entities that define allowed viewscdk_view_{viewKey}— small feature modules that implementViewHandlerInterface- Shared libs:
cdk_search,cdk_pagination,cdk_cache,cdk_scope_guard
Controller contract
interface ViewHandlerInterface {
public function build(Scope $scope, Request $request): array; // returns render array
public function requirements(): ViewRequirements; // filters, pagination, perms
}
Scope object
class Scope { public ?Province $province; public ?City $city; public ?Community $community; }
Access & ownership
- Read‑only public by default; enhanced datasets require roles (via SSO claims).
- Community Association role → extra blocks on
index, editable panels, QR admin, etc.
4) Guardrails to prevent meltdowns
A. Mandatory filters at wide scopes
- At province scope, heavy views (
businesses,nonprofits,education) require at least one of:- category/type filter, or
- search query, or
- geographic sub-filter (city pick)
- If missing → show filter UI and refuse to run full table scan.
B. Hard caps & cursor pagination
- Use keyset (cursor) pagination only. Forbid page=N offset pagination on large tables.
- Enforce
max_page_sizeper view and per scope (e.g., 50 at city, 25 at province).
C. Query timeouts & circuit breaker
- Per‑view DB timeout (e.g., 1.5s). On timeout → degrade to summary cards + prompt to refine.
D. Caching & precomputed views
- Pre‑aggregate counts per scope nightly (businesses per community, etc.).
- Cache HTML fragments per scope+filter key for
cache_ttl(e.g., 5–15 min), tag by entity.
E. Scope guard middleware
- Middleware before handlers:
- Validate
view∈ registry - Validate scope exists
- Check requirements (filters/perms) → else short‑circuit with UX guard page
- Validate
5) index view (the hub)
Purpose: the one safe default that always works.
- Hero: Name, breadcrumbs, description, parent links
- Quick tiles to the other views, with pill counts (precomputed): Businesses, Schools, Forums, Projects, Nonprofits, etc.
- Mini map (static image or light Leaflet)
- Recent activity stream (forums/posts)
- Admin/Association panel (for authorized users): edit links, QR tools, claim page
Design rule: If nothing else renders, index must still render.
6) View sketches
forums
- Use existing split‑view controller. Respect scope → auto‑select taxonomy children for that scope.
- SEO canonical to D3 topic routes when deep.
flightplan / projects
- Card grid of active projects at city scope; community scope shows subset (within geometry bounds).
- Timeline component (Recharts) + status chips.
businesses
- Facets: category (NAICS), status, tags; geo filter pre‑applied from scope
- Keyset pagination; precomp counts; list + map toggle
- Province scope requires filter(s); show guard page otherwise
nonprofits / religious_societies / education
- Same list pattern as businesses; shared component library
qr
- Public download of QR for the scope page; admin block to regenerate styles/logos
list
- Thin generic listing when we don’t have a specialized handler yet
7) Breadcrumbs, canonicals, and pathauto
- Canonical for a scope without view →
/index - Add
<link rel="canonical">to/indexwhen the view is omitted - Pathauto stays on canonical scope paths; views are explicit segments
- Consistent breadcrumbs: Country → Province → City → Community → View
8) Performance & data plumbing
- Move all heavy listings to read models (materialized views) with selective refresh
- Add supporting GIN/GIST indexes for text + geo (pg_trgm, postgis) where used
- Shared
QueryBuilderthat whitelists orderings/filters per view - Strict DTOs to prevent accidental
SELECT *
9) Security & roles
- Public read by default; paid tiers unlock aggregation above community
- Feature flags per view for experimental rollouts
- All handlers respect SSO roles propagated from Core (via your existing
/api/user/permissions/{username})
10) Error handling & UX fallbacks
- Friendly empty‑state cards with next steps (add filters, pick city, etc.)
- Never raw 500s; convert into guarded UX with incident code and drupal watchdog link for admins
11) Implementation roadmap
Phase 0: Triage & stabilize (1–2 days)
- Freeze config changes; export current state
- Re‑enable minimal
indexfor all scopes (hard‑coded if needed) - Add temporary 302s from broken routes to their
/index
Phase 1: Routing spine (Front Controller)
- Build
cdk_geo_routingroute + converters - Implement
Scopeservice andScopeGuardMiddleware - Implement
ViewRegistry(hardcoded array → config entity later)
Phase 2: First handlers
index,forums,projects|flightplan- Add guard UIs; add precompute job for counts
Phase 3: Directories
businesses,nonprofits,education,religious_societies- Facets, keyset pagination, province‑scope guards
Phase 4: Nice‑to‑haves
events,map,stats,alerts,people,about
Phase 5: Hardening
- Caching strategy, canary dashboards, query timeouts, budget tests
12) Minimal code stubs (orientation)
Routing YAML
cdk.geo.view:
path: '/ca/{province}/{city?}/{community?}/{view?}'
defaults:
_controller: '\\Drupal\\cdk_geo_routing\\Controller\\ViewFrontController::dispatch'
view: 'index'
requirements:
_permission: 'access content'
options:
parameters:
province: { type: 'cdk_province' }
city: { type: 'cdk_city' }
community: { type: 'cdk_community' }
Front Controller (pseudo)
public function dispatch(Province $p=null, City $c=null, Community $cm=null, string $view='index', Request $req): array {
$scope = new Scope($p,$c,$cm);
$this->guard->assert($scope, $view, $req);
$handler = $this->registry->get($view);
return $handler->build($scope, $req);
}
Registry (hardcoded v0)
private const VIEWS = [
'index' => ['scopes'=>['province','city','community'], 'max_page'=>0],
'forums' => ['scopes'=>['city','community']],
'projects' => ['scopes'=>['province','city','community']],
'flightplan' => ['aliasOf'=>'projects'],
'businesses' => ['scopes'=>['city','community','province'], 'min_filters'=>['province'=>1]],
'nonprofits' => ['scopes'=>['city','community','province'], 'min_filters'=>['province'=>1]],
'education' => ['scopes'=>['city','community','province'], 'min_filters'=>['province'=>1]],
'religious_societies' => ['scopes'=>['city','community','province'], 'min_filters'=>['province'=>1]],
'qr' => ['scopes'=>['city','community','province']],
];
13) Testing & observability
- Route unit tests for dispatch & invalid combos
- Query budget tests (per view + scope)
- Watchdog counters for guard hits, timeouts, cache hit ratio
- Synthetic monitors for
/indexat each scope level
14) Migration & cleanup
- Deprecate legacy view routes; add 301 to new schema
- Centralize templates under each
cdk_view_*module - Remove DB‑coupled views UI where replaced by code
15) Open questions
- Do we prefer
projectsorflightplanas canonical? - Confirm taxonomy alignment for
religious_societiesnaming. - Province‑wide listing caps — what’s the acceptable max (e.g., 10k cursor window per query/key)?
- Geo logic for projects at community level — use polygons or postal‑code joins first?
- Paid tier gates for aggregation: expose as separate routes or feature‑flag blocks?
16) Decision recap
- Adopt Scope + View URL model with
/indexdefault. - Build a front controller + registry + per‑view modules.
- Enforce guardrails (mandatory filters, cursor pagination, precomputed counts, timeouts, cache).
- Ship in phases: stabilize → routing spine → key views → directories → hardening.
17) Locked decisions from Daryl (2025‑10‑31)
- Canonical name:
flightplanis canonical for idea/workspace stage.projectsis a separate view for approved/committed initiatives. - Religion taxonomy: keep
religious_societiesfor now; we’ll query actual taxonomy later. - Listing caps: no province‑wide firehoses. Province‑scope directory URLs either (a) show a “choose a municipality” interstitial, or (b) render a guard page that requires filters. We do not surface province‑wide directory links on hubs.
- Geospatial: aim for polygons (parks, etc.) but defer implementation—postal codes don’t map well to parks or multi‑community assets.
- Paid tier gating: unlocks appear as add‑on blocks (callouts) on pages the user already visits; not separate routes for now.
18) View Registry v0.2 (with nice‑to‑haves enabled)
key
label
allowed_scopes
notes
index
Hub
province, city, community
Default; always renders
forums
Forums
city, community
Split‑view, taxonomy‑aware
flightplan
Flightplan (ideas/workspace)
province, city, community
Multi‑option proposals; timelines; voting hooks
projects
Projects (approved/committed)
province, city, community
Milestones, budget, status; read‑only to most users
businesses
Businesses
city, community, province*
*Province scope requires narrowing (interstitial)
nonprofits
Nonprofits
city, community, province*
Same guard as above
religious_societies
Religious societies
city, community, province*
Same guard as above
education
Education
city, community, province*
Schools, programs
qr
QR tools
province, city, community
Public download + admin block
events
Events/Calendar
province, city, community
ICS import hooks; upcoming/past toggles
services
Municipal services
province, city, community
Service directories, service areas
alerts
Notices/alerts
province, city, community
Time‑boxed banners; RSS/atom ingest
map
Interactive map
province, city, community
Leaflet layers; heavy → feature‑flag
stats
Public metrics
province, city, community
Precomputed tiles; paid unlocks more
news
Local news
province, city, community
Ingest + curation; canonical links
people
Reps/contacts
province, city, community
Assoc. reps; public officers
about
About/owner profile
province, city, community
Static metadata + links
19) Redirect & guard rules (finalized)
- Explicit interstitial for province directories: visiting
/ca/{prov}/{view}forbusinesses|nonprofits|education|religious_societies→ render municipality chooser (+ search) instead of running a query. Provide breadcrumbs and fast city search; only after a city is picked do we hit data. - Manual URL entry still lands on the interstitial; hub pages omit province‑wide directory links entirely.
- Guard pages show when required filters are missing (scope or facets); never attempt a full scan.
20) Data model sketch (flightplan vs projects)
flightplan (workspace; many proposals under one need)
flightplans(id, scope_type, scope_id, title, need_slug, description, created_by, status)flightplan_options(id, flightplan_id, summary, funding_model, builders_json, geometry, est_cost, pros_cons_json, vote_state)flightplan_activity(id, flightplan_id, actor_id, action, meta_json, ts)
projects (approved/committed)
projects(id, scope_type, scope_id, title, authority, budget, status, start_date, end_date, geometry, parent_flightplan_id NULL)project_milestones(id, project_id, title, due_date, done, notes)
Geometry can be
GEOMETRY(POLYGON|MULTI*)later; for now allow NULL and fall back to city/community containment.
21) Paid tier blocks (UX contract)
- Blocks advertise locked functionality with real numbers (e.g., “12,341 city‑wide responses—unlock municipal view”).
- Visibility via SSO claims →
cdk_paid_tier >= CityAggregate. - Same route; blocks add value without fragmenting URLs.
22) Implementation checklist (next 5–7 days)
Phase 0: Stabilize
- Freeze config; export.
- Force
/indexfallback on all scope routes. - Temporary 302s from known‑bad routes →
/index.
Phase 1: Routing spine
-
cdk_geo_routingfront controller + converters. -
ScopeGuardMiddlewareenforcing registry + guards. -
cdk_view_registryv0.2 with all views above.
Phase 2: Core handlers
-
index(hub) with tiles + precomputed counts. -
forumswired to existing split‑view. -
flightplan(workspace) base screens. -
projects(approved) read model + cards.
Phase 3: Directories + Interstitial
- Interstitial municipality chooser for province scope.
-
businesses|nonprofits|education|religious_societieslist handlers with keyset pagination.
Phase 4: Nice‑to‑haves
-
events,services,alerts,map,stats,news,people,about(behind feature flags where heavy).
Phase 5: Hardening
- Query timeouts, cache TTLs, metrics, synthetic monitors.
23) Code stubs to add
Province interstitial route (pseudo)
if ($scope->province && !$scope->city && in_array($view,['businesses','nonprofits','education','religious_societies'])) {
return $this->builder->municipalityChooser($scope->province);
}
Paid block visibility (YAML)
context_definitions:
cdk_paid_tier: { type: integer }
visibility:
user_has_tier:
id: cdk_paid_tier_min
settings: { min: 2 } # e.g., 2 = CityAggregate
Keyset pagination contract
GET ...?after=eyJpZCI6MTIzLCJuYW1lIjoiQm9iIn0&limit=50
24) Open items (tracked)
- Taxonomy audit for
religious_societiesnaming. - Polygons: choose source datasets + containment strategy.
- Decide which nice‑to‑haves ship behind feature flags initially.
- Analytics: define guard hit metrics + conversion funnel for paid blocks.