-
Notifications
You must be signed in to change notification settings - Fork 5
Add sitemap generator and serve sitemap from S3 #1037
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module BetterTogether | ||
| # Serves the generated sitemap stored in Active Storage | ||
| class SitemapsController < ApplicationController | ||
| def show | ||
| sitemap = Sitemap.current(helpers.host_platform) | ||
| if sitemap.file.attached? | ||
| redirect_to sitemap.file.url, allow_other_host: true | ||
| else | ||
| head :not_found | ||
| end | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'rake' | ||
|
|
||
| module BetterTogether | ||
| # Generates the sitemap in a background job so newly published pages are included | ||
| class SitemapRefreshJob < ApplicationJob | ||
| queue_as :default | ||
|
|
||
| def perform | ||
| Rails.application.load_tasks unless Rake::Task.task_defined?('sitemap:refresh') | ||
| Rake::Task['sitemap:refresh'].invoke | ||
| Rake::Task['sitemap:refresh'].reenable | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module BetterTogether | ||
| # Stores the generated sitemap in Active Storage for serving via S3 | ||
| class Sitemap < ApplicationRecord | ||
| belongs_to :platform | ||
|
|
||
| has_one_attached :file | ||
|
|
||
| validates :platform_id, uniqueness: true | ||
|
|
||
| def self.current(platform) | ||
| find_or_create_by!(platform: platform) | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |||||
| <% end %> | ||||||
| <%= csrf_meta_tags %> | ||||||
| <%= csp_meta_tag %> | ||||||
| <link rel="sitemap" type="application/xml" href="<%= sitemap_path %>"> | ||||||
|
||||||
| <link rel="sitemap" type="application/xml" href="<%= sitemap_path %>"> | |
| <link rel="sitemap" type="application/xml" href="<%= sitemap_path(locale: I18n.locale) %>"> |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,7 +2,9 @@ | |||||||||||||||||||
|
|
||||||||||||||||||||
| require 'sidekiq/web' | ||||||||||||||||||||
|
|
||||||||||||||||||||
| BetterTogether::Engine.routes.draw do # rubocop:todo Metrics/BlockLength | ||||||||||||||||||||
| BetterTogether::Engine.routes.draw do # rubocop:todo Metrics/BlockLength | ||||||||||||||||||||
| get '/sitemap.xml.gz', to: 'sitemaps#show', as: :sitemap | ||||||||||||||||||||
|
|
||||||||||||||||||||
| scope ':locale', # rubocop:todo Metrics/BlockLength | ||||||||||||||||||||
| locale: /#{I18n.available_locales.join('|')}/ do | ||||||||||||||||||||
|
Comment on lines
+6
to
9
|
||||||||||||||||||||
| get '/sitemap.xml.gz', to: 'sitemaps#show', as: :sitemap | |
| scope ':locale', # rubocop:todo Metrics/BlockLength | |
| locale: /#{I18n.available_locales.join('|')}/ do | |
| scope ':locale', # rubocop:todo Metrics/BlockLength | |
| locale: /#{I18n.available_locales.join('|')}/ do | |
| get '/sitemap.xml.gz', to: 'sitemaps#show', as: :sitemap |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,34 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # frozen_string_literal: true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SitemapGenerator::Sitemap.default_host = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "#{ENV.fetch('APP_PROTOCOL', 'http')}://#{ENV.fetch('APP_HOST', 'localhost:3000')}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| helpers = BetterTogether::Engine.routes.url_helpers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SitemapGenerator::Sitemap.create do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| add helpers.home_page_path(locale: I18n.default_locale) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| add helpers.communities_path(locale: I18n.default_locale) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BetterTogether::Community.find_each do |community| | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| add helpers.community_path(community, locale: I18n.default_locale), lastmod: community.updated_at | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| add helpers.conversations_path(locale: I18n.default_locale) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BetterTogether::Conversation.find_each do |conversation| | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| add helpers.conversation_path(conversation, locale: I18n.default_locale), lastmod: conversation.updated_at | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| add helpers.posts_path(locale: I18n.default_locale) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BetterTogether::Post.published.find_each do |post| | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| add helpers.post_path(post, locale: I18n.default_locale), lastmod: post.updated_at | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| add helpers.events_path(locale: I18n.default_locale) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BetterTogether::Event.find_each do |event| | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+12
to
+27
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BetterTogether::Community.find_each do |community| | |
| add helpers.community_path(community, locale: I18n.default_locale), lastmod: community.updated_at | |
| end | |
| add helpers.conversations_path(locale: I18n.default_locale) | |
| BetterTogether::Conversation.find_each do |conversation| | |
| add helpers.conversation_path(conversation, locale: I18n.default_locale), lastmod: conversation.updated_at | |
| end | |
| add helpers.posts_path(locale: I18n.default_locale) | |
| BetterTogether::Post.published.find_each do |post| | |
| add helpers.post_path(post, locale: I18n.default_locale), lastmod: post.updated_at | |
| end | |
| add helpers.events_path(locale: I18n.default_locale) | |
| BetterTogether::Event.find_each do |event| | |
| BetterTogether::Community.privacy_public.find_each do |community| | |
| add helpers.community_path(community, locale: I18n.default_locale), lastmod: community.updated_at | |
| end | |
| # Conversations are private and should not be included in the sitemap. | |
| add helpers.posts_path(locale: I18n.default_locale) | |
| BetterTogether::Post.published.privacy_public.find_each do |post| | |
| add helpers.post_path(post, locale: I18n.default_locale), lastmod: post.updated_at | |
| end | |
| add helpers.events_path(locale: I18n.default_locale) | |
| BetterTogether::Event.privacy_public.find_each do |event| |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing after_commit :refresh_sitemap callbacks for Post, Community, and Event models. Currently, only the Page model (line 60 in app/models/better_together/page.rb) triggers sitemap regeneration on changes. Since the sitemap includes Posts, Communities, and Events (config/sitemap.rb lines 22, 12, 27), these models should also trigger sitemap refresh when created, updated, or destroyed to keep the sitemap current.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class CreateBetterTogetherSitemaps < ActiveRecord::Migration[7.1] | ||
| def change | ||
| create_bt_table :sitemaps do |t| | ||
| t.bt_references :platform, | ||
| null: false, | ||
| index: { unique: true, name: 'unique_sitemaps_platform' } | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,29 @@ | ||||||||||||||||||||
| # frozen_string_literal: true | ||||||||||||||||||||
|
|
||||||||||||||||||||
| namespace :sitemap do | ||||||||||||||||||||
| desc 'Generate sitemap and upload to Active Storage' | ||||||||||||||||||||
| task refresh: :environment do | ||||||||||||||||||||
| require 'sitemap_generator' | ||||||||||||||||||||
|
|
||||||||||||||||||||
| SitemapGenerator::Sitemap.public_path = Rails.root.join('tmp') | ||||||||||||||||||||
| SitemapGenerator::Sitemap.sitemaps_path = '' | ||||||||||||||||||||
|
|
||||||||||||||||||||
| load Rails.root.join('config/sitemap.rb') | ||||||||||||||||||||
|
||||||||||||||||||||
| load Rails.root.join('config/sitemap.rb') | |
| load BetterTogether::Engine.root.join('config/sitemap.rb') |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rake task hooks into assets:precompile (lines 24-26) which may not be appropriate for sitemap generation. Asset precompilation typically happens during deployment build phase, before the database is available. Sitemap generation requires database access to query Pages, Posts, Events, etc. This could cause deployment failures if the database isn't accessible during asset precompilation. Consider using a different hook (e.g., after deployment) or making this integration optional.
| begin | |
| Rake::Task['assets:precompile'].enhance do | |
| Rake::Task['sitemap:refresh'].invoke | |
| end | |
| rescue RuntimeError | |
| # assets:precompile may not be defined in some environments | |
| end | |
| # The automatic invocation of sitemap:refresh during assets:precompile has been removed. | |
| # To generate the sitemap, run `rake sitemap:refresh` after deployment when the database is available. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'rails_helper' | ||
| require 'zlib' | ||
|
|
||
| RSpec.describe BetterTogether::SitemapRefreshJob, type: :job do | ||
| it 'generates and attaches a sitemap' do | ||
| host_platform = create(:platform, :host) | ||
| BetterTogether::Sitemap.destroy_all | ||
|
|
||
| described_class.new.perform | ||
|
|
||
| expect(BetterTogether::Sitemap.current(host_platform).file).to be_attached | ||
| end | ||
|
|
||
| it 'includes only public pages in the sitemap' do | ||
| host_platform = create(:platform, :host) | ||
| public_page = create(:page, privacy: 'public', slug: 'public-page') | ||
| private_page = create(:page, privacy: 'private', slug: 'private-page') | ||
| BetterTogether::Sitemap.destroy_all | ||
|
|
||
| described_class.perform_now | ||
|
Comment on lines
+11
to
+22
|
||
|
|
||
| data = BetterTogether::Sitemap.current(host_platform).file.download | ||
| xml = Zlib::GzipReader.new(StringIO.new(data)).read | ||
|
|
||
| expect(xml).to include(public_page.slug) | ||
| expect(xml).not_to include(private_page.slug) | ||
| end | ||
|
Comment on lines
+16
to
+29
|
||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'rails_helper' | ||
|
|
||
| RSpec.describe 'Sitemap', type: :request do | ||
| include BetterTogether::Engine.routes.url_helpers | ||
| include BetterTogether::DeviseSessionHelpers | ||
|
|
||
| let!(:host_platform) { configure_host_platform } | ||
|
|
||
| before do | ||
| host! 'www.example.com' | ||
| Rails.application.routes.default_url_options[:host] = 'www.example.com' | ||
| end | ||
|
|
||
| describe 'GET /sitemap.xml.gz' do | ||
| context 'when a sitemap is attached' do | ||
| it 'redirects to the file' do | ||
| sitemap = BetterTogether::Sitemap.current(host_platform) | ||
| sitemap.file.attach(io: StringIO.new('test'), filename: 'sitemap.xml.gz', content_type: 'application/gzip') | ||
|
|
||
| get sitemap_path | ||
|
|
||
| expect(response).to redirect_to(sitemap.file.url) | ||
| end | ||
| end | ||
|
|
||
| context 'when no sitemap exists' do | ||
| it 'returns not found' do | ||
| BetterTogether::Sitemap.current(host_platform).file.detach | ||
|
|
||
| get sitemap_path | ||
|
|
||
| expect(response).to have_http_status(:not_found) | ||
| end | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing factory for the Sitemap model. According to the coding guidelines, "Every Better Together model needs a corresponding FactoryBot factory with proper engine namespace handling." Add a factory file at
spec/factories/better_together/sitemaps.rbto support testing.