Merge pull request #2318 from basecamp/relative-urls
Use relative URLs where possible across all the app
This commit is contained in:
@@ -8,7 +8,7 @@ class Users::AvatarsController < ApplicationController
|
||||
if @user.system?
|
||||
redirect_to view_context.image_path("system_user.png")
|
||||
elsif @user.avatar.attached?
|
||||
redirect_to rails_blob_url(@user.avatar_thumbnail, disposition: "inline")
|
||||
redirect_to rails_blob_path(@user.avatar_thumbnail, disposition: "inline")
|
||||
elsif stale? @user, cache_control: cache_control
|
||||
render_initials
|
||||
end
|
||||
|
||||
@@ -42,6 +42,6 @@ module AvatarsHelper
|
||||
end
|
||||
|
||||
def avatar_image_tag(user, **options)
|
||||
image_tag user_avatar_url(user, script_name: user.account.slug), aria: { hidden: "true" }, size: 48, title: user.name, **options
|
||||
image_tag user_avatar_path(user, script_name: user.account.slug), aria: { hidden: "true" }, size: 48, title: user.name, **options
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
height: height %>
|
||||
<% elsif blob.audio? %>
|
||||
<audio controls="true" width="100%" preload="metadata">
|
||||
<source src="<%= rails_blob_url(blob) %>" type="<%= blob.content_type %>">
|
||||
<source src="<%= rails_blob_path(blob) %>" type="<%= blob.content_type %>">
|
||||
</audio>
|
||||
<% elsif blob.variable? %>
|
||||
<%= link_to url_for(blob.variant(variant)), data: { lightbox_target: "image", lightbox_caption_value: blob.filename.to_s } do %>
|
||||
<%= image_tag url_for(blob.variant(variant)), width: width, height: height %>
|
||||
<%= link_to rails_representation_path(blob.variant(variant)), data: { lightbox_target: "image", lightbox_caption_value: blob.filename.to_s } do %>
|
||||
<%= image_tag rails_representation_path(blob.variant(variant)), width: width, height: height %>
|
||||
<% end %>
|
||||
<% elsif blob.previewable? %>
|
||||
<%= image_tag url_for(blob.preview(variant)), width: width, height: height %>
|
||||
<%= image_tag rails_representation_path(blob.preview(variant)), width: width, height: height %>
|
||||
<% else %>
|
||||
<span class="attachment__icon"><%= blob.filename.extension&.downcase.presence || "unknown" %></span>
|
||||
<% end %>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<% height = attachment.metadata["height"] %>
|
||||
|
||||
<% if attachment.previewable? %>
|
||||
<%= image_tag url_for(attachment.preview(variant)), class: "attachment attachment--image", width: width, height: height %>
|
||||
<%= image_tag rails_representation_path(attachment.preview(variant)), class: "attachment attachment--image", width: width, height: height %>
|
||||
<% elsif attachment.variable? %>
|
||||
<%= image_tag url_for(attachment.variant(variant)), class: "attachment attachment--image", width: width, height: height %>
|
||||
<%= image_tag rails_representation_path(attachment.variant(variant)), class: "attachment attachment--image", width: width, height: height %>
|
||||
<% else %>
|
||||
<div class="attachment attachment--file attachment--<%= attachment.filename.extension -%>">
|
||||
<span class="attachment__icon"><%= attachment.filename.extension&.downcase.presence || "unknown" %></span>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<%= collapsible_nav_section "Accounts" do %>
|
||||
<%# Bust cache 1 Dec 2025 %>
|
||||
<% accounts.each do |account| %>
|
||||
<%= filter_place_menu_item landing_url(script_name: account.slug), account.name, "marker", current: account == Current.account, turbo: false %>
|
||||
<%= filter_place_menu_item landing_path(script_name: account.slug), account.name, "marker", current: account == Current.account, turbo: false %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<%= tag.li class: "popup__item", data: { filter_target: "item", navigable_list_target: "item" } do %>
|
||||
<%= icon_tag "logout", class: "popup__icon" %>
|
||||
<%= button_to session_url(script_name: nil), method: :delete, class: "popup__btn btn", data: { turbo: false } do %>
|
||||
<%= button_to session_path(script_name: nil), method: :delete, class: "popup__btn btn", data: { turbo: false } do %>
|
||||
<span>Sign out</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
#<%= card.number %> <%= card.title %>
|
||||
</template>
|
||||
<template type="editor">
|
||||
<%= link_to "##{card.number} #{card.title}", card_url(card) %>
|
||||
<%= link_to "##{card.number} #{card.title}", card_path(card) %>
|
||||
</template>
|
||||
</lexxy-prompt-item>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<% @accounts.each do |account| %>
|
||||
<li class="popup__item txt-medium">
|
||||
<%= icon_tag "marker", class: "popup__icon" %>
|
||||
<%= link_to landing_url(script_name: account.slug), class: "btn overflow-ellipsis fill-transparent popup__btn" do %>
|
||||
<%= link_to landing_path(script_name: account.slug), class: "btn overflow-ellipsis fill-transparent popup__btn" do %>
|
||||
<strong><%= account.name %></strong>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<% if Current.user == @user %>
|
||||
<hr class="separator--horizontal full-width flex-item-grow margin-block-start-double" style="--border-color: var(--color-ink-light);" aria-hidden="true">
|
||||
|
||||
<%= button_to session_url(script_name: nil), method: :delete, class: "btn txt-x-small center", data: { turbo: false } do %>
|
||||
<%= button_to session_path(script_name: nil), method: :delete, class: "btn txt-x-small center", data: { turbo: false } do %>
|
||||
<span>Sign out of Fizzy on this device</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%= @event.description_for(Current.user).to_plain_text %>
|
||||
<% if @event.eventable %>
|
||||
<%= link_to "↗︎", polymorphic_url(@event.eventable) %>
|
||||
<%= link_to "↗︎", polymorphic_url(@event.eventable, only_path: true) %>
|
||||
<% end %>
|
||||
|
||||
@@ -8,6 +8,17 @@ ActiveSupport.on_load(:active_storage_blob) do
|
||||
end
|
||||
end
|
||||
|
||||
ActiveSupport.on_load(:action_text_content) do
|
||||
# Install our extensions after ActionText::Engine's
|
||||
ActiveSupport.on_load(:active_storage_blob) do
|
||||
# Ensure all <action-text-attachment>s have a "url" attribute that's a relative
|
||||
# path (for portability across host name changes, beta environments, etc).
|
||||
def to_rich_text_attributes(*)
|
||||
super.merge url: Rails.application.routes.url_helpers.polymorphic_url(self, only_path: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Don't configure replica connections for ActiveStorage::Record.
|
||||
# When ActiveStorage uses `connects_to`, it creates a separate connection pool
|
||||
# from ApplicationRecord. This causes after_commit callbacks to fire in
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
# Convert absolute attachment URLs in rich text content to relative paths.
|
||||
# This fixes URLs that were stored with full hostnames (e.g., https://app.fizzy.do/...)
|
||||
# making them portable across beta environments and host changes.
|
||||
#
|
||||
# MUST BE RUN AFTER `decrypt!` when using ActiveRecord Encryption
|
||||
#
|
||||
# Run locally:
|
||||
# bin/rails runner script/migrations/convert-absolute-attachment-urls-to-relative.rb --help
|
||||
#
|
||||
# Run via Kamal:
|
||||
# kamal app exec -d <stage> -p --reuse "bin/rails runner script/migrations/convert-absolute-attachment-urls-to-relative.rb --help"
|
||||
#
|
||||
# Safe to re-run: won't modify already-relative URLs
|
||||
|
||||
class ConvertAbsoluteAttachmentUrlsToRelative
|
||||
# Match absolute URLs pointing to Active Storage routes, keeping the account slug
|
||||
ABSOLUTE_URL_PATTERN = %r{https?://[^/]+(/\d+/rails/active_storage/[^"']+)}
|
||||
|
||||
attr_reader :account, :dry_run
|
||||
|
||||
def initialize(account_id: nil, dry_run: false)
|
||||
@account = Account.find_by(external_account_id: account_id)
|
||||
@dry_run = dry_run
|
||||
end
|
||||
|
||||
def run
|
||||
puts "Converting absolute attachment URLs to relative paths"
|
||||
puts dry_run ? "DRY RUN MODE - no changes will be saved" : "LIVE MODE - changes will be saved"
|
||||
puts account ? "Only account: #{account.external_account_id} - #{account.name}" : "For **ALL ACCOUNTS**"
|
||||
|
||||
puts "\nPress ENTER to continue running or CTRL-C to bail..."
|
||||
gets
|
||||
|
||||
puts "\nRunning..."
|
||||
|
||||
# Suppress SQL logs
|
||||
Rails.event.debug_mode = false
|
||||
|
||||
seconds = Benchmark.realtime do
|
||||
suppressing_turbo_broadcasts do
|
||||
convert_urls
|
||||
end
|
||||
end
|
||||
|
||||
puts "\n\n"
|
||||
puts "Finished in %.2f seconds." % seconds
|
||||
end
|
||||
|
||||
private
|
||||
def suppressing_turbo_broadcasts
|
||||
Board.suppressing_turbo_broadcasts do
|
||||
Card.suppressing_turbo_broadcasts do
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def convert_urls
|
||||
scanned = 0
|
||||
fixed = 0
|
||||
urls_converted = 0
|
||||
|
||||
action_texts_scope.find_each do |rich_text|
|
||||
scanned += 1
|
||||
|
||||
body = rich_text.body
|
||||
|
||||
edited = false
|
||||
conversions = 0
|
||||
|
||||
body.send(:attachment_nodes).each do |node|
|
||||
url = node["url"]
|
||||
next unless url
|
||||
|
||||
if url.match?(ABSOLUTE_URL_PATTERN)
|
||||
node["url"] = url.gsub(ABSOLUTE_URL_PATTERN, '\1')
|
||||
edited = true
|
||||
conversions += 1
|
||||
end
|
||||
end
|
||||
|
||||
if edited
|
||||
record = rich_text.record
|
||||
puts " - modifying #{record.class.name} #{record.to_param} (account: #{record.account&.external_account_id}) - #{conversions} URL(s)"
|
||||
|
||||
unless dry_run
|
||||
rich_text.update! body: body.fragment.to_html
|
||||
end
|
||||
|
||||
fixed += 1
|
||||
urls_converted += conversions
|
||||
end
|
||||
end
|
||||
|
||||
puts "\n\nConversion complete!"
|
||||
puts " Rich texts examined: #{scanned}"
|
||||
puts " Rich texts modified: #{fixed}"
|
||||
puts " URLs converted: #{urls_converted}"
|
||||
end
|
||||
|
||||
def action_texts_scope
|
||||
# Only examine rich texts that have embedded attachments
|
||||
scope = ActionText::RichText.joins(:embeds_attachments)
|
||||
scope = scope.where(account: account) if account
|
||||
scope
|
||||
end
|
||||
end
|
||||
|
||||
require "optparse"
|
||||
|
||||
options = { account_id: nil, dry_run: true }
|
||||
|
||||
OptionParser.new do |opts|
|
||||
opts.banner = "Usage: bin/rails runner #{__FILE__} [options]"
|
||||
|
||||
opts.on("-a", "--account ACCOUNT_ID", "Restrict to a specific account (external_account_id)") do |id|
|
||||
options[:account_id] = id
|
||||
end
|
||||
|
||||
opts.on("--[no-]dry-run", "Run in dry-run mode (default: --dry-run)") do |v|
|
||||
options[:dry_run] = v
|
||||
end
|
||||
|
||||
opts.on("-h", "--help", "Show this help message") do
|
||||
puts opts
|
||||
exit
|
||||
end
|
||||
end.parse!
|
||||
|
||||
ConvertAbsoluteAttachmentUrlsToRelative.new(**options).run
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
# Rollback script: Convert relative attachment URLs back to absolute URLs.
|
||||
# Use this if you need to rollback the relative URL changes and want to
|
||||
# convert rich texts created during the rollout back to absolute URLs.
|
||||
#
|
||||
# Run locally:
|
||||
# bin/rails runner script/migrations/convert-relative-attachment-urls-to-absolute.rb --help
|
||||
#
|
||||
# Run via Kamal:
|
||||
# kamal app exec -d <stage> -p --reuse "bin/rails runner script/migrations/convert-relative-attachment-urls-to-absolute.rb --help"
|
||||
|
||||
class ConvertRelativeAttachmentUrlsToAbsolute
|
||||
# Match relative URLs pointing to Active Storage routes (with account slug)
|
||||
RELATIVE_URL_PATTERN = %r{\A(/\d+/rails/active_storage/[^"']+)\z}
|
||||
|
||||
attr_reader :host, :since
|
||||
|
||||
def initialize(host:, since:)
|
||||
@host = host
|
||||
@since = since
|
||||
end
|
||||
|
||||
def run
|
||||
puts "Converting relative attachment URLs to absolute URLs"
|
||||
puts "Host: #{host}"
|
||||
puts "Processing rich texts created since: #{since}"
|
||||
|
||||
puts "\nPress ENTER to continue running or CTRL-C to bail..."
|
||||
gets
|
||||
|
||||
puts "\nRunning..."
|
||||
|
||||
seconds = Benchmark.realtime do
|
||||
suppressing_turbo_broadcasts do
|
||||
convert_urls
|
||||
end
|
||||
end
|
||||
|
||||
puts "\n\n"
|
||||
puts "Finished in %.2f seconds." % seconds
|
||||
end
|
||||
|
||||
private
|
||||
def suppressing_turbo_broadcasts
|
||||
Board.suppressing_turbo_broadcasts do
|
||||
Card.suppressing_turbo_broadcasts do
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def convert_urls
|
||||
scanned = 0
|
||||
fixed = 0
|
||||
urls_converted = 0
|
||||
|
||||
action_texts_scope.find_each do |rich_text|
|
||||
scanned += 1
|
||||
|
||||
body = rich_text.body
|
||||
|
||||
edited = false
|
||||
conversions = 0
|
||||
|
||||
body.send(:attachment_nodes).each do |node|
|
||||
url = node["url"]
|
||||
next unless url
|
||||
|
||||
if url.match?(RELATIVE_URL_PATTERN)
|
||||
node["url"] = "#{host}#{url}"
|
||||
edited = true
|
||||
conversions += 1
|
||||
end
|
||||
end
|
||||
|
||||
if edited
|
||||
record = rich_text.record
|
||||
puts " - modifying #{record.class.name} #{record.to_param} (account: #{record.account&.external_account_id}) - #{conversions} URL(s)"
|
||||
|
||||
rich_text.update! body: body.fragment.to_html
|
||||
|
||||
fixed += 1
|
||||
urls_converted += conversions
|
||||
end
|
||||
end
|
||||
|
||||
puts "\n\nConversion complete!"
|
||||
puts " Rich texts examined: #{scanned}"
|
||||
puts " Rich texts modified: #{fixed}"
|
||||
puts " URLs converted: #{urls_converted}"
|
||||
end
|
||||
|
||||
def action_texts_scope
|
||||
ActionText::RichText.joins(:embeds_attachments).where("action_text_rich_texts.created_at >= ?", since)
|
||||
end
|
||||
end
|
||||
|
||||
require "optparse"
|
||||
require "time"
|
||||
|
||||
options = {}
|
||||
|
||||
OptionParser.new do |opts|
|
||||
opts.banner = "Usage: bin/rails runner #{__FILE__} [options]"
|
||||
|
||||
opts.on("--host HOST", "Host to prepend (e.g., https://app.fizzy.do)") do |host|
|
||||
options[:host] = host
|
||||
end
|
||||
|
||||
opts.on("--since TIME", "Process rich texts created since this time (ISO 8601 format)") do |time|
|
||||
options[:since] = Time.parse(time)
|
||||
end
|
||||
|
||||
opts.on("-h", "--help", "Show this help message") do
|
||||
puts opts
|
||||
exit
|
||||
end
|
||||
end.parse!
|
||||
|
||||
if options[:host].nil? || options[:since].nil?
|
||||
puts "Error: --host and --since are required"
|
||||
puts "Example: bin/rails runner #{__FILE__} --host https://app.fizzy.do --since 2026-01-14T10:00:00Z"
|
||||
exit 1
|
||||
end
|
||||
|
||||
ConvertRelativeAttachmentUrlsToAbsolute.new(**options).run
|
||||
Reference in New Issue
Block a user