Add design doc for type-ahead search feature
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
# Type-Ahead Search for Roux
|
||||
|
||||
Date: 2026-05-19
|
||||
|
||||
## Overview
|
||||
|
||||
Add a search box to the recipe index page that filters recipes by title or filename as the user types. The index page will be client-rendered from embedded JSON data, with progressive enhancement via a small vanilla JS script.
|
||||
|
||||
## Motivation
|
||||
|
||||
As the recipe collection grows, finding a recipe by scanning an alphabetical list becomes tedious. A type-ahead search box provides instant filtering with no page reloads.
|
||||
|
||||
## Scale Assumption
|
||||
|
||||
The recipe collection is assumed to be small — hundreds, at most ~1000 recipes. All data is embedded in the page at load time. No pagination or server-side search queries are needed.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Data flow
|
||||
|
||||
1. **Server** (Haskell / `Html.hs`) renders the index page shell — navbar, search `<input>` element, and two JSON blobs in `<script type="application/json">` tags.
|
||||
2. **No server-rendered recipe list**. The `ul.roux-recipes` and all recipe content is rendered entirely by JS from the embedded JSON.
|
||||
3. **JS** (inline `<script>`, vanilla, no dependencies) reads the JSON on page load, renders the initial recipe list, and re-renders on search input or sort-mode change.
|
||||
|
||||
### Progressive enhancement
|
||||
|
||||
Without JavaScript the page shows the navbar, search input, and a message like "Enable JavaScript to browse recipes." This is acceptable for a personal/family site where JS availability is assumed, but the shell still provides useful navigation.
|
||||
|
||||
If server-side rendering is desired later, the server can continue to render the list, and JS can use the embedded JSON for client-side filtering only.
|
||||
|
||||
### Routes / Hash-based sort mode
|
||||
|
||||
- Sort mode is stored in the URL hash:
|
||||
- `#alpha` (default) — alphabetical list
|
||||
- `#tags` — grouped by tag
|
||||
- `#course` — grouped by course
|
||||
- JS reads the hash on load and on `hashchange` events.
|
||||
- Sort links in the navbar set the hash (e.g., `<a href="#tags">By Tag</a>`).
|
||||
- This makes sort modes linkable and bookmarkable without server round-trips.
|
||||
|
||||
The existing server routes `GET /`, `GET /sorted/tags`, and `GET /sorted/course` all render the same shell page (same HTML, same JSON, same JS). The JS reads the hash to initialise the sort mode. Visiting a server route without a hash defaults to `#alpha`. The server-side route distinction is no longer needed; the three endpoints can serve identical content.
|
||||
|
||||
## JSON Payload
|
||||
|
||||
### Recipe data
|
||||
|
||||
Embedded in `<script type="application/json" id="roux-recipe-data">`:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": "Carrot Risotto",
|
||||
"filename": "carrot-risotto.cook",
|
||||
"course": "Main Course",
|
||||
"tags": ["italian", "vegetarian", "risotto"]
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
Fields:
|
||||
- `title` — display title from recipe metadata (or filename fallback)
|
||||
- `filename` — recipe file path, used for link URL
|
||||
- `course` — optional course/category from metadata
|
||||
- `tags` — list of tags from metadata (empty array if none)
|
||||
|
||||
### Error data
|
||||
|
||||
Embedded in `<script type="application/json" id="roux-errors-data">`:
|
||||
|
||||
```json
|
||||
[
|
||||
{"title": "Broken Recipe", "filename": "broken.cook"}
|
||||
]
|
||||
```
|
||||
|
||||
Rendered at the bottom under an "Unparseable recipes" heading, matching current server-rendered behavior.
|
||||
|
||||
## Client-Side Rendering (JS)
|
||||
|
||||
### Initialization
|
||||
|
||||
1. Parse JSON from script tags.
|
||||
2. Read `location.hash` to determine sort mode (default: `#alpha`).
|
||||
3. Render the recipe list.
|
||||
4. Attach event listeners: `input` on search box, `hashchange` on window.
|
||||
|
||||
### Search behavior
|
||||
|
||||
- Search matches against `title` and `filename`, case-insensitive (simple `includes` after `toLowerCase()`).
|
||||
- Search input is cleared on page load.
|
||||
- When search text is non-empty:
|
||||
- Recipes are filtered to matches.
|
||||
- List is always rendered as flat alphabetical (grouping suppressed).
|
||||
- When search text is empty:
|
||||
- Recipes render according to the current sort mode.
|
||||
|
||||
### Sort modes
|
||||
|
||||
All three modes match the current server-rendered output:
|
||||
|
||||
**AlphaSort** (`#alpha`):
|
||||
- Flat `<ul class="roux-recipes">` with `<li><a href="/recipes/...">title</a></li>`.
|
||||
- Sorted alphabetically by title.
|
||||
|
||||
**TagSort** (`#tags`):
|
||||
- Grouped by tag: `<h3>TagName</h3>` followed by `<ul class="roux-recipes">` for each tag group.
|
||||
- Tag order is alphabetical.
|
||||
- Recipes that appear in multiple tags appear under each tag.
|
||||
- Untagged recipes are omitted (matches current behavior).
|
||||
|
||||
**CourseSort** (`#course`):
|
||||
- Grouped by course: `<h3>CourseName</h3>` followed by `<ul class="roux-recipes">` for each course group.
|
||||
- Recipes without a course are omitted.
|
||||
|
||||
### Styling
|
||||
|
||||
Reuses existing CSS classes: `.roux-recipes`, `.roux-course`, `.tag`, `.roux-navbar`, etc. No new styles needed for the recipe list itself.
|
||||
|
||||
## Files Changed
|
||||
|
||||
### `src/Roux/Html.hs`
|
||||
|
||||
- Add `Aeson` import and encode recipe list + error list as JSON.
|
||||
- Modify `indexPage` to:
|
||||
- Embed JSON blobs.
|
||||
- Render search input (HTML element, present in all views).
|
||||
- Render the JS script block.
|
||||
- Remove server-rendered recipe list rendering (move sort-mode logic to JS).
|
||||
- Modify `SortMode` to become part of the JSON payload rather than server-side rendering selection.
|
||||
|
||||
### `src/Roux/RecipeIndex.hs`
|
||||
|
||||
- Minor: ensure `riTitle` and `riFilename` are exported and accessible for JSON encoding. No structural changes expected.
|
||||
|
||||
### `package.yaml`
|
||||
|
||||
- No new dependencies needed (`aeson` is already listed).
|
||||
|
||||
## Testing
|
||||
|
||||
- Manual testing in browser: verify all three sort modes render correctly.
|
||||
- Verify search filters by title and filename (case-insensitive).
|
||||
- Verify clearing search restores sort mode.
|
||||
- Verify hash-based navigation works (back/forward buttons).
|
||||
- Verify error recipes still shown at bottom.
|
||||
- Run `./hs stack build` and `./hs stack test` before marking complete.
|
||||
|
||||
## Future Considerations (explicitly out of scope)
|
||||
|
||||
- Server-side search endpoint — not needed at this scale.
|
||||
- Debouncing/throttling — not needed with client-side in-memory filtering on small dataset.
|
||||
- Fuzzy search — not needed with simple substring matching on small dataset.
|
||||
- External JS file — inline keeps things simple for this size.
|
||||
Reference in New Issue
Block a user