Commit bf636e87 authored by fjsanpedro's avatar fjsanpedro

Generate Sitemap dynamically

In this commit we create a controller that will generate
dynamically the sitemap for the `gitlab-org` group.

The controller is restricted for .com.
parent 456bec4c
---
name: gitlab_org_sitemap
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46661
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/276915
milestone: '13.6'
type: development
group: group::editor
default_enabled: false
...@@ -275,6 +275,10 @@ Rails.application.routes.draw do ...@@ -275,6 +275,10 @@ Rails.application.routes.draw do
draw :profile draw :profile
end end
Gitlab.ee do
get '/sitemap' => 'sitemap#show', format: :xml
end
root to: "root#index" root to: "root#index"
get '*unmatched_route', to: 'application#route_not_found' get '*unmatched_route', to: 'application#route_not_found'
......
# frozen_string_literal: true
class SitemapController < ApplicationController
skip_before_action :authenticate_user!
feature_category :metrics
def show
return render_404 unless Gitlab.com?
return render_404 unless Feature.enabled?(:gitlab_org_sitemap)
respond_to do |format|
format.xml do
response = Sitemap::CreateService.new.execute
xml_data = if response.success?
response.payload[:sitemap]
else
xml_error(response.message)
end
render inline: xml_data
end
end
end
private
def xml_error(message)
xml_builder = Builder::XmlMarkup.new(indent: 2)
xml_builder.instruct!
xml_builder.error message
end
end
# frozen_string_literal: true
module Sitemap
class CreateService
def execute
result = Gitlab::Sitemaps::Generator.execute
if result.is_a?(String)
error_response(result)
else
success_response(result)
end
end
private
def success_response(file)
Gitlab::AppLogger.info("Sitemap generated successfully")
ServiceResponse.success(payload: { sitemap: file.render } )
end
def error_response(message)
Gitlab::AppLogger.error("Sitemap error creating sitemap: #{message}")
ServiceResponse.error(
message: message
)
end
end
end
---
title: Generate dynamically sitemap through controller
merge_request: 46661
author:
type: changed
...@@ -10,19 +10,22 @@ module Gitlab ...@@ -10,19 +10,22 @@ module Gitlab
def execute def execute
unless Gitlab.com? unless Gitlab.com?
return "The sitemap can only be generated for Gitlab.com" return 'The sitemap can only be generated for Gitlab.com'
end end
file = Sitemaps::SitemapFile.new file = Sitemaps::SitemapFile.new
if gitlab_org_group return "The group '#{GITLAB_ORG_NAMESPACE}' was not found" unless gitlab_org_group
file.add_elements(generic_urls)
file.add_elements(gitlab_org_group) file.add_elements(generic_urls)
file.add_elements(gitlab_org_subgroups) file.add_elements(gitlab_org_group)
file.add_elements(gitlab_org_projects) file.add_elements(gitlab_org_subgroups)
file.save file.add_elements(gitlab_org_projects)
if file.empty?
'No urls found to generate the sitemap'
else else
"The group '#{GITLAB_ORG_NAMESPACE}' was not found" file
end end
end end
...@@ -37,7 +40,7 @@ module Gitlab ...@@ -37,7 +40,7 @@ module Gitlab
end end
def gitlab_org_group def gitlab_org_group
@gitlab_org_group ||= GroupFinder.new(nil).execute(path: 'gitlab-org', parent_id: nil, visibility_level: Gitlab::VisibilityLevel::PUBLIC) @gitlab_org_group ||= GroupFinder.new(nil).execute(path: GITLAB_ORG_NAMESPACE, parent_id: nil, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
end end
def gitlab_org_subgroups def gitlab_org_subgroups
......
...@@ -20,17 +20,23 @@ module Gitlab ...@@ -20,17 +20,23 @@ module Gitlab
end end
def save def save
return if urls.empty? return if empty?
File.write(SITEMAP_FILE_PATH, render) File.write(SITEMAP_FILE_PATH, render)
end end
def render def render
return if empty?
fragment = File.read(File.expand_path("fragments/sitemap_file.xml.builder", __dir__)) fragment = File.read(File.expand_path("fragments/sitemap_file.xml.builder", __dir__))
instance_eval fragment instance_eval fragment
end end
def empty?
urls.empty?
end
private private
def xml_builder def xml_builder
......
# frozen_string_literal: true # frozen_string_literal: true
# Generating the urls for the project and groups is the most
# expensive part of the sitemap generation because we need
# to call the Rails route helpers.
#
# We could hardcode them but if a route changes the sitemap
# urls will be invalid.
module Gitlab module Gitlab
module Sitemaps module Sitemaps
class UrlExtractor class UrlExtractor
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe SitemapController do
describe '#show' do
subject { get :show, format: :xml }
before do
allow(Gitlab).to receive(:com?).and_return(dot_com)
end
context 'when not Gitlab.com?' do
let(:dot_com) { false }
it 'returns :not_found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when Gitlab.com?' do
let(:dot_com) { true }
context 'with an authenticated user' do
let(:flag_value) { true }
before do
stub_feature_flags(gitlab_org_sitemap: flag_value)
allow(Sitemap::CreateService).to receive_message_chain(:new, :execute).and_return(result)
subject
end
shared_examples 'gitlab_org_sitemap flag is disabled' do
let(:flag_value) { false }
it 'returns :not_found' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when the sitemap generation raises an error' do
let(:result) { ServiceResponse.error(message: 'foo') }
it 'returns an xml error' do
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to include('<error>foo</error>')
end
it_behaves_like 'gitlab_org_sitemap flag is disabled'
end
context 'when the sitemap was created suscessfully' do
let(:result) { ServiceResponse.success(payload: { sitemap: 'foo' }) }
it 'returns sitemap' do
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to eq('foo')
end
it_behaves_like 'gitlab_org_sitemap flag is disabled'
end
end
end
end
end
...@@ -42,12 +42,7 @@ RSpec.describe Gitlab::Sitemaps::Generator do ...@@ -42,12 +42,7 @@ RSpec.describe Gitlab::Sitemaps::Generator do
let_it_be(:internal_subgroup_internal_project) { create(:project, :internal, namespace: internal_subgroup) } let_it_be(:internal_subgroup_internal_project) { create(:project, :internal, namespace: internal_subgroup) }
it 'includes default explore routes and gitlab-org group routes' do it 'includes default explore routes and gitlab-org group routes' do
new_path = Rails.root.join('tmp/tests/sitemap.xml') content = subject.render
stub_const('Gitlab::Sitemaps::SitemapFile::SITEMAP_FILE_PATH', new_path)
subject
content = File.read(new_path)
expect(content).to include('/explore/projects') expect(content).to include('/explore/projects')
expect(content).to include('/explore/groups') expect(content).to include('/explore/groups')
...@@ -63,8 +58,6 @@ RSpec.describe Gitlab::Sitemaps::Generator do ...@@ -63,8 +58,6 @@ RSpec.describe Gitlab::Sitemaps::Generator do
expect(content).not_to include(public_subgroup_internal_project.full_path) expect(content).not_to include(public_subgroup_internal_project.full_path)
expect(content).not_to include(internal_subgroup_private_project.full_path) expect(content).not_to include(internal_subgroup_private_project.full_path)
expect(content).not_to include(internal_subgroup_internal_project.full_path) expect(content).not_to include(internal_subgroup_internal_project.full_path)
File.delete(new_path)
end end
end end
end end
......
...@@ -10,6 +10,12 @@ RSpec.describe Gitlab::Sitemaps::SitemapFile do ...@@ -10,6 +10,12 @@ RSpec.describe Gitlab::Sitemaps::SitemapFile do
end end
describe '#render' do describe '#render' do
it 'returns if no elements has been provided' do
expect(File).not_to receive(:read)
described_class.new.save # rubocop: disable Rails/SaveBang
end
it 'generates a valid sitemap file' do it 'generates a valid sitemap file' do
freeze_time do freeze_time do
content = subject.render content = subject.render
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sitemap::CreateService do
describe '#execute' do
subject { described_class.new.execute}
it 'returns the successful service response with the sitemap content' do
sitemap_file = Gitlab::Sitemaps::SitemapFile.new
allow(sitemap_file).to receive(:render).and_return('foo')
allow(Gitlab::Sitemaps::Generator).to receive(:execute).and_return(sitemap_file)
expect(subject).to be_success
expect(subject.payload[:sitemap]).to eq 'foo'
end
context 'when the sitemap generator returns an error' do
it 'returns an error service response' do
allow(Gitlab).to receive(:com?).and_return(false)
expect(subject).to be_error
expect(subject.message).to eq 'The sitemap can only be generated for Gitlab.com'
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment