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 2 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
90 changes: 90 additions & 0 deletions packages/vitepress-plugin-breadcrumbs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# @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 { BreadcrumbsDataGenerator } from '@nolebase/vitepress-plugin-breadcrumbs'
import { defineConfig } from 'vitepress'

const breadcrumbsGenerator = new BreadcrumbsDataGenerator('<title of your website>', '<root dir of your documents>')

export default defineConfig({
// other config...
transformPageData(pageData, context) {
breadcrumbsGenerator.generate(pageData, context.siteConfig.pages)
// 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 { Breadcrumbs } 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(Breadcrumbs),
})
},
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.4.2"
}
}
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="breadcrumbs">
nekomeowww marked this conversation as resolved.
Show resolved Hide resolved
<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>
.breadcrumbs {
nekomeowww marked this conversation as resolved.
Show resolved Hide resolved
display: flex;
gap: 8px;
font-size: 14px;
margin-bottom: 2rem;
}

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

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

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

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

.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 Breadcrumbs from './components/Breadcrumbs.vue'

export { Breadcrumbs }
nekomeowww marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { PageData } from 'vitepress'
import { expect, it } from 'vitest'
import { BreadcrumbsDataGenerator } from '../vitepress'

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

const pages = ['a', 'a/b', 'a/b/index.md', 'a/b/c/d.md']

const generator = new BreadcrumbsDataGenerator('Home', 'a')

generator.generate(pageData, pages)

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: PageData = {
relativePath: 'a/b/c/d/index.md',
filePath: 'a/b/c/d/index.md',
title: 'd',
description: '',
headers: [],
frontmatter: {},
}

const pages = ['a', 'a/b', 'a/b/index.md', 'a/b/c/d/index.md']

const generator = new BreadcrumbsDataGenerator('Home', 'a')

generator.generate(pageData, pages)

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,53 @@
import type { PageData } from 'vitepress'

export class BreadcrumbsDataGenerator {
nekomeowww marked this conversation as resolved.
Show resolved Hide resolved
constructor(private readonly title: string, private readonly rootDirectory: string) { }
nekomeowww marked this conversation as resolved.
Show resolved Hide resolved

generate(pageData: PageData, pages: string[]) {
const splitPath = pageData.filePath.split('/')
const breadcrumbs: {
title: string
link: string
}[] = [{
title: this.title,
link: `/${this.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 { BreadcrumbsDataGenerator } 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.