├── .eslintrc.cjs ├── .github └── workflows │ ├── deploy.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── docs ├── .vitepress │ ├── config.ts │ └── theme │ │ └── index.ts ├── [page].md ├── [page].paths.ts ├── category.md ├── posts │ ├── config.md │ ├── frontmatter.md │ ├── pagination.md │ ├── pwa.md │ ├── quickstart.md │ ├── rss.md │ ├── slots.md │ └── tag.md ├── public │ ├── favicon.ico │ └── vitepress-theme-ououe.jpg ├── tag.md └── zh │ ├── [page].md │ ├── [page].paths.ts │ ├── category.md │ ├── posts │ ├── config.md │ ├── frontmatter.md │ ├── pagination.md │ ├── pwa.md │ ├── quickstart.md │ ├── rss.md │ ├── slots.md │ └── tag.md │ └── tag.md ├── package.json ├── pnpm-lock.yaml ├── src ├── components │ ├── VPAppearance.vue │ ├── VPArticleList.vue │ ├── VPCover.vue │ ├── VPFooter.vue │ ├── VPHeader.vue │ ├── VPNavTags.vue │ ├── VPPagination.vue │ ├── VPReadingProgress.vue │ └── VPTagList.vue ├── composables │ ├── index.ts │ ├── langs.ts │ ├── pagination.ts │ ├── prevNext.ts │ └── tag.ts ├── env.d.ts ├── index.ts ├── layouts │ ├── Article.vue │ ├── NotFound.vue │ ├── Page.vue │ ├── Tag.vue │ └── index.vue ├── posts.data.ts ├── styles │ ├── index.css │ ├── public.css │ └── vars.css ├── types │ ├── index.ts │ ├── slots.ts │ └── theme.ts └── utils │ ├── Classifiable.ts │ └── index.ts └── tsconfig.json /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | node: true, 5 | }, 6 | parser: 'vue-eslint-parser', 7 | parserOptions: { 8 | parser: '@typescript-eslint/parser', 9 | sourceType: 'module', 10 | ecmaVersion: 10, 11 | }, 12 | plugins: ['@typescript-eslint', 'prettier'], 13 | extends: [ 14 | 'eslint:recommended', 15 | 'plugin:vue/vue3-recommended', 16 | 'plugin:@typescript-eslint/recommended', 17 | 'plugin:vuejs-accessibility/recommended', 18 | ], 19 | rules: { 20 | 'no-console': 'error', 21 | 'no-debugger': 'error', 22 | quotes: ['error', 'single'], 23 | semi: ['error', 'never'], 24 | 'vue/multi-word-component-names': 'off', 25 | 'vue/no-v-html': 'off', 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - 'v**' 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Install Node.js 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: latest 23 | 24 | - name: Install pnpm 25 | uses: pnpm/action-setup@v4 26 | with: 27 | version: latest 28 | 29 | - name: Install dependencies 30 | run: pnpm i --frozen-lockfile 31 | 32 | - name: Build 33 | run: pnpm run build:docs 34 | 35 | - name: Deploy 36 | uses: JamesIves/github-pages-deploy-action@v4 37 | with: 38 | folder: dist 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v**' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Install Node.js 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: latest 22 | registry-url: https://registry.npmjs.org/ 23 | 24 | - name: Install pnpm 25 | uses: pnpm/action-setup@v4 26 | with: 27 | version: latest 28 | 29 | - name: Install dependencies 30 | run: pnpm i --frozen-lockfile 31 | 32 | - name: Publish 33 | run: pnpm publish --access public --no-git-checks 34 | env: 35 | NODE_AUTH_TOKEN: ${{secrets.NPM_PUBLISH_TOKEN}} 36 | 37 | - name: Create GitHub Release 38 | run: npx changelogithub 39 | env: 40 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Test 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, windows-latest] 12 | node-version: [18, latest] 13 | fail-fast: false 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Install pnpm 22 | uses: pnpm/action-setup@v4 23 | with: 24 | version: latest 25 | 26 | - name: Install Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | cache: pnpm 31 | 32 | - name: Install dependencies 33 | run: pnpm i --frozen-lockfile 34 | 35 | - name: Lint 36 | run: pnpm lint:test 37 | 38 | - name: Build 39 | run: pnpm build 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .editorconfig 3 | 4 | node_modules/ 5 | dist/ 6 | cache/ 7 | 8 | npm-debug.log 9 | yarn-error.log 10 | yarn.lock 11 | package-lock.json 12 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false 4 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Chrome", 6 | "request": "launch", 7 | "type": "chrome", 8 | "url": "http://localhost:5173", 9 | "webRoot": "${workspaceFolder}" 10 | }, 11 | { 12 | "name": "Launch Edge", 13 | "request": "launch", 14 | "type": "msedge", 15 | "url": "http://localhost:5173", 16 | "webRoot": "${workspaceFolder}" 17 | }, 18 | { 19 | "name": "Run and debug", 20 | "type": "node", 21 | "request": "launch", 22 | "cwd": "${workspaceFolder}", 23 | "runtimeExecutable": "pnpm", 24 | "runtimeArgs": ["run", "dev"] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2023 tolking 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vitepress-theme-ououe 2 | 3 | > A blog theme based on vitepress 4 | 5 | [Documentation and Demo](https://tolking.github.io/vitepress-theme-ououe) 6 | 7 | ![image](./docs/public/vitepress-theme-ououe.jpg) 8 | 9 | ## Install 10 | 11 | ```bash 12 | npm i vitepress-theme-ououe 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```ts 18 | // .vitepress/theme/index.ts 19 | import Theme from 'vitepress-theme-ououe' 20 | 21 | export default Theme 22 | ``` 23 | 24 | ```ts 25 | // .vitepress/config.ts 26 | import { defineConfigWithTheme } from 'vitepress' 27 | import type { Theme } from 'vitepress-theme-ououe' 28 | 29 | export default defineConfigWithTheme({ 30 | // ... 31 | themeConfig: { 32 | // config 33 | }, 34 | }) 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfigWithTheme } from 'vitepress' 3 | import type { Theme } from '../../src/types' 4 | 5 | export default defineConfigWithTheme({ 6 | title: 'vitepress-theme-ououe', 7 | outDir: '../dist', 8 | cleanUrls: true, 9 | lastUpdated: true, 10 | head: [ 11 | ['link', { rel: 'icon', href: '/favicon.ico' }], 12 | ['meta', { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge,chrome=1' }], 13 | ['meta', { name: 'renderer', content: 'webkit' }], 14 | ['meta', { name: 'force-rendering', content: 'webkit' }], 15 | ['meta', { name: 'applicable-device', content: 'pc,mobile' }], 16 | ['meta', { name: 'author', content: 'tolking ' }], 17 | ], 18 | locales: { 19 | root: { 20 | label: 'English', 21 | lang: 'en', 22 | description: 'A blog theme based on vitepress', 23 | themeConfig: { 24 | nav: [ 25 | { text: 'Home', link: '/' }, 26 | { text: 'Tag', link: '/tag' }, 27 | { text: 'Category', link: '/category' }, 28 | ], 29 | tag: '/tag', 30 | category: '/category', 31 | createTime: { 32 | text: 'Create Time', 33 | }, 34 | lastUpdated: { 35 | text: 'Last Updated', 36 | }, 37 | }, 38 | }, 39 | zh: { 40 | label: '简体中文', 41 | lang: 'zh-CN', 42 | description: '为 vuepress 制作的一款主题', 43 | themeConfig: { 44 | nav: [ 45 | { text: '首页', link: '/zh/' }, 46 | { text: '标签', link: '/zh/tag' }, 47 | { text: '分类', link: '/zh/category' }, 48 | ], 49 | tag: '/zh/tag', 50 | category: '/zh/category', 51 | createTime: { 52 | text: '创建时间', 53 | }, 54 | lastUpdated: { 55 | text: '最后更新', 56 | }, 57 | }, 58 | }, 59 | }, 60 | themeConfig: { 61 | // logo: { 62 | // src: 'https://avatars.githubusercontent.com/u/23313167?v=4', 63 | // alt: 'logo' 64 | // }, 65 | cover: { 66 | src: 'https://picsum.photos/1920/1080?random', 67 | alt: 'cover image', 68 | }, 69 | socialLinks: [ 70 | { 71 | ariaLabel: 'GitHub', 72 | link: 'https://github.com/tolking/vitepress-theme-ououe', 73 | icon: 'github', 74 | }, 75 | ], 76 | pagination: { 77 | prev: '<-', 78 | next: '->', 79 | // match: (path) => /^\/($|index|page-)/.test(path), 80 | sort: (a, b) => a.index - b.index, 81 | // filter: (page) => page.home, 82 | }, 83 | excerpt: '', 84 | // readingProgress: 'bottom', 85 | footer: { 86 | // nav: [ 87 | // { text: "Home", link: "/" }, 88 | // { text: "GitHub", link: "https://github.com/tolking/vitepress-theme-ououe" }, 89 | // ], 90 | copyright: 'tolking © 2023', 91 | }, 92 | search: { 93 | provider: 'local', 94 | }, 95 | }, 96 | 97 | vite: { 98 | resolve: { 99 | alias: { 100 | '@src': resolve(__dirname, '../../src'), 101 | }, 102 | }, 103 | }, 104 | }) 105 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import Theme from '@src/index' 2 | 3 | export default Theme 4 | -------------------------------------------------------------------------------- /docs/[page].md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | --- 4 | -------------------------------------------------------------------------------- /docs/[page].paths.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | export default { 4 | paths() { 5 | const limit = 12 // Item count of each page 6 | const files = fs 7 | .readdirSync('docs/posts') 8 | .filter((file) => !/^\[[^]*]\]\./.test(file)) 9 | const total = Math.ceil(files.length / limit) 10 | 11 | return Array.from({ length: total }).map((_, index) => { 12 | const current = index + 1 13 | const page = current === 1 ? 'index' : `page-${current}` 14 | 15 | return { params: { page, current, limit } } 16 | }) 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /docs/category.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: category 3 | --- 4 | -------------------------------------------------------------------------------- /docs/posts/config.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Theme Configuration 3 | descript: How to configure blog pagination 4 | date: 2023-08-19 5 | image: https://picsum.photos/536/354?random=10 6 | index: 10 7 | tags: 8 | - reference 9 | - config 10 | categories: 11 | - docs 12 | --- 13 | 14 | All configuration options for the theme 15 | 16 | 17 | 18 | ## Options 19 | 20 | ### logo 21 | 22 | - Type: `string` | `object` 23 | 24 | The logo image of the website 25 | 26 | ```ts 27 | logo: 'https://avatars.githubusercontent.com/u/23313167?v=4', 28 | // Or 29 | logo: { 30 | src: 'https://avatars.githubusercontent.com/u/23313167?v=4', 31 | alt: 'logo', 32 | }, 33 | // Or 34 | logo: { 35 | light: 'https://avatars.githubusercontent.com/u/23313167?v=4', 36 | dark: 'https://avatars.githubusercontent.com/u/23313167?v=4', 37 | alt: 'logo', 38 | }, 39 | ``` 40 | 41 | ### cover 42 | 43 | - Type: `string` | `object` 44 | 45 | The cover image of the website, used in the same way as the logo 46 | 47 | ### nav 48 | 49 | - Type: `array` 50 | 51 | The navigation bar of the website, configured the same as in vitepress 52 | 53 | ```ts 54 | nav: [ 55 | { text: 'Home', link: '/' }, 56 | { text: 'Tag', link: '/tag' }, 57 | { text: 'Category', link: '/category' }, 58 | ], 59 | ``` 60 | 61 | ### socialLinks 62 | 63 | - Type: `array` 64 | 65 | The social links of the website, configured the same as in vitepress 66 | 67 | ```ts 68 | socialLinks: [ 69 | { 70 | ariaLabel: 'GitHub', 71 | link: 'https://github.com/tolking/vitepress-theme-ououe', 72 | icon: 'github', 73 | }, 74 | ], 75 | ``` 76 | 77 | ### pagination 78 | 79 | - Type: `object | array` 80 | 81 | Pagination configuration, for detailed usage please refer to [Pagination](./pagination.md) 82 | 83 | ### excerpt 84 | 85 | - Type: `string | boolean | function` 86 | - default: `---` 87 | 88 | How to split the article excerpt 89 | 90 | ```ts 91 | excerpt: '', 92 | ``` 93 | 94 | ### tag 95 | 96 | - Type: `string` 97 | 98 | The link address of the tag. Before using, you need to refer to [Tags and Categories](./tag.md) to configure the tag page 99 | 100 | ```ts 101 | tag: '/tag', 102 | ``` 103 | 104 | ### category 105 | 106 | - Type: `string` 107 | 108 | The link address of the category. Before using, you need to refer to [Tags and Categories](./tag.md) to configure the category page 109 | 110 | ```ts 111 | category: '/category', 112 | ``` 113 | 114 | ### createTime 115 | 116 | - Type: `object` 117 | 118 | How to display the creation time 119 | 120 | ```ts 121 | createTime: { 122 | text: 'Creation Time', 123 | format: (date) => new Date(date).toLocaleDateString(), 124 | }, 125 | ``` 126 | 127 | ### lastUpdated 128 | 129 | - Type: `object` 130 | 131 | How to display the last update time 132 | 133 | ```ts 134 | lastUpdated: { 135 | text: 'Last Updated', 136 | format: (date) => new Date(date).toLocaleDateString(), 137 | }, 138 | ``` 139 | 140 | ### readingProgress 141 | 142 | - Type: `boolean | 'top' | 'bottom' | 'left' | 'right'` 143 | 144 | How to display the reading progress bar, only effective on article pages 145 | 146 | ### footer 147 | 148 | - Type: `object` 149 | 150 | The content displayed in the footer 151 | 152 | ```ts 153 | footer: { 154 | copyright: 'copyright © 2023', 155 | }, 156 | ``` 157 | 158 | ### search 159 | 160 | - Type: `object` 161 | 162 | Search configuration, for detailed usage please refer to [Search](https://vitepress.dev/reference/default-theme-search) 163 | 164 | ```ts 165 | search: { 166 | provider: 'local', 167 | }, 168 | ``` 169 | 170 | :::details Type Declaration 171 | <<< @./../src/types/theme.ts 172 | ::: 173 | -------------------------------------------------------------------------------- /docs/posts/frontmatter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: frontmatter 3 | descript: How to configure the frontmatter of the blog 4 | date: 2023-08-19 5 | image: https://picsum.photos/536/354?random=4 6 | index: 4 7 | tags: 8 | - guide 9 | - frontmatter 10 | categories: 11 | - docs 12 | --- 13 | 14 | How to configure the frontmatter information of blog posts 15 | 16 | 17 | 18 | ## Usage 19 | 20 | - A standard article's frontmatter 21 | 22 | ```md 23 | --- 24 | title: Title of the article 25 | descript: Description of the article 26 | date: 2023-08-19 27 | image: Image link (optional) 28 | tags: 29 | - guide 30 | - frontmatter 31 | categories: 32 | - docs 33 | --- 34 | ``` 35 | 36 | - Some parameters to control the interface 37 | 38 | ```md 39 | --- 40 | createTime: false 41 | lastUpdated: false 42 | articlePagination: false 43 | footer: false 44 | readingProgress: false (`boolean | 'top' | 'bottom' | 'left' | 'right'`) 45 | --- 46 | ``` 47 | 48 | - Special attribute layout 49 | 50 | ```md 51 | --- 52 | layout: article (`'page' | 'tag' | 'category' | 'article'`) 53 | --- 54 | ``` 55 | 56 | Refer to [Tags and Categories](./tag.md) and [Pagination](./pagination.md) for usage 57 | -------------------------------------------------------------------------------- /docs/posts/pagination.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pagination 3 | descript: How to configure blog pagination 4 | date: 2023-08-19 5 | image: https://picsum.photos/536/354?random=3 6 | index: 3 7 | tags: 8 | - guide 9 | - pagination 10 | categories: 11 | - docs 12 | --- 13 | 14 | How to enable pagination for the theme and display pagination data 15 | 16 | 17 | 18 | ## Usage 19 | 20 | 1. A simple blog post data statistic 21 | 22 | -> index.md (or xxx.md) 23 | 24 | ```md 25 | --- 26 | layout: page 27 | --- 28 | ``` 29 | 30 | With `layout: page`, blog post pagination data will be injected. If used alone, it will not trigger pagination functionality, and all posts will be displayed on one page. 31 | 32 | 2. A standard pagination 33 | 34 | > [page] can be replaced with other values 35 | -------------------------------------------------------------------------------- /docs/posts/pwa.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: pwa 3 | descript: How to add PWA support to the blog 4 | date: 2023-08-27 5 | image: https://picsum.photos/536/354?random=22 6 | index: 22 7 | tags: 8 | - pwa 9 | categories: 10 | - enhance 11 | --- 12 | 13 | Add PWA support to the blog 14 | 15 | 16 | 17 | ## Installation 18 | 19 | First, you need to install the [@vite-pwa/vitepress](https://vite-pwa-org.netlify.app/frameworks/vitepress.html) plugin 20 | 21 | ::: code-group 22 | 23 | ```sh [npm] 24 | npm i -D @vite-pwa/vitepress 25 | ``` 26 | 27 | ```sh [pnpm] 28 | pnpm add -D @vite-pwa/vitepress 29 | ``` 30 | 31 | ```sh [yarn] 32 | yarn add -D @vite-pwa/vitepress 33 | ``` 34 | 35 | ::: 36 | 37 | ## Configuration 38 | 39 | -> .vitepress/config.ts 40 | 41 | ```ts 42 | import { defineConfig } from 'vitepress' 43 | import { withPwa } from '@vite-pwa/vitepress' 44 | 45 | export default withPwa( 46 | defineConfig({ 47 | //... 48 | pwa: {}, 49 | }), 50 | ) 51 | ``` 52 | 53 | Then inject the component into the slot 54 | 55 | -> .vitepress/theme/index.ts 56 | 57 | ```ts 58 | import { h } from 'vue' 59 | import Theme from 'vitepress/theme' 60 | import RegisterSW from './components/RegisterSW.vue' 61 | 62 | export default { 63 | ...Theme, 64 | Layout() { 65 | return h(Theme.Layout!, null, { 66 | 'footer-after': () => h(RegisterSW), 67 | }) 68 | }, 69 | } 70 | ``` 71 | 72 | For more details, refer to the [@vite-pwa/vitepress](https://vite-pwa-org.netlify.app/frameworks/vitepress.html) documentation 73 | -------------------------------------------------------------------------------- /docs/posts/quickstart.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quick Start 3 | description: 4 | date: 2023-08-19 5 | image: https://picsum.photos/536/354?random=1 6 | index: 1 7 | tags: 8 | - guide 9 | categories: 10 | - docs 11 | --- 12 | 13 | How to quickly use vitepress-theme-ououe as your blog theme 14 | 15 | 16 | 17 | ## Installation 18 | 19 | Since this theme is based on [vitepress](https://vitepress.dev/), you need to install it accordingly 20 | 21 | ::: code-group 22 | 23 | ```sh [npm] 24 | npm i vitepress vitepress-theme-ououe 25 | ``` 26 | 27 | ```sh [pnpm] 28 | pnpm add vitepress vitepress-theme-ououe 29 | ``` 30 | 31 | ```sh [yarn] 32 | yarn add vitepress vitepress-theme-ououe 33 | ``` 34 | 35 | ::: 36 | 37 | ## Usage 38 | 39 | 1. Import the theme 40 | 41 | -> .vitepress/theme/index.ts 42 | 43 | ```ts 44 | import Theme from 'vitepress-theme-ououe' 45 | 46 | export default Theme 47 | ``` 48 | 49 | 2. Add the theme configuration file 50 | 51 | -> .vitepress/config.ts 52 | 53 | ```ts 54 | import { defineConfigWithTheme } from 'vitepress' 55 | import type { Theme } from 'vitepress-theme-ououe' 56 | 57 | export default defineConfigWithTheme({ 58 | // ... 59 | themeConfig: { 60 | // config 61 | }, 62 | }) 63 | ``` 64 | 65 | [Detailed configuration information](./config.md) 66 | 67 | ## Recommended Directory Structure 68 | 69 | ``` 70 | +- blog 71 | +- .vitepress 72 | +- theme 73 | +- index.ts 74 | +- config.ts 75 | +- posts 76 | +- one.md 77 | ... 78 | +- category.md 79 | +- tag.md 80 | +- index.md 81 | ... 82 | ``` 83 | 84 | Among them, posts is the directory mainly for placing articles 85 | -------------------------------------------------------------------------------- /docs/posts/rss.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: RSS Subscription 3 | descript: How to add RSS to the blog 4 | date: 2023-08-24 5 | image: https://picsum.photos/536/354?random=21 6 | index: 21 7 | tags: 8 | - rss 9 | categories: 10 | - enhance 11 | --- 12 | 13 | The theme does not include RSS by default, but you can generate it with simple configuration 14 | 15 | 16 | 17 | ## Installation 18 | 19 | First, you need to install a plugin to generate RSS, such as [feed](https://github.com/ekalinin/sitemap.js#readme) 20 | 21 | ::: code-group 22 | 23 | ```sh [npm] 24 | npm i -D feed 25 | ``` 26 | 27 | ```sh [pnpm] 28 | pnpm add -D feed 29 | ``` 30 | 31 | ```sh [yarn] 32 | yarn add -D feed 33 | ``` 34 | 35 | ::: 36 | 37 | ## Configuration 38 | 39 | Generate RSS using the [buildEnd](https://vitepress.dev/reference/site-config#buildend) hook 40 | 41 | The following configuration is for reference only and should be modified according to actual conditions 42 | 43 | -> .vitepress/config.ts 44 | 45 | ```ts 46 | import { resolve } from 'path' 47 | import { writeFileSync } from 'fs' 48 | import { Feed } from 'feed' 49 | import { 50 | createContentLoader, 51 | defineConfigWithTheme, 52 | type SiteConfig, 53 | } from 'vitepress' 54 | import type { Theme } from 'vitepress-theme-ououe' 55 | 56 | export default defineConfigWithTheme({ 57 | // ... 58 | buildEnd: genRSS, 59 | }) 60 | 61 | async function genRSS(siteConfig: SiteConfig) { 62 | const baseUrl = `https://my.blog` 63 | const feed = new Feed({ 64 | title: siteConfig.site.title, 65 | description: siteConfig.site.description, 66 | id: baseUrl, 67 | link: baseUrl, 68 | language: siteConfig.site.lang, 69 | favicon: `${baseUrl}/favicon.ico`, 70 | copyright: siteConfig.site.themeConfig.footer?.copyright || '', 71 | }) 72 | 73 | const posts = await createContentLoader('posts/*.md', { 74 | excerpt: true, 75 | render: true, 76 | }).load() 77 | 78 | posts 79 | .filter((item) => { 80 | return !( 81 | /\[[^\]]*\]\./.test(item.url) || 82 | ['page', 'tag', 'category'].includes(item.frontmatter.layout) 83 | ) 84 | }) 85 | .sort((a, b) => { 86 | if (!b.frontmatter.date || !a.frontmatter.date) return 0 87 | return ( 88 | new Date(b.frontmatter.date).getTime() - 89 | new Date(a.frontmatter.date).getTime() 90 | ) 91 | }) 92 | .forEach((post) => { 93 | feed.addItem({ 94 | title: post.frontmatter.title || '', 95 | id: `${baseUrl}${post.url}`, 96 | link: `${baseUrl}${post.url}`, 97 | description: post.excerpt || post.frontmatter.description, 98 | content: post.html, 99 | image: post.frontmatter.image, 100 | date: new Date(post.frontmatter.date || ''), 101 | }) 102 | }) 103 | 104 | const filePath = resolve(siteConfig.outDir, 'feed.rss') 105 | 106 | writeFileSync(filePath, feed.rss2()) 107 | } 108 | ``` 109 | -------------------------------------------------------------------------------- /docs/posts/slots.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Slots 3 | descript: How to extend the theme 4 | date: 2023-08-19 5 | image: https://picsum.photos/536/354?random=11 6 | index: 11 7 | tags: 8 | - reference 9 | - slots 10 | categories: 11 | - docs 12 | --- 13 | 14 | You can easily extend the theme interface through the provided slots 15 | 16 | 17 | 18 | ## Usage 19 | 20 | -> .vitepress/theme/index.ts 21 | 22 | ```ts 23 | import { h } from 'vue' 24 | import Theme from 'vitepress-theme-ououe' 25 | import PwaPopup from '../components/PwaPopup.vue' 26 | import './index.css' 27 | 28 | export default { 29 | Layout() { 30 | return h(Theme.Layout!, null, { 31 | 'footer-after': () => h(PwaPopup), 32 | }) 33 | }, 34 | } 35 | ``` 36 | 37 | ## All Slots 38 | 39 | - header-before 40 | - header-left 41 | - header-logo-after 42 | - header-search-before 43 | - header-right 44 | - header-after 45 | - not-found-top 46 | - not-found-bottom 47 | - page-top 48 | - page-pagination-before 49 | - page-bottom 50 | - article-item-top 51 | - article-item-bottom 52 | - tag-top 53 | - tag-after 54 | - tag-bottom 55 | - tag-item 56 | - article-top 57 | - article-content-before 58 | - article-content-after 59 | - article-pagination-before 60 | - article-bottom 61 | - article-item-top 62 | - article-item-bottom 63 | - footer-before 64 | - footer-after 65 | 66 | :::details Type Declaration 67 | <<< @./../src/types/slots.ts 68 | ::: 69 | -------------------------------------------------------------------------------- /docs/posts/tag.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tags and Categories 3 | descript: How to configure tags and categories for the blog 4 | date: 2023-08-19 5 | image: https://picsum.photos/536/354?random=2 6 | index: 2 7 | tags: 8 | - guide 9 | - tag 10 | categories: 11 | - docs 12 | --- 13 | 14 | How to quickly display tags and categories data for all articles 15 | 16 | 17 | 18 | ## Configuration 19 | 20 | Since vitepress currently cannot generate purely dynamic pages, you need to add `tag.md` and `category.md` to generate the corresponding information 21 | 22 | -> tag.md 23 | 24 | ```md 25 | --- 26 | layout: tag 27 | --- 28 | ``` 29 | 30 | -> category.md 31 | 32 | ```md 33 | --- 34 | layout: category 35 | --- 36 | ``` 37 | 38 | Then configure the corresponding links through the [tag and category](./config.md#tag) attributes in the configuration 39 | 40 | ## Usage 41 | 42 | Add relevant information in the [frontmatter](./frontmatter.md) of the document 43 | 44 | ```md 45 | --- 46 | tags: 47 | - vitepress 48 | - vitepress-theme-ououe 49 | categories: 50 | - blog 51 | - theme 52 | --- 53 | ``` 54 | 55 | Or 56 | 57 | ```md 58 | --- 59 | tag: vitepress 60 | category: blog 61 | --- 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolking/vitepress-theme-ououe/630e8fe73cee075055582dfc02f6d1c921c8c19c/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/vitepress-theme-ououe.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolking/vitepress-theme-ououe/630e8fe73cee075055582dfc02f6d1c921c8c19c/docs/public/vitepress-theme-ououe.jpg -------------------------------------------------------------------------------- /docs/tag.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: tag 3 | --- 4 | -------------------------------------------------------------------------------- /docs/zh/[page].md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | --- 4 | -------------------------------------------------------------------------------- /docs/zh/[page].paths.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | export default { 4 | paths() { 5 | const limit = 12 // Item count of each page 6 | const files = fs 7 | .readdirSync('docs/zh/posts') 8 | .filter((file) => !/^\[[^]*]\]\./.test(file)) 9 | const total = Math.ceil(files.length / limit) 10 | 11 | return Array.from({ length: total }).map((_, index) => { 12 | const current = index + 1 13 | const page = current === 1 ? 'index' : `page-${current}` 14 | 15 | return { params: { page, current, limit } } 16 | }) 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /docs/zh/category.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: category 3 | --- 4 | -------------------------------------------------------------------------------- /docs/zh/posts/config.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 主题配置 3 | descript: 如何配置博客的分页 4 | date: 2023-08-19 5 | image: https://picsum.photos/536/354?random=10 6 | index: 10 7 | tags: 8 | - reference 9 | - config 10 | categories: 11 | - 文档功能 12 | --- 13 | 14 | 主题的所有配置项 15 | 16 | 17 | 18 | ## Options 19 | 20 | ### logo 21 | 22 | - Type: `string` | `object` 23 | 24 | 网站的 logo 图片 25 | 26 | ```ts 27 | logo: 'https://avatars.githubusercontent.com/u/23313167?v=4', 28 | // 或者 29 | logo: { 30 | src: 'https://avatars.githubusercontent.com/u/23313167?v=4', 31 | alt: 'logo', 32 | }, 33 | // 或者 34 | logo: { 35 | light: 'https://avatars.githubusercontent.com/u/23313167?v=4', 36 | dark: 'https://avatars.githubusercontent.com/u/23313167?v=4', 37 | alt: 'logo', 38 | }, 39 | ``` 40 | 41 | ### cover 42 | 43 | - Type: `string` | `object` 44 | 45 | 网站的封面图片,使用方式同 logo 46 | 47 | ### nav 48 | 49 | - Type: `array` 50 | 51 | 网站的导航栏,和 vitepress 具有相同的配置 52 | 53 | ```ts 54 | nav: [ 55 | { text: 'Home', link: '/' }, 56 | { text: 'Tag', link: '/tag' }, 57 | { text: 'Category', link: '/category' }, 58 | ], 59 | ``` 60 | 61 | ### socialLinks 62 | 63 | - Type: `array` 64 | 65 | 网站的社交链接,和 vitepress 具有相同的配置 66 | 67 | ```ts 68 | socialLinks: [ 69 | { 70 | ariaLabel: 'GitHub', 71 | link: 'https://github.com/tolking/vitepress-theme-ououe', 72 | icon: 'github', 73 | }, 74 | ], 75 | ``` 76 | 77 | ### pagination 78 | 79 | - Type: `object | array` 80 | 81 | 分页的配置,具体的使用方法请参考[分页](./pagination.md) 82 | 83 | ### excerpt 84 | 85 | - Type: `string | boolean | function` 86 | - default: `---` 87 | 88 | 如何分割文章的摘要 89 | 90 | ```ts 91 | excerpt: '', 92 | ``` 93 | 94 | ### tag 95 | 96 | - Type: `string` 97 | 98 | 标签的链接地址。在使用前,你需要参考[标签和分类](./tag.md)来配置 tag 的页面 99 | 100 | ```ts 101 | tag: '/tag', 102 | ``` 103 | 104 | ### category 105 | 106 | - Type: `string` 107 | 108 | 分类的链接地址。在使用前,你需要参考[标签和分类](./tag.md)来配置 category 的页面 109 | 110 | ```ts 111 | category: '/category', 112 | ``` 113 | 114 | ### createTime 115 | 116 | - Type: `object` 117 | 118 | 如何显示创建时间 119 | 120 | ```ts 121 | createTime: { 122 | text: '创建时间', 123 | format: (date) => new Date(date).toLocaleDateString(), 124 | }, 125 | ``` 126 | 127 | ### lastUpdated 128 | 129 | - Type: `object` 130 | 131 | 如何显示最后更新时间 132 | 133 | ```ts 134 | lastUpdated: { 135 | text: '更新时间', 136 | format: (date) => new Date(date).toLocaleDateString(), 137 | }, 138 | ``` 139 | 140 | ### readingProgress 141 | 142 | - Type: `boolean | 'top' | 'bottom' | 'left' | 'right'` 143 | 144 | 如何显示阅读进度条,仅在文章页面有效 145 | 146 | ### footer 147 | 148 | - Type: `object` 149 | 150 | 页脚的显示内容 151 | 152 | ```ts 153 | footer: { 154 | copyright: 'copyright © 2023', 155 | }, 156 | ``` 157 | 158 | ### search 159 | 160 | - Type: `object` 161 | 162 | 搜索的配置,具体的使用方法请参考[搜索](https://vitepress.dev/reference/default-theme-search) 163 | 164 | ```ts 165 | search: { 166 | provider: 'local', 167 | }, 168 | ``` 169 | 170 | :::details 类型声明 171 | <<< @./../src/types/theme.ts 172 | ::: 173 | -------------------------------------------------------------------------------- /docs/zh/posts/frontmatter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: frontmatter 3 | descript: 如何配置博客的 frontmatter 4 | date: 2023-08-19 5 | image: https://picsum.photos/536/354?random=4 6 | index: 4 7 | tags: 8 | - guide 9 | - frontmatter 10 | categories: 11 | - 文档功能 12 | --- 13 | 14 | 如何配置博客文章的 frontmatter 信息 15 | 16 | 17 | 18 | ## 使用 19 | 20 | - 一个标准文章的 frontmatter 21 | 22 | ```md 23 | --- 24 | title: 文章的标题 25 | descript: 文章的描述 26 | date: 2023-08-19 27 | image: 图片链接 (可以省略) 28 | tags: 29 | - guide 30 | - frontmatter 31 | categories: 32 | - 文档功能 33 | --- 34 | ``` 35 | 36 | - 一些控制界面的参数 37 | 38 | ```md 39 | --- 40 | createTime: false 41 | lastUpdated: false 42 | articlePagination: false 43 | footer: false 44 | readingProgress: false (`boolean | 'top' | 'bottom' | 'left' | 'right'`) 45 | --- 46 | ``` 47 | 48 | - 特殊的属性 layout 49 | 50 | ```md 51 | --- 52 | layout: article (`'page' | 'tag' | 'category' | 'article'`) 53 | --- 54 | ``` 55 | 56 | 参考 [标签和分类](./tag.md) 和 [分页](./pagination.md) 使用 57 | -------------------------------------------------------------------------------- /docs/zh/posts/pagination.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 分页 3 | descript: 如何配置博客的分页 4 | date: 2023-08-19 5 | image: https://picsum.photos/536/354?random=3 6 | index: 3 7 | tags: 8 | - guide 9 | - pagination 10 | categories: 11 | - 文档功能 12 | --- 13 | 14 | 如何启用主题的分页,显示分页数据 15 | 16 | 17 | 18 | ## 使用 19 | 20 | 1. 一个最简单的博客文章数据统计 21 | 22 | -> index.md (or xxx.md) 23 | 24 | ```md 25 | --- 26 | layout: page 27 | --- 28 | ``` 29 | 30 | 带有 `layout: page` 将会被注入博客文章分页数据。如果仅这样使用将不会触发分页功能,所以的文章都会显示在一个页面中 31 | 32 | 2. 一个标准的分页 33 | 34 | > [page] 可以换成其它值 35 | 36 | ::: code-group 37 | 38 | <<< @/[page].md 39 | 40 | <<< @/[page].paths.ts 41 | 42 | ::: 43 | 44 | 基于 vitepess 的 [dynamic-routes](https://vitepress.dev/guide/routing#dynamic-routes) 功能,可以动态生成分页界面。默认将占用 `index.md` 和 `page-[n].md` 生成分页,如果你需要修改请参考下面的[formatPage](#formatPage)配置 45 | 46 | 3. 配置多个分页 47 | 48 | 主题支持配置多个分页,在需要分页数据的地方按照上面方式配置。然后在配置中增加 [pagination](./config.md#pagination) 属性 49 | 50 | 例如:目前有两个目录 `posts` `lib` 需要分页,同时还想在首页显示筛选出来的文章 51 | 52 | 首先需要在按照 1 的方式配置 `index.md` 用来显示首页筛选出来的文章 53 | 54 | 然后在 `posts` 和 `lib` 目录下按照 2 的方式配置 `[page].md` 和 `[page].paths.ts`。注意需要将 `readdirSync('docs/posts')` 修改为当前目录 55 | 56 | 最后在配置中增加 57 | 58 | -> .vitepress/config.ts 59 | 60 | ```ts 61 | pagination: [ 62 | { 63 | match: (path) => /^\/($|index|page-)/.test(path), 64 | filter: (page) => page.display === 'home', // 将匹配 frontmatter 中包含 `display: home` 的页面 65 | }, 66 | { 67 | dir: 'posts', 68 | prev: '上一页', 69 | next: '下一页', 70 | }, 71 | { 72 | dir: 'lib', 73 | prev: '上一页', 74 | next: '下一页', 75 | }, 76 | ], 77 | ``` 78 | 79 | ## pagination 配置 80 | 81 | 对于仅有一处分页的情况,pagination 支持传入一个对象;对于多处分页的情况,pagination 支持传入一个数组 82 | 83 | ### group 84 | 85 | - Type: `number` 86 | - Default: `5` 87 | 88 | 分页组件中显示的分页按钮数量 89 | 90 | ### prev 91 | 92 | - Type: `string` 93 | - Default: `Prev` 94 | 95 | 分页组件中上一页按钮的文本 96 | 97 | ### next 98 | 99 | - Type: `string` 100 | - Default: `Next` 101 | 102 | 分页组件中下一页按钮的文本 103 | 104 | ### dir 105 | 106 | - Type: `string | array` 107 | - Default: [srcDir](https://vitepress.dev/reference/site-config#srcdir) 108 | 109 | 需要统计分页数据的目录。当你有多个目录需要分页时,最好将该值设置为目录名,否则你需要配置 `match` 110 | 111 | ### match 112 | 113 | - Type: `function` 114 | 115 | 当你需要对多个目录进行分页时,可以通过该函数来匹配需要分页的目录。该函数接收一个参数 `path`,表示当前页面的路径。如果返回 `true` 则表示当前分页路径配置当前配置 116 | 117 | ### filter 118 | 119 | - Type: `function` 120 | 121 | 当前分页的过滤函数 122 | 123 | ### sort 124 | 125 | - Type: `function` 126 | 127 | 当前分页的排序函数,默认将通过 `frontmatter` 中的 `date` 字段进行排序 128 | 129 | ### formatPage 130 | 131 | - Type: `function` 132 | 133 | 格式化分页按钮链接的函数。如果你更改了 `[page].paths.ts` 中的 page 参数名,那么你需要增加该函数用于格式化分页链接 134 | 135 | :::details 类型声明 136 | <<< @./../src/types/theme.ts#pagination 137 | ::: 138 | -------------------------------------------------------------------------------- /docs/zh/posts/pwa.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: pwa 3 | descript: 如何为博客增加PWA支持 4 | date: 2023-08-27 5 | image: https://picsum.photos/536/354?random=22 6 | index: 22 7 | tags: 8 | - pwa 9 | categories: 10 | - 增强功能 11 | --- 12 | 13 | 为博客增加 PWA 支持 14 | 15 | 16 | 17 | ## 安装 18 | 19 | 首先需要安装 [@vite-pwa/vitepress](https://vite-pwa-org.netlify.app/frameworks/vitepress.html) 插件 20 | 21 | ::: code-group 22 | 23 | ```sh [npm] 24 | npm i -D @vite-pwa/vitepress 25 | ``` 26 | 27 | ```sh [pnpm] 28 | pnpm add -D @vite-pwa/vitepress 29 | ``` 30 | 31 | ```sh [yarn] 32 | yarn add -D @vite-pwa/vitepress 33 | ``` 34 | 35 | ::: 36 | 37 | ## 配置 38 | 39 | -> .vitepress/config.ts 40 | 41 | ```ts 42 | import { defineConfig } from 'vitepress' 43 | import { withPwa } from '@vite-pwa/vitepress' 44 | 45 | export default withPwa( 46 | defineConfig({ 47 | //... 48 | pwa: {}, 49 | }), 50 | ) 51 | ``` 52 | 53 | 然后在插槽中注入组件 54 | 55 | -> .vitepress/theme/index.ts 56 | 57 | ```ts 58 | import { h } from 'vue' 59 | import Theme from 'vitepress/theme' 60 | import RegisterSW from './components/RegisterSW.vue' 61 | 62 | export default { 63 | ...Theme, 64 | Layout() { 65 | return h(Theme.Layout!, null, { 66 | 'footer-after': () => h(RegisterSW), 67 | }) 68 | }, 69 | } 70 | ``` 71 | 72 | 更多细节参考 [@vite-pwa/vitepress](https://vite-pwa-org.netlify.app/frameworks/vitepress.html) 文档 73 | -------------------------------------------------------------------------------- /docs/zh/posts/quickstart.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 快速开始 3 | description: 4 | date: 2023-08-19 5 | image: https://picsum.photos/536/354?random=1 6 | index: 1 7 | tags: 8 | - guide 9 | categories: 10 | - 文档功能 11 | --- 12 | 13 | 如何快速使用 vitepress-theme-ououe 作为你的博客主题 14 | 15 | 16 | 17 | ## 安装 18 | 19 | 由于本主题是基于 [vitepress](https://vitepress.dev/) 实现,所以你需要根据情况安装它 20 | 21 | ::: code-group 22 | 23 | ```sh [npm] 24 | npm i vitepress vitepress-theme-ououe 25 | ``` 26 | 27 | ```sh [pnpm] 28 | pnpm add vitepress vitepress-theme-ououe 29 | ``` 30 | 31 | ```sh [yarn] 32 | yarn add vitepress vitepress-theme-ououe 33 | ``` 34 | 35 | ::: 36 | 37 | ## 使用 38 | 39 | 1. 引入主题 40 | 41 | -> .vitepress/theme/index.ts 42 | 43 | ```ts 44 | import Theme from 'vitepress-theme-ououe' 45 | 46 | export default Theme 47 | ``` 48 | 49 | 2. 增加主题的配置文件 50 | 51 | -> .vitepress/config.ts 52 | 53 | ```ts 54 | import { defineConfigWithTheme } from 'vitepress' 55 | import type { Theme } from 'vitepress-theme-ououe' 56 | 57 | export default defineConfigWithTheme({ 58 | // ... 59 | themeConfig: { 60 | // config 61 | }, 62 | }) 63 | ``` 64 | 65 | [详细的配置信息](./config.md) 66 | 67 | ## 建议目录结构 68 | 69 | ``` 70 | +- blog 71 | +- .vitepress 72 | +- theme 73 | +- index.ts 74 | +- config.ts 75 | +- posts 76 | +- one.md 77 | ... 78 | +- category.md 79 | +- tag.md 80 | +- index.md 81 | ... 82 | ``` 83 | 84 | 其中 posts 是主要放置文章的目录 85 | -------------------------------------------------------------------------------- /docs/zh/posts/rss.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: RSS 订阅 3 | descript: 如何为博客增加RSS 4 | date: 2023-08-24 5 | image: https://picsum.photos/536/354?random=21 6 | index: 21 7 | tags: 8 | - rss 9 | categories: 10 | - 增强功能 11 | --- 12 | 13 | 主题默认是不包含 RSS 的,但你可以通过简单的配置来生成它 14 | 15 | 16 | 17 | ## 安装 18 | 19 | 首先需要安装生成 RSS 的插件,如 [feed](https://github.com/ekalinin/sitemap.js#readme) 20 | 21 | ::: code-group 22 | 23 | ```sh [npm] 24 | npm i -D feed 25 | ``` 26 | 27 | ```sh [pnpm] 28 | pnpm add -D feed 29 | ``` 30 | 31 | ```sh [yarn] 32 | yarn add -D feed 33 | ``` 34 | 35 | ::: 36 | 37 | ## 配置 38 | 39 | 通过 [buildEnd](https://vitepress.dev/reference/site-config#buildend) 钩子来生成 RSS 40 | 41 | 下面配置仅供参考,根据实际情况修改 42 | 43 | -> .vitepress/config.ts 44 | 45 | ```ts 46 | import { resolve } from 'path' 47 | import { writeFileSync } from 'fs' 48 | import { Feed } from 'feed' 49 | import { 50 | createContentLoader, 51 | defineConfigWithTheme, 52 | type SiteConfig, 53 | } from 'vitepress' 54 | import type { Theme } from 'vitepress-theme-ououe' 55 | 56 | export default defineConfigWithTheme({ 57 | // ... 58 | buildEnd: genRSS, 59 | }) 60 | 61 | function genRSS(siteConfig: SiteConfig) { 62 | const baseUrl = `https://my.blog` 63 | const feed = new Feed({ 64 | title: siteConfig.site.title, 65 | description: siteConfig.site.description, 66 | id: baseUrl, 67 | link: baseUrl, 68 | language: siteConfig.site.lang, 69 | favicon: `${baseUrl}/favicon.ico`, 70 | copyright: siteConfig.site.themeConfig.footer?.copyright || '', 71 | }) 72 | 73 | const posts = await createContentLoader('posts/*.md', { 74 | excerpt: true, 75 | render: true, 76 | }).load() 77 | 78 | posts 79 | .filter((item) => { 80 | return !( 81 | /\[[^\]]*\]\./.test(item.url) || 82 | ['page', 'tag', 'category'].includes(item.frontmatter.layout) 83 | ) 84 | }) 85 | .sort((a, b) => { 86 | if (!b.frontmatter.date || !a.frontmatter.date) return 0 87 | return ( 88 | new Date(b.frontmatter.date).getTime() - 89 | new Date(a.frontmatter.date).getTime() 90 | ) 91 | }) 92 | .forEach((post) => { 93 | feed.addItem({ 94 | title: post.frontmatter.title || '', 95 | id: `${baseUrl}${post.url}`, 96 | link: `${baseUrl}${post.url}`, 97 | description: post.excerpt || post.frontmatter.description, 98 | content: post.html, 99 | image: post.frontmatter.image, 100 | date: new Date(post.frontmatter.date || ''), 101 | }) 102 | }) 103 | 104 | const filePath = resolve(siteConfig.outDir, 'feed.rss') 105 | 106 | writeFileSync(filePath, feed.rss2()) 107 | } 108 | ``` 109 | -------------------------------------------------------------------------------- /docs/zh/posts/slots.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 插槽 3 | descript: 如何扩展主题 4 | date: 2023-08-19 5 | image: https://picsum.photos/536/354?random=11 6 | index: 11 7 | tags: 8 | - reference 9 | - slots 10 | categories: 11 | - 文档功能 12 | --- 13 | 14 | 通过内部提供的插槽可以很轻松的扩展主题界面 15 | 16 | 17 | 18 | ## 使用 19 | 20 | -> .vitepress/theme/index.ts 21 | 22 | ```ts 23 | import { h } from 'vue' 24 | import Theme from 'vitepress-theme-ououe' 25 | import PwaPopup from '../components/PwaPopup.vue' 26 | import './index.css' 27 | 28 | export default { 29 | Layout() { 30 | return h(Theme.Layout!, null, { 31 | 'footer-after': () => h(PwaPopup), 32 | }) 33 | }, 34 | } 35 | ``` 36 | 37 | ## 全部插槽 38 | 39 | - header-before 40 | - header-left 41 | - header-logo-after 42 | - header-search-before 43 | - header-right 44 | - header-after 45 | - not-found-top 46 | - not-found-bottom 47 | - page-top 48 | - page-pagination-before 49 | - page-bottom 50 | - article-item-top 51 | - article-item-bottom 52 | - tag-top 53 | - tag-after 54 | - tag-bottom 55 | - tag-item 56 | - article-top 57 | - article-content-before 58 | - article-content-after 59 | - article-pagination-before 60 | - article-bottom 61 | - article-item-top 62 | - article-item-bottom 63 | - footer-before 64 | - footer-after 65 | 66 | :::details 类型声明 67 | <<< @./../src/types/slots.ts 68 | ::: 69 | -------------------------------------------------------------------------------- /docs/zh/posts/tag.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 标签和分类 3 | descript: 如何配置博客的标签和分类 4 | date: 2023-08-19 5 | image: https://picsum.photos/536/354?random=2 6 | index: 2 7 | tags: 8 | - guide 9 | - tag 10 | categories: 11 | - 文档功能 12 | --- 13 | 14 | 如何快速显示所有文章的标签和分类数据 15 | 16 | 17 | 18 | ## 配置 19 | 20 | 由于 vitepress 目前不能够生成纯动态的页面,所以需要增加 `tag.md` `category.md` 生成对应信息 21 | 22 | -> tag.md 23 | 24 | ```md 25 | --- 26 | layout: tag 27 | --- 28 | ``` 29 | 30 | -> category.md 31 | 32 | ```md 33 | --- 34 | layout: category 35 | --- 36 | ``` 37 | 38 | 然后在配置中通过 [tag 和 category](./config.md#tag) 属性配置对应链接 39 | 40 | ## 使用 41 | 42 | 在文档中的 [frontmatter](./frontmatter.md) 中增加相关信息即可 43 | 44 | ```md 45 | --- 46 | tags: 47 | - vitepress 48 | - vitepress-themt-ououe 49 | categories: 50 | - blog 51 | - theme 52 | --- 53 | ``` 54 | 55 | 或者 56 | 57 | ```md 58 | --- 59 | tag: vitepress 60 | category: blog 61 | --- 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/zh/tag.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: tag 3 | --- 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vitepress-theme-ououe", 3 | "version": "1.0.3", 4 | "description": "A blog theme based on vitepress", 5 | "type": "module", 6 | "main": "./src/index.ts", 7 | "types": "./src/types/index.ts", 8 | "scripts": { 9 | "dev": "vitepress dev docs", 10 | "build": "vitepress build docs", 11 | "build:docs": "vitepress build docs --base /vitepress-theme-ououe/", 12 | "preview": "vitepress preview docs", 13 | "types:test": "vue-tsc --noEmit", 14 | "lint": "eslint . --fix --ext .ts,.vue,.js", 15 | "lint:test": "eslint . --ext .ts,.vue,.js --max-warnings 0", 16 | "prettier": "prettier --check --write --ignore-unknown \"{docs,src}/**\"", 17 | "prepare": "husky install" 18 | }, 19 | "keywords": [ 20 | "vitepress", 21 | "blog", 22 | "blog-theme" 23 | ], 24 | "files": [ 25 | "src" 26 | ], 27 | "author": "tolking ", 28 | "license": "MIT", 29 | "peerDependencies": { 30 | "vitepress": "^1.0.0" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^22.7.5", 34 | "@typescript-eslint/eslint-plugin": "^7.18.0", 35 | "@typescript-eslint/parser": "^7.18.0", 36 | "eslint": "^8.57.1", 37 | "eslint-plugin-prettier": "^5.2.1", 38 | "eslint-plugin-vue": "^9.29.0", 39 | "eslint-plugin-vuejs-accessibility": "^2.4.1", 40 | "husky": "^8.0.3", 41 | "lint-staged": "^13.3.0", 42 | "prettier": "^3.3.3", 43 | "vitepress": "^1.4.0", 44 | "vue": "^3.5.12", 45 | "vue-tsc": "^1.8.27" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "git+https://github.com/tolking/vitepress-theme-ououe.git" 50 | }, 51 | "bugs": { 52 | "url": "https://github.com/tolking/vitepress-theme-ououe/issues" 53 | }, 54 | "homepage": "https://github.com/tolking/vitepress-theme-ououe#readme", 55 | "lint-staged": { 56 | "*.{ts,vue,js,tsx,jsx}": [ 57 | "prettier --write --no-verify", 58 | "eslint --fix" 59 | ], 60 | "*.{html,css,md,json}": "prettier --write" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/VPAppearance.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 73 | 74 | 96 | -------------------------------------------------------------------------------- /src/components/VPArticleList.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 72 | 73 | 82 | 83 | 132 | -------------------------------------------------------------------------------- /src/components/VPCover.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 76 | 77 | 134 | -------------------------------------------------------------------------------- /src/components/VPFooter.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 41 | 42 | 65 | -------------------------------------------------------------------------------- /src/components/VPHeader.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 64 | 65 | 128 | -------------------------------------------------------------------------------- /src/components/VPNavTags.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 46 | 47 | 54 | 55 | 68 | -------------------------------------------------------------------------------- /src/components/VPPagination.vue: -------------------------------------------------------------------------------- 1 | 102 | 103 | 150 | 151 | 161 | 162 | 205 | -------------------------------------------------------------------------------- /src/components/VPReadingProgress.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 83 | 84 | 127 | -------------------------------------------------------------------------------- /src/components/VPTagList.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 57 | 58 | 67 | 68 | 113 | -------------------------------------------------------------------------------- /src/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './langs' 2 | export * from './pagination' 3 | export * from './prevNext' 4 | export * from './tag' 5 | -------------------------------------------------------------------------------- /src/composables/langs.ts: -------------------------------------------------------------------------------- 1 | import { useData } from 'vitepress' 2 | import { computed } from 'vue' 3 | 4 | export function useLangs() { 5 | const { site, localeIndex } = useData() 6 | 7 | const prefix = computed(() => { 8 | return ( 9 | site.value.locales[localeIndex.value]?.link || 10 | (localeIndex.value === 'root' ? '/' : `/${localeIndex.value}/`) 11 | ) 12 | }) 13 | 14 | function isLocaleUrl(url: string) { 15 | if (localeIndex.value === 'root') { 16 | const locales = Object.keys(site.value.locales).filter( 17 | (lang) => lang !== 'root', 18 | ) 19 | const regExp = new RegExp(`^/(${locales.join('|')})/`) 20 | return !regExp.test(url) 21 | } else { 22 | return url.startsWith(prefix.value) 23 | } 24 | } 25 | 26 | return { prefix, isLocaleUrl } 27 | } 28 | -------------------------------------------------------------------------------- /src/composables/pagination.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { useData, useRoute } from 'vitepress' 3 | import { useLangs } from './index' 4 | import { data } from '../posts.data' 5 | import { isFunction, toArray } from '../utils/index' 6 | import type { Theme, PaginationParams } from '../types/index' 7 | 8 | export function usePagination() { 9 | const route = useRoute() 10 | const { theme, page } = useData() 11 | const { prefix, isLocaleUrl } = useLangs() 12 | 13 | /** Obtain the pagination config that matches the current route from the config file */ 14 | const config = computed(() => { 15 | const pagination = theme.value.pagination && toArray(theme.value.pagination) 16 | if (!pagination?.length) return 17 | if (pagination.length === 1) return pagination[0] 18 | 19 | return pagination.find((item) => { 20 | if (isFunction(item.match)) { 21 | return item.match(route.path) 22 | } else if (item.dir) { 23 | const regExp = new RegExp( 24 | `^${prefix.value}(${toArray(item.dir).join('|')})/`, 25 | ) 26 | return regExp.test(route.path) 27 | } 28 | }) 29 | }) 30 | 31 | /** Get all posts */ 32 | const posts = computed(() => { 33 | if (!data.length) return [] 34 | let list = data.filter((item) => isLocaleUrl(item.url)) 35 | 36 | if (config.value?.filter) { 37 | list = list.filter(config.value.filter) 38 | } else if (config.value?.dir) { 39 | const regExp = new RegExp(`^/(${toArray(config.value.dir).join('|')})/`) 40 | list = list.filter((item) => regExp.test(item.url)) 41 | } 42 | 43 | if (config.value?.sort) { 44 | list = list.sort(config.value.sort) 45 | } else { 46 | list = list.sort((a, b) => { 47 | if (!b.date || !a.date) return 0 48 | return new Date(b.date).getTime() - new Date(a.date).getTime() 49 | }) 50 | } 51 | 52 | return list 53 | }) 54 | 55 | /** Get the current page data */ 56 | const list = computed(() => { 57 | const params = page.value.params as PaginationParams | undefined 58 | if (!params?.current || !params?.limit) return posts.value 59 | return posts.value.slice( 60 | (params.current - 1) * params.limit, 61 | params.current * params.limit, 62 | ) 63 | }) 64 | 65 | return { 66 | config, 67 | posts, 68 | list, 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/composables/prevNext.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { useRoute, withBase } from 'vitepress' 3 | import { usePagination } from './index' 4 | import type { PostsItem } from '../types/index' 5 | 6 | export function usePrevNext() { 7 | const route = useRoute() 8 | const { posts } = usePagination() 9 | 10 | return computed(() => { 11 | const index = posts.value.findIndex( 12 | (item) => withBase(item.url) === route.path, 13 | ) 14 | const list: PostsItem[] = [] 15 | 16 | if (index > 0) { 17 | list.push(posts.value[index - 1]) 18 | } 19 | if (index < posts.value.length - 1) { 20 | list.push(posts.value[index + 1]) 21 | } 22 | 23 | return list 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/composables/tag.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref, watch } from 'vue' 2 | import { useData, inBrowser, useRoute } from 'vitepress' 3 | import { useLangs } from './index' 4 | import { data } from '../posts.data' 5 | import { Classifiable } from '../utils/index' 6 | import type { Theme } from '../types/index' 7 | 8 | const classifiable = new Classifiable(data) 9 | 10 | export function useTag() { 11 | const route = useRoute() 12 | const { frontmatter, theme } = useData() 13 | const { isLocaleUrl } = useLangs() 14 | 15 | const current = ref(getQuery()) 16 | 17 | const list = computed(() => { 18 | switch (frontmatter.value.layout) { 19 | case 'tag': 20 | return classifiable.getAllTags((item) => isLocaleUrl(item.url)) 21 | case 'category': 22 | return classifiable.getAllCategories((item) => isLocaleUrl(item.url)) 23 | default: 24 | return undefined 25 | } 26 | }) 27 | 28 | const posts = computed(() => { 29 | if (!current.value) return undefined 30 | 31 | switch (frontmatter.value.layout) { 32 | case 'tag': 33 | return classifiable.getPostsByTag(current.value, (item) => 34 | isLocaleUrl(item.url), 35 | ) 36 | case 'category': 37 | return classifiable.getPostsByCategory(current.value, (item) => 38 | isLocaleUrl(item.url), 39 | ) 40 | default: 41 | return undefined 42 | } 43 | }) 44 | 45 | watch( 46 | () => route.path, 47 | (value) => { 48 | if ([theme.value.tag, theme.value.category].includes(value)) { 49 | current.value = getQuery() 50 | } 51 | }, 52 | ) 53 | 54 | function getQuery() { 55 | if (!inBrowser) return '' 56 | const { search } = new URL(location.href) 57 | const searchParams = new URLSearchParams(search) 58 | const tag = searchParams.get('t') 59 | 60 | return tag || '' 61 | } 62 | 63 | return { 64 | current, 65 | list, 66 | posts, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue' 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | 8 | declare module 'vitepress/dist/client/theme-default/composables/nav' { 9 | export function useNav(): { 10 | isScreenOpen: boolean 11 | openScreen: () => void 12 | closeScreen: () => void 13 | toggleScreen: () => void 14 | } 15 | } 16 | 17 | declare module 'vitepress/dist/client/shared' { 18 | export function isActive( 19 | currentPath: string, 20 | matchPath: string, 21 | asRegex = false, 22 | ): boolean 23 | } 24 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Layout from './layouts/index.vue' 2 | import type { Theme } from 'vitepress' 3 | 4 | import 'vitepress/dist/client/theme-default/styles/vars.css' 5 | import 'vitepress/dist/client/theme-default/styles/base.css' 6 | import 'vitepress/dist/client/theme-default/styles/fonts.css' 7 | import 'vitepress/dist/client/theme-default/styles/icons.css' 8 | import 'vitepress/dist/client/theme-default/styles/utils.css' 9 | import 'vitepress/dist/client/theme-default/styles/components/custom-block.css' 10 | import 'vitepress/dist/client/theme-default/styles/components/vp-code-group.css' 11 | import 'vitepress/dist/client/theme-default/styles/components/vp-code.css' 12 | import 'vitepress/dist/client/theme-default/styles/components/vp-doc.css' 13 | import './styles/index.css' 14 | 15 | const theme: Theme = { 16 | Layout, 17 | } 18 | 19 | export default theme 20 | -------------------------------------------------------------------------------- /src/layouts/Article.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 114 | 115 | 156 | -------------------------------------------------------------------------------- /src/layouts/NotFound.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 41 | 42 | 75 | -------------------------------------------------------------------------------- /src/layouts/Page.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 39 | 40 | 45 | -------------------------------------------------------------------------------- /src/layouts/Tag.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 57 | 58 | 63 | -------------------------------------------------------------------------------- /src/layouts/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 155 | -------------------------------------------------------------------------------- /src/posts.data.ts: -------------------------------------------------------------------------------- 1 | import { createContentLoader } from 'vitepress' 2 | import { toArray } from './utils/index' 3 | import type { SiteConfig } from 'vitepress' 4 | import type { Theme, PostsItem } from './types/theme' 5 | 6 | declare const data: PostsItem[] 7 | export { data } 8 | 9 | type GlobalThis = typeof globalThis & { VITEPRESS_CONFIG: SiteConfig } 10 | 11 | const config = (globalThis as GlobalThis).VITEPRESS_CONFIG 12 | const pattern = getPattern() 13 | 14 | export default createContentLoader(pattern, { 15 | excerpt: config.site.themeConfig.excerpt ?? true, 16 | transform(raw): PostsItem[] { 17 | const posts: PostsItem[] = [] 18 | 19 | for (let i = 0; i < raw.length; i++) { 20 | const { excerpt, frontmatter, url } = raw[i] 21 | 22 | if ( 23 | /\[[^\]]*\]\./.test(url) || 24 | ['page', 'tag', 'category'].includes(frontmatter.layout) 25 | ) { 26 | continue 27 | } 28 | 29 | const tags = toArray(frontmatter.tags || frontmatter.tag) 30 | const categories = toArray(frontmatter.categories || frontmatter.category) 31 | 32 | posts.push({ 33 | ...frontmatter, 34 | tags, 35 | categories, 36 | url, 37 | excerpt, 38 | }) 39 | } 40 | 41 | return posts 42 | }, 43 | }) 44 | 45 | function getPattern() { 46 | const dirs = new Set() 47 | 48 | if (config.site.themeConfig.pagination) { 49 | toArray(config.site.themeConfig.pagination).forEach((item) => { 50 | item.dir && toArray(item.dir).forEach((item) => dirs.add(item)) 51 | }) 52 | } 53 | if (config.site.locales.length) { 54 | Object.values(config.site.locales).forEach((locale) => { 55 | if (locale.themeConfig?.pagination) { 56 | toArray(locale.themeConfig.pagination).forEach((item) => { 57 | item.dir && toArray(item.dir).forEach((item) => dirs.add(item)) 58 | }) 59 | } 60 | }) 61 | } 62 | 63 | return dirs.size > 0 64 | ? [...dirs].map((item) => `${item}/*.md`) 65 | : `${config.userConfig.srcDir || '**'}/*.md` 66 | } 67 | -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | @import './vars.css'; 2 | @import './public.css'; 3 | -------------------------------------------------------------------------------- /src/styles/public.css: -------------------------------------------------------------------------------- 1 | #app { 2 | display: flex; 3 | flex-direction: column; 4 | min-height: 100vh; 5 | } 6 | 7 | .main { 8 | margin-left: auto; 9 | margin-right: auto; 10 | padding-left: var(--vp-size-space); 11 | padding-right: var(--vp-size-space); 12 | width: auto; 13 | max-width: var(--vp-size-main-width); 14 | } 15 | 16 | .VPNavScreen { 17 | z-index: calc(var(--vp-z-index-nav) + 1); 18 | } 19 | 20 | .article .vp-doc div[class*='language-'], 21 | .article .vp-code-group .tabs { 22 | margin-left: calc(0px - var(--vp-size-space)); 23 | margin-right: calc(0px - var(--vp-size-space)); 24 | } 25 | 26 | @media (min-width: 768px) { 27 | .main { 28 | padding-left: calc(var(--vp-size-space) * 2); 29 | padding-right: calc(var(--vp-size-space) * 2); 30 | } 31 | 32 | .article .vp-doc div[class*='language-'], 33 | .article .vp-code-group .tabs { 34 | margin-left: 0; 35 | margin-right: 0; 36 | } 37 | } 38 | 39 | @media (min-width: 960px) { 40 | .main { 41 | padding-left: 0 var(--vp-size-space); 42 | padding-right: 0 var(--vp-size-space); 43 | } 44 | } 45 | 46 | @media (min-width: 1100px) { 47 | .main { 48 | padding-left: 0; 49 | padding-right: 0; 50 | } 51 | } 52 | 53 | .page-enter-from { 54 | transform: scale(0.99); 55 | opacity: 0; 56 | } 57 | .page-leave-to { 58 | transform: scale(1.01); 59 | opacity: 0; 60 | } 61 | .page-enter-to, 62 | .page-leave-from { 63 | transform: scale(1); 64 | opacity: 1; 65 | } 66 | .page-enter-active, 67 | .page-leave-active { 68 | overflow: hidden; 69 | transition: var(--vp-transition-transform); 70 | } 71 | 72 | .posts-move, 73 | .posts-enter-active { 74 | transition: all var(--vp-transition-duration) 75 | cubic-bezier(0.15, 0.32, 0.61, 1.31); 76 | transition-delay: var(--vp-posts-delay); 77 | } 78 | .posts-enter-from, 79 | .posts-leave-to { 80 | opacity: 0; 81 | transform: scale(0.9) translateY(2rem); 82 | } 83 | .posts-leave-active { 84 | transition: all 0s; 85 | } 86 | .posts-leave-active { 87 | position: absolute; 88 | } 89 | 90 | .scale-enter-from, 91 | .scale-leave-to { 92 | transform: scale(0); 93 | } 94 | .scale-enter-to, 95 | .scale-leave-from { 96 | transform: scale(1); 97 | } 98 | .posts-move, 99 | .scale-enter-active, 100 | .scale-leave-active { 101 | transition: transform var(--vp-transition-duration) 102 | cubic-bezier(0.15, 0.32, 0.61, 1.31); 103 | } 104 | .scale-leave-active { 105 | position: absolute; 106 | } 107 | 108 | ::view-transition-old(root), 109 | ::view-transition-new(root) { 110 | animation: none; 111 | mix-blend-mode: normal; 112 | } 113 | ::view-transition-old(root), 114 | .dark::view-transition-new(root) { 115 | z-index: 1; 116 | } 117 | ::view-transition-new(root), 118 | .dark::view-transition-old(root) { 119 | z-index: 999999999; 120 | } 121 | -------------------------------------------------------------------------------- /src/styles/vars.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vp-size-main-width: 68.75rem; 3 | --vp-size-cover-height: 20rem; 4 | --vp-size-cover-img-height: 25rem; 5 | --vp-size-cover-blur: 7px; 6 | --vp-size-space: 0.7rem; 7 | 8 | --vp-c-transparent: transparent; 9 | 10 | --vp-posts-span: 6; 11 | --vp-posts-direction: column; 12 | --vp-posts-height: 12rem; 13 | 14 | --vp-transition-duration: 0.25s; 15 | --vp-transition-all: all var(--vp-transition-duration); 16 | --vp-transition-color: color var(--vp-transition-duration); 17 | --vp-transition-transform: transform var(--vp-transition-duration), 18 | opacity var(--vp-transition-duration); 19 | 20 | /* the vars of reading progress */ 21 | --vp-reading-size: 3px; 22 | --vp-reading-gradient: none; 23 | --vp-reading-z-index: 100; 24 | } 25 | 26 | @media (min-width: 768px) { 27 | :root { 28 | --vp-posts-span: 3; 29 | } 30 | } 31 | 32 | @media (min-width: 960px) { 33 | :root { 34 | --vp-posts-span: 2; 35 | } 36 | } 37 | 38 | @media (prefers-reduced-motion: reduce) { 39 | :root { 40 | --vp-transition-duration: 0s; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import theme from '../index' 2 | 3 | export default theme 4 | export * from './slots' 5 | export * from './theme' 6 | 7 | export type MaybeArray = T | T[] 8 | -------------------------------------------------------------------------------- /src/types/slots.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { PostsItem } from './index' 3 | 4 | type Frontmatter = Record 5 | 6 | export interface ArticleListSlots { 7 | 'article-item-top'?: (props: { item: PostsItem }) => any 8 | 'article-item-bottom'?: (props: { item: PostsItem }) => any 9 | } 10 | 11 | export interface TagListSlots { 12 | 'tag-item'?: (props: { tag: string; count: number }) => any 13 | } 14 | 15 | export interface HeaderSlots { 16 | 'header-left'?: () => any 17 | 'header-logo-after'?: () => any 18 | 'header-search-before'?: () => any 19 | 'header-right'?: () => any 20 | } 21 | 22 | export interface ArticleSlots extends ArticleListSlots { 23 | 'article-top'?: (props: { frontmatter: Frontmatter }) => any 24 | 'article-content-before'?: (props: { frontmatter: Frontmatter }) => any 25 | 'article-content-after'?: (props: { frontmatter: Frontmatter }) => any 26 | 'article-pagination-before'?: (props: { frontmatter: Frontmatter }) => any 27 | 'article-bottom'?: (props: { frontmatter: Frontmatter }) => any 28 | } 29 | 30 | export interface PageSlots extends ArticleListSlots { 31 | 'page-top'?: () => any 32 | 'page-pagination-before'?: () => any 33 | 'page-bottom'?: () => any 34 | } 35 | 36 | export interface TagSlots extends TagListSlots { 37 | 'tag-top'?: () => any 38 | 'tag-after'?: () => any 39 | 'tag-bottom'?: () => any 40 | } 41 | 42 | export interface NotFoundSlots { 43 | 'not-found-top'?: () => any 44 | 'not-found-bottom'?: () => any 45 | } 46 | 47 | export interface LayoutsSlots 48 | extends ArticleSlots, 49 | PageSlots, 50 | TagSlots, 51 | HeaderSlots, 52 | NotFoundSlots { 53 | 'header-before'?: () => any 54 | 'header-after'?: () => any 55 | 'footer-before'?: () => any 56 | 'footer-after'?: () => any 57 | } 58 | -------------------------------------------------------------------------------- /src/types/theme.ts: -------------------------------------------------------------------------------- 1 | import type { DefaultTheme } from 'vitepress' 2 | import type { MaybeArray } from './index' 3 | 4 | export interface Theme { 5 | /** 6 | * The logo of website 7 | * 8 | * @example { src: '/public/logo.png', alt: 'logo' } 9 | * @example { src: 'https://avatars.githubusercontent.com/u/23313167?v=4', alt: 'logo' } 10 | */ 11 | logo?: DefaultTheme.ThemeableImage 12 | /** 13 | * The cover image of pagination, tag, category pages 14 | * 15 | * @example { src: '/public/cover.png', alt: 'cover image' } 16 | */ 17 | cover?: DefaultTheme.ThemeableImage 18 | /** 19 | * The nav items. 20 | */ 21 | nav?: DefaultTheme.NavItem[] 22 | /** 23 | * The social links to be displayed at the end of the nav bar. Perfect for 24 | * placing links to social services such as GitHub, Twitter, Facebook, etc. 25 | */ 26 | socialLinks?: DefaultTheme.SocialLink[] 27 | /** 28 | * Customize how to paginate posts 29 | * 30 | * Before using, you need to add some files to your directory 31 | * 32 | * ```md 33 | * // [page].md 34 | * --- 35 | * layout: page 36 | * --- 37 | * ``` 38 | * 39 | * ```js 40 | * // [page].paths.ts 41 | * export default { 42 | * paths() { ... } 43 | * } 44 | * ``` 45 | * 46 | * Refer to [examples](https://github.com/tolking/vitepress-theme-ououe/blob/main/docs/%5Bpage%5D.paths.ts) for more information 47 | * 48 | * @example 49 | * ```ts 50 | * // When you only have a pagination in the root directory 51 | * { 52 | * group: 7, 53 | * // ... 54 | * } 55 | * // When you have multiple directories that require pagination 56 | * [ 57 | * { 58 | * match: (path) => /^\/($|index|page-)/.test(path), // match the root directory 59 | * // ... 60 | * }, 61 | * { 62 | * dir: 'posts', // match posts 63 | * // ... 64 | * }, 65 | * // ... 66 | * ] 67 | * ``` 68 | */ 69 | pagination?: MaybeArray 70 | /** 71 | * If `boolean`, whether to parse and include excerpt? (rendered as HTML) 72 | * 73 | * If `function`, control how the excerpt is extracted from the content. 74 | * 75 | * If `string`, define a custom separator to be used for extracting the 76 | * excerpt. Default separator is `---` if `excerpt` is `true`. 77 | * 78 | * @default true 79 | * 80 | * @example '' 81 | */ 82 | excerpt?: boolean | ExcerptFunction | string 83 | /** 84 | * Link of the tag page 85 | * 86 | * Before using, you need to add a files to your directory. (eq. tag.md -> '/tag') 87 | * 88 | * ``` 89 | * --- 90 | * layout: tag 91 | * --- 92 | * ``` 93 | * 94 | * @example '/tag' 95 | */ 96 | tag?: string 97 | /** 98 | * Link of the tag page 99 | * 100 | * Before using, you need to add a files to your directory. (eq. category.md -> '/category') 101 | * 102 | * ``` 103 | * --- 104 | * layout: category 105 | * --- 106 | * ``` 107 | * 108 | * @example '/category' 109 | */ 110 | category?: string 111 | /** 112 | * Whether to show the create time? 113 | * 114 | * @example { text: 'Create Time', format(date) { ... }} 115 | */ 116 | createTime?: TimeFormatOptions 117 | /** 118 | * Whether to show the last updated time? 119 | * 120 | * Before using, you need add `lastUpdated` options to your config 121 | * 122 | * @example { text: 'Last updated', format(date) { ... }} 123 | */ 124 | lastUpdated?: TimeFormatOptions 125 | /** 126 | * Whether to show the reading progress bar? 127 | * 128 | * @default 'top' 129 | */ 130 | readingProgress?: ReadingProgress 131 | /** 132 | * The footer configuration. 133 | */ 134 | footer?: { 135 | /** The copyright message of the footer */ 136 | copyright?: string 137 | /** The nav of the footer */ 138 | nav?: DefaultTheme.NavItemWithLink[] 139 | } 140 | /** 141 | * Whether to enable the local search function? 142 | * 143 | * https://vitepress.dev/reference/default-theme-search#local-search 144 | */ 145 | search?: 146 | | { provider: 'local'; options?: DefaultTheme.LocalSearchOptions } 147 | | { provider: 'algolia'; options: DefaultTheme.AlgoliaSearchOptions } 148 | } 149 | 150 | interface TimeFormatOptions { 151 | /** 152 | * Set custom time text 153 | * 154 | * @requires 155 | */ 156 | text: string 157 | /** Set custom time format, Default transition to local timestamp */ 158 | format?: (date: string | number) => string 159 | } 160 | 161 | export type ReadingProgress = boolean | 'top' | 'bottom' | 'left' | 'right' 162 | 163 | export interface PostsItem { 164 | url: string 165 | title?: string 166 | description?: string 167 | excerpt?: string 168 | date?: string 169 | image?: DefaultTheme.ThemeableImage 170 | tags: string[] 171 | categories: string[] 172 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 173 | [key: string]: any 174 | } 175 | 176 | // #region pagination 177 | export interface PaginationItem { 178 | /** 179 | * Pagination collapses when the total page count exceeds this value 180 | * 181 | * @default 5 182 | */ 183 | group?: number 184 | /** 185 | * The prev button text 186 | * 187 | * @default 'Prev' 188 | */ 189 | prev?: string 190 | /** 191 | * The next button text 192 | * 193 | * @default 'Next' 194 | */ 195 | next?: string 196 | /** 197 | * Directory that requires statistical pagination data. **It is best to have a unique value throughout the pagination config** 198 | * 199 | * default value: [srcDir](https://vitepress.dev/reference/site-config#srcdir) 200 | * 201 | * When your blog requires multiple pagination, you need to set dir to the current directory name 202 | */ 203 | dir?: MaybeArray 204 | /** 205 | * Customize how to match the path of route and current config 206 | * 207 | * If your pagination data is incorrect, you should increase it 208 | */ 209 | match?: (path: string) => boolean 210 | /** 211 | * Customize how to filter the posts data 212 | */ 213 | filter?: (item: PostsItem) => boolean 214 | /** 215 | * Customize how to sort the posts data 216 | */ 217 | sort?: (a: PostsItem, b: PostsItem) => number 218 | /** 219 | * Custom pagination jump link data 220 | */ 221 | formatPage?: (page: number) => DefaultTheme.NavItemWithLink 222 | } 223 | 224 | export interface PaginationParams { 225 | /** 226 | * File name for generating [dynamic-routes](https://vitepress.dev/guide/routing#dynamic-routes) 227 | * 228 | * @example n === 1 ? 'index' : `page-${n}` 229 | * 230 | * If you are not using the recommended format, you need to custom `pagination -> formatPage` match 231 | */ 232 | page: string 233 | /** 234 | * Current pagination, starting from 1 235 | * 236 | * @requires 237 | */ 238 | current: number 239 | /** 240 | * Item count of each page 241 | * 242 | * @requires 243 | */ 244 | limit: number 245 | } 246 | // #endregion pagination 247 | 248 | type ExcerptFunction = ( 249 | file: { 250 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 251 | data: { [key: string]: any } 252 | content: string 253 | excerpt?: string 254 | }, 255 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 256 | options?: any, 257 | ) => void 258 | -------------------------------------------------------------------------------- /src/utils/Classifiable.ts: -------------------------------------------------------------------------------- 1 | import type { PostsItem } from '../types/index' 2 | 3 | export class Classifiable { 4 | public tags: Record 5 | public categories: Record 6 | 7 | constructor(data: PostsItem[]) { 8 | const _tags: Record = {} 9 | const _categories: Record = {} 10 | 11 | data.forEach((item) => { 12 | item.tags.length && 13 | item.tags.forEach((tag) => { 14 | _tags[tag] = [...(_tags[tag] || []), item] 15 | }) 16 | item.categories.length && 17 | item.categories.forEach((category) => { 18 | _categories[category] = [...(_categories[category] || []), item] 19 | }) 20 | }) 21 | 22 | this.tags = _tags 23 | this.categories = _categories 24 | } 25 | 26 | getAllTags(filter: (item: PostsItem) => boolean) { 27 | return this.toCountMap('tags', filter) 28 | } 29 | 30 | getAllCategories(filter: (item: PostsItem) => boolean) { 31 | return this.toCountMap('categories', filter) 32 | } 33 | 34 | getPostsByTag(tag: string, filter: (item: PostsItem) => boolean) { 35 | return this.tags[tag]?.filter(filter) 36 | } 37 | 38 | getPostsByCategory(category: string, filter: (item: PostsItem) => boolean) { 39 | return this.categories[category]?.filter(filter) 40 | } 41 | 42 | toCountMap( 43 | type: 'tags' | 'categories', 44 | filter: (item: PostsItem) => boolean, 45 | ) { 46 | const obj: Record = {} 47 | 48 | for (const key in this[type]) { 49 | const len = this[type][key]?.filter(filter)?.length ?? 0 50 | 51 | if (len) { 52 | obj[key] = len 53 | } 54 | } 55 | 56 | return obj 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Classifiable' 2 | 3 | /* eslint-disable @typescript-eslint/no-explicit-any */ 4 | export function toArray( 5 | value: T, 6 | ): T extends any[] ? T : T[] { 7 | if (value === undefined) return [] 8 | return Array.isArray(value) ? value : ([value] as any) 9 | } 10 | 11 | export function isFunction(value: any): value is (...args: any[]) => any { 12 | return typeof value === 'function' 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "lib": ["esnext", "dom"] 13 | }, 14 | "include": ["src/**/*.ts", "src/**/*.vue"] 15 | } 16 | --------------------------------------------------------------------------------