Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions app/app.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<script setup lang="ts">
import type { NavigationItem } from 'comark-content'
import type { SearchSection } from './utils/search-sections'
import { useRoute } from 'vue-router'

const { seo, docs } = useAppConfig()

const content = useDocsContent()
const route = useRoute()

const { data: navigation } = await useAsyncData('navigation', () => content.value.client.navigation(), {
watch: [() => content.value.base],
Expand All @@ -21,7 +23,14 @@ const {

onNuxtReady(() => loadSearchSections())

const nuxtApp = useNuxtApp()
const navTree = computed<NavigationItem[]>(() => prefixNavigation(navigation.value ?? [], content.value.base))
const navigationLayout = ref(findNavigationLayout(navTree.value, route.path))
onNuxtReady(() => {
nuxtApp.hook('page:finish', () => {
navigationLayout.value = findNavigationLayout(navTree.value, route.path)
})
})
const searchFiles = computed<SearchSection[]>(() =>
(files.value ?? []).map((section) => {
const [path, hash] = section.id.split('#')
Expand Down Expand Up @@ -52,6 +61,7 @@ useSeoMeta({
})

provide('navigation', navTree)
provide('layout', navigationLayout)

// const colorMode = useColorMode()
const historyOpen = useVersionHistory()
Expand Down Expand Up @@ -82,9 +92,17 @@ defineShortcuts({
<AppHeader />

<UMain>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<Suspense>
<LayoutsPage v-if="navigationLayout === 'page'">
<NuxtPage />
</LayoutsPage>
<LayoutsDocs v-else-if="navigationLayout === 'docs'">
<NuxtPage />
</LayoutsDocs>
<UContainer v-else>
<NuxtPage />
</UContainer>
</Suspense>
</UMain>

<AppFooter />
Expand Down
5 changes: 4 additions & 1 deletion app/components/docs/DocsAsideMobileBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const menuDrawerOpen = ref(false)
const tocDrawerOpen = ref(false)
const navigationRef = ref<HTMLElement | null>(null)
let observer: IntersectionObserver | undefined
const layout = inject('layout')

watch(menuDrawerOpen, (open) => {
nextTick(() => {
Expand All @@ -28,9 +29,11 @@ watch(menuDrawerOpen, (open) => {

<template>
<div
class="lg:hidden sticky top-(--ui-header-height) z-10 bg-default -mx-6 p-2 px-6 border-b border-muted flex justify-between"
class="lg:hidden sticky top-(--ui-header-height) z-10 bg-default -mx-6 p-2 px-6 border-b border-muted flex h-13"
:class="layout === 'page' ? 'justify-end' : 'justify-between'"
>
<UDrawer
v-if="layout === 'docs'"
v-model:open="menuDrawerOpen"
direction="left"
:title="title"
Expand Down
File renamed without changes.
7 changes: 7 additions & 0 deletions app/components/layouts/LayoutsPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<UContainer class="max-w-7xl">
<UTheme :ui="{ pageHeader: { container: 'lg:pt-8' }, contentToc: { container: 'lg:pt-16' } }">
<slot />
</UTheme>
</UContainer>
</template>
3 changes: 2 additions & 1 deletion app/pages/[...slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
import Browser from '../components/Browser.vue'

definePageMeta({
layout: 'docs',
path: '/:slug(.+)',
})

const { toc, seo } = useAppConfig()
const navigation = inject<Ref<NavigationItem[]>>('navigation')
const content = useDocsContent()
const layout = inject<Ref<NavigationLayout>>('layout')

const { data: page } = await useAsyncData(`${content.value.base}:${content.value.path}`, () =>
content.value.client.get(content.value.path)
Expand All @@ -36,8 +36,8 @@

const surroundLinks = computed(() => findSurroundLinks(navigation?.value, selfPath.value))

const fm = computed<Record<string, any>>(() => page.value?.data ?? {})

Check warning on line 39 in app/pages/[...slug].vue

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
const tocLinks = computed<any[]>(() => (page.value?.meta as any)?.toc?.links ?? [])

Check warning on line 40 in app/pages/[...slug].vue

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type

Check warning on line 40 in app/pages/[...slug].vue

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type

const title = computed(() => fm.value.seo?.title || fm.value.title)
const description = computed(() => fm.value.seo?.description || fm.value.description)
Expand Down Expand Up @@ -126,6 +126,7 @@
/>

<UContentSurround
v-if="layout === 'docs'"
:surround="surroundLinks as any"
:ui="{ root: !surroundLinks[0] ? 'sm:grid-cols-1' : 'sm:grid-cols-2' }"
/>
Expand Down
29 changes: 29 additions & 0 deletions app/utils/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,35 @@ export interface BreadcrumbItem {
path?: string
}

export type NavigationLayout = 'docs' | 'page'

/** Layout declared by the nearest matching page or directory navigation node. */
export function findNavigationLayout(
navigation: NavigationItem[] | undefined | null,
path: string | undefined
): NavigationLayout | undefined {
if (!navigation?.length || !path || path === '/') return undefined

let layout: NavigationLayout | undefined
const visit = (items: NavigationItem[]) => {
for (const item of items) {
const isPage = item.path === path
const isDirectory = Boolean(
item.children?.length
&& item.path !== '/'
&& path.startsWith(`${item.path}/`)
)
if (!isPage && !isDirectory) continue

if (item.layout === 'docs' || item.layout === 'page') layout = item.layout
if (item.children?.length) visit(item.children)
}
}

visit(navigation)
return layout || 'docs'
}

/** Trail of navigation items leading to `path`, including the page itself. */
export function findBreadcrumb(
navigation: NavigationItem[] | undefined | null,
Expand Down
1 change: 1 addition & 0 deletions playground/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default defineAppConfig({
ecosystem: [{ mark: 'comark-content', to: 'https://content.comark.dev', label: 'Comark Content' }],
nav: [
{ label: 'Documentation', sections: ['getting-started', 'writing', 'concepts', 'deployment'] },
{ label: 'Page', to: '/page' }
],
},
footer: {
Expand Down
11 changes: 11 additions & 0 deletions playground/content/2.writing/5.navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ title: Getting Started

Page titles come from frontmatter — `navigation.title` when set, `title` otherwise. Pages with `navigation: false` are excluded from the tree entirely (and therefore from prev/next links, search, and `llms.txt`).

## Section layouts

Pages use the `docs` layout by default, which includes the navigation sidebar. Set `layout: page` in a directory's `.navigation.yml` to render every page in that directory without the sidebar:

```yaml [content/3.examples/.navigation.yml]
title: Examples
layout: page
```

The layout applies to nested directories too. A nested `.navigation.yml` can set `layout: docs` to restore the sidebar. The `page` layout keeps the header, footer and page table of contents.

## Header tabs

The header renders one tab per group in `header.nav` from `app.config.ts`. Each group maps top-level content sections to a tab:
Expand Down
23 changes: 23 additions & 0 deletions playground/content/page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
title: Page
description: This is an example using the page layout.
navigation:
layout: page
---

In order to use the `page` layout without the left aside.

## Usage

You only need to set:

```yaml
---
navigation:
layout: page
---
```

## TOC

The Table of Contents stays if you have headings.
1 change: 1 addition & 0 deletions playground/content/pages/.navigation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
layout: page
21 changes: 21 additions & 0 deletions playground/content/pages/test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
title: Test
description: This is an example using the page layout.
---

In order to use the `page` layout without the left aside.

## Usage

You only need to set:

```yaml
---
navigation:
layout: page
---
```

## TOC

The Table of Contents stays if you have headings.
63 changes: 62 additions & 1 deletion test/navigation.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { findBreadcrumb, findPageHeadline, findSurroundLinks } from '../app/utils/navigation'
import { findBreadcrumb, findNavigationLayout, findPageHeadline, findSurroundLinks } from '../app/utils/navigation'
import type { NavigationItem } from 'comark-content'

const nav = [
Expand Down Expand Up @@ -76,6 +76,67 @@ describe('findBreadcrumb', () => {
})
})

describe('findNavigationLayout', () => {
const navigation = [
{
title: 'Examples',
path: '/examples',
page: false,
layout: 'page',
children: [
{ title: 'Overview', path: '/examples/overview' },
{
title: 'API',
path: '/examples/api',
page: false,
layout: 'docs',
children: [{ title: 'Reference', path: '/examples/api/reference' }],
},
],
},
{ title: 'Examples extended', path: '/examples-extended', layout: 'docs' },
] as unknown as NavigationItem[]

it('inherits a directory layout for its pages, including hidden pages', () => {
expect(findNavigationLayout(navigation, '/examples/overview')).toBe('page')
expect(findNavigationLayout(navigation, '/examples/hidden')).toBe('page')
})

it('lets a nested directory override an inherited layout', () => {
expect(findNavigationLayout(navigation, '/examples/api/reference')).toBe('docs')
})

it('does not inherit a layout from a partial path segment match', () => {
expect(findNavigationLayout(navigation, '/examples-extended/page')).toBe('docs')
})

it('supports navigation paths prefixed for version previews', () => {
const previewNavigation = [{
title: 'Examples',
path: '/tree/feature/examples',
page: false,
layout: 'page',
children: [{ title: 'Overview', path: '/tree/feature/examples/overview' }],
}] as unknown as NavigationItem[]

expect(findNavigationLayout(previewNavigation, '/tree/feature/examples/overview')).toBe('page')
})

it('falls back to docs without a matching supported layout', () => {
expect(findNavigationLayout(navigation, '/unknown')).toBe('docs')
expect(findNavigationLayout(
[{ title: 'Custom', path: '/custom', layout: 'custom' }] as unknown as NavigationItem[],
'/custom'
)).toBe('docs')
})

it('returns undefined for the landing page or without enough navigation context', () => {
expect(findNavigationLayout(navigation, '/')).toBeUndefined()
expect(findNavigationLayout([], '/examples')).toBeUndefined()
expect(findNavigationLayout(null, undefined)).toBeUndefined()
})
})

describe('findSurroundLinks', () => {
it('returns the flattened previous and next pages', () => {
expect(findSurroundLinks(nav, '/getting-started/installation')).toEqual([
Expand Down
Loading