Skip to content

Commit

Permalink
feat: support navigation field in content and _dir.yml (#1328)
Browse files Browse the repository at this point in the history
Co-authored-by: Ahad Birang <farnabaz@gmail.com>
  • Loading branch information
Atinux and farnabaz committed Jul 5, 2022
1 parent 0d3882d commit 4267bf2
Show file tree
Hide file tree
Showing 19 changed files with 196 additions and 70 deletions.
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

0 comments on commit 4267bf2

Please sign in to comment.