fix: random 404s (#6138)

* fix: random 404s when navigating from user, org, or collection page to project

* more

* revert server

* clean names

* prepr
This commit is contained in:
Prospector
2026-05-19 11:11:48 -07:00
committed by GitHub
parent f106dc580f
commit 302b60d89c
45 changed files with 127 additions and 123 deletions
@@ -233,7 +233,7 @@ const visibleNags = computed<Nag[]>(() => {
link: {
path: 'moderation',
title: messages.visitModerationPage,
shouldShow: () => props.routeName !== 'type-id-moderation',
shouldShow: () => props.routeName !== 'type-project-moderation',
},
})
}
@@ -739,8 +739,8 @@ async function navigateToNextUnlockedProject(): Promise<boolean> {
} else {
// Fallback: use project ID (will trigger middleware redirect)
navigateTo({
name: 'type-id',
params: { type: 'project', id: next.projectId },
name: 'type-project',
params: { type: 'project', project: next.projectId },
state: { showChecklist: true },
})
}
@@ -1067,8 +1067,8 @@ async function skipToNextProject() {
} else {
// Fallback: use project ID
navigateTo({
name: 'type-id',
params: { type: 'project', id },
name: 'type-project',
params: { type: 'project', project: id },
state: { showChecklist: true },
})
}
@@ -2127,8 +2127,8 @@ async function endChecklist(status?: string) {
} else {
// Fallback: use project ID
navigateTo({
name: 'type-id',
params: { type: 'project', id },
name: 'type-project',
params: { type: 'project', project: id },
state: { showChecklist: true },
})
}
+1 -1
View File
@@ -1148,7 +1148,7 @@ const isDiscovering = computed(
)
const isDiscoveringSubpage = computed(
() => route.name && route.name.startsWith('type-id') && !route.query.sid,
() => route.name && route.name.startsWith('type-project') && !route.query.sid,
)
const isRussia = computed(() => country.value === 'ru')
@@ -485,7 +485,7 @@
v-if="!isServerProject"
size="large"
:color="
(auth.user && currentMember) || route.name === 'type-id-version-version'
(auth.user && currentMember) || route.name === 'type-project-version-version'
? `standard`
: `brand`
"
@@ -507,7 +507,7 @@
v-else
size="large"
:color="
(auth.user && currentMember) || route.name === 'type-id-version-version'
(auth.user && currentMember) || route.name === 'type-project-version-version'
? `standard`
: `brand`
"
@@ -529,7 +529,7 @@
size="large"
circular
:color="
route.name === 'type-id-version-version' || (auth.user && currentMember)
route.name === 'type-project-version-version' || (auth.user && currentMember)
? `standard`
: `brand`
"
@@ -547,7 +547,7 @@
size="large"
circular
:color="
route.name === 'type-id-version-version' || (auth.user && currentMember)
route.name === 'type-project-version-version' || (auth.user && currentMember)
? `standard`
: `brand`
"
@@ -1136,7 +1136,7 @@ import { useModerationQueue } from '~/services/moderation-queue.ts'
import { getReportPath, reportProject } from '~/utils/report-helpers.ts'
definePageMeta({
key: (route) => `${route.params.id}`,
key: (route) => `${route.params.project}`,
})
const data = useNuxtApp()
@@ -1151,6 +1151,9 @@ const { addNotification } = notifications
const auth = await useAuth()
const user = await useUser()
// Route param for initial lookup (middleware caches by both slug and ID)
const routeProjectId = useRouteId('project')
const { createProjectDownloadUrl } = useCdnDownloadContext()
const downloadReason = ref('standalone')
@@ -1637,7 +1640,7 @@ const collections = computed(() =>
)
if (
!route.params.id ||
!routeProjectId ||
!(
tags.value.projectTypes.find((x) => x.id === route.params.type) ||
route.params.type === 'project'
@@ -1650,17 +1653,14 @@ if (
})
}
// Route param for initial lookup (middleware caches by both slug and ID)
const routeProjectId = computed(() => route.params.id)
// Use DI client for TanStack Query
const client = injectModrinthClient()
const queryClient = useQueryClient()
// V2 Project - hits middleware cache (uses route param for lookup)
const { data: projectRaw, error: projectV2Error } = useQuery({
queryKey: computed(() => ['project', 'v2', routeProjectId.value]),
queryFn: () => client.labrinth.projects_v2.get(routeProjectId.value),
queryKey: computed(() => ['project', 'v2', routeProjectId]),
queryFn: () => client.labrinth.projects_v2.get(routeProjectId),
staleTime: STALE_TIME,
})
@@ -1709,8 +1709,8 @@ const {
error: _projectV3Error,
isPending: projectV3Pending,
} = useQuery({
queryKey: computed(() => ['project', 'v3', routeProjectId.value]),
queryFn: () => client.labrinth.projects_v3.get(routeProjectId.value),
queryKey: computed(() => ['project', 'v3', routeProjectId]),
queryFn: () => client.labrinth.projects_v3.get(routeProjectId),
staleTime: STALE_TIME,
})
@@ -1870,7 +1870,7 @@ const { data: organizationRaw } = useQuery({
// Return null when the project no longer belongs to an organization.
const organization = computed(() => (projectRaw.value?.organization ? organizationRaw.value : null))
const isSettings = computed(() => route.name.startsWith('type-id-settings'))
const isSettings = computed(() => route.name.startsWith('type-project-settings'))
// Transform versionsV3 to be same shape as versionsV2 for compatibility in project pages
const versionsRaw = computed(() => {
@@ -1914,7 +1914,7 @@ const hasVersions = computed(() => (project.value?.versions?.length ?? 0) > 0)
async function updateProjectRoute() {
if (
project.value &&
route.params.id !== project.value.slug &&
routeProjectId !== project.value.slug &&
!flags.value.disablePrettyProjectUrlRedirects
) {
await navigateTo(
@@ -1933,9 +1933,9 @@ async function updateProjectRoute() {
}
async function invalidateProject() {
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
await queryClient.invalidateQueries({ queryKey: ['project', 'v3', routeProjectId.value] })
if (routeProjectId.value !== projectId.value) {
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', routeProjectId] })
await queryClient.invalidateQueries({ queryKey: ['project', 'v3', routeProjectId] })
if (routeProjectId !== projectId.value) {
await queryClient.invalidateQueries({ queryKey: ['project', 'v2', projectId.value] })
await queryClient.invalidateQueries({ queryKey: ['project', 'v3', projectId.value] })
}
@@ -1953,16 +1953,16 @@ const patchProjectMutation = useMutation({
onMutate: async ({ projectId, data }) => {
// Cancel outgoing refetches for both slug-based and ID-based cache keys
// The query may be keyed by slug (routeProjectId) but we also have the actual UUID (projectId)
await queryClient.cancelQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
if (routeProjectId.value !== projectId) {
await queryClient.cancelQueries({ queryKey: ['project', 'v2', routeProjectId] })
if (routeProjectId !== projectId) {
await queryClient.cancelQueries({ queryKey: ['project', 'v2', projectId] })
}
// Snapshot previous value from the active query (uses route param as key)
const previousProject = queryClient.getQueryData(['project', 'v2', routeProjectId.value])
const previousProject = queryClient.getQueryData(['project', 'v2', routeProjectId])
// Optimistic update on the active query key
queryClient.setQueryData(['project', 'v2', routeProjectId.value], (old) => {
queryClient.setQueryData(['project', 'v2', routeProjectId], (old) => {
if (!old) return old
return { ...old, ...data }
})
@@ -1973,7 +1973,7 @@ const patchProjectMutation = useMutation({
onError: (err, _variables, context) => {
// Rollback on error using the active query key
if (context?.previousProject) {
queryClient.setQueryData(['project', 'v2', routeProjectId.value], context.previousProject)
queryClient.setQueryData(['project', 'v2', routeProjectId], context.previousProject)
}
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
@@ -1995,16 +1995,16 @@ const patchStatusMutation = useMutation({
onMutate: async ({ projectId, status }) => {
// Cancel outgoing refetches for both slug-based and ID-based cache keys
await queryClient.cancelQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
if (routeProjectId.value !== projectId) {
await queryClient.cancelQueries({ queryKey: ['project', 'v2', routeProjectId] })
if (routeProjectId !== projectId) {
await queryClient.cancelQueries({ queryKey: ['project', 'v2', projectId] })
}
// Snapshot previous value from the active query (uses route param as key)
const previousProject = queryClient.getQueryData(['project', 'v2', routeProjectId.value])
const previousProject = queryClient.getQueryData(['project', 'v2', routeProjectId])
// Optimistic update on the active query key
queryClient.setQueryData(['project', 'v2', routeProjectId.value], (old) => {
queryClient.setQueryData(['project', 'v2', routeProjectId], (old) => {
if (!old) return old
return { ...old, status }
})
@@ -2015,7 +2015,7 @@ const patchStatusMutation = useMutation({
onError: (err, _variables, context) => {
// Rollback on error using the active query key
if (context?.previousProject) {
queryClient.setQueryData(['project', 'v2', routeProjectId.value], context.previousProject)
queryClient.setQueryData(['project', 'v2', routeProjectId], context.previousProject)
}
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
@@ -2121,11 +2121,11 @@ const createGalleryItemMutation = useMutation({
},
onMutate: async ({ title, description, featured, ordering }) => {
await queryClient.cancelQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
await queryClient.cancelQueries({ queryKey: ['project', 'v2', routeProjectId] })
const previousProject = queryClient.getQueryData(['project', 'v2', routeProjectId.value])
const previousProject = queryClient.getQueryData(['project', 'v2', routeProjectId])
queryClient.setQueryData(['project', 'v2', routeProjectId.value], (old) => {
queryClient.setQueryData(['project', 'v2', routeProjectId], (old) => {
if (!old) return old
const newItem = {
url: '',
@@ -2147,7 +2147,7 @@ const createGalleryItemMutation = useMutation({
onError: (err, _variables, context) => {
if (context?.previousProject) {
queryClient.setQueryData(['project', 'v2', routeProjectId.value], context.previousProject)
queryClient.setQueryData(['project', 'v2', routeProjectId], context.previousProject)
}
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
@@ -2172,11 +2172,11 @@ const editGalleryItemMutation = useMutation({
},
onMutate: async ({ imageUrl, title, description, featured, ordering }) => {
await queryClient.cancelQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
await queryClient.cancelQueries({ queryKey: ['project', 'v2', routeProjectId] })
const previousProject = queryClient.getQueryData(['project', 'v2', routeProjectId.value])
const previousProject = queryClient.getQueryData(['project', 'v2', routeProjectId])
queryClient.setQueryData(['project', 'v2', routeProjectId.value], (old) => {
queryClient.setQueryData(['project', 'v2', routeProjectId], (old) => {
if (!old) return old
return {
...old,
@@ -2200,7 +2200,7 @@ const editGalleryItemMutation = useMutation({
onError: (err, _variables, context) => {
if (context?.previousProject) {
queryClient.setQueryData(['project', 'v2', routeProjectId.value], context.previousProject)
queryClient.setQueryData(['project', 'v2', routeProjectId], context.previousProject)
}
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
@@ -2220,11 +2220,11 @@ const deleteGalleryItemMutation = useMutation({
},
onMutate: async ({ imageUrl }) => {
await queryClient.cancelQueries({ queryKey: ['project', 'v2', routeProjectId.value] })
await queryClient.cancelQueries({ queryKey: ['project', 'v2', routeProjectId] })
const previousProject = queryClient.getQueryData(['project', 'v2', routeProjectId.value])
const previousProject = queryClient.getQueryData(['project', 'v2', routeProjectId])
queryClient.setQueryData(['project', 'v2', routeProjectId.value], (old) => {
queryClient.setQueryData(['project', 'v2', routeProjectId], (old) => {
if (!old) return old
return {
...old,
@@ -2237,7 +2237,7 @@ const deleteGalleryItemMutation = useMutation({
onError: (err, _variables, context) => {
if (context?.previousProject) {
queryClient.setQueryData(['project', 'v2', routeProjectId.value], context.previousProject)
queryClient.setQueryData(['project', 'v2', routeProjectId], context.previousProject)
}
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
@@ -2372,7 +2372,7 @@ useHead({
],
})
if (!route.name.startsWith('type-id-settings')) {
if (!route.name.startsWith('type-project-settings')) {
useSeoMeta({
title: () => title.value,
description: () => description.value,
@@ -47,10 +47,8 @@ const navItems = computed(() => {
projectV3.value?.project_types?.some((type) => ['mod', 'modpack'].includes(type)) &&
isStaff(currentMember.value?.user)
const hasPermissionsPage = computed(
() =>
flags.value.modpackPermissionsPage &&
projectV3.value?.project_types?.some((type) => ['modpack'].includes(type)),
const hasPermissionsPage = computed(() =>
projectV3.value?.project_types?.some((type) => ['modpack'].includes(type)),
)
const items = [
@@ -360,7 +360,6 @@ const formatDateTimeShort = useFormatDateTime({
minute: 'numeric',
})
const route = useRoute()
const vintl = useVIntl()
const { formatMessage } = vintl
@@ -373,13 +372,15 @@ const messages = defineMessages({
},
})
const chargeId = useRouteId('charge')
const {
data: user,
error: userError,
suspense: userSuspense,
} = useQuery({
queryKey: ['user', route.params.id],
queryFn: () => labrinth.users_v2.get(route.params.id),
queryKey: ['user', chargeId],
queryFn: () => labrinth.users_v2.get(chargeId),
})
onServerPrefetch(userSuspense)
@@ -529,8 +529,8 @@ const returnLink = computed(() => {
return null
})
const collectionId = computed(() => route.params.id)
const isFollowingCollection = computed(() => collectionId.value === 'following')
const collectionId = useRouteId('collection')
const isFollowingCollection = computed(() => collectionId === 'following')
// Static collection for "following" page
const followingCollection = computed(() =>
@@ -555,15 +555,15 @@ const {
error: collectionError,
isPending: collectionIsPending,
} = useQuery({
queryKey: computed(() => ['collection', collectionId.value]),
queryFn: () => api.labrinth.collections.get(collectionId.value),
enabled: computed(() => !!collectionId.value && !isFollowingCollection.value),
queryKey: computed(() => ['collection', collectionId]),
queryFn: () => api.labrinth.collections.get(collectionId),
enabled: computed(() => !!collectionId && !isFollowingCollection.value),
})
watch(
collectionError,
(error) => {
if (error && collectionId.value && !isFollowingCollection.value) {
if (error && collectionId && !isFollowingCollection.value) {
const status = error.statusCode ?? error.status ?? 404
showError({
fatal: true,
@@ -1,7 +1,7 @@
<template>
<ReportView
:auth="auth"
:report-id="route.params.id"
:report-id="reportId"
:breadcrumbs-stack="[
{ href: '/dashboard/reports', label: formatMessage(messages.activeReportsTitle) },
]"
@@ -25,10 +25,10 @@ const messages = defineMessages({
},
})
const route = useNativeRoute()
const auth = await useAuth()
const reportId = useRouteId('report')
useHead({
title: () => `${formatMessage(messages.reportTitle, { id: route.params.id })} - Modrinth`,
title: () => `${formatMessage(messages.reportTitle, { id: reportId })} - Modrinth`,
})
</script>
+4 -4
View File
@@ -529,10 +529,10 @@ async function moderateAllInFilter() {
}
navigateTo({
name: 'type-id',
name: 'type-project',
params: {
type: 'project',
id: targetProject.project.slug,
project: targetProject.project.slug,
},
state: {
showChecklist: true,
@@ -566,10 +566,10 @@ async function startFromProject(projectId: string) {
}
navigateTo({
name: 'type-id',
name: 'type-project',
params: {
type: 'project',
id: targetProject.project.slug,
project: targetProject.project.slug,
},
state: {
showChecklist: true,
@@ -7,8 +7,7 @@ import ModerationReportCard from '~/components/ui/moderation/ModerationReportCar
import { enrichReportBatch } from '~/helpers/moderation.ts'
const client = injectModrinthClient()
const { params } = useRoute()
const reportId = params.id as string
const reportId = String(useRouteId('report'))
const { data: report } = useQuery({
queryKey: computed(() => ['report', reportId]),
@@ -9,13 +9,12 @@ import MaliciousSummaryModal, {
} from '~/components/ui/moderation/MaliciousSummaryModal.vue'
import ModerationTechRevCard from '~/components/ui/moderation/ModerationTechRevCard.vue'
const route = useRoute()
const client = injectModrinthClient()
const queryClient = useQueryClient()
const projectId = computed(() => route.params.id as string)
const projectId = String(useRouteId('project'))
useHead({ title: () => `Tech review - ${projectId.value} - Modrinth` })
useHead({ title: () => `Tech review - ${projectId} - Modrinth` })
const CACHE_TTL = 24 * 60 * 60 * 1000
const CACHE_KEY_PREFIX = 'tech_review_source_'
@@ -151,7 +150,7 @@ const {
} = useQuery({
queryKey: ['tech-review-project-report', projectId],
queryFn: async () => {
return await client.labrinth.tech_review_internal.getProjectReport(projectId.value)
return await client.labrinth.tech_review_internal.getProjectReport(projectId)
},
retry: false,
})
@@ -163,7 +162,7 @@ const {
} = useQuery({
queryKey: ['project', projectId],
queryFn: async () => {
return await client.labrinth.projects_v3.get(projectId.value)
return await client.labrinth.projects_v3.get(projectId)
},
retry: false,
})
@@ -171,14 +170,14 @@ const {
const { data: organizationData, isLoading: isLoadingOrg } = useQuery({
queryKey: ['project-organization', projectId],
queryFn: async () => {
return await client.labrinth.projects_v3.getOrganization(projectId.value)
return await client.labrinth.projects_v3.getOrganization(projectId)
},
})
const { data: membersData, isLoading: isLoadingMembers } = useQuery({
queryKey: ['project-members', projectId],
queryFn: async () => {
return await client.labrinth.projects_v3.getMembers(projectId.value)
return await client.labrinth.projects_v3.getMembers(projectId)
},
enabled: computed(() => !organizationData.value && !isLoadingOrg.value),
})
@@ -265,7 +264,7 @@ function handleShowMaliciousSummary(unsafeFiles: UnsafeFile[]) {
}
function refetch() {
queryClient.invalidateQueries({ queryKey: ['tech-review-project-report', projectId.value] })
queryClient.invalidateQueries({ queryKey: ['tech-review-project-report', projectId] })
}
</script>
@@ -348,7 +348,7 @@ const tags = useGeneratedState()
const config = useRuntimeConfig()
const modal_creation = useTemplateRef('modal_creation')
const orgId = useRouteId()
const orgId = useRouteId('organization')
if (route.path.includes('settings')) {
useSeoMeta({
@@ -85,7 +85,7 @@ const showPreviewImage = (files) => {
}
}
const orgId = useRouteId()
const orgId = useRouteId('orgId')
const save = async () => {
// Save field changes via useSavable
@@ -685,13 +685,15 @@ const messages = defineMessages({
const client = injectModrinthClient()
const userId = useRouteId('user')
const {
data: user,
error: userError,
suspense: userSuspense,
} = useQuery({
queryKey: computed(() => ['user', route.params.id]),
queryFn: () => client.labrinth.users_v2.get(route.params.id),
queryKey: computed(() => ['user', userId]),
queryFn: () => client.labrinth.users_v2.get(userId),
})
watch(
@@ -710,9 +712,9 @@ watch(
)
const { data: projects, suspense: projectsSuspense } = useQuery({
queryKey: computed(() => ['user', route.params.id, 'projects']),
queryKey: computed(() => ['user', userId, 'projects']),
queryFn: async () => {
const projects = await client.labrinth.users_v2.getProjects(route.params.id)
const projects = await client.labrinth.users_v2.getProjects(userId)
for (const project of projects) {
project.categories = project.categories.concat(project.loaders)
project.project_type = data.$getProjectTypeForUrl(
@@ -726,13 +728,13 @@ const { data: projects, suspense: projectsSuspense } = useQuery({
})
const { data: organizations, suspense: orgsSuspense } = useQuery({
queryKey: computed(() => ['user', route.params.id, 'organizations']),
queryFn: () => client.labrinth.users_v2.getOrganizations(route.params.id),
queryKey: computed(() => ['user', userId, 'organizations']),
queryFn: () => client.labrinth.users_v2.getOrganizations(userId),
})
const { data: collections, suspense: collectionsSuspense } = useQuery({
queryKey: computed(() => ['user', route.params.id, 'collections']),
queryFn: () => client.labrinth.users_v2.getCollections(route.params.id),
queryKey: computed(() => ['user', userId, 'collections']),
queryFn: () => client.labrinth.users_v2.getCollections(userId),
})
onServerPrefetch(async () => {
@@ -865,7 +867,7 @@ const isAdminViewing = computed(() => isAdmin(auth.value.user))
async function toggleAffiliate(id) {
await client.labrinth.users_v2.patch(id, { badges: user.value.badges ^ (1 << 7) })
queryClient.invalidateQueries({ queryKey: ['user', route.params.id] })
queryClient.invalidateQueries({ queryKey: ['user', userId] })
}
const navLinks = computed(() => [