diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 9a7c3dc03c367194add01eef7a1a6d518fc6a05c..fb90ddc1048022041ea0222ae8eb6df696a8b049 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -120,6 +120,13 @@ def clean_path(path)
       uniquify = Uniquify.new
       uniquify.string(path) { |s| Namespace.find_by_path_or_name(s) }
     end
+
+    def find_by_pages_host(host)
+      gitlab_host = "." + Settings.pages.host.downcase
+      name = host.downcase.delete_suffix(gitlab_host)
+
+      Namespace.find_by_full_path(name)
+    end
   end
 
   def visibility_level_field
@@ -305,8 +312,16 @@ def aggregation_scheduled?
     aggregation_schedule.present?
   end
 
+  def pages_virtual_domain
+    Pages::VirtualDomain.new(all_projects_with_pages, trim_prefix: full_path)
+  end
+
   private
 
+  def all_projects_with_pages
+    all_projects.with_pages_deployed
+  end
+
   def parent_changed?
     parent_id_changed?
   end
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index 1b3183a2a4352b613cb6c231d8d3af8ee68603ab..51c496c77d3d80eb8292585100f752f5952c437b 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -2,9 +2,10 @@
 
 module Pages
   class LookupPath
-    def initialize(project, domain: nil)
+    def initialize(project, trim_prefix: nil, domain: nil)
       @project = project
       @domain = domain
+      @trim_prefix = trim_prefix || project.full_path
     end
 
     def project_id
@@ -28,11 +29,15 @@ def source
     end
 
     def prefix
-      '/'
+      if project.pages_group_root?
+        '/'
+      else
+        project.full_path.delete_prefix(trim_prefix) + '/'
+      end
     end
 
     private
 
-    attr_reader :project, :domain
+    attr_reader :project, :trim_prefix, :domain
   end
 end
diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb
index 3a876dc06a229fd48327a253514c0b02c6073a8b..7e42b8e6ae29d5270daf655685c4a81b3b42e99b 100644
--- a/app/models/pages/virtual_domain.rb
+++ b/app/models/pages/virtual_domain.rb
@@ -2,8 +2,9 @@
 
 module Pages
   class VirtualDomain
-    def initialize(projects, domain: nil)
+    def initialize(projects, trim_prefix: nil, domain: nil)
       @projects = projects
+      @trim_prefix = trim_prefix
       @domain = domain
     end
 
@@ -17,12 +18,12 @@ def key
 
     def lookup_paths
       projects.map do |project|
-        project.pages_lookup_path(domain: domain)
+        project.pages_lookup_path(trim_prefix: trim_prefix, domain: domain)
       end.sort_by(&:prefix).reverse
     end
 
     private
 
-    attr_reader :projects, :domain
+    attr_reader :projects, :trim_prefix, :domain
   end
 end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 22a6bae7cf75c9fd7109fb063bb0bd9d19d571c2..6be3053f637325a287869d991b117382b8b34914 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -186,11 +186,17 @@ def gitlab_provided_key=(key)
   end
 
   def pages_virtual_domain
+    return unless pages_deployed?
+
     Pages::VirtualDomain.new([project], domain: self)
   end
 
   private
 
+  def pages_deployed?
+    project.pages_metadatum&.deployed?
+  end
+
   def set_verification_code
     return if self.verification_code.present?
 
diff --git a/app/models/project.rb b/app/models/project.rb
index 18afccf7ddc647da7f9c19fe78ec552387a0f9f9..883df947ccb2419d92e50b36e8bd6e4a36276cce 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -104,6 +104,9 @@ class Project < ApplicationRecord
     unless: :ci_cd_settings,
     if: proc { ProjectCiCdSetting.available? }
 
+  after_create :create_pages_metadatum,
+    unless: :pages_metadatum
+
   after_create :set_timestamps_for_create
   after_update :update_forks_visibility_level
 
@@ -295,6 +298,8 @@ class Project < ApplicationRecord
 
   has_many :external_pull_requests, inverse_of: :project
 
+  has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project
+
   accepts_nested_attributes_for :variables, allow_destroy: true
   accepts_nested_attributes_for :project_feature, update_only: true
   accepts_nested_attributes_for :import_data
@@ -425,6 +430,10 @@ class Project < ApplicationRecord
     .where(project_ci_cd_settings: { group_runners_enabled: true })
   end
 
+  scope :with_pages_deployed, -> do
+    joins(:pages_metadatum).merge(ProjectPagesMetadatum.deployed)
+  end
+
   enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
 
   chronic_duration_attr :build_timeout_human_readable, :build_timeout,
@@ -1643,6 +1652,10 @@ def pages_url
     "#{url}/#{url_path}"
   end
 
+  def pages_group_root?
+    pages_group_url == pages_url
+  end
+
   def pages_subdomain
     full_path.partition('/').first
   end
@@ -1681,6 +1694,7 @@ def remove_pages
     # Projects with a missing namespace cannot have their pages removed
     return unless namespace
 
+    mark_pages_as_not_deployed unless destroyed?
     ::Projects::UpdatePagesConfigurationService.new(self).execute
 
     # 1. We rename pages to temporary directory
@@ -1694,6 +1708,14 @@ def remove_pages
   end
   # rubocop: enable CodeReuse/ServiceClass
 
+  def mark_pages_as_deployed
+    ensure_pages_metadatum.update!(deployed: true)
+  end
+
+  def mark_pages_as_not_deployed
+    ensure_pages_metadatum.update!(deployed: false)
+  end
+
   # rubocop:disable Gitlab/RailsLogger
   def write_repository_config(gl_full_path: full_path)
     # We'd need to keep track of project full path otherwise directory tree
@@ -2213,8 +2235,8 @@ def access_request_approvers_to_be_notified
     members.maintainers.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
   end
 
-  def pages_lookup_path(domain: nil)
-    Pages::LookupPath.new(self, domain: domain)
+  def pages_lookup_path(trim_prefix: nil, domain: nil)
+    Pages::LookupPath.new(self, trim_prefix: trim_prefix, domain: domain)
   end
 
   private
@@ -2342,6 +2364,13 @@ def fetch_branch_allows_collaboration(user, branch_name = nil)
   def services_templates
     @services_templates ||= Service.where(template: true)
   end
+
+  def ensure_pages_metadatum
+    pages_metadatum || create_pages_metadatum!
+  rescue ActiveRecord::RecordNotUnique
+    reset
+    retry
+  end
 end
 
 Project.prepend_if_ee('EE::Project')
diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1fda388b1aec4a0d620fc693c0d4d6ccc8b228a7
--- /dev/null
+++ b/app/models/project_pages_metadatum.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class ProjectPagesMetadatum < ApplicationRecord
+  self.primary_key = :project_id
+
+  belongs_to :project, inverse_of: :pages_metadatum
+
+  scope :deployed, -> { where(deployed: true) }
+end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index fa7a4f0ed82b9af2cff14ab01d38ea5a75f8c2f0..e8a87fc4320aa45bcc65c330dca1b3ab1d524119 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -53,6 +53,7 @@ def execute
 
     def success
       @status.success
+      @project.mark_pages_as_deployed
       super
     end
 
diff --git a/changelogs/unreleased/28781-pages-namespaces-virtual-domain.yml b/changelogs/unreleased/28781-pages-namespaces-virtual-domain.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6725d0704400b3c9ce2f6ac2f9aef16af7a18cf7
--- /dev/null
+++ b/changelogs/unreleased/28781-pages-namespaces-virtual-domain.yml
@@ -0,0 +1,5 @@
+---
+title: Add project_pages_metadata DB table
+merge_request: 17197
+author:
+type: added
diff --git a/db/migrate/20190909045845_create_project_pages_metadata.rb b/db/migrate/20190909045845_create_project_pages_metadata.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5fc8fc6e6c16da7760e126e98b181a09be03bd11
--- /dev/null
+++ b/db/migrate/20190909045845_create_project_pages_metadata.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class CreateProjectPagesMetadata < ActiveRecord::Migration[5.2]
+  DOWNTIME = false
+
+  def change
+    create_table :project_pages_metadata, id: false do |t|
+      t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+      t.boolean :deployed, null: false, default: false
+
+      t.index :project_id, name: 'index_project_pages_metadata_on_project_id_and_deployed_is_true', where: "deployed = TRUE"
+    end
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 392db66f5b66dceb6d29e85112df272e66140c14..7703628d43372c2f5b097c14718674f5c02ca157 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -2774,6 +2774,13 @@
     t.index ["status"], name: "index_project_mirror_data_on_status"
   end
 
+  create_table "project_pages_metadata", id: false, force: :cascade do |t|
+    t.bigint "project_id", null: false
+    t.boolean "deployed", default: false, null: false
+    t.index ["project_id"], name: "index_project_pages_metadata_on_project_id", unique: true
+    t.index ["project_id"], name: "index_project_pages_metadata_on_project_id_and_deployed_is_true", where: "(deployed = true)"
+  end
+
   create_table "project_repositories", force: :cascade do |t|
     t.integer "shard_id", null: false
     t.string "disk_path", null: false
@@ -4084,6 +4091,7 @@
   add_foreign_key "project_incident_management_settings", "projects", on_delete: :cascade
   add_foreign_key "project_metrics_settings", "projects", on_delete: :cascade
   add_foreign_key "project_mirror_data", "projects", name: "fk_d1aad367d7", on_delete: :cascade
+  add_foreign_key "project_pages_metadata", "projects", on_delete: :cascade
   add_foreign_key "project_repositories", "projects", on_delete: :cascade
   add_foreign_key "project_repositories", "shards", on_delete: :restrict
   add_foreign_key "project_repository_states", "projects", on_delete: :cascade
diff --git a/lib/api/internal/pages.rb b/lib/api/internal/pages.rb
index eaa434cff51063b9684543523b72de1176922e03..003af7f6dd4fe6ddb74b2be316716ee279236531 100644
--- a/lib/api/internal/pages.rb
+++ b/lib/api/internal/pages.rb
@@ -17,11 +17,18 @@ def authenticate_gitlab_pages_request!
 
       namespace 'internal' do
         namespace 'pages' do
+          desc 'Get GitLab Pages domain configuration by hostname' do
+            detail 'This feature was introduced in GitLab 12.3.'
+          end
+          params do
+            requires :host, type: String, desc: 'The host to query for'
+          end
           get "/" do
-            host = PagesDomain.find_by_domain(params[:host])
+            host = Namespace.find_by_pages_host(params[:host]) || PagesDomain.find_by_domain(params[:host])
             not_found! unless host
 
             virtual_domain = host.pages_virtual_domain
+            no_content! unless virtual_domain
 
             present virtual_domain, with: Entities::Internal::Pages::VirtualDomain
           end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 3b43ff3a4e19625afc2cf4c8118f74ea775484fc..3315dd3b974aa1d22e540d86a2a3b9c29d02359d 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -410,6 +410,7 @@ project:
 - designs
 - project_aliases
 - external_pull_requests
+- pages_metadatum
 award_emoji:
 - awardable
 - user
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 972f26ac745e4d5daf8e515a0b46e971efed2fcc..e72e272f4d22a5922af89441c490dfdd67f7cd8c 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -191,6 +191,16 @@
     end
   end
 
+  describe '.find_by_pages_host' do
+    it 'finds namespace by GitLab Pages host and is case-insensitive' do
+      namespace = create(:namespace, name: 'topnamespace')
+      create(:namespace, name: 'annother_namespace')
+      host = "TopNamespace.#{Settings.pages.host.upcase}"
+
+      expect(described_class.find_by_pages_host(host)).to eq(namespace)
+    end
+  end
+
   describe '#ancestors_upto' do
     let(:parent) { create(:group) }
     let(:child) { create(:group, parent: parent) }
@@ -913,4 +923,18 @@ def project_rugged(project)
       end
     end
   end
+
+  describe '#pages_virtual_domain' do
+    let(:project) { create(:project, namespace: namespace) }
+
+    context 'when there are pages deployed for the project' do
+      before do
+        project.mark_pages_as_deployed
+      end
+
+      it 'returns the virual domain' do
+        expect(namespace.pages_virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+      end
+    end
+  end
 end
diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb
index 2146b0c9abd7e0445ada4a4d4924e4830e237db9..c05d4c82634048b667c554d32ed5c7067fa8caf7 100644
--- a/spec/models/pages/lookup_path_spec.rb
+++ b/spec/models/pages/lookup_path_spec.rb
@@ -57,8 +57,18 @@
   end
 
   describe '#prefix' do
-    it 'returns "/"' do
+    it 'returns "/" for pages group root projects' do
+      project = instance_double(Project, pages_group_root?: true)
+      lookup_path = described_class.new(project, trim_prefix: 'mygroup')
+
       expect(lookup_path.prefix).to eq('/')
     end
+
+    it 'returns the project full path with the provided prefix removed' do
+      project = instance_double(Project, pages_group_root?: false, full_path: 'mygroup/myproject')
+      lookup_path = described_class.new(project, trim_prefix: 'mygroup')
+
+      expect(lookup_path.prefix).to eq('/myproject/')
+    end
   end
 end
diff --git a/spec/models/pages/virtual_domain_spec.rb b/spec/models/pages/virtual_domain_spec.rb
index eaa57b7acd6ac10d62cddd091b37e51706701da3..a5310738482a3fffe27a69db206da614ef010909 100644
--- a/spec/models/pages/virtual_domain_spec.rb
+++ b/spec/models/pages/virtual_domain_spec.rb
@@ -25,19 +25,33 @@
   end
 
   describe '#lookup_paths' do
-    let(:domain) { instance_double(PagesDomain) }
     let(:project_a) { instance_double(Project) }
     let(:project_z) { instance_double(Project) }
     let(:pages_lookup_path_a) { instance_double(Pages::LookupPath, prefix: 'aaa') }
     let(:pages_lookup_path_z) { instance_double(Pages::LookupPath, prefix: 'zzz') }
 
-    subject(:virtual_domain) { described_class.new([project_a, project_z], domain: domain) }
+    context 'when there is pages domain provided' do
+      let(:domain) { instance_double(PagesDomain) }
 
-    it 'returns collection of projects pages lookup paths sorted by prefix in reverse' do
-      expect(project_a).to receive(:pages_lookup_path).with(domain: domain).and_return(pages_lookup_path_a)
-      expect(project_z).to receive(:pages_lookup_path).with(domain: domain).and_return(pages_lookup_path_z)
+      subject(:virtual_domain) { described_class.new([project_a, project_z], domain: domain) }
 
-      expect(virtual_domain.lookup_paths).to eq([pages_lookup_path_z, pages_lookup_path_a])
+      it 'returns collection of projects pages lookup paths sorted by prefix in reverse' do
+        expect(project_a).to receive(:pages_lookup_path).with(domain: domain, trim_prefix: nil).and_return(pages_lookup_path_a)
+        expect(project_z).to receive(:pages_lookup_path).with(domain: domain, trim_prefix: nil).and_return(pages_lookup_path_z)
+
+        expect(virtual_domain.lookup_paths).to eq([pages_lookup_path_z, pages_lookup_path_a])
+      end
+    end
+
+    context 'when there is trim_prefix provided' do
+      subject(:virtual_domain) { described_class.new([project_a, project_z], trim_prefix: 'group/') }
+
+      it 'returns collection of projects pages lookup paths sorted by prefix in reverse' do
+        expect(project_a).to receive(:pages_lookup_path).with(trim_prefix: 'group/', domain: nil).and_return(pages_lookup_path_a)
+        expect(project_z).to receive(:pages_lookup_path).with(trim_prefix: 'group/', domain: nil).and_return(pages_lookup_path_z)
+
+        expect(virtual_domain.lookup_paths).to eq([pages_lookup_path_z, pages_lookup_path_a])
+      end
     end
   end
 end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index f745820a404d96cc83549c9e84740b61469f9066..9ac80f8b79587e4982a3bd7f610ae9be2b6ca2b1 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -557,15 +557,27 @@
     end
   end
 
-  describe '.pages_virtual_domain' do
-    let(:project) { build(:project) }
+  describe '#pages_virtual_domain' do
+    let(:project) { create(:project) }
+    let(:pages_domain) { create(:pages_domain, project: project) }
 
-    subject(:pages_domain) { build(:pages_domain, project: project) }
+    context 'when there are no pages deployed for the project' do
+      it 'returns nil' do
+        expect(pages_domain.pages_virtual_domain).to be_nil
+      end
+    end
 
-    it 'returns instance of Pages::VirtualDomain' do
-      expect(Pages::VirtualDomain).to receive(:new).with([project], domain: pages_domain).and_call_original
+    context 'when there are pages deployed for the project' do
+      before do
+        project.mark_pages_as_deployed
+        project.reload
+      end
+
+      it 'returns the virual domain' do
+        expect(Pages::VirtualDomain).to receive(:new).with([project], domain: pages_domain).and_call_original
 
-      expect(pages_domain.pages_virtual_domain).to be_a(Pages::VirtualDomain)
+        expect(pages_domain.pages_virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+      end
     end
   end
 end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 67f6482218462d29420df564cad65f5d2e0f8228..e97e8c58bbdcbf25a9630272e98f707e2373289a 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -132,6 +132,13 @@
         expect(project.ci_cd_settings).to be_an_instance_of(ProjectCiCdSetting)
         expect(project.ci_cd_settings).to be_persisted
       end
+
+      it 'automatically creates a Pages metadata row' do
+        project = create(:project)
+
+        expect(project.pages_metadatum).to be_an_instance_of(ProjectPagesMetadatum)
+        expect(project.pages_metadatum).to be_persisted
+      end
     end
 
     context 'updating cd_cd_settings' do
@@ -3526,7 +3533,8 @@ def enable_lfs
   end
 
   describe '#remove_pages' do
-    let(:project) { create(:project) }
+    let(:project) { create(:project).tap { |project| project.mark_pages_as_deployed } }
+    let(:pages_metadatum) { project.pages_metadatum }
     let(:namespace) { project.namespace }
     let(:pages_path) { project.pages_path }
 
@@ -3539,12 +3547,12 @@ def enable_lfs
       end
     end
 
-    it 'removes the pages directory' do
+    it 'removes the pages directory and marks the project as not having pages deployed' do
       expect_any_instance_of(Projects::UpdatePagesConfigurationService).to receive(:execute)
       expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return(true)
       expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, namespace.full_path, anything)
 
-      project.remove_pages
+      expect { project.remove_pages }.to change { pages_metadatum.reload.deployed }.from(true).to(false)
     end
 
     it 'is a no-op when there is no namespace' do
@@ -3554,13 +3562,13 @@ def enable_lfs
       expect_any_instance_of(Projects::UpdatePagesConfigurationService).not_to receive(:execute)
       expect_any_instance_of(Gitlab::PagesTransfer).not_to receive(:rename_project)
 
-      project.remove_pages
+      expect { project.remove_pages }.not_to change { pages_metadatum.reload.deployed }
     end
 
     it 'is run when the project is destroyed' do
       expect(project).to receive(:remove_pages).and_call_original
 
-      project.destroy
+      expect { project.destroy }.not_to raise_error
     end
   end
 
@@ -5014,6 +5022,35 @@ def enable_lfs
     end
   end
 
+  context 'pages deployed' do
+    let(:project) { create(:project) }
+
+    {
+      mark_pages_as_deployed: true,
+      mark_pages_as_not_deployed: false
+    }.each do |method_name, flag|
+      describe method_name do
+        it "creates new record and sets deployed to #{flag} if none exists yet" do
+          project.pages_metadatum.destroy!
+          project.reload
+
+          project.send(method_name)
+
+          expect(project.pages_metadatum.reload.deployed).to eq(flag)
+        end
+
+        it "updates the existing record and sets deployed to #{flag}" do
+          pages_metadatum = project.pages_metadatum
+          pages_metadatum.update!(deployed: !flag)
+
+          expect { project.send(method_name) }.to change {
+            pages_metadatum.reload.deployed
+          }.from(!flag).to(flag)
+        end
+      end
+    end
+  end
+
   describe '#has_pool_repsitory?' do
     it 'returns false when it does not have a pool repository' do
       subject = create(:project, :repository)
@@ -5054,9 +5091,34 @@ def enable_lfs
     let(:project) { build(:project) }
 
     it 'returns instance of Pages::LookupPath' do
-      expect(Pages::LookupPath).to receive(:new).with(project, domain: pages_domain).and_call_original
+      expect(Pages::LookupPath).to receive(:new).with(project, domain: pages_domain, trim_prefix: 'mygroup').and_call_original
+
+      expect(project.pages_lookup_path(domain: pages_domain, trim_prefix: 'mygroup')).to be_a(Pages::LookupPath)
+    end
+  end
+
+  describe '.with_pages_deployed' do
+    it 'returns only projects that have pages deployed' do
+      _project_without_pages = create(:project)
+      project_with_pages = create(:project)
+      project_with_pages.mark_pages_as_deployed
+
+      expect(described_class.with_pages_deployed).to contain_exactly(project_with_pages)
+    end
+  end
+
+  describe '#pages_group_root?' do
+    it 'returns returns true if pages_url is same as pages_group_url' do
+      project = build(:project)
+      expect(project).to receive(:pages_url).and_return(project.pages_group_url)
+
+      expect(project.pages_group_root?).to eq(true)
+    end
+
+    it 'returns returns false if pages_url is different than pages_group_url' do
+      project = build(:project)
 
-      expect(project.pages_lookup_path(domain: pages_domain)).to be_a(Pages::LookupPath)
+      expect(project.pages_group_root?).to eq(false)
     end
   end
 
diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb
index e1b563b92f41cdc273e2f124a34c18ca35782db6..03bf748b471a9f25ac4a49ca33a534c13a69406a 100644
--- a/spec/requests/api/internal/pages_spec.rb
+++ b/spec/requests/api/internal/pages_spec.rb
@@ -43,6 +43,10 @@ def query_host(host)
           super(host, headers)
         end
 
+        def deploy_pages(project)
+          project.mark_pages_as_deployed
+        end
+
         context 'not existing host' do
           it 'responds with 404 Not Found' do
             query_host('pages.gitlab.io')
@@ -56,18 +60,104 @@ def query_host(host)
           let(:project) { create(:project, namespace: namespace, name: 'gitlab-ce') }
           let!(:pages_domain) { create(:pages_domain, domain: 'pages.gitlab.io', project: project) }
 
-          it 'responds with the correct domain configuration' do
-            query_host('pages.gitlab.io')
+          context 'when there are no pages deployed for the related project' do
+            it 'responds with 204 No Content' do
+              query_host('pages.gitlab.io')
 
-            expect(response).to have_gitlab_http_status(200)
-            expect(response).to match_response_schema('internal/pages/virtual_domain')
+              expect(response).to have_gitlab_http_status(204)
+            end
+          end
 
-            expect(json_response['certificate']).to eq(pages_domain.certificate)
-            expect(json_response['key']).to eq(pages_domain.key)
+          context 'when there are pages deployed for the related project' do
+            it 'responds with the correct domain configuration' do
+              deploy_pages(project)
+
+              query_host('pages.gitlab.io')
+
+              expect(response).to have_gitlab_http_status(200)
+              expect(response).to match_response_schema('internal/pages/virtual_domain')
+
+              expect(json_response['certificate']).to eq(pages_domain.certificate)
+              expect(json_response['key']).to eq(pages_domain.key)
+
+              expect(json_response['lookup_paths']).to eq(
+                [
+                  {
+                    'project_id' => project.id,
+                    'access_control' => false,
+                    'https_only' => false,
+                    'prefix' => '/',
+                    'source' => {
+                      'type' => 'file',
+                      'path' => 'gitlab-org/gitlab-ce/public/'
+                    }
+                  }
+                ]
+              )
+            end
+          end
+        end
+
+        context 'namespaced domain' do
+          let(:group) { create(:group, name: 'mygroup') }
+
+          before do
+            allow(Settings.pages).to receive(:host).and_return('gitlab-pages.io')
+            allow(Gitlab.config.pages).to receive(:url).and_return("http://gitlab-pages.io")
+          end
+
+          context 'regular project' do
+            it 'responds with the correct domain configuration' do
+              project = create(:project, group: group, name: 'myproject')
+              deploy_pages(project)
+
+              query_host('mygroup.gitlab-pages.io')
+
+              expect(response).to have_gitlab_http_status(200)
+              expect(response).to match_response_schema('internal/pages/virtual_domain')
+
+              expect(json_response['lookup_paths']).to eq(
+                [
+                  {
+                    'project_id' => project.id,
+                    'access_control' => false,
+                    'https_only' => false,
+                    'prefix' => '/myproject/',
+                    'source' => {
+                      'type' => 'file',
+                      'path' => 'mygroup/myproject/public/'
+                    }
+                  }
+                ]
+              )
+            end
+          end
 
-            lookup_path = json_response['lookup_paths'][0]
-            expect(lookup_path['prefix']).to eq('/')
-            expect(lookup_path['source']['path']).to eq('gitlab-org/gitlab-ce/public/')
+          context 'group root project' do
+            it 'responds with the correct domain configuration' do
+              project = create(:project, group: group, name: 'mygroup.gitlab-pages.io')
+              deploy_pages(project)
+
+              query_host('mygroup.gitlab-pages.io')
+
+              expect(response).to have_gitlab_http_status(200)
+              expect(response).to match_response_schema('internal/pages/virtual_domain')
+
+              expect(json_response['lookup_paths']).to eq(
+                [
+                  {
+                    'project_id' => project.id,
+                    'access_control' => false,
+                    'https_only' => false,
+                    'prefix' => '/',
+                    'source' => {
+                      'type' => 'file',
+                      'path' => 'mygroup/mygroup.gitlab-pages.io/public/'
+                    }
+                  }
+                ]
+              )
+            end
           end
         end
       end
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index b597717c347d1001adebe0930e39b96fa39d115c..fe92b53cd91c6b11f4a4cab65feb07aa83050310 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -40,6 +40,7 @@
         it "doesn't delete artifacts after deploying" do
           expect(execute).to eq(:success)
 
+          expect(project.pages_metadatum).to be_deployed
           expect(build.artifacts?).to eq(true)
         end
       end
@@ -47,6 +48,7 @@
       it 'succeeds' do
         expect(project.pages_deployed?).to be_falsey
         expect(execute).to eq(:success)
+        expect(project.pages_metadatum).to be_deployed
         expect(project.pages_deployed?).to be_truthy
 
         # Check that all expected files are extracted
@@ -63,16 +65,23 @@
       it 'removes pages after destroy' do
         expect(PagesWorker).to receive(:perform_in)
         expect(project.pages_deployed?).to be_falsey
+
         expect(execute).to eq(:success)
+
+        expect(project.pages_metadatum).to be_deployed
         expect(project.pages_deployed?).to be_truthy
+
         project.destroy
+
         expect(project.pages_deployed?).to be_falsey
+        expect(ProjectPagesMetadatum.find_by_project_id(project)).to be_nil
       end
 
       it 'fails if sha on branch is not latest' do
         build.update(ref: 'feature')
 
         expect(execute).not_to eq(:success)
+        expect(project.pages_metadatum).not_to be_deployed
       end
 
       context 'when using empty file' do
@@ -94,6 +103,7 @@
 
           it 'succeeds to extract' do
             expect(execute).to eq(:success)
+            expect(project.pages_metadatum).to be_deployed
           end
         end
       end
@@ -109,6 +119,7 @@
 
           build.reload
           expect(deploy_status).to be_failed
+          expect(project.pages_metadatum).not_to be_deployed
         end
       end
 
@@ -125,6 +136,7 @@
 
           build.reload
           expect(deploy_status).to be_failed
+          expect(project.pages_metadatum).not_to be_deployed
         end
       end
 
@@ -138,6 +150,7 @@
 
           build.reload
           expect(deploy_status).to be_failed
+          expect(project.pages_metadatum).not_to be_deployed
         end
       end
     end
@@ -179,6 +192,7 @@
         expect(deploy_status.description)
           .to match(/artifacts for pages are too large/)
         expect(deploy_status).to be_script_failure
+        expect(project.pages_metadatum).not_to be_deployed
       end
     end
 
@@ -196,6 +210,7 @@
           subject.execute
 
           expect(deploy_status.description).not_to be_present
+          expect(project.pages_metadatum).to be_deployed
         end
       end