berrypod/docs/plans/draft-publish-workflow.md
jamey ae0a149ecd
All checks were successful
deploy / deploy (push) Successful in 53s
add draft-then-publish workflow plan
Plan for auto-saving drafts with explicit publish. Visitors always see
published version, admins see their drafts while editing. Removes need
for beforeunload warnings since work is never lost.

Planned to follow unified-editing-mode completion.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-09 11:07:46 +00:00

6.6 KiB

Draft-then-publish workflow

Status: Planned (after unified-editing-mode)

Problem

Current editing workflow uses explicit save — changes are either saved (immediately live) or lost on navigation. This creates:

  1. Data loss anxiety — navigate away accidentally, lose all work
  2. beforeunload warnings — browser-native dialogs with no custom options
  3. No experimentation — can't safely try changes without affecting live site

Solution

Implement a draft-then-publish model where all edits auto-save as drafts, visitors see only published versions, and explicit "Publish" commits changes live.

Design principles

  1. Drafts are per-user — each admin sees their own pending changes
  2. Visitors always see published — no accidental exposure of drafts
  3. Auto-save constantly — no risk of lost work
  4. Clear visual distinction — obvious when viewing draft vs published
  5. Single publish action — one button publishes all pending changes

Scope

Three types of editable content:

Type Storage Draft approach
Page blocks pages.blocks (JSON) draft_blocks column
Theme settings settings.theme_settings (JSON) draft_theme_settings key
Shop settings settings table (various keys) draft_* keys or settings_drafts table

Data model

# Migration: add draft columns to pages
alter table(:pages) do
  add :draft_blocks, :map
  add :draft_user_id, references(:users, type: :binary_id, on_delete: :nilify_all)
  add :draft_updated_at, :utc_datetime_usec
end

# New settings keys
# - "draft_theme_settings" → JSON blob
# - "draft_site_name" → string
# - "draft_header_nav" → JSON
# etc.

Option B: Separate drafts table (more flexible)

create table(:drafts, primary_key: false) do
  add :id, :binary_id, primary_key: true
  add :user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false
  add :entity_type, :string, null: false  # "page", "theme", "settings"
  add :entity_id, :string                  # page id, or nil for global
  add :data, :map, null: false
  timestamps(type: :utc_datetime_usec)
end

create unique_index(:drafts, [:user_id, :entity_type, :entity_id])

Recommendation: Start with Option A (simpler), migrate to Option B if multi-user or more complex draft needs emerge.

Implementation

Phase 1: Page drafts

Task Est Notes
Add draft columns to pages table 30m Migration
Update Pages context with draft functions 1h save_draft/2, publish_draft/1, discard_draft/1, has_draft?/1
Modify PageEditorHook to auto-save drafts 1h Debounced save on every change
Add "Publish" and "Discard" buttons to editor 1h Replace current "Save"
Show draft indicator when viewing page with unpublished changes 1h Banner or badge
Remove beforeunload warning (no longer needed) 15m Drafts persist

Phase 2: Theme drafts

Task Est Notes
Add draft_theme_settings to settings 30m New key
Update Settings context with theme draft functions 1h Similar to pages
Modify theme editor to auto-save drafts 1h
Theme preview shows draft, shop shows published 1h Conditional CSS injection
Add publish/discard to theme editor 30m

Phase 3: Settings drafts

Task Est Notes
Decide which settings are draftable 30m Not all need drafts (e.g. API keys)
Add draft handling to Settings context 1.5h
Update settings editor to use drafts 1h
Unified "Publish all" option 1h Publish page + theme + settings together

Phase 4: Polish

Task Est Notes
Draft age indicator ("Last saved 5 mins ago") 30m
Draft conflict handling (stale draft warning) 1h Edge case: published changed since draft created
Version history / rollback (stretch goal) 3h Store previous published versions

UX flow

Editing a page

  1. Admin navigates to page, clicks "Edit"
  2. Editor panel opens, showing current published state
  3. Admin makes changes — auto-saved as draft every few seconds
  4. Admin can navigate away freely — draft persists
  5. Admin returns later — draft loaded automatically
  6. "Publish" button commits draft → published
  7. "Discard" button deletes draft, reverts to published

Visual indicators

┌─────────────────────────────────────────────────────────┐
│ ⚠️ You have unpublished changes (last saved 2 mins ago) │
│                                [Publish] [Discard]      │
└─────────────────────────────────────────────────────────┘

When admin views a page with a draft:

  • Banner at top of editor panel
  • "Unpublished" badge next to page title
  • Different background tint in editor (subtle)

Visitors see published only

The render_page/1 function checks:

  1. If admin is viewing AND has draft → show draft
  2. Otherwise → show published blocks
defp get_blocks_for_render(page, current_user) do
  if current_user && page.draft_user_id == current_user.id && page.draft_blocks do
    page.draft_blocks
  else
    page.blocks
  end
end

Migration path

  1. Deploy with draft support disabled (feature flag)
  2. Run migration to add draft columns
  3. Enable draft mode
  4. Existing pages continue working (no draft = show published)
  5. New edits create drafts

Future considerations

  • Multi-user drafts — currently single draft per page, could extend to per-user drafts
  • Draft preview link — shareable URL that shows draft to non-admins (for review)
  • Scheduled publishing — "Publish at 9am tomorrow"
  • Draft comparison — side-by-side diff of draft vs published

Dependencies

  • Unified editing mode (phases 1-7) must be complete first
  • This work builds on the editor panel UI established there

Estimates

Phase Est
Phase 1: Page drafts 5h
Phase 2: Theme drafts 4h
Phase 3: Settings drafts 4h
Phase 4: Polish 5h
Total 18h

References