Compare commits

...

90 Commits

Author SHA1 Message Date
a8e6ee073f feat: 添加 ICP 备案号。
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-30 09:02:43 +00:00
14719936fc fix: 头像图片编码问题.
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-01 20:47:17 +08:00
26350e033b build: 支持构建并上传网页到景安虚机。xN
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2023-03-01 20:00:36 +08:00
91d3acc358 build: 支持构建并上传网页到景安虚机。xN
Some checks failed
continuous-integration/drone/push Build is failing
2023-03-01 19:56:50 +08:00
e41238fb60 build: 支持构建并上传网页到景安虚机。xN
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-03-01 00:01:23 +08:00
404c7cba87 build: 支持构建并上传网页到景安虚机。xN
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2023-03-01 00:00:12 +08:00
930953fc1a build: 支持构建并上传网页到景安虚机。xN
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-28 22:40:59 +08:00
7ad3729ae0 build: 支持构建并上传网页到景安虚机。xN
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-28 22:31:51 +08:00
9a3297e1c7 build: 支持构建并上传网页到景安虚机。xN
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-28 22:25:59 +08:00
abb37dbcac build: 支持构建并上传网页到景安虚机。xN
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-28 21:56:12 +08:00
0549b3c385 build: 支持构建并上传网页到景安虚机。xN
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-02-28 21:49:51 +08:00
c2dca0e57b build: 支持构建并上传网页到景安虚机。x8
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-27 21:28:05 +08:00
1b05eae89b build: 支持构建并上传网页到景安虚机。x7
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2023-02-27 13:26:01 +00:00
c85737fa3f build: 支持构建并上传网页到景安虚机。x6.
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2023-02-27 21:22:56 +08:00
364b85cdc6 build: 支持构建并上传网页到景安虚机。x5.
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-27 09:29:35 +08:00
6e0b88bd1e build: 支持构建并上传网页到景安虚机。x4.
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-27 09:24:18 +08:00
d137dfac70 build: 支持构建并上传网页到景安虚机。x3.
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-27 00:07:48 +08:00
864085ea4a build: 支持构建并上传网页到景安虚机。x2
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-26 13:53:59 +00:00
16d5d1f32e build: 支持构建并上传网页到景安虚机。
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2023-02-26 13:17:16 +00:00
2f526f713c blog: React 18,新的严格模式.
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-07 15:50:03 +00:00
0b3bdfc36a blog: 简单调整。
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-23 12:28:18 +00:00
b2c2b0eb98 blog: 再见 2022,你好 2023
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-01 05:31:59 +00:00
97904d66b4 blog: 再见 2022,你好 2023
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-31 16:04:16 +00:00
61efce68b5 blog: 更新《安装并配置 Arch Linux》
Some checks failed
continuous-integration/drone/push Build is failing
2022-11-27 16:14:02 +00:00
de5081da69 blog: 完善。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-30 13:16:44 +00:00
8e0f1e4ff1 blog: 完善内容。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-29 05:57:41 +00:00
a8a1fe1e0d blog: 完善样式。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-29 02:30:36 +00:00
8d9020da3a build(dev): 开发容器迁移到 pnpm. 2022-10-29 02:15:14 +00:00
4cc2f920a1 blog: 完善内容。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-28 15:45:48 +00:00
7a2d689a4f blog: 完善内容。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-28 15:40:21 +00:00
7550421a74 blog: add image.
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-28 15:13:23 +00:00
6557412554 blog: 在 PVE 宿主机上使用桌面环境
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-28 15:06:06 +00:00
49ad571864 blog: 优化文章内的 CLI 命令。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-20 21:25:45 +08:00
1bde01a6a9 blog/arch-linux-quick-setup
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-20 20:31:58 +08:00
5bee02b567 blog: 补充遗漏的内容。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-19 23:57:51 +08:00
35b92490d8 build: remove .ssh mount.
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-19 20:53:59 +08:00
fd187a1370 fix: 文章语法错误。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-17 15:39:21 +00:00
c081d55a32 style: auto fix. 2022-10-17 15:37:01 +00:00
10f64a9ba4 blog: 安装并配置 Arch Linux
Some checks failed
continuous-integration/drone/push Build is failing
2022-10-17 14:24:48 +00:00
6f94a476c5 blog: typo.
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-16 16:52:35 +08:00
f9b127f2bc blog: 使用 NAT VPS 作为 SD-LAN 中继
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-14 18:49:28 +08:00
4d5fc7ef60 fix: comment 组件加载问题。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-09 19:35:33 +08:00
7f3474f8b5 fix: comment 组件加载问题。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-09 19:03:32 +08:00
7591d486f5 ci: 完善部署。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-09 14:44:32 +08:00
bcdc4ce087 ci: 完善部署。 2022-10-09 14:37:44 +08:00
0e44afcbea ci: 完善部署。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-09 13:25:29 +08:00
ea037d04e2 ci: 完善部署。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-09 13:16:59 +08:00
92590e849d feat: 完善部署。 2022-10-09 13:16:25 +08:00
0340f28993 feat: 完善部署。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-09 13:11:41 +08:00
05faf000cb feat: 完善部署。
Some checks failed
continuous-integration/drone/push Build is failing
2022-10-09 13:07:02 +08:00
c897b46f5c feat: 完善部署。
Some checks failed
continuous-integration/drone/push Build is failing
2022-10-09 13:05:57 +08:00
9cef9fe8d8 feat: 完善部署。
Some checks failed
continuous-integration/drone/push Build is failing
2022-10-09 13:04:51 +08:00
adb7b2cf3f Revert "fix: Dockerfile can not build."
Some checks failed
continuous-integration/drone/push Build is failing
This reverts commit b87470d051.
2022-10-09 11:50:16 +08:00
8d2406e3d5 feat: 更新 nextjs 到最新不卡 cpu 的版本。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-09 01:22:24 +08:00
66ea9d7df7 feat: 更新 nextjs 到最新不卡 cpu 的版本。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-09 01:17:01 +08:00
b87470d051 fix: Dockerfile can not build.
Some checks failed
continuous-integration/drone/push Build is failing
2022-10-09 01:07:44 +08:00
83440b09da fix: Dockerfile can not build.
Some checks failed
continuous-integration/drone/push Build is failing
2022-10-09 00:47:29 +08:00
541c9e6e8f fix: Dockerfile can not build.
Some checks failed
continuous-integration/drone/push Build is failing
2022-10-09 00:29:37 +08:00
727046805b fix: Dockerfile can not build.
Some checks failed
continuous-integration/drone/push Build is failing
2022-10-09 00:23:16 +08:00
2ed23c4327 fix: js 执行崩溃
Some checks failed
continuous-integration/drone/push Build is failing
- deps and types.
- use pnpm instead of npm.
- fix cpu 100%.
2022-10-08 22:45:31 +08:00
85fa2ae57f fix: 生产环境运行问题。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-08 08:23:13 +00:00
4738a03fb9 doc: 增加 30 天网站在线情况。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-08 00:12:24 +08:00
e64ece00ab fix: 修复 CI 清理 dev deps 失败。
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-07 23:58:05 +08:00
8f70c41086 fix: 改进 CI 速度。
Some checks failed
continuous-integration/drone/push Build is failing
2022-10-07 23:53:01 +08:00
2ee5810930 fix: 构建失败。
Some checks failed
continuous-integration/drone/push Build is failing
2022-10-07 23:09:30 +08:00
f1bbe539a7 fix: CI 安装依赖失败。
Some checks failed
continuous-integration/drone/push Build is failing
2022-10-07 22:20:15 +08:00
079ae47a30 build: 修复依赖安装问题。
Some checks failed
continuous-integration/drone/push Build is failing
2022-10-07 22:10:26 +08:00
ca5eb7cd5e feat: 添加并改用 commento 评论。
Some checks failed
continuous-integration/drone/push Build is failing
2022-10-07 22:04:38 +08:00
eee38148ee feat: 添加 cusdis 评论。
Some checks failed
continuous-integration/drone/push Build is failing
2022-10-07 17:12:50 +08:00
31a4cb3bd1 Merge pull request 'typescript. Close #1.' (#2) from typescript into master
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #2
2022-10-07 14:43:11 +08:00
ae952694d7 chore: update from master. 2022-10-07 14:40:51 +08:00
2bd6937564 doc: 恢复 README。 2022-10-07 14:14:21 +08:00
d9fbbc19e6 chore: update deps. 2022-10-07 14:09:53 +08:00
1dfd5e5271 refactor: 改用 TypeScript。close #1. 2022-10-07 13:55:39 +08:00
11b9017a07 blog: 利用 SNI 路由 TLS 连接实现端口复用。
All checks were successful
continuous-integration/drone Build is passing
2022-10-06 18:02:31 +08:00
d7c65cf444 feat: 显示文章 metadata 中的 images。
All checks were successful
continuous-integration/drone/push Build is passing
2022-09-24 01:55:41 +00:00
45af8732c0 feat: support dev container.
All checks were successful
continuous-integration/drone/push Build is passing
2022-09-24 00:23:38 +00:00
8c277bfd4d blog: 使用 Verdaccio 自建 Node 存储库。
All checks were successful
continuous-integration/drone/push Build is passing
2022-09-23 23:05:03 +08:00
0f86455590 fix(blog): 标题重复。
All checks were successful
continuous-integration/drone/push Build is passing
2022-09-18 00:18:21 +08:00
dae62eb4db blog: 在 PVE 中运行 Arch Linux。
Some checks are pending
continuous-integration/drone/push Build is running
2022-09-18 00:14:26 +08:00
c9b6220699 project(UPS): 添加 UPS 项目。
All checks were successful
continuous-integration/drone/push Build is passing
2022-09-10 18:41:52 +08:00
9427837151 blog: 利用 SNI 路由 TLS 连接实现端口复用。
All checks were successful
continuous-integration/drone/push Build is passing
2022-09-08 21:08:32 +08:00
168af0e9bc blog(draft): 利用 SNI 路由 TLS 连接实现端口复用。
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is failing
2022-09-08 17:14:42 +08:00
97c637f050 feat(blog): 使用 Xray、acme.sh、Docker Compose 搭建内网穿透服务。
All checks were successful
continuous-integration/drone/push Build is passing
2022-09-04 19:37:15 +08:00
0c74f2f7a6 fix: delete useless things.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-20 20:43:16 +08:00
7cc4c54ef6 feat: 你好,我的朋友!
Some checks reported errors
continuous-integration/drone/push Build encountered an error
continuous-integration/drone Build is failing
2022-07-19 23:06:55 +08:00
e8ea16d917 build: 修复缺少开发依赖导致程序无法运行的问题
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-19 11:56:51 +08:00
26e61bfd9b build: add notify for drone install pipline.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-18 22:36:20 +08:00
42fc2d9fac build: fix wrong name for docker image
Some checks are pending
continuous-integration/drone/push Build is running
2022-07-18 22:35:04 +08:00
d5aac3e833 build: fix wrong name for docker image 2022-07-18 22:34:27 +08:00
152 changed files with 13174 additions and 29982 deletions

14
.devcontainer/Dockerfile Normal file
View File

@ -0,0 +1,14 @@
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT=16-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
# [Optional] Uncomment if you want to install more global node packages
RUN su node -c "npm install -g pnpm"

View File

@ -0,0 +1,17 @@
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT=16-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
# Install tslint, typescript. eslint is installed by javascript image
ARG NODE_MODULES="tslint-to-eslint-config typescript"
COPY library-scripts/meta.env /usr/local/etc/vscode-dev-containers
RUN su node -c "umask 0002 && npm install -g ${NODE_MODULES}" \
&& npm cache clean --force > /dev/null 2>&1
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"

View File

@ -0,0 +1,55 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/typescript-node
{
"name": "Node.js & TypeScript",
"build": {
"dockerfile": "Dockerfile",
// Update 'VARIANT' to pick a Node version: 18, 16, 14.
// Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local on arm64/Apple Silicon.
"args": {
"VARIANT": "16-bullseye"
}
},
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"dbaeumer.vscode-eslint",
"formulahendry.auto-rename-tag",
"aaron-bond.better-comments",
"bierner.color-info",
"ldez.ignore-files",
"gooooloo.smartquote",
"wmaurer.change-case",
"streetsidesoftware.code-spell-checker",
"naumovs.color-highlight",
"EditorConfig.EditorConfig",
"mhutchie.git-graph",
"donjayamanne.githistory",
"ecmel.vscode-html-css",
"yzhang.markdown-all-in-one",
"christian-kohler.path-intellisense",
"esbenp.prettier-vscode",
"shardulm94.trailing-spaces",
"lihui.vs-color-picker",
"bradlc.vscode-tailwindcss"
]
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [3000],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node",
"features": {
"git": "os-provided",
"git-lfs": "latest",
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
"mounts": [],
"postStartCommand": "pnpm install && npm run dev"
}

View File

@ -35,4 +35,6 @@ yarn-error.log*
.env.test.local
.env.production.local
secrets.txt
secrets.txt
.pnpm-store

View File

@ -1,6 +1,6 @@
---
kind: pipeline
name: base
name: deps
type: docker
steps:
@ -12,23 +12,52 @@ steps:
from_secret: ivan-docker-username
password:
from_secret: ivan-docker-password
repo: docker-registry.ivanli.cc/ivan/gatsby-blog
repo: docker-registry.ivanli.cc/ivan/tailwind-nextjs-blog-deps
cache_from:
- docker-registry.ivanli.cc/ivan/gatsby-blog:${DRONE_BRANCH}${DRONE_TAG}-amd64
- docker-registry.ivanli.cc/ivan/tailwind-nextjs-blog-deps:${DRONE_BRANCH}${DRONE_TAG}-amd64
dockerfile: Dockerfile
build_args:
- BUILDKIT_INLINE_CACHE=1
target: base
target: deps
tags:
- '${DRONE_COMMIT_SHA:0:8}-amd64'
- '${DRONE_BRANCH}${DRONE_TAG}-amd64'
- name: notify
image: appleboy/drone-telegram
when:
status:
- failure
failure: ignore
detach: true
environment:
PLUGIN_TOKEN:
from_secret: drone-telegram-bot-token
PLUGIN_TO:
from_secret: telegram-notify-to
settings:
format: markdown
message: >
{{#success build.status}}
✅ Install Deps #{{build.number}} of `{{repo.name}}` succeeded.
📝 Commit by {{commit.author}} on `{{commit.branch}}`:
```
{{commit.message}}
```
🌐 {{ build.link }}
{{else}}
❌ Install Deps #{{build.number}} of `{{repo.name}}` failed.
📝 Commit by {{commit.author}} on `{{commit.branch}}`:
```
{{commit.message}}
```
🌐 {{ build.link }}
{{/success}}
---
kind: pipeline
name: linux-amd64
type: docker
depends_on:
- base
- deps
steps:
- name: build&publish
@ -39,17 +68,18 @@ steps:
from_secret: ivan-docker-username
password:
from_secret: ivan-docker-password
repo: docker-registry.ivanli.cc/ivan/gatsby-blog
repo: docker-registry.ivanli.cc/ivan/tailwind-nextjs-blog
dockerfile: Dockerfile
target: release
cache_from:
- docker-registry.ivanli.cc/ivan/gatsby-blog:${DRONE_COMMIT_SHA:0:8}-amd64
- docker-registry.ivanli.cc/ivan/tailwind-nextjs-blog-deps:${DRONE_BRANCH}${DRONE_TAG}-amd64
tags:
- '${DRONE_COMMIT_SHA:0:8}'
- '${DRONE_BRANCH}${DRONE_TAG}'
- name: notify
image: appleboy/drone-telegram
failure: ignore
detach: true
when:
status:
- success
@ -89,12 +119,12 @@ kind: pipeline
type: docker
name: deploy
clone:
disable: true
disable: false
depends_on:
- linux-amd64
steps:
- name: deploy
- name: watchtower-online
image: plugins/webhook
settings:
token_value:
@ -116,6 +146,7 @@ steps:
- success
- failure
failure: ignore
detach: true
environment:
PLUGIN_TOKEN:
from_secret: drone-telegram-bot-token
@ -139,3 +170,74 @@ steps:
```
🌐 {{ build.link }}
{{/success}}
---
kind: pipeline
type: docker
name: deploy-to-zzidc
clone:
disable: false
depends_on:
- linux-amd64
trigger:
branch:
- master
- develop
steps:
- name: upload
image: docker:dind
volumes:
- name: dockersock
path: /var/run/docker.sock
commands:
- docker pull docker-registry.ivanli.cc/ivan/tailwind-nextjs-blog-deps:${DRONE_BRANCH}${DRONE_TAG}-amd64
- docker build --pull=true --target upload -t docker-registry.ivanli.cc/ivan/tailwind-nextjs-blog-upload:${DRONE_COMMIT_SHA:0:8} --cache-from docker-registry.ivanli.cc/ivan/tailwind-nextjs-blog-deps:${DRONE_BRANCH}${DRONE_TAG}-amd64 .
- docker run --rm -t -e FTP_ACCOUNT=$${FTP_ACCOUNT} -e FTP_PASSWORD=$${FTP_PASSWORD} -e FTP_HOST=$${FTP_HOST} docker-registry.ivanli.cc/ivan/tailwind-nextjs-blog-upload:${DRONE_COMMIT_SHA:0:8}
environment:
DOCKER_BUILDKIT: "1"
FTP_ACCOUNT:
from_secret: zzidc_ftp_account
FTP_PASSWORD:
from_secret: zzidc_ftp_password
FTP_HOST:
from_secret: zzidc_ftp_host
- name: notify
image: appleboy/drone-telegram
when:
status:
- success
- failure
failure: ignore
detach: true
environment:
PLUGIN_TOKEN:
from_secret: drone-telegram-bot-token
PLUGIN_TO:
from_secret: telegram-notify-to
settings:
format: markdown
message: >
{{#success build.status}}
✅ Deploy #{{build.number}} of `{{repo.name}}` to ZZIDC succeeded.
📝 Commit by {{commit.author}} on `{{commit.branch}}`:
```
{{commit.message}}
```
🌐 {{ build.link }}
{{else}}
❌ Deploy #{{build.number}} of `{{repo.name}}` to ZZIDC failed.
📝 Commit by {{commit.author}} on `{{commit.branch}}`:
```
{{commit.message}}
```
🌐 {{ build.link }}
{{/success}}
volumes:
- name: dockersock
host:
path: /var/run/docker.sock

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

1
.env Normal file
View File

@ -0,0 +1 @@
NEXT_PUBLIC_COMMENTO_URL=https://comment.ivanli.cc/js/commento.js

View File

@ -4,6 +4,9 @@ NEXT_PUBLIC_GISCUS_CATEGORY=
NEXT_PUBLIC_GISCUS_CATEGORY_ID=
NEXT_PUBLIC_UTTERANCES_REPO=
NEXT_PUBLIC_DISQUS_SHORTNAME=
NEXT_PUBLIC_CUSDIS_APPID=
NEXT_PUBLIC_CUSDIS_HOST=
NEXT_PUBLIC_COMMENTO_URL=
MAILCHIMP_API_KEY=
@ -15,8 +18,8 @@ BUTTONDOWN_API_KEY=
CONVERTKIT_API_URL=https://api.convertkit.com/v3/
CONVERTKIT_API_KEY=
// curl https://api.convertkit.com/v3/forms?api_key=<your_public_api_key> to get your form ID
CONVERTKIT_FORM_ID=
// curl https://api.convertkit.com/v3/forms?api_key=<your_public_api_key> to get your form ID
CONVERTKIT_FORM_ID=
KLAVIYO_API_KEY=
KLAVIYO_LIST_ID=

4
.gitignore vendored
View File

@ -35,4 +35,6 @@ yarn-error.log*
.env.test.local
.env.production.local
secrets.txt
secrets.txt
.pnpm-store

6
.prettierrc.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
singleQuote: true,
trailingCommas: 'all',
bracketSpacing: true,
bracketSameLine: true,
};

32
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,32 @@
{
"cSpell.words": [
"alpn",
"appleboy",
"blackhole",
"BUTTONDOWN",
"Commento",
"CONVERTKIT",
"Cusdis",
"Discuz",
"Disqus",
"dokodemo",
"EMAILOCTOPUS",
"fullchain",
"Giscus",
"KLAVIYO",
"Kutt",
"lastmod",
"Logseq",
"MAILCHIMP",
"Miniflux",
"nextjs",
"Nuxt",
"outbounds",
"rprx",
"unist",
"vfile",
"VLESS",
"vmess",
"xtls"
]
}

View File

@ -1,13 +1,35 @@
FROM node:16-alpine as base
RUN npm i --location=global pnpm@7
FROM base as deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY pnpm-lock.yaml package.json ./
RUN pnpm i
FROM deps as build
WORKDIR /app
COPY package-lock.json package.json ./
RUN npm ci --no-audit
COPY . .
COPY --from=deps /app ./
RUN pnpm build
FROM build as pre-release
WORKDIR /app
RUN pnpm prune --prod --config.ignore-scripts=true
FROM node:16-alpine as release
WORKDIR /app
COPY --from=base /app ./
RUN npm run build &&\
npm prune --omit dev
COPY --from=pre-release /app ./
EXPOSE 80
CMD npm run serve -- --port 80
CMD npm run serve -- -p 80
FROM build as export
WORKDIR /app
RUN npm run export
FROM alpine:latest as upload
RUN apk add lftp
WORKDIR /app
COPY --from=export /app/out ./
CMD lftp -u "${FTP_ACCOUNT},${FTP_PASSWORD}" "${FTP_HOST}" -e 'set ftp:ssl-allow off && set use-feat no && mirror -c -R --use-pget-n=10 . ./WEB && exit'

156
README.md
View File

@ -1,142 +1,16 @@
![tailwind-nextjs-banner](/public/static/images/twitter-card.png)
# Tailwind Nextjs Starter Blog
# Ivan Li's Blog
[![GitHub Repo stars](https://img.shields.io/github/stars/timlrx/tailwind-nextjs-starter-blog?style=social)](https://GitHub.com/timlrx/tailwind-nextjs-starter-blog/stargazers/)
[![GitHub forks](https://img.shields.io/github/forks/timlrx/tailwind-nextjs-starter-blog?style=social)](https://GitHub.com/timlrx/tailwind-nextjs-starter-blog/network/)
[![Twitter URL](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Ftwitter.com%2Ftimlrxx)](https://twitter.com/timlrxx)
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&link=https://github.com/sponsors/timlrx)](https://github.com/sponsors/timlrx)
[![Build Status](https://ci.ivanli.cc/api/badges/Ivan/tailwind-nextjs-blog/status.svg)](https://ci.ivanli.cc/Ivan/tailwind-nextjs-blog)
[![Website Status](https://uptime.sg.ivanli.cc/api/badge/18/uptime/720?label=30&labelSuffix=d)](https://ivanli.cc)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/timlrx/tailwind-nextjs-starter-blog)
This is a [Next.js](https://nextjs.org/), [Tailwind CSS](https://tailwindcss.com/) blogging starter template. Probably the most feature-rich Next.js markdown blogging template out there. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.
Check out the documentation below to get started.
Facing issues? Check the [FAQ page](https://github.com/timlrx/tailwind-nextjs-starter-blog/wiki) and do a search on past issues. Feel free to open a new issue if none has been posted previously.
Feature request? Check the past discussions to see if it has been brought up previously. Otherwise, feel free to start a new discussion thread. All ideas are welcomed!
## Examples
- [Demo Blog](https://tailwind-nextjs-starter-blog.vercel.app/) - this repo
- [My personal blog](https://www.timlrx.com) - modified to auto-generate blog posts with dates
- [Aloisdg's cookbook](https://tambouille.vercel.app/) - with pictures and recipes!
- [GautierArcin's demo with next translate](https://tailwind-nextjs-starter-blog-seven.vercel.app/) - includes translation of mdx posts, [source code](https://github.com/GautierArcin/tailwind-nextjs-starter-blog/tree/demo/next-translate)
- [David Levai's digital garden](https://davidlevai.com/) - customized design and added email subscriptions
- [Thinh's Corner](https://thinhcorner.com/) - [customized layout](https://github.com/Th1nhNg0/th1nhng0.vercel.app/blob/5e73a420828d82f01e7147512a2c3273c4ec19f8/layouts/PostLayout.js) with sticky side table of contents
- [Leo's Blog](https://leohuynh.dev) - Tuan Anh Huynh's personal site. Add Snippets Page, Author Profile Card, Image Lightbox, ...
- [thvu.dev](https://thvu.dev) - Added `mdx-embed`, view count, reading minutes and more.
- [fiqrychoerudin.dev](https://www.fiqrychoerudin.dev/) - simple portfolio.
- [irvin.dev](https://www.irvin.dev/) - Irvin Lin's personal site. Added YouTube embedding.
- [the all JavaScript Blog](https://the-all-javascript-blog.vercel.app/) - a JavaScript enlightenment blog uses this
- [KirillSo.com](https://www.kirillso.com/) - Personal blog & website.
- [ghali.dev](https://ghali.dev) - Cyril's Blog
- [DevBoy Blog](https://devboy.vercel.app/) - M.Reza's personal blog
- [slightlysharpe.com](https://slightlysharpe.com) - [Tincre's](https://tincre.com) main company blog
- [blog.b00st.com](https://blog.b00st.com) - [b00st.com's](https://b00st.com) main music promotion blog
- [astrosaurus.me](https://astrosaurus.me/) - Ephraim Atta-Duncan's Personal Blog
- [dhanrajsp.me](https://dhanrajsp.me/) - Dhanraj's personal site and blog.
- [blog.r00ks.io](https://blog.r00ks.io/) - Austin Rooks's personal blog ([source code](https://github.com/Austionian/blog.r00ks)).
- [honghong.me](https://honghong.me) - Tszhong's personal website ([source code](https://github.com/tszhong0411/home))
- [alfoncode.com](https://alfoncode.com) - Alfonso García's personar website. Customized design ([source code](https://github.com/alfoncode/personal-web))
- [marceloformentao.dev](https://marceloformentao.dev) - Marcelo Formentão personal website ([source code](https://github.com/marceloavf/marceloformentao.dev)).
- [abiraja.com](https://www.abiraja.com/) - with a [runnable JS code snippet component!](https://www.abiraja.com/blog/querying-solana-blockchain)
- [einargudni.com](https://www.einargudni.com) - with a customized theme, command pallette and more ([source code](https://github.com/einargudnig/einargudni.com))
- [bpiggin.com](https://www.bpiggin.com) - Ben Piggin's personal blog
- [maqib.cn](https://maqib.cn) - A blog of Chinese front-end developers 狂奔小马的博客 ([源码](https://github.com/maqi1520/nextjs-tailwind-blog))
- [ambilena.com](https://ambilena.com/) - Electronic Music Blog & imprint for upcoming musicians.
- [kittan.ru](https://www.kittan.ru/) - Kittanb's personal blog about linux ([source code](https://github.com/kittanb/blog))
- [nchristopher.me](https://nchristopher.me) - Nicholas Christopher's personal website and blog ([source code](https://github.com/nchristopher/blog))
- [dalelarroder.com](https://dalelarroder.com) - Dale Larroder's personal website and blog ([source code](https://github.com/dlarroder/dalelarroder))
- [devahoy.com](https://devahoy.com) - Chai's personal blog (Thai language)
- [0xchai.io](https://0xchai.io) - Chai's personal blog
- [techipedia](https://techipedia.vercel.app) - Simple blogging progressive web app with custom installation button and top progress bar
- [reubence.com](https://reubence.com) - Reuben Rapose's Digital Garden
- [axolo.co/blog](https://axolo.co/blog) - Engineering management news & axolo.co updates (with image preview for article in the home page)
- [musing.vercel.app](https://musing.vercel.app/) - Parth Desai's personal blog ([source code](https://github.com/pycoder2000/blog))
- [onyourmental.com](https://www.onyourmental.com/) - [Curtis Warcup's](https://github.com/Cwarcup) website for the On Your Mental Podcast ([source code](https://github.com/Cwarcup/on-your-mental))
- [cwarcup.com](https://www.cwarcup.com/) - Curtis Warcup's personal website and blog ([source code](https://github.com/Cwarcup/personal-blog).
Using the template? Feel free to create a PR and add your blog to this list.
## Motivation
I wanted to port my existing blog to Nextjs and Tailwind CSS but there was no easy out of the box template to use so I decided to create one. Design is adapted from [Tailwindlabs blog](https://github.com/tailwindlabs/blog.tailwindcss.com).
I wanted it to be nearly as feature-rich as popular blogging templates like [beautiful-jekyll](https://github.com/daattali/beautiful-jekyll) and [Hugo Academic](https://github.com/wowchemy/wowchemy-hugo-modules) but with the best of React's ecosystem and current web development's best practices.
## Features
- Easy styling customization with [Tailwind 3.0](https://tailwindcss.com/blog/tailwindcss-v3) and primary color attribute
- Near perfect lighthouse score - [Lighthouse report](https://www.webpagetest.org/result/210111_DiC1_08f3670c3430bf4a9b76fc3b927716c5/)
- Lightweight, 45kB first load JS, uses Preact in production build
- Mobile-friendly view
- Light and dark theme
- Self-hosted font with [Fontsource](https://fontsource.org/)
- Supports [plausible](https://plausible.io/), [simple analytics](https://simpleanalytics.com/) and google analytics
- [MDX - write JSX in markdown documents!](https://mdxjs.com/)
- Server-side syntax highlighting with line numbers and line highlighting via [rehype-prism-plus](https://github.com/timlrx/rehype-prism-plus)
- Math display supported via [KaTeX](https://katex.org/)
- Citation and bibliography support via [rehype-citation](https://github.com/timlrx/rehype-citation)
- Automatic image optimization via [next/image](https://nextjs.org/docs/basic-features/image-optimization)
- Flexible data retrieval with [mdx-bundler](https://github.com/kentcdodds/mdx-bundler)
- Support for tags - each unique tag will be its own page
- Support for multiple authors
- Blog templates
- TOC component
- Support for nested routing of blog posts
- Newsletter component with support for mailchimp, buttondown, convertkit, klaviyo, revue, and emailoctopus
- Supports [giscus](https://github.com/laymonage/giscus), [utterances](https://github.com/utterance/utterances) or disqus
- Projects page
- Preconfigured security headers
- SEO friendly with RSS feed, sitemaps and more!
## Sample posts
- [A markdown guide](https://tailwind-nextjs-starter-blog.vercel.app/blog/github-markdown-guide)
- [Learn more about images in Next.js](https://tailwind-nextjs-starter-blog.vercel.app/blog/guide-to-using-images-in-nextjs)
- [A tour of math typesetting](https://tailwind-nextjs-starter-blog.vercel.app/blog/deriving-ols-estimator)
- [Simple MDX image grid](https://tailwind-nextjs-starter-blog.vercel.app/blog/pictures-of-canada)
- [Example of long prose](https://tailwind-nextjs-starter-blog.vercel.app/blog/the-time-machine)
- [Example of Nested Route Post](https://tailwind-nextjs-starter-blog.vercel.app/blog/nested-route/introducing-multi-part-posts-with-nested-routing)
## Quick Start Guide
1. Try installing the starter using the new [Pliny project CLI](https://github.com/timlrx/pliny):
```bash
npm i -g @pliny/cli
pliny new --template=starter-blog my-blog
```
It supports the updated version of the blog with Contentlayer, optional choice of TS/JS and different package managers as well as more modularized components which will be the basis of the template going forward.
Alternatively to stick with the current version, TypeScript and Contentlayer:
```bash
npx degit 'timlrx/tailwind-nextjs-starter-blog#contentlayer'
```
or JS (official support)
```bash
npx degit https://github.com/timlrx/tailwind-nextjs-starter-blog.git
```
2. Personalize `siteMetadata.js` (site related information)
3. Modify the content security policy in `next.config.js` if you want to use
any analytics provider or a commenting solution other than giscus.
4. Personalize `authors/default.md` (main author)
5. Modify `projectsData.js`
6. Modify `headerNavLinks.js` to customize navigation links
7. Add blog posts
8. Deploy on Vercel
Ivan Li's Blog, base [timlrx/tailwind-nextjs-starter-blog](https://github.com/timlrx/tailwind-nextjs-starter-blog)
## Installation
```bash
npm install
pnpm install
```
## Development
@ -144,13 +18,13 @@ npm install
First, run the development server:
```bash
npm start
pnpm start
```
or
```bash
npm run dev
pnpm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
@ -233,18 +107,4 @@ Follow the interactive prompt to generate a post with pre-filled front matter.
## Deploy
**Vercel**
The easiest way to deploy the template is to use the [Vercel Platform](https://vercel.com) from the creators of Next.js. Check out the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
**Netlify / GitHub Pages / Firebase etc.**
As the template uses `next/image` for image optimization, additional configurations have to be made to deploy on other popular static hosting websites like [Netlify](https://www.netlify.com/) or [GitHub Pages](https://pages.github.com/). An alternative image optimization provider such as Imgix, Cloudinary or Akamai has to be used. Alternatively, replace the `next/image` component with a standard `<img>` tag. See [`next/image` documentation](https://nextjs.org/docs/basic-features/image-optimization) for more details.
The API routes used in the newsletter component cannot be used in a static site export. You will need to use a form API endpoint provider and substitute the route in the newsletter component accordingly. Other hosting platforms such as Netlify also offer alternative solutions - please refer to their docs for more information.
## Support
Using the template? Support this effort by giving a star on GitHub, sharing your own blog and giving a shoutout on Twitter or becoming a project [sponsor](https://github.com/sponsors/timlrx).
## Licence
[MIT](https://github.com/timlrx/tailwind-nextjs-starter-blog/blob/master/LICENSE) © [Timothy Lin](https://www.timlrx.com)
Drone CI.

View File

@ -1,13 +1,12 @@
import Image from './Image'
import Link from './Link'
import Image from './Image';
import Link from './Link';
const Card = ({ title, description, imgSrc, href }) => (
<div className="md p-4 md:w-1/2" style={{ maxWidth: '544px' }}>
<div
className={`${
imgSrc && 'h-full'
} overflow-hidden rounded-md border-2 border-gray-200 border-opacity-60 dark:border-gray-700`}
>
} overflow-hidden rounded-md border-2 border-gray-200 border-opacity-60 dark:border-gray-700`}>
{imgSrc &&
(href ? (
<Link href={href} aria-label={`Link to ${title}`}>
@ -38,19 +37,20 @@ const Card = ({ title, description, imgSrc, href }) => (
title
)}
</h2>
<p className="prose mb-3 max-w-none text-gray-500 dark:text-gray-400">{description}</p>
<p className="prose mb-3 max-w-none text-gray-500 dark:text-gray-400">
{description}
</p>
{href && (
<Link
href={href}
className="text-base font-medium leading-6 text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
aria-label={`Link to ${title}`}
>
aria-label={`Link to ${title}`}>
Learn more &rarr;
</Link>
)}
</div>
</div>
</div>
)
);
export default Card
export default Card;

View File

@ -1,5 +1,5 @@
import { useEffect } from 'react'
import Router from 'next/router'
import { useEffect } from 'react';
import Router from 'next/router';
/**
* Client-side complement to next-remote-watch
@ -10,14 +10,14 @@ export const ClientReload = () => {
// Exclude socket.io from prod bundle
useEffect(() => {
import('socket.io-client').then((module) => {
const socket = module.io()
socket.on('reload', (data) => {
const socket = module.io();
socket.on('reload', () => {
Router.replace(Router.asPath, undefined, {
scroll: false,
})
})
})
}, [])
});
});
});
}, []);
return null
}
return null;
};

View File

@ -1,18 +1,22 @@
import Link from './Link'
import siteMetadata from '@/data/siteMetadata'
import SocialIcon from '@/components/social-icons'
import Link from './Link';
import siteMetadata from '@/data/siteMetadata';
import SocialIcon from '@/components/social-icons';
export default function Footer() {
return (
<footer>
<div className="mt-16 flex flex-col items-center">
<div className="mb-3 flex space-x-4">
<SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} size="6" />
<SocialIcon kind="github" href={siteMetadata.github} size="6" />
<SocialIcon kind="facebook" href={siteMetadata.facebook} size="6" />
<SocialIcon kind="youtube" href={siteMetadata.youtube} size="6" />
<SocialIcon kind="linkedin" href={siteMetadata.linkedin} size="6" />
<SocialIcon kind="twitter" href={siteMetadata.twitter} size="6" />
<SocialIcon
kind="mail"
href={`mailto:${siteMetadata.email}`}
size={6}
/>
<SocialIcon kind="github" href={siteMetadata.github} size={6} />
<SocialIcon kind="facebook" href={siteMetadata.facebook} size={6} />
<SocialIcon kind="youtube" href={siteMetadata.youtube} size={6} />
<SocialIcon kind="linkedin" href={siteMetadata.linkedin} size={6} />
<SocialIcon kind="twitter" href={siteMetadata.twitter} size={6} />
</div>
<div className="mb-2 flex space-x-2 text-sm text-gray-500 dark:text-gray-400">
<div>{siteMetadata.author}</div>
@ -21,12 +25,19 @@ export default function Footer() {
<div>{``}</div>
<Link href="/">{siteMetadata.title}</Link>
</div>
<div className="mb-2 text-sm text-gray-500 dark:text-gray-400">
<Link href="https://beian.miit.gov.cn" rel="nofollow">
ICP备2023000043号
</Link>
</div>
<div className="mb-8 text-sm text-gray-500 dark:text-gray-400">
<Link href="https://github.com/timlrx/tailwind-nextjs-starter-blog">
<Link
href="https://github.com/timlrx/tailwind-nextjs-starter-blog"
rel="nofollow">
Tailwind Nextjs Theme
</Link>
</div>
</div>
</footer>
)
);
}

View File

@ -1,6 +0,0 @@
import NextImage from 'next/image'
// eslint-disable-next-line jsx-a11y/alt-text
const Image = ({ ...rest }) => <NextImage {...rest} />
export default Image

5
components/Image.tsx Normal file
View File

@ -0,0 +1,5 @@
import NextImage, { ImageProps } from 'next/image';
const Image = ({ ...rest }: ImageProps) => <NextImage {...rest} />;
export default Image;

View File

@ -1,13 +1,18 @@
import siteMetadata from '@/data/siteMetadata'
import headerNavLinks from '@/data/headerNavLinks'
import Logo from '@/data/logo.svg'
import Link from './Link'
import SectionContainer from './SectionContainer'
import Footer from './Footer'
import MobileNav from './MobileNav'
import ThemeSwitch from './ThemeSwitch'
import siteMetadata from '@/data/siteMetadata';
import headerNavLinks from '@/data/headerNavLinks';
import Logo from '@/data/logo.svg';
import Link from './Link';
import SectionContainer from './SectionContainer';
import Footer from './Footer';
import MobileNav from './MobileNav';
import ThemeSwitch from './ThemeSwitch';
import { ReactNode } from 'react';
const LayoutWrapper = ({ children }) => {
interface Props {
children: ReactNode;
}
const LayoutWrapper = ({ children }: Props) => {
return (
<SectionContainer>
<div className="flex h-screen flex-col justify-between">
@ -34,8 +39,7 @@ const LayoutWrapper = ({ children }) => {
<Link
key={link.title}
href={link.href}
className="p-1 font-medium text-gray-900 dark:text-gray-100 sm:p-4"
>
className="p-1 font-medium text-gray-900 dark:text-gray-100 sm:p-4">
{link.title}
</Link>
))}
@ -48,7 +52,7 @@ const LayoutWrapper = ({ children }) => {
<Footer />
</div>
</SectionContainer>
)
}
);
};
export default LayoutWrapper
export default LayoutWrapper;

View File

@ -1,23 +0,0 @@
/* eslint-disable jsx-a11y/anchor-has-content */
import Link from 'next/link'
const CustomLink = ({ href, ...rest }) => {
const isInternalLink = href && href.startsWith('/')
const isAnchorLink = href && href.startsWith('#')
if (isInternalLink) {
return (
<Link href={href}>
<a {...rest} />
</Link>
)
}
if (isAnchorLink) {
return <a href={href} {...rest} />
}
return <a target="_blank" rel="noopener noreferrer" href={href} {...rest} />
}
export default CustomLink

30
components/Link.tsx Normal file
View File

@ -0,0 +1,30 @@
/* eslint-disable jsx-a11y/anchor-has-content */
import Link from 'next/link';
import { AnchorHTMLAttributes, DetailedHTMLProps } from 'react';
const CustomLink = ({
href,
...rest
}: DetailedHTMLProps<
AnchorHTMLAttributes<HTMLAnchorElement>,
HTMLAnchorElement
>) => {
const isInternalLink = href && href.startsWith('/');
const isAnchorLink = href && href.startsWith('#');
if (isInternalLink) {
return (
<Link href={href}>
<a {...rest} />
</Link>
);
}
if (isAnchorLink) {
return <a href={href} {...rest} />;
}
return <a target="_blank" rel="noopener noreferrer" href={href} {...rest} />;
};
export default CustomLink;

View File

@ -1,26 +0,0 @@
/* eslint-disable react/display-name */
import { useMemo } from 'react'
import { getMDXComponent } from 'mdx-bundler/client'
import Image from './Image'
import CustomLink from './Link'
import TOCInline from './TOCInline'
import Pre from './Pre'
import { BlogNewsletterForm } from './NewsletterForm'
export const MDXComponents = {
Image,
TOCInline,
a: CustomLink,
pre: Pre,
BlogNewsletterForm: BlogNewsletterForm,
wrapper: ({ components, layout, ...rest }) => {
const Layout = require(`../layouts/${layout}`).default
return <Layout {...rest} />
},
}
export const MDXLayoutRenderer = ({ layout, mdxSource, ...rest }) => {
const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource])
return <MDXLayout layout={layout} components={MDXComponents} {...rest} />
}

View File

@ -0,0 +1,39 @@
/* eslint-disable react/display-name */
import React, { useMemo } from 'react';
import { ComponentMap, getMDXComponent } from 'mdx-bundler/client';
import Image from './Image';
import CustomLink from './Link';
import TOCInline from './TOCInline';
import Pre from './Pre';
import { BlogNewsletterForm } from './NewsletterForm';
const Wrapper: React.ComponentType<{ layout: string }> = ({
layout,
...rest
}) => {
const Layout = require(`../layouts/${layout}`).default;
return <Layout {...rest} />;
};
export const MDXComponents: ComponentMap = {
Image,
//@ts-ignore
TOCInline,
a: CustomLink,
pre: Pre,
wrapper: Wrapper,
//@ts-ignore
BlogNewsletterForm,
};
interface Props {
layout: string;
mdxSource: string;
[key: string]: unknown;
}
export const MDXLayoutRenderer = ({ layout, mdxSource, ...rest }: Props) => {
const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource]);
return <MDXLayout layout={layout} components={MDXComponents} {...rest} />;
};

View File

@ -1,85 +0,0 @@
import { useState } from 'react'
import Link from './Link'
import headerNavLinks from '@/data/headerNavLinks'
const MobileNav = () => {
const [navShow, setNavShow] = useState(false)
const onToggleNav = () => {
setNavShow((status) => {
if (status) {
document.body.style.overflow = 'auto'
} else {
// Prevent scrolling
document.body.style.overflow = 'hidden'
}
return !status
})
}
return (
<div className="sm:hidden">
<button
type="button"
className="ml-1 mr-1 h-8 w-8 rounded py-1"
aria-label="Toggle Menu"
onClick={onToggleNav}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="text-gray-900 dark:text-gray-100"
>
<path
fillRule="evenodd"
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
clipRule="evenodd"
/>
</svg>
</button>
<div
className={`fixed top-0 left-0 z-10 h-full w-full transform bg-gray-200 opacity-95 duration-300 ease-in-out dark:bg-gray-800 ${
navShow ? 'translate-x-0' : 'translate-x-full'
}`}
>
<div className="flex justify-end">
<button
type="button"
className="mr-5 mt-11 h-8 w-8 rounded"
aria-label="Toggle Menu"
onClick={onToggleNav}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="text-gray-900 dark:text-gray-100"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
<nav className="fixed mt-8 h-full">
{headerNavLinks.map((link) => (
<div key={link.title} className="px-12 py-4">
<Link
href={link.href}
className="text-2xl font-bold tracking-widest text-gray-900 dark:text-gray-100"
onClick={onToggleNav}
>
{link.title}
</Link>
</div>
))}
</nav>
</div>
</div>
)
}
export default MobileNav

73
components/MobileNav.tsx Normal file
View File

@ -0,0 +1,73 @@
import { useState } from 'react';
import Link from './Link';
import headerNavLinks from '@/data/headerNavLinks';
const MobileNav = () => {
const [navShow, setNavShow] = useState(false);
const onToggleNav = () => {
setNavShow((status) => {
if (status) {
document.body.style.overflow = 'auto';
} else {
// Prevent scrolling
document.body.style.overflow = 'hidden';
}
return !status;
});
};
return (
<div className="sm:hidden">
<button
type="button"
className="ml-1 mr-1 h-8 w-8 rounded py-1"
aria-label="Toggle Menu"
onClick={onToggleNav}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="text-gray-900 dark:text-gray-100">
{navShow ? (
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
) : (
<path
fillRule="evenodd"
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
clipRule="evenodd"
/>
)}
</svg>
</button>
<div
className={`fixed top-24 right-0 z-10 h-full w-full transform bg-gray-200 opacity-95 duration-300 ease-in-out dark:bg-gray-800 ${
navShow ? 'translate-x-0' : 'translate-x-full'
}`}>
<button
type="button"
aria-label="toggle modal"
className="fixed h-full w-full cursor-auto focus:outline-none"
onClick={onToggleNav}></button>
<nav className="fixed mt-8 h-full">
{headerNavLinks.map((link) => (
<div key={link.title} className="px-12 py-4">
<Link
href={link.href}
className="text-2xl font-bold tracking-widest text-gray-900 dark:text-gray-100"
onClick={onToggleNav}>
{link.title}
</Link>
</div>
))}
</nav>
</div>
</div>
);
};
export default MobileNav;

View File

@ -1,15 +1,15 @@
import { useRef, useState } from 'react'
import React, { useRef, useState } from 'react';
import siteMetadata from '@/data/siteMetadata'
import siteMetadata from '@/data/siteMetadata';
const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => {
const inputEl = useRef(null)
const [error, setError] = useState(false)
const [message, setMessage] = useState('')
const [subscribed, setSubscribed] = useState(false)
const inputEl = useRef<HTMLInputElement>(null);
const [error, setError] = useState(false);
const [message, setMessage] = useState('');
const [subscribed, setSubscribed] = useState(false);
const subscribe = async (e) => {
e.preventDefault()
const subscribe = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const res = await fetch(`/api/${siteMetadata.newsletter.provider}`, {
body: JSON.stringify({
@ -19,24 +19,28 @@ const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => {
'Content-Type': 'application/json',
},
method: 'POST',
})
});
const { error } = await res.json()
const { error } = await res.json();
if (error) {
setError(true)
setMessage('Your e-mail address is invalid or you are already subscribed!')
return
setError(true);
setMessage(
'Your e-mail address is invalid or you are already subscribed!'
);
return;
}
inputEl.current.value = ''
setError(false)
setSubscribed(true)
setMessage('Successfully! 🎉 You are now subscribed.')
}
inputEl.current.value = '';
setError(false);
setSubscribed(true);
setMessage('Successfully! 🎉 You are now subscribed.');
};
return (
<div>
<div className="pb-1 text-lg font-semibold text-gray-800 dark:text-gray-100">{title}</div>
<div className="pb-1 text-lg font-semibold text-gray-800 dark:text-gray-100">
{title}
</div>
<form className="flex flex-col sm:flex-row" onSubmit={subscribe}>
<div>
<label className="sr-only" htmlFor="email-input">
@ -47,7 +51,9 @@ const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => {
className="w-72 rounded-md px-4 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600 dark:bg-black"
id="email-input"
name="email"
placeholder={subscribed ? "You're subscribed ! 🎉" : 'Enter your email'}
placeholder={
subscribed ? "You're subscribed ! 🎉" : 'Enter your email'
}
ref={inputEl}
required
type="email"
@ -57,23 +63,26 @@ const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => {
<div className="mt-2 flex w-full rounded-md shadow-sm sm:mt-0 sm:ml-3">
<button
className={`w-full rounded-md bg-primary-500 py-2 px-4 font-medium text-white sm:py-0 ${
subscribed ? 'cursor-default' : 'hover:bg-primary-700 dark:hover:bg-primary-400'
subscribed
? 'cursor-default'
: 'hover:bg-primary-700 dark:hover:bg-primary-400'
} focus:outline-none focus:ring-2 focus:ring-primary-600 focus:ring-offset-2 dark:ring-offset-black`}
type="submit"
disabled={subscribed}
>
disabled={subscribed}>
{subscribed ? 'Thank you!' : 'Sign up'}
</button>
</div>
</form>
{error && (
<div className="w-72 pt-2 text-sm text-red-500 dark:text-red-400 sm:w-96">{message}</div>
<div className="w-72 pt-2 text-sm text-red-500 dark:text-red-400 sm:w-96">
{message}
</div>
)}
</div>
)
}
);
};
export default NewsletterForm
export default NewsletterForm;
export const BlogNewsletterForm = ({ title }) => (
<div className="flex items-center justify-center">
@ -81,4 +90,4 @@ export const BlogNewsletterForm = ({ title }) => (
<NewsletterForm title={title} />
</div>
</div>
)
);

View File

@ -1,7 +1,13 @@
export default function PageTitle({ children }) {
import { ReactNode } from 'react';
interface Props {
children: ReactNode;
}
export default function PageTitle({ children }: Props) {
return (
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-5xl md:leading-14">
{children}
</h1>
)
);
}

View File

@ -1,36 +0,0 @@
import Link from '@/components/Link'
export default function Pagination({ totalPages, currentPage }) {
const prevPage = parseInt(currentPage) - 1 > 0
const nextPage = parseInt(currentPage) + 1 <= parseInt(totalPages)
return (
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
<nav className="flex justify-between">
{!prevPage && (
<button rel="previous" className="cursor-auto disabled:opacity-50" disabled={!prevPage}>
Previous
</button>
)}
{prevPage && (
<Link href={currentPage - 1 === 1 ? `/blog/` : `/blog/page/${currentPage - 1}`}>
<button rel="previous">Previous</button>
</Link>
)}
<span>
{currentPage} of {totalPages}
</span>
{!nextPage && (
<button rel="next" className="cursor-auto disabled:opacity-50" disabled={!nextPage}>
Next
</button>
)}
{nextPage && (
<Link href={`/blog/page/${currentPage + 1}`}>
<button rel="next">Next</button>
</Link>
)}
</nav>
</div>
)
}

48
components/Pagination.tsx Normal file
View File

@ -0,0 +1,48 @@
import Link from '@/components/Link';
interface Props {
totalPages: number;
currentPage: number;
}
export default function Pagination({ totalPages, currentPage }: Props) {
const prevPage = currentPage - 1 > 0;
const nextPage = currentPage + 1 <= totalPages;
return (
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
<nav className="flex justify-between">
{!prevPage && (
<button
className="cursor-auto disabled:opacity-50"
disabled={!prevPage}>
Previous
</button>
)}
{prevPage && (
<Link
href={
currentPage - 1 === 1 ? `/blog/` : `/blog/page/${currentPage - 1}`
}>
<button>Previous</button>
</Link>
)}
<span>
{currentPage} of {totalPages}
</span>
{!nextPage && (
<button
className="cursor-auto disabled:opacity-50"
disabled={!nextPage}>
Next
</button>
)}
{nextPage && (
<Link href={`/blog/page/${currentPage + 1}`}>
<button>Next</button>
</Link>
)}
</nav>
</div>
);
}

View File

@ -1,27 +1,35 @@
import { useState, useRef } from 'react'
import { useState, useRef, ReactNode } from 'react';
const Pre = (props) => {
const textInput = useRef(null)
const [hovered, setHovered] = useState(false)
const [copied, setCopied] = useState(false)
interface Props {
children: ReactNode;
}
const Pre = ({ children }: Props) => {
const textInput = useRef(null);
const [hovered, setHovered] = useState(false);
const [copied, setCopied] = useState(false);
const onEnter = () => {
setHovered(true)
}
setHovered(true);
};
const onExit = () => {
setHovered(false)
setCopied(false)
}
setHovered(false);
setCopied(false);
};
const onCopy = () => {
setCopied(true)
navigator.clipboard.writeText(textInput.current.textContent)
setCopied(true);
navigator.clipboard.writeText(textInput.current.textContent);
setTimeout(() => {
setCopied(false)
}, 2000)
}
setCopied(false);
}, 2000);
};
return (
<div ref={textInput} onMouseEnter={onEnter} onMouseLeave={onExit} className="relative">
<div
ref={textInput}
onMouseEnter={onEnter}
onMouseLeave={onExit}
className="relative">
{hovered && (
<button
aria-label="Copy code"
@ -31,15 +39,13 @@ const Pre = (props) => {
? 'border-green-400 focus:border-green-400 focus:outline-none'
: 'border-gray-300'
}`}
onClick={onCopy}
>
onClick={onCopy}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
stroke="currentColor"
fill="none"
className={copied ? 'text-green-400' : 'text-gray-300'}
>
className={copied ? 'text-green-400' : 'text-gray-300'}>
{copied ? (
<>
<path
@ -63,9 +69,9 @@ const Pre = (props) => {
</button>
)}
<pre>{props.children}</pre>
<pre>{children}</pre>
</div>
)
}
);
};
export default Pre
export default Pre;

View File

@ -1,21 +1,49 @@
import Head from 'next/head'
import { useRouter } from 'next/router'
import siteMetadata from '@/data/siteMetadata'
import Head from 'next/head';
import { useRouter } from 'next/router';
import siteMetadata from '@/data/siteMetadata';
import { AuthorFrontMatter } from 'types/AuthorFrontMatter';
import { PostFrontMatter } from 'types/PostFrontMatter';
const CommonSEO = ({ title, description, ogType, ogImage, twImage, canonicalUrl }) => {
const router = useRouter()
interface CommonSEOProps {
title: string;
description: string;
ogType: string;
ogImage:
| string
| {
'@type': string;
url: string;
}[];
twImage: string;
canonicalUrl?: string;
}
const CommonSEO = ({
title,
description,
ogType,
ogImage,
twImage,
canonicalUrl,
}: CommonSEOProps) => {
const router = useRouter();
return (
<Head>
<title>{title}</title>
<meta name="robots" content="follow, index" />
<meta name="description" content={description} />
<meta property="og:url" content={`${siteMetadata.siteUrl}${router.asPath}`} />
<meta
property="og:url"
content={`${siteMetadata.siteUrl}${router.asPath}`}
/>
<meta property="og:type" content={ogType} />
<meta property="og:site_name" content={siteMetadata.title} />
<meta property="og:description" content={description} />
<meta property="og:title" content={title} />
{ogImage.constructor.name === 'Array' ? (
ogImage.map(({ url }) => <meta property="og:image" content={url} key={url} />)
{Array.isArray(ogImage) ? (
ogImage.map(({ url }) => (
<meta property="og:image" content={url} key={url} />
))
) : (
<meta property="og:image" content={ogImage} key={ogImage} />
)}
@ -26,15 +54,24 @@ const CommonSEO = ({ title, description, ogType, ogImage, twImage, canonicalUrl
<meta name="twitter:image" content={twImage} />
<link
rel="canonical"
href={canonicalUrl ? canonicalUrl : `${siteMetadata.siteUrl}${router.asPath}`}
href={
canonicalUrl
? canonicalUrl
: `${siteMetadata.siteUrl}${router.asPath}`
}
/>
</Head>
)
);
};
interface PageSEOProps {
title: string;
description: string;
}
export const PageSEO = ({ title, description }) => {
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
export const PageSEO = ({ title, description }: PageSEOProps) => {
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner;
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner;
return (
<CommonSEO
title={title}
@ -43,13 +80,13 @@ export const PageSEO = ({ title, description }) => {
ogImage={ogImageUrl}
twImage={twImageUrl}
/>
)
}
);
};
export const TagSEO = ({ title, description }) => {
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
const router = useRouter()
export const TagSEO = ({ title, description }: PageSEOProps) => {
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner;
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner;
const router = useRouter();
return (
<>
<CommonSEO
@ -68,7 +105,12 @@ export const TagSEO = ({ title, description }) => {
/>
</Head>
</>
)
);
};
interface BlogSeoProps extends PostFrontMatter {
authorDetails?: AuthorFrontMatter[];
url: string;
}
export const BlogSEO = ({
@ -80,37 +122,36 @@ export const BlogSEO = ({
url,
images = [],
canonicalUrl,
}) => {
const router = useRouter()
const publishedAt = new Date(date).toISOString()
const modifiedAt = new Date(lastmod || date).toISOString()
let imagesArr =
}: BlogSeoProps) => {
const publishedAt = new Date(date).toISOString();
const modifiedAt = new Date(lastmod || date).toISOString();
const imagesArr =
images.length === 0
? [siteMetadata.socialBanner]
: typeof images === 'string'
? [images]
: images
: images;
const featuredImages = imagesArr.map((img) => {
return {
'@type': 'ImageObject',
url: img.includes('http') ? img : siteMetadata.siteUrl + img,
}
})
url: `${siteMetadata.siteUrl}${img}`,
};
});
let authorList
let authorList;
if (authorDetails) {
authorList = authorDetails.map((author) => {
return {
'@type': 'Person',
name: author.name,
}
})
};
});
} else {
authorList = {
'@type': 'Person',
name: siteMetadata.author,
}
};
}
const structuredData = {
@ -134,9 +175,9 @@ export const BlogSEO = ({
},
},
description: summary,
}
};
const twImageUrl = featuredImages[0].url
const twImageUrl = featuredImages[0].url;
return (
<>
@ -149,8 +190,12 @@ export const BlogSEO = ({
canonicalUrl={canonicalUrl}
/>
<Head>
{date && <meta property="article:published_time" content={publishedAt} />}
{lastmod && <meta property="article:modified_time" content={modifiedAt} />}
{date && (
<meta property="article:published_time" content={publishedAt} />
)}
{lastmod && (
<meta property="article:modified_time" content={modifiedAt} />
)}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
@ -159,5 +204,5 @@ export const BlogSEO = ({
/>
</Head>
</>
)
}
);
};

View File

@ -1,61 +0,0 @@
import siteMetadata from '@/data/siteMetadata'
import { useEffect, useState } from 'react'
const ScrollTopAndComment = () => {
const [show, setShow] = useState(false)
useEffect(() => {
const handleWindowScroll = () => {
if (window.scrollY > 50) setShow(true)
else setShow(false)
}
window.addEventListener('scroll', handleWindowScroll)
return () => window.removeEventListener('scroll', handleWindowScroll)
}, [])
const handleScrollTop = () => {
window.scrollTo({ top: 0 })
}
const handleScrollToComment = () => {
document.getElementById('comment').scrollIntoView()
}
return (
<div
className={`fixed right-8 bottom-8 hidden flex-col gap-3 ${show ? 'md:flex' : 'md:hidden'}`}
>
{siteMetadata.comment.provider && (
<button
aria-label="Scroll To Comment"
type="button"
onClick={handleScrollToComment}
className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600"
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M18 10c0 3.866-3.582 7-8 7a8.841 8.841 0 01-4.083-.98L2 17l1.338-3.123C2.493 12.767 2 11.434 2 10c0-3.866 3.582-7 8-7s8 3.134 8 7zM7 9H5v2h2V9zm8 0h-2v2h2V9zM9 9h2v2H9V9z"
clipRule="evenodd"
/>
</svg>
</button>
)}
<button
aria-label="Scroll To Top"
type="button"
onClick={handleScrollTop}
className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600"
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
)
}
export default ScrollTopAndComment

View File

@ -0,0 +1,57 @@
import { useEffect, useState } from 'react';
const ScrollTopAndComment = () => {
const [show, setShow] = useState(false);
useEffect(() => {
const handleWindowScroll = () => {
if (window.scrollY > 50) setShow(true);
else setShow(false);
};
window.addEventListener('scroll', handleWindowScroll);
return () => window.removeEventListener('scroll', handleWindowScroll);
}, []);
const handleScrollTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleScrollToComment = () => {
document.getElementById('comment').scrollIntoView();
};
return (
<div
className={`fixed right-8 bottom-8 hidden flex-col gap-3 ${
show ? 'md:flex' : 'md:hidden'
}`}>
<button
aria-label="Scroll To Comment"
type="button"
onClick={handleScrollToComment}
className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600">
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M18 10c0 3.866-3.582 7-8 7a8.841 8.841 0 01-4.083-.98L2 17l1.338-3.123C2.493 12.767 2 11.434 2 10c0-3.866 3.582-7 8-7s8 3.134 8 7zM7 9H5v2h2V9zm8 0h-2v2h2V9zM9 9h2v2H9V9z"
clipRule="evenodd"
/>
</svg>
</button>
<button
aria-label="Scroll To Top"
type="button"
onClick={handleScrollTop}
className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600">
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
);
};
export default ScrollTopAndComment;

View File

@ -1,3 +0,0 @@
export default function SectionContainer({ children }) {
return <div className="mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0">{children}</div>
}

View File

@ -0,0 +1,13 @@
import { ReactNode } from 'react';
interface Props {
children: ReactNode;
}
export default function SectionContainer({ children }: Props) {
return (
<div className="mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0">
{children}
</div>
);
}

View File

@ -1,23 +1,27 @@
/**
* @typedef TocHeading
* @prop {string} value
* @prop {number} depth
* @prop {string} url
*/
import { Toc } from 'types/Toc';
interface TOCInlineProps {
toc: Toc;
indentDepth?: number;
fromHeading?: number;
toHeading?: number;
asDisclosure?: boolean;
exclude?: string | string[];
}
/**
* Generates an inline table of contents
* Exclude titles matching this string (new RegExp('^(' + string + ')$', 'i')).
* If an array is passed the array gets joined with a pipe (new RegExp('^(' + array.join('|') + ')$', 'i')).
*
* @param {{
* toc: TocHeading[],
* indentDepth?: number,
* fromHeading?: number,
* toHeading?: number,
* asDisclosure?: boolean,
* exclude?: string|string[]
* }} props
* @param {TOCInlineProps} {
* toc,
* indentDepth = 3,
* fromHeading = 1,
* toHeading = 6,
* asDisclosure = false,
* exclude = '',
* }
*
*/
const TOCInline = ({
@ -27,38 +31,44 @@ const TOCInline = ({
toHeading = 6,
asDisclosure = false,
exclude = '',
}) => {
}: TOCInlineProps) => {
const re = Array.isArray(exclude)
? new RegExp('^(' + exclude.join('|') + ')$', 'i')
: new RegExp('^(' + exclude + ')$', 'i')
: new RegExp('^(' + exclude + ')$', 'i');
const filteredToc = toc.filter(
(heading) =>
heading.depth >= fromHeading && heading.depth <= toHeading && !re.test(heading.value)
)
heading.depth >= fromHeading &&
heading.depth <= toHeading &&
!re.test(heading.value)
);
const tocList = (
<ul>
{filteredToc.map((heading) => (
<li key={heading.value} className={`${heading.depth >= indentDepth && 'ml-6'}`}>
<li
key={heading.value}
className={`${heading.depth >= indentDepth && 'ml-6'}`}>
<a href={heading.url}>{heading.value}</a>
</li>
))}
</ul>
)
);
return (
<>
{asDisclosure ? (
<details open>
<summary className="ml-6 pt-2 pb-2 text-xl font-bold">Table of Contents</summary>
<summary className="ml-6 pt-2 pb-2 text-xl font-bold">
Table of Contents
</summary>
<div className="ml-6">{tocList}</div>
</details>
) : (
tocList
)}
</>
)
}
);
};
export default TOCInline
export default TOCInline;

View File

@ -1,14 +1,18 @@
import Link from 'next/link'
import kebabCase from '@/lib/utils/kebabCase'
import Link from 'next/link';
import kebabCase from '@/lib/utils/kebabCase';
const Tag = ({ text }) => {
interface Props {
text: string;
}
const Tag = ({ text }: Props) => {
return (
<Link href={`/tags/${kebabCase(text)}`}>
<a className="mr-3 text-sm font-medium uppercase text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
{text.split(' ').join('-')}
</a>
</Link>
)
}
);
};
export default Tag
export default Tag;

View File

@ -1,26 +1,28 @@
import { useEffect, useState } from 'react'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react';
import { useTheme } from 'next-themes';
const ThemeSwitch = () => {
const [mounted, setMounted] = useState(false)
const { theme, setTheme, resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false);
const { theme, setTheme, resolvedTheme } = useTheme();
// When mounted on client, now we can show the UI
useEffect(() => setMounted(true), [])
useEffect(() => setMounted(true), []);
return (
<button
aria-label="Toggle Dark Mode"
type="button"
className="ml-1 mr-1 h-8 w-8 rounded p-1 sm:ml-4"
onClick={() => setTheme(theme === 'dark' || resolvedTheme === 'dark' ? 'light' : 'dark')}
>
onClick={() =>
setTheme(
theme === 'dark' || resolvedTheme === 'dark' ? 'light' : 'dark'
)
}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="text-gray-900 dark:text-gray-100"
>
className="text-gray-900 dark:text-gray-100">
{mounted && (theme === 'dark' || resolvedTheme === 'dark') ? (
<path
fillRule="evenodd"
@ -32,7 +34,7 @@ const ThemeSwitch = () => {
)}
</svg>
</button>
)
}
);
};
export default ThemeSwitch
export default ThemeSwitch;

View File

@ -1,6 +1,6 @@
import Script from 'next/script'
import Script from 'next/script';
import siteMetadata from '@/data/siteMetadata'
import siteMetadata from '@/data/siteMetadata';
const GAScript = () => {
return (
@ -21,10 +21,10 @@ const GAScript = () => {
`}
</Script>
</>
)
}
);
};
export default GAScript
export default GAScript;
// https://developers.google.com/analytics/devguides/collection/gtagjs/events
export const logEvent = (action, category, label, value) => {
@ -32,5 +32,5 @@ export const logEvent = (action, category, label, value) => {
event_category: category,
event_label: label,
value: value,
})
}
});
};

View File

@ -1,6 +1,6 @@
import Script from 'next/script'
import Script from 'next/script';
import siteMetadata from '@/data/siteMetadata'
import siteMetadata from '@/data/siteMetadata';
const PlausibleScript = () => {
return (
@ -16,12 +16,12 @@ const PlausibleScript = () => {
`}
</Script>
</>
)
}
);
};
export default PlausibleScript
export default PlausibleScript;
// https://plausible.io/docs/custom-event-goals
export const logEvent = (eventName, ...rest) => {
return window.plausible?.(eventName, ...rest)
}
return window.plausible?.(eventName, ...rest);
};

View File

@ -1,18 +0,0 @@
import Script from 'next/script'
import siteMetadata from '@/data/siteMetadata'
const PosthogScript = () => {
return (
<>
<Script strategy="lazyOnload" id="posthog-script">
{`
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('${siteMetadata.analytics.posthogAnalyticsId}',{api_host:'https://app.posthog.com'})
`}
</Script>
</>
)
}
export default PosthogScript

View File

@ -1,4 +1,4 @@
import Script from 'next/script'
import Script from 'next/script';
const SimpleAnalyticsScript = () => {
return (
@ -8,18 +8,21 @@ const SimpleAnalyticsScript = () => {
window.sa_event=window.sa_event||function(){var a=[].slice.call(arguments);window.sa_event.q?window.sa_event.q.push(a):window.sa_event.q=[a]};
`}
</Script>
<Script strategy="lazyOnload" src="https://scripts.simpleanalyticscdn.com/latest.js" />
<Script
strategy="lazyOnload"
src="https://scripts.simpleanalyticscdn.com/latest.js"
/>
</>
)
}
);
};
// https://docs.simpleanalytics.com/events
export const logEvent = (eventName, callback) => {
if (callback) {
return window.sa_event?.(eventName, callback)
return window.sa_event?.(eventName, callback);
} else {
return window.sa_event?.(eventName)
return window.sa_event?.(eventName);
}
}
};
export default SimpleAnalyticsScript
export default SimpleAnalyticsScript;

View File

@ -1,6 +1,6 @@
import Script from 'next/script'
import Script from 'next/script';
import siteMetadata from '@/data/siteMetadata'
import siteMetadata from '@/data/siteMetadata';
const UmamiScript = () => {
return (
@ -12,7 +12,7 @@ const UmamiScript = () => {
src="https://umami.example.com/umami.js" // Replace with your umami instance
/>
</>
)
}
);
};
export default UmamiScript
export default UmamiScript;

View File

@ -1,22 +0,0 @@
import GA from './GoogleAnalytics'
import Plausible from './Plausible'
import SimpleAnalytics from './SimpleAnalytics'
import Umami from './Umami'
import Posthog from './Posthog'
import siteMetadata from '@/data/siteMetadata'
const isProduction = process.env.NODE_ENV === 'production'
const Analytics = () => {
return (
<>
{isProduction && siteMetadata.analytics.plausibleDataDomain && <Plausible />}
{isProduction && siteMetadata.analytics.simpleAnalytics && <SimpleAnalytics />}
{isProduction && siteMetadata.analytics.umamiWebsiteId && <Umami />}
{isProduction && siteMetadata.analytics.googleAnalyticsId && <GA />}
{isProduction && siteMetadata.analytics.posthogAnalyticsId && <Posthog />}
</>
)
}
export default Analytics

View File

@ -0,0 +1,32 @@
import GA from './GoogleAnalytics';
import Plausible from './Plausible';
import SimpleAnalytics from './SimpleAnalytics';
import Umami from './Umami';
import siteMetadata from '@/data/siteMetadata';
declare global {
interface Window {
gtag?: (...args: any[]) => void;
plausible?: (...args: any[]) => void;
sa_event?: (...args: any[]) => void;
}
}
const isProduction = process.env.NODE_ENV === 'production';
const Analytics = () => {
return (
<>
{isProduction && siteMetadata.analytics.plausibleDataDomain && (
<Plausible />
)}
{isProduction && siteMetadata.analytics.simpleAnalytics && (
<SimpleAnalytics />
)}
{isProduction && siteMetadata.analytics.umamiWebsiteId && <Umami />}
{isProduction && siteMetadata.analytics.googleAnalyticsId && <GA />}
</>
);
};
export default Analytics;

View File

@ -0,0 +1,33 @@
import React, { useMemo, useState } from 'react';
import siteMetadata from '@/data/siteMetadata';
import { PostFrontMatter } from 'types/PostFrontMatter';
import { useTheme } from 'next-themes';
import ReactCommento from './commento/ReactCommento';
interface Props {
frontMatter: PostFrontMatter;
}
const Commento = ({ frontMatter }: Props) => {
const { resolvedTheme } = useTheme();
const commentsTheme = useMemo(() => {
switch (resolvedTheme) {
case 'light':
case 'dark':
return resolvedTheme;
default:
return 'auto';
}
}, [resolvedTheme]);
return (
<div className="my-2">
<ReactCommento
url={siteMetadata.comment.commentoConfig.url}
pageId={frontMatter.slug}
/>
</div>
);
};
export default Commento;

View File

@ -0,0 +1,41 @@
import React, { useMemo, useState } from 'react';
import siteMetadata from '@/data/siteMetadata';
import { PostFrontMatter } from 'types/PostFrontMatter';
import { ReactCusdis } from 'react-cusdis';
import { useTheme } from 'next-themes';
interface Props {
frontMatter: PostFrontMatter;
}
const Cusdis = ({ frontMatter }: Props) => {
const { resolvedTheme } = useTheme();
const commentsTheme = useMemo(() => {
switch (resolvedTheme) {
case 'light':
case 'dark':
return resolvedTheme;
default:
return 'auto';
}
}, [resolvedTheme]);
return (
<div className="my-2">
<ReactCusdis
key={commentsTheme}
lang={siteMetadata.language?.toLocaleLowerCase()}
attrs={{
appId: siteMetadata.comment.cusdisConfig.appId,
host: siteMetadata.comment.cusdisConfig.host,
pageId: frontMatter.slug,
pageUrl: window.location.href,
pageTitle: frontMatter.title,
theme: commentsTheme,
}}
/>
</div>
);
};
export default Cusdis;

View File

@ -1,37 +0,0 @@
import React, { useState } from 'react'
import siteMetadata from '@/data/siteMetadata'
const Disqus = ({ frontMatter }) => {
const [enableLoadComments, setEnabledLoadComments] = useState(true)
const COMMENTS_ID = 'disqus_thread'
function LoadComments() {
setEnabledLoadComments(false)
window.disqus_config = function () {
this.page.url = window.location.href
this.page.identifier = frontMatter.slug
}
if (window.DISQUS === undefined) {
const script = document.createElement('script')
script.src = 'https://' + siteMetadata.comment.disqusConfig.shortname + '.disqus.com/embed.js'
script.setAttribute('data-timestamp', +new Date())
script.setAttribute('crossorigin', 'anonymous')
script.async = true
document.body.appendChild(script)
} else {
window.DISQUS.reset({ reload: true })
}
}
return (
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
{enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
<div className="disqus-frame" id={COMMENTS_ID} />
</div>
)
}
export default Disqus

View File

@ -0,0 +1,51 @@
import React, { useState } from 'react';
import siteMetadata from '@/data/siteMetadata';
import { PostFrontMatter } from 'types/PostFrontMatter';
interface Props {
frontMatter: PostFrontMatter;
}
const Disqus = ({ frontMatter }: Props) => {
const [enableLoadComments, setEnabledLoadComments] = useState(true);
const COMMENTS_ID = 'disqus_thread';
function LoadComments() {
setEnabledLoadComments(false);
// @ts-ignore
window.disqus_config = function () {
this.page.url = window.location.href;
this.page.identifier = frontMatter.slug;
};
// @ts-ignore
if (window.DISQUS === undefined) {
const script = document.createElement('script');
script.src =
'https://' +
siteMetadata.comment.disqusConfig.shortname +
'.disqus.com/embed.js';
// @ts-ignore
script.setAttribute('data-timestamp', +new Date());
script.setAttribute('crossorigin', 'anonymous');
script.async = true;
document.body.appendChild(script);
} else {
// @ts-ignore
window.DISQUS.reset({ reload: true });
}
}
return (
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
{enableLoadComments && (
<button onClick={LoadComments}>Load Comments</button>
)}
<div className="disqus-frame" id={COMMENTS_ID} />
</div>
);
};
export default Disqus;

View File

@ -1,72 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react'
import { useTheme } from 'next-themes'
import siteMetadata from '@/data/siteMetadata'
const Giscus = () => {
const [enableLoadComments, setEnabledLoadComments] = useState(true)
const { theme, resolvedTheme } = useTheme()
const commentsTheme =
siteMetadata.comment.giscusConfig.themeURL === ''
? theme === 'dark' || resolvedTheme === 'dark'
? siteMetadata.comment.giscusConfig.darkTheme
: siteMetadata.comment.giscusConfig.theme
: siteMetadata.comment.giscusConfig.themeURL
const COMMENTS_ID = 'comments-container'
const LoadComments = useCallback(() => {
setEnabledLoadComments(false)
const {
repo,
repositoryId,
category,
categoryId,
mapping,
reactions,
metadata,
inputPosition,
lang,
} = siteMetadata?.comment?.giscusConfig
const script = document.createElement('script')
script.src = 'https://giscus.app/client.js'
script.setAttribute('data-repo', repo)
script.setAttribute('data-repo-id', repositoryId)
script.setAttribute('data-category', category)
script.setAttribute('data-category-id', categoryId)
script.setAttribute('data-mapping', mapping)
script.setAttribute('data-reactions-enabled', reactions)
script.setAttribute('data-emit-metadata', metadata)
script.setAttribute('data-input-position', inputPosition)
script.setAttribute('data-lang', lang)
script.setAttribute('data-theme', commentsTheme)
script.setAttribute('crossorigin', 'anonymous')
script.async = true
const comments = document.getElementById(COMMENTS_ID)
if (comments) comments.appendChild(script)
return () => {
const comments = document.getElementById(COMMENTS_ID)
if (comments) comments.innerHTML = ''
}
}, [commentsTheme])
// Reload on theme change
useEffect(() => {
const iframe = document.querySelector('iframe.giscus-frame')
if (!iframe) return
LoadComments()
}, [LoadComments])
return (
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
{enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
<div className="giscus" id={COMMENTS_ID} />
</div>
)
}
export default Giscus

View File

@ -0,0 +1,78 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useTheme } from 'next-themes';
import siteMetadata from '@/data/siteMetadata';
interface Props {
mapping: string;
}
const Giscus = ({ mapping }: Props) => {
const [enableLoadComments, setEnabledLoadComments] = useState(true);
const { theme, resolvedTheme } = useTheme();
const commentsTheme =
siteMetadata.comment.giscusConfig.themeURL === ''
? theme === 'dark' || resolvedTheme === 'dark'
? siteMetadata.comment.giscusConfig.darkTheme
: siteMetadata.comment.giscusConfig.theme
: siteMetadata.comment.giscusConfig.themeURL;
const COMMENTS_ID = 'comments-container';
const LoadComments = useCallback(() => {
setEnabledLoadComments(false);
const script = document.createElement('script');
script.src = 'https://giscus.app/client.js';
script.setAttribute('data-repo', siteMetadata.comment.giscusConfig.repo);
script.setAttribute(
'data-repo-id',
siteMetadata.comment.giscusConfig.repositoryId
);
script.setAttribute(
'data-category',
siteMetadata.comment.giscusConfig.category
);
script.setAttribute(
'data-category-id',
siteMetadata.comment.giscusConfig.categoryId
);
script.setAttribute('data-mapping', mapping);
script.setAttribute(
'data-reactions-enabled',
siteMetadata.comment.giscusConfig.reactions
);
script.setAttribute(
'data-emit-metadata',
siteMetadata.comment.giscusConfig.metadata
);
script.setAttribute('data-theme', commentsTheme);
script.setAttribute('crossorigin', 'anonymous');
script.async = true;
const comments = document.getElementById(COMMENTS_ID);
if (comments) comments.appendChild(script);
return () => {
const comments = document.getElementById(COMMENTS_ID);
if (comments) comments.innerHTML = '';
};
}, [commentsTheme, mapping]);
// Reload on theme change
useEffect(() => {
const iframe = document.querySelector('iframe.giscus-frame');
if (!iframe) return;
LoadComments();
}, [LoadComments]);
return (
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
{enableLoadComments && (
<button onClick={LoadComments}>Load Comments</button>
)}
<div className="giscus" id={COMMENTS_ID} />
</div>
);
};
export default Giscus;

View File

@ -1,52 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react'
import { useTheme } from 'next-themes'
import siteMetadata from '@/data/siteMetadata'
const Utterances = () => {
const [enableLoadComments, setEnabledLoadComments] = useState(true)
const { theme, resolvedTheme } = useTheme()
const commentsTheme =
theme === 'dark' || resolvedTheme === 'dark'
? siteMetadata.comment.utterancesConfig.darkTheme
: siteMetadata.comment.utterancesConfig.theme
const COMMENTS_ID = 'comments-container'
const LoadComments = useCallback(() => {
setEnabledLoadComments(false)
const script = document.createElement('script')
script.src = 'https://utteranc.es/client.js'
script.setAttribute('repo', siteMetadata.comment.utterancesConfig.repo)
script.setAttribute('issue-term', siteMetadata.comment.utterancesConfig.issueTerm)
script.setAttribute('label', siteMetadata.comment.utterancesConfig.label)
script.setAttribute('theme', commentsTheme)
script.setAttribute('crossorigin', 'anonymous')
script.async = true
const comments = document.getElementById(COMMENTS_ID)
if (comments) comments.appendChild(script)
return () => {
const comments = document.getElementById(COMMENTS_ID)
if (comments) comments.innerHTML = ''
}
}, [commentsTheme])
// Reload on theme change
useEffect(() => {
const iframe = document.querySelector('iframe.utterances-frame')
if (!iframe) return
LoadComments()
}, [LoadComments])
// Added `relative` to fix a weird bug with `utterances-frame` position
return (
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
{enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
<div className="utterances-frame relative" id={COMMENTS_ID} />
</div>
)
}
export default Utterances

View File

@ -0,0 +1,58 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useTheme } from 'next-themes';
import siteMetadata from '@/data/siteMetadata';
interface Props {
issueTerm: string;
}
const Utterances = ({ issueTerm }: Props) => {
const [enableLoadComments, setEnabledLoadComments] = useState(true);
const { theme, resolvedTheme } = useTheme();
const commentsTheme =
theme === 'dark' || resolvedTheme === 'dark'
? siteMetadata.comment.utterancesConfig.darkTheme
: siteMetadata.comment.utterancesConfig.theme;
const COMMENTS_ID = 'comments-container';
const LoadComments = useCallback(() => {
setEnabledLoadComments(false);
const script = document.createElement('script');
script.src = 'https://utteranc.es/client.js';
script.setAttribute('repo', siteMetadata.comment.utterancesConfig.repo);
script.setAttribute('issue-term', issueTerm);
script.setAttribute('label', siteMetadata.comment.utterancesConfig.label);
script.setAttribute('theme', commentsTheme);
script.setAttribute('crossorigin', 'anonymous');
script.async = true;
const comments = document.getElementById(COMMENTS_ID);
if (comments) comments.appendChild(script);
return () => {
const comments = document.getElementById(COMMENTS_ID);
if (comments) comments.innerHTML = '';
};
}, [commentsTheme, issueTerm]);
// Reload on theme change
useEffect(() => {
const iframe = document.querySelector('iframe.utterances-frame');
if (!iframe) return;
LoadComments();
}, [LoadComments]);
// Added `relative` to fix a weird bug with `utterances-frame` position
return (
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
{enableLoadComments && (
<button onClick={LoadComments}>Load Comments</button>
)}
<div className="utterances-frame relative" id={COMMENTS_ID} />
</div>
);
};
export default Utterances;

View File

@ -0,0 +1,93 @@
import { createRef } from 'preact';
import React, { useLayoutEffect, useMemo, useRef } from 'react';
interface DataAttributes {
[key: string]: string | boolean | undefined;
}
const insertScript = (
src: string,
id: string,
dataAttributes: DataAttributes,
onload = () => {}
) => {
const script = window.document.createElement('script');
script.async = true;
script.src = src;
script.id = id;
if (document.getElementById(id)) {
return;
}
script.addEventListener('load', onload, { capture: true, once: true });
Object.entries(dataAttributes).forEach(([key, value]) => {
if (value === undefined) {
return;
}
script.setAttribute(`data-${key}`, value.toString());
});
document.body.appendChild(script);
return () => {
script.remove();
};
};
const ReactCommento = ({
url,
cssOverride,
autoInit,
noFonts,
hideDeleted,
pageId,
}: {
url: string;
cssOverride?: string;
autoInit?: boolean;
noFonts?: boolean;
hideDeleted?: boolean;
pageId?: string;
}) => {
const containerId = useMemo(
() => `commento-${Math.random().toString().slice(2, 8)}`,
[]
);
const container = createRef<HTMLDivElement>();
useLayoutEffect(() => {
if (!window) {
return;
}
window['commento'] = container.current;
const removeScript = insertScript(
url,
`${containerId}-script`,
{
'css-override': cssOverride,
'auto-init': autoInit,
'no-fonts': noFonts,
'hide-deleted': hideDeleted,
'page-id': pageId,
'id-root': containerId,
},
() => {
removeScript();
}
);
}, [
autoInit,
cssOverride,
hideDeleted,
noFonts,
pageId,
url,
containerId,
container,
]);
return <div ref={container} id={containerId} />;
};
export default ReactCommento;

View File

@ -1,39 +0,0 @@
import siteMetadata from '@/data/siteMetadata'
import dynamic from 'next/dynamic'
const UtterancesComponent = dynamic(
() => {
return import('@/components/comments/Utterances')
},
{ ssr: false }
)
const GiscusComponent = dynamic(
() => {
return import('@/components/comments/Giscus')
},
{ ssr: false }
)
const DisqusComponent = dynamic(
() => {
return import('@/components/comments/Disqus')
},
{ ssr: false }
)
const Comments = ({ frontMatter }) => {
const comment = siteMetadata?.comment
if (!comment || Object.keys(comment).length === 0) return <></>
return (
<div id="comment">
{siteMetadata.comment && siteMetadata.comment.provider === 'giscus' && <GiscusComponent />}
{siteMetadata.comment && siteMetadata.comment.provider === 'utterances' && (
<UtterancesComponent />
)}
{siteMetadata.comment && siteMetadata.comment.provider === 'disqus' && (
<DisqusComponent frontMatter={frontMatter} />
)}
</div>
)
}
export default Comments

View File

@ -0,0 +1,80 @@
import React from 'react';
import siteMetadata from '@/data/siteMetadata';
import dynamic from 'next/dynamic';
import { PostFrontMatter } from 'types/PostFrontMatter';
interface Props {
frontMatter: PostFrontMatter;
}
const UtterancesComponent = dynamic(
() => {
return import('@/components/comments/Utterances');
},
{ ssr: false }
);
const GiscusComponent = dynamic(
() => {
return import('@/components/comments/Giscus');
},
{ ssr: false }
);
const DisqusComponent = dynamic(
() => {
return import('@/components/comments/Disqus');
},
{ ssr: false }
);
const CusdisComponent = dynamic(
() => {
return import('@/components/comments/Cusdis');
},
{ ssr: false }
);
const CommentoComponent = dynamic(
() => {
return import('@/components/comments/Commento');
},
{ ssr: false }
);
const Comments = ({ frontMatter }: Props) => {
let term;
switch (
siteMetadata.comment.giscusConfig.mapping ||
siteMetadata.comment.utterancesConfig.issueTerm
) {
case 'pathname':
term = frontMatter.slug;
break;
case 'url':
term = window.location.href;
break;
case 'title':
term = frontMatter.title;
break;
}
return (
<div id="comment">
{siteMetadata.comment && siteMetadata.comment.provider === 'giscus' && (
<GiscusComponent mapping={term} />
)}
{siteMetadata.comment &&
siteMetadata.comment.provider === 'utterances' && (
<UtterancesComponent issueTerm={term} />
)}
{siteMetadata.comment && siteMetadata.comment.provider === 'disqus' && (
<DisqusComponent frontMatter={frontMatter} />
)}
{siteMetadata.comment && siteMetadata.comment.provider === 'cusdis' && (
<CusdisComponent frontMatter={frontMatter} />
)}
{siteMetadata.comment && siteMetadata.comment.provider === 'commento' && (
<CommentoComponent frontMatter={frontMatter} />
)}
</div>
);
};
export default Comments;

View File

@ -1,9 +1,9 @@
import Mail from './mail.svg'
import Github from './github.svg'
import Facebook from './facebook.svg'
import Youtube from './youtube.svg'
import Linkedin from './linkedin.svg'
import Twitter from './twitter.svg'
import Mail from './mail.svg';
import Github from './github.svg';
import Facebook from './facebook.svg';
import Youtube from './youtube.svg';
import Linkedin from './linkedin.svg';
import Twitter from './twitter.svg';
// Icons taken from: https://simpleicons.org/
@ -14,27 +14,30 @@ const components = {
youtube: Youtube,
linkedin: Linkedin,
twitter: Twitter,
}
};
const SocialIcon = ({ kind, href, size = 8 }) => {
if (!href || (kind === 'mail' && !/^mailto:\w+([.-]?\w+)@\w+([.-]?\w+)(.\w{2,3})+$/.test(href)))
return null
if (
!href ||
(kind === 'mail' &&
!/^mailto:\w+([.-]?\w+)@\w+([.-]?\w+)(.\w{2,3})+$/.test(href))
)
return null;
const SocialSvg = components[kind]
const SocialSvg = components[kind];
return (
<a
className="text-sm text-gray-500 transition hover:text-gray-600"
target="_blank"
rel="noopener noreferrer"
href={href}
>
href={href}>
<span className="sr-only">{kind}</span>
<SocialSvg
className={`fill-current text-gray-700 hover:text-blue-500 dark:text-gray-200 dark:hover:text-blue-400 h-${size} w-${size}`}
/>
</a>
)
}
);
};
export default SocialIcon
export default SocialIcon;

View File

@ -32,7 +32,7 @@
}
.highlight-line {
@apply -mx-4 border-l-4 border-primary-500 bg-gray-700 bg-opacity-50;
@apply -mx-4 border-l-4 border-primary-500 bg-gray-700 bg-opacity-50;
}
.line-number::before {

View File

@ -1,6 +1,6 @@
---
name: Ivan Li
avatar: https://pan.ivanli.cc/api/v3/file/source/1234/%E5%A4%B4%E5%83%8F.png?sign=xIgy54DyFRYupxjZJbK02HmpKX8C53YR-O0I18Rxm70%3D%3A0
avatar: https://pan.ivanli.cc/api/v3/file/source/1234/头像.png?sign=xIgy54DyFRYupxjZJbK02HmpKX8C53YR-O0I18Rxm70%3D%3A0
occupation: Web Full Stack Developer
email: master@ivanli.cc
github: https://github.com/IvanLi-CN

View File

@ -1,12 +0,0 @@
---
name: Sparrow Hawk
avatar: /static/images/sparrowhawk-avatar.jpg
occupation: Wizard of Earthsea
company: Earthsea
twitter: https://twitter.com/sparrowhawk
linkedin: https://www.linkedin.com/sparrowhawk
---
At birth, Ged was given the child-name Duny by his mother. He was born on the island of Gont, as a son of a bronzesmith. His mother died before he reached the age of one. As a small boy, Ged had overheard the village witch, his maternal aunt, using various words of power to call goats. Ged later used the words without an understanding of their meanings, to surprising effect.
The witch knew that using words of power effectively without understanding them required innate power, so she endeavored to teach him what little she knew. After learning more from her, he was able to call animals to him. Particularly, he was seen in the company of wild sparrowhawks so often that his "use name" became Sparrowhawk.

View File

@ -0,0 +1,19 @@
---
title: 搭建日常使用的 Arch Linux
date: '2022-10-17'
tags: ['Arch Linux', '环境搭建', 'VPS']
draft: false
summary: 有了上次快速安装步骤后,接下来就是使用这个环境了。要使用环境,首先需要做一些初始化操作。我的步骤适合我,但不一定适合你,只是记录和参考。
images:
[
'https://pan.ivanli.cc/api/v3/file/source/2238/archlinux-logo-light.png?sign=bWxqFFy3RUDT5UsWb4UD5byt-_L4h79wede3runRKFc%3D%3A0',
]
---
## Docker
登录私有仓库,以便拉取镜像。
```zsh
docker login -u="ivan+hk_nat" docker-registry.ivanli.cc
```

View File

@ -0,0 +1,221 @@
---
title: 在 PVE 中运行 Arch Linux
date: '2022-02-18'
lastmod: '2022-09-17'
tags: ['Arch Linux', 'Linux', 'PVE']
draft: false
summary: Arch Linux 的好,懂的都懂。这次在 PVE 中的 LCX 虚拟化了几个 Arch Linux 环境,用于跑一些服务和开发环境。本文主要分享了 Arch Linux 的配置步骤,其他方式入教的同志也可参考本文配置。
---
Arch Linux 准入门槛确实有点高,在 PVE 中,使用 LCX 容器运行 Arch Linux似乎是一个不错的选择难度比物理机安装低就是资料也少了许多……不过问题不大毕竟最蛋疼的部分我们可以忽略掉了。前几个月还想着直接在树莓派上安装 Arch Linux操作一波太难了时间有限就没继续搞了(没有备用设备,折腾完怕是要旷工了),最后还是再次给树每派安装了 Manjaro。
## 起步
### 0. 创建 LCX 容器
打开 Proxmox VE选择你的宿主机然后在界面右上角点击“创建 CT”。
然后你就看着搞咯,创建这个没有像 OpenWRT 那样讲究。
值得注意的一点是,记得先在 PVE 中通过 `pveam` 更新并下载 Arch Linux 的模板 _([Proxmox Container Toolkit](https://pve.proxmox.com/pve-docs/chapter-pct.html#pct_container_images))_。
进入容器后,我们将以 `root` 用户登录。
### 1. 配置系统
位置(Location)
先编辑 `/etc/locale.gen`,取消 `en_US.UTF-8 UTF-8` 的注释。
```bash
sed -i "s/#en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/" /etc/locale.gen
```
然后执行:
```bash
locale-gen
```
语言:
然后创建文件 `/etc/locale.conf`,内容如下:
```ini
LANG=en_US.UTF-8
```
命令:
```zsh
echo 'LANG=en_US.UTF-8' > /etc/locale.conf
```
时区
查看当前时区:
```zsh
date +"%Z %z"
```
![image.png](https://notes.ivanli.cc/assets/image_1651218347929_0.png)
如果在中国大陆,那么执行以下命令:
```bash
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
# 验证
date
# outputs:
# Sat Jan 15 23:26:18 CST 2022
```
### 2. 配置 pacman
我们知道 pacman 是 Arch Linux 自带的包管理器,系统到手,得先装点软件,毕竟 Arch Linux 比较简约。
首先配置 pacman 的源的镜像:
```bash
nano /etc/pacman.d/mirrorlist
```
选择你喜欢并且方便连接的镜像,然后删除该行的“#”取消注释。可以选择一个或多个,在前面的优先级高。
开启并行下载,在 `/etc/pacman.conf` 中取消 `ParallelDownloads` 前的注释,值为并行下载数:
```bash
sed -i "s/#ParallelDownloads = 5/ParallelDownloads = 5/" /etc/pacman.conf
```
接下来我们更新已安装的软件,我们的哲学就是时刻保持最新。
```bash
pacman -Syu
```
**一般来说,执行上面的命令后,会拉取索引数据库,之后会优先更新 `archlinuxx-keyring`。如果不是这样的话,应当手动执行下面的代码:**
初始化并刷新 pacman 的 keys。这个 key 是 pacman 的每个用户都拥有的,包括开发者和使用者。所以执行下面两条命令:
```bash
pacman-key --init
pacman-key --populate
pacman-key --refresh-keys
pacman -Sy archlinux-keyring
pacman -Syu
```
没执行上面步骤的,要手动一个个确认软件包开发者的签名……很蛋疼。如果遇到各种错误的话,可以执行下面几条命令后,再执行上面的命令:
```bash
pacman -Sc
pacman-mirrors -f0
rm -fr /etc/pacman.d/gnupg
```
_参考[Cant Upgrade because of keyring - Technical Issues and Assistance / Package update process - Manjaro Linux Forum](https://archived.forum.manjaro.org/t/cant-upgrade-because-of-keyring/106893/10)_
### 3. 创建用户
首先,安装 `sudo`
```bash
pacman -S sudo
```
让我们给自己分配一个具有 sudo 权限的账户
```zsh
useradd -m ivan
passwd ivan
usermod -aG wheel ivan
```
_参考[Create a Sudo User on Arch Linux - Vultr.com](https://www.vultr.com/docs/create-a-sudo-user-on-arch-linux?__cf_chl_captcha_tk__=zPG_V_axFV3IH5lhY2j_1ChaaZgIcdPe_eYDPUOSouY-1642259505-0-gaNycGzNCZE)_
如果 `visudo` 找不到编辑器,那么可以执行:
```zsh
EDITOR=vim visudo
```
接下来使用刚刚创建的用户登录吧!
### 4. 使用 SSH 远程登录
先安装 OpenSSH
```bash
sudo pacman -S openssh
```
然后启用并启动:
```bash
sudo systemctl enable sshd
sudo systemctl start sshd
```
接下来就可以在其他机子上以刚刚的用户通过 ssh 访问了。
### 5. 安装 Yay
安装 AUR 上的软件,怎么少得了 [[yay]] 呢?安装 Yay 需要切换到非 root 账户。
```bash
sudo pacman -S git
sudo pacman -S --needed base-devel
# 上面的命令有选装的项目,简单起见,全都装上
git clone https://aur.archlinux.org/yay.git
cd yay
makepkg -si
```
### 6. Zsh
安装 Zsh
```shell
yay -Sy zsh-git
```
安装 Zinit 和我常用的插件
```shell
sh -c "$(curl -fsSL https://git.io/zinit-install)"
echo 'zinit load zsh-users/zsh-syntax-highlighting
zinit load zsh-users/zsh-autosuggestions
zinit load ael-code/zsh-colored-man-pages
zinit load agkozak/zsh-z
zinit ice depth=1; zinit light romkatv/powerlevel10k' >> ~/.zshrc
```
然后进入到 `zsh` 中,执行一次 `source ~/.zshrc`
```shell
zsh
source ~/.zshrc
```
设置 Zsh 为默认的 shell 程序:
```bash
# 列出所有已安装的 shell 程序
chsh -l
# 从上面的结果中找到 zsh 的完整路径
# 我的是 /bin/zsh
chsh -s /bin/zsh
```
### 7. Docker
安装 Docker 也很简单:
```bash
yay -S docker
# 启动
sudo systemctl start docker
# 启用
sudo systemctl enable docker
# 安装 Compose
yay -S docker-compose
# 添加当前用户到 docker 组
sudo usermod -aG docker $USER
# log in to a new group
newgrp docker
```

View File

@ -0,0 +1,271 @@
---
title: 使用 Xray、acme.sh、Docker Compose 搭建内网穿透服务
date: '2022-06-11'
tags: ['xray', 'acme', 'acme.sh', 'docker', 'docker compose', '内网穿透']
draft: false
summary: 为了能在外直接访问家中网络,我组建了三套方案,一是 [[Xray]],二是 [[ZeroTier]],三是 [[NPS]]。今天,我准备在我上个月购入的服务器上再部署一套 Xray 服务,提高可用性。本次准备完全仰仗 Docker 容器,让我未来迁移服务更加省事。
---
## 简介
为了能在外直接访问家中网络,我组建了三套方案,一是 [Xray](/tags/xray),二是 [ZeroTier](/tags/zerotier),三是 [NPS](/tags/nps)。今天,我准备在我上个月购入的服务器上再部署一套 Xray 服务,提高可用性。本次准备完全仰仗 Docker 容器,让我未来迁移服务更加省事。
## 目标与方案
个人自用,成本得控制到零(bushi),安全性还是得做得好些,所以选用 Xray 来承载功能,使用免费的 TLS CA 来签发证书。由于免费的证书一般有效期比较短 (常见的是 90 天),所以还需要实现自动续签。
Let's Encrypt 和 acme.sh 是不错的组合。不过听说 Let's Encrypt 被收购了,不知道是否有安全风险,未来需要再确认下。由于财力并不雄厚,考虑到未来可能服务会”流离失所“,用容器方案比较好迁移。
## 技术栈
- Xray
一款支持加密传输、内网穿透的网络工具。由 GoLang 编写,支持很多平台。
_官方站点[Project X](https://xtls.github.io/)_
- acme.sh
用于签发 TLS 证书。顾名思义,支持 ACME 协议签发、自动续签证书的脚本。
_官方站点[acmesh-official/acme.sh](https://github.com/acmesh-official/acme.sh)_
- Caddy
用于反向代理部署在家里的 Web 服务。它是现代的反向代理服务。
_官方站点[Caddy 2](https://caddyserver.com/v2)_
- Docker Compose
众所周知?
## 搭建步骤
### Docker Compose
首先需要拥有并运行 Docker 和 Docker Compose。
创建一个用于存放配置文件目录,并进入该目录。
创建 Compose 配置文件:
```bash
touch docker-compose.yml
vim docker-compose.yml
```
文件内容:
```yaml
version: '3.9'
networks:
caddy:
name: caddy
xray:
name: xray
volumes:
caddy-data:
name: caddy-data
caddy-config:
name: caddy-config
acme-sh-data:
name: acme-sh-data
services:
caddy:
image: caddy:2
container_name: caddy
restart: always
ports:
- 80:80
- 443:443
networks:
- caddy
volumes:
- $PWD/caddy/Caddyfile:/etc/caddy/Caddyfile
- $PWD/site:/srv
- caddy-data:/data
- caddy-config:/config
xray:
image: teddysun/xray
container_name: xray
restart: always
networks:
- xray
- caddy
ports:
- 3332-3334:3332-3334
volumes:
- ./xray:/etc/xray
- acme-sh-data:/certs
command: "xray -c=/etc/xray/config.yml"
acme.sh:
image: neilpang/acme.sh
container_name: acme.sh
# restart: always
volumes:
- acme-sh-data:/acme.sh
env_file: acme.env
command: "daemon"
```
### 签发证书
使用 DNS Challenge 来签发证书,所以需要 DNS 服务商的 API 来实现自动化签发流程。
以阿里云举例:
1. 创建 RAM 子账户,并只允许访问 API
2. 复制 key 和 secret
3. 为 RAM 子账户授权 DNS 解析的管理权限。
在当前目录创建 `acme.env` 文件:
```zsh
touch acme.env
vim acme.env
```
文件内容:
```zsh
Ali_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
Ali_Secret="jlsdflanljkljlfdsaklkjflsa"
```
启动 compose 服务:
```zsh
docker-compose up -d
```
前面我们启动了刚刚创建的 compose 服务,现在,我们使用 `acme.sh` 容器运行以下命令签发证书:
```zsh
docker exec acme.sh acme.sh --log --issue --dns dns_ali --server letsencrypt -d ivanli.cc -d "*.ivanli.cc"
```
签发成功后你将会在输出末尾看到如下内容:
![签发成功时,程序输出图示](https://notes.ivanli.cc/assets/image_1654257070519_0.png)
注意,签发通配符证书时,需要一次性将所有通配的子域都写在同一条命令上,使用 `-d` 参数追加。
### 配置 Xray
因为前面挂载了 `acme.sh` 的数据卷,所以默认的证书位于 `/certs/ivanli.cc/` 目录下。证书要使用 `fullchain` 的,避免证书链不完整,导致客户端连接验证失败。
创建 Xray 配置文件:
```zsh
mkdir ./xray
vim ./xray/config.yml
```
内容如下:
```yml
inbounds:
# listening for host-name.home
- tag: host-name.home.in
listen: 0.0.0.0
port: 3332
protocol: vless
settings:
clients:
- id: <uuid> # 你的 UUID
flow: xtls-rprx-direct
decryption: none
streamSettings:
network: tcp
security: xtls
xtlsSettings:
serverName: ivanli.cc
alpn:
- http/1.1
certificates:
- certificateFile: /certs/ivanli.cc/fullchain.cer
keyFile: /certs/ivanli.cc/ivanli.cc.key
# reverse ssh to host-name.home
- tag: ssh.host-name.home.in
listen: 0.0.0.0
port: 3334
protocol: dokodemo-door
settings:
network: tcp
address: 127.0.0.1
port: 22
# reverse http to 101.home
- tag: http.host-name.home.in
listen: 0.0.0.0
port: 3333
protocol: dokodemo-door
settings:
network: tcp
address: 127.0.0.1
port: 80
outbounds:
- protocol: freedom
tag: direct
- tag: blocked
protocol: blackhole
reverse:
portals:
- tag: host-name.home.portal
domain: host-name.home.reverse
routing:
- type: field
inboundTag:
- ssh.host-name.home.in
- http.host-name.home.in
outboundTag: host-name.home.portal
- type: field
domain:
- full:host-name.home.reverse
outboundTag: host-name.home.portal
```
配置说明
- `3332` 端口用于客户端连接服务端;
- `3333` 端口用于 HTTP 穿透,映射了 `server:3333 <--> client:80` 端口;
- `3334` 端口用于 SSH 穿透。
- 如果需要连接更多的内网主机和端口,可以继续依葫芦画瓢地加。
### 配置 Caddy
为了让我们的 Web 站点能够公开到互联网,并且增强可控性,没有直接公开 Xray 的端口,而是使用 Caddy 反向代理 Xray 的穿透的本地端口。
创建 Caddy 配置文件:
```zsh
mkdir ./caddy
vim ./caddy/Caddyfile
```
内容如下:
```caddyfile
{
servers {
protocol {
allow_h2c
}
}
admin off
}
any-service.ivanli.cc, another-service.ivanli.cc {
reverse_proxy http://localhost:3333
}
```
端口 `3333` 是 Xray Server 映射家里 HTTP 服务的端口,所以我们这里反向代理服务器上的 3333 端口就好了。
因为 Caddy 会自动从 CA 签发证书,所以这里不需要我们手动配置证书。
配置完成后,重启服务就好
```zsh
docker-compose restart
```
现在,你拥有一个安全的内网穿透服务了~
用户通过 HTTPS 协议访问服务器,服务器通过 TLS 加密连接与内网主机通讯。
TODO 自动重启

View File

@ -1,38 +0,0 @@
---
title: Sample .md file
date: '2016-03-08'
tags: ['markdown', 'code', 'features']
draft: false
summary: Example of a markdown file with code blocks and syntax highlighting
---
A sample post with markdown.
## Inline Highlighting
Sample of inline highlighting `sum = parseInt(num1) + parseInt(num2)`
## Code Blocks
Some Javascript code
```javascript
var num1, num2, sum
num1 = prompt('Enter first number')
num2 = prompt('Enter second number')
sum = parseInt(num1) + parseInt(num2) // "+" means "add"
alert('Sum = ' + sum) // "+" means combine into a string
```
Some Python code 🐍
```python
def fib():
a, b = 0, 1
while True: # First iteration:
yield a # yield 0 to start with and then
a, b = b, a + b # a will now be 1, and b will also be 1, (0 + 1)
for index, fibonacci_number in zip(range(10), fib()):
print('{i:3}: {f:3}'.format(i=index, f=fibonacci_number))
```

View File

@ -0,0 +1,181 @@
---
title: 在 PVE 宿主机上使用桌面环境
date: '2022-10-28'
tags: ['PVE', 'PVE', 'DE', '环境搭建', 'Debian']
draft: false
summary: 虽然 PVE 宿主机不应该安装乱七八糟的东西,但是我穷,为了物尽其用,为了在主力电脑翻车时有一个立即可用的备用环境,所以还是安装了基础的桌面环境。现在的 Linux 桌面环境越来越好了,我选择安装 KDE Plasma 作为桌面环境,并且默认关闭,按需启用。
images:
[
'https://pan.ivanli.cc/api/v3/file/source/2243/1200px-Kde_dragons.png?sign=yGZL9jYeVt53Ve43ddhHt_0EzVV2cW_WbxHc0dEcwWY%3D%3A0',
]
---
## 前言
过几天就是双十一了,或许是我去购物网站上看了下显卡价格吧,我的显卡当晚就闹情绪,不工作还引发宕机。虽然拔了显卡,我还能用核显开机,但是我懒呀,所以我花了一个晚上在 PVE 宿主机上搭了一个临时环境,用于日常娱乐(看番、听歌)和一般工作(敲代码)。还别说,我在一开始装 PVE 时,就预先装上了桌面环境,这就是预判呀!
现在 Linux 桌面环境已经非常好了,相比 17 年左右的体验,又上了一个新的台阶。不过,作为临时应急环境,倒也不会去装那些没啥用的国产软件,本着够用就好的原则,主要是以 Web App > Web > Linux Client 的顺序挑选软件。一般来说,我用到的也不多:
- **浏览器Google Chrome**。主要是好用,能同步,还能远程桌面。
## 准备
首先应该拥有自己的账户,否则你将会发现自己无法登录桌面环境。因为桌面环境默认在登录时没有 `root` 用户选项。
### 创建账户:
```bash
useradd -m ivan
passwd ivan
usermod -aG wheel ivan
```
给刚刚创建的账户分配一个具有 sudo 权限的账户
```bash
EDITOR=vim visudo
```
找到 `%wheel ALL=(ALL: ALL) ALL` 这行,取消这行的注释。
现在,你自己的账号具有 sudo 权限了。
### 生成 SSH 密钥
2022 年,应该生成 `ed25519` 算法的密钥:
```bash
ssh-keygen -t ed25519
```
## 启用和禁用桌面环境
**使用 `root` 账户执行下面的命令!**
查看当前的默认目标:
```bash
systemctl get-default
```
临时禁用图形界面:
```bash
init 3
```
临时启用图形界面:
```bash
init 5
```
永久禁用图形界面:重启生效:
```bash
systemctl set-default multi-user.target
```
永久启用图形界面,重启生效:
```bash
systemctl set-default graphical.target
```
## Google Chrome Browser
安装方式就是直接[官网下载](https://www.google.com/chrome/)。下载完成后双击打开安装。
或者通过命令行安装:
```bash
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo apt install ./google-chrome-stable_current_amd64.deb
```
安装过程中可能会出错,可以使用命令进行安装,然后根据提示修复问题。修复过程中可能会重启电脑。具体情况我没留意,下次遇到的话再补充,嘿嘿。
## VS Code
同样从官网下载安装:[Download Visual Studio Code - Mac, Linux, Windows](https://code.visualstudio.com/download)
### 同步问题
参考:[Visual Studio Code 中的设置同步](https://code.visualstudio.com/docs/editor/settings-sync#_linux)
我用的是 KDE Plasma似乎[再等等](https://github.com/microsoft/vscode/issues/104319#issuecomment-1250089491)就能直接正常使用了,所以我先忍受同步问题吧。
## 中文输入法
我使用 iBus + Rime + 小鹤音形.
执行以下命令安装 iBus + Rime
```bash
sudo apt install ibus ibus-rime
```
接下来配置小鹤音形方案。
访问[小鹤的网盘](http://flypy.ysepan.com/)下载小鹤音形的挂接文件,小狼毫、鼠须管的都可以。
下载完成后解压出来,把压缩文件里的 `rime` 目录复制到 `/home/ivan/.config/ibus/rime`
```bash
# 如果你没有 unzip通过下面命令安装
# sudo apt install unzip
cd ~/Downloads
unzip '小鹤音形“鼠须管”for macOS.zip'
cd '小鹤音形Rime平台鼠须管for macOS'
cp -r ./rime ~/.config/ibus/rime
```
创建 `~/.config/ibus/rime/default.custom.yaml` 文件,并设为以下内容:
```yaml
patch:
schema_list:
- { schema: flypy }
- { schema: luna_pinyin }
```
参考:[分享我的输入法配置 Rime 小狼豪 + 小鹤音形) - 炒饭之道](https://itx.ink/2018/11/21/SHARE_MY_RIME/)
配置 iBus 环境变量:
```bash
cat >> ~/.zshrc <<EOF
# ibus
export GTK_IM_MODULE=ibus
export XMODIFIERS=@im=ibus
export QT_IM_MODULE=ibus
EOF
```
启动 ibus
```bash
ibus-setup
```
在打开的 GUI 中添加中文输入法,找到 Rime 并添加输入法:
![rime](https://pan.ivanli.cc/api/v3/file/source/2241/Screen%20Capture_select-area_20221028225457.png?sign=XVrl7rPk4Gd7QRFBCCDGruB2L7V1bvxDpK9-v9pC0Nc%3D%3A0)
现在,新打开的软件应该能使用输入法了。像 Chrome 这类,关闭后还需要手动杀死进程后再打开才能使用。最简单的方法就是重启电脑啦~
## 快捷键
我习惯使用 Mac OS 系统的快捷键,所以 [Kinto](https://github.com/rbreaves/kinto) 是我的不二之选。key
安装:
```bash
/bin/bash -c "$(wget -qO- https://raw.githubusercontent.com/rbreaves/kinto/HEAD/install/linux.sh || curl -fsSL https://raw.githubusercontent.com/rbreaves/kinto/HEAD/install/linux.sh)"
```
卸载:
```bash
/bin/bash <( wget -qO- https://raw.githubusercontent.com/rbreaves/kinto/HEAD/install/linux.sh || curl -fsSL https://raw.githubusercontent.com/rbreaves/kinto/HEAD/install/linux.sh ) -r
```

View File

@ -1,141 +0,0 @@
---
title: Deriving the OLS Estimator
date: '2020-12-21'
tags: ['next js', 'math', 'ols']
draft: false
summary: 'How to derive the OLS Estimator with matrix notation and a tour of math typesetting using markdown with the help of KaTeX.'
---
# Introduction
Parsing and display of math equations is included in this blog template. Parsing of math is enabled by `remark-math` and `rehype-katex`.
KaTeX and its associated font is included in `_document.js` so feel free to use it on any page.
^[For the full list of supported TeX functions, check out the [KaTeX documentation](https://katex.org/docs/supported.html)]
Inline math symbols can be included by enclosing the term between the `$` symbol.
Math code blocks are denoted by `$$`.
If you intend to use the `$` sign instead of math, you can escape it (`\$`), or specify the HTML entity (`&dollar;`) [^2]
Inline or manually enumerated footnotes are also supported. Click on the links above to see them in action.
[^2]: \$10 and &dollar;20.
# Deriving the OLS Estimator
Using matrix notation, let $n$ denote the number of observations and $k$ denote the number of regressors.
The vector of outcome variables $\mathbf{Y}$ is a $n \times 1$ matrix,
```tex
\mathbf{Y} = \left[\begin{array}
{c}
y_1 \\
. \\
. \\
. \\
y_n
\end{array}\right]
```
$$
\mathbf{Y} = \left[\begin{array}
{c}
y_1 \\
. \\
. \\
. \\
y_n
\end{array}\right]
$$
The matrix of regressors $\mathbf{X}$ is a $n \times k$ matrix (or each row is a $k \times 1$ vector),
```latex
\mathbf{X} = \left[\begin{array}
{ccccc}
x_{11} & . & . & . & x_{1k} \\
. & . & . & . & . \\
. & . & . & . & . \\
. & . & . & . & . \\
x_{n1} & . & . & . & x_{nn}
\end{array}\right] =
\left[\begin{array}
{c}
\mathbf{x}'_1 \\
. \\
. \\
. \\
\mathbf{x}'_n
\end{array}\right]
```
$$
\mathbf{X} = \left[\begin{array}
{ccccc}
x_{11} & . & . & . & x_{1k} \\
. & . & . & . & . \\
. & . & . & . & . \\
. & . & . & . & . \\
x_{n1} & . & . & . & x_{nn}
\end{array}\right] =
\left[\begin{array}
{c}
\mathbf{x}'_1 \\
. \\
. \\
. \\
\mathbf{x}'_n
\end{array}\right]
$$
The vector of error terms $\mathbf{U}$ is also a $n \times 1$ matrix.
At times it might be easier to use vector notation. For consistency, I will use the bold small x to denote a vector and capital letters to denote a matrix. Single observations are denoted by the subscript.
## Least Squares
**Start**:
$$y_i = \mathbf{x}'_i \beta + u_i$$
**Assumptions**:
1. Linearity (given above)
2. $E(\mathbf{U}|\mathbf{X}) = 0$ (conditional independence)
3. rank($\mathbf{X}$) = $k$ (no multi-collinearity i.e. full rank)
4. $Var(\mathbf{U}|\mathbf{X}) = \sigma^2 I_n$ (Homoskedascity)
**Aim**:
Find $\beta$ that minimises the sum of squared errors:
$$
Q = \sum_{i=1}^{n}{u_i^2} = \sum_{i=1}^{n}{(y_i - \mathbf{x}'_i\beta)^2} = (Y-X\beta)'(Y-X\beta)
$$
**Solution**:
Hints: $Q$ is a $1 \times 1$ scalar, by symmetry $\frac{\partial b'Ab}{\partial b} = 2Ab$.
Take matrix derivative w.r.t $\beta$:
```tex
\begin{aligned}
\min Q & = \min_{\beta} \mathbf{Y}'\mathbf{Y} - 2\beta'\mathbf{X}'\mathbf{Y} +
\beta'\mathbf{X}'\mathbf{X}\beta \\
& = \min_{\beta} - 2\beta'\mathbf{X}'\mathbf{Y} + \beta'\mathbf{X}'\mathbf{X}\beta \\
\text{[FOC]}~~~0 & = - 2\mathbf{X}'\mathbf{Y} + 2\mathbf{X}'\mathbf{X}\hat{\beta} \\
\hat{\beta} & = (\mathbf{X}'\mathbf{X})^{-1}\mathbf{X}'\mathbf{Y} \\
& = (\sum^{n} \mathbf{x}_i \mathbf{x}'_i)^{-1} \sum^{n} \mathbf{x}_i y_i
\end{aligned}
```
$$
\begin{aligned}
\min Q & = \min_{\beta} \mathbf{Y}'\mathbf{Y} - 2\beta'\mathbf{X}'\mathbf{Y} +
\beta'\mathbf{X}'\mathbf{X}\beta \\
& = \min_{\beta} - 2\beta'\mathbf{X}'\mathbf{Y} + \beta'\mathbf{X}'\mathbf{X}\beta \\
\text{[FOC]}~~~0 & = - 2\mathbf{X}'\mathbf{Y} + 2\mathbf{X}'\mathbf{X}\hat{\beta} \\
\hat{\beta} & = (\mathbf{X}'\mathbf{X})^{-1}\mathbf{X}'\mathbf{Y} \\
& = (\sum^{n} \mathbf{x}_i \mathbf{x}'_i)^{-1} \sum^{n} \mathbf{x}_i y_i
\end{aligned}
$$

View File

@ -0,0 +1,19 @@
---
title: 你好,我的朋友!
date: '2022-07-19'
tags: ['Blog', 'Markdown', 'Next.js', 'Tailwind CSS']
draft: false
summary: 这是本站当前版本的第一篇文章。2015 - 2022希望是最后一次重开博客。
---
2015 年开始,我开发了自己的第一个博客站,当时还是使用 PHP 5 + MySQL + HTML + CSS 3 开发的。当时非常地执着,不想用 JavaScript希望用纯 CSS 实现所有功能。当然,这所有功能也很朴素。不过最后好像还是加上了一些 JavaScript 代码。当年使用 PHP主要是因为更早之前我一直在运营和维护 Discuz! 论坛,混“机油”圈。当时认识了很多网友呢。
后来,博客慢慢迭代,之后的后端都是使用 Node.js 了,前端从 Vue (Nuxt.js) -> Angular -> React (Next.js)。现在,我发现文章存数据库里是非常不明智的选择,因为我每次重写博客,都不想迁移文章……是的,就是不想……以至于后来,我开始考虑从文件生成静态站点。毕竟我们现在拥有 Markdown 这么棒的格式。
本来 22 年初的时候就准备放弃 React 版本的博客了,为什么要放弃呢?因为去年一年一直在研究微服务,全部自己实现确实费时费力,博客迟迟没有完全完成,所以当时那版博客也没有在主域名上部署。后来我就一直在使用 Logseq 做笔记,写文章。不过我发现这家伙确实不方便完整地将内容无损地转换为静态站点……这就是现在这版本博客上线的原因了。这么想想我的主域名吃灰了一年有余了……
不知道会不会坚持经常更新博客,因为我还是比较喜欢记录一些碎片内容,以备将来回顾。而博文主要还是为了做一些分享。做分享,动力还是有些不足,而且学海无涯,要学的东西还是真的多。精力有限,估计短期是无法预见能有经常分享的机会了。
总之,先上线再说。后面会陆续添加评论、评分和通知功能。会尽量使用开源、自部署的程序。自从看上了容器化和容器编排技术,我已经爱上了用开源项目自部署一堆服务,配合使用。
_日积月累厚积薄发。_

View File

@ -1,198 +0,0 @@
---
title: 'Markdown Guide'
date: '2019-10-11'
tags: ['github', 'guide']
draft: false
summary: 'Markdown cheatsheet for all your blogging needs - headers, lists, images, tables and more! An illustrated guide based on GitHub Flavored Markdown.'
---
# Introduction
Markdown and Mdx parsing is supported via `unified`, and other remark and rehype packages. `next-mdx-remote` allows us to parse `.mdx` and `.md` files in a more flexible manner without touching webpack.
GitHub flavored markdown is used. `mdx-prism` provides syntax highlighting capabilities for code blocks. Here's a demo of how everything looks.
The following markdown cheatsheet is adapted from: https://guides.github.com/features/mastering-markdown/
# What is Markdown?
Markdown is a way to style text on the web. You control the display of the document; formatting words as bold or italic, adding images, and creating lists are just a few of the things we can do with Markdown. Mostly, Markdown is just regular text with a few non-alphabetic characters thrown in, like `#` or `*`.
# Syntax guide
Heres an overview of Markdown syntax that you can use anywhere on GitHub.com or in your own text files.
## Headers
```
# This is a h1 tag
## This is a h2 tag
#### This is a h4 tag
```
# This is a h1 tag
## This is a h2 tag
#### This is a h4 tag
## Emphasis
```
_This text will be italic_
**This text will be bold**
_You **can** combine them_
```
_This text will be italic_
**This text will be bold**
_You **can** combine them_
## Lists
### Unordered
```
- Item 1
- Item 2
- Item 2a
- Item 2b
```
- Item 1
- Item 2
- Item 2a
- Item 2b
### Ordered
```
1. Item 1
1. Item 2
1. Item 3
1. Item 3a
1. Item 3b
```
1. Item 1
1. Item 2
1. Item 3
1. Item 3a
1. Item 3b
## Images
```
![GitHub Logo](https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png)
Format: ![Alt Text](url)
```
![GitHub Logo](https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png)
## Links
```
http://github.com - automatic!
[GitHub](http://github.com)
```
http://github.com - automatic!
[GitHub](http://github.com)
## Blockquotes
```
As Kanye West said:
> We're living the future so
> the present is our past.
```
As Kanye West said:
> We're living the future so
> the present is our past.
## Inline code
```
I think you should use an
`<addr>` element here instead.
```
I think you should use an
`<addr>` element here instead.
## Syntax highlighting
Heres an example of how you can use syntax highlighting with [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/):
````
```js:fancyAlert.js
function fancyAlert(arg) {
if (arg) {
$.facebox({ div: '#foo' })
}
}
```
````
And here's how it looks - nicely colored with styled code titles!
```js:fancyAlert.js
function fancyAlert(arg) {
if (arg) {
$.facebox({ div: '#foo' })
}
}
```
## Footnotes
```
Here is a simple footnote[^1]. With some additional text after it.
[^1]: My reference.
```
Here is a simple footnote[^1]. With some additional text after it.
[^1]: My reference.
## Task Lists
```
- [x] list syntax required (any unordered or ordered list supported)
- [x] this is a complete item
- [ ] this is an incomplete item
```
- [x] list syntax required (any unordered or ordered list supported)
- [x] this is a complete item
- [ ] this is an incomplete item
## Tables
You can create tables by assembling a list of words and dividing them with hyphens `-` (for the first row), and then separating each column with a pipe `|`:
```
| First Header | Second Header |
| --------------------------- | ---------------------------- |
| Content from cell 1 | Content from cell 2 |
| Content in the first column | Content in the second column |
```
| First Header | Second Header |
| --------------------------- | ---------------------------- |
| Content from cell 1 | Content from cell 2 |
| Content in the first column | Content in the second column |
## Strikethrough
Any word wrapped with two tildes (like `~~this~~`) will appear ~~crossed out~~.

View File

@ -1,76 +0,0 @@
---
title: Images in Next.js
date: '2020-11-11'
tags: ['next js', 'guide']
draft: false
summary: 'In this article we introduce adding images in the tailwind starter blog and the benefits and limitations of the next/image component.'
authors: ['sparrowhawk']
---
# Introduction
The tailwind starter blog has out of the box support for [Next.js's built-in image component](https://nextjs.org/docs/api-reference/next/image) and automatically swaps out default image tags in markdown or mdx documents to use the Image component provided.
# Usage
To use in a new page route / javascript file, simply import the image component and call it e.g.
```js
import Image from 'next/image'
function Home() {
return (
<>
<h1>My Homepage</h1>
<Image src="/me.png" alt="Picture of the author" width={500} height={500} />
<p>Welcome to my homepage!</p>
</>
)
}
export default Home
```
For a markdown file, the default image tag can be used and the default `img` tag gets replaced by the `Image` component in the build process.
Assuming we have a file called `ocean.jpg` in `data/img/ocean.jpg`, the following line of code would generate the optimized image.
```
![ocean](/static/images/ocean.jpg)
```
Alternatively, since we are using mdx, we can just use the image component directly! Note, that you would have to provide a fixed width and height. The `img` tag method parses the dimension automatically.
```js
<Image alt="ocean" src="/static/images/ocean.jpg" width={256} height={128} />
```
_Note_: If you try to save the image, it is in webp format, if your browser supports it!
![ocean](/static/images/ocean.jpeg)
<p>
Photo by [YUCAR
FotoGrafik](https://unsplash.com/@yucar?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText)
on
[Unsplash](https://unsplash.com/s/photos/sea?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText)
</p>
# Benefits
- Smaller image size with Webp (~30% smaller than jpeg)
- Responsive images - the correct image size is served based on the user's viewport
- Lazy loading - images load as they are scrolled to the viewport
- Avoids [Cumulative Layout Shift](https://web.dev/cls/)
- Optimization on demand instead of build-time - no increase in build time!
# Limitations
- Due to the reliance on `next/image`, unless you are using an external image CDN like Cloudinary or Imgix, it is practically required to use Vercel for hosting. This is because the component acts like a serverless function that calls a highly optimized image CDN.
If you do not want to be tied to Vercel, you can remove `imgToJsx` in `remarkPlugins` in `lib/mdx.js`. This would avoid substituting the default `img` tag.
Alternatively, one could wait for image optimization at build time to be supported. A different library, [next-optimized-images](https://github.com/cyrilwanner/next-optimized-images) does that, although it requires transforming the images through webpack which is not done here.
- Images from external links are not passed through `next/image`
- All images have to be stored in the `public` folder e.g `/static/images/ocean.jpeg`

View File

@ -0,0 +1,159 @@
---
title: 使用 Verdaccio 自建 Node 存储库
date: '2022-09-23'
tags: ['Verdaccio', 'Self-Hosted', 'Docker', 'Caddy', 'registry', 'Node.js']
draft: false
summary: 作为靠着 JavaScript 生态吃饭的 Web 开发者,自建一个 Node regsitry 是很有必要的,我这次继续选择 Verdaccio 来搭建存储库。这次使用 Docker Compose 部署 Verdaccio并将 Caddy 用于反向代理该服务。
images:
[
'https://pan.ivanli.cc/api/v3/file/source/2233/verdaccio.png?sign=qpoeADXzbhHk2MY5CehgTftUJ67pnUj-Ylko9D5jscU%3D%3A0',
]
---
## 为何自建存储库?
平常开发项目时,会抽出一些可复用的逻辑封装成 package。如果别人用得上就可以发布到公开存储库中比如 NPM但有些没必要的或者是不可公开的就需要有私有库了。自建私有库是一个比较好的方案毕竟至少人手一个服务器不是四舍五入就是不要钱白送呐
再说了,要是遇到了上游 package 有缺陷,无论是自己提了 PR还是上游已有修复代码如果想要方便地使用并且与他人共享已修复的 package使用自建的存储库也是很方便的。
更重要的一点是Verdaccio 能作为任何仓库的代理。这样我们可以将本地的远程存储库设为 Verdaccio然后 Verdaccio 上游设为 `https://registry.npmjs.org` (这也是缺省值)就能得到一个带有缓存的反向代理了很适合国内的网络环境bushi)。
接着,就能解锁另一个功能了,假设我们修复了 `axios` 的一个缺陷,我们可以继续使用 `axios` 作为包名发布到 `Verdaccio` 中,这样再拉到的依赖就是我们修复的版本了。当然,版本号应当保持不变。之后上游合并了你的代码后,官方发包后版本号会增加。本地项目更新依赖后就能获取到官方更新的版本了,从而实现了”无感“的效果。
## 如何自建存储库
已有环境:
- Docker, Docker Compose
- Caddy (in Docker)
- 网络:`caddy`
新增:
- Verdaccio
接下来使用 Docker Compose 部署 Verdaccio并将其加入到 `caddy` 网络中,之后配置 Caddy使其反向代理 Verdaccio。
### 使用 Docker Compose 部署
创建文件 `docker-compose.yml`
```yml {9,16-17} showLineNumbers
version: '3'
networks:
caddy:
external: true # 目前我的 caddy 在其他 compose 中
services:
verdaccio:
image: verdaccio/verdaccio:5.x-next
container_name: verdaccio
restart: unless-stopped
networks:
- caddy
expose:
- 4873
# environment:
# VERDACCIO_PUBLIC_URL: "https://node-registry.ivanli.cc"
volumes:
- ./verdaccio:/verdaccio/conf
- verdaccio-storage-data:/verdaccio/storage
- verdaccio-plugins-data:/verdaccio/plugins
volumes:
verdaccio-storage-data:
verdaccio-plugins-data:
```
上面第 9 行可以看到我现在2022 年 09 月 22 日)是使用不是正式版本,因为当前的正式版有个缺陷,就是无法正确读取到反向代理提供的 `X-Forwarded-Proto`,这有可能导致访问问题。如果使用正式版本,需要加上第 17 行的环境变量。
**不要启动 compose**,因为你还没有配置文件。当然启动了也没关系,无伤大雅。
### 创建 Verdaccio 配置文件
因为前面将配置文件目录 `verdaccio/conf` 设为了 `verdaccio`,所以:
创建配置文件 `verdaccio/config.yaml`
```zsh {1,2} showLineNumbers
storage: /verdaccio/storage
plugins: /verdaccio/plugins
auth:
htpasswd:
file: ./htpasswd
algorithm: bcrypt
rounds: 10
uplinks:
npmjs:
url: https://registry.npmjs.org/
packages:
"@*/*":
access: $all
publish: $authenticated
proxy: npmjs
"**":
proxy: npmjs
publish: $authenticated
access: $all
log: { type: stdout, format: pretty, level: http }
web:
enable: true
title: "Ivan's Node Package Registry"
logo: logo.png
scope:
```
第一、二行对应 compose 文件的第 19、20 行。
### htpasswd
使用 [.htpasswd](https://en.wikipedia.org/wiki/.htpasswd) 配置账号密码。
因为我们使用了 `bcrypt` 算法保存密码,所以可以借助 [Bcrypt-Generator.com](https://bcrypt-generator.com/) 生成保存的密码。
创建文件:`verdaccio/htpasswd`:
```htpasswd
admin:$2a$12$9xxxxxxxxxxxxxxlO.slh2k2
```
### 配置 Caddy
```Caddyfile
http://node-registry.ivanli.cc, https://node-registry.ivanli.cc {
encode zstd gzip
reverse_proxy verdaccio:4873 {
// trusted_proxies 172.0.0.0/8 192.168.31.0/24
}
}
```
结合[官方文档关于反向代理的说明](https://verdaccio.org/docs/reverse-proxy/)Caddy 默认会传递 `Host` 和 `X-Forwarded-Proto` 字段。所以不需要像 Nginx 和 Apache 一样配置那么多东西。
第四行可选,因为我是多重代理,这个 Caddy 下游还有反向代理服务,所以需要使用 `trusted_proxies` 指令。_([参考](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#defaults))_
### 启动
大工告成,启动 compose
```bash
docker compose up -d
```
助你成功!
## 使用
### 配置默认远程仓库地址
[Using a private registry | Verdaccio](https://verdaccio.org/docs/cli-registry/)
### 发布与撤销发布
```bash
npm publish --registry="https://node-registry.ivanli.cc"
npm unpublish -f --registry="https://node-registry.ivanli.cc"
```

View File

@ -0,0 +1,267 @@
---
title: 安装并配置 Arch Linux
date: '2022-10-17'
tags: ['Arch Linux', '环境搭建', 'VPS']
draft: false
summary: 又到了新装 Arch Linux 的日子了。这次又是温故而知新的机会,把之前写的笔记稍微整理了一下,在这里记录下教徒搭窝的备忘录。
images:
[
'https://pan.ivanli.cc/api/v3/file/source/2238/archlinux-logo-light.png?sign=bWxqFFy3RUDT5UsWb4UD5byt-_L4h79wede3runRKFc%3D%3A0',
]
---
## 起势
首先,通过 SSH 以 `root` 用户连接服务器,然后修改 `root` 密码:
```bash
passwd
# 输入两次你的新密码
```
## 重装系统
为了避免 IDC 提供的系统镜像有加料、后门、老旧等问题,拿到服务器后第一件事是重装系统。 Arch Linux 是我的第一选择。
借助 felixonmars 的 [vps2arch](https://github.com/felixonmars/vps2arch),我们可以将绝大多数的 Linux 系统转换成 Arch Linux 🎉。
```bash
wget https://felixc.at/vps2arch
chmod +x vps2arch
./vps2arch
```
等待几分钟就完成了。如果是中国大陆境内的机子,建议全局代理或使用自定义的系统镜像源。可以从下面的网站获取镜像地址。地址上有查询参数,可以根据自己需要修改。
> [https://archlinux.org/mirrorlist/?country=HK&protocol=https&use_mirror_status=on](https://archlinux.org/mirrorlist/?country=HK&protocol=https&use_mirror_status=on)
推荐使用 `https://hkg.mirror.rackspace.com/archlinux/`
```bash
./vps2arch -m 'https://hkg.mirror.rackspace.com/archlinux/'
```
如果系统装不上,可以在 IDC 面板上重装其他系统后再试,推荐使用 Debian。
脚本执行完成后,按脚本提示执行下面的命令重启设备,密码将被保留:
```bash
sync ; reboot -f
```
重启时SSH 会断开连接。因为新系统的 SSH 主机指纹会变化,所以需要忘记旧指纹:
```bash
ssh-keygen -R <remote-host>
# example
ssh-keygen -R '[20.20.20.20]:20000'
```
之后重新连接 SSH。
## 基本配置
设置主机名:
```bash
hostnamectl set-hostname arch.example.com
```
启用 pacman 并行下载:
- 编辑 `/etc/pacman.conf`
- 取消 `ParallelDownloads` 前的注释,值为并行下载数
## 常用环境安装
我的常用环境如下:
- 一个自己的账户
- Git
- Yay
- Zsh
- Docker
- TailScale
### 创建账户
安装 `sudo`
```bash
pacman -Sy sudo
```
创建账户:
```bash
useradd -m ivan
passwd ivan
usermod -aG wheel ivan
```
给刚刚创建的账户分配一个具有 sudo 权限的账户
```bash
EDITOR=vim visudo
```
找到 `%wheel ALL=(ALL: ALL) ALL` 这行,取消这行的注释。
现在,你自己的账号具有 sudo 权限了,接下来切换到自己的账户连接终端吧。
### Git
需要手动安装:
```bash
sudo pacman -S git
```
### Zsh
[Zsh](https://wiki.archlinux.org/title/zsh) 是一个不错的终端外壳Shell)。
使用 `pacman` 安装:
```bash
sudo pacman -S zsh
```
如果你想执行交互式的初始化配置,可以输入下面命令进入 zsh 并开始初始化配置,否则不要执行下面的命令:
```bash
zsh
```
接下来安装我常用的插件:
```zsh
sh -c "$(curl -fsSL https://git.io/zinit-install)"
echo 'zinit load zsh-users/zsh-syntax-highlighting
zinit load zsh-users/zsh-autosuggestions
zinit load ael-code/zsh-colored-man-pages
zinit load agkozak/zsh-z
zinit ice depth=1; zinit light romkatv/powerlevel10k' >> ~/.zshrc
```
#### zsh-z
一个快速跳转目录的插件。
避免优先匹配到子目录,在 `.zshrc` 中添加如下行:
```zsh
cat >> ~/.zshrc <<EOF
# zsh-z
ZSHZ_UNCOMMON=1
ZSHZ_TRAILING_SLASH=1
EOF
```
#### History
配置历史记录,在 `.zshrc` 中添加如下行:
```zsh
cat >> ~/.zshrc <<EOF
# History
HISTFILE=~/.zsh_history
HISTSIZE=10000
SAVEHIST=1000
setopt INC_APPEND_HISTORY_TIME
EOF
```
详细配置参考:[Better zsh history | SoberKoder](https://www.soberkoder.com/better-zsh-history/)
文档:[zsh: 16 Options](https://zsh.sourceforge.io/Doc/Release/Options.html)
然后进入到 `zsh` 中,执行一次 `source ~/.zshrc`
```shell
zsh
source ~/.zshrc
```
设置 Zsh 为默认的 shell 程序:
```bash
# 列出所有已安装的 shell 程序
chsh -l
# 从上面的结果中找到 zsh 的完整路径
# 我的是 /bin/zsh
chsh -s /bin/zsh
```
### Docker
安装 Docker 和 Docker Compose 也很简单:
```zsh
sudo pacman -S docker docker-compose
# 启动
sudo systemctl start docker
# 启用
sudo systemctl enable docker
# 添加当前用户到 docker 组
sudo usermod -aG docker $USER
# log in to a new group
newgrp docker
```
## 安全配置
### 禁用 SSH 密码登录
修改 `/etc/ssh/sshd_config`,找到 `PasswordAuthentication`,改为 `no`
然后重启:
```zsh
sudo systemctl restart sshd
```
### 使用 Fail2Ban
安装:
```zsh
sudo pacman -S fail2ban
```
复制配置文件:
```zsh
sudo cp /etc/fail2ban/jail.{conf,local}
```
编辑配置文件,找到 `[sshd]` 块,并添加 `enabled=true`(**不是解除注释**)
```zsh
sudo vim /etc/fail2ban/jail.local
```
```text
[sshd]
enabled = true
```
启动 fail2ban
```zsh
sudo systemctl start fail2ban
sudo systemctl enable fail2ban
```
查看状态:
```zsh
sudo fail2ban-client status
```
## 写在最后
大功告成,现在又拥有了一个崭新的 Arch Linux 系统了。后面有机会的话,我得把这些配置脚本化,不然天天配也是有点蠢,哈哈。

View File

@ -1,214 +0,0 @@
---
title: 'Introducing Tailwind Nextjs Starter Blog'
date: '2021-01-12'
lastmod: '2021-02-01'
tags: ['next-js', 'tailwind', 'guide']
draft: false
summary: 'Looking for a performant, out of the box template, with all the best in web technology to support your blogging needs? Checkout the Tailwind Nextjs Starter Blog template.'
images: ['/static/images/canada/mountains.jpg', '/static/images/canada/toronto.jpg']
authors: ['default', 'sparrowhawk']
---
![tailwind-nextjs-banner](/static/images/twitter-card.png)
# Tailwind Nextjs Starter Blog
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/timlrx/tailwind-nextjs-starter-blog)
This is a [Next.js](https://nextjs.org/), [Tailwind CSS](https://tailwindcss.com/) blogging starter template. Probably the most feature-rich Next.js markdown blogging template out there. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.
Check out the documentation below to get started.
Facing issues? Check the [FAQ page](https://github.com/timlrx/tailwind-nextjs-starter-blog/wiki) and do a search on past issues. Feel free to open a new issue if none has been posted previously.
Feature request? Check the past discussions to see if it has been brought up previously. Otherwise, feel free to start a new discussion thread. All ideas are welcomed!
## Examples
- [Demo Blog](https://tailwind-nextjs-starter-blog.vercel.app/) - this repo
- [My personal blog](https://www.timlrx.com) - modified to auto-generate blog posts with dates
- [Aloisdg's cookbook](https://tambouille.vercel.app/) - with pictures and recipes!
- [GautierArcin's demo with next translate](https://tailwind-nextjs-starter-blog-seven.vercel.app/) - includes translation of mdx posts, [source code](https://github.com/GautierArcin/tailwind-nextjs-starter-blog/tree/demo/next-translate)
- [David Levai's digital garden](https://davidlevai.com/) - customized design and added email subscriptions
- [Thinh's Corner](https://thinhcorner.com/) - [customized layout](https://github.com/Th1nhNg0/th1nhng0.vercel.app/blob/5e73a420828d82f01e7147512a2c3273c4ec19f8/layouts/PostLayout.js) with sticky side table of contents
Using the template? Feel free to create a PR and add your blog to this list.
## Motivation
I wanted to port my existing blog to Nextjs and Tailwind CSS but there was no easy out of the box template to use so I decided to create one. Design is adapted from [Tailwindlabs blog](https://github.com/tailwindlabs/blog.tailwindcss.com).
I wanted it to be nearly as feature-rich as popular blogging templates like [beautiful-jekyll](https://github.com/daattali/beautiful-jekyll) and [Hugo Academic](https://github.com/wowchemy/wowchemy-hugo-modules) but with the best of React's ecosystem and current web development's best practices.
## Features
- Easy styling customization with [Tailwind 3.0](https://tailwindcss.com/blog/tailwindcss-v3) and primary color attribute
- Near perfect lighthouse score - [Lighthouse report](https://www.webpagetest.org/result/210111_DiC1_08f3670c3430bf4a9b76fc3b927716c5/)
- Lightweight, 45kB first load JS, uses Preact in production build
- Mobile-friendly view
- Light and dark theme
- Self-hosted font with [Fontsource](https://fontsource.org/)
- Supports [plausible](https://plausible.io/), [simple analytics](https://simpleanalytics.com/) and google analytics
- [MDX - write JSX in markdown documents!](https://mdxjs.com/)
- Server-side syntax highlighting with line numbers and line highlighting via [rehype-prism-plus](https://github.com/timlrx/rehype-prism-plus)
- Math display supported via [KaTeX](https://katex.org/)
- Citation and bibliography support via [rehype-citation](https://github.com/timlrx/rehype-citation)
- Automatic image optimization via [next/image](https://nextjs.org/docs/basic-features/image-optimization)
- Flexible data retrieval with [mdx-bundler](https://github.com/kentcdodds/mdx-bundler)
- Support for tags - each unique tag will be its own page
- Support for multiple authors
- Blog templates
- TOC component
- Support for nested routing of blog posts
- Newsletter component with support for mailchimp, buttondown, convertkit, klaviyo, revue, and emailoctopus
- Supports [giscus](https://github.com/laymonage/giscus), [utterances](https://github.com/utterance/utterances) or disqus
- Projects page
- Preconfigured security headers
- SEO friendly with RSS feed, sitemaps and more!
## Sample posts
- [A markdown guide](https://tailwind-nextjs-starter-blog.vercel.app/blog/github-markdown-guide)
- [Learn more about images in Next.js](https://tailwind-nextjs-starter-blog.vercel.app/blog/guide-to-using-images-in-nextjs)
- [A tour of math typesetting](https://tailwind-nextjs-starter-blog.vercel.app/blog/deriving-ols-estimator)
- [Simple MDX image grid](https://tailwind-nextjs-starter-blog.vercel.app/blog/pictures-of-canada)
- [Example of long prose](https://tailwind-nextjs-starter-blog.vercel.app/blog/the-time-machine)
- [Example of Nested Route Post](https://tailwind-nextjs-starter-blog.vercel.app/blog/nested-route/introducing-multi-part-posts-with-nested-routing)
## Quick Start Guide
1. Try installing the starter using the new [Pliny project CLI](https://github.com/timlrx/pliny):
```bash
npm i -g @pliny/cli
pliny new --template=starter-blog my-blog
```
It supports the updated version of the blog with Contentlayer, optional choice of TS/JS and different package managers as well as more modularized components which will be the basis of the template going forward.
Alternatively to stick with the current version, TypeScript and Contentlayer:
```bash
npx degit 'timlrx/tailwind-nextjs-starter-blog#contentlayer'
```
or JS (official support)
```bash
npx degit https://github.com/timlrx/tailwind-nextjs-starter-blog.git
```
2. Personalize `siteMetadata.js` (site related information)
3. Modify the content security policy in `next.config.js` if you want to use
any analytics provider or a commenting solution other than giscus.
4. Personalize `authors/default.md` (main author)
5. Modify `projectsData.js`
6. Modify `headerNavLinks.js` to customize navigation links
7. Add blog posts
8. Deploy on Vercel
## Development
First, run the development server:
```bash
npm start
# or
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
## Extend / Customize
`data/siteMetadata.js` - contains most of the site related information which should be modified for a user's need.
`data/authors/default.md` - default author information (required). Additional authors can be added as files in `data/authors`.
`data/projectsData.js` - data used to generate styled card on the projects page.
`data/headerNavLinks.js` - navigation links.
`data/logo.svg` - replace with your own logo.
`data/blog` - replace with your own blog posts.
`public/static` - store assets such as images and favicons.
`tailwind.config.js` and `css/tailwind.css` - contain the tailwind stylesheet which can be modified to change the overall look and feel of the site.
`css/prism.css` - controls the styles associated with the code blocks. Feel free to customize it and use your preferred prismjs theme e.g. [prism themes](https://github.com/PrismJS/prism-themes).
`components/social-icons` - to add other icons, simply copy an svg file from [Simple Icons](https://simpleicons.org/) and map them in `index.js`. Other icons use [heroicons](https://heroicons.com/).
`components/MDXComponents.js` - pass your own JSX code or React component by specifying it over here. You can then call them directly in the `.mdx` or `.md` file. By default, a custom link and image component is passed.
`layouts` - main templates used in pages.
`pages` - pages to route to. Read the [Next.js documentation](https://nextjs.org/docs) for more information.
`next.config.js` - configuration related to Next.js. You need to adapt the Content Security Policy if you want to load scripts, images etc. from other domains.
## Post
### Frontmatter
Frontmatter follows [Hugo's standards](https://gohugo.io/content-management/front-matter/).
Currently 7 fields are supported.
```
title (required)
date (required)
tags (required, can be empty array)
lastmod (optional)
draft (optional)
summary (optional)
images (optional, if none provided defaults to socialBanner in siteMetadata config)
authors (optional list which should correspond to the file names in `data/authors`. Uses `default` if none is specified)
layout (optional list which should correspond to the file names in `data/layouts`)
canonicalUrl (optional, canonical url for the post for SEO)
```
Here's an example of a post's frontmatter:
```
---
title: 'Introducing Tailwind Nexjs Starter Blog'
date: '2021-01-12'
lastmod: '2021-01-18'
tags: ['next-js', 'tailwind', 'guide']
draft: false
summary: 'Looking for a performant, out of the box template, with all the best in web technology to support your blogging needs? Checkout the Tailwind Nextjs Starter Blog template.'
images: ['/static/images/canada/mountains.jpg', '/static/images/canada/toronto.jpg']
authors: ['default', 'sparrowhawk']
layout: PostLayout
canonicalUrl: https://tailwind-nextjs-starter-blog.vercel.app/blog/introducing-tailwind-nextjs-starter-blog
---
```
### Compose
Run `node ./scripts/compose.js` to bootstrap a new post.
Follow the interactive prompt to generate a post with pre-filled front matter.
## Deploy
**Vercel**
The easiest way to deploy the template is to use the [Vercel Platform](https://vercel.com) from the creators of Next.js. Check out the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
**Netlify / GitHub Pages / Firebase etc.**
As the template uses `next/image` for image optimization, additional configurations have to be made to deploy on other popular static hosting websites like [Netlify](https://www.netlify.com/) or [GitHub Pages](https://pages.github.com/). An alternative image optimization provider such as Imgix, Cloudinary or Akamai has to be used. Alternatively, replace the `next/image` component with a standard `<img>` tag. See [`next/image` documentation](https://nextjs.org/docs/basic-features/image-optimization) for more details.
The API routes used in the newsletter component cannot be used in a static site export. You will need to use a form API endpoint provider and substitute the route in the newsletter component accordingly. Other hosting platforms such as Netlify also offer alternative solutions - please refer to their docs for more information.
## Support
Using the template? Support this effort by giving a star on GitHub, sharing your own blog and giving a shoutout on Twitter or be a project [sponsor](https://github.com/sponsors/timlrx).
## Licence
[MIT](https://github.com/timlrx/tailwind-nextjs-starter-blog/blob/master/LICENSE) © [Timothy Lin](https://www.timrlx.com)

View File

@ -1,10 +0,0 @@
---
title: My fancy title
date: '2021-01-31'
tags: ['hello']
draft: true
summary:
images: []
---
Draft post which should not display

View File

@ -0,0 +1,143 @@
---
title: 使用 NAT VPS 作为 SD-WAN 中继
date: '2022-10-13'
tags: ['ZeroTier', 'TailScale', SD-WAN, 'NAT', 'VPS', 'FRP', 'Self-Hosted']
draft: false
summary: ZeroTier 和 TailScale 是我目前正在同时使用的 SD-WAN 组网工具。这次购入了一台 NAT VPS准备用它中继 ZeroTier 和 TailScale。
---
## 诉苦
由于工作需要,我已经好几个月使用 SD-WAN 在公司和宿舍组建了 SD-WAN可是国内网络环境实在是不友好公司网络质量也差我忍受了很久的随机掉线的问题近两个月我改用手机热点避免了这个问题但是只能在开发后端时使用如果开发前端或者是对生产环境进行部署与验证时访问线上环境需要几百兆流量我吃不消哇频繁切换 Wi-Fi 接入点也不是个令人愉快的事。但天无绝人之路,我兜兜转转购入了一台香港的 NAT VPS规划用于中继 SD-WAN。
## 方案
## 步骤
### 重装系统
// TODO:
## ZeroTier 中继
### VPS 上安装步骤
#### 安装 ZeroTier 客户端
```bash
yay -S zerotier-one
```
#### 启动服务
```bash
sudo systemctl enable zerotier-one
sudo systemctl start zerotier-one
```
#### 加入网络
打开官方 planet 面板:[ZeroTier Central](https://my.zerotier.com/),复制你的网络 ID然后
```bash
sudo zerotier-cli join <your_network_id>
```
回到面板,授权刚刚加入的节点。
#### 转换为 Moon
生成 moon 配置
```bash
sudo zerotier-idtool initmoon identity.public | sudo tee -a moon.json
```
输出示例:
```json
{
"id": "9axxxxxx12",
"objtype": "world",
"roots": [
{
"identity": "9axxxxxx12:0:258xxxxxx38b34a2fa88b46d290137a6ecb3a185dacdaee957c30e33f1977ca7e",
"stableEndpoints": []
}
],
"signingKey": "df18369fxxxxxx70036a356c",
"signingKey_SECRET": "b1524155faa6f779b8xxxxxxf811f711fed",
"updatesMustBeSignedBy": "df18369f3b54xxxxxx036a356c",
"worldType": "moon"
}
```
`"stableEndpoints": []` 中添加 `"<server_ip>/<server_nat_port>"`,其中 `server_ip` 是你的公网地址,`server_nat_port` 是 NAT 后的外网端口。示例如下:
```json
{
//...
"roots": [
{
"identity": "9axxxxxx12:0:258xxxxxx38b34a2fa88b46d290137a6ecb3a185dacdaee957c30e33f1977ca7e",
"stableEndpoints": ["1.2.3.4/19993"]
}
]
//...
}
```
生成 `.moon` 文件:
```bash
sudo zerotier-idtool genmoon moon.json
```
执行完成后,可以在当前目录中看到 `.moon` 文件:
```bash
ls -l | grep *.moon
```
将生成的 `.moon` 文件移动到 `./moons.d` 目录中:
```bash
sudo mkdir moons.d
sudo mv 000000xxxxxxxxxx.moon moons.d
```
#### 重启 ZeroTier 服务
```bash
sudo systemctl restart zerotier-one
```
### 使用 ZeroTier 中继
前面生成的 "000000xxxxxxxx.moon" 文件,除去后缀 `.moon` 外就是 Moon ID前面的零没啥用把后面的内容放到下面的命令中两个参数都是 Moon ID。
```bash
sudo zerotier-cli orbit xxxxxxxxxx xxxxxxxxxx
```
### 问题排查
#### 无法加入网络
```bash
sudo zerotier-cli join xxxxxxxxxxxx
# outputs:
# zerotier-cli: missing port and zerotier-one.port not found in /var/lib/zerotier-one
```
原因:没有[启动服务](#启动服务)。
#### 无法初始化 moon
```bash
sudo zerotier-idtool initmoon identity.public | sudo tee -a moon.json
# outputs:
# identity.public is not a valid identity
```
原因:没有[启动过服务](#启动服务)。第一次启动后 ZeroTier 会创建 `identity.public` 文件。

View File

@ -1,30 +0,0 @@
---
title: Introducing Multi-part Posts with Nested Routing
date: '2021-05-02'
tags: ['multi-author', 'next-js', 'feature']
draft: false
summary: 'The blog template supports posts in nested sub-folders. This can be used to group posts of similar content e.g. a multi-part course. This post is itself an example of a nested route!'
---
# Nested Routes
The blog template supports posts in nested sub-folders. This helps in organisation and can be used to group posts of similar content e.g. a multi-part series. This post is itself an example of a nested route! It's located in the `/data/blog/nested-route` folder.
## How
Simplify create multiple folders inside the main `/data/blog` folder and add your `.md`/`.mdx` files to them. You can even create something like `/data/blog/nested-route/deeply-nested-route/my-post.md`
We use Next.js catch all routes to handle the routing and path creations.
## Use Cases
Here are some reasons to use nested routes
- More logical content organisation (blogs will still be displayed based on the created date)
- Multi-part posts
- Different sub-routes for each author
- Internationalization (though it would be recommended to use [Next.js built-in i8n routing](https://nextjs.org/docs/advanced-features/i18n-routing))
## Note
- The previous/next post links at bottom of the template are currently sorted by date. One could explore modifying the template to refer the reader to the previous/next post in the series, rather than by date.

View File

@ -0,0 +1,251 @@
---
title: 利用一台小鸡实现网络自由
date: '2022-10-06'
tags:
[
'SNI',
'TLS',
'Reverse Proxy',
'反向代理',
'正向代理',
‘内网穿透',
'Caddy',
'Xray',
'Vless',
]
draft: true
summary: SNI Proxy 进行 TLS 分流Caddy 对网站和 Xray 进行反向代理Xray 实现正向、反向代理(内网穿透)。
---
## 前言
由于条件所限,我在住所使用了一台 AMD 小主机运行了 PVE上面跑了许多服务他们有一个共同点——具有 Web UI。另一方面我的宽带使用共享公网 IP使用 Full Cone NAT这就导致我在外难以方便地访问内网服务。为了解决这个令人不爽的问题我准备使用 VPS 主机作为中间人,用于反射相关流量。
比较悲剧的是在撰写本文时VPS 的 443 端口被干掉……不过还好VPS 即将到期,如果不能恢复,就换别的了。
## 网络拓扑图
简单画一下,大概网络拓扑长下面这样:
```mermaid
graph TB;
subgraph CLIENT
BROWSER(Browser)
XRAY-C(Xray)
GIT-C(Git)
end
subgraph VPS
SNI-PROXY(SNI Proxy)
CADDY-S(Caddy)
XRAY-S(Xray)
end
subgraph HOME
CADDY-H(Caddy)
XRAY-H(Xray)
GIT-S(Git)
end
BROWSER--https://www.example.com:443-->SNI-PROXY--https://www.example.com:443-->CADDY-S
BROWSER--https://home.example.com:443-->SNI-PROXY--https://home.example.com:443-->XRAY-H--https://home.example.com:443-->CADDY-H
XRAY-C--vless://vless.example.com:443-->SNI-PROXY--vless://vless.example.com:443-->XRAY-S
XRAY-C--vmess://vmess.example.com:443-->SNI-PROXY--vmess://vmess.example.com:443-->CADDY-S--vmess://vmess.example.com WITHOUT LTS-->XRAY-S
BROWSER--http://www.example.com:80-->CADDY-S
GIT-C--git://git.example.com:2222-->XRAY-S--git://git.example.com:2222-->XRAY-H--git://git.example.com:2222-->GIT-S
```
从上图中可以看到VPS 公网 443 端口由 SNI Proxy 监听80 端口由 Caddy 监听2222 端口作为 家庭 Git 服务器的透传端口,由 Xray 直接监听并反射relay给家庭 Git 服务。
SNI Proxy 通过对 TLS 的 SNI 对流量进行区分,将访问部署在家庭中的网站的流量直接转发给 Xray再由 Xray 根据规则转发给家庭中的 Caddy 服务,最后由家庭中的 Caddy 服务将 HTTPS 流量转为 HTTP 流量,并转发给目标服务。对于 VLESS 流量,将转发给 Xray 对应端口处理;对于 vmess 和其他网站流量,转发给 Caddy 的 443 端口处理。
Caddy 接收到 443 端口的流量,将根据域名等规则处理并转发给对应服务,如果是 vmess 流量,将扒掉 TLS 层并交由 Xray 处理。Caddy 还接收 80 端口的流量,这部分流量基本上都转发给对应 HTTP 服务进行处理。
## 部署
### Docker Compose
```yml
version: '3.9'
networks:
caddy:
xray:
volumes:
caddy-data:
caddy-config:
acme-sh-data:
services:
xray:
image: teddysun/xray
container_name: xray
restart: always
ports:
- '2222:2222'
networks:
- xray
volumes:
- ./xray/config.yml:/etc/xray/config.yml
- acme-sh-data:/certs
command: 'xray -c=/etc/xray/config.yml'
caddy:
image: caddy:2
container_name: caddy
restart: always
ports:
- '80:80'
networks:
- caddy
- xray
volumes:
- $PWD/caddy/Caddyfile:/etc/caddy/Caddyfile
- $PWD/site:/srv
- caddy-data:/data
- caddy-config:/config
- acme-sh-data:/certs
sniproxy:
image: tommylau/sniproxy
container_name: sniproxy
restart: always
networks:
- caddy
- xray
ports:
- '443:443'
volumes:
- $PWD/sniproxy:/etc/sniproxy
- /var/log/sniproxy:/var/log/sniproxy
acme.sh:
image: neilpang/acme.sh:dev
container_name: acme.sh
restart: always
volumes:
- acme-sh-data:/acme.sh
env_file: acme.env
command: 'daemon'
```
### SNI Proxy 配置
文件位于 `sniproxy/sniproxy.conf
```conf
user nobody
group nogroup
# PID file
pidfile /var/run/sniproxy.pid
error_log {
# Log to the daemon syslog facility
# syslog deamon
# Alternatively we could log to file
filename /var/log/sniproxy/sniproxy.log
# Control the verbosity of the log
priority notice
}
listen 443 {
proto tls
table https_hosts
access_log {
filename /var/log/sniproxy/https_access.log
priority notice
}
}
table https_hosts {
vmess.example.com caddy:443 # vmess
vless.example.com xray:443 # VLESS
.*\.example\.com xray:8443 # HTTPS tunnel
www.example.com caddy:443 # VPS vhost
}
```
### Caddy 配置
文件位于 `caddy/Caddyfile`。
```caddyfile
www.example.com {
encode zstd gzip
reverse_proxy some-host:80
}
vmess.example.com {
reverse_proxy h2c://xray:80 // vmess
}
```
### Xray 配置
文件位于 `xray/config.yml`
```yml
---
inbounds:
- tag: vless-xtls.in
listen: 0.0.0.0
port: 443
protocol: vless
settings:
clients:
- id: <your-uuid>
flow: xtls-rprx-direct
decryption: none
streamSettings:
network: tcp
security: xtls
xtlsSettings:
serverName: vless.example.com
alpn:
- h2
- http/1.1
certificates:
- certificateFile: /certs/*.example.com/fullchain.cer
keyFile: /certs/*.example.com/*.example.com.key
falklbacks:
- name: 'vmess.ivanli.cc'
dist: 80
xver: 1
- dest: 'caddy:80'
xver: 1
- listen: 0.0.0.0
port: 80
protocol: vmess
settings:
clients:
- id: <your-uuid>
decryption: none
streamSettings:
network: h2
security: none
httpSettings:
path: /
host:
- vmess.example.com
# ... 其他配置 ...
```
关于反向代理及泛域名证书相关配置,参考[使用 Xray、acme.sh、Docker Compose 搭建内网穿透服务](https://ivanli.cc/blog/build-an-frp-using-xray-acme.sh-docker-compose)。
### 启动服务
启动服务前,确保你的配置都完成了。之后在 `docker-compose.yml` 同级目录下执行下面的命令:
```bash
docker compose up -d
```
如果有防火墙,注意放行端口。
## 方案浅析
上述方案主要解决了一个问题443 端口的复用。使得所有**“HTTPS 流量”**都流向 443 端口,显得非常治愈。因为使用了境外服务器,为了避免被误杀,实现了站点伪装,这样在一定程度上避免被主动探测发现问题。可是在撰写本文前,翻车了,原因可能是客户端指纹特征导致的。因为除了穿透服务的客户端是完全可控的,能通过配置[开启 uTLS 来规避](https://t.ivanli.cc/UFImlX),但是在网关上的 Clash 客户端却没提供相关配置选项。大意了。

View File

@ -1,451 +0,0 @@
---
title: 'New features in v1'
date: 2021-08-07T15:32:14Z
lastmod: '2021-02-01'
tags: ['next-js', 'tailwind', 'guide']
draft: false
summary: 'An overview of the new features released in v1 - code block copy, multiple authors, frontmatter layout and more'
layout: PostSimple
bibliography: references-data.bib
canonicalUrl: https://tailwind-nextjs-starter-blog.vercel.app/blog/new-features-in-v1/
---
## Overview
A post on the new features introduced in v1.0. New features:
<TOCInline toc={props.toc} exclude="Overview" toHeading={2} />
First load JS decreased from 43kB to 39kB despite all the new features added!^[With the new changes in Nextjs 12, first load JS increase to 45kB.]
See [upgrade guide](#upgrade-guide) below if you are migrating from v0 version of the template.
## Theme colors
You can easily modify the theme color by changing the primary attribute in the tailwind config file:
```js:tailwind.config.js
theme: {
colors: {
primary: colors.teal,
gray: colors.neutral,
...
}
...
}
```
The primary color attribute should be assigned an object with keys from 50, 100, 200 ... 900 and the corresponding color code values.
Tailwind includes great default color palettes that can be used for theming your own website. Check out [customizing colors documentation page](https://tailwindcss.com/docs/customizing-colors) for the full range of options.
Migrating from v1? You can revert to the previous theme by setting `primary` to `colors.sky` (Tailwind 2.2.2 and above, otherwise `colors.lightBlue`) and changing gray to `colors.gray`.
From v1.1.2+, you can also customize the style of your code blocks easily by modifying the `css/prism.css` stylesheet. Token classnames are compatible with prismjs
so you can copy and adapt token styles from a prismjs stylesheet e.g. [prism themes](https://github.com/PrismJS/prism-themes).
## Xdm MDX compiler
We switched the MDX bundler from [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote) to [mdx-bundler](https://github.com/kentcdodds/mdx-bundler).
This uses [xdm](https://github.com/wooorm/xdm) under the hood, the latest micromark 3 and remark, rehype libraries.
**Warning:** If you were using custom remark or rehype libraries, please upgrade to micromark 3 compatible ones. If you are upgrading, please delete `node_modules` and `package-lock.json` to avoid having past dependencies related issues.
[xdm](https://github.com/wooorm/xdm) contains multiple improvements over [@mdx-js/mdx](https://github.com/mdx-js/mdx), the compiler used internally by next-mdx-remote, but there might be some breaking behaviour changes.
Please check your markdown output to verify.
Some new possibilities include loading components directly in the mdx file using the import syntax and including js code which could be compiled and bundled at the build step.
For example, the following jsx snippet can be used directly in an MDX file to render the page title component:
```jsx
import PageTitle from './PageTitle.js'
;<PageTitle> Using JSX components in MDX </PageTitle>
```
import PageTitle from './PageTitle.js'
<PageTitle> Using JSX components in MDX </PageTitle>
The default configuration resolves all components relative to the `components` directory.
**Note**:
Components which require external image loaders also require additional esbuild configuration.
Components which are dependent on global application state on lifecycle like the Nextjs `Link` component would also not work with this setup as each mdx file is built independently.
For such cases, it is better to use component substitution.
## Table of contents component
Inspired by [Docusaurus](https://docusaurus.io/docs/next/markdown-features/inline-toc) and Gatsby's [gatsby-remark-table-of-contents](https://www.gatsbyjs.com/plugins/gatsby-remark-table-of-contents/),
the `toc` variable containing all the top level headings of the document is passed to the MDX file and can be styled accordingly.
To make generating a table of contents (TOC) simple, you can use the existing `TOCInline` component.
For example, the TOC in this post was generated with the following code:
```jsx
<TOCInline toc={props.toc} exclude="Overview" toHeading={2} />
```
You can customise the headings that are displayed by configuring the `fromHeading` and `toHeading` props, or exclude particular headings
by passing a string or a string array to the `exclude` prop. By default, all headings that are of depth 3 or smaller are indented. This can be configured by changing the `indentDepth` property.
A `asDisclosure` prop can be used to render the TOC within an expandable disclosure element.
Here's the full TOC rendered in a disclosure element.
```jsx
<TOCInline toc={props.toc} asDisclosure />
```
<TOCInline toc={props.toc} asDisclosure />
## Layouts
You can map mdx blog content to layout components by configuring the frontmatter field. For example, this post is written with the new `PostSimple` layout!
### Adding new templates
layout templates are stored in the `./layouts` folder. You can add your React components that you want to map to markdown content in this folder.
The component file name must match that specified in the markdown frontmatter `layout` field.
The only required field is `children` which contains the rendered MDX content, though you would probably want to pass in the frontMatter contents and render it in the template.
You can configure the template to take in other fields - see `PostLayout` component for an example.
Here's an example layout which you can further customise:
```jsx
export default function ExampleLayout({ frontMatter, children }) {
const { date, title } = frontMatter
return (
<SectionContainer>
<div>{date}</div>
<h1>{title}</h1>
<div>{children}</div>
</SectionContainer>
)
}
```
### Configuring a blog post frontmatter
Use the `layout` frontmatter field to specify the template you want to map the markdown post to. Here's how the frontmatter of this post looks like:
```
---
title: 'New features in v1'
date: '2021-05-26 '
tags: ['next-js', 'tailwind', 'guide']
draft: false
summary: 'Introducing the new layout features - you can map mdx blog content to layout components by configuring the frontmatter field'
layout: PostSimple
---
```
You can configure the default layout in the respective page section by modifying the `DEFAULT_LAYOUT` variable.
The `DEFAULT_LAYOUT` for blog posts page is set to `PostLayout`.
### Extend
`layout` is mapped to wrapper which wraps the entire MDX content.
```jsx
export const MDXComponents = {
Image,
a: CustomLink,
pre: Pre,
wrapper: ({ components, layout, ...rest }) => {
const Layout = require(`../layouts/${layout}`).default
return <Layout {...rest} />
},
}
export const MDXLayoutRenderer = ({ layout, mdxSource, ...rest }) => {
const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource])
return <MDXLayout layout={layout} components={MDXComponents} {...rest} />
}
```
Use the `MDXLayoutRenderer` component on a page where you want to accept a layout name to map to the desired layout.
You need to pass the layout name from the layout folder (it has to be an exact match).
## Analytics
The template now supports [plausible](https://plausible.io/), [simple analytics](https://simpleanalytics.com/) and google analytics.
Configure `siteMetadata.js` with the settings that correspond with the desired analytics provider.
```js
analytics: {
// supports plausible, simpleAnalytics or googleAnalytics
plausibleDataDomain: '', // e.g. tailwind-nextjs-starter-blog.vercel.app
simpleAnalytics: false, // true or false
googleAnalyticsId: '', // e.g. UA-000000-2 or G-XXXXXXX
},
```
Custom events are also supported. You can import the `logEvent` function from `@components/analytics/[ANALYTICS-PROVIDER]` file and call it when
triggering certain events of interest. _Note_: Additional configuration might be required depending on the analytics provider, please check their official
documentation for more information.
## Blog comments system
We have also added support for [giscus](https://github.com/laymonage/giscus), [utterances](https://github.com/utterance/utterances) or disqus.
To enable, simply configure `siteMetadata.js` comments property with the desired provider and settings as specified in the config file.
```js
comment: {
// Select a provider and use the environment variables associated to it
// https://vercel.com/docs/environment-variables
provider: 'giscus', // supported providers: giscus, utterances, disqus
giscusConfig: {
// Visit the link below, and follow the steps in the 'configuration' section
// https://giscus.app/
repo: process.env.NEXT_PUBLIC_GISCUS_REPO,
repositoryId: process.env.NEXT_PUBLIC_GISCUS_REPOSITORY_ID,
category: process.env.NEXT_PUBLIC_GISCUS_CATEGORY,
categoryId: process.env.NEXT_PUBLIC_GISCUS_CATEGORY_ID,
mapping: 'pathname', // supported options: pathname, url, title
reactions: '1', // Emoji reactions: 1 = enable / 0 = disable
// Send discussion metadata periodically to the parent window: 1 = enable / 0 = disable
metadata: '0',
// theme example: light, dark, dark_dimmed, dark_high_contrast
// transparent_dark, preferred_color_scheme, custom
theme: 'light',
// theme when dark mode
darkTheme: 'transparent_dark',
// If the theme option above is set to 'custom`
// please provide a link below to your custom theme css file.
// example: https://giscus.app/themes/custom_example.css
themeURL: '',
},
utterancesConfig: {
// Visit the link below, and follow the steps in the 'configuration' section
// https://utteranc.es/
repo: process.env.NEXT_PUBLIC_UTTERANCES_REPO,
issueTerm: '', // supported options: pathname, url, title
label: '', // label (optional): Comment 💬
// theme example: github-light, github-dark, preferred-color-scheme
// github-dark-orange, icy-dark, dark-blue, photon-dark, boxy-light
theme: '',
// theme when dark mode
darkTheme: '',
},
disqus: {
// https://help.disqus.com/en/articles/1717111-what-s-a-shortname
shortname: process.env.NEXT_PUBLIC_DISQUS_SHORTNAME,
},
},
```
## Multiple authors
Information on authors is now split from `siteMetadata.js` and stored in its own `data/authors` folder as a markdown file. Minimally, you will need to have a `default.md` file with authorship information. You can create additional files as required and the file name will be used as the reference to the author.
Here's how an author markdown file might look like:
```md:default.md
---
name: Tails Azimuth
avatar: /static/images/avatar.png
occupation: Professor of Atmospheric Science
company: Stanford University
email: address@yoursite.com
twitter: https://twitter.com/Twitter
linkedin: https://www.linkedin.com
github: https://github.com
---
A long description of yourself...
```
You can use this information in multiple places across the template. For example in the about section of the page, we grab the default author information with this line of code:
```js
const authorDetails = await getFileBySlug('authors', ['default'])
```
This is rendered in the `AuthorLayout` template.
### Multiple authors in blog post
The frontmatter of a blog post accepts an optional `authors` array field. If no field is specified, it is assumed that the default author is used. Simply pass in an array of authors to render multiple authors associated with a post.
For example, the following frontmatter will display the authors given by `data/authors/default.md` and `data/authors/sparrowhawk.md`
```yaml
title: 'My first post'
date: '2021-01-12'
draft: false
summary: 'My first post'
authors: ['default', 'sparrowhawk']
```
A demo of a multiple authors post is shown in [Introducing Tailwind Nextjs Starter Blog post](/blog/introducing-tailwind-nextjs-starter-blog).
## Copy button for code blocks
Hover over a code block and you will notice a GitHub-inspired copy button! You can modify `./components/Pre.js` to further customise it.
The component is passed to `MDXComponents` and modifies all `<pre>` blocks.
## Line highlighting and line numbers
Line highlighting and line numbers are now supported out of the box thanks to the new [rehype-prism-plus plugin](https://github.com/timlrx/rehype-prism-plus)
The following javascript code block:
````
```js {1, 3-4} showLineNumbers
var num1, num2, sum
num1 = prompt('Enter first number')
num2 = prompt('Enter second number')
sum = parseInt(num1) + parseInt(num2) // "+" means "add"
alert('Sum = ' + sum) // "+" means combine into a string
```
````
will appear as:
```js {1,3-4} showLineNumbers
var num1, num2, sum
num1 = prompt('Enter first number')
num2 = prompt('Enter second number')
sum = parseInt(num1) + parseInt(num2) // "+" means "add"
alert('Sum = ' + sum) // "+" means combine into a string
```
To modify the styles, change the following class selectors in the `prism.css` file:
```css
.code-highlight {
@apply float-left min-w-full;
}
.code-line {
@apply -mx-4 block border-l-4 border-opacity-0 pl-4 pr-4;
}
.code-line.inserted {
@apply bg-green-500 bg-opacity-20;
}
.code-line.deleted {
@apply bg-red-500 bg-opacity-20;
}
.highlight-line {
@apply -mx-4 border-l-4 border-primary-500 bg-gray-700 bg-opacity-50;
}
.line-number::before {
@apply mr-4 -ml-2 inline-block w-4 text-right text-gray-400;
content: attr(line);
}
```
## Newletter component (v1.1.3)
Introduced in v1.1.3, the newsletter component gives you an easy way to build an audience. It integrates with the following providers:
- [Mailchimp](https://mailchimp.com/)
- [Buttondown](https://buttondown.email/)
- [Convertkit](https://convertkit.com/)
To use it, specify the provider which you are using in the config file and add the necessary environment variables to the `.env` file.
For more information on the required variables, check out `.env.sample.`
Two components are exported, a default `NewsletterForm` and a `BlogNewsletterForm` component, which is also passed in as an MDX component
and can be used in a blog post:
```jsx
<BlogNewsletterForm title="Like what you are reading?" />
```
<BlogNewsletterForm title="Like what you are reading?" />
The component relies on nextjs's [API routes](https://nextjs.org/docs/api-routes/introduction) which requires a server-side instance of nextjs to be setup
and is not compatible with a 100% static site export. Users should either self-host or use a compatible platform like Vercel or Netlify which supports this functionality.
A static site compatible alternative is to substitute the route in the newsletter component with a form API endpoint provider.
## Bibliography and Citations (v1.2.1)
`rehype-citation` plugin is added to the xdm processing pipeline in v1.2.1. This allows you to easily format citations and insert bibliography from an existing bibtex or CSL-json file.
For example, the following markdown code sample:
```md
Standard citation [@Nash1950]
In-text citations e.g. @Nash1951
Multiple citations [see @Nash1950; @Nash1951, page 50]
**References:**
[^ref]
```
is rendered to the following:
Standard citation [@Nash1950]
In-text citations e.g. @Nash1951
Multiple citations [see @Nash1950; @Nash1951, page 50]
**References:**
[^ref]
A bibliography will be inserted at the end of the document, but this can be overwritten by specifying a `[^Ref]` tag at the intended location.
The plugin uses APA citation formation, but also supports the following CSLs, 'apa', 'vancouver', 'harvard1', 'chicago', 'mla', or a path to a user-specified CSL file.
See [rehype-citation readme](https://github.com/timlrx/rehype-citation) for more information on the configuration options.
## Self-hosted font (v1.5.0)
Google font has been replaced with self-hosted font from [Fontsource](https://fontsource.org/). This gives the following [advantages](https://fontsource.org/docs/introduction):
> Self-hosting brings significant performance gains as loading fonts from hosted services, such as Google Fonts, lead to an extra (render blocking) network request. To provide perspective, for simple websites it has been seen to double visual load times.
>
> Fonts remain version locked. Google often pushes updates to their fonts without notice, which may interfere with your live production projects. Manage your fonts like any other NPM dependency.
>
> Commit to privacy. Google does track the usage of their fonts and for those who are extremely privacy concerned, self-hosting is an alternative.
This leads to a smaller font bundle and a 0.1s faster load time ([webpagetest comparison](https://www.webpagetest.org/video/compare.php?tests=220201_AiDcFH_f68a69b758454dd52d8e67493fdef7da,220201_BiDcMC_bf2d53f14483814ba61e794311dfa771)).
To change the default Inter font:
1. Install the preferred [font](https://fontsource.org/fonts) - `npm install -save @fontsource/<font-name>`
2. Update the import at `pages/_app.js`- `import '@fontsource/<font-name>.css'`
3. Update the `fontfamily` property in the tailwind css config file
## Upgrade guide
There are significant portions of the code that has been changed from v0 to v1 including support for layouts and a new mdx engine.
There's also no real reason to change if the previous one serves your needs and it might be easier to copy
the component changes you are interested in to your existing blog rather than migrating everything over.
Nonetheless, if you want to do so and have not changed much of the template, you could clone the new version and copy over the blog post over to the new template.
Another alternative would be to pull the latest template version with the following code:
```bash
git remote add template git@github.com:timlrx/tailwind-nextjs-starter-blog.git
git pull template v1 --allow-unrelated-histories
rm -rf node_modules
```
You can see an example of such a migration in this [commit](https://github.com/timlrx/timlrx.com/commit/bba1c185384fd6d5cdaac15abf802fdcff027286) for my personal blog.
v1 also uses `feed.xml` rather than `index.xml`, to avoid some build issues with Vercel. If you are migrating you should add a redirect to `next.config.js` like so:
```js
async redirects() {
return [
{
source: '/:path/index.xml',
destination: '/:path/feed.xml',
permanent: true,
}
]
}
```

View File

@ -1,82 +0,0 @@
---
title: O Canada
date: '2017-07-15'
tags: ['holiday', 'canada', 'images']
draft: false
summary: The scenic lands of Canada featuring maple leaves, snow-capped mountains, turquoise lakes and Toronto. Take in the sights in this photo gallery exhibition and see how easy it is to replicate with some MDX magic and tailwind classes.
---
# O Canada
The scenic lands of Canada featuring maple leaves, snow-capped mountains, turquoise lakes and Toronto. Take in the sights in this photo gallery exhibition and see how easy it is to replicate with some MDX magic and tailwind classes.
Features images served using `next/image` component. The locally stored images are located in a folder with the following path: `/static/images/canada/[filename].jpg`
Since we are using mdx, we can create a simple responsive flexbox grid to display our images with a few tailwind css classes.
---
# Gallery
<div className="flex flex-wrap -mx-2 overflow-hidden xl:-mx-2">
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
![Maple](/static/images/canada/maple.jpg)
</div>
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
![Lake](/static/images/canada/lake.jpg)
</div>
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
![Mountains](/static/images/canada/mountains.jpg)
</div>
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
![Toronto](/static/images/canada/toronto.jpg)
</div>
</div>
# Implementation
```js
<div className="flex flex-wrap -mx-2 overflow-hidden xl:-mx-2">
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
![Maple](/static/images/canada/maple.jpg)
</div>
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
![Lake](/static/images/canada/lake.jpg)
</div>
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
![Mountains](/static/images/canada/mountains.jpg)
</div>
<div className="my-1 px-2 w-full overflow-hidden xl:my-1 xl:px-2 xl:w-1/2">
![Toronto](/static/images/canada/toronto.jpg)
</div>
</div>
```
With MDX v2, one can interleave markdown in jsx as shown in the example code.
### Photo Credits
<div>
Maple photo by [Guillaume
Jaillet](https://unsplash.com/@i_am_g?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText)
on
[Unsplash](https://unsplash.com/s/photos/canada?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText)
</div>
<div>
Mountains photo by [John
Lee](https://unsplash.com/@john_artifexfilms?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText)
on
[Unsplash](https://unsplash.com/s/photos/canada?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText)
</div>
<div>
Lake photo by [Tj
Holowaychuk](https://unsplash.com/@tjholowaychuk?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText)
on
[Unsplash](https://unsplash.com/s/photos/canada?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText)
</div>
<div>
Toronto photo by [Matthew
Henry](https://unsplash.com/@matthewhenry?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText)
on
[Unsplash](https://unsplash.com/s/photos/canada?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText)
</div>

View File

@ -0,0 +1,74 @@
---
title: React 18新的严格模式
date: '2023-02-06'
tags: ['React']
draft: false
summary: 好好的 useMemo、useEffect 居然执行了两次,我明明传入了依赖,为什么会执行两次呢?原来是 React 18 的破坏性改动!
---
之前在开发模式时,一直记得 `useMemo` 在严格模式下不会二次执行, `useEffect` 在有传入 `deps` 时也不会二次执行。而今天我在排下面这段代码时,发现一个要命的事情!
```tsx
const camera = useMemo(/* .. */, []);
const renderer = props; // from parent
const controls = useMemo(
() => new OrbitControls(camera, renderer.domElement),
[camera, renderer],
);
useEffect(() => {
// 同一个 controls 进入两次
return () => {
controls.dispose(); // 执行一次controls 被释放
};
}, [controls]);
```
`controls` 生成了两个,第一个似乎没用到了,第二个是后续要正常使用的对象,并且进入了 `useEffect` 中,而且进去了两次!
这导致两个严重的问题就是,第一个 `controls` 没有被销毁,第二个 `controls` 被销毁了!
第一个没销毁是内存泄漏,第二个被销毁了导致 `controls` 对象不可用了!
我一度以为是我对 useEffect 特性的记忆出现了偏差,后来我在官方文档翻了半天没啥收获,指到我看见了更新说明里的 [Stricter Strict Mode](https://github.com/facebook/react/blob/main/CHANGELOG.md#1820-june-14-2022:~:text=Stricter%20Strict%20Mode)。
## 新的严格模式
从更新日志可以看到,新的严格模式会自动卸载并再次重新挂载每个组件,这就解释了为什么 `useMemo``useEffect` 即使在有传入 `deps` 也会多执行一次。
这个特性是破坏性的,会影响之前版本的程序逻辑,所以 React 在[更新说明](https://github.com/facebook/react/blob/main/CHANGELOG.md#1820-june-14-2022:~:text=If%20this%20breaks%20your%20app%2C%20consider%20removing%20Strict%20Mode%20until%20you%20can%20fix%20the%20components%20to%20be%20resilient%20to%20remounting%20with%20existing%20state.)也建议如果旧的应用因为这个出现兼容性问题,建议先关掉 `strictMode`
### 在 React 18 的测试代码
![React 18 Stricter Strict Mode.png](https://pan.ivanli.cc/api/v3/file/source/2753/React%2018%20Stricter%20Strict%20Mode.png?sign=ARQ8AVTh-NEaeJRypJlVokuUVhocPeaK8n7GRSDwqNw%3D%3A0)
代码:[Code Sandbox](https://codesandbox.io/p/sandbox/clever-cache-pm1oct?file=%2Fsrc%2FApp.tsx&selection=%5B%7B%22endColumn%22%3A20%2C%22endLineNumber%22%3A33%2C%22startColumn%22%3A20%2C%22startLineNumber%22%3A33%7D%5D)
红色是第一次渲染,绿色是第二次渲染。输出日志里淡些的是 React 18 在二次调用时输出的 log 的默认效果,在 React 17 中是被默认隐藏的。蓝色指向的是最终应用在界面上呈现的结果。
我在 V 站上也提出了我的[疑问](https://v2ex.com/t/913595),根据大佬们的回复,我总结了一下:
1. `useMemo(() => /* */, [])` 执行一此后,以新的严格模式的规则,进行了二次调用,第一次的值作废。
2. `useEffect(() => /* */, [])`执行一此后,以新的严格模式的规则,调用了 `destructor` 后,进行了二次调用。
在第 2 点中,两次 useEffect 都是使用同一个值,是因为严格模式的二次调用按钩子分别执行两次,所以 useMemo 两次的调用都完毕后,得到的值再被 useEffect 执行两次。我调整了一下代码,将测试代码复制了一份在后面,可以看到 “useMemo” 和 “useMemo 2” 先执行了一次,又再执行了一次,然后再到 “useEffect“ 和 “useEffect 2"
![加倍快乐](https://pan.ivanli.cc/api/v3/file/source/2754/React%2018%20Stricter%20Strict%20Mode%202.png?sign=iYz9KP9uMuccRCesjqoRPKejEoUOj4FZfnBPt8kCXnQ%3D%3A0)
## 结论
`useEffect` 应该独自管理副作用,要做到自己创建,自己销毁。
```tsx
const camera = useMemo(/* .. */, []);
const renderer = props; // from parent
const [controls, setControls] = useState<OrbitControls | null>(null);
useEffect(() => {
const controls = new OrbitControls(camera, renderer.domElement) // 自己的锅
setControls(controls);
return () => {
controls.dispose(); // 自己背
};
}, [camera, renderer]);
```
今天深究了一下这个问题,解决方案其实我也知道,但是之前的写法突然以我不理解的方式失效了,还是要较个劲,万一是 React 不规范呢?(狗头

View File

@ -0,0 +1,35 @@
---
title: 自部署的 BaaS 服务对比
date: '2023-01-24'
tags: ['BaaS', 'Self-Hosted', 'appwrite', 'nhost', 'supabase']
draft: false
summary: supabase、nhost、appwrite 之间的对比,关注自部署方向。
---
BaaS后端即服务。
最近关注 BaaS 自部署主要还是因为自己有一些简单的服务端开发需求,可能就一两个函数的事。
如果专门去写一个后端,显得有些铺张了。而且创建一个项目是个麻烦事,我也不愿意从旧项目复制可能已经过时代码进来,所以就想到 FaaS。
但是 FaaS 我搜罗了一圈,适合个人自部署的 FaaS 平台少之又少,最后找了个 Serverless 的函数项目 [Trusted CGI](https://trusted-cgi.reddec.net/)[Repo](https://github.com/reddec/trusted-cgi)Go Lang
这个挺适合个人使用,但是无状态意味着我还得自己搞状态存储,还是不太符合我的需求,所以就看上了 BaaS。
## BaaS 简要介绍
目前 BaaS 大概是用户管理、授权、认证,加上数据库设计和存储、对象存储,再加上各类的 event hook 和 push再加上 Serverless Functions 构成的。这个组件构成非常适合原型、Demo 以及轻量的应用开发。
当然,如果后续有性能瓶颈,至少垂直扩展和 functions 层水平扩展是没有任何问题。至于持久层的水平扩展,也能像传统方案处理。
BaaS 最佳的应用场景就是各类 Apps 的服务端了。包括 Web Apps 在内,主要业务由 Apps 端处理的话BaaS 就是绝佳的生产力工具。而且服务端的业务逻辑基本上都是写在 Serverless Functions 里,所以根本不需要考虑升级会暂停服务,因为它是无状态的,更新时是能做到零秒重载的。
更权威的定义可以看[这里](https://www.cloudflare.com/zh-cn/learning/serverless/glossary/backend-as-a-service-baas/)。
## 对比
适合生产的环境自然也就是各大平台自有的云 BaaS 平台了,在他们各自的云平台上,你可以享受到完整、轻松的开发体验。但是这是对于企业和专业用途的个人用户,而玩票性质的我就暂时用不上了,所以我的目标就是开源的、可自部署的 BaaS 服务。
我淘了好久,找到了三个不错的开源项目,分别是 supabase <img alt="Packagist Stars" class="inline" src="https://img.shields.io/packagist/stars/appwrite/appwrite?style=social"/>、appwrite <img alt="Packagist Stars" class="inline" src="https://img.shields.io/packagist/stars/nhost/nhost?style=social"/>、nhost <img alt="Packagist Stars" class="inline" src="https://img.shields.io/packagist/stars/appwrite/appwrite?style=social"/>。他们仨在 Github 上的 Stars 目前是从多到少的。
接下来就以自部署的角度来对比下他们三个之间的差异。
TL; DR, 如果小项目多,推荐使用 appwrite如果现阶段需要表的关联建议使用 nhostsupabase 不适合自部署,他没有可以自部署的 serverless functions。
### Supabase
Github 上的星星老多了,可以说是目前最火的 BaaS 项目了。他对标的是  Firebase Firebase 的开源版本自居。

View File

@ -0,0 +1,61 @@
---
title: 利用 SNI 路由 TLS 连接实现端口复用
date: '2022-09-08'
tags: ['SNI', 'TLS', 'Reverse Proxy', '反向代理', 'Caddy', 'Xray', 'Vless']
draft: false
summary: 通过 SNI 反向代理,实现 VLESS 与 Web 站点共享 443 端口。
---
## 前言
这次的目标是通过 TLS 的 SNI 来实现对 TLS 连接的路由,以实现 SD-WAN 中的 VLESS 连接与 HTTPS 连接复用 443 端口。
如此一来,我在外访问家庭网络内的非 Web 服务时,能够很轻松地通过防火墙,因为 443 端口作为 HTTPS 默认端口,并且通过 VLESS 隧道访问的流量特征与 HTTPS 流量特征相同,有效避免被误杀。
### 试错
因为苦 Nginx 久矣,所以我近一年来都在用 Caddy 2而这次也是因为 Caddy 2 生态不够强大,所以绕了一圈。
#### Caddy 反向代理 Xray
Caddy 标准版本不提供 TCP 或 TLS 反向代理的能力,但是它有一个非官方的模块支持四层反向代理——[mholt/caddy-l4: Layer 4 (TCP/UDP) app for Caddy](https://github.com/mholt/caddy-l4)。
Caddy L4 支持 TCP 和 UDP 反向代理。但是,它目前为止还不支持 Caddyfile 配置,只能通过 JSON 配置。这就不友好了,我也就干脆地放弃这方案了。
#### Xray 反向代理 Caddy
Xray 的 VLESS 协议支持[回落](https://xtls.github.io/Xray-docs-next/document/level-1/fallbacks-with-sni.html)功能,能让非 VLESS 连接传递给后备服务。将 Caddy 作为后被服务,则能让 HTTPS 连接交由 Caddy 处理。
这个方案还行,首先,流量特征与 Caddy 差异小,这得益于都是 Go Lang 开发的程序;其次是性能损耗也可接受。唯一的问题就是,签发的泛域名证书只是 `*.ivanli.cc`,所以 443 端口只能部署 `*.ivanli.cc` 的站点,部署主域或者三级域名之类的站点将会报证书错误。所以这个方案也 pass 掉了。
### 在 Caddy 和 Xray 之前路由
使用一个支持 TLS SNI 路由的程序将连接按 SNI 反向代理到 Caddy 或 Xray 中。这个方案没啥局限性,就是浪费了些性能,增加了些延迟。我能接受,毕竟咱有追求不是?
那接下来就用这个方案来实现,那个路由就由 SNI-Proxy 来实现。
## 技术栈
### SNI Proxy
[dlundquist/sniproxy](https://github.com/dlundquist/sniproxy) 是一款支持对 HTTP 和 TLS 连接按虚拟主机名转发流量的反向代理程序。
对于 TLS 连接,它通过 SNI 进行区分并按规则转发到对应的端口,并不需要对 TLS 进行解密。并且它支持 HAProxy 代理协议将原始源地址传递给后端。
### VLESS
VLESS 是一种安全高效的数据传输协议,其支持的 xTLS 协议非常适合作为网络隧道传递 TLS 数据,能够极大地降低网关计算资源。
### Caddy
一款现代的 HTTP 反向代理程序。
## 部署步骤
使用 Docker Compose 部署。
以下是用到的容器:
- [caddy - Official Image | Docker Hub](https://registry.hub.docker.com/_/caddy)
- [tommylau/sniproxy - Docker Image | Docker Hub](https://registry.hub.docker.com/r/tommylau/sniproxy)
- [neilpang/acme.sh - Docker Image | Docker Hub](https://registry.hub.docker.com/r/neilpang/acme.sh)
- [gists/xray - Docker Image | Docker Hub](https://registry.hub.docker.com/r/gists/xray)
部署方式:[利用一台小鸡实现网络自由](./network-freedom-with-vps)

View File

@ -1,238 +0,0 @@
---
title: 'The Time Machine'
date: '2018-08-15'
tags: ['writings', 'book', 'reflection']
draft: false
summary: 'The Time Traveller (for so it will be convenient to speak of him) was
expounding a recondite matter to us. His pale grey eyes shone and
twinkled, and his usually pale face was flushed and animated...'
---
# The Time Machine by H. G. Wells
_Title_: The Time Machine
_Author_: H. G. Wells
_Subject_: Science Fiction
_Language_: English
_Source_: [Project Gutenberg](https://www.gutenberg.org/ebooks/35)
## Introduction
The Time Traveller (for so it will be convenient to speak of him) was
expounding a recondite matter to us. His pale grey eyes shone and
twinkled, and his usually pale face was flushed and animated. The fire
burnt brightly, and the soft radiance of the incandescent lights in the
lilies of silver caught the bubbles that flashed and passed in our
glasses. Our chairs, being his patents, embraced and caressed us rather
than submitted to be sat upon, and there was that luxurious
after-dinner atmosphere, when thought runs gracefully free of the
trammels of precision. And he put it to us in this way—marking the
points with a lean forefinger—as we sat and lazily admired his
earnestness over this new paradox (as we thought it) and his fecundity.
“You must follow me carefully. I shall have to controvert one or two
ideas that are almost universally accepted. The geometry, for instance,
they taught you at school is founded on a misconception.”
“Is not that rather a large thing to expect us to begin upon?” said
Filby, an argumentative person with red hair.
“I do not mean to ask you to accept anything without reasonable ground
for it. You will soon admit as much as I need from you. You know of
course that a mathematical line, a line of thickness _nil_, has no real
existence. They taught you that? Neither has a mathematical plane.
These things are mere abstractions.”
“That is all right,” said the Psychologist.
“Nor, having only length, breadth, and thickness, can a cube have a
real existence.”
“There I object,” said Filby. “Of course a solid body may exist. All
real things—”
“So most people think. But wait a moment. Can an _instantaneous_ cube
exist?”
“Dont follow you,” said Filby.
“Can a cube that does not last for any time at all, have a real
existence?”
Filby became pensive. “Clearly,” the Time Traveller proceeded, “any
real body must have extension in _four_ directions: it must have
Length, Breadth, Thickness, and—Duration. But through a natural
infirmity of the flesh, which I will explain to you in a moment, we
incline to overlook this fact. There are really four dimensions, three
which we call the three planes of Space, and a fourth, Time. There is,
however, a tendency to draw an unreal distinction between the former
three dimensions and the latter, because it happens that our
consciousness moves intermittently in one direction along the latter
from the beginning to the end of our lives.”
“That,” said a very young man, making spasmodic efforts to relight his
cigar over the lamp; “that . . . very clear indeed.”
“Now, it is very remarkable that this is so extensively overlooked,”
continued the Time Traveller, with a slight accession of cheerfulness.
“Really this is what is meant by the Fourth Dimension, though some
people who talk about the Fourth Dimension do not know they mean it. It
is only another way of looking at Time. _There is no difference between
Time and any of the three dimensions of Space except that our
consciousness moves along it_. But some foolish people have got hold of
the wrong side of that idea. You have all heard what they have to say
about this Fourth Dimension?”
“_I_ have not,” said the Provincial Mayor.
“It is simply this. That Space, as our mathematicians have it, is
spoken of as having three dimensions, which one may call Length,
Breadth, and Thickness, and is always definable by reference to three
planes, each at right angles to the others. But some philosophical
people have been asking why _three_ dimensions particularly—why not
another direction at right angles to the other three?—and have even
tried to construct a Four-Dimensional geometry. Professor Simon Newcomb
was expounding this to the New York Mathematical Society only a month
or so ago. You know how on a flat surface, which has only two
dimensions, we can represent a figure of a three-dimensional solid, and
similarly they think that by models of three dimensions they could
represent one of four—if they could master the perspective of the
thing. See?”
“I think so,” murmured the Provincial Mayor; and, knitting his brows,
he lapsed into an introspective state, his lips moving as one who
repeats mystic words. “Yes, I think I see it now,” he said after some
time, brightening in a quite transitory manner.
“Well, I do not mind telling you I have been at work upon this geometry
of Four Dimensions for some time. Some of my results are curious. For
instance, here is a portrait of a man at eight years old, another at
fifteen, another at seventeen, another at twenty-three, and so on. All
these are evidently sections, as it were, Three-Dimensional
representations of his Four-Dimensioned being, which is a fixed and
unalterable thing.
“Scientific people,” proceeded the Time Traveller, after the pause
required for the proper assimilation of this, “know very well that Time
is only a kind of Space. Here is a popular scientific diagram, a
weather record. This line I trace with my finger shows the movement of
the barometer. Yesterday it was so high, yesterday night it fell, then
this morning it rose again, and so gently upward to here. Surely the
mercury did not trace this line in any of the dimensions of Space
generally recognised? But certainly it traced such a line, and that
line, therefore, we must conclude, was along the Time-Dimension.”
“But,” said the Medical Man, staring hard at a coal in the fire, “if
Time is really only a fourth dimension of Space, why is it, and why has
it always been, regarded as something different? And why cannot we move
in Time as we move about in the other dimensions of Space?”
The Time Traveller smiled. “Are you so sure we can move freely in
Space? Right and left we can go, backward and forward freely enough,
and men always have done so. I admit we move freely in two dimensions.
But how about up and down? Gravitation limits us there.”
“Not exactly,” said the Medical Man. “There are balloons.”
“But before the balloons, save for spasmodic jumping and the
inequalities of the surface, man had no freedom of vertical movement.”
“Still they could move a little up and down,” said the Medical Man.
“Easier, far easier down than up.”
“And you cannot move at all in Time, you cannot get away from the
present moment.”
“My dear sir, that is just where you are wrong. That is just where the
whole world has gone wrong. We are always getting away from the present
moment. Our mental existences, which are immaterial and have no
dimensions, are passing along the Time-Dimension with a uniform
velocity from the cradle to the grave. Just as we should travel _down_
if we began our existence fifty miles above the earths surface.”
“But the great difficulty is this,” interrupted the Psychologist. You
_can_ move about in all directions of Space, but you cannot move about
in Time.”
“That is the germ of my great discovery. But you are wrong to say that
we cannot move about in Time. For instance, if I am recalling an
incident very vividly I go back to the instant of its occurrence: I
become absent-minded, as you say. I jump back for a moment. Of course
we have no means of staying back for any length of Time, any more than
a savage or an animal has of staying six feet above the ground. But a
civilised man is better off than the savage in this respect. He can go
up against gravitation in a balloon, and why should he not hope that
ultimately he may be able to stop or accelerate his drift along the
Time-Dimension, or even turn about and travel the other way?”
“Oh, _this_,” began Filby, “is all—”
“Why not?” said the Time Traveller.
“Its against reason,” said Filby.
“What reason?” said the Time Traveller.
“You can show black is white by argument,” said Filby, “but you will
never convince me.”
“Possibly not,” said the Time Traveller. “But now you begin to see the
object of my investigations into the geometry of Four Dimensions. Long
ago I had a vague inkling of a machine—”
“To travel through Time!” exclaimed the Very Young Man.
“That shall travel indifferently in any direction of Space and Time, as
the driver determines.”
Filby contented himself with laughter.
“But I have experimental verification,” said the Time Traveller.
“It would be remarkably convenient for the historian,” the Psychologist
suggested. “One might travel back and verify the accepted account of
the Battle of Hastings, for instance!”
“Dont you think you would attract attention?” said the Medical Man.
“Our ancestors had no great tolerance for anachronisms.”
“One might get ones Greek from the very lips of Homer and Plato,” the
Very Young Man thought.
“In which case they would certainly plough you for the Little-go. The
German scholars have improved Greek so much.”
“Then there is the future,” said the Very Young Man. “Just think! One
might invest all ones money, leave it to accumulate at interest, and
hurry on ahead!”
“To discover a society,” said I, “erected on a strictly communistic
basis.”
“Of all the wild extravagant theories!” began the Psychologist.
“Yes, so it seemed to me, and so I never talked of it until—”
“Experimental verification!” cried I. “You are going to verify _that_?”
“The experiment!” cried Filby, who was getting brain-weary.
“Lets see your experiment anyhow,” said the Psychologist, “though its
all humbug, you know.”
The Time Traveller smiled round at us. Then, still smiling faintly, and
with his hands deep in his trousers pockets, he walked slowly out of
the room, and we heard his slippers shuffling down the long passage to
his laboratory.
The Psychologist looked at us. “I wonder what hes got?”
“Some sleight-of-hand trick or other,” said the Medical Man, and Filby
tried to tell us about a conjuror he had seen at Burslem, but before he
had finished his preface the Time Traveller came back, and Filbys
anecdote collapsed.

View File

@ -0,0 +1,119 @@
---
title: 再见 2022你好 2023
date: '2022-12-31'
tags: ['总结']
draft: false
summary: 2022 年就要结束了,写个工作与学习的总结,记录下我的 2022 年。
---
二零二二,疫情不出意外地还在我们身边,而我们也早已经习惯了疫情。
## 电子电路与嵌入式开发
我一直对电子电路有着兴趣,而今年三月,我突然觉得我行了。购买了仪器和工具设备,开始自学电子电路知识。
软件开发一直和数字电路有着密不可分的联系,草草地学习完模拟电路和数字电路后,更加肯定软件和硬件不能独立,而是相辅相成的。
而我的第一个目标是构建一个锂电池 UPS为的是让充当服务器的 J4125 工控机能从容应对突然的断电。
项目我拆分了好几部分,一个阶段,一个阶段地完成,向着目标努力。
印象比较深的还是第一个部分的实现。第一个部分是实现一个理想二极管。
所谓理想二极管就是电流单向流动,并且(几乎)没有普通二极管的压降。
还记得我开开心心地在各大电子电路论坛爬楼找思路时,找到了个方案,在电路模拟软件搭建好电路后,运行了模拟,结果挺有效的。
我就赶紧画电路板打样了。等待样板寄来的包裹的这几天,我思来想去这电路好像不应该有效啊,换了个模拟软件一跑,坏了,不对。
接着就在洞洞板上做实验,果然,效果不对,压降那叫一个大。
最后这样改方案,画了第三块板后,终于实现了这一电路。果然实践才是检验真理的唯一途径,三个月从零搞出这么个东西,其实没啥,但是过程确实有趣。
就这样,边学边试,软件和硬件都验证完了,过程充满松香味。
不过可惜的是,半年后的八月,我换了 AMD 迷你主机UPS 设计供电已经不满足新主机的要求,项目搁置了。
电路的理论论证和各部分的功能验证板已经实现了,外壳也已经设计好了,但是还是没继续往下走,因为当时工作上发生了一些变动,就没余力折腾了。
之后又开始折腾屏幕氛围灯,挺有趣的,不过还没做完,目前一周也就花几小时在边学边弄。
我能想着开始折腾电子电路,还是因为国产单片机的崛起。
乐鑫公司的 ESP32 系列单片机真的是物美价廉,白嫖嘉立创的 PCB 也非常的香。
学习成本就低了很多,想想去年我入手的树莓派 4B。当时也有考虑折腾物联网但是一个三四百也只适合当个上位机干活了。
而现在用着 ESP32 模组和开发板,烧了也不心疼,嘿嘿。
嵌入式开发是一个陌生的领域C 语言是一个上古的高级语言,而天天写着现代高级语言的我,一时之间回不到大一时写 C 语言的感觉,
所以用了一段 C 语言做嵌入式开发后,我看上了 C++。或许是 C++ 没看上我,编译器报错我是一个也看不懂,所以我想起了 Rust。
Rust 比着“耶”向我招手,我大意了,就进了这个跟大的坑。
Rust 语言确实费脑子,因为开发过程中将会被编译器一直教育,非常地严谨。还记得我被引用变量的生命周期教育得死去活来,被结构体没有实现 `Copy``Send``Sync` 折磨得痛不欲生。
其实本来没这么复杂的,可是我却拿着嵌入式开发和 Rust 一起学,难度不能说陡增吧,只能说是经常看不到明天的太阳。
不过我还是磨出来了 UPS 的程序,还行,能用。之后开发屏幕氛围灯就滚回去用 C 语言写了,还别说,嵌入式开发我也学出了点感觉,写起来可亲切了。
因为示例多、论坛上的开发者主要也是用 C 写,资料很充足,也没有那群还在 0.0.x 版本的 Rust 库,感觉代码好写多了……
但是我贼心不死,拿着 Rust 转眼就配着 Tauri 开始开发氛围灯的上位机程序。
不知是没有了 rust-embedded 系的折磨,还是我懂得了 Rust 的脾气,开发得比较顺利,很舒服。
嵌入式的世界非常的美妙,将虚幻的软件借着硬件能更真实地让我们触碰到。
看好 IoT这是极客们的方向也是科技改变生活的方向。
## 元宇宙
年中因为业务调整,我的工作和元宇宙搭上了边。年中出去嗨皮了一周后,回来就开始学习 Unity 3D。
不得不说,国内疫情防控挺好的,走了好几个城市,回来也没阳,哈哈。
跑题了不得不说Unity 3D 作为入门 3D 游戏开发确实挺合适的,虽然我们当时本想着用 Unity 开发元宇宙项目。
正当我在庆幸我还没把 C Sharp 忘光光时,又被要求换成了现在正在使用的 Three.js 作为引擎,也就回到了 Web 领域。
接下来的半年,我和我的同事们便开始踩坑之旅。
因为是全新的领域,我和我的同事经历了 Unity 3D、Lingo 3D、r3f 这三个阶段,踩了许多的坑。
不知为什么,他们似乎对游戏开发好像并不觉得是全新的世界,极其低估了所需的知识储备。
现在回头看,每次的技术选型其实都不合理。
Lingo 3D 作为刚出现的框架,并没有经过市场的检验,也并没有基于该框架商业项目,使用这个框架和二开这个框架并没有什么区别。反对无力,作罢。
当我向 Lingo 3D 提了一个 PR 后,同事就抛弃了 Lingo 3D转向 r3f + BVH 碰撞检测。
而接下来,继续遇到了大量的性能问题。
从项目开发的第一周起,我就有一个 3D 场景渲染性能优化的任务挂着。
不过滑稽的是我的开发任务基本上在 WebRTC 相关部分和后端,而前端游戏场景渲染这部分并不是我开发的……
虽然处于尴尬的位置,优化是没处优化了,但是问题还是能另起项目去发现和验证。
我断断续续地折腾这事,现在回头看看,其实得出的结论挺正确的,但是当时没人懂也没人信,我也是半验证半猜测,没想到正确率还行。
- 游戏开发确实很考验建模师的素质,调优后的模型性能直接翻倍;
- 模型拆分、复用、LOD 是真的有明显的性能提升;
- 内存瓶颈是存在的,降低内存占用量能够让程序更稳定
虽然我对元宇宙没啥兴趣,不过可视化这方向是很有价值的。
看好元宇宙的风口,也看好可视化的前景。
## 自建服务
八月换了新的迷你主机作为服务器,依然使用移动平台的 CPU性能和功耗还可以。
比之前的机子性能好太多了,当然,满载时也学会芜湖起飞了。
我也多部署了几个服务。
### RSS 阅读器
现在信息茧房问题挺严重的,所以使用 Miniflux 自建了 RSS 阅读器。目前搜寻了一些个人博客、小众资讯站作为资讯源,感觉挺好的,没有乱七八糟的内容,很舒适。
### 标签打印机和短网址服务
另外,买了一个标签打印机,配合短网址服务 Kutt给我买的一堆电子元件做分类打标签。标签上带了个二维码里面存了描述元件信息文件的超链接地址。
### 日志分析服务
最后,因为部署了太多服务,自建服务又容易挂,所以跑了 Grafana + loki 用作日志分析服务。再配合在云服务器上部署的 Kuma 服务健康监控服务,目前服务状况了然于胸。
我还购入了国内的 NAT VPS做内网穿透和 SD-WAN比使用境外服务器快太多啦还稳定。
公司网络不太稳定,直接组网性能经常断线;
使用 VPN 拨入 NAT VPS 后,再由 NAT VPS 与家里组网,就没在掉过线。
现在从公网访问我的自建服务是由从境外服务器反向代理的,延迟比较大。
目前域名重新备案,希望能套上国内 CDN这样再由 NAT VPS 反向代理应该能改善国内访问速度。
## 开发容器与远程开发
那么,我为什么要组网呢,更大原因还是我想进行远程开发。
我在公司使用的是毕业时买的 MacBook Pro 2018 款,性能其实还不错,但是跑测试用例还是有些慢。
而且我今年也拥抱了 Dev Container 的开发方式,开发容器能很好地解决开发环境搭建问题。
再配合上数据库迁移脚本和数据生成工具,能极大地解决前后端开发时出现的数据污染问题。
可谓是 2022 年我最正确的选择了,哈哈哈哈。
这个选择是有代价的,那就是每开发一个项目都是有自己的持久化、缓存和应用组件,比较消耗硬件资源。
这不就换了个迷你主机嘛AMD yes!
因为在家里,所以就搞起了远程开发,家里网络质量也比较好,拉依赖什么的速度和稳定性比在公司高出了不少,这也极大地改善了我的开发体验。
要说这远程开发有啥不好的,那就是怕家里突然断网断电,那就有可能痛失劳动成果了。
开发容器是个不错的东西,今年看很多开源项目都用上了开发容器,也有很多开源项目还没用上。
但很明显,这是未来。我今年在开源社区也算是小小地冒了个泡,无论项目是否有开发容器的配置,我都会拉完项目后在开发容器运行,不用担心弄乱我的电脑,也不用担心环境冲突。
而且最重要的,是不用那么担心去年的项目今年怎么也跑不起来。这感觉,经历过的都懂。
最后,洁癖万岁~
比较可惜VS Code 的 Web 版本还不能支持开发容器,要是支持了,在 iPad 上快乐生产是多么值得的一件事呐。
## 未来可期
以上是我在 2022 年的经历,有些是我的计划,有些是命运的安排。贴近底层、满足兴趣,让我接触了嵌入式开发,意外的变动让我接触了 3D 游戏开发;心心念念的 Rust 终于安排上了,为了修自建的服务而改起了好几个 Go 语言项目。回顾过往,我已经摸了好多好多门编程语言了,也换了好多口味的编程风格,也大概摸清了自己向往的方向和风格。希望能够继续用着 TypeScript 和 Rust快乐地写着后端玩着前端搞着偏后端的全栈开发在 Arch Linux 上维护着服务。我可见不得 Java 和 PHP希望依旧再也不见。对还有 Python我和你不熟可别过来。

View File

@ -0,0 +1,7 @@
---
title: 使用 code-server 自部署 Web 版 VS Code
date: '2022-12-24'
tags: ['Develop', 'VS Code', 'Self Hosted']
draft: true
summary: 心心念念的在浏览器中写代码iPad 终于有希望回归生产力了。
---

View File

@ -1,20 +0,0 @@
const projectsData = [
// {
// title: 'A Search Engine',
// description: `What if you could look up any information in the world? Webpages, images, videos
// and more. Google has many features to help you find exactly what you're looking
// for.`,
// imgSrc: '/static/images/google.png',
// href: 'https://www.google.com',
// },
// {
// title: 'The Time Machine',
// description: `Imagine being able to travel back in time or to the future. Simple turn the knob
// to the desired date and press "Go". No more worrying about lost keys or
// forgotten headphones with this simple yet affordable solution.`,
// imgSrc: '/static/images/time-machine.jpg',
// href: '/blog/the-time-machine',
// },
]
export default projectsData

17
data/projectsData.ts Normal file
View File

@ -0,0 +1,17 @@
interface ProjectData {
title?: string
description?: string
imgSrc?: string
href?: string
}
const projectsData: ProjectData[] = [
{
title: 'UPS',
description: `一个不间断电源UPS的全栈项目。核心硬件使用乐鑫×安信可的 ESP32-C3-32S 模块作为主控,软件部分使用了 Rust + ESP-IDF 开发。`,
// imgSrc: '/static/images/google.png',
href: 'https://git.ivanli.cc/Ivan/ups-esp32c3-rust',
},
]
export default projectsData

View File

@ -30,4 +30,4 @@
author={Xie, Yihui},
year={2016},
publisher={CRC Press}
}
}

View File

@ -7,7 +7,7 @@ const siteMetadata = {
language: 'zh-CN',
theme: 'system', // system, dark or light
siteUrl: 'https://ivanli.cc/',
siteRepo: 'https://git.ivanli.cc/ivan/taliwind-nextjs-blog',
siteRepo: 'https://git.ivanli.cc/ivan/tailwind-nextjs-blog',
siteLogo:
'https://pan.ivanli.cc/api/v3/file/source/1234/%E5%A4%B4%E5%83%8F.png?sign=xIgy54DyFRYupxjZJbK02HmpKX8C53YR-O0I18Rxm70%3D%3A0',
image:
@ -33,14 +33,14 @@ const siteMetadata = {
newsletter: {
// supports mailchimp, buttondown, convertkit, klaviyo, revue, emailoctopus
// Please add your .env file and modify it according to your selection
provider: 'buttondown',
provider: '',
},
comment: {
// If you want to use a commenting system other than giscus you have to add it to the
// content security policy in the `next.config.js` file.
// Select a provider and use the environment variables associated to it
// https://vercel.com/docs/environment-variables
provider: 'giscus', // supported providers: giscus, utterances, disqus
provider: 'commento', // supported providers: giscus, utterances, disqus
giscusConfig: {
// Visit the link below, and follow the steps in the 'configuration' section
// https://giscus.app/
@ -55,10 +55,6 @@ const siteMetadata = {
// theme example: light, dark, dark_dimmed, dark_high_contrast
// transparent_dark, preferred_color_scheme, custom
theme: 'light',
// Place the comment box above the comments. options: bottom, top
inputPosition: 'bottom',
// Choose the language giscus will be displayed in. options: en, es, zh-CN, zh-TW, ko, ja etc
lang: 'en',
// theme when dark mode
darkTheme: 'transparent_dark',
// If the theme option above is set to 'custom`
@ -82,6 +78,13 @@ const siteMetadata = {
// https://help.disqus.com/en/articles/1717111-what-s-a-shortname
shortname: process.env.NEXT_PUBLIC_DISQUS_SHORTNAME,
},
cusdisConfig: {
appId: process.env.NEXT_PUBLIC_CUSDIS_APPID,
host: process.env.NEXT_PUBLIC_CUSDIS_HOST,
},
commentoConfig: {
url: process.env.NEXT_PUBLIC_COMMENTO_URL,
},
},
}

View File

@ -1,5 +1,6 @@
{
"compilerOptions": {
"jsx": "react",
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"],

View File

@ -1,9 +1,25 @@
import SocialIcon from '@/components/social-icons'
import Image from '@/components/Image'
import { PageSEO } from '@/components/SEO'
import SocialIcon from '@/components/social-icons';
import Image from '@/components/Image';
import { PageSEO } from '@/components/SEO';
import { ReactNode } from 'react';
import { AuthorFrontMatter } from 'types/AuthorFrontMatter';
export default function AuthorLayout({ children, frontMatter }) {
const { name, avatar, occupation, company, email, twitter, linkedin, github } = frontMatter
interface Props {
children: ReactNode;
frontMatter: AuthorFrontMatter;
}
export default function AuthorLayout({ children, frontMatter }: Props) {
const {
name,
avatar,
occupation,
company,
email,
twitter,
linkedin,
github,
} = frontMatter;
return (
<>
@ -23,7 +39,9 @@ export default function AuthorLayout({ children, frontMatter }) {
height="192px"
className="h-48 w-48 rounded-full"
/>
<h3 className="pt-4 pb-2 text-2xl font-bold leading-8 tracking-tight">{name}</h3>
<h3 className="pt-4 pb-2 text-2xl font-bold leading-8 tracking-tight">
{name}
</h3>
<div className="text-gray-500 dark:text-gray-400">{occupation}</div>
<div className="text-gray-500 dark:text-gray-400">{company}</div>
<div className="flex space-x-3 pt-6">
@ -33,9 +51,11 @@ export default function AuthorLayout({ children, frontMatter }) {
<SocialIcon kind="twitter" href={twitter} />
</div>
</div>
<div className="prose max-w-none pt-8 pb-8 dark:prose-dark xl:col-span-2">{children}</div>
<div className="prose max-w-none pt-8 pb-8 dark:prose-dark xl:col-span-2">
{children}
</div>
</div>
</div>
</>
)
);
}

View File

@ -1,20 +1,34 @@
import Link from '@/components/Link'
import Tag from '@/components/Tag'
import siteMetadata from '@/data/siteMetadata'
import { useState } from 'react'
import Pagination from '@/components/Pagination'
import formatDate from '@/lib/utils/formatDate'
import Link from '@/components/Link';
import Tag from '@/components/Tag';
import { ComponentProps, useState } from 'react';
import Pagination from '@/components/Pagination';
import formatDate from '@/lib/utils/formatDate';
import { PostFrontMatter } from 'types/PostFrontMatter';
interface Props {
posts: PostFrontMatter[];
title: string;
initialDisplayPosts?: PostFrontMatter[];
pagination?: ComponentProps<typeof Pagination>;
}
export default function ListLayout({ posts, title, initialDisplayPosts = [], pagination }) {
const [searchValue, setSearchValue] = useState('')
export default function ListLayout({
posts,
title,
initialDisplayPosts = [],
pagination,
}: Props) {
const [searchValue, setSearchValue] = useState('');
const filteredBlogPosts = posts.filter((frontMatter) => {
const searchContent = frontMatter.title + frontMatter.summary + frontMatter.tags.join(' ')
return searchContent.toLowerCase().includes(searchValue.toLowerCase())
})
const searchContent =
frontMatter.title + frontMatter.summary + frontMatter.tags.join(' ');
return searchContent.toLowerCase().includes(searchValue.toLowerCase());
});
// If initialDisplayPosts exist, display it if no searchValue is specified
const displayPosts =
initialDisplayPosts.length > 0 && !searchValue ? initialDisplayPosts : filteredBlogPosts
initialDisplayPosts.length > 0 && !searchValue
? initialDisplayPosts
: filteredBlogPosts;
return (
<>
@ -36,8 +50,7 @@ export default function ListLayout({ posts, title, initialDisplayPosts = [], pag
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
@ -50,7 +63,7 @@ export default function ListLayout({ posts, title, initialDisplayPosts = [], pag
<ul>
{!filteredBlogPosts.length && 'No posts found.'}
{displayPosts.map((frontMatter) => {
const { slug, date, title, summary, tags } = frontMatter
const { slug, date, title, summary, tags } = frontMatter;
return (
<li key={slug} className="py-4">
<article className="space-y-2 xl:grid xl:grid-cols-4 xl:items-baseline xl:space-y-0">
@ -63,7 +76,9 @@ export default function ListLayout({ posts, title, initialDisplayPosts = [], pag
<div className="space-y-3 xl:col-span-3">
<div>
<h3 className="text-2xl font-bold leading-8 tracking-tight">
<Link href={`/blog/${slug}`} className="text-gray-900 dark:text-gray-100">
<Link
href={`/blog/${slug}`}
className="text-gray-900 dark:text-gray-100">
{title}
</Link>
</h3>
@ -79,13 +94,16 @@ export default function ListLayout({ posts, title, initialDisplayPosts = [], pag
</div>
</article>
</li>
)
);
})}
</ul>
</div>
{pagination && pagination.totalPages > 1 && !searchValue && (
<Pagination currentPage={pagination.currentPage} totalPages={pagination.totalPages} />
<Pagination
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
/>
)}
</>
)
);
}

View File

@ -1,23 +1,70 @@
import Link from '@/components/Link'
import PageTitle from '@/components/PageTitle'
import SectionContainer from '@/components/SectionContainer'
import { BlogSEO } from '@/components/SEO'
import Image from '@/components/Image'
import Tag from '@/components/Tag'
import siteMetadata from '@/data/siteMetadata'
import Comments from '@/components/comments'
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
import Link from '@/components/Link';
import PageTitle from '@/components/PageTitle';
import SectionContainer from '@/components/SectionContainer';
import { BlogSEO } from '@/components/SEO';
import Image from '@/components/Image';
import Tag from '@/components/Tag';
import siteMetadata from '@/data/siteMetadata';
import Comments from '@/components/comments';
import ScrollTopAndComment from '@/components/ScrollTopAndComment';
import { ReactNode, useMemo } from 'react';
import { PostFrontMatter } from 'types/PostFrontMatter';
import { AuthorFrontMatter } from 'types/AuthorFrontMatter';
const editUrl = (fileName) => `${siteMetadata.siteRepo}/blob/master/data/blog/${fileName}`
const editUrl = (fileName) =>
`${siteMetadata.siteRepo}/raw/master/data/blog/${fileName}`;
const discussUrl = (slug) =>
`https://mobile.twitter.com/search?q=${encodeURIComponent(
`${siteMetadata.siteUrl}/blog/${slug}`
)}`
)}`;
const Copyright = () => (
<a
rel="license"
href="http://creativecommons.org/licenses/by-sa/4.0/"
className="inline-flex self-center">
<Image
className="border-0"
alt="知识共享许可协议"
width="88"
height="15"
src="https://i.creativecommons.org/l/by-sa/4.0/80x15.png"
/>
</a>
);
const postDateTemplate = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }
const postDateTemplate: Intl.DateTimeFormatOptions = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
};
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }) {
const { slug, fileName, date, title, images, tags } = frontMatter
interface Props {
frontMatter: PostFrontMatter;
authorDetails: AuthorFrontMatter[];
next?: { slug: string; title: string };
prev?: { slug: string; title: string };
children: ReactNode;
}
export default function PostLayout({
frontMatter,
authorDetails,
next,
prev,
children,
}: Props) {
const { slug, fileName, date, title, images, tags } = frontMatter;
const headerStyles = useMemo(
() =>
images?.[0]
? {
backgroundImage: `url(${images[0]})`,
}
: {},
[images]
);
return (
<SectionContainer>
@ -29,14 +76,27 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
<ScrollTopAndComment />
<article>
<div className="xl:divide-y xl:divide-gray-200 xl:dark:divide-gray-700">
<header className="pt-6 xl:pb-6">
<div className="space-y-1 text-center">
<header className="relative h-48 pt-6 xl:pb-6">
{images?.[0] && (
<Image
alt="background"
layout="fill"
objectFit="cover"
src={images[0]}
style={headerStyles}
className="blur-xs -z-10 opacity-50 bg-blend-soft-light"
/>
)}
<div className="space-y-5 text-center">
<dl className="space-y-10">
<div>
<dt className="sr-only">Published on</dt>
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
<time dateTime={date}>
{new Date(date).toLocaleDateString(siteMetadata.locale, postDateTemplate)}
{new Date(date).toLocaleDateString(
siteMetadata.locale,
postDateTemplate
)}
</time>
</dd>
</div>
@ -48,14 +108,15 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
</header>
<div
className="divide-y divide-gray-200 pb-8 dark:divide-gray-700 xl:grid xl:grid-cols-4 xl:gap-x-6 xl:divide-y-0"
style={{ gridTemplateRows: 'auto 1fr' }}
>
style={{ gridTemplateRows: 'auto 1fr' }}>
<dl className="pt-6 pb-10 xl:border-b xl:border-gray-200 xl:pt-11 xl:dark:border-gray-700">
<dt className="sr-only">Authors</dt>
<dd>
<ul className="flex justify-center space-x-8 sm:space-x-12 xl:block xl:space-x-0 xl:space-y-8">
{authorDetails.map((author) => (
<li className="flex items-center space-x-2" key={author.name}>
<li
className="flex items-center space-x-2"
key={author.name}>
{author.avatar && (
<Image
src={author.avatar}
@ -67,15 +128,19 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
)}
<dl className="whitespace-nowrap text-sm font-medium leading-5">
<dt className="sr-only">Name</dt>
<dd className="text-gray-900 dark:text-gray-100">{author.name}</dd>
<dd className="text-gray-900 dark:text-gray-100">
{author.name}
</dd>
<dt className="sr-only">Twitter</dt>
<dd>
{author.twitter && (
<Link
href={author.twitter}
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
>
{author.twitter.replace('https://twitter.com/', '@')}
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
{author.twitter.replace(
'https://twitter.com/',
'@'
)}
</Link>
)}
</dd>
@ -86,13 +151,12 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
</dd>
</dl>
<div className="divide-y divide-gray-200 dark:divide-gray-700 xl:col-span-3 xl:row-span-2 xl:pb-0">
<div className="prose max-w-none pt-10 pb-8 dark:prose-dark">{children}</div>
<div className="pt-6 pb-6 text-sm text-gray-700 dark:text-gray-300">
<Link href={discussUrl(slug)} rel="nofollow">
{'Discuss on Twitter'}
</Link>
{``}
<Link href={editUrl(fileName)}>{'View on GitHub'}</Link>
<div className="prose max-w-none pt-10 pb-8 dark:prose-dark">
{children}
</div>
<div className="flex items-center gap-4 pt-6 pb-6 text-sm text-gray-700 dark:text-gray-300">
<Copyright />
<Link href={editUrl(fileName)}>{'View source'}</Link>
</div>
<Comments frontMatter={frontMatter} />
</div>
@ -138,8 +202,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
<div className="pt-4 xl:pt-8">
<Link
href="/blog"
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
>
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
&larr; Back to the blog
</Link>
</div>
@ -148,5 +211,5 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
</div>
</article>
</SectionContainer>
)
);
}

View File

@ -1,18 +1,32 @@
import Link from '@/components/Link'
import PageTitle from '@/components/PageTitle'
import SectionContainer from '@/components/SectionContainer'
import { BlogSEO } from '@/components/SEO'
import siteMetadata from '@/data/siteMetadata'
import formatDate from '@/lib/utils/formatDate'
import Comments from '@/components/comments'
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
import Link from '@/components/Link';
import PageTitle from '@/components/PageTitle';
import SectionContainer from '@/components/SectionContainer';
import { BlogSEO } from '@/components/SEO';
import siteMetadata from '@/data/siteMetadata';
import formatDate from '@/lib/utils/formatDate';
import Comments from '@/components/comments';
import ScrollTopAndComment from '@/components/ScrollTopAndComment';
import { ReactNode } from 'react';
import { PostFrontMatter } from 'types/PostFrontMatter';
export default function PostLayout({ frontMatter, authorDetails, next, prev, children }) {
const { date, title } = frontMatter
interface Props {
frontMatter: PostFrontMatter;
children: ReactNode;
next?: { slug: string; title: string };
prev?: { slug: string; title: string };
}
export default function PostLayout({
frontMatter,
next,
prev,
children,
}: Props) {
const { slug, date, title } = frontMatter;
return (
<SectionContainer>
<BlogSEO url={`${siteMetadata.siteUrl}/blog/${frontMatter.slug}`} {...frontMatter} />
<BlogSEO url={`${siteMetadata.siteUrl}/blog/${slug}`} {...frontMatter} />
<ScrollTopAndComment />
<article>
<div>
@ -33,10 +47,11 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
</header>
<div
className="divide-y divide-gray-200 pb-8 dark:divide-gray-700 xl:divide-y-0 "
style={{ gridTemplateRows: 'auto 1fr' }}
>
style={{ gridTemplateRows: 'auto 1fr' }}>
<div className="divide-y divide-gray-200 dark:divide-gray-700 xl:col-span-3 xl:row-span-2 xl:pb-0">
<div className="prose max-w-none pt-10 pb-8 dark:prose-dark">{children}</div>
<div className="prose max-w-none pt-10 pb-8 dark:prose-dark">
{children}
</div>
</div>
<Comments frontMatter={frontMatter} />
<footer>
@ -45,8 +60,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
<div className="pt-4 xl:pt-8">
<Link
href={`/blog/${prev.slug}`}
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
>
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
&larr; {prev.title}
</Link>
</div>
@ -55,8 +69,7 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
<div className="pt-4 xl:pt-8">
<Link
href={`/blog/${next.slug}`}
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
>
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
{next.title} &rarr;
</Link>
</div>
@ -67,5 +80,5 @@ export default function PostLayout({ frontMatter, authorDetails, next, prev, chi
</div>
</article>
</SectionContainer>
)
);
}

View File

@ -1,8 +1,9 @@
import { escape } from '@/lib/utils/htmlEscaper'
import { escape } from '@/lib/utils/htmlEscaper';
import siteMetadata from '@/data/siteMetadata'
import siteMetadata from '@/data/siteMetadata';
import { PostFrontMatter } from 'types/PostFrontMatter';
const generateRssItem = (post) => `
const generateRssItem = (post: PostFrontMatter) => `
<item>
<guid>${siteMetadata.siteUrl}/blog/${post.slug}</guid>
<title>${escape(post.title)}</title>
@ -12,21 +13,25 @@ const generateRssItem = (post) => `
<author>${siteMetadata.email} (${siteMetadata.author})</author>
${post.tags && post.tags.map((t) => `<category>${t}</category>`).join('')}
</item>
`
`;
const generateRss = (posts, page = 'feed.xml') => `
const generateRss = (posts: PostFrontMatter[], page = 'feed.xml') => `
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>${escape(siteMetadata.title)}</title>
<link>${siteMetadata.siteUrl}/blog</link>
<description>${escape(siteMetadata.description)}</description>
<language>${siteMetadata.language}</language>
<managingEditor>${siteMetadata.email} (${siteMetadata.author})</managingEditor>
<managingEditor>${siteMetadata.email} (${
siteMetadata.author
})</managingEditor>
<webMaster>${siteMetadata.email} (${siteMetadata.author})</webMaster>
<lastBuildDate>${new Date(posts[0].date).toUTCString()}</lastBuildDate>
<atom:link href="${siteMetadata.siteUrl}/${page}" rel="self" type="application/rss+xml"/>
<atom:link href="${
siteMetadata.siteUrl
}/${page}" rel="self" type="application/rss+xml"/>
${posts.map(generateRssItem).join('')}
</channel>
</rss>
`
export default generateRss
`;
export default generateRss;

View File

@ -1,136 +0,0 @@
import { bundleMDX } from 'mdx-bundler'
import fs from 'fs'
import matter from 'gray-matter'
import path from 'path'
import readingTime from 'reading-time'
import { visit } from 'unist-util-visit'
import getAllFilesRecursively from './utils/files'
// Remark packages
import remarkGfm from 'remark-gfm'
import remarkFootnotes from 'remark-footnotes'
import remarkMath from 'remark-math'
import remarkExtractFrontmatter from './remark-extract-frontmatter'
import remarkCodeTitles from './remark-code-title'
import remarkTocHeadings from './remark-toc-headings'
import remarkImgToJsx from './remark-img-to-jsx'
// Rehype packages
import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypeKatex from 'rehype-katex'
import rehypeCitation from 'rehype-citation'
import rehypePrismPlus from 'rehype-prism-plus'
import rehypePresetMinify from 'rehype-preset-minify'
const root = process.cwd()
export function getFiles(type) {
const prefixPaths = path.join(root, 'data', type)
const files = getAllFilesRecursively(prefixPaths)
// Only want to return blog/path and ignore root, replace is needed to work on Windows
return files.map((file) => file.slice(prefixPaths.length + 1).replace(/\\/g, '/'))
}
export function formatSlug(slug) {
return slug.replace(/\.(mdx|md)/, '')
}
export function dateSortDesc(a, b) {
if (a > b) return -1
if (a < b) return 1
return 0
}
export async function getFileBySlug(type, slug) {
const mdxPath = path.join(root, 'data', type, `${slug}.mdx`)
const mdPath = path.join(root, 'data', type, `${slug}.md`)
const source = fs.existsSync(mdxPath)
? fs.readFileSync(mdxPath, 'utf8')
: fs.readFileSync(mdPath, 'utf8')
// https://github.com/kentcdodds/mdx-bundler#nextjs-esbuild-enoent
if (process.platform === 'win32') {
process.env.ESBUILD_BINARY_PATH = path.join(root, 'node_modules', 'esbuild', 'esbuild.exe')
} else {
process.env.ESBUILD_BINARY_PATH = path.join(root, 'node_modules', 'esbuild', 'bin', 'esbuild')
}
let toc = []
const { code, frontmatter } = await bundleMDX({
source,
// mdx imports can be automatically source from the components directory
cwd: path.join(root, 'components'),
xdmOptions(options, frontmatter) {
// this is the recommended way to add custom remark/rehype plugins:
// The syntax might look weird, but it protects you in case we add/remove
// plugins in the future.
options.remarkPlugins = [
...(options.remarkPlugins ?? []),
remarkExtractFrontmatter,
[remarkTocHeadings, { exportRef: toc }],
remarkGfm,
remarkCodeTitles,
[remarkFootnotes, { inlineNotes: true }],
remarkMath,
remarkImgToJsx,
]
options.rehypePlugins = [
...(options.rehypePlugins ?? []),
rehypeSlug,
rehypeAutolinkHeadings,
rehypeKatex,
[rehypeCitation, { path: path.join(root, 'data') }],
[rehypePrismPlus, { ignoreMissing: true }],
rehypePresetMinify,
]
return options
},
esbuildOptions: (options) => {
options.loader = {
...options.loader,
'.js': 'jsx',
}
return options
},
})
return {
mdxSource: code,
toc,
frontMatter: {
readingTime: readingTime(code),
slug: slug || null,
fileName: fs.existsSync(mdxPath) ? `${slug}.mdx` : `${slug}.md`,
...frontmatter,
date: frontmatter.date ? new Date(frontmatter.date).toISOString() : null,
},
}
}
export async function getAllFilesFrontMatter(folder) {
const prefixPaths = path.join(root, 'data', folder)
const files = getAllFilesRecursively(prefixPaths)
const allFrontMatter = []
files.forEach((file) => {
// Replace is needed to work on Windows
const fileName = file.slice(prefixPaths.length + 1).replace(/\\/g, '/')
// Remove Unexpected File
if (path.extname(fileName) !== '.md' && path.extname(fileName) !== '.mdx') {
return
}
const source = fs.readFileSync(file, 'utf8')
const { data: frontmatter } = matter(source)
if (frontmatter.draft !== true) {
allFrontMatter.push({
...frontmatter,
slug: formatSlug(fileName),
date: frontmatter.date ? new Date(frontmatter.date).toISOString() : null,
})
}
})
return allFrontMatter.sort((a, b) => dateSortDesc(a.date, b.date))
}

157
lib/mdx.ts Normal file
View File

@ -0,0 +1,157 @@
import { bundleMDX } from 'mdx-bundler';
import fs from 'fs';
import matter from 'gray-matter';
import path from 'path';
import readingTime from 'reading-time';
import getAllFilesRecursively from './utils/files';
import { PostFrontMatter } from 'types/PostFrontMatter';
import { AuthorFrontMatter } from 'types/AuthorFrontMatter';
import { Toc } from 'types/Toc';
// Remark packages
import remarkGfm from 'remark-gfm';
import remarkFootnotes from 'remark-footnotes';
import remarkMath from 'remark-math';
import remarkExtractFrontmatter from './remark-extract-frontmatter';
import remarkCodeTitles from './remark-code-title';
import remarkTocHeadings from './remark-toc-headings';
import remarkImgToJsx from './remark-img-to-jsx';
// Rehype packages
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeKatex from 'rehype-katex';
import rehypeCitation from 'rehype-citation';
import rehypePrismPlus from 'rehype-prism-plus';
import rehypePresetMinify from 'rehype-preset-minify';
const root = process.cwd();
export function getFiles(type: 'blog' | 'authors') {
const prefixPaths = path.join(root, 'data', type);
const files = getAllFilesRecursively(prefixPaths);
// Only want to return blog/path and ignore root, replace is needed to work on Windows
return files.map((file) =>
file.slice(prefixPaths.length + 1).replace(/\\/g, '/')
);
}
export function formatSlug(slug: string) {
return slug.replace(/\.(mdx|md)/, '');
}
export function dateSortDesc(a: string, b: string) {
if (a > b) return -1;
if (a < b) return 1;
return 0;
}
export async function getFileBySlug<T>(
type: 'authors' | 'blog',
slug: string | string[]
) {
const mdxPath = path.join(root, 'data', type, `${slug}.mdx`);
const mdPath = path.join(root, 'data', type, `${slug}.md`);
const source = fs.existsSync(mdxPath)
? fs.readFileSync(mdxPath, 'utf8')
: fs.readFileSync(mdPath, 'utf8');
// https://github.com/kentcdodds/mdx-bundler#nextjs-esbuild-enoent
if (process.platform === 'win32') {
process.env.ESBUILD_BINARY_PATH = path.join(
root,
'node_modules',
'esbuild',
'esbuild.exe'
);
} else {
process.env.ESBUILD_BINARY_PATH = path.join(
root,
'node_modules',
'esbuild',
'bin',
'esbuild'
);
}
const toc: Toc = [];
const { code, frontmatter } = await bundleMDX({
source,
// mdx imports can be automatically source from the components directory
cwd: path.join(root, 'components'),
xdmOptions(options, frontmatter) {
// this is the recommended way to add custom remark/rehype plugins:
// The syntax might look weird, but it protects you in case we add/remove
// plugins in the future.
options.remarkPlugins = [
...(options.remarkPlugins ?? []),
remarkExtractFrontmatter,
[remarkTocHeadings, { exportRef: toc }],
remarkGfm,
remarkCodeTitles,
[remarkFootnotes, { inlineNotes: true }],
remarkMath,
remarkImgToJsx,
];
options.rehypePlugins = [
...(options.rehypePlugins ?? []),
rehypeSlug,
rehypeAutolinkHeadings,
rehypeKatex,
[rehypeCitation, { path: path.join(root, 'data') }],
[rehypePrismPlus, { ignoreMissing: true }],
rehypePresetMinify,
];
return options;
},
esbuildOptions: (options) => {
options.loader = {
...options.loader,
'.js': 'jsx',
};
return options;
},
});
return {
mdxSource: code,
toc,
frontMatter: {
readingTime: readingTime(code),
slug: slug || null,
fileName: fs.existsSync(mdxPath) ? `${slug}.mdx` : `${slug}.md`,
...frontmatter,
date: frontmatter.date ? new Date(frontmatter.date).toISOString() : null,
},
};
}
export async function getAllFilesFrontMatter(folder: 'blog') {
const prefixPaths = path.join(root, 'data', folder);
const files = getAllFilesRecursively(prefixPaths);
const allFrontMatter: PostFrontMatter[] = [];
files.forEach((file: string) => {
// Replace is needed to work on Windows
const fileName = file.slice(prefixPaths.length + 1).replace(/\\/g, '/');
// Remove Unexpected File
if (path.extname(fileName) !== '.md' && path.extname(fileName) !== '.mdx') {
return;
}
const source = fs.readFileSync(file, 'utf8');
const matterFile = matter(source);
const frontmatter = matterFile.data as AuthorFrontMatter | PostFrontMatter;
if ('draft' in frontmatter && frontmatter.draft !== true) {
allFrontMatter.push({
...frontmatter,
slug: formatSlug(fileName),
date: frontmatter.date
? new Date(frontmatter.date).toISOString()
: null,
});
}
});
return allFrontMatter.sort((a, b) => dateSortDesc(a.date, b.date));
}

View File

@ -1,32 +0,0 @@
import { visit } from 'unist-util-visit'
export default function remarkCodeTitles() {
return (tree) =>
visit(tree, 'code', (node, index, parent) => {
const nodeLang = node.lang || ''
let language = ''
let title = ''
if (nodeLang.includes(':')) {
language = nodeLang.slice(0, nodeLang.search(':'))
title = nodeLang.slice(nodeLang.search(':') + 1, nodeLang.length)
}
if (!title) {
return
}
const className = 'remark-code-title'
const titleNode = {
type: 'mdxJsxFlowElement',
name: 'div',
attributes: [{ type: 'mdxJsxAttribute', name: 'className', value: className }],
children: [{ type: 'text', value: title }],
data: { _xdmExplicitJsx: true },
}
parent.children.splice(index, 0, titleNode)
node.lang = language
})
}

38
lib/remark-code-title.ts Normal file
View File

@ -0,0 +1,38 @@
import { visit, Parent } from 'unist-util-visit';
export default function remarkCodeTitles() {
return (tree: Parent & { lang?: string }) =>
visit(
tree,
'code',
(node: Parent & { lang?: string }, index, parent: Parent) => {
const nodeLang = node.lang || '';
let language = '';
let title = '';
if (nodeLang.includes(':')) {
language = nodeLang.slice(0, nodeLang.search(':'));
title = nodeLang.slice(nodeLang.search(':') + 1, nodeLang.length);
}
if (!title) {
return;
}
const className = 'remark-code-title';
const titleNode = {
type: 'mdxJsxFlowElement',
name: 'div',
attributes: [
{ type: 'mdxJsxAttribute', name: 'className', value: className },
],
children: [{ type: 'text', value: title }],
data: { _xdmExplicitJsx: true },
};
parent.children.splice(index, 0, titleNode);
node.lang = language;
}
);
}

View File

@ -1,10 +0,0 @@
import { visit } from 'unist-util-visit'
import { load } from 'js-yaml'
export default function extractFrontmatter() {
return (tree, file) => {
visit(tree, 'yaml', (node, index, parent) => {
file.data.frontmatter = load(node.value)
})
}
}

Some files were not shown because too many files have changed in this diff Show More