diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
index fbc7ddf5c9119a9d42f4c415c063eecf90c69236..91aaf2237b74e165fa8ac1726385dd46df729469 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
@@ -12,7 +12,7 @@ export default {
     GlTableLite,
   },
   props: {
-    resourceId: {
+    resourcePath: {
       type: String,
       required: true,
     },
@@ -27,7 +27,7 @@ export default {
       query: getCiCatalogResourceComponents,
       variables() {
         return {
-          id: this.resourceId,
+          fullPath: this.resourcePath,
         };
       },
       update(data) {
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
index 026a30988fdfbf07832d6cd366075377a4db8163..b1170b13ef6c974d2ada6856596f330945166bb3 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
@@ -14,7 +14,7 @@ export default {
   },
   mixins: [glFeatureFlagsMixin()],
   props: {
-    resourceId: {
+    resourcePath: {
       type: String,
       required: true,
     },
@@ -31,10 +31,10 @@ export default {
 <template>
   <gl-tabs>
     <gl-tab :title="$options.i18n.tabs.readme" lazy>
-      <ci-resource-readme :resource-id="resourceId" />
+      <ci-resource-readme :resource-path="resourcePath" />
     </gl-tab>
     <gl-tab v-if="glFeatures.ciCatalogComponentsTab" :title="$options.i18n.tabs.components" lazy>
-      <ci-resource-components :resource-id="resourceId"
+      <ci-resource-components :resource-path="resourcePath"
     /></gl-tab>
   </gl-tabs>
 </template>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue
index d473833869d517c8bc21f918c3108d7be95a537c..343b555c4d8518c344834fc0297eb2c8447f3240 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue
@@ -11,7 +11,7 @@ export default {
   },
   directives: { SafeHtml },
   props: {
-    resourceId: {
+    resourcePath: {
       type: String,
       required: true,
     },
@@ -26,7 +26,7 @@ export default {
       query: getCiCatalogResourceReadme,
       variables() {
         return {
-          id: this.resourceId,
+          fullPath: this.resourcePath,
         };
       },
       update(data) {
diff --git a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
index 6675118db59ec5668fc8c642cfd7b75ed5cf72d1..57d19af614f7a6c9d79188e5f9d4d2c094fb9786 100644
--- a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
@@ -34,11 +34,17 @@ export default {
     authorProfileUrl() {
       return this.latestVersion.author.webUrl;
     },
-    detailsPageHref() {
+    resourceId() {
+      return cleanLeadingSeparator(this.resource.webPath);
+    },
+    detailsPageResolved() {
       return this.$router.resolve({
         name: CI_RESOURCE_DETAILS_PAGE_NAME,
-        params: { id: this.entityId },
-      }).href;
+        params: { id: this.resourceId },
+      });
+    },
+    detailsPageHref() {
+      return decodeURIComponent(this.detailsPageResolved.href);
     },
     entityId() {
       return getIdFromGraphQLId(this.resource.id);
@@ -79,10 +85,8 @@ export default {
       // open a new tab.
       e.preventDefault();
 
-      this.$router.push({
-        name: CI_RESOURCE_DETAILS_PAGE_NAME,
-        params: { id: this.entityId },
-      });
+      // Push to the decoded URL to avoid all the / being encoded
+      this.$router.push({ path: decodeURIComponent(this.resourceId) });
     },
   },
 };
diff --git a/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue b/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue
index da2c73be900bc29914f1e6ebfd3151dfe3f84f53..b7e117f9c260b42fbc498bcaf477e9ccd2f36b81 100644
--- a/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue
+++ b/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue
@@ -2,8 +2,7 @@
 import { GlEmptyState } from '@gitlab/ui';
 import { s__ } from '~/locale';
 import { createAlert } from '~/alert';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { CI_CATALOG_RESOURCE_TYPE } from '../../graphql/settings';
+import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
 import getCatalogCiResourceDetails from '../../graphql/queries/get_ci_catalog_resource_details.query.graphql';
 import getCatalogCiResourceSharedData from '../../graphql/queries/get_ci_catalog_resource_shared_data.query.graphql';
 import CiResourceDetails from '../details/ci_resource_details.vue';
@@ -28,7 +27,7 @@ export default {
       query: getCatalogCiResourceSharedData,
       variables() {
         return {
-          id: this.graphQLId,
+          fullPath: this.cleanFullPath,
         };
       },
       update(data) {
@@ -43,7 +42,7 @@ export default {
       query: getCatalogCiResourceDetails,
       variables() {
         return {
-          id: this.graphQLId,
+          fullPath: this.cleanFullPath,
         };
       },
       update(data) {
@@ -56,8 +55,8 @@ export default {
     },
   },
   computed: {
-    graphQLId() {
-      return convertToGraphQLId(CI_CATALOG_RESOURCE_TYPE, this.$route.params.id);
+    cleanFullPath() {
+      return cleanLeadingSeparator(this.$route.params.id);
     },
     isLoadingDetails() {
       return this.$apollo.queries.resourceAdditionalDetails.loading;
@@ -103,7 +102,7 @@ export default {
         :pipeline-status="pipelineStatus"
         :resource="resourceSharedData"
       />
-      <ci-resource-details :resource-id="graphQLId" />
+      <ci-resource-details :resource-path="cleanFullPath" />
     </div>
   </div>
 </template>
diff --git a/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
index 0dc16e3492df0b0c6a6d645c7d88c71151bbea11..b3a750e9604a2be9245907d6a046340326c08a87 100644
--- a/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
@@ -1,5 +1,6 @@
 fragment CatalogResourceFields on CiCatalogResource {
   id
+  webPath
   icon
   name
   description
@@ -15,5 +16,4 @@ fragment CatalogResourceFields on CiCatalogResource {
       webUrl
     }
   }
-  webPath
 }
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql
index 6aef5dcc4e768afe1c6128f56ac3cb10c2b370ed..92272bd97c501ef5e50cf3bcc9dcf16da20739d6 100644
--- a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql
@@ -1,6 +1,7 @@
-query getCiCatalogResourceComponents($id: CiCatalogResourceID!) {
-  ciCatalogResource(id: $id) {
+query getCiCatalogResourceComponents($fullPath: ID!) {
+  ciCatalogResource(fullPath: $fullPath) {
     id
+    webPath
     components @client {
       nodes {
         id
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql
index 382d38667953ce9dea74028806dc4ecb8fd38020..a77e8f12d032733d8fd570d2f4488143f0b2b893 100644
--- a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql
@@ -1,6 +1,7 @@
-query getCiCatalogResourceDetails($id: CiCatalogResourceID!) {
-  ciCatalogResource(id: $id) {
+query getCiCatalogResourceDetails($fullPath: ID!) {
+  ciCatalogResource(fullPath: $fullPath) {
     id
+    webPath
     openIssuesCount
     openMergeRequestsCount
     versions(first: 1) {
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql
index 6b3d0cdcfc70e1b9f19565afc1226ca0851c4d83..c1fde8dcb433dc477eb3ec47ccafa72dbc88c175 100644
--- a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql
@@ -1,6 +1,7 @@
-query getCiCatalogResourceReadme($id: CiCatalogResourceID!) {
-  ciCatalogResource(id: $id) {
+query getCiCatalogResourceReadme($fullPath: ID!) {
+  ciCatalogResource(fullPath: $fullPath) {
     id
+    webPath
     readmeHtml
   }
 }
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql
index 4ac4cb0e39474d21857f351823e4aa293be5d3ef..3d5d139a33484a5aa60b5f53ee5a4c3d3fbf3188 100644
--- a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql
@@ -1,7 +1,7 @@
 #import "../fragments/catalog_resource.fragment.graphql"
 
-query getCiCatalogResourceSharedData($id: CiCatalogResourceID!) {
-  ciCatalogResource(id: $id) {
+query getCiCatalogResourceSharedData($fullPath: ID!) {
+  ciCatalogResource(fullPath: $fullPath) {
     ...CatalogResourceFields
   }
 }
diff --git a/app/assets/javascripts/ci/catalog/graphql/settings.js b/app/assets/javascripts/ci/catalog/graphql/settings.js
index a87b26ca4fc986ebcf5a43ac9a6abb55b65d7e82..5e71fee818e30959b0524f58640411cb407c6592 100644
--- a/app/assets/javascripts/ci/catalog/graphql/settings.js
+++ b/app/assets/javascripts/ci/catalog/graphql/settings.js
@@ -11,7 +11,8 @@ export const cacheConfig = {
           ciCatalogResource(_, { args, toReference }) {
             return toReference({
               __typename: 'CiCatalogResource',
-              id: args.id,
+              // Webpath is the fullpath with a leading slash
+              webPath: `/${args.fullPath}`,
             });
           },
           ciCatalogResources: {
@@ -19,6 +20,9 @@ export const cacheConfig = {
           },
         },
       },
+      CiCatalogResource: {
+        keyFields: ['webPath'],
+      },
     },
   },
 };
diff --git a/app/assets/javascripts/ci/catalog/index.js b/app/assets/javascripts/ci/catalog/index.js
index 6d3a4814ffa74a65be4548fdac2b6772d3ee0034..5815245506c1bcf5035dd084756f9df9e34fe583 100644
--- a/app/assets/javascripts/ci/catalog/index.js
+++ b/app/assets/javascripts/ci/catalog/index.js
@@ -27,6 +27,9 @@ export const initCatalog = (selector = '#js-ci-cd-catalog') => {
     name: 'GlobalCatalog',
     router: createRouter(ciCatalogPath, CiResourcesPage),
     apolloProvider,
+    provide: {
+      ciCatalogPath,
+    },
     render(h) {
       return h(GlobalCatalog);
     },
diff --git a/app/assets/javascripts/ci/catalog/router/routes.js b/app/assets/javascripts/ci/catalog/router/routes.js
index ccfb0673c8328d210c6a81f7b1d378498a020ae0..ce859e266d7610456d0d8682c20ce2790b134eaf 100644
--- a/app/assets/javascripts/ci/catalog/router/routes.js
+++ b/app/assets/javascripts/ci/catalog/router/routes.js
@@ -4,6 +4,6 @@ import { CI_RESOURCES_PAGE_NAME, CI_RESOURCE_DETAILS_PAGE_NAME } from './constan
 export const createRoutes = (listComponent) => {
   return [
     { name: CI_RESOURCES_PAGE_NAME, path: '', component: listComponent },
-    { name: CI_RESOURCE_DETAILS_PAGE_NAME, path: '/:id', component: CiResourceDetailsPage },
+    { name: CI_RESOURCE_DETAILS_PAGE_NAME, path: '/:id+', component: CiResourceDetailsPage },
   ];
 };
diff --git a/app/controllers/explore/catalog_controller.rb b/app/controllers/explore/catalog_controller.rb
index 8207f3e55c66a190a8b56bd32f81651e18727a66..50846c21b1bdc067ea12e99e8c11870b7118f0e1 100644
--- a/app/controllers/explore/catalog_controller.rb
+++ b/app/controllers/explore/catalog_controller.rb
@@ -23,7 +23,7 @@ def check_resource_access
     end
 
     def catalog_resource
-      ::Ci::Catalog::Listing.new(current_user).find_resource(id: params[:id])
+      ::Ci::Catalog::Listing.new(current_user).find_resource(full_path: params[:full_path])
     end
   end
 end
diff --git a/app/models/ci/catalog/listing.rb b/app/models/ci/catalog/listing.rb
index 9baf5e7b2ccd8c6dc45638b4adeed95aae40f8c9..ebdaba55eeefae9f72db9c853a9401b5790877db 100644
--- a/app/models/ci/catalog/listing.rb
+++ b/app/models/ci/catalog/listing.rb
@@ -30,8 +30,8 @@ def resources(namespace: nil, sort: nil, search: nil, scope: :all)
         end
       end
 
-      def find_resource(id:)
-        resource = Ci::Catalog::Resource.find_by_id(id)
+      def find_resource(id: nil, full_path: nil)
+        resource = id ? Ci::Catalog::Resource.find_by_id(id) : Project.find_by_full_path(full_path)&.catalog_resource
 
         return unless resource.present?
         return unless resource.published?
diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb
index 225f00ab35ea0a267f653b50bb52b52b8ff5ce56..8739450fb8e472b58d79a5d8a6344d380a223eaa 100644
--- a/app/models/ci/catalog/resource.rb
+++ b/app/models/ci/catalog/resource.rb
@@ -31,12 +31,16 @@ class Resource < ::ApplicationRecord
       scope :order_by_latest_released_at_desc, -> { reorder(arel_table[:latest_released_at].desc.nulls_last) }
       scope :order_by_latest_released_at_asc, -> { reorder(arel_table[:latest_released_at].asc.nulls_last) }
 
-      delegate :avatar_path, :star_count, to: :project
+      delegate :avatar_path, :star_count, :full_path, to: :project
 
       enum state: { draft: 0, published: 1 }
 
       before_create :sync_with_project
 
+      def to_param
+        full_path
+      end
+
       def unpublish!
         update!(state: :draft)
       end
diff --git a/config/routes/explore.rb b/config/routes/explore.rb
index 36c2432d0ccb0374b4b19ec53acc0996e8247050..4c7ed50567984826601e1b7dabd57170b1b49b82 100644
--- a/config/routes/explore.rb
+++ b/config/routes/explore.rb
@@ -11,7 +11,10 @@
   end
 
   resources :groups, only: [:index]
-  resources :catalog, only: [:index, :show], constraints: { id: /\d+/ }
+  scope :catalog do
+    get '/' => 'catalog#index', as: :catalog_index
+    get '/*full_path' => 'catalog#show', as: :catalog
+  end
   resources :snippets, only: [:index]
   root to: 'projects#index'
 end
diff --git a/spec/features/explore/catalog/catalog_spec.rb b/spec/features/explore/catalog/catalog_spec.rb
index d9ad27904e90b8502042491ef48f5ec9c9c4452c..00bbb02ebbf38a925be59bbf483da6da5d5df1bf 100644
--- a/spec/features/explore/catalog/catalog_spec.rb
+++ b/spec/features/explore/catalog/catalog_spec.rb
@@ -114,11 +114,11 @@
     let_it_be(:project) { create(:project, :repository, namespace: namespace) }
 
     before do
-      visit explore_catalog_path(id: new_ci_resource["id"])
+      visit explore_catalog_path(new_ci_resource)
     end
 
     context 'when the resource is published' do
-      let_it_be(:new_ci_resource) { create(:ci_catalog_resource, :published, project: project) }
+      let(:new_ci_resource) { create(:ci_catalog_resource, :published, project: project) }
 
       it 'navigates to the details page' do
         expect(page).to have_content('Go to the project')
@@ -126,7 +126,7 @@
     end
 
     context 'when the resource is not published' do
-      let_it_be(:new_ci_resource) { create(:ci_catalog_resource, project: project, state: :draft) }
+      let(:new_ci_resource) { create(:ci_catalog_resource, project: project, state: :draft) }
 
       it 'returns a 404' do
         expect(page).to have_title('Not Found')
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js
index 382f8e4620320ceea1d286c3e5e817998dc3ce02..0293cfec6f0ac618c3ac667842650e56a5a7c610 100644
--- a/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js
@@ -19,9 +19,9 @@ describe('CiResourceComponents', () => {
 
   const components = mockComponents.data.ciCatalogResource.components.nodes;
 
-  const resourceId = 'gid://gitlab/Ci::Catalog::Resource/1';
+  const resourcePath = 'twitter/project-1';
 
-  const defaultProps = { resourceId };
+  const defaultProps = { resourcePath };
 
   const createComponent = async () => {
     const handlers = [[getCiCatalogcomponentComponents, mockComponentsResponse]];
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_details_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_details_spec.js
index 1f7dcf9d4e5bf2d516dd661f92aaa319d4afd932..e4b6c1cd046f396527faa213769cf67d7d5adc8a 100644
--- a/spec/frontend/ci/catalog/components/details/ci_resource_details_spec.js
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_details_spec.js
@@ -8,7 +8,7 @@ describe('CiResourceDetails', () => {
   let wrapper;
 
   const defaultProps = {
-    resourceId: 'gid://gitlab/Ci::Catalog::Resource/1',
+    resourcePath: 'twitter/project-1',
   };
   const defaultProvide = {
     glFeatures: { ciCatalogComponentsTab: true },
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_readme_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_readme_spec.js
index 0dadac236a8df63318d213f4f98fa691dde941fe..ad76b47db574e2a585b0e847bbbdd1a9925a542d 100644
--- a/spec/frontend/ci/catalog/components/details/ci_resource_readme_spec.js
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_readme_spec.js
@@ -23,12 +23,13 @@ describe('CiResourceReadme', () => {
     data: {
       ciCatalogResource: {
         id: resourceId,
+        webPath: 'twitter/project-1',
         readmeHtml,
       },
     },
   };
 
-  const defaultProps = { resourceId };
+  const defaultProps = { resourcePath: readmeMockData.data.ciCatalogResource.webPath };
 
   const createComponent = ({ props = {} } = {}) => {
     const handlers = [[getCiCatalogResourceReadme, mockReadmeResponse]];
diff --git a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
index 5d20c64c1cb67a5a650e452a9e0f000646cb8b7a..d74b133f3868e46be5eefb0bb2ecb360bbefb56d 100644
--- a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
+++ b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
@@ -2,10 +2,10 @@ import Vue from 'vue';
 import VueRouter from 'vue-router';
 import { GlAvatar, GlBadge, GlSprintf } from '@gitlab/ui';
 import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
 import { createRouter } from '~/ci/catalog/router/index';
 import CiResourcesListItem from '~/ci/catalog/components/list/ci_resources_list_item.vue';
 import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { CI_RESOURCE_DETAILS_PAGE_NAME } from '~/ci/catalog/router/constants';
 import { catalogSinglePageResponse } from '../../mock';
 
 Vue.use(VueRouter);
@@ -70,9 +70,7 @@ describe('CiResourcesListItem', () => {
 
     it('renders the resource name and link', () => {
       expect(findResourceName().exists()).toBe(true);
-      expect(findResourceName().attributes().href).toBe(
-        `/${getIdFromGraphQLId(defaultProps.resource.id)}`,
-      );
+      expect(findResourceName().attributes().href).toBe(defaultProps.resource.webPath);
     });
 
     it('renders the resource version badge', () => {
@@ -128,10 +126,7 @@ describe('CiResourcesListItem', () => {
         await findResourceName().vm.$emit('click', defaultEvent);
 
         expect(routerPush).toHaveBeenCalledWith({
-          name: CI_RESOURCE_DETAILS_PAGE_NAME,
-          params: {
-            id: getIdFromGraphQLId(resource.id),
-          },
+          path: cleanLeadingSeparator(resource.webPath),
         });
       });
     });
@@ -160,12 +155,7 @@ describe('CiResourcesListItem', () => {
     });
 
     it('navigates to the details page', () => {
-      expect(routerPush).toHaveBeenCalledWith({
-        name: CI_RESOURCE_DETAILS_PAGE_NAME,
-        params: {
-          id: getIdFromGraphQLId(resource.id),
-        },
-      });
+      expect(routerPush).toHaveBeenCalledWith({ path: cleanLeadingSeparator(resource.webPath) });
     });
   });
 
diff --git a/spec/frontend/ci/catalog/components/pages/ci_resource_details_page_spec.js b/spec/frontend/ci/catalog/components/pages/ci_resource_details_page_spec.js
index 40f243ed89164342fef68576fc55bee6f22cc3f5..015c6504fa5c67402948384f10ec93ccf1b5a1e8 100644
--- a/spec/frontend/ci/catalog/components/pages/ci_resource_details_page_spec.js
+++ b/spec/frontend/ci/catalog/components/pages/ci_resource_details_page_spec.js
@@ -5,7 +5,8 @@ import { GlEmptyState } from '@gitlab/ui';
 import { shallowMount } from '@vue/test-utils';
 import createMockApollo from 'helpers/mock_apollo_helper';
 import waitForPromises from 'helpers/wait_for_promises';
-import { CI_CATALOG_RESOURCE_TYPE, cacheConfig } from '~/ci/catalog/graphql/settings';
+import { cacheConfig } from '~/ci/catalog/graphql/settings';
+import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
 
 import getCiCatalogResourceSharedData from '~/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql';
 import getCiCatalogResourceDetails from '~/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql';
@@ -17,7 +18,6 @@ import CiResourceHeaderSkeletonLoader from '~/ci/catalog/components/details/ci_r
 
 import { createRouter } from '~/ci/catalog/router/index';
 import { CI_RESOURCE_DETAILS_PAGE_NAME } from '~/ci/catalog/router/constants';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
 import { catalogSharedDataMock, catalogAdditionalDetailsMock } from '../../mock';
 
 Vue.use(VueApollo);
@@ -75,7 +75,7 @@ describe('CiResourceDetailsPage', () => {
     router = createRouter();
     await router.push({
       name: CI_RESOURCE_DETAILS_PAGE_NAME,
-      params: { id: defaultSharedData.id },
+      params: { id: defaultSharedData.webPath },
     });
   });
 
@@ -178,7 +178,7 @@ describe('CiResourceDetailsPage', () => {
 
       it('passes expected props', () => {
         expect(findDetailsComponent().props()).toEqual({
-          resourceId: convertToGraphQLId(CI_CATALOG_RESOURCE_TYPE, defaultAdditionalData.id),
+          resourcePath: cleanLeadingSeparator(defaultSharedData.webPath),
         });
       });
     });
diff --git a/spec/frontend/ci/catalog/mock.js b/spec/frontend/ci/catalog/mock.js
index 49aaadf3c672cd94157b500b4d43e16d3ec85005..2d5d198ebe1cb02b408229250c9682025837a116 100644
--- a/spec/frontend/ci/catalog/mock.js
+++ b/spec/frontend/ci/catalog/mock.js
@@ -305,7 +305,7 @@ export const catalogSharedDataMock = {
         releasedAt: Date.now(),
         author: { id: 1, webUrl: 'profile/1', name: 'username' },
       },
-      webPath: 'path/to/project',
+      webPath: '/path/to/project',
     },
   },
 };
@@ -315,6 +315,7 @@ export const catalogAdditionalDetailsMock = {
     ciCatalogResource: {
       __typename: 'CiCatalogResource',
       id: `gid://gitlab/CiCatalogResource/1`,
+      webPath: '/twitter/project',
       openIssuesCount: 4,
       openMergeRequestsCount: 10,
       readmeHtml: '<h1>Hello world</h1>',
@@ -386,6 +387,7 @@ export const mockComponents = {
     ciCatalogResource: {
       __typename: 'CiCatalogResource',
       id: `gid://gitlab/CiCatalogResource/1`,
+      webPath: 'twitter/project-1',
       components: {
         ...componentsMockData,
       },
@@ -398,6 +400,7 @@ export const mockComponentsEmpty = {
     ciCatalogResource: {
       __typename: 'CiCatalogResource',
       id: `gid://gitlab/CiCatalogResource/1`,
+      webPath: 'twitter/project-1',
       components: [],
     },
   },
diff --git a/spec/models/ci/catalog/listing_spec.rb b/spec/models/ci/catalog/listing_spec.rb
index 9d20d944e5ac6175336724fdf59bd53427af335c..2ffffb9112cbbab8f9f16e863ce9bd3552fd899d 100644
--- a/spec/models/ci/catalog/listing_spec.rb
+++ b/spec/models/ci/catalog/listing_spec.rb
@@ -186,55 +186,78 @@
 
   describe '#find_resource' do
     let_it_be(:accessible_resource) { create(:ci_catalog_resource, :published, project: public_project) }
+    let_it_be(:inaccessible_resource) { create(:ci_catalog_resource, :published, project: project_noaccess) }
+    let_it_be(:draft_resource) { create(:ci_catalog_resource, project: public_namespace_project, state: :draft) }
 
-    subject { list.find_resource(id: id) }
+    context 'when using the ID argument' do
+      subject { list.find_resource(id: id) }
 
-    context 'when the resource is published and visible to the user' do
-      let(:id) { accessible_resource.id }
+      context 'when the resource is published and visible to the user' do
+        let(:id) { accessible_resource.id }
 
-      it 'fetches the resource' do
-        is_expected.to eq(accessible_resource)
+        it 'fetches the resource' do
+          is_expected.to eq(accessible_resource)
+        end
       end
-    end
 
-    context 'when the resource is not found' do
-      let(:id) { 'not-an-id' }
+      context 'when the resource is not found' do
+        let(:id) { 'not-an-id' }
 
-      it { is_expected.to be_nil }
-    end
+        it 'returns nil' do
+          is_expected.to be_nil
+        end
+      end
+
+      context 'when the resource is not published' do
+        let(:id) { draft_resource.id }
 
-    context 'when the resource is not published' do
-      let_it_be(:draft_resource) { create(:ci_catalog_resource, project: public_namespace_project, state: :draft) }
+        it 'returns nil' do
+          is_expected.to be_nil
+        end
+      end
 
-      let(:id) { draft_resource.id }
+      context "when the current user cannot read code on the resource's project" do
+        let(:id) { inaccessible_resource.id }
 
-      it { is_expected.to be_nil }
+        it 'returns nil' do
+          is_expected.to be_nil
+        end
+      end
     end
 
-    context "when the current user cannot read code on the resource's project" do
-      let_it_be(:inaccessible_resource) { create(:ci_catalog_resource, :published, project: project_noaccess) }
+    context 'when using the full_path argument' do
+      subject { list.find_resource(full_path: full_path) }
 
-      let(:id) { inaccessible_resource.id }
+      context 'when the resource is published and visible to the user' do
+        let(:full_path) { accessible_resource.project.full_path }
 
-      it { is_expected.to be_nil }
-    end
+        it 'fetches the resource' do
+          is_expected.to eq(accessible_resource)
+        end
+      end
 
-    context 'when the current user is anonymous' do
-      let(:user) { nil }
+      context 'when the resource is not found' do
+        let(:full_path) { 'not-a-path' }
 
-      context 'when the resource is public' do
-        let(:id) { accessible_resource.id }
+        it 'returns nil' do
+          is_expected.to be_nil
+        end
+      end
 
-        it 'fetches the public resource' do
-          is_expected.to eq(accessible_resource)
+      context 'when the resource is not published' do
+        let(:full_path) { draft_resource.project.full_path }
+
+        it 'returns nil' do
+          is_expected.to be_nil
         end
       end
 
-      context 'when the resource is internal' do
-        let(:internal_resource) { create(:ci_catalog_resource, :published, project: internal_project) }
-        let(:id) { internal_resource.id }
+      context "when the current user cannot read code on the resource's project" do
+        let(:full_path) { inaccessible_resource.project.full_path }
 
-        it { is_expected.to be_nil }
+        it 'returns nil' do
+          is_expected.to be_nil
+        end
       end
     end
   end
diff --git a/spec/requests/explore/catalog_controller_spec.rb b/spec/requests/explore/catalog_controller_spec.rb
index 25131eb17b5e353ac9389f3d499c4221683f935b..1361e20777926462ef4a3b7efd9d29095fd55948 100644
--- a/spec/requests/explore/catalog_controller_spec.rb
+++ b/spec/requests/explore/catalog_controller_spec.rb
@@ -19,7 +19,7 @@
       if action == :index
         explore_catalog_index_path
       else
-        explore_catalog_path(id: catalog_resource.id)
+        explore_catalog_path(catalog_resource)
       end
     end
 
@@ -55,7 +55,7 @@
       it 'responds with 404' do
         catalog_resource = create(:ci_catalog_resource, state: :draft)
 
-        get explore_catalog_path(id: catalog_resource.id)
+        get explore_catalog_path(catalog_resource)
 
         expect(response).to have_gitlab_http_status(:not_found)
       end