├── .editorconfig
├── .example.env
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc.js
├── .vscode
├── extensions.json
├── post.code-snippets
└── settings.json
├── LICENSE
├── README.md
├── astro.config.ts
├── biome.json
├── package.json
├── pnpm-lock.yaml
├── postcss.config.cjs
├── public
├── admin
│ └── config.yml
├── icon.svg
├── icons
│ ├── bilibili.svg
│ ├── linux.do.svg
│ └── nodeseek.svg
└── social-card.avif
├── src
├── assets
│ ├── roboto-mono-700.ttf
│ └── roboto-mono-regular.ttf
├── components
│ ├── BaseHead.astro
│ ├── FormattedDate.astro
│ ├── Paginator.astro
│ ├── Search.astro
│ ├── SkipLink.astro
│ ├── SocialList.astro
│ ├── ThemeProvider.astro
│ ├── ThemeToggle.astro
│ ├── blog
│ │ ├── Masthead.astro
│ │ ├── PostPreview.astro
│ │ ├── TOC.astro
│ │ ├── TOCHeading.astro
│ │ └── webmentions
│ │ │ ├── Comments.astro
│ │ │ ├── Likes.astro
│ │ │ └── index.astro
│ ├── layout
│ │ ├── Footer.astro
│ │ └── Header.astro
│ └── note
│ │ └── Note.astro
├── content.config.ts
├── content
│ ├── note
│ │ └── demo.md
│ └── post
│ │ └── demo
│ │ ├── cover-image
│ │ ├── cover.png
│ │ └── index.md
│ │ ├── draft-post.md
│ │ ├── markdown-elements
│ │ ├── admonistions.md
│ │ ├── index.md
│ │ └── logo.png
│ │ └── social-image
│ │ ├── 1215191008.avif
│ │ └── index.md
├── data
│ └── post.ts
├── env.d.ts
├── layouts
│ ├── Base.astro
│ └── BlogPost.astro
├── pages
│ ├── 404.astro
│ ├── about.astro
│ ├── index.astro
│ ├── notes
│ │ ├── [...page].astro
│ │ ├── [...slug].astro
│ │ └── rss.xml.ts
│ ├── og-image
│ │ └── [...slug].png.ts
│ ├── posts
│ │ ├── [...page].astro
│ │ └── [...slug].astro
│ ├── rss.xml.ts
│ └── tags
│ │ ├── [tag]
│ │ └── [...page].astro
│ │ └── index.astro
├── plugins
│ ├── remark-admonitions.ts
│ └── remark-reading-time.ts
├── site.config.ts
├── styles
│ └── global.css
├── types.ts
└── utils
│ ├── date.ts
│ ├── domElement.ts
│ ├── generateToc.ts
│ └── webmentions.ts
├── tailwind.config.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | indent_style = space
6 | indent_size = 2
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
--------------------------------------------------------------------------------
/.example.env:
--------------------------------------------------------------------------------
1 | WEBMENTION_API_KEY=
2 | WEBMENTION_URL=
3 | WEBMENTION_PINGBACK=#optional
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | .output/
4 |
5 | # dependencies
6 | node_modules/
7 |
8 | # logs
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | pnpm-debug.log*
13 |
14 |
15 | # environment variables
16 | .env
17 | .env.production
18 |
19 | # macOS-specific files
20 | .DS_Store
21 |
22 | # misc
23 | *.pem
24 | .cache
25 | .astro
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | enable-pre-post-scripts=true
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | *.min.js
2 | node_modules
3 |
4 | # cache-dirs
5 | **/.cache
6 |
7 | pnpm-lock.yaml
8 | dist
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | /** @type {import("@types/prettier").Options} */
2 | module.exports = {
3 | printWidth: 100,
4 | semi: true,
5 | singleQuote: false,
6 | tabWidth: 2,
7 | useTabs: true,
8 | plugins: ["prettier-plugin-astro", "prettier-plugin-tailwindcss" /* Must come last */],
9 | overrides: [
10 | {
11 | files: "**/*.astro",
12 | options: {
13 | parser: "astro",
14 | },
15 | },
16 | {
17 | files: ["*.mdx", "*.md"],
18 | options: {
19 | printWidth: 80,
20 | },
21 | },
22 | ],
23 | };
24 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/post.code-snippets:
--------------------------------------------------------------------------------
1 | {
2 | // Place your astro-cactus workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
7 | // Placeholders with the same ids are connected.
8 | // Example:
9 | // "Print to console": {
10 | // "scope": "javascript,typescript",
11 | // "prefix": "log",
12 | // "body": [
13 | // "console.log('$1');",
14 | // "$2"
15 | // ],
16 | // "description": "Log output to console"
17 | // }
18 | "Add frontmatter to an Astro Cactus Post": {
19 | "scope": "markdown,mdx",
20 | "prefix": "frontmatter-post",
21 | "body": [
22 | "---",
23 | "title: ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}",
24 | "description: 'Please enter a description of your post here, between 50-160 chars!'",
25 | "publishDate: $CURRENT_DATE $CURRENT_MONTH_NAME $CURRENT_YEAR",
26 | "tags: []",
27 | "draft: false",
28 | "---",
29 | "$2",
30 | ],
31 | "description": "Add frontmatter for new Markdown post",
32 | },
33 | "Add frontmatter to an Astro Cactus Note": {
34 | "scope": "markdown,mdx",
35 | "prefix": "frontmatter-note",
36 | "body": [
37 | "---",
38 | "title: ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}",
39 | "description: 'Enter a description here (optional)'",
40 | "publishDate: \"${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}T${CURRENT_HOUR}:${CURRENT_MINUTE}:00Z\"",
41 | "---",
42 | "$2",
43 | ],
44 | "description": "Add frontmatter for a new Markdown note",
45 | },
46 | }
47 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[javascript]": { "editor.defaultFormatter": "biomejs.biome" },
3 | "[typescript]": { "editor.defaultFormatter": "biomejs.biome" },
4 | "[javascriptreact]": { "editor.defaultFormatter": "biomejs.biome" },
5 | "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" },
6 | "[json]": { "editor.defaultFormatter": "biomejs.biome" },
7 | "[jsonc]": { "editor.defaultFormatter": "biomejs.biome" },
8 | "editor.formatOnSave": true,
9 | "prettier.documentSelectors": ["**/*.astro"],
10 | "editor.codeActionsOnSave": {
11 | "source.organizeImports": "never",
12 | "source.organizeImports.biome": "explicit",
13 | "quickfix.biome": "explicit"
14 | },
15 | "[markdown]": {
16 | "editor.wordWrap": "on"
17 | },
18 | "typescript.tsdk": "node_modules/typescript/lib",
19 | "astro.content-intellisense": true
20 | }
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Chris Williams
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, 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,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | Astro 仙人掌
6 |
7 |
8 | Astro 仙人掌 是一个基于 Astro 框架的博客主题,使用 Astro 和 TailwindCSS。是 Astro Cactus 主题项目的中文汉化版。同时集成 **decap cms**,实现在线编辑、发布。
9 |
10 | 原主题地址: https://github.com/chrismwilliams/astro-theme-cactus
11 |
12 | ## 演示站点 💻
13 |
14 | 点击预览 [Demo](https://demo.343700.xyz/)
15 |
16 | ## 快速开始 🚀
17 |
18 | ### A、网页编辑模式
19 |
20 | 教学视频:[【零基础】【零成本】搭建一个属于自己的Astro博客网站](https://www.bilibili.com/video/BV18eCpYcEAk)
21 |
22 | 1. 点击 Fork 按钮,复制本项目到你的GitHub 仓库
23 | 2. [Vercel](vercel.com) 注册登录,关联 GitHub 账户,导入仓库
24 | 3. 添加一个[GitHub认证](https://github.com/settings/applications/new),得到 Oauth ID 和 secret
25 | - Homepage URL —— https://你的域名
26 | - Authorization callback URL —— https://域名/oauth/callback
27 | 4. 在 Vercel -> Settings -> Environment Variables,添加2个环境变量
28 | - OAUTH_GITHUB_CLIENT_ID -> Oauth ID
29 | - OAUTH_GITHUB_CLIENT_SECRET -> Oauth secret
30 | 5. 修改GitHub仓库 `public/admin/config.yml`,修改 `repo`、`site_domain`、`base_url`
31 | 6. 通过访问 `你的域名/admin` 访问博客后台,进行编辑、发布文章
32 |
33 |
34 |
35 | ### B、本地编辑模式
36 |
37 | 先完成【A、网页编辑模式】中的步骤,然后执行下面的步骤
38 |
39 | 1. 点击 Fork 按钮,复制本项目到你的GitHub 仓库,然后点击 Code 按钮,复制项目地址。
40 | 2. 本地电脑上执行下面代码,安装项目
41 | ```bash
42 | git clone https://github.com/your-username/astro-theme-cactus-zh-cn.git
43 |
44 | cd astro-theme-cactus-zh-cn
45 |
46 | pnpm install
47 | ```
48 | 3. 在 `src/content` 文件夹中,新建 markdown 文件,例如 `src/content/posts/hello-world.md`
49 | 4. 保存md文件,执行 git push 推送到远程仓库
50 |
51 | #### 命令
52 |
53 | | 命令 | 操作 |
54 | | :--------------- | :------------------------------------------------------------- |
55 | | `pnpm install` | 安装依赖项 |
56 | | `pnpm dev` | 在 `localhost:3000` 启动本地开发服务器 |
57 | | `pnpm build` | 将生产站点构建到 `./dist/` 目录下 |
58 | | `pnpm postbuild` | 执行 Pagefind 脚本,为博客文章构建静态搜索功能 |
59 | | `pnpm preview` | 在部署前本地预览构建结果 |
60 | | `pnpm sync` | 根据 `src/content/config.ts` 中的配置生成类型 |
61 |
62 | ## 个性化配置 ⚙
63 |
64 | - 修改导航栏标题,图片 -> `src/components/layout/Header.astro`
65 | - 修改网站配置 -> `src/site.config.ts`
66 | - 修改框架配置 -> `astro.config.ts`
67 | - 修改社交图标链接 -> `src/components/SocialList.astro`
68 |
69 |
70 | ## License
71 |
72 | MIT
73 |
--------------------------------------------------------------------------------
/astro.config.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import mdx from "@astrojs/mdx";
3 | import sitemap from "@astrojs/sitemap";
4 | import tailwind from "@astrojs/tailwind";
5 | import expressiveCode from "astro-expressive-code";
6 | import icon from "astro-icon";
7 | import robotsTxt from "astro-robots-txt";
8 | import webmanifest from "astro-webmanifest";
9 | import { defineConfig, envField } from "astro/config";
10 | import { expressiveCodeOptions } from "./src/site.config";
11 | import { siteConfig } from "./src/site.config";
12 | import vercel from "@astrojs/vercel";
13 |
14 | // Remark plugins
15 | import remarkDirective from "remark-directive"; // Handle ::: directives as nodes
16 | import { remarkAdmonitions } from "./src/plugins/remark-admonitions"; // Add admonitions
17 | import { remarkReadingTime } from "./src/plugins/remark-reading-time";
18 | import remarkMath from "remark-math"; // Add LaTeX support
19 | import remarkGemoji from "remark-gemoji"; // Add emoji support
20 |
21 | // Rehype plugins
22 | import rehypeExternalLinks from "rehype-external-links";
23 | import rehypeUnwrapImages from "rehype-unwrap-images";
24 | import rehypeKatex from "rehype-katex"; // Render LaTeX with KaTeX
25 |
26 |
27 | import decapCmsOauth from "astro-decap-cms-oauth";
28 |
29 | // https://astro.build/config
30 | export default defineConfig({
31 | output: 'server',
32 | adapter: vercel(),
33 | image: {
34 | domains: ["webmention.io"],
35 | },
36 | integrations: [expressiveCode(expressiveCodeOptions), icon({
37 | iconDir: "public/icons", // 修改:指定自定义图标目录 name = svg文件名
38 | }), tailwind({
39 | applyBaseStyles: false,
40 | nesting: true,
41 | }), sitemap(), mdx(), robotsTxt(), webmanifest({
42 | // See: https://github.com/alextim/astro-lib/blob/main/packages/astro-webmanifest/README.md
43 | /**
44 | * required
45 | **/
46 | name: siteConfig.title,
47 | /**
48 | * optional
49 | **/
50 | short_name: "仙人掌主题",
51 | description: siteConfig.description,
52 | lang: siteConfig.lang,
53 | icon: "public/icon.svg", // the source for generating favicon & icons
54 | icons: [
55 | {
56 | src: "icons/apple-touch-icon.png", // used in src/components/BaseHead.astro L:26
57 | sizes: "180x180",
58 | type: "image/png",
59 | },
60 | {
61 | src: "icons/icon-192.png",
62 | sizes: "192x192",
63 | type: "image/png",
64 | },
65 | {
66 | src: "icons/icon-512.png",
67 | sizes: "512x512",
68 | type: "image/png",
69 | },
70 | ],
71 | start_url: "/",
72 | background_color: "#1d1f21",
73 | theme_color: "#2bbc8a",
74 | display: "standalone",
75 | config: {
76 | insertFaviconLinks: false,
77 | insertThemeColorMeta: false,
78 | insertManifestLink: false,
79 | },
80 | }), decapCmsOauth()],
81 | markdown: {
82 | rehypePlugins: [
83 | [
84 | rehypeExternalLinks,
85 | {
86 | rel: ["nofollow, noreferrer"],
87 | target: "_blank",
88 | },
89 | ],
90 | rehypeUnwrapImages,
91 | rehypeKatex, // 添加 KaTeX 用于 LaTeX 渲染
92 | ],
93 | remarkPlugins: [
94 | remarkReadingTime,
95 | remarkDirective,
96 | remarkAdmonitions,
97 | remarkMath, // 添加 LaTeX 功能
98 | remarkGemoji, // 添加 emoji 功能
99 | ],
100 | remarkRehype: {
101 | footnoteLabelProperties: {
102 | className: [""],
103 | },
104 | footnoteLabel: '脚注:',
105 | },
106 | },
107 | // https://docs.astro.build/en/guides/prefetch/
108 | prefetch: {
109 | defaultStrategy: 'viewport',
110 | prefetchAll: true,
111 | },
112 | // ! 改为你的网站地址,不然社交图片无法加载
113 | site: "https://demo.343700.xyz/",
114 | vite: {
115 | optimizeDeps: {
116 | exclude: ["@resvg/resvg-js"],
117 | },
118 | plugins: [rawFonts([".ttf", ".woff"])],
119 | },
120 | env: {
121 | schema: {
122 | WEBMENTION_API_KEY: envField.string({ context: "server", access: "secret", optional: true }),
123 | WEBMENTION_URL: envField.string({ context: "client", access: "public", optional: true }),
124 | WEBMENTION_PINGBACK: envField.string({ context: "client", access: "public", optional: true }),
125 | },
126 | },
127 | });
128 |
129 | function rawFonts(ext: string[]) {
130 | return {
131 | name: "vite-plugin-raw-fonts",
132 | // @ts-expect-error:next-line
133 | transform(_, id) {
134 | if (ext.some((e) => id.endsWith(e))) {
135 | const buffer = fs.readFileSync(id);
136 | return {
137 | code: `export default ${JSON.stringify(buffer)}`,
138 | map: null,
139 | };
140 | }
141 | },
142 | };
143 | }
144 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
3 | "formatter": {
4 | "indentStyle": "tab",
5 | "indentWidth": 2,
6 | "lineWidth": 100,
7 | "formatWithErrors": true,
8 | "ignore": ["*.astro"]
9 | },
10 | "organizeImports": {
11 | "enabled": true
12 | },
13 | "linter": {
14 | "enabled": true,
15 | "rules": {
16 | "recommended": true,
17 | "a11y": {
18 | "noSvgWithoutTitle": "off"
19 | },
20 | "suspicious": {
21 | "noExplicitAny": "warn"
22 | }
23 | }
24 | },
25 | "javascript": {
26 | "formatter": {
27 | "trailingCommas": "all",
28 | "semicolons": "always"
29 | }
30 | },
31 | "vcs": {
32 | "clientKind": "git",
33 | "enabled": true,
34 | "useIgnoreFile": true
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "version": "5.0.0",
4 | "scripts": {
5 | "dev": "astro dev",
6 | "start": "astro dev",
7 | "build": "astro build",
8 | "postbuild": "pagefind --site dist/client --output-path .vercel/output/static/pagefind",
9 | "preview": "astro preview",
10 | "lint": "biome lint .",
11 | "format": "pnpm run format:code && pnpm run format:imports",
12 | "format:code": "biome format . --write && prettier -w \"**/*\" \"!**/*.{md,mdx}\" --ignore-unknown --cache",
13 | "format:imports": "biome check --formatter-enabled=false --write",
14 | "check": "astro check"
15 | },
16 | "dependencies": {
17 | "@astrojs/mdx": "4.0.1",
18 | "@astrojs/rss": "4.0.10",
19 | "@astrojs/sitemap": "3.2.1",
20 | "@astrojs/tailwind": "5.1.3",
21 | "@astrojs/vercel": "^8.0.1",
22 | "astro": "5.0.3",
23 | "astro-decap-cms-oauth": "^0.5.1",
24 | "astro-expressive-code": "^0.38.3",
25 | "astro-icon": "^1.1.4",
26 | "astro-robots-txt": "^1.0.0",
27 | "astro-webmanifest": "^1.0.0",
28 | "cssnano": "^7.0.6",
29 | "hastscript": "^9.0.0",
30 | "mdast-util-directive": "^3.0.0",
31 | "mdast-util-to-markdown": "^2.1.2",
32 | "mdast-util-to-string": "^4.0.0",
33 | "rehype-external-links": "^3.0.0",
34 | "rehype-unwrap-images": "^1.0.0",
35 | "remark-directive": "^3.0.0",
36 | "satori": "0.12.0",
37 | "satori-html": "^0.3.2",
38 | "sharp": "^0.33.5",
39 | "unified": "^11.0.5",
40 | "unist-util-visit": "^5.0.0",
41 | "rehype-katex": "^7.0.1",
42 | "remark-gemoji": "^8.0.0",
43 | "remark-math": "^6.0.0"
44 | },
45 | "devDependencies": {
46 | "@astrojs/check": "^0.9.4",
47 | "@biomejs/biome": "^1.9.4",
48 | "@iconify-json/mdi": "^1.2.1",
49 | "@pagefind/default-ui": "^1.2.0",
50 | "@resvg/resvg-js": "^2.6.2",
51 | "@tailwindcss/typography": "^0.5.15",
52 | "@types/hast": "^3.0.4",
53 | "@types/mdast": "^4.0.4",
54 | "autoprefixer": "^10.4.20",
55 | "pagefind": "^1.2.0",
56 | "prettier": "^3.4.2",
57 | "prettier-plugin-astro": "0.14.1",
58 | "prettier-plugin-tailwindcss": "^0.6.9",
59 | "reading-time": "^1.5.0",
60 | "tailwindcss": "^3.4.16",
61 | "typescript": "^5.7.2"
62 | },
63 | "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c"
64 | }
65 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require("autoprefixer"),
4 | ...(process.env.NODE_ENV === "production" ? [require("cssnano")] : []),
5 | ],
6 | };
7 |
--------------------------------------------------------------------------------
/public/admin/config.yml:
--------------------------------------------------------------------------------
1 | backend:
2 | name: github
3 | branch: main # change this to your branch
4 | repo: example/cactus # 1、change this to your repo
5 | site_domain: example.com # 2、change this to your domain
6 | base_url: https://example.com # 3、change this to your prod URL
7 | auth_endpoint: oauth # the oauth route provided by the integration
8 |
9 | collections:
10 | - name: "note"
11 | label: "笔记"
12 | folder: "src/content/note"
13 | create: true
14 | slug: "{{year}}.{{month}}.{{day}}_{{slug}}"
15 | fields:
16 | - { label: "标题", name: "title", widget: "string", required: true }
17 | - {
18 | label: "简介",
19 | name: "description",
20 | widget: "string",
21 | default: "这是一篇有意思的文章",
22 | required: true,
23 | }
24 | - {
25 | label: "发布日期",
26 | name: "publishDate",
27 | widget: "datetime",
28 | date_format: "YYYY-MM-DD",
29 | time_format: "HH:mm",
30 | required: true,
31 | }
32 | - { label: "正文", name: "body", widget: "markdown" }
33 |
34 | - name: "post"
35 | label: "博文"
36 | folder: "src/content/post"
37 | create: true
38 | slug: "{{year}}.{{month}}.{{day}}_{{slug}}"
39 | fields:
40 | - { label: "标题", name: "title", widget: "string", required: true }
41 | - {
42 | label: "简介",
43 | name: "description",
44 | widget: "string",
45 | default: "这是一篇有意思的文章",
46 | required: true,
47 | }
48 | - {
49 | label: "发布日期",
50 | name: "publishDate",
51 | widget: "datetime",
52 | date_format: "YYYY-MM-DD",
53 | required: true,
54 | }
55 | - { label: "标签", name: "tags", widget: "list", required: true }
56 | - {
57 | label: "ogImage",
58 | name: "ogImage",
59 | widget: "string",
60 | default: "/social-card.avif",
61 | required: true,
62 | }
63 | - { label: "正文", name: "body", widget: "markdown" }
64 |
65 | media_folder: "public/assets/images" # 文件将被存储在仓库中的位置
66 | public_folder: "/assets/images" # 上传媒体文件的 src 属性
67 |
68 |
--------------------------------------------------------------------------------
/public/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/bilibili.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/public/icons/linux.do.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icons/nodeseek.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/social-card.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zouzonghao/Astro-theme-Cactus-zh_CN/b25a7399c3fadca5b176dd8e6afca6d805c866ce/public/social-card.avif
--------------------------------------------------------------------------------
/src/assets/roboto-mono-700.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zouzonghao/Astro-theme-Cactus-zh_CN/b25a7399c3fadca5b176dd8e6afca6d805c866ce/src/assets/roboto-mono-700.ttf
--------------------------------------------------------------------------------
/src/assets/roboto-mono-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zouzonghao/Astro-theme-Cactus-zh_CN/b25a7399c3fadca5b176dd8e6afca6d805c866ce/src/assets/roboto-mono-regular.ttf
--------------------------------------------------------------------------------
/src/components/BaseHead.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { WEBMENTION_PINGBACK, WEBMENTION_URL } from "astro:env/client";
3 | import { siteConfig } from "@/site.config";
4 | import type { SiteMeta } from "@/types";
5 | import "@/styles/global.css";
6 |
7 | type Props = SiteMeta;
8 |
9 | const { articleDate, description, ogImage, title } = Astro.props;
10 |
11 | const titleSeparator = "•";
12 | const siteTitle = `${title} ${titleSeparator} ${siteConfig.title}`;
13 | const canonicalURL = new URL(Astro.url.pathname, Astro.site);
14 | const socialImageURL = new URL(ogImage ? ogImage : "/social-card.avif", Astro.url).href;
15 | ---
16 |
17 |
18 |
19 | {siteTitle}
20 |
21 | {/* Icons */}
22 |
23 | {
24 | import.meta.env.PROD && (
25 | <>
26 | {/* Favicon & Apple Icon */}
27 |
28 |
29 | {/* Manifest */}
30 |
31 | >
32 | )
33 | }
34 |
35 | {/* Canonical URL */}
36 |
37 |
38 | {/* Primary Meta Tags */}
39 |
40 |
41 |
42 |
43 | {/* Theme Colour */}
44 |
45 |
46 | {/* Open Graph / Facebook */}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | {
57 | articleDate && (
58 | <>
59 |
60 |
61 | >
62 | )
63 | }
64 |
65 | {/* Twitter */}
66 |
67 |
68 |
69 |
70 |
71 |
72 | {/* Sitemap */}
73 |
74 |
75 | {/* RSS auto-discovery */}
76 |
77 |
78 | {/* Webmentions */}
79 | {
80 | WEBMENTION_URL && (
81 | <>
82 |
83 | {WEBMENTION_PINGBACK && }
84 | >
85 | )
86 | }
87 |
88 |
89 |
--------------------------------------------------------------------------------
/src/components/FormattedDate.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getFormattedDate } from "@/utils/date";
3 | import type { HTMLAttributes } from "astro/types";
4 |
5 | type Props = HTMLAttributes<"time"> & {
6 | date: Date;
7 | dateTimeOptions?: Intl.DateTimeFormatOptions;
8 | };
9 |
10 | const { date, dateTimeOptions, ...attrs } = Astro.props;
11 |
12 | const postDate = getFormattedDate(date, dateTimeOptions);
13 | const ISO = date.toISOString();
14 | ---
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/components/Paginator.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { PaginationLink } from "@/types";
3 |
4 | interface Props {
5 | nextUrl?: PaginationLink;
6 | prevUrl?: PaginationLink;
7 | }
8 |
9 | const { nextUrl, prevUrl } = Astro.props;
10 | ---
11 |
12 | {
13 | (prevUrl || nextUrl) && (
14 | // 修改:翻页
15 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/Search.astro:
--------------------------------------------------------------------------------
1 | ---
2 | // Heavy inspiration taken from Astro Starlight -> https://github.com/withastro/starlight/blob/main/packages/starlight/components/Search.astro
3 |
4 | import "@pagefind/default-ui/css/ui.css";
5 | export const prerender = true;
6 | ---
7 |
8 |
9 |
31 |
56 |
57 |
58 |
137 |
138 |
196 |
197 |
206 |
--------------------------------------------------------------------------------
/src/components/SkipLink.astro:
--------------------------------------------------------------------------------
1 | skip to content
3 |
4 |
--------------------------------------------------------------------------------
/src/components/SocialList.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Icon } from "astro-icon/components";
3 |
4 | /**
5 | Uses https://www.astroicon.dev/getting-started/
6 | Find icons via guide: https://www.astroicon.dev/guides/customization/#open-source-icon-sets
7 | Only installed pack is: @iconify-json/mdi
8 | */
9 | const socialLinks: {
10 | friendlyName: string;
11 | isWebmention?: boolean;
12 | link: string;
13 | name: string;
14 | }[] = [
15 | {
16 | friendlyName: "Github",
17 | link: "https://github.com/zouzonghao",
18 | name: "mdi:github",
19 | },
20 | // 修改:添加其他社交平台链接,name 为 /public/icons/ 下的 svg 文件名 下的 svg 文件名
21 | {
22 | friendlyName: "Bilibili",
23 | link: "https://bilibili.com/",
24 | name: "bilibili",
25 | },
26 | {
27 | friendlyName: "Nodeseek",
28 | link: "https://www.nodeseek.com/",
29 | name: "nodeseek",
30 | },
31 | {
32 | friendlyName: "linux.do",
33 | link: "https://linux.do/",
34 | name: "linux.do",
35 | },
36 | ];
37 | ---
38 |
39 |
59 |
--------------------------------------------------------------------------------
/src/components/ThemeProvider.astro:
--------------------------------------------------------------------------------
1 | {/* Inlined to avoid FOUC. This is a parser blocking script. */}
2 |
48 |
--------------------------------------------------------------------------------
/src/components/ThemeToggle.astro:
--------------------------------------------------------------------------------
1 |
2 |
53 |
54 |
55 |
85 |
--------------------------------------------------------------------------------
/src/components/blog/Masthead.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Image } from "astro:assets";
3 | import type { CollectionEntry } from "astro:content";
4 | import FormattedDate from "@/components/FormattedDate.astro";
5 |
6 | interface Props {
7 | content: CollectionEntry<"post">;
8 | }
9 |
10 | const {
11 | content: { data },
12 | } = Astro.props;
13 |
14 | const dateTimeOptions: Intl.DateTimeFormatOptions = {
15 | month: "long",
16 | };
17 | ---
18 |
19 | {
20 | data.coverImage && (
21 |
22 |
29 |
30 | )
31 | }
32 | {data.draft ? (Draft) : null}
33 |
34 | {data.title}
35 |
36 |
37 |
38 | /{" "}
39 | {/* @ts-ignore:next-line. TODO: add reading time to collection schema? */}
40 | {data.readingTime}
41 |
42 | {
43 | data.updatedDate && (
44 |
45 | Updated:
46 |
47 |
48 | )
49 | }
50 |
51 | {
52 | !!data.tags?.length && (
53 |
54 |
71 | {data.tags.map((tag, i) => (
72 | <>
73 | {/* prettier-ignore */}
74 |
75 | {tag}
76 | {i < data.tags.length - 1 && ", "}
77 |
78 | >
79 | ))}
80 |
81 | )
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/blog/PostPreview.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { CollectionEntry } from "astro:content";
3 | import FormattedDate from "@/components/FormattedDate.astro";
4 | import type { HTMLTag, Polymorphic } from "astro/types";
5 |
6 | type Props = Polymorphic<{ as: Tag }> & {
7 | post: CollectionEntry<"post">;
8 | withDesc?: boolean;
9 | };
10 |
11 | const { as: Tag = "div", post, withDesc = false } = Astro.props;
12 | ---
13 |
14 |
15 |
19 |
20 | {post.data.draft && (Draft) }
21 |
22 | {post.data.title}
23 |
24 |
25 | {withDesc && {post.data.description}
}
26 |
--------------------------------------------------------------------------------
/src/components/blog/TOC.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { generateToc } from "@/utils/generateToc";
3 | import type { MarkdownHeading } from "astro";
4 | import TOCHeading from "./TOCHeading.astro";
5 |
6 | interface Props {
7 | headings: MarkdownHeading[];
8 | }
9 |
10 | const { headings } = Astro.props;
11 |
12 | const toc = generateToc(headings);
13 | ---
14 |
15 |
22 |
--------------------------------------------------------------------------------
/src/components/blog/TOCHeading.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { TocItem } from "@/utils/generateToc";
3 |
4 | interface Props {
5 | heading: TocItem;
6 | }
7 |
8 | const {
9 | heading: { children, depth, slug, text },
10 | } = Astro.props;
11 | ---
12 |
13 | 2 ? "ms-2" : ""}`}>
14 | #{text}
19 | {
20 | !!children.length && (
21 |
22 | {children.map((subheading) => (
23 |
24 | ))}
25 |
26 | )
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/src/components/blog/webmentions/Comments.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Image } from "astro:assets";
3 | import type { WebmentionsChildren } from "@/types";
4 | import { Icon } from "astro-icon/components";
5 |
6 | interface Props {
7 | mentions: WebmentionsChildren[];
8 | }
9 |
10 | const { mentions } = Astro.props;
11 |
12 | const validComments = ["mention-of", "in-reply-to"];
13 |
14 | const comments = mentions.filter(
15 | (mention) => validComments.includes(mention["wm-property"]) && mention.content?.text,
16 | );
17 |
18 | /**
19 | ! show a link to the mention
20 |
21 | */
22 | ---
23 |
24 | {
25 | !!comments.length && (
26 |
27 |
28 | {comments.length} Mention{comments.length > 1 ? "s" : ""}
29 |
30 |
31 | {comments.map((mention) => (
32 |
88 | ))}
89 |
90 |
91 | )
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/blog/webmentions/Likes.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Image } from "astro:assets";
3 | import type { WebmentionsChildren } from "@/types";
4 |
5 | interface Props {
6 | mentions: WebmentionsChildren[];
7 | }
8 |
9 | const { mentions } = Astro.props;
10 | const MAX_LIKES = 10;
11 |
12 | const likes = mentions.filter((mention) => mention["wm-property"] === "like-of");
13 | const likesToShow = likes
14 | .filter((like) => like.author?.photo && like.author.photo !== "")
15 | .slice(0, MAX_LIKES);
16 | ---
17 |
18 | {
19 | !!likes.length && (
20 |
21 |
22 | {likes.length}
23 | {likes.length > 1 ? " People" : " Person"} liked this
24 |
25 | {!!likesToShow.length && (
26 |
49 | )}
50 |
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/blog/webmentions/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getWebmentionsForUrl } from "@/utils/webmentions";
3 | import Comments from "./Comments.astro";
4 | import Likes from "./Likes.astro";
5 |
6 | const url = new URL(Astro.url.pathname, Astro.site);
7 |
8 | const webMentions = await getWebmentionsForUrl(`${url}`);
9 |
10 | // Return if no webmentions
11 | if (!webMentions.length) return;
12 | ---
13 |
14 |
15 | Webmentions for this post
16 |
17 |
18 |
19 |
20 |
21 | Responses powered by{" "}
22 | Webmentions
23 |
24 |
--------------------------------------------------------------------------------
/src/components/layout/Footer.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { menuLinks, siteConfig } from "@/site.config";
3 |
4 | const year = new Date().getFullYear();
5 | ---
6 |
7 |
29 |
--------------------------------------------------------------------------------
/src/components/layout/Header.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Search from "@/components/Search.astro";
3 | import ThemeToggle from "@/components/ThemeToggle.astro";
4 | import { menuLinks } from "@/site.config";
5 | ---
6 |
7 |
8 |
62 |
63 |
64 |
65 |
102 |
103 |
104 |
105 |
125 |
--------------------------------------------------------------------------------
/src/components/note/Note.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { type CollectionEntry, render } from "astro:content";
3 | import FormattedDate from "@/components/FormattedDate.astro";
4 | import type { HTMLTag, Polymorphic } from "astro/types";
5 |
6 | type Props = Polymorphic<{ as: Tag }> & {
7 | note: CollectionEntry<"note">;
8 | isPreview?: boolean | undefined;
9 | };
10 |
11 | const { as: Tag = "div", note, isPreview = false } = Astro.props;
12 | const { Content } = await render(note);
13 | ---
14 |
15 |
21 |
22 | {
23 | isPreview ? (
24 |
25 | {note.data.title}
26 |
27 | ) : (
28 | <>{note.data.title}>
29 | )
30 | }
31 |
32 |
33 |
43 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/content.config.ts:
--------------------------------------------------------------------------------
1 | import { defineCollection, z } from "astro:content";
2 | import { glob } from "astro/loaders";
3 |
4 | function removeDupsAndLowerCase(array: string[]) {
5 | return [...new Set(array.map((str) => str.toLowerCase()))];
6 | }
7 |
8 | const baseSchema = z.object({
9 | title: z.string().max(60),
10 | });
11 |
12 | const post = defineCollection({
13 | loader: glob({ base: "./src/content/post", pattern: "**/*.{md,mdx}" }),
14 | schema: ({ image }) =>
15 | baseSchema.extend({
16 | description: z.string(),
17 | coverImage: z
18 | .object({
19 | alt: z.string(),
20 | src: image(),
21 | })
22 | .optional(),
23 | draft: z.boolean().default(false),
24 | ogImage: z.string().optional(),
25 | tags: z.array(z.string()).default([]).transform(removeDupsAndLowerCase),
26 | publishDate: z
27 | .string()
28 | .or(z.date())
29 | .transform((val) => new Date(val)),
30 | updatedDate: z
31 | .string()
32 | .optional()
33 | .transform((str) => (str ? new Date(str) : undefined)),
34 | }),
35 | });
36 |
37 | const note = defineCollection({
38 | loader: glob({ base: "./src/content/note", pattern: "**/*.{md,mdx}" }),
39 | schema: baseSchema.extend({
40 | description: z.string().optional(),
41 | publishDate: z
42 | .string()
43 | // .datetime({ offset: true }) // Ensures ISO 8601 format with offsets allowed (e.g. "2024-01-01T00:00:00Z" and "2024-01-01T00:00:00+02:00")
44 | // .transform((val) => new Date(val)),
45 | .refine((val) => {
46 | // 修改:解析自定义格式的日期字符串,兼容 "YYYY-MM-DD HH:mm" 和 "YYYY-MM-DDTHH:mm"
47 | const datePattern = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}$/;
48 | return datePattern.test(val);
49 | }, "Invalid date format. Expected YYYY-MM-DD HH:mm or YYYY-MM-DDTHH:mm")
50 | .transform((val) => {
51 | // 统一处理分隔符,将 "T" 替换为空格
52 | const normalizedVal = val.replace("T", " ");
53 | const [datePart, timePart] = normalizedVal.split(" ");
54 | if (!datePart || !timePart) {
55 | throw new Error("Invalid date format. Expected YYYY-MM-DD HH:mm or YYYY-MM-DDTHH:mm");
56 | }
57 | const [year, month, day] = datePart.split("-");
58 | const [hour, minute] = timePart.split(":");
59 | if (!year || !month || !day || !hour || !minute) {
60 | throw new Error("Invalid date format. Expected YYYY-MM-DD HH:mm or YYYY-MM-DDTHH:mm");
61 | }
62 | return new Date(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute));
63 | }),
64 | }),
65 | });
66 |
67 | export const collections = { post, note };
68 |
--------------------------------------------------------------------------------
/src/content/note/demo.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 示例
3 | description: 一则笔记
4 | publishDate: "2024-12-13 16:35"
5 | ---
6 |
7 | 学海无涯苦做舟
8 |
--------------------------------------------------------------------------------
/src/content/post/demo/cover-image/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zouzonghao/Astro-theme-Cactus-zh_CN/b25a7399c3fadca5b176dd8e6afca6d805c866ce/src/content/post/demo/cover-image/cover.png
--------------------------------------------------------------------------------
/src/content/post/demo/cover-image/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "示例3 添加封面图"
3 | description: "这篇文章是如何添加封面的示例"
4 | publishDate: "1998-07-28"
5 | coverImage:
6 | src: "./cover.png"
7 | alt: "封面图"
8 | tags: ["示例"]
9 | ogImage: "/social-card.avif"
10 | ---
11 |
12 | ## 添加封面图
13 |
14 | 在 Front Matter(最上面用`---`包裹的内容)里,添加一个 `coverImage` 属性,并设置图片路径和描述。
15 |
16 | ```yaml
17 | ---
18 | title: "示例3 添加封面图"
19 | description: "这篇文章是如何添加封面的示例"
20 | publishDate: "1998-07-28"
21 | coverImage:
22 | src: "./cover.png"
23 | alt: "封面图"
24 | tags: ["示例"]
25 | ogImage: "/social-card.avif"
26 | ---
27 |
28 | ```
29 |
30 | ## 路径
31 |
32 | 将图片和文章放入相同文件夹下
33 |
34 | 通过 `./cover.png` 来引入图片
35 |
36 | 在文章中也是一样的
37 |
38 | 如:
39 |
40 | ```md
41 | 
42 | ```
43 |
44 | 显示:
45 |
46 | 
47 |
--------------------------------------------------------------------------------
/src/content/post/demo/draft-post.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "示例4 草稿"
3 | description: "This post is for testing the draft post functionality"
4 | publishDate: "1998-07-27"
5 | tags: ["示例"]
6 | draft: true
7 | ogImage: "/social-card.avif"
8 | ---
9 |
10 | 如果你的文章还未完成
11 |
12 | 在 Front Matter(最上面用`---`包裹的内容)里,添加一个 `draft` 属性,并设置为 `true`。
13 |
14 | 此时你的文章只会在运行 `pnpm run dev` 时可见,其他情况下会被忽略
15 |
16 | ```yaml
17 | ---
18 | title: "示例4 草稿"
19 | description: "This post is for testing the draft post functionality"
20 | publishDate: "1998-07-27"
21 | tags: ["示例"]
22 | draft: true
23 | ogImage: "/social-card.avif"
24 | ---
25 | ```
26 |
--------------------------------------------------------------------------------
/src/content/post/demo/markdown-elements/admonistions.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "示例2 Markdown 提示框"
3 | description: "本文展示了在仙人掌中使用 Markdown 提示框功能"
4 | publishDate: "1998-07-29"
5 | tags: ["示例"]
6 | ogImage: "/social-card.avif"
7 | ---
8 |
9 | ## 什么是提示框
10 |
11 | 提示框(也称为“侧边栏”)用于提供与内容相关的支持性和/或补充性信息。
12 |
13 | ## 如何使用
14 |
15 | 在 Astro Cactus 中使用提示框,将你的 Markdown 内容包裹在一对三冒号 `:::` 中。第一对冒号还应包含你想要使用的提示框类型。
16 |
17 | 例如,使用以下 Markdown:
18 |
19 | ```md
20 | :::note
21 | 突出显示用户应注意的信息,即使在快速浏览时也应留意。
22 | :::
23 | ```
24 |
25 | 输出:
26 |
27 | :::note
28 | 突出显示用户应注意的信息,即使在快速浏览时也应留意。
29 | :::
30 |
31 | ## 提示框类型
32 |
33 | 目前支持以下提示框类型:
34 |
35 | - `note`
36 | - `tip`
37 | - `important`
38 | - `warning`
39 | - `caution`
40 |
41 | ### 注意
42 |
43 | ```md
44 | :::note
45 | 突出显示用户应注意的信息,即使在快速浏览时也应留意。
46 | :::
47 | ```
48 |
49 | :::note
50 | 突出显示用户应注意的信息,即使在快速浏览时也应留意。
51 | :::
52 |
53 | ### 提示
54 |
55 | ```md
56 | :::tip
57 | 可选信息,帮助用户更成功。
58 | :::
59 | ```
60 |
61 | :::tip
62 | 可选信息,帮助用户更成功。
63 | :::
64 |
65 | ### 重要
66 |
67 | ```md
68 | :::important
69 | 用户成功所必需的关键信息。
70 | :::
71 | ```
72 |
73 | :::important
74 | 用户成功所必需的关键信息。
75 | :::
76 |
77 | ### 警告
78 |
79 | ```md
80 | :::warning
81 | 由于潜在风险,需要用户立即关注的关键内容。
82 | :::
83 | ```
84 |
85 | :::warning
86 | 由于潜在风险,需要用户立即关注的关键内容。
87 | :::
88 |
89 | ### 小心
90 |
91 | ```md
92 | :::caution
93 | 行动的负面潜在后果。
94 | :::
95 | ```
96 |
97 | :::caution
98 | 行动的负面潜在后果。
99 | :::
100 |
101 | ## 自定义提示框标题
102 |
103 | 你可以使用以下标记自定义提示框标题:
104 |
105 | ```md
106 | :::note[我的自定义标题]
107 | 这是一个带有自定义标题的提示框。
108 | :::
109 | ```
110 |
111 | 输出:
112 |
113 | :::note[我的自定义标题]
114 | 这是一个带有自定义标题的提示框。
115 | :::
116 |
--------------------------------------------------------------------------------
/src/content/post/demo/markdown-elements/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "示例1 Markdown 基本语法"
3 | description: "这篇文章用于测试和列出多种不同的Markdown元素"
4 | publishDate: "1998-07-30"
5 | tags: ["示例"]
6 | ogImage: "/social-card.avif"
7 | ---
8 |
9 | ## 这是一个H2标题
10 |
11 | ### 这是一个H3标题
12 |
13 | #### 这是一个H4标题
14 |
15 | ##### 这是一个H5标题
16 |
17 | ###### 这是一个H6标题
18 |
19 | ## 水平线
20 |
21 | ***
22 |
23 | ---
24 |
25 | ___
26 |
27 | ## 强调
28 |
29 | **这是粗体文本**
30 |
31 | _这是斜体文本_
32 |
33 | ~~删除线~~
34 |
35 | ## 引用
36 |
37 | "双引号" 和 '单引号'
38 |
39 | ## 块引用
40 |
41 | > 块引用也可以嵌套...
42 | >
43 | > > ...通过在每个块引用符号旁边使用额外的大于号...
44 |
45 | ## 参考文献
46 |
47 | 一个包含可点击参考文献[^1]并链接到来源的例子。
48 |
49 | 第二个包含参考文献[^2]并链接到来源的例子。
50 |
51 | [^1]: 第一个脚注的参考文献,带有返回内容的链接。
52 |
53 | [^2]: 第二个参考文献,带有一个链接。
54 |
55 | 如果你查看`src/content/post/markdown-elements/index.md`中的这个例子,你会发现参考文献和“脚注”标题是通过 [remark-rehype](https://github.com/remarkjs/remark-rehype#options) 插件添加到页面底部的。
56 |
57 | ## 列表
58 |
59 | 无序列表
60 |
61 | - 通过在行首使用 `+`, `-`, 或 `*` 来创建列表
62 | - 子列表通过缩进两个空格来实现:
63 | - 更改标记字符会强制开始新的列表:
64 | - Ac tristique libero volutpat at
65 | - Facilisis in pretium nisl aliquet
66 | - Nulla volutpat aliquam velit
67 | - 非常简单!
68 |
69 | 有序列表
70 |
71 | 1. Lorem ipsum dolor sit amet
72 | 2. Consectetur adipiscing elit
73 | 3. Integer molestie lorem at massa
74 |
75 | 4. 你可以使用连续的数字...
76 | 5. ...或者将所有数字都设为 `1.`
77 |
78 | 从偏移量开始编号:
79 |
80 | 57. foo
81 | 1. bar
82 |
83 | ## 代码
84 |
85 | 内联 `code`
86 |
87 | 缩进代码
88 |
89 | // Some comments
90 | line 1 of code
91 | line 2 of code
92 | line 3 of code
93 |
94 | 代码块 "fences"
95 |
96 | ```
97 | Sample text here...
98 | ```
99 |
100 | 语法高亮
101 |
102 | ```js
103 | var foo = function (bar) {
104 | return bar++;
105 | };
106 |
107 | console.log(foo(5));
108 | ```
109 |
110 | ### 表达性代码示例
111 |
112 | 添加标题
113 |
114 | ```js title="file.js"
115 | console.log("Title example");
116 | ```
117 |
118 | Bash终端
119 |
120 | ```bash
121 | echo "A base terminal example"
122 | ```
123 |
124 | 高亮代码行
125 |
126 | ```js title="line-markers.js" del={2} ins={3-4} {6}
127 | function demo() {
128 | console.log("this line is marked as deleted");
129 | // This line and the next one are marked as inserted
130 | console.log("this is the second inserted line");
131 |
132 | return "this line uses the neutral default marker type";
133 | }
134 | ```
135 |
136 | [Expressive Code](https://expressive-code.com/) 可以做比这里展示的多得多的事情,并且包括很多 [自定义选项](https://expressive-code.com/reference/configuration/)。
137 |
138 | ## 表格
139 |
140 | | Option | Description |
141 | | ------ | ------------------------------------------------------------------------- |
142 | | data | path to data files to supply the data that will be passed into templates. |
143 | | engine | engine to be used for processing templates. Handlebars is the default. |
144 | | ext | extension to be used for dest files. |
145 |
146 | ### 表格对齐
147 |
148 | | Item | Price | # In stock |
149 | | ------------ | :---: | ---------: |
150 | | Juicy Apples | 1.99 | 739 |
151 | | Bananas | 1.89 | 6 |
152 |
153 | ### 键盘元素
154 |
155 | | Action | Shortcut |
156 | | --------------------- | ------------------------------------------ |
157 | | Vertical split | Alt+Shift++ |
158 | | Horizontal split | Alt+Shift+- |
159 | | Auto split | Alt+Shift+d |
160 | | Switch between splits | Alt + arrow keys |
161 | | Resizing a split | Alt+Shift + arrow keys |
162 | | Close a split | Ctrl+Shift+W |
163 | | Maximize a pane | Ctrl+Shift+P + Toggle pane zoom |
164 |
165 | ## 图像
166 |
167 | 同一文件夹中的图像:`src/content/post/demo/markdown-elements/logo.png`
168 |
169 | 
170 |
171 | ## 链接
172 |
173 | [Markdown-it的内容](https://markdown-it.github.io/)
174 |
--------------------------------------------------------------------------------
/src/content/post/demo/markdown-elements/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zouzonghao/Astro-theme-Cactus-zh_CN/b25a7399c3fadca5b176dd8e6afca6d805c866ce/src/content/post/demo/markdown-elements/logo.png
--------------------------------------------------------------------------------
/src/content/post/demo/social-image/1215191008.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zouzonghao/Astro-theme-Cactus-zh_CN/b25a7399c3fadca5b176dd8e6afca6d805c866ce/src/content/post/demo/social-image/1215191008.avif
--------------------------------------------------------------------------------
/src/content/post/demo/social-image/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "示例5 社交图片卡片"
3 | publishDate: "1998-07-26"
4 | description: "详细说明如何在 frontmatter 中添加自定义社交图片卡片"
5 | tags: ["示例"]
6 | ogImage: "/social-card.avif"
7 | ---
8 |
9 | ## 什么是社交图片卡片?
10 |
11 | 社交图片,也称为 OG(OpenGraph) 图片
12 |
13 | 当你想要分享你的文章到社交平台时,你可能会看到一张由网站自动生成的卡片。
14 |
15 | 
16 |
17 | ## 添加自定义社交图片
18 |
19 | 在 Front Matter(最上面用`---`包裹的内容)里,添加一个 `ogImage` 属性,并设置路径(相对路径的根路径为 `src/public` 文件夹)。
20 |
21 | ```yaml
22 | ---
23 | title: "示例5 社交图片卡片"
24 | publishDate: "1998-07-26"
25 | description: "详细说明如何在 frontmatter 中添加自定义社交图片卡片"
26 | tags: ["示例"]
27 | ogImage: "/social-card.avif"
28 | ---
29 | ```
30 | ## 如果不添加`ogImage`属性(不推荐)
31 |
32 | 如果 `ogImage` 属性没有设置,则项目会在构建的时候,根据文章标题、标签生成一张社交图片。
33 |
34 | 通过编辑 `src/pages/og-image/[...slug].png.ts` 控制生成规则。
35 |
36 | 此举会增加构建时间,极大地增加构建体积。
37 |
38 | 推荐全部文章使用一张图片,作为默认的社交图片。
39 |
--------------------------------------------------------------------------------
/src/data/post.ts:
--------------------------------------------------------------------------------
1 | import { type CollectionEntry, getCollection } from "astro:content";
2 |
3 | /** filter out draft posts based on the environment */
4 | export async function getAllPosts(): Promise[]> {
5 | return await getCollection("post", ({ data }) => {
6 | return import.meta.env.PROD ? !data.draft : true;
7 | });
8 | }
9 |
10 | /** groups posts by year (based on option siteConfig.sortPostsByUpdatedDate), using the year as the key
11 | * Note: This function doesn't filter draft posts, pass it the result of getAllPosts above to do so.
12 | */
13 | export function groupPostsByYear(posts: CollectionEntry<"post">[]) {
14 | return posts.reduce[]>>((acc, post) => {
15 | const year = post.data.publishDate.getFullYear();
16 | if (!acc[year]) {
17 | acc[year] = [];
18 | }
19 | acc[year]?.push(post);
20 | return acc;
21 | }, {});
22 | }
23 |
24 | /** returns all tags created from posts (inc duplicate tags)
25 | * Note: This function doesn't filter draft posts, pass it the result of getAllPosts above to do so.
26 | * */
27 | export function getAllTags(posts: CollectionEntry<"post">[]) {
28 | return posts.flatMap((post) => [...post.data.tags]);
29 | }
30 |
31 | /** returns all unique tags created from posts
32 | * Note: This function doesn't filter draft posts, pass it the result of getAllPosts above to do so.
33 | * */
34 | export function getUniqueTags(posts: CollectionEntry<"post">[]) {
35 | return [...new Set(getAllTags(posts))];
36 | }
37 |
38 | /** returns a count of each unique tag - [[tagName, count], ...]
39 | * Note: This function doesn't filter draft posts, pass it the result of getAllPosts above to do so.
40 | * */
41 | export function getUniqueTagsWithCount(posts: CollectionEntry<"post">[]): [string, number][] {
42 | return [
43 | ...getAllTags(posts).reduce(
44 | (acc, t) => acc.set(t, (acc.get(t) ?? 0) + 1),
45 | new Map(),
46 | ),
47 | ].sort((a, b) => b[1] - a[1]);
48 | }
49 |
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | declare module "@pagefind/default-ui" {
2 | declare class PagefindUI {
3 | constructor(arg: unknown);
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/layouts/Base.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import BaseHead from "@/components/BaseHead.astro";
3 | import SkipLink from "@/components/SkipLink.astro";
4 | import ThemeProvider from "@/components/ThemeProvider.astro";
5 | import Footer from "@/components/layout/Footer.astro";
6 | import Header from "@/components/layout/Header.astro";
7 | import { siteConfig } from "@/site.config";
8 | import type { SiteMeta } from "@/types";
9 |
10 | interface Props {
11 | meta: SiteMeta;
12 | }
13 |
14 | const {
15 | meta: { articleDate, description = siteConfig.description, ogImage, title },
16 | } = Astro.props;
17 | ---
18 |
19 |
20 |
21 |
22 |
27 |
28 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/layouts/BlogPost.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { type CollectionEntry, render } from "astro:content";
3 |
4 | import Masthead from "@/components/blog/Masthead.astro";
5 | import TOC from "@/components/blog/TOC.astro";
6 | import WebMentions from "@/components/blog/webmentions/index.astro";
7 |
8 | import BaseLayout from "./Base.astro";
9 |
10 | interface Props {
11 | post: CollectionEntry<"post">;
12 | }
13 |
14 | const { post } = Astro.props;
15 | const { ogImage, title, description, updatedDate, publishDate } = post.data;
16 | const socialImage = ogImage ?? `/og-image/${post.id}.png`;
17 | const articleDate = updatedDate?.toISOString() ?? publishDate.toISOString();
18 | const { headings } = await render(post);
19 | ---
20 |
21 |
29 |
30 | {!!headings.length &&
}
31 |
32 |
33 |
34 |
37 |
38 |
39 |
40 |
41 |
42 |
60 |
61 |
62 |
80 |
--------------------------------------------------------------------------------
/src/pages/404.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import PageLayout from "@/layouts/Base.astro";
3 | export const prerender = true;
4 |
5 | const meta = {
6 | description: "Oops! It looks like this page is lost in space!",
7 | title: "Oops! You found a missing page!",
8 | };
9 | ---
10 |
11 |
12 |
116 | 404
这里什么也没有!
117 |
118 |
--------------------------------------------------------------------------------
/src/pages/about.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import PageLayout from "@/layouts/Base.astro";
3 | import SocialList from "@/components/SocialList.astro";
4 | export const prerender = true;
5 |
6 | const meta = {
7 | description: "仙人掌主题",
8 | // 修改:关于
9 | title: "关于",
10 | };
11 | ---
12 |
13 |
14 | 关于
15 |
16 |
17 |
18 | 随笔是一种自由的写作形式,它不受严格的格式和结构的限制,而是以轻松、随意的方式记录生活中的点滴。无论是对日常琐事的感悟,还是对某个瞬间的思考,随笔都能帮助我们捕捉那些容易被遗忘的瞬间。
19 |
20 |
21 | 在这里,你可以找到我对生活的观察、对世界的思考,以及对未来的期待。每一篇随笔都是一次心灵的旅程,记录着我在某个时刻的所思所感。
22 |
23 |
24 | 札记是一种系统化的记录方式,它帮助我们将学习到的知识、阅读的书籍、观看的电影等内容进行整理和总结。通过札记,我们可以更好地理解所学的内容,并将其转化为自己的知识储备。
25 |
26 |
27 | 在这个页面中,你会看到我对书籍、文章、电影等的摘录和思考。每一篇札记都是一次知识的积累,记录着我在学习过程中的收获与感悟。
28 |
29 |
30 | 随笔与札记,一个记录生活,一个记录知识。它们是我与世界对话的方式,也是我与自己对话的桥梁。希望这些文字能够带给你一些启发,或者至少让你感受到一丝共鸣。
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { type CollectionEntry, getCollection } from "astro:content";
3 | // import SocialList from "@/components/SocialList.astro";
4 | import PostPreview from "@/components/blog/PostPreview.astro";
5 | import Note from "@/components/note/Note.astro";
6 | import { getAllPosts } from "@/data/post";
7 | import PageLayout from "@/layouts/Base.astro";
8 | import { collectionDateSort } from "@/utils/date";
9 | export const prerender = true;
10 |
11 | // Posts
12 | const MAX_POSTS = 10;
13 | const allPosts = await getAllPosts();
14 | const allPostsByDate = allPosts
15 | .sort(collectionDateSort)
16 | .slice(0, MAX_POSTS) as CollectionEntry<"post">[];
17 |
18 | // Notes
19 | const MAX_NOTES = 0;
20 | const allNotes = await getCollection("note");
21 | const latestNotes = allNotes.sort(collectionDateSort).slice(0, MAX_NOTES);
22 | ---
23 |
24 |
25 |
26 |
27 |
36 |
37 |
38 |
39 | {
40 | allPostsByDate.map((p) => (
41 | -
42 |
43 |
44 | ))
45 | }
46 |
47 |
48 |
49 | 更多...
50 |
51 |
52 | {
53 | latestNotes.length > 0 && (
54 |
55 |
56 | 笔记
57 |
58 |
59 | {latestNotes.map((note) => (
60 | -
61 |
62 |
63 | ))}
64 |
65 |
66 | )
67 | }
68 |
69 |
--------------------------------------------------------------------------------
/src/pages/notes/[...page].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { type CollectionEntry, getCollection } from "astro:content";
3 | import Pagination from "@/components/Paginator.astro";
4 | import Note from "@/components/note/Note.astro";
5 | import PageLayout from "@/layouts/Base.astro";
6 | import { collectionDateSort } from "@/utils/date";
7 | import type { GetStaticPaths, Page } from "astro";
8 | import { Icon } from "astro-icon/components";
9 | export const prerender = true;
10 |
11 | export const getStaticPaths = (async ({ paginate }) => {
12 | const MAX_NOTES_PER_PAGE = 10;
13 | const allNotes = await getCollection("note");
14 | return paginate(allNotes.sort(collectionDateSort), { pageSize: MAX_NOTES_PER_PAGE });
15 | }) satisfies GetStaticPaths;
16 |
17 | interface Props {
18 | page: Page>;
19 | uniqueTags: string[];
20 | }
21 |
22 | const { page } = Astro.props;
23 |
24 | const meta = {
25 | description: "仙人掌主题",
26 | // 修改:笔记
27 | title: "笔记",
28 | };
29 |
30 | const paginationProps = {
31 | ...(page.url.prev && {
32 | prevUrl: {
33 | text: "← 上一页",
34 | url: page.url.prev,
35 | },
36 | }),
37 | ...(page.url.next && {
38 | nextUrl: {
39 | text: "下一页 →",
40 | url: page.url.next,
41 | },
42 | }),
43 | };
44 | ---
45 |
46 |
47 |
48 |
54 |
55 | {
56 | page.data.map((note) => (
57 | -
58 |
59 |
60 | ))
61 | }
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/pages/notes/[...slug].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getCollection } from "astro:content";
3 |
4 | import Note from "@/components/note/Note.astro";
5 | import PageLayout from "@/layouts/Base.astro";
6 | import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
7 | export const prerender = true;
8 |
9 | // if you're using an adaptor in SSR mode, getStaticPaths wont work -> https://docs.astro.build/en/guides/routing/#modifying-the-slug-example-for-ssr
10 | export const getStaticPaths = (async () => {
11 | const allNotes = await getCollection("note");
12 | return allNotes.map((note) => ({
13 | params: { slug: note.id },
14 | props: { note },
15 | }));
16 | }) satisfies GetStaticPaths;
17 |
18 | export type Props = InferGetStaticPropsType;
19 |
20 | const { note } = Astro.props;
21 |
22 | const meta = {
23 | description:
24 | note.data.description ||
25 | `Read about my note posted on: ${note.data.publishDate.toLocaleDateString()}`,
26 | title: note.data.title,
27 | };
28 | ---
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/pages/notes/rss.xml.ts:
--------------------------------------------------------------------------------
1 | import { getCollection } from "astro:content";
2 | import { siteConfig } from "@/site.config";
3 | import rss from "@astrojs/rss";
4 |
5 | export const GET = async () => {
6 | const notes = await getCollection("note");
7 |
8 | return rss({
9 | title: siteConfig.title,
10 | description: siteConfig.description,
11 | site: import.meta.env.SITE,
12 | items: notes.map((note) => ({
13 | title: note.data.title,
14 | pubDate: note.data.publishDate,
15 | link: `notes/${note.id}/`,
16 | })),
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/src/pages/og-image/[...slug].png.ts:
--------------------------------------------------------------------------------
1 | import RobotoMonoBold from "@/assets/roboto-mono-700.ttf";
2 | import RobotoMono from "@/assets/roboto-mono-regular.ttf";
3 | import { getAllPosts } from "@/data/post";
4 | import { siteConfig } from "@/site.config";
5 | import { getFormattedDate } from "@/utils/date";
6 | import { Resvg } from "@resvg/resvg-js";
7 | import type { APIContext, InferGetStaticPropsType } from "astro";
8 | import satori, { type SatoriOptions } from "satori";
9 | import { html } from "satori-html";
10 | export const prerender = true;
11 |
12 | const ogOptions: SatoriOptions = {
13 | // debug: true,
14 | fonts: [
15 | {
16 | data: Buffer.from(RobotoMono),
17 | name: "Roboto Mono",
18 | style: "normal",
19 | weight: 400,
20 | },
21 | {
22 | data: Buffer.from(RobotoMonoBold),
23 | name: "Roboto Mono",
24 | style: "normal",
25 | weight: 700,
26 | },
27 | ],
28 | height: 630,
29 | width: 1200,
30 | };
31 |
32 | const markup = (title: string, pubDate: string) =>
33 | html`
34 |
35 |
${pubDate}
36 |
${title}
37 |
38 |
39 |
40 |
55 |
${siteConfig.title}
56 |
57 |
by ${siteConfig.author}
58 |
59 |
`;
60 |
61 | type Props = InferGetStaticPropsType;
62 |
63 | export async function GET(context: APIContext) {
64 | const { pubDate, title } = context.props as Props;
65 |
66 | const postDate = getFormattedDate(pubDate, {
67 | month: "long",
68 | weekday: "long",
69 | });
70 | const svg = await satori(markup(title, postDate), ogOptions);
71 | const png = new Resvg(svg).render().asPng();
72 | return new Response(png, {
73 | headers: {
74 | "Cache-Control": "public, max-age=31536000, immutable",
75 | "Content-Type": "image/png",
76 | },
77 | });
78 | }
79 |
80 | export async function getStaticPaths() {
81 | const posts = await getAllPosts();
82 | return posts
83 | .filter(({ data }) => !data.ogImage)
84 | .map((post) => ({
85 | params: { slug: post.id },
86 | props: {
87 | pubDate: post.data.updatedDate ?? post.data.publishDate,
88 | title: post.data.title,
89 | },
90 | }));
91 | }
92 |
--------------------------------------------------------------------------------
/src/pages/posts/[...page].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { CollectionEntry } from "astro:content";
3 | import Pagination from "@/components/Paginator.astro";
4 | import PostPreview from "@/components/blog/PostPreview.astro";
5 | import { getAllPosts, getUniqueTags, groupPostsByYear } from "@/data/post";
6 | import PageLayout from "@/layouts/Base.astro";
7 | import { collectionDateSort } from "@/utils/date";
8 | import type { GetStaticPaths, Page } from "astro";
9 | import { Icon } from "astro-icon/components";
10 | export const prerender = true;
11 |
12 | export const getStaticPaths = (async ({ paginate }) => {
13 | // 修改:每页显示文章数,最多标签数
14 | const MAX_POSTS_PER_PAGE = 12;
15 | const MAX_TAGS = 7;
16 | const allPosts = await getAllPosts();
17 | const uniqueTags = getUniqueTags(allPosts).slice(0, MAX_TAGS);
18 | return paginate(allPosts.sort(collectionDateSort), {
19 | pageSize: MAX_POSTS_PER_PAGE,
20 | props: { uniqueTags },
21 | });
22 | }) satisfies GetStaticPaths;
23 |
24 | interface Props {
25 | page: Page>;
26 | uniqueTags: string[];
27 | }
28 |
29 | const { page, uniqueTags } = Astro.props;
30 |
31 | const meta = {
32 | description: "点点滴滴,江河湖海",
33 | // 修改:博客
34 | title: "博客",
35 | };
36 |
37 | const paginationProps = {
38 | ...(page.url.prev && {
39 | prevUrl: {
40 | text: "上一页",
41 | url: page.url.prev,
42 | },
43 | }),
44 | ...(page.url.next && {
45 | nextUrl: {
46 | text: "下一页",
47 | url: page.url.next,
48 | className: "font-bold text-blue-500 text-lg",
49 | },
50 | }),
51 | };
52 |
53 | const groupedByYear = groupPostsByYear(page.data);
54 | const descYearKeys = Object.keys(groupedByYear).sort((a, b) => +b - +a);
55 | ---
56 |
57 |
58 |
65 |
66 |
67 | {
68 | descYearKeys.map((yearKey) => (
69 | <>
70 | {/* 修改:注释博客按年分类 */}
71 | {/* {yearKey}
*/}
72 |
73 | {groupedByYear[yearKey]?.map((p) => (
74 | -
75 |
76 |
77 | ))}
78 |
79 | >
80 | ))
81 | }
82 |
83 |
84 | {
85 | !!uniqueTags.length && (
86 |
126 | )
127 | }
128 |
129 |
130 |
--------------------------------------------------------------------------------
/src/pages/posts/[...slug].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { render } from "astro:content";
3 | import { getAllPosts } from "@/data/post";
4 | import PostLayout from "@/layouts/BlogPost.astro";
5 | import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
6 | export const prerender = true;
7 |
8 | // if you're using an adaptor in SSR mode, getStaticPaths wont work -> https://docs.astro.build/en/guides/routing/#modifying-the-slug-example-for-ssr
9 | export const getStaticPaths = (async () => {
10 | const blogEntries = await getAllPosts();
11 | return blogEntries.map((post) => ({
12 | params: { slug: post.id },
13 | props: { post },
14 | }));
15 | }) satisfies GetStaticPaths;
16 |
17 | type Props = InferGetStaticPropsType;
18 |
19 | const { post } = Astro.props;
20 | const { Content } = await render(post);
21 | ---
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/pages/rss.xml.ts:
--------------------------------------------------------------------------------
1 | import { getAllPosts } from "@/data/post";
2 | import { siteConfig } from "@/site.config";
3 | import rss from "@astrojs/rss";
4 |
5 | export const GET = async () => {
6 | const posts = await getAllPosts();
7 |
8 | return rss({
9 | title: siteConfig.title,
10 | description: siteConfig.description,
11 | site: import.meta.env.SITE,
12 | items: posts.map((post) => ({
13 | title: post.data.title,
14 | description: post.data.description,
15 | pubDate: post.data.publishDate,
16 | link: `posts/${post.id}/`,
17 | })),
18 | });
19 | };
20 |
--------------------------------------------------------------------------------
/src/pages/tags/[tag]/[...page].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { CollectionEntry } from "astro:content";
3 | import Pagination from "@/components/Paginator.astro";
4 | import PostPreview from "@/components/blog/PostPreview.astro";
5 | import { getAllPosts, getUniqueTags } from "@/data/post";
6 | import PageLayout from "@/layouts/Base.astro";
7 | import { collectionDateSort } from "@/utils/date";
8 | import type { GetStaticPaths, Page } from "astro";
9 | export const prerender = true;
10 |
11 | export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
12 | const allPosts = await getAllPosts();
13 | const sortedPosts = allPosts.sort(collectionDateSort);
14 | const uniqueTags = getUniqueTags(sortedPosts);
15 |
16 | return uniqueTags.flatMap((tag) => {
17 | const filterPosts = sortedPosts.filter((post) => post.data.tags.includes(tag));
18 | return paginate(filterPosts, {
19 | pageSize: 10,
20 | params: { tag },
21 | });
22 | });
23 | };
24 |
25 | interface Props {
26 | page: Page>;
27 | }
28 |
29 | const { page } = Astro.props;
30 | const { tag } = Astro.params;
31 |
32 | const meta = {
33 | description: `关于 ${tag} 的全部`,
34 | title: `标签: ${tag}`,
35 | };
36 |
37 | const paginationProps = {
38 | ...(page.url.prev && {
39 | prevUrl: {
40 | text: "上一页",
41 | url: page.url.prev,
42 | },
43 | }),
44 | ...(page.url.next && {
45 | nextUrl: {
46 | text: "下一页",
47 | url: page.url.next,
48 | },
49 | }),
50 | };
51 | ---
52 |
53 |
54 |
55 | 标签
56 |
57 | #{tag}
58 |
59 |
60 |
61 | {
62 | page.data.map((p) => (
63 | -
64 |
65 |
66 | ))
67 | }
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/src/pages/tags/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getAllPosts, getUniqueTagsWithCount } from "@/data/post";
3 | import PageLayout from "@/layouts/Base.astro";
4 |
5 | const allPosts = await getAllPosts();
6 | const allTags = getUniqueTagsWithCount(allPosts);
7 | export const prerender = true;
8 |
9 | const meta = {
10 | description: "A list of all the topics I've written about in my posts",
11 | title: "All Tags",
12 | };
13 | ---
14 |
15 |
16 | 全部标签
17 |
18 | {
19 | // allTags.map(([tag, val]) => (
20 | allTags.map(([tag, _]) => (
21 | -
22 |
28 | #{tag}
29 |
30 | {/* 修改:隐藏标签博客数 */}
31 | {/* - {val}篇 */}
32 |
33 | ))
34 | }
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/plugins/remark-admonitions.ts:
--------------------------------------------------------------------------------
1 | import type { AdmonitionType } from "@/types";
2 | import { type Properties, h as _h } from "hastscript";
3 | import type { Node, Paragraph as P, Parent, PhrasingContent, Root } from "mdast";
4 | import type { Directives, LeafDirective, TextDirective } from "mdast-util-directive";
5 | import { directiveToMarkdown } from "mdast-util-directive";
6 | import { toMarkdown } from "mdast-util-to-markdown";
7 | import { toString as mdastToString } from "mdast-util-to-string";
8 | import type { Plugin } from "unified";
9 | import { visit } from "unist-util-visit";
10 |
11 | // Supported admonition types
12 | const Admonitions = new Set(["tip", "note", "important", "caution", "warning"]);
13 |
14 | /** Checks if a string is a supported admonition type. */
15 | function isAdmonition(s: string): s is AdmonitionType {
16 | return Admonitions.has(s as AdmonitionType);
17 | }
18 |
19 | /** Checks if a node is a directive. */
20 | function isNodeDirective(node: Node): node is Directives {
21 | return (
22 | node.type === "containerDirective" ||
23 | node.type === "leafDirective" ||
24 | node.type === "textDirective"
25 | );
26 | }
27 |
28 | /**
29 | * From Astro Starlight:
30 | * Transforms directives not supported back to original form as it can break user content and result in 'broken' output.
31 | */
32 | function transformUnhandledDirective(
33 | node: LeafDirective | TextDirective,
34 | index: number,
35 | parent: Parent,
36 | ) {
37 | const textNode = {
38 | type: "text",
39 | value: toMarkdown(node, { extensions: [directiveToMarkdown()] }),
40 | } as const;
41 | if (node.type === "textDirective") {
42 | parent.children[index] = textNode;
43 | } else {
44 | parent.children[index] = {
45 | children: [textNode],
46 | type: "paragraph",
47 | };
48 | }
49 | }
50 |
51 | /** From Astro Starlight: Function that generates an mdast HTML tree ready for conversion to HTML by rehype. */
52 | // biome-ignore lint/suspicious/noExplicitAny:
53 | function h(el: string, attrs: Properties = {}, children: any[] = []): P {
54 | const { properties, tagName } = _h(el, attrs);
55 | return {
56 | children,
57 | data: { hName: tagName, hProperties: properties },
58 | type: "paragraph",
59 | };
60 | }
61 |
62 | export const remarkAdmonitions: Plugin<[], Root> = () => (tree) => {
63 | visit(tree, (node, index, parent) => {
64 | if (!parent || index === undefined || !isNodeDirective(node)) return;
65 | if (node.type === "textDirective" || node.type === "leafDirective") {
66 | transformUnhandledDirective(node, index, parent);
67 | return;
68 | }
69 |
70 | const admonitionType = node.name;
71 | if (!isAdmonition(admonitionType)) return;
72 |
73 | let title: string = admonitionType;
74 | let titleNode: PhrasingContent[] = [{ type: "text", value: title }];
75 |
76 | // Check if there's a custom title
77 | const firstChild = node.children[0];
78 | if (
79 | firstChild?.type === "paragraph" &&
80 | firstChild.data &&
81 | "directiveLabel" in firstChild.data &&
82 | firstChild.children.length > 0
83 | ) {
84 | titleNode = firstChild.children;
85 | title = mdastToString(firstChild.children);
86 | // The first paragraph contains a custom title, we can safely remove it.
87 | node.children.splice(0, 1);
88 | }
89 |
90 | // Do not change prefix to AD, ADM, or similar, adblocks will block the content inside.
91 | const aside = h("aside", { "aria-label": title, class: `aside aside-${admonitionType}` }, [
92 | h("p", { class: "aside-title", "aria-hidden": "true" }, [...titleNode]),
93 | h("div", { class: "aside-content" }, node.children),
94 | ]);
95 |
96 | parent.children[index] = aside;
97 | });
98 | };
99 |
--------------------------------------------------------------------------------
/src/plugins/remark-reading-time.ts:
--------------------------------------------------------------------------------
1 | import { toString as mdastToString } from "mdast-util-to-string";
2 | import getReadingTime from "reading-time";
3 |
4 | export function remarkReadingTime() {
5 | // @ts-expect-error:next-line
6 | return (tree, { data }) => {
7 | const textOnPage = mdastToString(tree);
8 | const readingTime = getReadingTime(textOnPage);
9 | data.astro.frontmatter.readingTime = readingTime.text;
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/src/site.config.ts:
--------------------------------------------------------------------------------
1 | import type { SiteConfig } from "@/types";
2 | import type { AstroExpressiveCodeOptions } from "astro-expressive-code";
3 |
4 | export const siteConfig: SiteConfig = {
5 | // Used as both a meta property (src/components/BaseHead.astro L:31 + L:49) & the generated satori png (src/pages/og-image/[slug].png.ts)
6 | author: "仙人掌主题",
7 | // Date.prototype.toLocaleDateString() parameters, found in src/utils/date.ts.
8 | date: {
9 | locale: "zh-CN",
10 | options: {
11 | day: "numeric",
12 | month: "narrow",
13 | year: "numeric",
14 | },
15 | },
16 | // Used as the default description meta property and webmanifest description
17 | description: "仙人掌主题",
18 | // HTML lang property, found in src/layouts/Base.astro L:18 & astro.config.ts L:48
19 | lang: "zh-CN",
20 | // Meta property, found in src/components/BaseHead.astro L:42
21 | ogLocale: "zh-CN",
22 | // Used to construct the meta title property found in src/components/BaseHead.astro L:11, and webmanifest name found in astro.config.ts L:42
23 | title: "仙人掌主题",
24 | };
25 |
26 | // Used to generate links in both the Header & Footer.
27 | export const menuLinks: { path: string; title: string }[] = [
28 | // 修改:改为中文
29 | {
30 | path: "/",
31 | title: "主页",
32 | },
33 | {
34 | path: "/about/",
35 | title: "关于",
36 | },
37 | {
38 | path: "/posts/",
39 | title: "博客",
40 | },
41 | {
42 | path: "/notes/",
43 | title: "笔记",
44 | },
45 | ];
46 |
47 | // https://expressive-code.com/reference/configuration/
48 | export const expressiveCodeOptions: AstroExpressiveCodeOptions = {
49 | styleOverrides: {
50 | borderRadius: "4px",
51 | codeFontFamily:
52 | 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;',
53 | codeFontSize: "0.875rem",
54 | codeLineHeight: "1.7142857rem",
55 | codePaddingInline: "1rem",
56 | frames: {
57 | frameBoxShadowCssValue: "none",
58 | },
59 | uiLineHeight: "inherit",
60 | },
61 | themeCssSelector(theme, { styleVariants }) {
62 | // If one dark and one light theme are available
63 | // generate theme CSS selectors compatible with cactus-theme dark mode switch
64 | if (styleVariants.length >= 2) {
65 | const baseTheme = styleVariants[0]?.theme;
66 | const altTheme = styleVariants.find((v) => v.theme.type !== baseTheme?.type)?.theme;
67 | if (theme === baseTheme || theme === altTheme) return `[data-theme='${theme.type}']`;
68 | }
69 | // return default selector
70 | return `[data-theme="${theme.name}"]`;
71 | },
72 | // One dark, one light theme => https://expressive-code.com/guides/themes/#available-themes
73 | themes: ["dracula", "github-light"],
74 | useThemedScrollbars: false,
75 | };
76 |
--------------------------------------------------------------------------------
/src/styles/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root,
7 | :root[data-theme="light"] {
8 | color-scheme: light;
9 | /* https://tailwindcss.com/docs/customizing-colors#using-css-variables */
10 | --theme-bg: 0deg 0% 98%;
11 | --theme-link: 172deg 18% 41%;
12 | --theme-text: 203deg 11% 15%;
13 | --theme-accent: 351deg 66% 48%;
14 | --theme-accent-2: 0deg 0% 7%;
15 | --theme-quote: 351deg 66% 48%;
16 | }
17 |
18 | :root[data-theme="dark"] {
19 | color-scheme: dark;
20 | --theme-bg: 210deg 6% 12%;
21 | --theme-link: 330deg 49% 67%;
22 | --theme-text: 220deg 3% 79%;
23 | --theme-accent: 159deg 64% 45%;
24 | --theme-accent-2: 0deg 0% 93%;
25 | --theme-quote: 102deg 100% 86%;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface SiteConfig {
2 | author: string;
3 | date: {
4 | locale: string | string[] | undefined;
5 | options: Intl.DateTimeFormatOptions;
6 | };
7 | description: string;
8 | lang: string;
9 | ogLocale: string;
10 | title: string;
11 | }
12 |
13 | export interface PaginationLink {
14 | srLabel?: string;
15 | text?: string;
16 | url: string;
17 | }
18 |
19 | export interface SiteMeta {
20 | articleDate?: string | undefined;
21 | description?: string;
22 | ogImage?: string | undefined;
23 | title: string;
24 | }
25 |
26 | /** Webmentions */
27 | export interface WebmentionsFeed {
28 | children: WebmentionsChildren[];
29 | name: string;
30 | type: string;
31 | }
32 |
33 | export interface WebmentionsCache {
34 | children: WebmentionsChildren[];
35 | lastFetched: null | string;
36 | }
37 |
38 | export interface WebmentionsChildren {
39 | author: Author | null;
40 | content?: Content | null;
41 | "mention-of": string;
42 | name?: null | string;
43 | photo?: null | string[];
44 | published?: null | string;
45 | rels?: Rels | null;
46 | summary?: Summary | null;
47 | syndication?: null | string[];
48 | type: string;
49 | url: string;
50 | "wm-id": number;
51 | "wm-private": boolean;
52 | "wm-property": string;
53 | "wm-protocol": string;
54 | "wm-received": string;
55 | "wm-source": string;
56 | "wm-target": string;
57 | }
58 |
59 | export interface Author {
60 | name: string;
61 | photo: string;
62 | type: string;
63 | url: string;
64 | }
65 |
66 | export interface Content {
67 | "content-type": string;
68 | html: string;
69 | text: string;
70 | value: string;
71 | }
72 |
73 | export interface Rels {
74 | canonical: string;
75 | }
76 |
77 | export interface Summary {
78 | "content-type": string;
79 | value: string;
80 | }
81 |
82 | export type AdmonitionType = "tip" | "note" | "important" | "caution" | "warning";
83 |
--------------------------------------------------------------------------------
/src/utils/date.ts:
--------------------------------------------------------------------------------
1 | import type { CollectionEntry } from "astro:content";
2 | import { siteConfig } from "@/site.config";
3 |
4 | export function getFormattedDate(
5 | date: Date | undefined,
6 | options?: Intl.DateTimeFormatOptions,
7 | ): string {
8 | if (date === undefined) {
9 | return "Invalid Date";
10 | }
11 |
12 | return new Intl.DateTimeFormat(siteConfig.date.locale, {
13 | ...(siteConfig.date.options as Intl.DateTimeFormatOptions),
14 | ...options,
15 | }).format(date);
16 | }
17 |
18 | export function collectionDateSort(
19 | a: CollectionEntry<"post" | "note">,
20 | b: CollectionEntry<"post" | "note">,
21 | ) {
22 | return b.data.publishDate.getTime() - a.data.publishDate.getTime();
23 | }
24 |
--------------------------------------------------------------------------------
/src/utils/domElement.ts:
--------------------------------------------------------------------------------
1 | export function toggleClass(element: HTMLElement, className: string) {
2 | element.classList.toggle(className);
3 | }
4 |
5 | export function elementHasClass(element: HTMLElement, className: string) {
6 | return element.classList.contains(className);
7 | }
8 |
9 | export function rootInDarkMode() {
10 | return document.documentElement.getAttribute("data-theme") === "dark";
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/generateToc.ts:
--------------------------------------------------------------------------------
1 | // Heavy inspiration from starlight: https://github.com/withastro/starlight/blob/main/packages/starlight/utils/generateToC.ts
2 | import type { MarkdownHeading } from "astro";
3 |
4 | export interface TocItem extends MarkdownHeading {
5 | children: TocItem[];
6 | }
7 |
8 | interface TocOpts {
9 | maxHeadingLevel?: number | undefined;
10 | minHeadingLevel?: number | undefined;
11 | }
12 |
13 | /** Inject a ToC entry as deep in the tree as its `depth` property requires. */
14 | function injectChild(items: TocItem[], item: TocItem): void {
15 | const lastItem = items.at(-1);
16 | if (!lastItem || lastItem.depth >= item.depth) {
17 | items.push(item);
18 | } else {
19 | injectChild(lastItem.children, item);
20 | return;
21 | }
22 | }
23 |
24 | export function generateToc(
25 | headings: ReadonlyArray,
26 | { maxHeadingLevel = 4, minHeadingLevel = 2 }: TocOpts = {},
27 | ) {
28 | // by default this ignores/filters out h1 and h5 heading(s)
29 | const bodyHeadings = headings.filter(
30 | ({ depth }) => depth >= minHeadingLevel && depth <= maxHeadingLevel,
31 | );
32 | const toc: Array = [];
33 |
34 | for (const heading of bodyHeadings) injectChild(toc, { ...heading, children: [] });
35 |
36 | return toc;
37 | }
38 |
--------------------------------------------------------------------------------
/src/utils/webmentions.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "node:fs";
2 | import { WEBMENTION_API_KEY } from "astro:env/server";
3 | import type { WebmentionsCache, WebmentionsChildren, WebmentionsFeed } from "@/types";
4 |
5 | const DOMAIN = import.meta.env.SITE;
6 | const CACHE_DIR = ".data";
7 | const filePath = `${CACHE_DIR}/webmentions.json`;
8 | const validWebmentionTypes = ["like-of", "mention-of", "in-reply-to"];
9 |
10 | const hostName = new URL(DOMAIN).hostname;
11 |
12 | // Calls webmention.io api.
13 | async function fetchWebmentions(timeFrom: string | null, perPage = 1000) {
14 | if (!DOMAIN) {
15 | console.warn("No domain specified. Please set in astro.config.ts");
16 | return null;
17 | }
18 |
19 | if (!WEBMENTION_API_KEY) {
20 | console.warn("No webmention api token specified in .env");
21 | return null;
22 | }
23 |
24 | let url = `https://webmention.io/api/mentions.jf2?domain=${hostName}&token=${WEBMENTION_API_KEY}&sort-dir=up&per-page=${perPage}`;
25 |
26 | if (timeFrom) url += `&since${timeFrom}`;
27 |
28 | const res = await fetch(url);
29 |
30 | if (res.ok) {
31 | const data = (await res.json()) as WebmentionsFeed;
32 | return data;
33 | }
34 |
35 | return null;
36 | }
37 |
38 | // Merge cached entries [a] with fresh webmentions [b], merge by wm-id
39 | function mergeWebmentions(a: WebmentionsCache, b: WebmentionsFeed): WebmentionsChildren[] {
40 | return Array.from(
41 | [...a.children, ...b.children]
42 | .reduce((map, obj) => map.set(obj["wm-id"], obj), new Map())
43 | .values(),
44 | );
45 | }
46 |
47 | // filter out WebmentionChildren
48 | export function filterWebmentions(webmentions: WebmentionsChildren[]) {
49 | return webmentions.filter((webmention) => {
50 | // make sure the mention has a property so we can sort them later
51 | if (!validWebmentionTypes.includes(webmention["wm-property"])) return false;
52 |
53 | // make sure 'mention-of' or 'in-reply-to' has text content.
54 | if (webmention["wm-property"] === "mention-of" || webmention["wm-property"] === "in-reply-to") {
55 | return webmention.content && webmention.content.text !== "";
56 | }
57 |
58 | return true;
59 | });
60 | }
61 |
62 | // save combined webmentions in cache file
63 | function writeToCache(data: WebmentionsCache) {
64 | const fileContent = JSON.stringify(data, null, 2);
65 |
66 | // create cache folder if it doesn't exist already
67 | if (!fs.existsSync(CACHE_DIR)) {
68 | fs.mkdirSync(CACHE_DIR);
69 | }
70 |
71 | // write data to cache json file
72 | fs.writeFile(filePath, fileContent, (err) => {
73 | if (err) throw err;
74 | console.log(`Webmentions saved to ${filePath}`);
75 | });
76 | }
77 |
78 | function getFromCache(): WebmentionsCache {
79 | if (fs.existsSync(filePath)) {
80 | const data = fs.readFileSync(filePath, "utf-8");
81 | return JSON.parse(data);
82 | }
83 | // no cache found
84 | return {
85 | lastFetched: null,
86 | children: [],
87 | };
88 | }
89 |
90 | async function getAndCacheWebmentions() {
91 | const cache = getFromCache();
92 | const mentions = await fetchWebmentions(cache.lastFetched);
93 |
94 | if (mentions) {
95 | mentions.children = filterWebmentions(mentions.children);
96 | const webmentions: WebmentionsCache = {
97 | lastFetched: new Date().toISOString(),
98 | // Make sure the first arg is the cache
99 | children: mergeWebmentions(cache, mentions),
100 | };
101 |
102 | writeToCache(webmentions);
103 | return webmentions;
104 | }
105 |
106 | return cache;
107 | }
108 |
109 | let webMentions: WebmentionsCache;
110 |
111 | export async function getWebmentionsForUrl(url: string) {
112 | if (!webMentions) webMentions = await getAndCacheWebmentions();
113 |
114 | return webMentions.children.filter((entry) => entry["wm-target"] === url);
115 | }
116 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 | import { fontFamily } from "tailwindcss/defaultTheme";
3 | import plugin from "tailwindcss/plugin";
4 |
5 | export default {
6 | content: [
7 | "./src/**/*.{astro,html,js,jsx,md,svelte,ts,tsx,vue}",
8 | "!./src/pages/og-image/[slug].png.ts",
9 | ],
10 | corePlugins: {
11 | // disable some core plugins as they are included in the css, even when unused
12 | borderOpacity: false,
13 | fontVariantNumeric: false,
14 | ringOffsetColor: false,
15 | ringOffsetWidth: false,
16 | scrollSnapType: false,
17 | textOpacity: false,
18 | touchAction: false,
19 | },
20 | darkMode: ["class", '[data-theme="dark"]'],
21 | plugins: [
22 | require("@tailwindcss/typography"),
23 | plugin(({ addComponents }) => {
24 | addComponents({
25 | ".cactus-link": {
26 | "&:hover": {
27 | "@apply decoration-link decoration-2": {},
28 | },
29 | "@apply underline underline-offset-2": {},
30 | },
31 | ".title": {
32 | "@apply text-2xl font-semibold text-accent-2": {},
33 | },
34 | });
35 | }),
36 | ],
37 | theme: {
38 | extend: {
39 | colors: {
40 | accent: "hsl(var(--theme-accent) / )",
41 | "accent-2": "hsl(var(--theme-accent-2) / )",
42 | bgColor: "hsl(var(--theme-bg) / )",
43 | link: "hsl(var(--theme-link) / )",
44 | quote: "hsl(var(--theme-quote) / )",
45 | textColor: "hsl(var(--theme-text) / )",
46 | },
47 | fontFamily: {
48 | // Add any custom fonts here
49 | sans: [...fontFamily.sans],
50 | serif: [...fontFamily.serif],
51 | },
52 | transitionProperty: {
53 | height: "height",
54 | },
55 | // @ts-expect-error
56 | // Remove above once tailwindcss exposes theme type
57 | typography: (theme) => ({
58 | DEFAULT: {
59 | css: {
60 | a: {
61 | "@apply cactus-link": "",
62 | },
63 | blockquote: {
64 | borderLeftWidth: "0",
65 | },
66 | code: {
67 | border: "1px dotted #666",
68 | borderRadius: "2px",
69 | },
70 | kbd: {
71 | "@apply dark:bg-textColor": "",
72 | },
73 | hr: {
74 | borderTopStyle: "dashed",
75 | },
76 | strong: {
77 | fontWeight: "700",
78 | },
79 | sup: {
80 | "@apply ms-0.5": "",
81 | a: {
82 | "&:after": {
83 | content: "']'",
84 | },
85 | "&:before": {
86 | content: "'['",
87 | },
88 | "&:hover": {
89 | "@apply text-link no-underline bg-none": "",
90 | },
91 | "@apply bg-none": "",
92 | },
93 | },
94 | /* Table */
95 | "tbody tr": {
96 | borderBottomWidth: "none",
97 | },
98 | tfoot: {
99 | borderTop: "1px dashed #666",
100 | },
101 | thead: {
102 | borderBottomWidth: "none",
103 | },
104 | "thead th": {
105 | borderBottom: "1px dashed #666",
106 | fontWeight: "700",
107 | },
108 | 'th[align="center"], td[align="center"]': {
109 | "text-align": "center",
110 | },
111 | 'th[align="right"], td[align="right"]': {
112 | "text-align": "right",
113 | },
114 | 'th[align="left"], td[align="left"]': {
115 | "text-align": "left",
116 | },
117 | /* Admonitions/Aside */
118 | ".aside": {
119 | "--admonition-color": "var(--tw-prose-quotes)",
120 | "@apply my-4 py-4 ps-4 border-s-2 border-[--admonition-color]": "",
121 | ".aside-title": {
122 | "@apply font-bold text-base flex items-center gap-2 my-0 capitalize text-[--admonition-color]":
123 | "",
124 | "&:before": {
125 | "@apply inline-block shrink-0 overflow-visible h-4 w-4 align-middle content-[''] bg-[--admonition-color]":
126 | "",
127 | "mask-size": "contain",
128 | "mask-position": "center",
129 | "mask-repeat": "no-repeat",
130 | },
131 | },
132 | ".aside-content": {
133 | "> :last-child": {
134 | "@apply mb-0": "",
135 | },
136 | },
137 | },
138 | ".aside.aside-note": {
139 | "--admonition-color": theme("colors.blue.400"),
140 | "@apply bg-blue-400/5": "",
141 | ".aside-title": {
142 | "&:before": {
143 | maskImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath fill='var(--admonitions-color-tip)' d='M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'%3E%3C/path%3E%3C/svg%3E")`,
144 | },
145 | },
146 | },
147 | ".aside.aside-tip": {
148 | "--admonition-color": theme("colors.lime.500"),
149 | "@apply bg-lime-500/5": "",
150 | ".aside-title": {
151 | "&:before": {
152 | maskImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath d='M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z'%3E%3C/path%3E%3C/svg%3E")`,
153 | },
154 | },
155 | },
156 | ".aside.aside-important": {
157 | "--admonition-color": theme("colors.purple.400"),
158 | "@apply bg-purple-400/5": "",
159 | ".aside-title": {
160 | "&:before": {
161 | maskImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath d='M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z'%3E%3C/path%3E%3C/svg%3E")`,
162 | },
163 | },
164 | },
165 | ".aside.aside-warning": {
166 | "--admonition-color": theme("colors.orange.400"),
167 | "@apply bg-orange-400/5": "",
168 | ".aside-title": {
169 | "&:before": {
170 | maskImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath d='M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z'%3E%3C/path%3E%3C/svg%3E")`,
171 | },
172 | },
173 | },
174 | ".aside.aside-caution": {
175 | "--admonition-color": theme("colors.red.500"),
176 | "@apply bg-red-500/5": "",
177 | ".aside-title": {
178 | "&:before": {
179 | maskImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath d='M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'%3E%3C/path%3E%3C/svg%3E")`,
180 | },
181 | },
182 | },
183 | },
184 | },
185 | cactus: {
186 | css: {
187 | "--tw-prose-body": theme("colors.textColor / 1"),
188 | "--tw-prose-bold": theme("colors.textColor / 1"),
189 | "--tw-prose-bullets": theme("colors.textColor / 1"),
190 | "--tw-prose-code": theme("colors.textColor / 1"),
191 | "--tw-prose-headings": theme("colors.accent-2 / 1"),
192 | "--tw-prose-hr": "0.5px dashed #666",
193 | "--tw-prose-links": theme("colors.textColor / 1"),
194 | "--tw-prose-quotes": theme("colors.quote / 1"),
195 | "--tw-prose-th-borders": "#666",
196 | },
197 | },
198 | sm: {
199 | css: {
200 | code: {
201 | fontSize: theme("fontSize.sm")[0],
202 | fontWeight: "400",
203 | },
204 | },
205 | },
206 | }),
207 | },
208 | },
209 | } satisfies Config;
210 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strictest",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "lib": ["es2022", "dom", "dom.iterable"],
6 | "paths": {
7 | "@/*": ["src/*"]
8 | }
9 | },
10 | "include": [".astro/types.d.ts", "**/*"],
11 | "exclude": ["node_modules", "**/node_modules/*", ".vscode", "dist"]
12 | }
13 |
--------------------------------------------------------------------------------