mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 13:45:18 +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"}
|
: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
|
## 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>`:
|
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,
|
dynamic,
|
||||||
optional,
|
optional,
|
||||||
catchall,
|
catchall,
|
||||||
|
group,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SegmentTokenType {
|
enum SegmentTokenType {
|
||||||
@ -30,6 +31,7 @@ enum SegmentTokenType {
|
|||||||
dynamic,
|
dynamic,
|
||||||
optional,
|
optional,
|
||||||
catchall,
|
catchall,
|
||||||
|
group,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SegmentToken {
|
interface SegmentToken {
|
||||||
@ -115,7 +117,13 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
|
|||||||
const segment = segments[i]
|
const segment = segments[i]
|
||||||
|
|
||||||
const tokens = parseSegment(segment)
|
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
|
// ex: parent/[slug].vue -> parent-slug
|
||||||
route.name += (route.name && '/') + segmentName
|
route.name += (route.name && '/') + segmentName
|
||||||
@ -298,7 +306,9 @@ function getRoutePath (tokens: SegmentToken[]): string {
|
|||||||
? `:${token.value}()`
|
? `:${token.value}()`
|
||||||
: token.type === SegmentTokenType.catchall
|
: token.type === SegmentTokenType.catchall
|
||||||
? `:${token.value}(.*)*`
|
? `:${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
|
? SegmentTokenType.dynamic
|
||||||
: state === SegmentParserState.optional
|
: state === SegmentParserState.optional
|
||||||
? SegmentTokenType.optional
|
? SegmentTokenType.optional
|
||||||
: SegmentTokenType.catchall,
|
: state === SegmentParserState.catchall
|
||||||
|
? SegmentTokenType.catchall
|
||||||
|
: SegmentTokenType.group,
|
||||||
value: buffer,
|
value: buffer,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -343,6 +355,8 @@ function parseSegment (segment: string) {
|
|||||||
buffer = ''
|
buffer = ''
|
||||||
if (c === '[') {
|
if (c === '[') {
|
||||||
state = SegmentParserState.dynamic
|
state = SegmentParserState.dynamic
|
||||||
|
} else if (c === '(') {
|
||||||
|
state = SegmentParserState.group
|
||||||
} else {
|
} else {
|
||||||
i--
|
i--
|
||||||
state = SegmentParserState.static
|
state = SegmentParserState.static
|
||||||
@ -353,6 +367,9 @@ function parseSegment (segment: string) {
|
|||||||
if (c === '[') {
|
if (c === '[') {
|
||||||
consumeBuffer()
|
consumeBuffer()
|
||||||
state = SegmentParserState.dynamic
|
state = SegmentParserState.dynamic
|
||||||
|
} else if (c === '(') {
|
||||||
|
consumeBuffer()
|
||||||
|
state = SegmentParserState.group
|
||||||
} else {
|
} else {
|
||||||
buffer += c
|
buffer += c
|
||||||
}
|
}
|
||||||
@ -361,6 +378,7 @@ function parseSegment (segment: string) {
|
|||||||
case SegmentParserState.catchall:
|
case SegmentParserState.catchall:
|
||||||
case SegmentParserState.dynamic:
|
case SegmentParserState.dynamic:
|
||||||
case SegmentParserState.optional:
|
case SegmentParserState.optional:
|
||||||
|
case SegmentParserState.group:
|
||||||
if (buffer === '...') {
|
if (buffer === '...') {
|
||||||
buffer = ''
|
buffer = ''
|
||||||
state = SegmentParserState.catchall
|
state = SegmentParserState.catchall
|
||||||
@ -375,10 +393,16 @@ function parseSegment (segment: string) {
|
|||||||
consumeBuffer()
|
consumeBuffer()
|
||||||
}
|
}
|
||||||
state = SegmentParserState.initial
|
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)) {
|
} else if (PARAM_CHAR_RE.test(c)) {
|
||||||
buffer += c
|
buffer += c
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// console.debug(`[pages]Ignored character "${c}" while building param "${buffer}" from "segment"`)
|
// console.debug(`[pages]Ignored character "${c}" while building param "${buffer}" from "segment"`)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
@ -339,6 +339,34 @@
|
|||||||
"redirect": "mockMeta?.redirect",
|
"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": [
|
"should handle trailing slashes with index routes": [
|
||||||
{
|
{
|
||||||
"alias": "mockMeta?.alias || []",
|
"alias": "mockMeta?.alias || []",
|
||||||
|
@ -234,6 +234,25 @@
|
|||||||
"path": ""/parent"",
|
"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": [
|
"should handle trailing slashes with index routes": [
|
||||||
{
|
{
|
||||||
"children": [
|
"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> = {}
|
const normalizedResults: Record<string, any> = {}
|
||||||
|
@ -570,6 +570,14 @@ describe('pages', () => {
|
|||||||
|
|
||||||
await normalInitialPage.close()
|
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', () => {
|
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