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(breadcrumbs): add vitepress breadcrumbs plugin #327

Merged
merged 9 commits into from
Nov 13, 2024
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
99 changes: 99 additions & 0 deletions packages/vitepress-plugin-breadcrumbs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# @nolebase/vitepress-plugin-breadcrumbs

A VitePress plugin that adds breadcrumbs to your documentation.

## Get started

Install:

```shell
nekomeowww marked this conversation as resolved.
Show resolved Hide resolved
pnpm install @nolebase/vitepress-plugin-breadcrumbs
# or use bun
bun install @nolebase/vitepress-plugin-breadcrumbs
```

Generate breadcrumbs data when build your pages in `.vitepress/config.ts`:

```typescript
import { generateBreadcrumbsData } from '@nolebase/vitepress-plugin-breadcrumbs'
import { defineConfig } from 'vitepress'

export default defineConfig({
// other config...
transformPageData(pageData, context) {
generateBreadcrumbsData(pageData, context)
// other transforming...
},
// other config...
})

```

Add default breadcrumb vue component to each page in `.vitepress/theme/index.ts`:

```typescript
import type { Theme as ThemeConfig } from 'vitepress'
import { NolebaseBreadcrumbs } from '@nolebase/vitepress-plugin-breadcrumbs/client'
import DefaultTheme from 'vitepress/theme'
import { h } from 'vue'

export const Theme: ThemeConfig = {
extends: DefaultTheme,
Layout: () => {
return h(DefaultTheme.Layout, null, {
// add breadcrumb above document
'doc-before': () => h(NolebaseBreadcrumbs),
})
},
enhanceApp({ app }) {
app.provide<Options>(InjectionKey, {
spotlight: {
defaultToggle: true
}
})
}
}

export default Theme
```

Add this plugin to `noExternal` and `exclude` properties when building:

```typescript
export default defineConfig({
vite: {
optimizeDeps: {
exclude: [
'@nolebase/vitepress-plugin-breadcrumbs/client'
]
},
ssr: {
noExternal: [
'@nolebase/vitepress-plugin-breadcrumbs'
]
}
},
// other config...
})
```

## Use custom breadcrumb component

If you don't like the style or other something of default breadcrumb component, you can create your own component, this plugin will inject breadcrumb data into frontmatter of the page, so you can use breadcrumb data like this:

```vue
<script setup lang="ts">
import { useData } from 'vitepress'

const { frontmatter } = useData()

console.log(frontmatter.breadcrumbs)
// and do something other...
</script>

<template>
<div>
<!-- ui of your own component -->
</div>
</template>
```
16 changes: 16 additions & 0 deletions packages/vitepress-plugin-breadcrumbs/build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
entries: [
{ builder: 'mkdist', input: './src/client', outDir: './dist/client', pattern: ['**/*.vue'], loaders: ['vue'] },
{ builder: 'mkdist', input: './src/client', outDir: './dist/client', pattern: ['**/*.ts'], format: 'cjs', loaders: ['js'] },
{ builder: 'mkdist', input: './src/client', outDir: './dist/client', pattern: ['**/*.ts'], format: 'esm', loaders: ['js'] },
{ builder: 'rollup', input: './src/vitepress/index', outDir: 'dist/vitepress/' },
],
rollup: {
emitCJS: true,
},
clean: true,
sourcemap: true,
declaration: true,
})
58 changes: 58 additions & 0 deletions packages/vitepress-plugin-breadcrumbs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "@nolebase/vitepress-plugin-breadcrumbs",
"type": "module",
"version": "2.8.1",
"description": "A VitePress plugin that adds breadcrumbs to your documentation.",
"author": {
"name": "Nólëbase",
"email": "[email protected]",
"url": "https://github.com/nolebase"
},
"license": "MIT",
"homepage": "https://nolebase-integrations.ayaka.io/pages/en/integrations/vitepress-plugin-breadcrumbs/",
"repository": {
"type": "git",
"url": "https://github.com/nolebase/integrations.git",
"directory": "packages/vitepress-plugin-breadcrumbs"
},
"keywords": [
"vitepress",
"nolebase",
"vitepress-plugin",
"nolebase-integration"
],
"exports": {
".": {
"types": "./dist/vitepress/index.d.ts",
"import": "./dist/vitepress/index.mjs",
"require": "./dist/vitepress/index.cjs"
},
"./vitepress": {
"types": "./dist/vitepress/index.d.ts",
"import": "./dist/vitepress/index.mjs",
"require": "./dist/vitepress/index.cjs"
},
"./client": {
"types": "./dist/client/index.d.ts",
"import": "./dist/client/index.mjs",
"require": "./dist/client/index.js"
}
},
"main": "./dist/vitepress/index.cjs",
"module": "./dist/vitepress/index.mjs",
"types": "./dist/vitepress/index.d.ts",
"files": [
"README.md",
"dist",
"package.json"
],
"scripts": {
"dev": "unbuild --stub",
"stub": "unbuild --stub",
"build": "unbuild",
"package:publish": "pnpm build && pnpm publish --access public --no-git-checks"
},
"dependencies": {
"vitepress": "^1.5.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script setup lang="ts">
import { useData } from 'vitepress'

const { frontmatter } = useData()
</script>

<template>
<div class="vp-nolebase-breadcrumbs">
<span v-for="item in frontmatter.breadcrumbs" :key="item">
<a v-if="item.link" :href="item.link">{{ item.title }}</a>
<span v-else>{{ item.title }}</span>
</span>
</div>
</template>

<style scoped>
.vp-nolebase-breadcrumbs {
display: flex;
gap: 8px;
font-size: 14px;
margin-bottom: 2rem;
}

.vp-nolebase-breadcrumbs span {
transition: color 0.25s, opacity 0.25s;
color: var(--vp-c-text-2);
}

.vp-nolebase-breadcrumbs span:hover {
color: var(--vp-c-brand-1);
}

.vp-nolebase-breadcrumbs span:last-child {
color: var(--vp-c-text-1);
}

.vp-nolebase-breadcrumbs span:last-child:hover {
color: var(--vp-c-brand-1);
}

.vp-nolebase-breadcrumbs span:not(:first-child)::before {
content: '/';
padding-right: 8px;
color: var(--vp-c-text-3);
}
</style>
3 changes: 3 additions & 0 deletions packages/vitepress-plugin-breadcrumbs/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import NolebaseBreadcrumbs from './components/NolebaseBreadcrumbs.vue'

export { NolebaseBreadcrumbs }
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { PageData, TransformPageContext } from 'vitepress'
import { expect, it } from 'vitest'
import { generateBreadcrumbsData } from '../vitepress'

it('page is not index', () => {
const pageData = {
relativePath: 'a/b/c/d.md',
filePath: 'a/b/c/d.md',
title: 'd',
frontmatter: {},
} as PageData

const context = {
siteConfig: {
site: {
title: 'Home',
},
pages: ['a', 'a/b', 'a/b/index.md', 'a/b/c/d.md'],
},
} as TransformPageContext

generateBreadcrumbsData(pageData, context)

expect(pageData.frontmatter.breadcrumbs).toEqual([
{ title: 'Home', link: '/a' },
{ title: 'b', link: '/a/b' },
{ title: 'c', link: '' },
{ title: 'd', link: '/a/b/c/d' },
])
})

it('page is index', () => {
const pageData = {
relativePath: 'a/b/c/d/index.md',
filePath: 'a/b/c/d/index.md',
title: 'd',
frontmatter: {},
} as PageData

const context = {
siteConfig: {
site: {
title: 'Home',
},
pages: ['a', 'a/b', 'a/b/index.md', 'a/b/c/d/index.md'],
},
} as TransformPageContext

generateBreadcrumbsData(pageData, context)

expect(pageData.frontmatter.breadcrumbs).toEqual([
{ title: 'Home', link: '/a' },
{ title: 'b', link: '/a/b' },
{ title: 'c', link: '' },
{ title: 'd', link: '/a/b/c/d' },
])
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { PageData, TransformPageContext } from 'vitepress'

export function generateBreadcrumbsData(pageData: PageData, context: TransformPageContext) {
const splitPath = pageData.filePath.split('/')
const rootDirectory = splitPath[0]
const pages = context.siteConfig.pages
const breadcrumbs: {
title: string
link: string
}[] = [{
title: context.siteConfig.site.title,
link: `/${rootDirectory}`,
}]

for (let i = 1; i < splitPath.length; i++) {
let link = ''
let encodedLink = ''
let title = splitPath[i]
if (i === splitPath.length - 1) {
title = pageData.title
}

for (let j = 0; j <= i; j++) {
link += `${splitPath[j]}`
encodedLink += `${encodeURIComponent(splitPath[j])}`

if (j !== i) {
link += `/`
encodedLink += `/`
}
}

if (!pages.includes(link) && !pages.includes(`${link}/index.md`)) {
breadcrumbs.push({ title: splitPath[i], link: '' })
continue
}

if (link.endsWith('index.md')) {
continue
}

if (link.endsWith('.md')) {
encodedLink = encodedLink.slice(0, -3)
}

breadcrumbs.push({ title, link: `/${encodedLink}` })
}

if (splitPath.length)
pageData.frontmatter.breadcrumbs = breadcrumbs
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { generateBreadcrumbsData } from './breadcrumbs-data-generator'
35 changes: 35 additions & 0 deletions packages/vitepress-plugin-breadcrumbs/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": false,
"baseUrl": ".",
"strict": true,
"strictNullChecks": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"declaration": true,
"noEmit": false,
"outDir": "./dist",
"removeComments": false,
"sourceMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipDefaultLibCheck": true,
"skipLibCheck": true
},
"include": [
"src/**/*.ts",
"src/**/*.mts",
"src/**/*.d.ts",
"src/**/*.vue",
"src/**/*.tsx"
],
"exclude": [
"**/dist/**",
"node_modules"
]
}
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.