# 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 1. The `

` recipe title on the recipe detail page gets a click handler. 2. On click: - `contenteditable` is set to `true` on the `

`. - The element receives focus (via `.focus()` at the end of the text). - A CSS class `.roux-editing` is added for a subtle visual cue (faint dashed border / changed cursor). - A hidden `` stores the original text for revert. 3. On **Enter** (without Shift): prevent default newline, trigger save. 4. On **Escape**: revert to the original text, remove `contenteditable`, remove `.roux-editing`. 5. On **blur** (clicking away): trigger save. 6. **Save flow**: - Read the current text content from the `

`. - If unchanged from original, do nothing (just exit edit mode). - Send `PATCH /recipes/{filename}/title` with JSON body `{ "title": "New Title" }`. - On 200 OK: update the `

` text to the server's response (the server normalizes/trims it), remove `contenteditable`, remove `.roux-editing`. - On error: revert to original text, remove edit mode, show a brief error indication. 7. **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. 1. Validate HTTP method is PATCH (return 405 otherwise). 2. Parse JSON body: expects `{ "title": "..." }`. Return 400 if missing or invalid. 3. Look up the recipe file: - Strip `.cook` extension, lowercase, same pattern as existing `lookupRecipe`. - Read the `.cook` file from disk. 4. Update the title in the YAML front matter: - Find the `title:` line in the front matter block (text between the opening `---\n` and `\n---`). - Replace its value with the new title. - If no `title:` field exists, insert one after the opening `---`. - Write the file back to disk. 5. The existing fsnotify watcher picks up the change and updates the shared IORef state. SSE events notify other connected clients. 6. Return `200 OK` with JSON `{ "success": true, "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`**: Add `titleUpdateHandler` and route registration. - **`src/Roux/Html.hs`**: Add inline JS for title editing (`titleEditJs`), add CSS for `.roux-editing` state 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: `contenteditable` behavior, PATCH route, YAML update pattern are all explicit.