Replace the per-page Publish with one Publish for all pending changes #156

Closed
opened 2026-10-06 23:29:58 +02:00 by dominik.polakovics · 0 comments

This was generated by AI during triage.

Summary

Remove the per-page Publish and keep one Publish: today's "Publish all" in the app header, renamed. It publishes every pending change, as Publish all does now. More and more single editor actions change several pages at once. Renaming a page rewrites links to it on other pages, and renaming or deleting a category rewrites every page that uses it (#152). Publishing one page at a time leaves the rest of such a change unpublished.

The draft-link warning, which today only the per-page publish runs, moves to the one Publish, narrowed to links to disabled pages.

Current behavior

  • Publish (per page) is a button in the page module's toolbar (its green primary button) and in a feature module's entry editor, shown while the page's badge is not published (PublishButton in web/src/components/PageEditorChrome.tsx, used by PagePane and EntryEditor). POST /api/page/publish {page, lang, force?}:
    • publishes the page's bundle (a section's descendant pages included) plus every media-library file its pages reference, and refuses when a referenced file is missing from the library;
    • answers empty when nothing in that set differs from main;
    • unless force is set, first scans the page for links to pages that would 404 on the live site: disabled pages (front-matter draft), and never-published pages that this publish does not carry. If it finds any, it answers {"warning":{"draftLinks":[…]}} and publishes nothing. The editor shows "Linked pages are unpublished" with Cancel and "Publish anyway", which posts again with force. The scan is best-effort: it reads markdown link destinations, inline <a href> and link-typed element params, and matches them by content path and default pretty URL.
    • Its feedback chip ("Publishing… → Live ✓") and error strip (Retry repeats the same publish, force included) sit under the page's toolbar; a failed pre-flight opens a build-failed dialog.
  • Publish all is a button in the app header, also inside a host's bar (App.tsx). POST /api/publish-all {lang} publishes every pending change under content/ and assets/media/ in one commit, answers empty when nothing is pending, and never scans links. Its chip, error strip (with Retry) and build-failed dialog belong to the header.
  • Both run the pre-flight. It exports main plus the publish's paths, refuses when a page in the export would reference a library file main would not have, and builds with the pinned Hugo. A failure answers 422 and nothing is committed.
  • deckle-hosted calls /api/publish-all. Its end-to-end test (server/e2e_publish_test.go in deckle-hosted) POSTs {"lang":"en"} over the harness socket and expects a 200 with a commit, pushed: true and feedback: "polling". Nothing outside deckle calls /api/page/publish.
  • Badges: GET /api/badges gives every page published, modified or draft (shown as "unpublished"), compared with main. A page is modified when its bundle or a library file it references differs. Badges show in the page tree, in the page's toolbar and in the status column of module lists.
  • Discard… resets one page to main (a never-published page is removed) and is hidden while the page is published. Disable (page actions) sets front-matter draft: true, and Hugo leaves the page out of the live build. Delete… already warns when other pages link to the page (GET /api/page/refs); Disable does not.
  • In deckle-hosted a site has one draft branch, shared by all of its editors (deckle-hosted README, "The editor branch"), so all editors of a site share one set of pending changes.
  • Documented in the README ("Git backbone" under "How it works", "Host navigation", and the entry editor's toolbar under modules) and in scaffold/site/AGENTS.md ("Library media publishes with the page").

Desired behavior

One Publish

  • The page module and the entry editor no longer offer Publish. Everything else in their toolbars stays as it is. The page module's toolbar has no green primary button any more; nothing takes Publish's place.
  • The header's button reads Publish ("Publishing…" while busy). It publishes every pending change under content/ and assets/media/, exactly as Publish all does today: pre-flight, empty result, chip, error strip with Retry, build-failed dialog. The same holds inside a host's bar.
  • The per-page publish chip, error strip and build-failed dialog go with the per-page Publish. The header's chip reports the one Publish, and the host navigation is still requested again when a publish reaches a final state.
  • POST /api/page/publish is gone: it answers 404, like every unknown /api/ path.
  • Badges, Discard… and Undo do not change. The badges now show which pages the next Publish carries.
  • When Publish is pressed, the server checks the pages this Publish carries for links to disabled pages (front-matter draft), which would 404 on the live site. The carried pages are every page file under content/ that is added or modified relative to main. A carried page that is itself disabled is not checked, because its links never reach the live site.
  • Never-published pages are not targets: Publish carries every one of them.
  • Links are found and matched as the per-page scan does today: markdown destinations, inline <a href> and link-typed element params, matched by content path and default pretty URL.
  • When links are found, nothing is published. The editor shows a dialog naming each disabled page together with the carried pages that link to it, with Cancel and Publish anyway. Its wording says "disabled", not "unpublished", because "unpublished" is the badge word for never-published pages. Cancel publishes nothing. Publish anyway publishes without the check.
  • When nothing is pending, the check does not run and the answer is empty, as today.
  • The check runs before the pre-flight build, as today, so a warning never costs a build.
  • A Retry after a rejected push repeats the request that failed: after Publish anyway it does not ask about the links again.

The /api/publish-all contract

  • The check runs only when the request asks for it, through a new optional request field, for example a boolean checkDraftLinks. A request without it, such as deckle-hosted's {"lang":"en"}, publishes exactly as today and never answers with a warning.
  • A request that asks for the check and finds links answers 200 with a warning body that names the disabled pages and the pages linking to each, and publishes nothing. The old {"warning":{"draftLinks":[…]}} shape belonged to the removed endpoint; only the editor reads the new one.
  • Every other answer (success, empty, 422 pre-flight, 502 retryable, 409 degraded, 500) keeps its current shape.

Docs

  • The README describes one Publish: what it carries, the pre-flight, the link check, and that it publishes every pending change, including other editors' unfinished ones. On a site with several editors (deckle-hosted's shared draft branch), one editor's Publish ships everyone's pending edits. The only ways to hold a change back are Disable, which also takes a live page off the live site, and Discard….
  • The README's host navigation section and its description of the entry editor's toolbar no longer name Publish all or a per-page Publish.
  • scaffold/site/AGENTS.md's "Library media publishes with the page" describes the one Publish: it carries pending pages and library files together, and refuses when a page it carries would reference a missing library file. The advice for hand commits stays.

Decisions

  • One Publish for everything pending. This is the maintainer's direction: more and more single actions change several pages at once.
  • A separate issue from #152, as the maintainer chose.
  • /api/page/publish is removed, not kept without a button. Nothing calls it besides the removed button (deckle-hosted does not), and a publish path nobody uses would still have to be maintained and tested. No deckle sites exist yet (as of 2026-10-06), so there is nothing to stay compatible with.
  • /api/publish-all keeps its name, and for requests that don't ask for the check, its behavior and answers. deckle-hosted's end-to-end test calls it with only {lang}, and changes on the deckle side keep deckle-hosted working unchanged. The button's label changes; the endpoint's name does not.
  • The draft-link check stays, narrowed to disabled pages and run over every page the Publish carries. It keeps the protection the per-page publish gives today, and the scan, its tests and its dialog already exist. Never-published pages need no check, because Publish carries all of them. The check is opt-in on the endpoint for the reason above.
  • The dialog names the linking pages. With one Publish the linking page is no longer the open page, so the editor has to be told where each link is in order to fix it.
  • Badges stay as they are. They now tell the editor which pages the next Publish carries.
  • Discard… stays per page. It does not publish anything.
  • No button replaces Publish as the page module's primary button, as the maintainer chose.

Key interfaces

  • handlePublishAll and draftLinkTargets (internal/server/publish.go): the check moves from the per-page handler to publish-all and runs over the pending page files (gitrepo.Repo.ChangedPublishFiles).
  • handlePagePublish goes, with its route (internal/server/server.go) and its mutation-lock entry (internal/server/mutation.go). publishPathsFor (internal/server/publishset.go) loses its only caller. gitrepo.Repo.PublishPaths and StatusForPaths lose their only server caller. They are the package's tested API, so whether the package keeps them is the implementer's call.
  • publishAll, publishPage and PublishOutcome (web/src/api/publish.ts); the header's publish in web/src/App.tsx; PublishButton, PageEditorNotices and PageEditorDialogs (web/src/components/PageEditorChrome.tsx); the publish state in web/src/stores/pageEditor.ts; the feedback store's keys (web/src/publish.ts, PUBLISH_ALL_KEY).
  • The badges (handleBadges, internal/server/git.go) don't change.

Acceptance criteria

  • The page module and the entry editor show no Publish button, and the page module's toolbar has no primary button in its normal mode.
  • The header's button reads "Publish" ("Publishing…" while busy), standalone and inside a host's bar, and publishes every pending change under content/ and assets/media/.
  • POST /api/page/publish answers 404.
  • POST /api/publish-all with only {lang} publishes even when a pending page links to a disabled page, and answers 200 with commit, pushed and feedback as today.
  • With the check requested, a pending page linking to a disabled page gets a warning naming the disabled page and the linking page, and nothing is committed or pushed.
  • With the check requested, no warning is given for a link to a never-published page that this Publish carries, or for a link on a carried page that is itself disabled.
  • With the check requested and nothing pending, the answer is empty and no warning is given.
  • In the editor, the warning dialog lists each disabled page with the pages linking to it. Cancel publishes nothing, and Publish anyway publishes.
  • A Retry after a rejected push that followed Publish anyway publishes without showing the dialog again.
  • Badges, Discard… and Undo behave as before.
  • The README and scaffold/site/AGENTS.md describe one Publish as above, including that it publishes every editor's pending changes. Neither names Publish all or a per-page Publish.
  • Coverage that the per-page tests gave for behavior that still exists (pre-flight failure, feedback polling, degraded git, a page referencing a missing library file) is kept through /api/publish-all.

How to verify

  • make test, which builds web/, runs the web tests and runs go test ./....
  • Go tests in internal/server for the publish-all contract: a {lang}-only request with a link to a disabled page publishes; a request asking for the check warns as above and commits nothing; /api/page/publish answers 404.
  • Web tests: the header's Publish with the warning dialog (Cancel, Publish anyway, Retry after Publish anyway), and no Publish button in the page module's or the entry editor's toolbar.

Assumptions to check

  • That nothing besides the editor calls /api/page/publish, and that deckle-hosted calls /api/publish-all as described above. Both were checked against the deckle-hosted snapshot at c94bc74; check again if it has moved.

Out of scope

  • A warning when someone disables a page that other pages link to, as Delete… already gives. That could be a separate issue.
  • A confirmation step listing what Publish will carry. The maintainer left it out; the badges show what is pending.
  • Renaming /api/publish-all, or changing its answers for requests that don't ask for the check.
  • Improving the scan's link matching (relative links, ref/relref, custom permalinks).
  • deckle-hosted's docs and design mockup that say "Publish all". That is a deckle-hosted change.
  • Badges, Discard… and Undo.

Blocked by

None - can start immediately

> *This was generated by AI during triage.* ## Summary Remove the per-page Publish and keep one **Publish**: today's "Publish all" in the app header, renamed. It publishes every pending change, as Publish all does now. More and more single editor actions change several pages at once. Renaming a page rewrites links to it on other pages, and renaming or deleting a category rewrites every page that uses it (#152). Publishing one page at a time leaves the rest of such a change unpublished. The draft-link warning, which today only the per-page publish runs, moves to the one Publish, narrowed to links to disabled pages. ## Current behavior - **Publish (per page)** is a button in the page module's toolbar (its green primary button) and in a feature module's entry editor, shown while the page's badge is not `published` (`PublishButton` in `web/src/components/PageEditorChrome.tsx`, used by `PagePane` and `EntryEditor`). `POST /api/page/publish {page, lang, force?}`: - publishes the page's bundle (a section's descendant pages included) plus every media-library file its pages reference, and refuses when a referenced file is missing from the library; - answers empty when nothing in that set differs from `main`; - unless `force` is set, first scans the page for links to pages that would 404 on the live site: disabled pages (front-matter `draft`), and never-published pages that this publish does not carry. If it finds any, it answers `{"warning":{"draftLinks":[…]}}` and publishes nothing. The editor shows "Linked pages are unpublished" with Cancel and "Publish anyway", which posts again with `force`. The scan is best-effort: it reads markdown link destinations, inline `<a href>` and link-typed element params, and matches them by content path and default pretty URL. - Its feedback chip ("Publishing… → Live ✓") and error strip (Retry repeats the same publish, `force` included) sit under the page's toolbar; a failed pre-flight opens a build-failed dialog. - **Publish all** is a button in the app header, also inside a host's bar (`App.tsx`). `POST /api/publish-all {lang}` publishes every pending change under `content/` and `assets/media/` in one commit, answers empty when nothing is pending, and never scans links. Its chip, error strip (with Retry) and build-failed dialog belong to the header. - Both run the pre-flight. It exports `main` plus the publish's paths, refuses when a page in the export would reference a library file `main` would not have, and builds with the pinned Hugo. A failure answers 422 and nothing is committed. - **deckle-hosted calls `/api/publish-all`.** Its end-to-end test (`server/e2e_publish_test.go` in deckle-hosted) POSTs `{"lang":"en"}` over the harness socket and expects a 200 with a `commit`, `pushed: true` and `feedback: "polling"`. Nothing outside deckle calls `/api/page/publish`. - **Badges:** `GET /api/badges` gives every page `published`, `modified` or `draft` (shown as "unpublished"), compared with `main`. A page is `modified` when its bundle or a library file it references differs. Badges show in the page tree, in the page's toolbar and in the `status` column of module lists. - **Discard…** resets one page to `main` (a never-published page is removed) and is hidden while the page is published. **Disable** (page actions) sets front-matter `draft: true`, and Hugo leaves the page out of the live build. **Delete…** already warns when other pages link to the page (`GET /api/page/refs`); Disable does not. - In deckle-hosted a site has one draft branch, shared by all of its editors (deckle-hosted README, "The editor branch"), so all editors of a site share one set of pending changes. - Documented in the README ("Git backbone" under "How it works", "Host navigation", and the entry editor's toolbar under modules) and in `scaffold/site/AGENTS.md` ("Library media publishes with the page"). ## Desired behavior ### One Publish - The page module and the entry editor no longer offer Publish. Everything else in their toolbars stays as it is. The page module's toolbar has no green primary button any more; nothing takes Publish's place. - The header's button reads **Publish** ("Publishing…" while busy). It publishes every pending change under `content/` and `assets/media/`, exactly as Publish all does today: pre-flight, empty result, chip, error strip with Retry, build-failed dialog. The same holds inside a host's bar. - The per-page publish chip, error strip and build-failed dialog go with the per-page Publish. The header's chip reports the one Publish, and the host navigation is still requested again when a publish reaches a final state. - `POST /api/page/publish` is gone: it answers 404, like every unknown `/api/` path. - Badges, Discard… and Undo do not change. The badges now show which pages the next Publish carries. ### The link check - When Publish is pressed, the server checks the pages this Publish carries for links to **disabled** pages (front-matter `draft`), which would 404 on the live site. The carried pages are every page file under `content/` that is added or modified relative to `main`. A carried page that is itself disabled is not checked, because its links never reach the live site. - Never-published pages are not targets: Publish carries every one of them. - Links are found and matched as the per-page scan does today: markdown destinations, inline `<a href>` and link-typed element params, matched by content path and default pretty URL. - When links are found, nothing is published. The editor shows a dialog naming each disabled page together with the carried pages that link to it, with Cancel and **Publish anyway**. Its wording says "disabled", not "unpublished", because "unpublished" is the badge word for never-published pages. Cancel publishes nothing. Publish anyway publishes without the check. - When nothing is pending, the check does not run and the answer is empty, as today. - The check runs before the pre-flight build, as today, so a warning never costs a build. - A Retry after a rejected push repeats the request that failed: after Publish anyway it does not ask about the links again. ### The `/api/publish-all` contract - The check runs only when the request asks for it, through a new optional request field, for example a boolean `checkDraftLinks`. A request without it, such as deckle-hosted's `{"lang":"en"}`, publishes exactly as today and never answers with a warning. - A request that asks for the check and finds links answers 200 with a warning body that names the disabled pages and the pages linking to each, and publishes nothing. The old `{"warning":{"draftLinks":[…]}}` shape belonged to the removed endpoint; only the editor reads the new one. - Every other answer (success, empty, 422 pre-flight, 502 retryable, 409 degraded, 500) keeps its current shape. ### Docs - The README describes one Publish: what it carries, the pre-flight, the link check, and that it publishes every pending change, including other editors' unfinished ones. On a site with several editors (deckle-hosted's shared draft branch), one editor's Publish ships everyone's pending edits. The only ways to hold a change back are Disable, which also takes a live page off the live site, and Discard…. - The README's host navigation section and its description of the entry editor's toolbar no longer name Publish all or a per-page Publish. - `scaffold/site/AGENTS.md`'s "Library media publishes with the page" describes the one Publish: it carries pending pages and library files together, and refuses when a page it carries would reference a missing library file. The advice for hand commits stays. ## Decisions - **One Publish for everything pending.** This is the maintainer's direction: more and more single actions change several pages at once. - **A separate issue from #152**, as the maintainer chose. - **`/api/page/publish` is removed**, not kept without a button. Nothing calls it besides the removed button (deckle-hosted does not), and a publish path nobody uses would still have to be maintained and tested. No deckle sites exist yet (as of 2026-10-06), so there is nothing to stay compatible with. - **`/api/publish-all` keeps its name, and for requests that don't ask for the check, its behavior and answers.** deckle-hosted's end-to-end test calls it with only `{lang}`, and changes on the deckle side keep deckle-hosted working unchanged. The button's label changes; the endpoint's name does not. - **The draft-link check stays, narrowed to disabled pages and run over every page the Publish carries.** It keeps the protection the per-page publish gives today, and the scan, its tests and its dialog already exist. Never-published pages need no check, because Publish carries all of them. The check is opt-in on the endpoint for the reason above. - **The dialog names the linking pages.** With one Publish the linking page is no longer the open page, so the editor has to be told where each link is in order to fix it. - **Badges stay as they are.** They now tell the editor which pages the next Publish carries. - **Discard… stays per page.** It does not publish anything. - **No button replaces Publish as the page module's primary button**, as the maintainer chose. ## Key interfaces - `handlePublishAll` and `draftLinkTargets` (`internal/server/publish.go`): the check moves from the per-page handler to publish-all and runs over the pending page files (`gitrepo.Repo.ChangedPublishFiles`). - `handlePagePublish` goes, with its route (`internal/server/server.go`) and its mutation-lock entry (`internal/server/mutation.go`). `publishPathsFor` (`internal/server/publishset.go`) loses its only caller. `gitrepo.Repo.PublishPaths` and `StatusForPaths` lose their only server caller. They are the package's tested API, so whether the package keeps them is the implementer's call. - `publishAll`, `publishPage` and `PublishOutcome` (`web/src/api/publish.ts`); the header's publish in `web/src/App.tsx`; `PublishButton`, `PageEditorNotices` and `PageEditorDialogs` (`web/src/components/PageEditorChrome.tsx`); the publish state in `web/src/stores/pageEditor.ts`; the feedback store's keys (`web/src/publish.ts`, `PUBLISH_ALL_KEY`). - The badges (`handleBadges`, `internal/server/git.go`) don't change. ## Acceptance criteria - [ ] The page module and the entry editor show no Publish button, and the page module's toolbar has no primary button in its normal mode. - [ ] The header's button reads "Publish" ("Publishing…" while busy), standalone and inside a host's bar, and publishes every pending change under `content/` and `assets/media/`. - [ ] `POST /api/page/publish` answers 404. - [ ] `POST /api/publish-all` with only `{lang}` publishes even when a pending page links to a disabled page, and answers 200 with `commit`, `pushed` and `feedback` as today. - [ ] With the check requested, a pending page linking to a disabled page gets a warning naming the disabled page and the linking page, and nothing is committed or pushed. - [ ] With the check requested, no warning is given for a link to a never-published page that this Publish carries, or for a link on a carried page that is itself disabled. - [ ] With the check requested and nothing pending, the answer is empty and no warning is given. - [ ] In the editor, the warning dialog lists each disabled page with the pages linking to it. Cancel publishes nothing, and Publish anyway publishes. - [ ] A Retry after a rejected push that followed Publish anyway publishes without showing the dialog again. - [ ] Badges, Discard… and Undo behave as before. - [ ] The README and `scaffold/site/AGENTS.md` describe one Publish as above, including that it publishes every editor's pending changes. Neither names Publish all or a per-page Publish. - [ ] Coverage that the per-page tests gave for behavior that still exists (pre-flight failure, feedback polling, degraded git, a page referencing a missing library file) is kept through `/api/publish-all`. ## How to verify - `make test`, which builds `web/`, runs the web tests and runs `go test ./...`. - Go tests in `internal/server` for the publish-all contract: a `{lang}`-only request with a link to a disabled page publishes; a request asking for the check warns as above and commits nothing; `/api/page/publish` answers 404. - Web tests: the header's Publish with the warning dialog (Cancel, Publish anyway, Retry after Publish anyway), and no Publish button in the page module's or the entry editor's toolbar. ## Assumptions to check - That nothing besides the editor calls `/api/page/publish`, and that deckle-hosted calls `/api/publish-all` as described above. Both were checked against the deckle-hosted snapshot at `c94bc74`; check again if it has moved. ## Out of scope - A warning when someone disables a page that other pages link to, as Delete… already gives. That could be a separate issue. - A confirmation step listing what Publish will carry. The maintainer left it out; the badges show what is pending. - Renaming `/api/publish-all`, or changing its answers for requests that don't ask for the check. - Improving the scan's link matching (relative links, `ref`/`relref`, custom permalinks). - deckle-hosted's docs and design mockup that say "Publish all". That is a deckle-hosted change. - Badges, Discard… and Undo. ## Blocked by None - can start immediately
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle#156
No description provided.