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/nodes_controller.rb | 18 ++++++++++++++++++ app/controllers/shared_previews_controller.rb | 13 +++++++++++++ app/views/nodes/show.html.erb | 17 +++++++++++++++++ config/routes.rb | 4 ++++ 4 files changed, 52 insertions(+) create mode 100644 app/controllers/shared_previews_controller.rb diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index 380a659..69aa268 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb @@ -104,6 +104,24 @@ class NodesController < ApplicationController redirect_to node_path(@node) end + def generate_shared_preview + @node = Node.find(params[:id]) + if @node.draft + @node.draft.ensure_preview_token! + flash[:notice] = "Shareable preview link created - see below." + else + flash[:notice] = "Create or edit a draft first - shared preview links are only available for pages with an active draft." + end + redirect_to node_path(@node) + end + + def revoke_shared_preview + @node = Node.find(params[:id]) + @node.draft.revoke_preview_token! if @node.draft + flash[:notice] = "Shareable preview link revoked." + redirect_to node_path(@node) + end + def parameterize_preview render plain: slug_for(params[:title]) end 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 diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index c533a55..d63df86 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb @@ -12,6 +12,23 @@ Public Link <%= link_to @page.public_link, content_url(@node.unique_path) %> + <% if @node.draft %> + + Admin Preview + <%= link_to preview_page_path(@node.draft), preview_page_path(@node.draft) %> + + + Public Preview + + <% if @node.draft.preview_token.present? %> + <%= link_to shared_preview_path(token: @node.draft.preview_token), shared_preview_url(token: @node.draft.preview_token), target: "_blank", rel: "noopener" %> + <%= button_to 'Revoke', revoke_shared_preview_node_path(@node), method: :put, form: { data: { confirm: "Revoke this preview link? Anyone currently using it will immediately lose access." } } %> + <% else %> + <%= button_to 'Create public preview link', generate_shared_preview_node_path(@node), method: :put %> + <% end %> + + + <% end %> Author <%= @page.user.try(:login) %> diff --git a/config/routes.rb b/config/routes.rb index 6c07414..26c3d4d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,6 +38,8 @@ Cccms::Application.routes.draw do member do put :unlock put :publish + put :generate_shared_preview + put :revoke_shared_preview end resources :revisions do @@ -50,6 +52,8 @@ Cccms::Application.routes.draw do end end + get 'preview/:token', to: 'shared_previews#show', as: :shared_preview + scope '/admin' do resources :assets end -- cgit v1.3