From 9e63a6bec1b4ccc45dd684f7b6a941b75f9b9cf0 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Mon, 6 Jul 2026 06:09:21 +0200 Subject: Add public preview links for drafts Lets editors share a draft with people outside the admin system, via a random per-page token rather than an enumerable id - /preview/. shared_previews#show is intentionally unauthenticated. Redirects to the real public URL once the page is published. Surfaced on nodes#show (Admin Preview + Public Preview, next to Public Link) as generate/revoke buttons. --- app/controllers/shared_previews_controller.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 app/controllers/shared_previews_controller.rb (limited to 'app/controllers/shared_previews_controller.rb') diff --git a/app/controllers/shared_previews_controller.rb b/app/controllers/shared_previews_controller.rb new file mode 100644 index 0000000..a8a540f --- /dev/null +++ b/app/controllers/shared_previews_controller.rb @@ -0,0 +1,13 @@ +class SharedPreviewsController < ApplicationController + def show + @page = Page.find_by!(preview_token: params[:token]) + + if @page.node && @page.node.head_id == @page.id + redirect_to node_path(@page.node) + return + end + + response.headers['X-Robots-Tag'] = 'noindex' + render template: @page.valid_template, layout: "application" + end +end -- cgit v1.3 From 889e15eabbe107d2642fdd8aa3f03821058c00dc Mon Sep 17 00:00:00 2001 From: erdgeist Date: Mon, 6 Jul 2026 13:52:28 +0200 Subject: Fix shared preview links breaking after a second publish A token's page could stop being node.head_id (superseded by a newer draft) while still being published_at.present? - the old check only compared against the current head, so a superseded page fell through to direct rendering instead of redirecting, serving a stale frozen snapshot indefinitely instead of the current live content. Also handles scheduled publishing correctly: a page can be head_id but not yet public? (published_at in the future) - that case must still render directly, not redirect into a 404 on the not-yet-live public URL. --- app/controllers/shared_previews_controller.rb | 9 +++++-- test/models/page_test.rb | 35 ++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) (limited to 'app/controllers/shared_previews_controller.rb') diff --git a/app/controllers/shared_previews_controller.rb b/app/controllers/shared_previews_controller.rb index a8a540f..d482466 100644 --- a/app/controllers/shared_previews_controller.rb +++ b/app/controllers/shared_previews_controller.rb @@ -1,9 +1,14 @@ class SharedPreviewsController < ApplicationController def show @page = Page.find_by!(preview_token: params[:token]) + node = @page.node - if @page.node && @page.node.head_id == @page.id - redirect_to node_path(@page.node) + was_published = @page.published_at.present? + superseded = was_published && node && node.head_id != @page.id + currently_public = was_published && node && node.head_id == @page.id && @page.public? + + if superseded || currently_public + redirect_to node_path(node) return end diff --git a/test/models/page_test.rb b/test/models/page_test.rb index afba8b5..ad2742f 100644 --- a/test/models/page_test.rb +++ b/test/models/page_test.rb @@ -142,5 +142,38 @@ class PageTest < ActiveSupport::TestCase update = updates2009.children.create!( :slug => "my-first-update" ) assert_equal "update", update.draft.template_name end - + + test "a page scheduled for future publication is not yet public even after being published" do + node = Node.root.children.create!(slug: "preview-scheduled-test") + draft = node.find_or_create_draft(@user1) + draft.title = "Scheduled test" + draft.published_at = 1.day.from_now + draft.save! + token = draft.ensure_preview_token! + + node.publish_draft! + page = Page.find_by(preview_token: token) + + assert_equal page.id, page.node.head_id + assert_not page.public? + end + + test "a superseded page is no longer the head, even though it was once published" do + node = Node.root.children.create!(slug: "preview-superseded-test") + first_draft = node.find_or_create_draft(@user1) + first_draft.title = "First version" + first_draft.save! + first_token = first_draft.ensure_preview_token! + node.publish_draft! + + second_draft = node.find_or_create_draft(@user1) + second_draft.title = "Second version" + second_draft.save! + node.publish_draft! + + first_page = Page.find_by(preview_token: first_token) + + assert_not_equal first_page.id, first_page.node.head_id + assert first_page.published_at.present? + end end -- cgit v1.3 From 40adeec1bcc6fa276d765e3b9c37e781c64a48c1 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Mon, 13 Jul 2026 17:09:45 +0200 Subject: Fix shared preview redirecting to admin for an ordinary new draft superseded/currently_public used published_at presence as a proxy for page-row relevance, but published_at is carried forward onto every new draft descended from previously-published content (Page#clone_attributes_from, Node#save_draft!) -- so it read true for any draft on an already-published node, whether or not that specific draft had ever gone live itself. Compare draft_id/head_id identity directly instead; no timestamp involved. A stale (superseded) link now redirects to the live public page rather than an admin URL an anonymous holder can't reach anyway. --- app/controllers/shared_previews_controller.rb | 16 ++++++---- .../controllers/shared_previews_controller_test.rb | 35 ++++++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 test/controllers/shared_previews_controller_test.rb (limited to 'app/controllers/shared_previews_controller.rb') diff --git a/app/controllers/shared_previews_controller.rb b/app/controllers/shared_previews_controller.rb index d482466..f6fb45a 100644 --- a/app/controllers/shared_previews_controller.rb +++ b/app/controllers/shared_previews_controller.rb @@ -3,13 +3,17 @@ class SharedPreviewsController < ApplicationController @page = Page.find_by!(preview_token: params[:token]) node = @page.node - was_published = @page.published_at.present? - superseded = was_published && node && node.head_id != @page.id - currently_public = was_published && node && node.head_id == @page.id && @page.public? + if node + is_head = node.head_id == @page.id + is_draft = node.draft_id == @page.id - if superseded || currently_public - redirect_to node_path(node) - return + currently_public = is_head && @page.public? + superseded = !is_head && !is_draft + + if superseded || currently_public + redirect_to @page.public_link + return + end end response.headers['X-Robots-Tag'] = 'noindex' diff --git a/test/controllers/shared_previews_controller_test.rb b/test/controllers/shared_previews_controller_test.rb new file mode 100644 index 0000000..5f2d20d --- /dev/null +++ b/test/controllers/shared_previews_controller_test.rb @@ -0,0 +1,35 @@ +require 'test_helper' + +class SharedPreviewsControllerTest < ActionController::TestCase + test "renders the preview for a draft that is current but not yet head" do + node = Node.root.children.create!(:slug => "shared_preview_draft_test") + node.draft.ensure_preview_token! + + get :show, params: { :token => node.draft.preview_token } + + assert_response :success + end + + test "renders the preview for a brand-new draft on an already-published node" do + node = Node.root.children.create!(:slug => "shared_preview_published_node_test") + node.publish_draft! + node.find_or_create_draft(User.first) + node.draft.ensure_preview_token! + + get :show, params: { :token => node.draft.preview_token } + + assert_response :success + end + + test "redirects to the live page once the previewed draft has been published and become head" do + node = Node.root.children.create!(:slug => "shared_preview_now_head_test") + node.draft.ensure_preview_token! + token = node.draft.preview_token + + node.publish_draft! + + get :show, params: { :token => token } + + assert_redirected_to node.head.public_link + end +end -- cgit v1.3