diff --git a/docs/content/3.guide/2.displaying/3.navigation.md b/docs/content/3.guide/2.displaying/3.navigation.md index 5b8b38553..ad4926512 100644 --- a/docs/content/3.guide/2.displaying/3.navigation.md +++ b/docs/content/3.guide/2.displaying/3.navigation.md @@ -1,11 +1,9 @@ --- title: Navigation -description: 'The fetchContentNavigation utility returns a tree of items based on the content/ directory structure and files.' +description: 'Nuxt Content provides a component and composable to display a navigation based on the content/ directory structure and files.' --- -Navigation generation is based on your content sources directory structure. - -Based on the generated `_id` and `_path` keys, we generate a whole navigation structure for your content. +Based on the generated `_id` and `_path` keys, Nuxt Content generates a whole navigation structure for your content. It allows you to create advanced navigation components without having to maintain any querying logic related to it. @@ -59,13 +57,64 @@ content/ :: -## Configuration +## Custom keys + +You can use the `navigation` property in the front-matter of your content files to add keys to the navigation object. + +::code-group +```md [index.md] +--- +navigation: + title: 'Home' + icon: '🏡' +--- + +# Welcome +``` + +```json [Navigation] +[ + { + "title": "Home", + "icon": "🏡", + "_path": "/", + } +] +``` +:: + + +Alternatively, the navigation also allows you to configure directory nodes via `_dir.yml` files. + +It allows you to overwrite the `title` and custom properties to directory nodes in navigation. + +::code-group + +``` [Directory structure] +content/ + my-directory/ + _dir.yml + page.md +``` -### Module +```yaml [_dir.yml] +title: 'My awesome directory' +navigation.icon: '📁' +``` -The only key available in [module configuration](/api/configuration#navigation) about navigation is `fields`. +```json [Navigation] +{ + "title": "My awesome directory", + "icon": "📁", + "_path": "/my-directory", + "children": [ + ... + ] +} +``` +:: -`fields` allows you to define the properties that will be included in the navigation items. +If you want to use top-level keys in the front-matter to be included in the navigation object, use the [`content.navigation.fields`](/api/configuration#navigation) property in the `nuxt.config`: ::code-group @@ -99,15 +148,10 @@ publishedAt: '15-06-2022' :: -### Per-directory - -Alternatively, the navigation also allows you to configure directory nodes via `_dir.yml` files. - -It allows you to overwrite directory names, and add custom properties to directory nodes in navigation. ## Excluding -Set `navigation: false` in the [front-matter](/guide/writing/markdown) of a page to filter it out of navigation. +Set `navigation: false` in the [front-matter](/guide/writing/markdown) of a page to filter it out. ```md [page.md] --- @@ -174,6 +218,6 @@ const query = queryContent({ ::alert Go deeper in the **API** section: -- [fetchContentNavigation()](/api/composables/fetch-content-navigation) composable to fetch navigation in ` ``` @@ -40,13 +38,13 @@ const { data: navigation } = await useAsyncData('navigation', fetchContentNaviga ] }, { - "title": "Sub Directory", - "_path": "/sub-directory", + "title": "Sub Folder", + "_path": "/sub-folder", "children": [ { "title": "About Content V2", - "_id": "content:sub-directory:about.md", - "_path": "/sub-directory/about" + "_id": "content:sub-folder:about.md", + "_path": "/sub-folder/about" } ] } @@ -55,6 +53,8 @@ const { data: navigation } = await useAsyncData('navigation', fetchContentNaviga :: +:ReadMore{link="/examples/navigation/fetch-content-navigation"} + ## Arguments - `queryBuilder`{lang=ts} @@ -62,5 +62,3 @@ const { data: navigation } = await useAsyncData('navigation', fetchContentNaviga - Definition: Any query built via `queryContent()`{lang=ts} - Default: `/`{lang=ts} -::ReadMore{link="/examples/navigation/fetch-content-navigation"} -:: diff --git a/playground/navigation/app.vue b/playground/navigation/app.vue new file mode 100644 index 000000000..31b7fbe95 --- /dev/null +++ b/playground/navigation/app.vue @@ -0,0 +1,6 @@ + diff --git a/playground/navigation/content/about.md b/playground/navigation/content/about.md new file mode 100644 index 000000000..449429d43 --- /dev/null +++ b/playground/navigation/content/about.md @@ -0,0 +1 @@ +# About diff --git a/playground/navigation/content/index.md b/playground/navigation/content/index.md new file mode 100644 index 000000000..1a7557e04 --- /dev/null +++ b/playground/navigation/content/index.md @@ -0,0 +1,5 @@ +--- +title: Home +--- + +# Hello World diff --git a/playground/navigation/content/resources/_dir.yml b/playground/navigation/content/resources/_dir.yml new file mode 100644 index 000000000..9e65ca286 --- /dev/null +++ b/playground/navigation/content/resources/_dir.yml @@ -0,0 +1,2 @@ +navigation: + hello: true diff --git a/playground/navigation/content/resources/case-studies.md b/playground/navigation/content/resources/case-studies.md new file mode 100644 index 000000000..6deb35c71 --- /dev/null +++ b/playground/navigation/content/resources/case-studies.md @@ -0,0 +1,2 @@ +# Case studies + diff --git a/playground/navigation/content/resources/index.md b/playground/navigation/content/resources/index.md new file mode 100644 index 000000000..f1804eaee --- /dev/null +++ b/playground/navigation/content/resources/index.md @@ -0,0 +1,5 @@ +--- +navigation: false +--- + +# Resources diff --git a/playground/navigation/nuxt.config.ts b/playground/navigation/nuxt.config.ts new file mode 100644 index 000000000..06c5155d3 --- /dev/null +++ b/playground/navigation/nuxt.config.ts @@ -0,0 +1,9 @@ +import { defineNuxtConfig } from 'nuxt' +import contentModule from '../../src/module' // eslint-disable-line + +export default defineNuxtConfig({ + modules: [contentModule], + content: { + documentDriven: true + } +}) diff --git a/src/runtime/components/ContentNavigation.ts b/src/runtime/components/ContentNavigation.ts index b115dc3f0..b08c9bdf4 100644 --- a/src/runtime/components/ContentNavigation.ts +++ b/src/runtime/components/ContentNavigation.ts @@ -2,7 +2,8 @@ import { PropType, toRefs, defineComponent, h, useSlots, computed } from 'vue' import { hash } from 'ohash' import type { NavItem, QueryBuilderParams } from '../types' import { QueryBuilder } from '../types' -import { useAsyncData, fetchContentNavigation } from '#imports' +import { useAsyncData, fetchContentNavigation, useState, useContent } from '#imports' +import { NuxtLink } from '#components' export default defineComponent({ name: 'ContentNavigation', @@ -33,15 +34,17 @@ export default defineComponent({ return query.value }) - const { data, refresh } = await useAsyncData( + // If doc driven mode and no query given, re-use the fetched navigation + if (!queryBuilder.value && useState('dd-navigation').value) { + const { navigation } = useContent() + + return { navigation } + } + const { data: navigation } = await useAsyncData( `content-navigation-${hash(queryBuilder.value)}`, () => fetchContentNavigation(queryBuilder.value) ) - - return { - data, - refresh - } + return { navigation } }, /** @@ -51,22 +54,24 @@ export default defineComponent({ render (ctx) { const slots = useSlots() - const { - query, - data, - refresh - } = ctx - - const emptyNode = (slot: string, data: any) => h('pre', null, JSON.stringify({ message: 'You should use slots with ', slot, data }, null, 2)) - - // Render empty data object - if (slots?.empty && (!data || !data?.length)) { - return slots?.empty?.({ query, ...this.$attrs }) || emptyNode('empty', { query, data }) - } + const { navigation } = ctx + const renderLink = (link: NavItem) => h(NuxtLink, { to: link._path }, () => link.title) + const renderLinks = (data: NavItem[], level: number) => + h( + 'ul', + level ? { 'data-level': level } : null, + data.map((link) => { + if (link.children) { + return h('li', null, [renderLink(link), renderLinks(link.children, level + 1)]) + } + return h('li', null, renderLink(link)) + }) + ) + const defaultNode = (data: NavItem[]) => renderLinks(data, 0) // Render default slot with navigation as `data` return slots?.default - ? slots.default({ navigation: data, refresh, ...this.$attrs }) - : emptyNode('default', data) + ? slots.default({ navigation, ...this.$attrs }) + : defaultNode(navigation) } }) diff --git a/src/runtime/composables/content.ts b/src/runtime/composables/content.ts index c8ac97f30..f28d4a2c5 100644 --- a/src/runtime/composables/content.ts +++ b/src/runtime/composables/content.ts @@ -5,24 +5,24 @@ export const useContentState = () => { /** * Current page complete data. */ - const page = useState('content-document-driven-page') + const page = useState('dd-page') /** * Navigation tree from root of app. */ - const navigation = useState('content-document-driven-navigation') + const navigation = useState('dd-navigation') /** * Previous and next page data. * Format: [prev, next] */ - const surround = useState[]>('content-document-driven-page-surround') + const surround = useState[]>('dd-surround') /** * Globally loaded content files. * Format: { [key: string]: ParsedContent } */ - const globals = useState>('content-document-driven-globals', () => ({})) + const globals = useState>('dd-globals', () => ({})) return { page, diff --git a/src/runtime/markdown-parser/content.ts b/src/runtime/markdown-parser/content.ts index c8b6580cf..28549a2a9 100644 --- a/src/runtime/markdown-parser/content.ts +++ b/src/runtime/markdown-parser/content.ts @@ -72,8 +72,8 @@ export function contentHeading (body: MarkdownRoot) { let title = '' let description = '' const children = body.children - // top level `text` can be ignored - .filter(node => node.type !== 'text') + // top level `text` and `hr` can be ignored + .filter(node => node.type !== 'text' && node.tag !== 'hr') if (children.length && children[0].tag === 'h1') { /** diff --git a/src/runtime/server/navigation.ts b/src/runtime/server/navigation.ts index 52dcfd544..31c897428 100644 --- a/src/runtime/server/navigation.ts +++ b/src/runtime/server/navigation.ts @@ -12,7 +12,10 @@ export function createNav (contents: ParsedContentMeta[], configs: Record ({ + ...pick(['title', ...navigation.fields])(content), + ...(isObject(content?.navigation) ? content.navigation : {}) + }) // Create navigation object const nav = contents @@ -142,3 +145,7 @@ function pick (keys?: string[]) { return obj } } + +function isObject (obj: any) { + return Object.prototype.toString.call(obj) === '[object Object]' +} diff --git a/test/features/navigation.ts b/test/features/navigation.ts index 3df3f6749..33bd4a4c3 100644 --- a/test/features/navigation.ts +++ b/test/features/navigation.ts @@ -12,8 +12,16 @@ export const testNavigation = () => { _params: jsonStringify(query) } }) + console.log(JSON.stringify(list, null, 2)) + expect(list.find(item => item._path === '/')).toBeTruthy() expect(list.find(item => item._path === '/').children).toBeUndefined() + + // Check navigation data in front-matter + const testNavigation = list.find(item => item._path === '/test-navigation') + expect(testNavigation['custom-field']).toEqual('_dir file') + expect(testNavigation.children[0]['custom-field']).toEqual('index file') + expect(testNavigation.children[1]['custom-field']).toEqual('page file') }) test('Get cats navigation', async () => { diff --git a/test/fixtures/basic/content/test-navigation/1.index.md b/test/fixtures/basic/content/test-navigation/1.index.md index e2f4b3bf0..a01752011 100644 --- a/test/fixtures/basic/content/test-navigation/1.index.md +++ b/test/fixtures/basic/content/test-navigation/1.index.md @@ -1 +1,4 @@ +--- +navigation.custom-field: index file +--- # /test-navigation diff --git a/test/fixtures/basic/content/test-navigation/2.page.md b/test/fixtures/basic/content/test-navigation/2.page.md index fe3f2ce6d..aee5d0255 100644 --- a/test/fixtures/basic/content/test-navigation/2.page.md +++ b/test/fixtures/basic/content/test-navigation/2.page.md @@ -1 +1,4 @@ +--- +navigation.custom-field: page file +--- # /test-navigation/page diff --git a/test/fixtures/basic/content/test-navigation/_dir.yml b/test/fixtures/basic/content/test-navigation/_dir.yml index a6a94f243..b4ff2a77f 100644 --- a/test/fixtures/basic/content/test-navigation/_dir.yml +++ b/test/fixtures/basic/content/test-navigation/_dir.yml @@ -1 +1,3 @@ title: Test Navigation +navigation: + custom-field: _dir file