Files
Modrinth-plus/apps/frontend/src/middleware/project.global.ts
Mingxuan Ding 1848ba3b29 fix: server-only project middleware (#5538)
* fix(navigation): use replaceState for project filters to prevent history pollution

* fix: add replace prop to NavTabs and enable it on project and discover pages

* style: run pnpm run fix on affected files

* enable NavTabs replace prop on collection, user, and org pages

* fix: guard project middleware on client

* fix: lint

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
2026-03-16 22:09:43 +00:00

81 lines
2.5 KiB
TypeScript

import { useGeneratedState } from '~/composables/generated'
import { projectQueryOptions } from '~/composables/queries/project'
import { useAppQueryClient } from '~/composables/query-client'
import { getProjectTypeForUrlShorthand } from '~/helpers/projects.js'
import { useServerModrinthClient } from '~/server/utils/api-client'
// All valid project type URL segments
const PROJECT_TYPES = [
'project',
'mod',
'plugin',
'datapack',
'shader',
'resourcepack',
'modpack',
'server',
'minecraft_java_server',
]
export default defineNuxtRouteMiddleware(async (to) => {
// Only run this middleware on the server - it relies on server-only runtime config
if (import.meta.client) return
// Only handle project routes
if (!to.params.id || !PROJECT_TYPES.includes(to.params.type as string)) {
return
}
const queryClient = useAppQueryClient()
const authToken = useCookie('auth-token')
const client = useServerModrinthClient({ authToken: authToken.value || undefined })
const tags = useGeneratedState()
const projectId = to.params.id as string
try {
// Fetch v2 project for redirect check AND cache it for the page
// Using fetchQuery ensures the page's useQuery gets this cached result
const project = await queryClient.fetchQuery(projectQueryOptions.v2(projectId, client))
const projectV3 = await queryClient.fetchQuery(projectQueryOptions.v3(projectId, client))
// Let page handle 404
if (!project) return
// Cache by slug if we looked up by ID (or vice versa)
if (projectId !== project.slug) {
queryClient.setQueryData(['project', 'v2', project.slug], project)
}
if (projectId !== project.id) {
queryClient.setQueryData(['project', 'v2', project.id], project)
}
const projectType = projectV3.minecraft_server != null ? 'server' : project.project_type
// Determine the correct URL type
const correctType = getProjectTypeForUrlShorthand(projectType, project.loaders, tags.value)
// Preserve the rest of the path (subpages like /versions, /settings, etc.)
const pathParts = to.path.split('/')
pathParts.splice(0, 3) // Remove '', type, and id
const remainder = pathParts.filter((x) => x).join('/')
// Build the canonical path
const canonicalPath = `/${correctType}/${project.slug}${remainder ? `/${remainder}` : ''}`
// Only redirect if the path actually changed
if (to.path !== canonicalPath) {
return navigateTo(
{
path: canonicalPath,
query: to.query,
hash: to.hash,
},
{
redirectCode: 301,
replace: true,
},
)
}
} catch {
// Let the page handle 404s and other errors
}
})