Files
roux/docs/specs/2026-05-21-inline-title-editing-design.md
T

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

  1. The <h1> recipe title on the recipe detail page gets a click handler.
  2. On click:
    • contenteditable is set to true on the <h1>.
    • 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 <span id="roux-title-original"> 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 <h1>.
    • 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 <h1> 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": "<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: 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.