mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 05:35:13 +00:00
feat(nuxt): allow organising pages within route groups (#28276)
This commit is contained in:
parent
5bf971f4ef
commit
c4ae151de5
@ -226,6 +226,22 @@ definePageMeta({
|
||||
|
||||
:link-example{to="/docs/examples/routing/pages"}
|
||||
|
||||
## Route Groups
|
||||
|
||||
In some cases, you may want to group a set of routes together in a way which doesn't affect file-based routing. For this purpose, you can put files in a folder which is wrapped in parentheses - `(` and `)`.
|
||||
|
||||
For example:
|
||||
|
||||
```bash [Directory structure]
|
||||
-| pages/
|
||||
---| index.vue
|
||||
---| (marketing)/
|
||||
-----| about.vue
|
||||
-----| contact.vue
|
||||
```
|
||||
|
||||
This will produce `/`, `/about` and `/contact` pages in your app. The `marketing` group is ignored for purposes of your URL structure.
|
||||
|
||||
## Page Metadata
|
||||
|
||||
You might want to define metadata for each route in your app. You can do this using the `definePageMeta` macro, which will work both in `<script>` and in `<script setup>`:
|
||||
|
@ -23,6 +23,7 @@ enum SegmentParserState {
|
||||
dynamic,
|
||||
optional,
|
||||
catchall,
|
||||
group,
|
||||
}
|
||||
|
||||
enum SegmentTokenType {
|
||||
@ -30,6 +31,7 @@ enum SegmentTokenType {
|
||||
dynamic,
|
||||
optional,
|
||||
catchall,
|
||||
group,
|
||||
}
|
||||
|
||||
interface SegmentToken {
|
||||
@ -115,7 +117,13 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
|
||||
const segment = segments[i]
|
||||
|
||||
const tokens = parseSegment(segment)
|
||||
const segmentName = tokens.map(({ value }) => value).join('')
|
||||
|
||||
// Skip group segments
|
||||
if (tokens.every(token => token.type === SegmentTokenType.group)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const segmentName = tokens.map(({ value, type }) => type === SegmentTokenType.group ? '' : value).join('')
|
||||
|
||||
// ex: parent/[slug].vue -> parent-slug
|
||||
route.name += (route.name && '/') + segmentName
|
||||
@ -298,7 +306,9 @@ function getRoutePath (tokens: SegmentToken[]): string {
|
||||
? `:${token.value}()`
|
||||
: token.type === SegmentTokenType.catchall
|
||||
? `:${token.value}(.*)*`
|
||||
: encodePath(token.value).replace(/:/g, '\\:'))
|
||||
: token.type === SegmentTokenType.group
|
||||
? ''
|
||||
: encodePath(token.value).replace(/:/g, '\\:'))
|
||||
)
|
||||
}, '/')
|
||||
}
|
||||
@ -328,7 +338,9 @@ function parseSegment (segment: string) {
|
||||
? SegmentTokenType.dynamic
|
||||
: state === SegmentParserState.optional
|
||||
? SegmentTokenType.optional
|
||||
: SegmentTokenType.catchall,
|
||||
: state === SegmentParserState.catchall
|
||||
? SegmentTokenType.catchall
|
||||
: SegmentTokenType.group,
|
||||
value: buffer,
|
||||
})
|
||||
|
||||
@ -343,6 +355,8 @@ function parseSegment (segment: string) {
|
||||
buffer = ''
|
||||
if (c === '[') {
|
||||
state = SegmentParserState.dynamic
|
||||
} else if (c === '(') {
|
||||
state = SegmentParserState.group
|
||||
} else {
|
||||
i--
|
||||
state = SegmentParserState.static
|
||||
@ -353,6 +367,9 @@ function parseSegment (segment: string) {
|
||||
if (c === '[') {
|
||||
consumeBuffer()
|
||||
state = SegmentParserState.dynamic
|
||||
} else if (c === '(') {
|
||||
consumeBuffer()
|
||||
state = SegmentParserState.group
|
||||
} else {
|
||||
buffer += c
|
||||
}
|
||||
@ -361,6 +378,7 @@ function parseSegment (segment: string) {
|
||||
case SegmentParserState.catchall:
|
||||
case SegmentParserState.dynamic:
|
||||
case SegmentParserState.optional:
|
||||
case SegmentParserState.group:
|
||||
if (buffer === '...') {
|
||||
buffer = ''
|
||||
state = SegmentParserState.catchall
|
||||
@ -375,10 +393,16 @@ function parseSegment (segment: string) {
|
||||
consumeBuffer()
|
||||
}
|
||||
state = SegmentParserState.initial
|
||||
} else if (c === ')' && state === SegmentParserState.group) {
|
||||
if (!buffer) {
|
||||
throw new Error('Empty group')
|
||||
} else {
|
||||
consumeBuffer()
|
||||
}
|
||||
state = SegmentParserState.initial
|
||||
} else if (PARAM_CHAR_RE.test(c)) {
|
||||
buffer += c
|
||||
} else {
|
||||
|
||||
// console.debug(`[pages]Ignored character "${c}" while building param "${buffer}" from "segment"`)
|
||||
}
|
||||
break
|
||||
|
@ -339,6 +339,34 @@
|
||||
"redirect": "mockMeta?.redirect",
|
||||
},
|
||||
],
|
||||
"should handle route groups": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/(foo)/index.vue").then(m => m.default || m)",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "index"",
|
||||
"path": "mockMeta?.path ?? "/"",
|
||||
"redirect": "mockMeta?.redirect",
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"children": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/(bar)/about/index.vue").then(m => m.default || m)",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "about"",
|
||||
"path": "mockMeta?.path ?? """,
|
||||
"redirect": "mockMeta?.redirect",
|
||||
},
|
||||
],
|
||||
"component": "() => import("pages/(foo)/about.vue").then(m => m.default || m)",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? undefined",
|
||||
"path": "mockMeta?.path ?? "/about"",
|
||||
"redirect": "mockMeta?.redirect",
|
||||
},
|
||||
],
|
||||
"should handle trailing slashes with index routes": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
|
@ -234,6 +234,25 @@
|
||||
"path": ""/parent"",
|
||||
},
|
||||
],
|
||||
"should handle route groups": [
|
||||
{
|
||||
"component": "() => import("pages/(foo)/index.vue").then(m => m.default || m)",
|
||||
"name": ""index"",
|
||||
"path": ""/"",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"component": "() => import("pages/(bar)/about/index.vue").then(m => m.default || m)",
|
||||
"name": ""about"",
|
||||
"path": """",
|
||||
},
|
||||
],
|
||||
"component": "() => import("pages/(foo)/about.vue").then(m => m.default || m)",
|
||||
"name": "mockMeta?.name",
|
||||
"path": ""/about"",
|
||||
},
|
||||
],
|
||||
"should handle trailing slashes with index routes": [
|
||||
{
|
||||
"children": [
|
||||
|
@ -601,6 +601,37 @@ describe('pages:generateRoutesFromFiles', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: 'should handle route groups',
|
||||
files: [
|
||||
{ path: `${pagesDir}/(foo)/index.vue` },
|
||||
{ path: `${pagesDir}/(foo)/about.vue` },
|
||||
{ path: `${pagesDir}/(bar)/about/index.vue` },
|
||||
],
|
||||
output: [
|
||||
{
|
||||
name: 'index',
|
||||
path: '/',
|
||||
file: `${pagesDir}/(foo)/index.vue`,
|
||||
meta: undefined,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
file: `${pagesDir}/(foo)/about.vue`,
|
||||
meta: undefined,
|
||||
children: [
|
||||
|
||||
{
|
||||
name: 'about',
|
||||
path: '',
|
||||
file: `${pagesDir}/(bar)/about/index.vue`,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const normalizedResults: Record<string, any> = {}
|
||||
|
@ -570,6 +570,14 @@ describe('pages', () => {
|
||||
|
||||
await normalInitialPage.close()
|
||||
})
|
||||
|
||||
it('groups routes', async () => {
|
||||
for (const targetRoute of ['/group-page', '/nested-group/group-page', '/nested-group']) {
|
||||
const { status } = await fetch(targetRoute)
|
||||
|
||||
expect(status).toBe(200)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('nuxt composables', () => {
|
||||
|
5
test/fixtures/basic/pages/(new-group)/group-page.vue
vendored
Normal file
5
test/fixtures/basic/pages/(new-group)/group-page.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
Hello from new group
|
||||
</div>
|
||||
</template>
|
5
test/fixtures/basic/pages/nested-group/(deep-group)/group-page.vue
vendored
Normal file
5
test/fixtures/basic/pages/nested-group/(deep-group)/group-page.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
Page deep in group
|
||||
</div>
|
||||
</template>
|
5
test/fixtures/basic/pages/nested-group/(index-group)/index.vue
vendored
Normal file
5
test/fixtures/basic/pages/nested-group/(index-group)/index.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
Index page of a group
|
||||
</div>
|
||||
</template>
|
5
test/fixtures/basic/pages/nested-group/more-nested/(more-deep)/group-page.vue
vendored
Normal file
5
test/fixtures/basic/pages/nested-group/more-nested/(more-deep)/group-page.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
Page deep, deep in group
|
||||
</div>
|
||||
</template>
|
Loading…
Reference in New Issue
Block a user