diff --git a/app/finders/pages/deployments_finder.rb b/app/finders/pages/deployments_finder.rb
index 2726bbfb4c05481259a9768e4873c919a5f14d4d..66e476ed8a76d3d36140d7128ea5f855189a27bd 100644
--- a/app/finders/pages/deployments_finder.rb
+++ b/app/finders/pages/deployments_finder.rb
@@ -4,14 +4,14 @@ module Pages
   class DeploymentsFinder
     attr_reader :params
 
-    def initialize(namespace, params = {})
-      @namespace = namespace
+    def initialize(parent, params = {})
+      @parent = parent
       @params = params
     end
 
     def execute
       deployments = PagesDeployment
-      deployments = by_namespace(deployments)
+      deployments = by_parent(deployments)
       deployments = by_active_status(deployments)
       deployments = find_versioned(deployments)
       sort(deployments)
@@ -19,8 +19,20 @@ def execute
 
     private
 
+    def by_parent(deployments)
+      case @parent
+      when Namespace then by_namespace(deployments)
+      when Project then by_project(deployments)
+      else raise "Pages::DeploymentsFinder only supports Namespace or Projects as parent"
+      end
+    end
+
+    def by_project(deployments)
+      deployments.project_id_in(@parent.id)
+    end
+
     def by_namespace(deployments)
-      deployments.project_id_in(@namespace.projects.select(:id))
+      deployments.project_id_in(@parent.projects.select(:id))
     end
 
     def by_active_status(deployments)
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index e69e29aafebc9458d155a449d3ac02d24b1f90c4..6a11f1fbf3963780c6b01ccb581323cebd4df298 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -731,6 +731,11 @@ class ProjectType < BaseObject
           description: 'Term by which to search deploy key titles'
       end
 
+    field :pages_deployments, Types::PagesDeploymentType.connection_type, null: true,
+      resolver: Resolvers::PagesDeploymentsResolver,
+      connection: true,
+      description: "List of the project's Pages Deployments."
+
     def protectable_branches
       ProtectableDropdown.new(project, :branches).protectable_ref_names
     end
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 684b12a920f753d70ce48e2d37cf33f90a9a1b3f..f42880491d17f9f7a92191dbbb8143a366840319 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -28388,6 +28388,24 @@ four standard [pagination arguments](#pagination-arguments):
 | <a id="projectpackagessort"></a>`sort` | [`PackageSort`](#packagesort) | Sort packages by this criteria. |
 | <a id="projectpackagesstatus"></a>`status` | [`PackageStatus`](#packagestatus) | Filter a package by status. |
 
+##### `Project.pagesDeployments`
+
+List of the project's Pages Deployments.
+
+Returns [`PagesDeploymentConnection`](#pagesdeploymentconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#pagination-arguments):
+`before: String`, `after: String`, `first: Int`, and `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="projectpagesdeploymentsactive"></a>`active` | [`Boolean`](#boolean) | Filter by active or inactive state. |
+| <a id="projectpagesdeploymentssort"></a>`sort` | [`Sort`](#sort) | Sort results. |
+| <a id="projectpagesdeploymentsversioned"></a>`versioned` | [`Boolean`](#boolean) | Filter deployments that are versioned or unversioned. |
+
 ##### `Project.pipeline`
 
 Build pipeline of the project.
diff --git a/spec/finders/pages/deployments_finder_spec.rb b/spec/finders/pages/deployments_finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3a9a29aee1f8703486e4df4c765a2d1fec385dd8
--- /dev/null
+++ b/spec/finders/pages/deployments_finder_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Pages::DeploymentsFinder, feature_category: :pages do
+  # Most of Pages::DeploymentsFinder is tested with the GraphQL request specs
+  # so this spec will only test remaining conditions that cannot be
+  # tested otherwise.
+
+  it 'execute throws an error when passed a parent that\'s not of type Project or Namespace' do
+    expect { described_class.new("Foo").execute }.to raise_error(
+      RuntimeError, "Pages::DeploymentsFinder only supports Namespace or Projects as parent"
+    )
+  end
+end
diff --git a/spec/requests/api/graphql/pages/deployments_query_spec.rb b/spec/requests/api/graphql/pages/namespace_deployments_query_spec.rb
similarity index 98%
rename from spec/requests/api/graphql/pages/deployments_query_spec.rb
rename to spec/requests/api/graphql/pages/namespace_deployments_query_spec.rb
index 6af0a7950a41a5d08c21fd69f9f51081bb42c7f0..fa7473fef917392a7bb621495d69f4d25b961bec 100644
--- a/spec/requests/api/graphql/pages/deployments_query_spec.rb
+++ b/spec/requests/api/graphql/pages/namespace_deployments_query_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe 'Pages Deployments query', feature_category: :pages do
+RSpec.describe 'Namespace Pages Deployments query', feature_category: :pages do
   include GraphqlHelpers
 
   let(:project_maintainer) { create(:user) }
diff --git a/spec/requests/api/graphql/pages/project_deployments_query_spec.rb b/spec/requests/api/graphql/pages/project_deployments_query_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..13df0a0e45dd00cdf7ee370d30cdcdd39ef6ec5c
--- /dev/null
+++ b/spec/requests/api/graphql/pages/project_deployments_query_spec.rb
@@ -0,0 +1,179 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project Pages Deployments query', feature_category: :pages do
+  include GraphqlHelpers
+
+  let_it_be(:project_maintainer) { create(:user) }
+  let_it_be(:project_developer) { create(:user) }
+  let_it_be(:project) { create(:project, maintainers: project_maintainer, developers: project_developer) }
+  let_it_be(:pages_deployments) do
+    [
+      # primary deployment, still active
+      create(:pages_deployment, project: project),
+      # primary deployment, inactive
+      create(:pages_deployment, project: project, deleted_at: Time.now - 2.hours),
+      # versioned deployment, still active
+      create(:pages_deployment, project: project, path_prefix: 'foo'),
+      # versioned deployment, inactive
+      create(:pages_deployment, project: project, path_prefix: 'foo', deleted_at: Time.now - 2.hours)
+    ]
+  end
+
+  let(:query) do
+    <<~GRAPHQL
+      query GetProjectWithPagesDeployments {
+        project(fullPath: "#{project.full_path}") {
+          pagesDeployments(#{pages_deployments_arguments}) {
+            count
+            pageInfo {
+              endCursor
+              hasNextPage
+              hasPreviousPage
+              startCursor
+            }
+            edges {
+              cursor
+              node {
+                #{all_graphql_fields_for('PagesDeployment', max_depth: 1)}
+              }
+            }
+          }
+        }
+      }
+    GRAPHQL
+  end
+
+  let(:pages_deployments_arguments) { 'sort: CREATED_ASC, first: 10' }
+  let(:deployments_response) do
+    graphql_data_at(:project, :pages_deployments, :edges)
+  end
+
+  def fields_for_deployment(deployment)
+    {
+      'id' => deployment.to_global_id.to_s,
+      'active' => deployment.active?,
+      'ciBuildId' => anything,
+      'createdAt' => deployment.created_at.iso8601,
+      'deletedAt' => deployment.deleted_at&.iso8601,
+      'fileCount' => deployment.file_count,
+      'pathPrefix' => deployment.path_prefix,
+      'rootDirectory' => deployment.root_directory,
+      'size' => deployment.size,
+      'updatedAt' => deployment.updated_at.iso8601,
+      'url' => deployment.url
+    }
+  end
+
+  def match_deployment_node_for(deployment)
+    expect(deployments_response.first).to match({
+      'cursor' => instance_of(String),
+      'node' => fields_for_deployment(deployment)
+    })
+  end
+
+  before do
+    post_graphql(query, current_user: current_user)
+  end
+
+  describe 'user is authorized' do
+    let(:current_user) { project_maintainer }
+    let(:expected_deployment) { pages_deployments[0] }
+
+    describe 'response' do
+      it 'returns a deployment with all of the expected fields' do
+        match_deployment_node_for(expected_deployment)
+      end
+    end
+
+    describe 'default connection fields' do
+      let(:pages_deployments_arguments) { 'first: 3' }
+
+      it 'has all expected connection pagination fields' do
+        expect(
+          graphql_data_at(:project, :pages_deployments)
+        ).to match(hash_including({
+          'count' => 4,
+          'pageInfo' => {
+            'endCursor' => instance_of(String),
+            'hasNextPage' => true,
+            'hasPreviousPage' => false,
+            'startCursor' => instance_of(String)
+          }
+        }))
+      end
+    end
+
+    describe 'sorting' do
+      let(:pages_deployments_arguments) { 'sort: CREATED_DESC' }
+      let(:expected_deployment) { pages_deployments[pages_deployments.length - 1] }
+
+      it 'returns the expected deployment' do
+        match_deployment_node_for(expected_deployment)
+      end
+    end
+
+    describe 'filtering' do
+      describe 'active deployments' do
+        let(:pages_deployments_arguments) { 'active: true' }
+
+        it 'only returns active deployments' do
+          expect(graphql_data_at(:project, :pages_deployments, :edges)).to all(match({
+            'cursor' => instance_of(String),
+            'node' => hash_including({
+              'active' => true
+            })
+          }))
+        end
+      end
+
+      describe 'only inactive deployments' do
+        let(:pages_deployments_arguments) { 'active: false' }
+
+        it 'only returns inactive deployments' do
+          expect(graphql_data_at(:project, :pages_deployments, :edges)).to all(match({
+            'cursor' => instance_of(String),
+            'node' => hash_including({
+              'active' => false
+            })
+          }))
+        end
+      end
+
+      describe 'versioned deployments' do
+        let(:pages_deployments_arguments) { 'versioned: true' }
+
+        it 'only returns versioned deployments' do
+          expect(graphql_data_at(:project, :pages_deployments, :edges)).to all(match({
+            'cursor' => instance_of(String),
+            'node' => hash_including({
+              'pathPrefix' => 'foo'
+            })
+          }))
+        end
+      end
+
+      describe 'unversioned deployments' do
+        let(:pages_deployments_arguments) { 'versioned: false' }
+
+        it 'only returns unversioned deployments' do
+          expect(graphql_data_at(:project, :pages_deployments, :edges)).to all(match({
+            'cursor' => instance_of(String),
+            'node' => hash_including({
+              'pathPrefix' => nil
+            })
+          }))
+        end
+      end
+    end
+
+    describe 'user is unauthorized to view pages deployments' do
+      let(:current_user) { project_developer }
+
+      it 'returns an empty result' do
+        expect(deployments_response).to match([])
+      end
+    end
+  end
+end