Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support navigation field in content and _dir.yml #1328

Merged
merged 10 commits into from
Jul 5, 2022
76 changes: 60 additions & 16 deletions docs/content/3.guide/2.displaying/3.navigation.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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]
---
Expand Down Expand Up @@ -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 `<script>`
- [ContentNavigation](/api/components/content-navigation) component made to shorten access to navigation in `<template>`
- [`fetchContentNavigation()`](/api/composables/fetch-content-navigation) composable to fetch navigation in `<script>`
- [`<ContentNavigation>`](/api/components/content-navigation) component made to shorten access to navigation in `<template>`
::
19 changes: 2 additions & 17 deletions docs/content/4.api/1.components/3.content-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: '<ContentNavigation>'
description: 'Building complex navigation from your content has never been easier.'
---

The `<ContentNavigation>`{lang=html} is a renderless component shortening the access to navigation.
The `<ContentNavigation>`{lang=html} is a renderless component shortening the access to [the navigation](/guide/displaying/navigation).

## Props

Expand All @@ -13,7 +13,7 @@ The `<ContentNavigation>`{lang=html} is a renderless component shortening the ac

## Slots

The `default`{lang=ts} slot can be used to render the content via `v-slot="{ data }"`{lang=html} syntax.
The `default`{lang=ts} slot can be used to render the content with `v-slot="{ navigation }"`{lang=html} syntax.

```html [components/Navbar.vue]
<template>
Expand All @@ -27,21 +27,6 @@ The `default`{lang=ts} slot can be used to render the content via `v-slot="{ dat
</template>
```

The `empty`{lang=ts} slot can be used to display a default content before rendering the document.

```html [components/Navbar.vue]
<template>
<nav>
<ContentNavigation v-slot="{ navigation }">
<!-- ...default slot -->
<template #empty>
<p>No navigation found.</p>
</template>
</ContentNavigation>
</nav>
</template>
```

## Query

By providing the `query` prop you can customise the content used for navigation.
Expand Down
41 changes: 41 additions & 0 deletions docs/content/4.api/1.components/6.prose.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ description: 'A list of Prose components currently implemented.'

Here is a list of all the Prose components currently implemented.

To overwrite a prose component, create a component with the same name in your project `components/content/` directory (ex: `components/content/ProseA.vue`)

## `ProseA`

[:icon{name="fa-brands:github" class="inline -mt-1 w-6"} Source](https://github.com/nuxt/content/tree/main/src/runtime/components/Prose/ProseA.vue)
Expand Down Expand Up @@ -133,6 +135,45 @@ Here is a list of all the Prose components currently implemented.

::

## `ProseH4`

[:icon{name="fa-brands:github" class="inline -mt-1 w-6"} Source](https://github.com/nuxt/content/tree/main/src/runtime/components/Prose/ProseH4.vue)

::code-group
```markdown [Code]
## H4 Heading
```
::code-block{label="Preview"}
## H4 Heading
::
::

## `ProseH5`

[:icon{name="fa-brands:github" class="inline -mt-1 w-6"} Source](https://github.com/nuxt/content/tree/main/src/runtime/components/Prose/ProseH5.vue)

::code-group
```markdown [Code]
## H5 Heading
```
::code-block{label="Preview"}
## H5 Heading
::
::

## `ProseH6`

[:icon{name="fa-brands:github" class="inline -mt-1 w-6"} Source](https://github.com/nuxt/content/tree/main/src/runtime/components/Prose/ProseH6.vue)

::code-group
```markdown [Code]
## H6 Heading
```
::code-block{label="Preview"}
## H6 Heading
::
::

## `ProseHr`

[:icon{name="fa-brands:github" class="inline -mt-1 w-6"} Source](https://github.com/nuxt/content/tree/main/src/runtime/components/Prose/ProseHr.vue)
Expand Down
16 changes: 7 additions & 9 deletions docs/content/4.api/2.composables/2.fetch-content-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ const { data: navigation } = await useAsyncData('navigation', fetchContentNaviga
</script>

<template>
<pre>
{{ navigation }}
</pre>
<pre>{{ navigation }}</pre>
</template>
```

Expand All @@ -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"
}
]
}
Expand All @@ -55,12 +53,12 @@ const { data: navigation } = await useAsyncData('navigation', fetchContentNaviga

::

:ReadMore{link="/examples/navigation/fetch-content-navigation"}

## Arguments

- `queryBuilder`{lang=ts}
- Type: `QueryBuilder`{lang=ts}
- Definition: Any query built via `queryContent()`{lang=ts}
- Default: `/`{lang=ts}

::ReadMore{link="/examples/navigation/fetch-content-navigation"}
::
6 changes: 6 additions & 0 deletions playground/navigation/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<div>
<ContentNavigation class="navigation" />
<NuxtPage />
</div>
</template>
1 change: 1 addition & 0 deletions playground/navigation/content/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# About
5 changes: 5 additions & 0 deletions playground/navigation/content/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Home
---

# Hello World
2 changes: 2 additions & 0 deletions playground/navigation/content/resources/_dir.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
navigation:
hello: true
2 changes: 2 additions & 0 deletions playground/navigation/content/resources/case-studies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Case studies

5 changes: 5 additions & 0 deletions playground/navigation/content/resources/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
navigation: false
---

# Resources
9 changes: 9 additions & 0 deletions playground/navigation/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineNuxtConfig } from 'nuxt'
import contentModule from '../../src/module' // eslint-disable-line

export default defineNuxtConfig({
modules: [contentModule],
content: {
documentDriven: true
}
})
47 changes: 26 additions & 21 deletions src/runtime/components/ContentNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -33,15 +34,17 @@ export default defineComponent({
return query.value
})

const { data, refresh } = await useAsyncData<NavItem[]>(
// 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<NavItem[]>(
`content-navigation-${hash(queryBuilder.value)}`,
() => fetchContentNavigation(queryBuilder.value)
)

return {
data,
refresh
}
return { navigation }
},

/**
Expand All @@ -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 <ContentNavigation>', 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)
}
})
8 changes: 4 additions & 4 deletions src/runtime/composables/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,24 @@ export const useContentState = () => {
/**
* Current page complete data.
*/
const page = useState<ParsedContent>('content-document-driven-page')
const page = useState<ParsedContent>('dd-page')

/**
* Navigation tree from root of app.
*/
const navigation = useState<NavItem[]>('content-document-driven-navigation')
const navigation = useState<NavItem[]>('dd-navigation')

/**
* Previous and next page data.
* Format: [prev, next]
*/
const surround = useState<Omit<ParsedContent, 'body'>[]>('content-document-driven-page-surround')
const surround = useState<Omit<ParsedContent, 'body'>[]>('dd-surround')

/**
* Globally loaded content files.
* Format: { [key: string]: ParsedContent }
*/
const globals = useState<Record<string, ParsedContent>>('content-document-driven-globals', () => ({}))
const globals = useState<Record<string, ParsedContent>>('dd-globals', () => ({}))

return {
page,
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/markdown-parser/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
/**
Expand Down