
filter-system-dev
by Esdeveniments
Esdeveniments.cat és una iniciativa per veure de manera fàcil i ràpida tots els actes culturals que es fan a Catalunya.
SKILL.md
name: filter-system-dev description: Guide for working with the configuration-driven filter system. Use when adding/modifying filters, updating URL parsing, debugging filter-related issues, or working with config/filters.ts. license: MIT
Filter System Development Guide
This skill helps you navigate the most complex domain logic in this codebase: the configuration-driven URL-first filtering system for event discovery.
When to Use This Skill
- Adding a new filter (e.g., price, accessibility, venue type)
- Modifying existing filter behavior (dependencies, special cases)
- Debugging URL parsing or canonicalization issues
- Understanding filter removal chains
- Working with
config/filters.tsorutils/filter-operations.ts - Implementing filter UI components
Core Architecture
The filter system has one canonical source of truth: config/filters.ts
All filter logic flows through FilterOperations class which auto-integrates any filter added to the config.
Key Files
config/filters.ts- Single source of truth (ADD NEW FILTERS HERE)utils/filter-operations.ts- Auto-integration logic (DO NOT modify for new filters)utils/url-filters.ts- URL building (uses FilterOperations)utils/url-parsing.ts- URL parsing (uses FilterOperations)test/filter-system.test.ts- Validation tests
IMPORTANT: Do NOT create a utils/filter-config.ts file. All filter logic must go through config/filters.ts.
Filter Configuration Anatomy
{
key: 'filterName', // Unique identifier (camelCase)
defaultValue: 'all', // Default state (omitted from URL)
isEnabled: (filters) => true, // Conditional enablement
getDisplayText: (value) => getLabel(value), // Human-readable text
getRemovalChanges: (value, segments, queryParams) => ({
// What to change when filter is removed
queryParams: { ...queryParams, filterName: undefined }
}),
dependencies: ['lat', 'lon'], // Optional: filters this depends on
specialCases: { // Optional: custom redirect logic
shouldRedirectToHome: (filters) => ...
}
}
Step-by-Step: Adding a New Filter
1. Define Filter Configuration
Add to config/filters.ts in the FILTER_CONFIGURATIONS array:
{
key: 'price',
defaultValue: 'all',
isEnabled: (filters) => true,
getDisplayText: (value) => {
const labels = {
all: 'Tots els preus',
free: 'Gratuït',
paid: 'De pagament',
};
return labels[value] || value;
},
getRemovalChanges: (value, segments, queryParams) => ({
queryParams: { ...queryParams, price: undefined }
}),
}
2. Auto-Integration (No Action Needed)
Once in config/filters.ts, FilterOperations automatically provides:
getAllConfigurations()- includes your filtergetConfiguration('price')- retrieves configgetRemovalUrl('price', ...)- builds removal URLisFilterActive('price', ...)- checks active stategetDefaultValue('price')- returns default
No manual wiring needed in utils/filter-operations.ts!
3. URL Strategy Decision
Choose where your filter lives in the URL:
Path Segment (e.g., /barcelona/avui/musica)
Use when:
- Core filtering dimensions (place, date, category)
- Important for SEO
- Limited set of values
- Omitted when default for canonical URLs
Query Parameter (e.g., ?search=jazz&distance=10)
Use when:
- User input (search term, location)
- Optional filters (distance, accessibility)
- Many possible values
- Omitted when default to reduce URL bloat
Decision tree for your filter:
- Is it core to SEO? → Path segment
- Is it user-driven input? → Query param
- Does it have limited values? → Path segment
- Is it optional enhancement? → Query param
4. Implement Dependencies
If your filter depends on others:
{
key: 'distance',
defaultValue: 50,
dependencies: ['lat', 'lon'], // Distance requires location
getRemovalChanges: (value, segments, queryParams) => ({
queryParams: {
...queryParams,
distance: undefined,
lat: undefined, // Remove dependencies too!
lon: undefined,
}
}),
}
Critical: When removing a filter, always remove its dependencies to avoid invalid states.
5. Special Cases (If Needed)
For complex redirect logic:
{
key: 'category',
// ...
specialCases: {
shouldRedirectToHome: (filters) =>
filters.byDate &&
filters.place === 'catalunya' &&
filters.category === 'tots'
}
}
This handles edge cases like "all categories in all of Catalunya by date" → redirect to home.
6. Run Validation Tests
yarn test test/filter-system.test.ts
If tests fail:
- Check that your
defaultValuematches expected behavior - Verify
getRemovalChangesreturns correct shape - Update test expectations if you intentionally changed behavior
Common Pitfalls
❌ Encoding Default Values in URLs
// WRONG: Includes default 'all' in URL
const url = `/barcelona?price=all`;
// CORRECT: Omits default (canonical URL)
const url = `/barcelona`; // price defaults to 'all'
Fix: Use buildCanonicalUrlDynamic which auto-omits defaults based on FILTER_CONFIGURATIONS.
❌ Forgetting Dependent Filters
// WRONG: Only removes distance
getRemovalChanges: (value, segments, queryParams) => ({
queryParams: { ...queryParams, distance: undefined },
});
// CORRECT: Removes distance + dependencies
getRemovalChanges: (value, segments, queryParams) => ({
queryParams: {
...queryParams,
distance: undefined,
lat: undefined, // Dependency
lon: undefined, // Dependency
},
});
Why: Leaving lat/lon without distance creates invalid state.
❌ Duplicating Logic in Multiple Files
DO NOT add filter-specific logic to:
utils/url-parsing.ts(usesFilterOperations.getAllConfigurations())utils/url-filters.ts(usesFilterOperations.getConfiguration())- Individual page components (import
FilterOperations)
Single source of truth: config/filters.ts → everything else reads from it.
❌ Using Legacy Filter Patterns
// WRONG: Creating separate filter config files
// Do NOT create files like: utils/filter-config.ts
export const PRICE_FILTER = { ... }
// CORRECT: Add to canonical config
// File: config/filters.ts
export const FILTER_CONFIGURATIONS = [
{ key: 'price', ... }
]
Note: Never create standalone filter config files. All filter configurations must go in config/filters.ts.
❌ Creating /tots/ URLs
// WRONG: Includes 'tots' in canonical URL
const url = `/barcelona/tots/musica`;
// CORRECT: Omits 'tots' when it's a default
const url = `/barcelona/musica`; // date defaults to 'tots'
SEO impact: /tots/ segments bloat URLs and dilute SEO value. Canonical URLs omit defaults.
URL Canonicalization Rules
Critical for SEO and avoiding $300 cost spikes (see Section 12 in AGENTS.md):
-
Default omission:
date === 'tots'ANDcategory === 'tots'→/placedate === 'tots'andcategory !== 'tots'→/place/categorydate !== 'tots'andcategory === 'tots'→/place/date
-
Query params:
- Only for:
search,distance,lat,lon - Omit
distancewhen default (50)
- Only for:
-
Legacy redirects (handled by middleware):
?category=X&date=Y→/place/date/category(301)/place/tots/category→/place/category(301)
NEVER read searchParams in app/[place]/* pages - causes DynamoDB explosion (ISR caching per unique URL+query).
Example: Adding a "Price" Filter
See examples/add-price-filter-example.md for full walkthrough.
Quick summary:
- Add config to
config/filters.ts - Create UI component reading
FilterOperations.getConfiguration('price') - Build URLs with
buildCanonicalUrlDynamic({ price: 'free' }) - Test with
yarn test test/filter-system.test.ts
Testing Your Filter
Unit Tests
yarn test test/filter-system.test.ts
Validates:
- Filter configuration structure
- Default value handling
- Removal URL generation
- Active state detection
E2E Tests
yarn test:e2e e2e/filters.flow.spec.ts
Validates:
- URL construction in real navigation
- Filter UI interactions
- Canonical URL redirects
Manual Testing Checklist
- Filter defaults to correct value
- Removing filter returns to canonical URL
- Dependencies removed when filter removed
- URL omits default values
- No
/tots/segments in canonical URLs - Special cases trigger correct redirects
Filter UI Patterns
Reading Filter State
import { FilterOperations } from "@utils/filter-operations";
const config = FilterOperations.getConfiguration("price");
const isActive = FilterOperations.isFilterActive("price", currentFilters);
const displayText = config.getDisplayText("free");
Building Removal URL
const removalUrl = FilterOperations.getRemovalUrl(
"price",
segments, // { place, date, category }
queryParams // { search, distance, lat, lon }
);
Checking if Filter Enabled
const config = FilterOperations.getConfiguration("price");
const enabled = config.isEnabled(currentFilters);
Integration with API Layer
Filters must be translated to API query params:
// Client-side filter state
const filters = { price: "free", place: "barcelona" };
// API query building (uses buildEventsQuery from utils/api-helpers.ts)
const query = buildEventsQuery({
place: filters.place,
price: filters.price === "free" ? 0 : undefined, // API expects numeric
});
Important: Filter keys in UI may differ from API param names. Map them in buildEventsQuery.
Resources
Templates
- filter-config-template.ts - Boilerplate for new filters
Examples
- add-price-filter-example.md - Complete walkthrough
Reference Files
config/filters.ts- Canonical source of truthutils/filter-operations.ts- Auto-integration logicutils/url-filters.ts- URL buildingtest/filter-system.test.ts- Test suite
FAQ
Q: Should my filter be a path segment or query param?
A: Path segment if core/SEO-important, query param if optional/user-input.
Q: Do I need to modify filter-operations.ts?
A: No. Just add to config/filters.ts and everything auto-integrates.
Q: What if my filter depends on another?
A: Add dependencies: ['filterKey'] and include removals in getRemovalChanges.
Q: How do I test my filter?
A: Run yarn test test/filter-system.test.ts for unit tests, yarn test:e2e e2e/filters.flow.spec.ts for E2E.
Q: Why does URL building omit my filter?
A: It matches your defaultValue. This is correct for canonical URLs.
Q: Can I override URL building for my filter?
A: No. Canonical URL building is centralized for SEO consistency. Special cases go in specialCases config.
Troubleshooting
Filter not appearing in UI
- Check
isEnabledreturnstruefor current filter state - Verify UI component imports
FilterOperations.getConfiguration('yourFilter') - Confirm filter key matches exactly (case-sensitive)
URL includes default value
- Check
defaultValuein config matches the value you're seeing - Verify you're using
buildCanonicalUrlDynamic(not manual concatenation) - Ensure
getRemovalChangessets filter toundefined(not default value)
Filter removal doesn't work
- Check
getRemovalChangesreturns correct shape - Verify dependencies are also removed
- Test removal URL with
FilterOperations.getRemovalUrl()
Tests failing after adding filter
- Update test expectations if you intentionally changed behavior
- Verify config structure matches schema in tests
- Check that default value is handled correctly
Last Updated: January 15, 2026
Maintainer: Development Team
Related Skills: url-canonicalization, api-layer-patterns
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon