summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-08-20 17:37:01 +0200
committererdgeist <erdgeist@erdgeist.org>2026-08-20 17:37:01 +0200
commita1794d3087e6cbd228f3641e7313815d8c59695f (patch)
tree85a9ac0c50faf47a33be57af818942b692530ffc
parent3e77e5fca9f66f5be9c12085fd68c4e9d0ba79be (diff)
Surface redirects in nodes#show, the action log and when publishing
-rw-r--r--app/helpers/admin_helper.rb8
-rw-r--r--app/models/node.rb2
-rw-r--r--app/models/node_action.rb6
-rw-r--r--app/models/page.rb17
-rw-r--r--app/views/nodes/edit.html.erb3
-rw-r--r--app/views/nodes/show.html.erb37
-rw-r--r--app/views/revisions/diff.html.erb19
-rw-r--r--config/locales/de.yml11
-rw-r--r--config/locales/en.yml13
9 files changed, 107 insertions, 9 deletions
diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb
index be20eb8b..b696c21c 100644
--- a/app/helpers/admin_helper.rb
+++ b/app/helpers/admin_helper.rb
@@ -38,4 +38,12 @@ module AdminHelper
38 } 38 }
39 }.to_json 39 }.to_json
40 end 40 end
41
42 def redirect_flag_hint page
43 target = page.redirect_target
44 return t("nodes.show.redirect_flag_broken") unless target
45
46 target.internal? ? t("nodes.show.redirect_flag", :path => target.node.unique_name)
47 : t("nodes.show.redirect_flag_url", :url => target.url)
48 end
41end 49end
diff --git a/app/models/node.rb b/app/models/node.rb
index 52e06d2d..ef981a97 100644
--- a/app/models/node.rb
+++ b/app/models/node.rb
@@ -243,7 +243,7 @@ class Node < ApplicationRecord
243 end 243 end
244 244
245 if self.draft.redirect_node_id.present? 245 if self.draft.redirect_node_id.present?
246 target = Node.find_by(:id => self.draft.redirect_node_id) 246 target = self.draft.redirect_node
247 247
248 unless target 248 unless target
249 errors.add(:base, :redirect_target_missing) 249 errors.add(:base, :redirect_target_missing)
diff --git a/app/models/node_action.rb b/app/models/node_action.rb
index aab1d238..a7036833 100644
--- a/app/models/node_action.rb
+++ b/app/models/node_action.rb
@@ -200,6 +200,12 @@ class NodeAction < ApplicationRecord
200 old_url, new_url = old_page.external_url, new_page.external_url 200 old_url, new_url = old_page.external_url, new_page.external_url
201 diff[:external_url] = { "from" => old_url, "to" => new_url } if old_url != new_url 201 diff[:external_url] = { "from" => old_url, "to" => new_url } if old_url != new_url
202 202
203 old_redirect, new_redirect = old_page.redirect, new_page.redirect
204 diff[:redirect] = { "from" => old_redirect, "to" => new_redirect } if old_redirect != new_redirect
205
206 old_target, new_target = old_page.redirect_node&.unique_name, new_page.redirect_node&.unique_name
207 diff[:redirect_target] = { "from" => old_target, "to" => new_target } if old_target != new_target
208
203 old_tags, new_tags = old_page.tag_list.sort, new_page.tag_list.sort 209 old_tags, new_tags = old_page.tag_list.sort, new_page.tag_list.sort
204 diff[:tags] = { "from" => old_tags, "to" => new_tags } if old_tags != new_tags 210 diff[:tags] = { "from" => old_tags, "to" => new_tags } if old_tags != new_tags
205 211
diff --git a/app/models/page.rb b/app/models/page.rb
index 5d3cb0aa..91b7e975 100644
--- a/app/models/page.rb
+++ b/app/models/page.rb
@@ -36,6 +36,7 @@ class Page < ApplicationRecord
36 belongs_to :parent_node, :class_name => "Node", :optional => true 36 belongs_to :parent_node, :class_name => "Node", :optional => true
37 belongs_to :user, optional: true 37 belongs_to :user, optional: true
38 belongs_to :editor, :class_name => "User", optional: true 38 belongs_to :editor, :class_name => "User", optional: true
39 belongs_to :redirect_node, :class_name => "Node", :optional => true
39 has_many :related_assets, :dependent => :destroy 40 has_many :related_assets, :dependent => :destroy
40 has_many :assets, -> { order("position ASC") }, :through => :related_assets 41 has_many :assets, -> { order("position ASC") }, :through => :related_assets
41 42
@@ -289,7 +290,11 @@ class Page < ApplicationRecord
289 changed: user_id != other.user_id }, 290 changed: user_id != other.user_id },
290 tags: { added: tag_list.to_a - other.tag_list.to_a, removed: other.tag_list.to_a - tag_list.to_a }, 291 tags: { added: tag_list.to_a - other.tag_list.to_a, removed: other.tag_list.to_a - tag_list.to_a },
291 template_name: { from: other.template_name, to: template_name, changed: template_name != other.template_name }, 292 template_name: { from: other.template_name, to: template_name, changed: template_name != other.template_name },
292 assets: { added: assets.to_a - other.assets.to_a, removed: other.assets.to_a - assets.to_a } 293 assets: { added: assets.to_a - other.assets.to_a, removed: other.assets.to_a - assets.to_a },
294 redirect: { from: other.redirect, to: redirect,
295 changed: redirect != other.redirect },
296 redirect_node: { from: other.redirect_node, to: redirect_node,
297 changed: redirect_node_id != other.redirect_node_id }
293 ) 298 )
294 end 299 end
295 300
@@ -326,9 +331,8 @@ class Page < ApplicationRecord
326 return nil if redirect.blank? 331 return nil if redirect.blank?
327 332
328 if redirect_node_id.present? 333 if redirect_node_id.present?
329 target = Node.find_by(:id => redirect_node_id) 334 return nil unless redirect_node&.head
330 return nil unless target&.head 335 return RedirectTarget.new(redirect_node, nil)
331 return RedirectTarget.new(target, nil)
332 end 336 end
333 337
334 return RedirectTarget.new(nil, external_url) if external_url.present? 338 return RedirectTarget.new(nil, external_url) if external_url.present?
@@ -345,6 +349,11 @@ class Page < ApplicationRecord
345 Node.where(:head_id => where(:redirect_node_id => node_id).select(:id)) 349 Node.where(:head_id => where(:redirect_node_id => node_id).select(:id))
346 end 350 end
347 351
352 def redirect_differs_from? other
353 mine, theirs = redirect_target, other&.redirect_target
354 mine&.url != theirs&.url || mine&.node != theirs&.node
355 end
356
348 # The address this page will have once published. 357 # The address this page will have once published.
349 def prospective_unique_name 358 def prospective_unique_name
350 return nil if parent_node_id.nil? 359 return nil if parent_node_id.nil?
diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb
index f629985e..a40f3949 100644
--- a/app/views/nodes/edit.html.erb
+++ b/app/views/nodes/edit.html.erb
@@ -154,8 +154,7 @@
154 <div id="redirect_target_field" style="<%= "display: none;" unless @page.redirect.present? %>"> 154 <div id="redirect_target_field" style="<%= "display: none;" unless @page.redirect.present? %>">
155 <div class="input_group" data-clears="page_redirect_node_id"> 155 <div class="input_group" data-clears="page_redirect_node_id">
156 <span class="field_search_icon"><%= icon("search", library: "tabler", "aria-hidden": true) %></span> 156 <span class="field_search_icon"><%= icon("search", library: "tabler", "aria-hidden": true) %></span>
157 <%= text_field_tag :redirect_search_term, 157 <%= text_field_tag :redirect_search_term, @page.redirect_node&.title,
158 Node.find_by(:id => @page.redirect_node_id)&.title,
159 :placeholder => "—", :class => "clearable_input" %> 158 :placeholder => "—", :class => "clearable_input" %>
160 <div id="redirect_search_results" class="search_results"></div> 159 <div id="redirect_search_results" class="search_results"></div>
161 <button type="button" class="field_clear" aria-label="clear input">×</button> 160 <button type="button" class="field_clear" aria-label="clear input">×</button>
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb
index 24ac4084..d8c08eb1 100644
--- a/app/views/nodes/show.html.erb
+++ b/app/views/nodes/show.html.erb
@@ -9,14 +9,35 @@
9 <div class="info_group_items"> 9 <div class="info_group_items">
10 <div class="info_item"> 10 <div class="info_item">
11 <span class="info_label"><%= t(".head") %></span> 11 <span class="info_label"><%= t(".head") %></span>
12 <% if @node.head&.redirect.present? %>
13 <span class="flag_stack">
14 <%= flag("arrow-ramp-right",
15 redirect_flag_hint(@node.head),
16 :tier => :attention) %>
17 </span>
18 <% end %>
12 <%= @node.head ? t(".head_line", :title => @node.head.title, :rev => @node.head.revision, :time => @node.head.updated_at) : t(".never_published") %> 19 <%= @node.head ? t(".head_line", :title => @node.head.title, :rev => @node.head.revision, :time => @node.head.updated_at) : t(".never_published") %>
13 </div> 20 </div>
14 <div class="info_item"> 21 <div class="info_item">
15 <span class="info_label"><%= t(".draft") %></span> 22 <span class="info_label"><%= t(".draft") %></span>
23 <% if @node.draft&.redirect.present? %>
24 <span class="flag_stack">
25 <%= flag("arrow-ramp-right",
26 redirect_flag_hint(@node.draft),
27 :tier => :attention) %>
28 </span>
29 <% end %>
16 <%= @node.draft ? t(".draft_line", :title => @node.draft.title, :rev => @node.draft.revision, :time => @node.draft.updated_at) : t(".none") %> 30 <%= @node.draft ? t(".draft_line", :title => @node.draft.title, :rev => @node.draft.revision, :time => @node.draft.updated_at) : t(".none") %>
17 </div> 31 </div>
18 <div class="info_item"> 32 <div class="info_item">
19 <span class="info_label"><%= t(".autosave") %></span> 33 <span class="info_label"><%= t(".autosave") %></span>
34 <% if @node.autosave&.redirect.present? %>
35 <span class="flag_stack">
36 <%= flag("arrow-ramp-right",
37 redirect_flag_hint(@node.autosave),
38 :tier => :attention) %>
39 </span>
40 <% end %>
20 <%= @node.autosave ? t(".autosave_line", :title => @node.autosave.title, :time => @node.autosave.updated_at) : t(".none") %> 41 <%= @node.autosave ? t(".autosave_line", :title => @node.autosave.title, :time => @node.autosave.updated_at) : t(".none") %>
21 </div> 42 </div>
22 </div> 43 </div>
@@ -56,6 +77,11 @@
56 <% confirm += " " + t(".confirm_publish_moves", 77 <% confirm += " " + t(".confirm_publish_moves",
57 :path => @node.prospective_unique_name) %> 78 :path => @node.prospective_unique_name) %>
58 <% end %> 79 <% end %>
80 <% if @node.draft.redirect.present? && @node.head&.redirect.blank? %>
81 <% confirm += " " + t(".confirm_publish_redirects") %>
82 <% elsif @node.draft.redirect.present? && @node.draft.redirect_differs_from?(@node.head) %>
83 <% confirm += " " + t(".confirm_publish_redirect_changes") %>
84 <% end %>
59 <%= button_to t(".publish"), publish_node_path(@node), method: :put, 85 <%= button_to t(".publish"), publish_node_path(@node), method: :put,
60 form: { data: { confirm: confirm }, class: 'button_to state_changing' } %> 86 form: { data: { confirm: confirm }, class: 'button_to state_changing' } %>
61 <% else %> 87 <% else %>
@@ -246,6 +272,17 @@
246 </p> 272 </p>
247 <% end %> 273 <% end %>
248 274
275 <% if (redirect = @node.head&.redirect_target) %>
276 <p class="pending_address">
277 <%= icon("arrow-right", library: "tabler", "aria-hidden": true) %>
278 <% if redirect.internal? %>
279 <%= t(".redirects_to", :path => redirect.node.unique_name) %>
280 <% else %>
281 <%= t(".redirects_to_url", :url => redirect.url) %>
282 <% end %>
283 </p>
284 <% end %>
285
249 <div class="link_matrix link_matrix_<%= link_locales.size %>"> 286 <div class="link_matrix link_matrix_<%= link_locales.size %>">
250 <div class="link_matrix_head"> 287 <div class="link_matrix_head">
251 <span></span> 288 <span></span>
diff --git a/app/views/revisions/diff.html.erb b/app/views/revisions/diff.html.erb
index 17146615..eb7955c2 100644
--- a/app/views/revisions/diff.html.erb
+++ b/app/views/revisions/diff.html.erb
@@ -74,7 +74,8 @@
74 @diff[:template_name][:changed] || 74 @diff[:template_name][:changed] ||
75 @diff[:published_at][:changed] || @diff[:user][:changed] || 75 @diff[:published_at][:changed] || @diff[:user][:changed] ||
76 @diff[:tags][:added].any? || @diff[:tags][:removed].any? || 76 @diff[:tags][:added].any? || @diff[:tags][:removed].any? ||
77 @diff[:assets][:added].any? || @diff[:assets][:removed].any? %> 77 @diff[:assets][:added].any? || @diff[:assets][:removed].any? ||
78 @diff[:redirect][:changed] || @diff[:redirect_node][:changed] %>
78 79
79 <div class="diff_preamble"> 80 <div class="diff_preamble">
80 <div class="diff_meta"> 81 <div class="diff_meta">
@@ -98,6 +99,22 @@
98 </p> 99 </p>
99 <% end %> 100 <% end %>
100 101
102 <% if @diff[:redirect][:changed] %>
103 <h3><%= Page.human_attribute_name(:redirect) %></h3>
104 <p>
105 <del><%= @diff[:redirect][:from] ? t("nodes.edit.redirect_mode_#{@diff[:redirect][:from]}") : t(".none_marker") %></del>
106 <ins><%= @diff[:redirect][:to] ? t("nodes.edit.redirect_mode_#{@diff[:redirect][:to]}") : t(".none_marker") %></ins>
107 </p>
108 <% end %>
109
110 <% if @diff[:redirect_node][:changed] %>
111 <h3><%= Page.human_attribute_name(:redirect_node) %></h3>
112 <p>
113 <del><%= @diff[:redirect_node][:from]&.unique_name || t(".none_marker") %></del>
114 <ins><%= @diff[:redirect_node][:to]&.unique_name || t(".none_marker") %></ins>
115 </p>
116 <% end %>
117
101 <% if @diff[:published_at][:changed] %> 118 <% if @diff[:published_at][:changed] %>
102 <h3><%= Page.human_attribute_name(:published_at) %></h3> 119 <h3><%= Page.human_attribute_name(:published_at) %></h3>
103 <p> 120 <p>
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 95d6edc2..1d8d7090 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -107,6 +107,8 @@ de:
107 template_name: "Template" 107 template_name: "Template"
108 assets: "Anhänge" 108 assets: "Anhänge"
109 external_url: "Externe Homepage" 109 external_url: "Externe Homepage"
110 redirect: "Weiterleitung"
111 redirect_node: "Weiterleitungsziel"
110 published_at: "Veröffentlichungsdatum" 112 published_at: "Veröffentlichungsdatum"
111 user: "Autor" 113 user: "Autor"
112 menu_item: 114 menu_item:
@@ -459,6 +461,8 @@ de:
459 redirect_hint: "Ohne Ziel-Node wird auf die externe Homepage weitergeleitet." 461 redirect_hint: "Ohne Ziel-Node wird auf die externe Homepage weitergeleitet."
460 redirect_target_hint: "Bleibt erhalten, wenn die Weiterleitung abgeschaltet wird." 462 redirect_target_hint: "Bleibt erhalten, wenn die Weiterleitung abgeschaltet wird."
461 redirect_clear: "Ziel entfernen" 463 redirect_clear: "Ziel entfernen"
464 redirect_mode_temporary: "vorübergehend"
465 redirect_mode_permanent: "dauerhaft"
462 show: 466 show:
463 status: "Status" 467 status: "Status"
464 head: "Head" 468 head: "Head"
@@ -480,6 +484,8 @@ de:
480 publish: "Veröffentlichen" 484 publish: "Veröffentlichen"
481 confirm_publish: "Diesen Entwurf veröffentlichen?" 485 confirm_publish: "Diesen Entwurf veröffentlichen?"
482 confirm_publish_moves: "Dabei wird die Seite nach %{path} verschoben." 486 confirm_publish_moves: "Dabei wird die Seite nach %{path} verschoben."
487 confirm_publish_redirects: "Danach zeigt die Seite ihren Inhalt nicht mehr, sondern leitet weiter."
488 confirm_publish_redirect_changes: "Das Ziel der Weiterleitung ändert sich."
483 destroy_draft: "Entwurf löschen" 489 destroy_draft: "Entwurf löschen"
484 discard_autosave: "Autosave verwerfen" 490 discard_autosave: "Autosave verwerfen"
485 confirm_trash: "Diese Seite und alles darunter in den Papierkorb verschieben? Alle veröffentlichten Inhalte darin gehen offline." 491 confirm_trash: "Diese Seite und alles darunter in den Papierkorb verschieben? Alle veröffentlichten Inhalte darin gehen offline."
@@ -516,6 +522,11 @@ de:
516 links: "Links" 522 links: "Links"
517 public: "Öffentlich" 523 public: "Öffentlich"
518 pending_address: "Beim Veröffentlichen wird diese Seite nach %{path} verschoben" 524 pending_address: "Beim Veröffentlichen wird diese Seite nach %{path} verschoben"
525 redirects_to: "Diese Seite leitet auf %{path} weiter."
526 redirects_to_url: "Diese Seite leitet auf %{url} weiter."
527 redirect_flag: "Leitet auf %{path} weiter"
528 redirect_flag_url: "Leitet auf %{url} weiter"
529 redirect_flag_broken: "Weiterleitung ohne gültiges Ziel"
519 admin_preview: "Admin-Vorschau" 530 admin_preview: "Admin-Vorschau"
520 public_preview: "Öffentliche Vorschau" 531 public_preview: "Öffentliche Vorschau"
521 revoke: "Zurückziehen" 532 revoke: "Zurückziehen"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 8b67f0ed..9fab0f58 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -58,6 +58,8 @@ en:
58 template_name: "Template" 58 template_name: "Template"
59 assets: "Assets" 59 assets: "Assets"
60 external_url: "External homepage" 60 external_url: "External homepage"
61 redirect: "Redirect"
62 redirect_node: "Redirect node"
61 published_at: "Publication date" 63 published_at: "Publication date"
62 user: "Author" 64 user: "Author"
63 menu_item: 65 menu_item:
@@ -411,6 +413,8 @@ en:
411 redirect_hint: "If the taget node is empty, the external url will be used." 413 redirect_hint: "If the taget node is empty, the external url will be used."
412 redirect_target_hint: "Disabling does not clear the destination" 414 redirect_target_hint: "Disabling does not clear the destination"
413 redirect_clear: "Clear redirect" 415 redirect_clear: "Clear redirect"
416 redirect_mode_temporary: "temporary"
417 redirect_mode_permanent: "permanent"
414 show: 418 show:
415 status: "Status" 419 status: "Status"
416 head: "Head" 420 head: "Head"
@@ -432,11 +436,16 @@ en:
432 publish: "Publish" 436 publish: "Publish"
433 confirm_publish: "Publish this draft?" 437 confirm_publish: "Publish this draft?"
434 confirm_publish_moves: "This will also move the page to %{path}." 438 confirm_publish_moves: "This will also move the page to %{path}."
439 confirm_publish_redirects: "This will make the page redirect elsewhere."
440 confirm_publish_redirect_changes: "This will change the page's redirect target."
435 destroy_draft: "Destroy Draft" 441 destroy_draft: "Destroy Draft"
436 discard_autosave: "Discard Autosave" 442 discard_autosave: "Discard Autosave"
437 confirm_trash: "Move this page and everything beneath it to the Trash? All published content in it will go offline." 443 confirm_trash: "Move this page and everything beneath it to the Trash? All published content in it will go offline."
438 move_to_trash: "Move to Trash" 444 move_to_trash: "Move to Trash"
439 locked_hint: "Locked — see People below to unlock before editing, publishing, or discarding." 445 locked_hint: "Locked — see People below to unlock before editing, publishing, or discarding."
446 redirect_flag: "Redirects to %{path}"
447 redirect_flag_url: "Redirects to %{url}"
448 redirect_flag_broken: "Broken redirect"
440 visit: "Visit" 449 visit: "Visit"
441 copy_url: "Copy URL" 450 copy_url: "Copy URL"
442 trash: "Trash" 451 trash: "Trash"
@@ -467,7 +476,9 @@ en:
467 will_publish_at: "Will publish at" 476 will_publish_at: "Will publish at"
468 links: "Links" 477 links: "Links"
469 public: "Public" 478 public: "Public"
470 pending_address: "Beim Veröffentlichen wird diese Seite nach %{path} verschoben" 479 pending_address: "Publishing will move this page to %{path}"
480 redirects_to: "This page redirects to %{path}"
481 redirects_to_url: "This page redirects to %{url}"
471 admin_preview: "Admin Preview" 482 admin_preview: "Admin Preview"
472 public_preview: "Public Preview" 483 public_preview: "Public Preview"
473 revoke: "Revoke" 484 revoke: "Revoke"