3.8 KiB
3.8 KiB
Inline Recipe Title Editing
Date: 2026-05-21
Summary
Add in-page editing for recipe titles. Clicking the recipe title on the detail page makes it editable inline (using contenteditable). Pressing Enter or clicking away saves the change via a PATCH request. Escape cancels. The DOM updates in-place on success — no page reload.
Motivation
This is the first step toward making all recipe fields editable in-page, keeping the layout stable during editing.
Client-side interaction
- The
<h1>recipe title on the recipe detail page gets a click handler. - On click:
contenteditableis set totrueon the<h1>.- The element receives focus (via
.focus()at the end of the text). - A CSS class
.roux-editingis added for a subtle visual cue (faint dashed border / changed cursor). - A hidden
<span id="roux-title-original">stores the original text for revert.
- On Enter (without Shift): prevent default newline, trigger save.
- On Escape: revert to the original text, remove
contenteditable, remove.roux-editing. - On blur (clicking away): trigger save.
- Save flow:
- Read the current text content from the
<h1>. - If unchanged from original, do nothing (just exit edit mode).
- Send
PATCH /recipes/{filename}/titlewith JSON body{ "title": "New Title" }. - On 200 OK: update the
<h1>text to the server's response (the server normalizes/trims it), removecontenteditable, remove.roux-editing. - On error: revert to original text, remove edit mode, show a brief error indication.
- Read the current text content from the
- Hover hint: a subtle pencil icon or "Edit" text appears next to the title on hover (CSS only — no extra JS needed).
Server-side
New route: PATCH /recipes/{filename}/title
Handler: titleUpdateHandler in Roux.Server module.
- Validate HTTP method is PATCH (return 405 otherwise).
- Parse JSON body: expects
{ "title": "..." }. Return 400 if missing or invalid. - Look up the recipe file:
- Strip
.cookextension, lowercase, same pattern as existinglookupRecipe. - Read the
.cookfile from disk.
- Strip
- Update the title in the YAML front matter:
- Find the
title:line in the front matter block (text between the opening---\nand\n---). - Replace its value with the new title.
- If no
title:field exists, insert one after the opening---. - Write the file back to disk.
- Find the
- The existing fsnotify watcher picks up the change and updates the shared IORef state. SSE events notify other connected clients.
- Return
200 OKwith JSON{ "success": true, "title": "<normalized title>" }.
Error handling
- Recipe file not found → 404.
- File read/write error → 500 with JSON
{ "error": "Could not update recipe file" }. - Empty title (after trimming) → 400 with JSON
{ "error": "Title cannot be empty" }. - Client-side: on any error, revert to original text and show a brief visual error state.
YAML update logic
Reuses the same pattern as the existing updateImageMetadata / addOrUpdateYamlKey functions in Server.hs. A dedicated updateTitleInCookFile function reads the file, performs the replacement, and writes it back.
Files changed
src/Roux/Server.hs: AddtitleUpdateHandlerand route registration.src/Roux/Html.hs: Add inline JS for title editing (titleEditJs), add CSS for.roux-editingstate and hover hint.
What's not included (future steps)
- Editing other recipe fields (source, description, servings, etc.)
- Debouncing or optimistic updates
- Keyboard shortcut indicators
Self-review
- No placeholders or TODOs.
- Internally consistent: client save matches server endpoint, error handling is symmetric.
- Scoped appropriately for a single implementation plan (one server handler + one client JS block).
- No ambiguity:
contenteditablebehavior, PATCH route, YAML update pattern are all explicit.