\\n </a>\\n </div>\\n \\n <div>\\n <div class=\\"letterboxd-embed-tc-title\\">\\n The Volunteers: The Battle of Life and Death\\n <span class=\\"letterboxd-embed-tc-year\\"> 2024 </span>\\n </div>\\n \\n <div class=\\"letterboxd-embed-tc-date\\">October 3, 2024</div>\\n \\n <div class=\\"letterboxd-embed-tc-rating\\">★★½</div>\\n </div>\\n </div>\\n </div>\\n \\n <div class=\\"letterboxd-embed-tc-divider\\"></div>\\n \\n <div class=\\"letterboxd-embed-tc-diary-entry\\">\\n <div class=\\"letterboxd-embed-tc-content\\">\\n <div class=\\"letterboxd-embed-tc-poster\\">\\n <a\\n href=\\"https://letterboxd.com/lkw123/film/the-cloud-in-her-room/1/\\"\\n target=\\"_blank\\"\\n >\\n <img\\n src=\\"https://a.ltrbxd.com/resized/film-poster/5/0/4/5/1/8/504518-the-cloud-in-her-room-0-70-0-105-crop.jpg?v=60cbaf1d17\\"\\n alt=\\"The Cloud in Her Room poster\\"\\n />\\n </a>\\n </div>\\n \\n <div>\\n <div class=\\"letterboxd-embed-tc-title\\">\\n The Cloud in Her Room\\n <span class=\\"letterboxd-embed-tc-year\\"> 2020 </span>\\n </div>\\n \\n <div class=\\"letterboxd-embed-tc-date\\">♺ September 7, 2024</div>\\n \\n <div class=\\"letterboxd-embed-tc-rating\\">★★★½</div>\\n \\n <div class=\\"letterboxd-embed-tc-review\\">\\n One of my favorite film posters\\n </div>\\n </div>\\n </div>\\n </div>\\n \\n <div class=\\"letterboxd-embed-tc-divider\\"></div>\\n \\n <div class=\\"letterboxd-embed-tc-diary-entry\\">\\n <div class=\\"letterboxd-embed-tc-content\\">\\n <div class=\\"letterboxd-embed-tc-poster\\">\\n <a href=\\"https://letterboxd.com/lkw123/film/alien/\\" target=\\"_blank\\">\\n <img\\n src=\\"https://a.ltrbxd.com/resized/sm/upload/8v/f1/qw/aa/bg7K6VtUG7Ew70gQj6SSroD5d4R-0-70-0-105-crop.jpg?v=a932f9e98e\\"\\n alt=\\"Alien poster\\"\\n />\\n </a>\\n </div>\\n \\n <div>\\n <div class=\\"letterboxd-embed-tc-title\\">\\n Alien\\n <span class=\\"letterboxd-embed-tc-year\\"> 1979 </span>\\n </div>\\n \\n <div class=\\"letterboxd-embed-tc-date\\">August 25, 2024</div>\\n \\n <div class=\\"letterboxd-embed-tc-rating\\">★★★★</div>\\n </div>\\n </div>\\n </div>\\n \\n <div class=\\"letterboxd-embed-tc-divider\\"></div>\\n \\n <div class=\\"letterboxd-embed-tc-diary-entry\\">\\n <div class=\\"letterboxd-embed-tc-content\\">\\n <div class=\\"letterboxd-embed-tc-poster\\">\\n <a\\n href=\\"https://letterboxd.com/lkw123/film/alien-romulus/\\"\\n target=\\"_blank\\"\\n >\\n <img\\n src=\\"https://a.ltrbxd.com/resized/film-poster/8/5/0/4/5/9/850459-alien-romulus-0-70-0-105-crop.jpg?v=acabb7fd83\\"\\n alt=\\"Alien: Romulus poster\\"\\n />\\n </a>\\n </div>\\n \\n <div>\\n <div class=\\"letterboxd-embed-tc-title\\">\\n Alien: Romulus\\n <span class=\\"letterboxd-embed-tc-year\\"> 2024 </span>\\n </div>\\n \\n <div class=\\"letterboxd-embed-tc-date\\">August 21, 2024</div>\\n \\n <div class=\\"letterboxd-embed-tc-rating\\">★★★★</div>\\n </div>\\n </div>\\n </div>\\n \\n <div class=\\"letterboxd-embed-tc-divider\\"></div>\\n \\n <div class=\\"letterboxd-embed-tc-diary-entry\\">\\n <div class=\\"letterboxd-embed-tc-content\\">\\n <div class=\\"letterboxd-embed-tc-poster\\">\\n <a\\n href=\\"https://letterboxd.com/lkw123/film/despicable-me-4/\\"\\n target=\\"_blank\\"\\n >\\n <img\\n src=\\"https://a.ltrbxd.com/resized/film-poster/4/4/8/5/0/6/448506-despicable-me-4-0-70-0-105-crop.jpg?v=3391582e43\\"\\n alt=\\"Despicable Me 4 poster\\"\\n />\\n </a>\\n </div>\\n \\n <div>\\n <div class=\\"letterboxd-embed-tc-title\\">\\n Despicable Me 4\\n <span class=\\"letterboxd-embed-tc-year\\"> 2024 </span>\\n </div>\\n \\n <div class=\\"letterboxd-embed-tc-date\\">August 13, 2024</div>\\n \\n <div class=\\"letterboxd-embed-tc-rating\\">★★</div>\\n </div>\\n </div>\\n </div>\\n \\n <div class=\\"letterboxd-embed-tc-divider\\"></div>\\n \\n <div class=\\"letterboxd-embed-tc-more\\">\\n <a href=\\"https://letterboxd.com/lkw123\\" target=\\"_blank\\"\\n >...more on Letterboxd</a\\n >\\n </div>\\n</div>\\n
可以通过如下方式将其嵌入到博客中。具体效果详见我的博客的 Misc 页面 的 Movie Stats 部分。
\\n<div id=\\"letterboxd-embed-wrapper-tc\\">Loading...</div>\\n \\n<script>\\n fetch(\'https://letterboxd-embed.lkwplus.com?username=lkw123\')\\n .then((response) => response.text())\\n .then((data) => {\\n const element = document.getElementById(\'letterboxd-embed-wrapper-tc\')\\n if (element) {\\n element.innerHTML = data\\n }\\n })\\n</script>
根据豆瓣、IMDb、Bangumi、Steam 链接自动生成简介,主要用于 PT(自动化)发种,文档地址。
\\n此前在 Vercel 上部署的项目很多,但自决定全面转向 Cloudflare 的 CDN 和各类服务后,逐步对项目进行迁移。
\\nRSSHub 是一个开源的、用于将各种网站的内容聚合到一个统一的接口中生成 RSS Feed 的项目,主要用于订阅未提供 RSS Feed 的网站或社交媒体。在这个碎片化信息爆炸的时代,做一些试图逃脱信息茧房的挣扎。
\\n根据 GitHub Issue #14622 的讨论,在今年上半年起 Vercel/Cloudflare Workers 无法部署 RSSHub 的最新代码,测试可用的最新 Commit 截至 29276d8
。这个问题的修复目前卡在 got 和 NextJS 不兼容,需要等待 got 依赖从 RSSHub 项目移除。
暂时性的解决方案为:首先将 DIYgod/RSSHub 完整的 Fork 至自己账号下,可以在 Vercel 的 Create Deployment 处填入相应的 Commit 链接 https://github.com/{your-username}/RSSHub/commit/29276d8
,或是在 GitHub 将默认分支由 master 切换至 legacy 分支后再部署。
跨平台的电子书阅读器,部署教程详见 README。
\\nGoogle Analytics 的开源替代,项目地址。
\\n采用 Vercel 免费提供的 Postgres 作为后端数据库,和项目进行关联后,在部署页面配置数据库连接信息即可。同时,可以修改 TRACKER_SCRIPT_NAME
环境变量为不包含 umami、analytics 等关键字的名称,避免目标网站中引入的 js 脚本被 Adblock 等插件拦截。
项目地址,基于 Notion 的 Next.js 静态博客,类似的开源解决方案如 NotionNext,闭源解决方案如 Super,或是直接选择使用官方的 Notion Website 功能即可。
\\n项目地址,极简风格的静态面板,比较适合用于服务器、Homelab、NAS 等,可以方便的集中整理自己的各类自部署服务或书签。
\\n项目后端 imsyy/DailyHotApi-Vercel,项目前端 imsyy/DailyHot。后端部署完毕后,更改前端项目的环境变量 VITE_GLOBAL_API
即可,如 “https://dailyhot-api.lkwplus.com”。
项目地址,用于生成个人的最新 Last.fm 听歌记录,可美观的展示于 GitHub Profile 或其他地方,类似的还可以选择同开发者的 JeffreyCA/spotify-recently-played-readme 项目。
\\n由于该仓库自身提供服务使用的 *.vercel.app
子域名被墙,因此部署后绑定自定义域名以规避这个问题。具体效果详见我的博客的 Misc 页面 的 Music Stats 部分。
网易云音乐 Node.js API service,原仓库由于版权问题现已删库,如有需求可以考虑部署 PHP 实现 kilingzhang/NeteaseCloudMusicApi。自己此前 fork 的 仓库 尚且幸存,部署的 API 一直可以能正常使用。此前主要用于配合 qier222/YesPlayMusic 在 Vercel 搭建在线音乐 Web 端,但很少使用。
\\n我的 fly.io 账户幸运的停留在了 Legacy Hobby plan,可以免费部署三个 cpu-1x mem-256M 的实例,相当的大方且慷慨,感激 :)
\\n阅读的服务器端项目,可以方便的在网页端摸鱼看小说。文档地址,fly.toml
文件内容如下:
app = \'lkw123-reader\'\\nprimary_region = \'iad\'\\n \\n[build]\\n image = \\"hectorqin/reader:openj9-latest\\"\\n \\n[env]\\n SPRING_PROFILES_ACTIVE = \\"prod\\"\\n READER_APP_USERLIMIT = \\"1\\"\\n READER_APP_USERBOOKLIMIT = \\"200\\"\\n READER_APP_CACHECHAPTERCONTENT = \\"true\\"\\n READER_APP_SECURE = \\"true\\"\\n READER_APP_SECUREKEY = \\"{your_secret_key}\\"\\n \\n[http_service]\\n internal_port = 8080\\n force_https = true\\n auto_stop_machines = \'off\'\\n auto_start_machines = false\\n min_machines_running = 0\\n processes = [\'app\']\\n \\n[mounts]\\n source=\\"reader_data\\"\\n destination=\\"/storage\\"\\n \\n[[vm]]\\n size = \'shared-cpu-1x\'
源码存放于我的 GitHub 仓库 synthpop123/fly-fastapi,fly.toml
文件内容如下:
app = \'lkw123-fastapi\'\\nprimary_region = \'iad\'\\n \\n[build]\\n builder = \'paketobuildpacks/builder:base\'\\n \\n[env]\\n PORT = \'8000\'\\n \\n[http_service]\\n internal_port = 8000\\n force_https = true\\n auto_stop_machines = \'off\'\\n auto_start_machines = false\\n min_machines_running = 0\\n processes = [\'app\']\\n \\n[[vm]]\\n size = \'shared-cpu-1x\'
使用 Rust 构建的开源密码管理器 Bitwarden 的服务端实现,且和上游的 Bitwarden 客户端兼容,fly.toml
文件内容如下:
app = \'lkw123-vault\'\\nprimary_region = \'iad\'\\n \\n[http_service]\\n internal_port = 8080\\n force_https = true\\n auto_stop_machines = \'off\'\\n auto_start_machines = false\\n min_machines_running = 0\\n processes = [\'app\']\\n \\n[env]\\n SIGNUPS_ALLOWED = \\"false\\"\\n \\n[build]\\n image = \\"vaultwarden/server:latest\\"\\n \\n[mounts]\\n source = \\"vw_data\\"\\n destination = \\"/data\\"\\n \\n[[services]]\\n http_checks = []\\n internal_port = 80\\n [[services.ports]]\\n force_https = true\\n handlers = [\\"http\\"]\\n port = 80\\n \\n [[services.ports]]\\n handlers = [\\"tls\\", \\"http\\"]\\n port = 443\\n \\n [[services.tcp_checks]]\\n grace_period = \\"1s\\"\\n interval = \\"15s\\"\\n restart_limit = 0\\n timeout = \\"2s\\"\\n \\n[[vm]]\\n size = \'shared-cpu-1x\'
参考 hu3rror/memos-on-fly 的文档进行部署,除了保有项目的原功能外,还支持通过 Litestream 自动备份数据库到个人的 Backblaze B2。
\\n为了接入 Telegram Bot,需要采用标签为 stable-memogram
的镜像,并需要手动添加 Bot Token 至环境变量:
flyctl secrets set BOT_TOKEN=\\"{your_bot_token}\\"
fly.toml
文件内容如下:
app = \'lkw123-memos\'\\nprimary_region = \'iad\'\\n \\n[build]\\n image = \'ghcr.io/hu3rror/memos-litestream:stable-memogram\'\\n \\n[env]\\n LITESTREAM_REPLICA_BUCKET = \'{lkw123-memos}\'\\n LITESTREAM_REPLICA_ENDPOINT = \'{s3.us-west-004.backblazeb2.com}\'\\n LITESTREAM_REPLICA_PATH = \'memos_prod.db\'\\n \\n[[mounts]]\\n source = \'memos_data\'\\n destination = \'/var/opt/memos\'\\n \\n[http_service]\\n internal_port = 5230\\n force_https = true\\n auto_stop_machines = false\\n auto_start_machines = true\\n min_machines_running = 0\\n \\n[[vm]]\\n size = \'shared-cpu-1x\'
顺手记录一下自己的 Memos 自定义 CSS 样式:
\\n/* 设置 Memos 标签样式 */\\nspan.inline-block.w-auto.text-blue-600.dark\\\\:text-blue-400 {\\n color: #f3f3f3;\\n background-color: #40b76b;\\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\\n border-radius: 2px;\\n padding: 2px 6px;\\n font-size: 15px;\\n margin-bottom: 4px;\\n}\\n/* 设置同级下不同的标签显示不同的颜色 */\\n/* 第2个标签 */\\nspan.inline-block.w-auto.text-blue-600.dark\\\\:text-blue-400:nth-child(n + 2) {\\n background-color: #157cf5;\\n}\\n/* 第3个标签 */\\nspan.inline-block.w-auto.text-blue-600.dark\\\\:text-blue-400:nth-child(n + 4) {\\n background-color: #f298a6;\\n}\\n/* 第4个标签 */\\nspan.inline-block.w-auto.text-blue-600.dark\\\\:text-blue-400:nth-child(n + 6) {\\n background-color: #fdb15d;\\n}\\n/* 第5个标签 */\\nspan.inline-block.w-auto.text-blue-600.dark\\\\:text-blue-400:nth-child(n + 8) {\\n background-color: #67d6ca;\\n}\\n/* 第6个标签 */\\nspan.inline-block.w-auto.text-blue-600.dark\\\\:text-blue-400:nth-child(n + 10) {\\n background-color: #7445e0;\\n}
Zeabur 是一个比较新的云端部署服务,作为国人团队开发的产品,它会有更多的本地化支持和对大陆用户的优化。
\\n在通常情况下,我们使用如 Zeabur 这类 PaaS 服务时,主要是将项目部署至其多区域的共享集群 (Shared Cluster),利用其 Edge 节点提供的高速网络连接,同时降低运维成本。
\\nZeabur 最近推出了“独立服务器”的功能,用户可以将自己的服务器注册到 Zeabur 上,从而无需支付硬件资源的使用费用,又能享受到平台为部署流程带来的便利。我用 VKVM 的洛杉矶 9950X 4C8G VPS 测试了下,效果还不错,但是占用系统资源比较多,如果有闲置的大内存机器可以尝试。
\\n基于 Astro 构建,修改自 astro-erudite,并使用了 theme-astro-pure 中的部分组件样式。
\\n个人很喜欢博客主题 astro-erudite 的样式,就根据博客 clone 了一份,去除无用的依赖,保留 index.astro
单页面中的 Card 组建,并稍微调整样式,便成了新的个人主页。
基于 Astro 构建,修改自 AstroPaper,部分修改过程记录在了博文 AstroPaper 博客自定义 中,代码存放于我的 GitHub 仓库 synthpop123/astro-blog。自迁移至新的主题后,该旧博客暂时保留。
\\n参考:
\\n\\n\\n因本站已对博客主题完成重构,此博客内容已过时,仅存档。
\\n
根据个人喜好对主题的深色模式中的颜色定义进行了修改,修改 --color-fill
以更改网站的背景颜色,修改 --color-accent
以更改强调色。相关 CSS 定义于 src/styles/base.css
中:
diff --git a/src/styles/base.css b/src/styles/base.css\\nindex 6efa219..7b4de7a 100644\\n--- a/src/styles/base.css\\n+++ b/src/styles/base.css\\n@@ -13,9 +13,9 @@\\n --color-border: 236, 233, 233;\\n }\\n html[data-theme=\\"dark\\"] {\\n- --color-fill: 33, 39, 55;\\n+ --color-fill: 27, 32, 37;\\n --color-text-base: 234, 237, 243;\\n- --color-accent: 255, 107, 1;\\n+ --color-accent: 217, 122, 72;\\n --color-card: 52, 63, 96;\\n --color-card-muted: 138, 51, 2;\\n --color-border: 171, 75, 8;
AstroPaper 通过 Shiki 实现代码高亮,支持多种主题,可以在 astro.config.ts
中进行配置。我选择采用 catppuccino-mocha
主题替换默认的 one-dark-pro
主题。
diff --git a/astro.config.ts b/astro.config.ts\\nindex 52d3437..cc33c96 100644\\n--- a/astro.config.ts\\n+++ b/astro.config.ts\\n@@ -25,14 +25,28 @@ export default defineConfig({\\n rehypePlugins: [rehypeKatex],\\n shikiConfig: {\\n- theme: \\"one-dark-pro\\",\\n+ // https://shiki.style/themes\\n+ theme: \\"catppuccin-mocha\\",\\n+ // 另外,也提供了多种主题\\n+ // https://shiki.style/guide/dual-themes\\n+ // themes: {\\n+ // light: \'github-light\',\\n+ // dark: \'min-dark\',\\n+ // },\\n+ // 添加自定义语言\\n+ // https://shiki.style/languages\\n+ // langs: [],\\n+ // 启用自动换行,以防止水平滚动\\n wrap: true,\\n+ // 添加自定义转换器:https://shiki.style/guide/transformers\\n+ // 查找常用转换器:https://shiki.style/packages/transformers\\n+ // transformers: [],\\n },\\n },\\n vite: {
由于 AstroPaper 主题默认采用的 IBM Plex Mono 字体对于中文支持不佳,我选择将主字体替换为个人更偏好的 Sono,而对于 Sono 字体无法渲染的中文等部分,则由开源中文字体 霞鹜文楷 提供,最后以 monospace 作为 fallback 字体。
\\n在 tailwind.config.cjs
中将 IBM Plex Mono
替换为 Sono
和 LXGW WenKai Screen
:
diff --git a/tailwind.config.cjs b/tailwind.config.cjs\\nindex 8e43860..06dacbc 100644\\n--- a/tailwind.config.cjs\\n+++ b/tailwind.config.cjs\\n@@ -55,6 +55,7 @@ module.exports = {\\n },\\n fontFamily: {\\n- mono: [\\"IBM Plex Mono\\", \\"monospace\\"],\\n+ mono: [\\"Sono\\", \\"LXGW WenKai Screen\\", \\"monospace\\"],\\n },\\n typography: {
接下来需要引入霞鹜文楷字体的 Stylesheet,为了避免阻塞渲染,可以将 media
设置为 print
,在加载完成后再将 media
设置为 all
。同时,采用饿了么提供的 CDN 提高加载速度,相关代码添加到 src/layouts/Layout.astro
中:
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro\\nindex 826c3d2..931baa1 100644\\n--- a/src/layouts/Layout.astro\\n+++ b/src/layouts/Layout.astro\\n@@ -99,6 +94,14 @@ const socialImageURL = new URL(\\n rel=\\"stylesheet\\"\\n />\\n \\n+ <!-- LXGW WenKai Font --\x3e\\n+ <link\\n+ rel=\\"stylesheet\\"\\n+ href=\\"https://npm.elemecdn.com/lxgw-wenkai-screen-webfont/style.css\\"\\n+ media=\\"print\\"\\n+ onload=\\"this.media=\'all\'\\"\\n+ />\\n+\\n <meta name=\\"theme-color\\" content=\\"\\" />
但是通过以上的修改,会存在两个问题:
\\n对于第一个问题,需要在 src/components/Tag.astro
中将字体设定为 font-mono
:
diff --git a/src/components/Tag.astro b/src/components/Tag.astro\\nindex 5a4a376..72f65e0 100644\\n--- a/src/components/Tag.astro\\n+++ b/src/components/Tag.astro\\n@@ -31,6 +31,7 @@ const { tag, size = \\"sm\\" } = Astro.props;\\n <style>\\n a {\\n- @apply relative underline decoration-dashed hover:-top-0.5 hover:text-skin-accent focus-visible:p-1;\\n+ @apply relative underline decoration-dashed hover:-top-0.5 hover:text-skin-accent focus-visible:p-1 font-mono;\\n }\\n a svg {\\n @apply -mr-5 h-6 w-6 scale-95 text-skin-base opacity-80 group-hover:fill-skin-accent;
对于第二个问题,在 src/styles/base.css
中,将 pre > code
的字体设定为 font-mono
即可:
diff --git a/src/styles/base.css b/src/styles/base.css\\nindex 6efa219..7b4de7a 100644\\n--- a/src/styles/base.css\\n+++ b/src/styles/base.css\\n@@ -122,6 +120,33 @@\\n pre:has(code) {\\n- @apply border border-skin-line;\\n+ @apply border border-skin-line font-mono;\\n }\\n }
在默认情况下,所有在 src/config.ts
中 active
字段值为 true 的社交图标都会同时在首页和 Footer 中展示,但是我希望 Footer 中只展示部分社交图标。
首先,在 src/components/
目录下添加 Socials-footer.astro
组件,基本与 Socials.astro
相同,区别在于只展示 footeractive
为 true
的社交图标:
diff --git a/src/components/Socials-footer.astro b/src/components/Socials-footer.astro\\nnew file mode 100644\\nindex 0000000..e90cdcc\\n--- /dev/null\\n+++ b/src/components/Socials-footer.astro\\n@@ -0,0 +1,35 @@\\n+---\\n+import { SOCIALS } from \\"@config\\";\\n+import LinkButton from \\"./LinkButton.astro\\";\\n+import socialIcons from \\"@assets/socialIcons\\";\\n+\\n+export interface Props {\\n+ centered?: boolean;\\n+}\\n+\\n+const { centered = false } = Astro.props;\\n+---\\n+\\n+<div class={`social-icons ${centered ? \\"flex\\" : \\"\\"}`}>\\n+ {\\n+ SOCIALS.filter(social => social.footeractive).map(social => (\\n+ <LinkButton\\n+ href={social.href}\\n+ className=\\"link-button\\"\\n+ title={social.linkTitle}\\n+ >\\n+ <Fragment set:html={socialIcons[social.name]} />\\n+ <span class=\\"sr-only\\">{social.linkTitle}</span>\\n+ </LinkButton>\\n+ ))\\n+ }\\n+</div>\\n+\\n+<style>\\n+ .social-icons {\\n+ @apply flex-wrap justify-center gap-1;\\n+ }\\n+ .link-button {\\n+ @apply p-2 hover:rotate-6 sm:p-1;\\n+ }\\n+</style>
对于 src/config.ts
中定义的 SOCIALS
常量,添加布尔值 footeractive
字段,用于控制是否在 Footer 中展示:
diff --git a/src/config.ts b/src/config.ts\\nindex 35aae63..6e9089a 100644\\n--- a/src/config.ts\\n+++ b/src/config.ts\\n@@ -17,131 +18,151 @@ export const SOCIALS: SocialObjects = [\\n {\\n name: \\"Github\\",\\n href: \\"https://github.com/synthpop123\\",\\n linkTitle: `${SITE.author} on Github`,\\n active: true,\\n+ footeractive: true,\\n },\\n {\\n name: \\"Instagram\\",\\n href: \\"https://www.instagram.com/whoamamiii/\\",\\n linkTitle: `${SITE.author} on Instagram`,\\n active: true,\\n+ footeractive: false,\\n },
为了保证 footeractive
的合法性,需要在 src/types.ts
中添加对应的类型定义:
diff --git a/src/types.ts b/src/types.ts\\nindex 72ba2f0..97ccdca 100644\\n--- a/src/types.ts\\n+++ b/src/types.ts\\n@@ -15,5 +16,6 @@ export type SocialObjects = {\\n name: keyof typeof socialIcons;\\n href: string;\\n active: boolean;\\n+ footeractive: boolean;\\n linkTitle: string;\\n }[];
在博客中添加一个 Misc 页面,用于展示一些额外的内容,如友链、听歌观影记录等。首先在 src/layouts
目录下添加 MiscLayout.astro
:
diff --git a/src/layouts/MiscLayout.astro b/src/layouts/MiscLayout.astro\\nnew file mode 100644\\nindex 0000000..dc7d48f\\n--- /dev/null\\n+++ b/src/layouts/MiscLayout.astro\\n@@ -0,0 +1,58 @@\\n+---\\n+import { SITE } from \\"@config\\";\\n+import Breadcrumbs from \\"@components/Breadcrumbs.astro\\";\\n+import Footer from \\"@components/Footer.astro\\";\\n+import Header from \\"@components/Header.astro\\";\\n+import Layout from \\"./Layout.astro\\";\\n+\\n+export interface Props {\\n+ frontmatter: {\\n+ title: string;\\n+ description?: string;\\n+ };\\n+}\\n+\\n+const { frontmatter } = Astro.props;\\n+---\\n+\\n+<Layout title={`${frontmatter.title} | ${SITE.title}`}>\\n+ <Header activeNav=\\"misc\\" />\\n+ <Breadcrumbs />\\n+ <main id=\\"main-content\\">\\n+ <section id=\\"misc\\" class=\\"prose prose-img:border-0 mb-28 max-w-3xl\\">\\n+ <h1 class=\\"text-2xl tracking-wider sm:text-3xl\\">{frontmatter.title}</h1>\\n+ <slot />\\n+ </section>\\n+ </main>\\n+</Layout>\\n+<Footer />
接下来需要修改 Header.astro
,使 Misc 页面可以正常显示在导航栏中:
diff --git a/src/components/Header.astro b/src/components/Header.astro\\nindex 43a4a71..54264c7 100644\\n--- a/src/components/Header.astro\\n+++ b/src/components/Header.astro\\n@@ -4,7 +4,7 @@ import Hr from \\"./Hr.astro\\";\\n import LinkButton from \\"./LinkButton.astro\\";\\n \\n export interface Props {\\n- activeNav?: \\"posts\\" | \\"tags\\" | \\"about\\" | \\"search\\";\\n+ activeNav?: \\"posts\\" | \\"tags\\" | \\"misc\\" | \\"about\\" | \\"search\\";\\n }\\n \\n const { activeNav } = Astro.props;\\n@@ -65,6 +65,11 @@ const { activeNav } = Astro.props;\\n Tags\\n </a>\\n </li>\\n+ <li>\\n+ <a href=\\"/misc/\\" class={activeNav === \\"misc\\" ? \\"active\\" : \\"\\"}>\\n+ Misc\\n+ </a>\\n+ </li>\\n <li>\\n <a href=\\"/about/\\" class={activeNav === \\"about\\" ? \\"active\\" : \\"\\"}>
详见此前的博客文章 为 AstroPaper 主题添加 KaTeX 支持。
\\n自 2021 年起,我开始采用 Last.fm 记录自己所听的音乐。为了能将其展示在博客中,我借助开源项目 lastfm-recently-played-readme,生成最近听歌记录的图片,可以便携地嵌入到博客 Markdown 文件中。
\\n由于该项目部署于 Vercel,其默认网址 lastfm-recently-played.vercel.app
在中国大陆无法访问,因此我选择自己重新部署,并绑定到我的域名 lastfm.lkwplus.com
上,以避免被墙。
需要注意的是,官方的部署教程见 GitHub,需要设置两个环境变量,分别是 Last.fm 账户的 API_KEY
及由 Vercel 自动进行分配的 VERCEL_URL
。
但是我在部署过程中遇到了无法显示专辑和头像图片的问题,我的解决方式是将项目内的 VERCEL_URL
替换为任意其他名称,如 BASE_URL
,从而可以手动将其设置为项目的自定义域名,以规避 Vercel 的自动分配。
- const BaseUrl = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : \'http://localhost:3000\';\\n+ const BaseUrl = process.env.BASE_URL ? `https://${process.env.BASE_URL}` : \'http://localhost:3000\';
由于国内的最好用的记录观影记录的平台豆瓣存在各种限制,也并不提供标准可访问的 API,我选择采用相对小众一点的 Letterboxd 平台来记录自己所看的电影。
\\n同样,我借助开源项目 letterboxd-embed-landing-page,将最近的观影记录(包含影评)嵌入到博客中,具体效果可以在博客的 Misc 页面查看。
\\n在项目提供的 前端 中输入自己的 Letterboxd 用户名,点击 Generate
按钮,生成嵌入代码,如下图所示:
个人通过 Cloudflare Workers 自部署了 timciep/letterboxd-diary-embed,也就是该项目的后端,用于处理相关请求,并简单的修改了一些自定义样式,可以通过 https://letterboxd-embed.lkwplus.com?username={username} 的形式访问。
\\n将网站中生成的 snippet 添加到需要放置的页面的 Layout.astro
中,以 Misc 为例,其中有两点需要注意:
将 letterboxd-embed-wrapper-tc
放在 id 为 misc
的 section
中,以继承原有的样式;
为了适配网站的深色模式,添加了一些自定义的 CSS 样式,注入到插入的 document 的 style 部分。
\\ndiff --git a/src/layouts/MiscLayout.astro b/src/layouts/MiscLayout.astro\\nindex c2fb104..dc7d48f 100644\\n--- a/src/layouts/MiscLayout.astro\\n+++ b/src/layouts/MiscLayout.astro\\n@@ -21,8 +21,38 @@ const { frontmatter } = Astro.props;\\n <main id=\\"main-content\\">\\n <section id=\\"misc\\" class=\\"prose prose-img:border-0 mb-28 max-w-3xl\\">\\n <h1 class=\\"text-2xl tracking-wider sm:text-3xl\\">{frontmatter.title}</h1>\\n- <slot />\\n+ <slot /><br />\\n+ <div id=\\"letterboxd-embed-wrapper-tc\\">Loading...</div>\\n </section>\\n </main>\\n- <Footer />\\n </Layout>\\n+<script>\\n+ fetch(\\"https://letterboxd-embed.lkwplus.com?username=lkw123\\")\\n+ .then(response => response.text())\\n+ .then(data => {\\n+ const element = document.getElementById(\\"letterboxd-embed-wrapper-tc\\");\\n+ if (element) {\\n+ element.innerHTML = data;\\n+ }\\n+\\n+ const style = document.createElement(\\"style\\");\\n+ style.innerHTML = `\\n+ html[data-theme=\\"dark\\"] .letterboxd-embed-tc-title {\\n+ color: lightgray !important;\\n+ }\\n+ html[data-theme=\\"dark\\"] .letterboxd-embed-tc-date {\\n+ color: gray !important;\\n+ }\\n+ html[data-theme=\\"dark\\"] .letterboxd-embed-tc-review {\\n+ color: lightgray !important;\\n+ }\\n+ `;\\n+ if (element) {\\n+ element.appendChild(style);\\n+ }\\n+ });\\n+</script>\\n+<Footer />
为了展示自己的编程活动统计,我采用了 WakaTime 平台,为常用的 IDE/编辑器(如 VS Code、PyCharm、NeoVim、Vim 等)安装了对应插件,通过其提供的 可嵌入图标 的功能,展示在博客中:
\\n<figure><embed src=\\"https://wakatime.com/share/@lkw123/XXX.svg\\"></embed></figure>
自从我将博客和一些其他网站从 Vercel 迁移至 Zeabur 以来(现在又迁到了 Cloudflare),我一直希望可以在网站的 Footer 中添加一个 Zeabur 的 Logo,以便让让更多的人了解到这个优秀的服务。
在 AstroPaper 主题中,Footer 的内容是通过 src/layouts/Layout.astro
中的 Footer
组件定义的,因此我可以在这里添加展示 Zeabur
的 Logo。
为了使得这个 Logo 同时适配网站的浅色模式和深色模式,我采用了一个非常 Naive 的方式:同时引入 id 为 zeabur-light
的浅色 Logo 和 id 为 zeabur-dark
的深色 Logo,然后通过 CSS 控制其显示与隐藏,虽然不太优雅,但是至少可以正常工作了。
diff --git a/src/components/Footer.astro b/src/components/Footer.astro\\nindex 31f452e..dd8fe14 100644\\n--- a/src/components/Footer.astro\\n+++ b/src/components/Footer.astro\\n@@ -1,6 +1,7 @@\\n ---\\n+import { SITE } from \\"@config\\";\\n import Hr from \\"./Hr.astro\\";\\n@@ -18,7 +19,26 @@ const { noMarginTop = false } = Astro.props;\\n <div class=\\"copyright-wrapper\\">\\n <span>© 2021 - {currentYear}</span>\\n <span class=\\"separator\\"> | </span>\\n+ {\\n+ SITE.lightAndDarkMode && (\\n+ <>\\n+ <a href=\\"https://zeabur.com?referralCode=synthpop123\\">\\n+ <img\\n+ src=\\"https://zeabur.com/deployed-on-zeabur-light.svg\\"\\n+ alt=\\"Deployed on Zeabur\\"\\n+ id=\\"zeabur-light\\"\\n+ />\\n+ </a>\\n+ <a href=\\"https://zeabur.com?referralCode=synthpop123\\">\\n+ <img\\n+ src=\\"https://zeabur.com/deployed-on-zeabur-dark.svg\\"\\n+ alt=\\"Deployed on Zeabur\\"\\n+ id=\\"zeabur-dark\\"\\n+ />\\n+ </a>\\n+ </>\\n+ )\\n+ }\\n </div>\\n </div>\\n </footer>
相关 CSS 定义如下:
\\ndiff --git a/src/styles/base.css b/src/styles/base.css\\nindex 6efa219..7b4de7a 100644\\n--- a/src/styles/base.css\\n+++ b/src/styles/base.css\\n@@ -130,6 +155,27 @@\\n+\\n+ html[data-theme=\\"dark\\"] #zeabur-light {\\n+ display: none;\\n+ }\\n+\\n+ html[data-theme=\\"light\\"] #zeabur-dark {\\n+ display: none;\\n }\\n }
from argparse import ArgumentParser\\n \\ndef parse_arguments():\\n # 以 Add Port Group 为例\\n parser = ArgumentParser(description=\\"Add port group to a cluster in vCenter\\")\\n \\n parser.add_argument(\\"--vc-ip\\", dest=\\"vc_ip\\", type=str, required=True, help=\\"vCenter IP address\\")\\n parser.add_argument(\\"-u\\", \\"--username\\", dest=\\"username\\", type=str, default=\\"vc_username\\", help=\\"vCenter username\\")\\n parser.add_argument(\\"-p\\", \\"--password\\", dest=\\"password\\", type=str, default=\\"vc_password\\", help=\\"vCenter password\\")\\n parser.add_argument(\\"-c\\", \\"--cluster\\", dest=\\"cluster\\", type=str, required=True, help=\\"Cluster name\\")\\n parser.add_argument(\\"-n\\", \\"--vlan-name\\", dest=\\"vlanname\\", type=str, required=True, help=\\"Name of the VLAN\\")\\n parser.add_argument(\\"-i\\", \\"--vlan-id\\", dest=\\"vlanid\\", type=int, required=True, help=\\"VLAN ID\\")\\n parser.add_argument(\\"-v\\", \\"--vswitch\\", dest=\\"vswitch\\", type=str, required=True, help=\\"Name of the vSwitch\\")\\n \\n return parser.parse_args()
def init_connection(vc_ip, username, password):\\n \\"\\"\\"\\n 初始化与 vCenter 的连接\\n \\n :param vc_ip: vCenter 的 IP 地址\\n :param username: 登录用户名\\n :param password: 登录密码\\n :return: vCenter 的内容对象\\n \\"\\"\\"\\n service_instance = connect.Connect(\\n host=vc_ip, port=443, user=username, pwd=password, disableSslCertValidation=True\\n )\\n atexit.register(connect.Disconnect, service_instance)\\n content = service_instance.RetrieveContent()\\n return content
def get_obj(content, vimtype, name=None):\\n \\"\\"\\"\\n 从 vc 的内容中获取指定类型的对象列表\\n \\n :param content: vCenter 内容对象\\n :param vimtype: 要获取的对象类型\\n :param name: 对象名称,可选\\n :return: 匹配的对象列表\\n \\"\\"\\"\\n container = content.viewManager.CreateContainerView(\\n content.rootFolder, vimtype, True\\n )\\n \\n if name is not None:\\n objects = [view for view in container.view if name and view.name == name]\\n else:\\n objects = [view for view in container.view]\\n \\n return objects
# 连接到 VC 获取相关信息\\nvc_content = init_connection(vc_ip, username, password)\\n \\n# 一些常用对象的获取\\nhost_obj = get_obj(vc_content, [vim.HostSystem])\\nvm_obj = get_obj(vc_content, [vim.VirtualMachine])\\ncluster_obj = get_obj(vc_content, [vim.ClusterComputeResource])\\nds_obj = get_obj(vc_content, [vim.Datastore])
def add_disk(vm, disk_size, ds):\\n \\"\\"\\"\\n 虚拟机加盘\\n \\n :param vm: 虚拟机对象\\n :param disk_size: 加盘的大小\\n :param ds: 加盘所属的 Datastore\\n :return: None\\n \\"\\"\\"\\n # 默认磁盘类型为 thick\\n disk_type =\'thick\'\\n ds_name = ds.summary.name\\n path_on_ds = \'[\' + ds_name + \']\' + vm.name\\n \\n # get all disks on a VM, set unit_number to the next available\\n for dev in vm.config.hardware.device:\\n if hasattr(dev.backing, \'fileName\'):\\n unit_number = int(dev.unitNumber) + 1\\n # unit_number 7 reserved for scsi controller\\n if unit_number == 7:\\n unit_number += 1\\n if unit_number >= 16:\\n logging.error(\\"We don\'t support this many disks\\")\\n return\\n if isinstance(dev, vim.vm.device.VirtualSCSIController):\\n controller = dev\\n \\n spec = vim.vm.ConfigSpec()\\n device_change = []\\n disk_spec = vim.vm.device.VirtualDeviceSpec()\\n disk_spec.fileOperation = \\"create\\"\\n disk_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add\\n disk_spec.device = vim.vm.device.VirtualDisk()\\n disk_spec.device.backing = \\\\\\n vim.vm.device.VirtualDisk.FlatVer2BackingInfo()\\n if disk_type == \'thin\':\\n disk_spec.device.backing.thinProvisioned = True\\n disk_spec.device.backing.diskMode = \\"persistent\\"\\n disk_spec.device.backing.datastore = ds\\n disk_spec.device.backing.fileName = path_on_ds + \'/\' + vm.name + \'_\' + str(unit_number) + \'.vmdk\'\\n disk_spec.device.unitNumber = unit_number\\n disk_spec.device.capacityInKB = int(disk_size) * 1024 * 1024 # GB to KB\\n disk_spec.device.controllerKey = controller.key\\n \\n device_change.append(disk_spec)\\n spec.deviceChange = device_change\\n vm.ReconfigVM_Task(spec=spec)\\n logging.info(f\\"虚拟机 {vm.name} 已加盘,加盘大小为 {disk_size} GB\\")\\n logging.info(f\\"所属 Datastore 为 {ds.name}\\")
def add_disk_to_vm(vmname, vmip, size):\\n \\"\\"\\"\\n 单台虚拟机加盘\\n \\n :param vmname: 虚拟机名称\\n :param vmip: 虚拟机 IP\\n :param size: 加盘大小\\n :return: None\\n \\"\\"\\"\\n logging.info(f\\"确认执行参数:vmname:{vmname} vmip:{vmip} size:{size}\\")\\n \\n # 从数据库表中获取对应虚机的相关信息 (vc_ip, from_cluster)\\n vc, cluster = get_vminfo(vmname, vmip)\\n username = \\"vc_username\\"\\n password = \\"vc_password\\"\\n \\n # 连接到所属 VC\\n vc_content = init_connection(vc_ip=vc, username=username, password=password)\\n logging.info(f\\"********** 已连接至 VC {vc} **********\\")\\n \\n # 找到所属 Cluster 的全部 Datastore\\n cluster_obj = get_obj(vc_content, [vim.ClusterComputeResource], cluster)\\n if cluster_obj is None:\\n logging.error(f\\"未找到目标 Cluster: {cluster}\\")\\n return\\n datastore_obj = cluster_obj[0].datastore\\n \\n # 筛选出剩余容量最大的 Datastore\\n max_freespace_ds = None\\n max_freespace = 0\\n for ds_obj in datastore_obj:\\n # 排除本地 Datastore\\n if ds_obj.summary.multipleHostAccess is True:\\n freespace = 0\\n \\n freespace += ds_obj.summary.freeSpace\\n freespace -= ds_obj.summary.uncommitted if ds_obj.summary.uncommitted is not None else 0\\n # 更新最大剩余容量的 Datastore 对象\\n if freespace > max_freespace:\\n max_freespace_ds = ds_obj\\n max_freespace = freespace\\n \\n logging.info(f\\"已找到剩余容量最大的 Datastore: {max_freespace_ds.summary.name}\\")\\n logging.info(f\\"剩余容量为 {max_freespace / (1024**3):.1f} GB,总容量为 {max_freespace_ds.summary.capacity / (1024**3)} GB\\")\\n \\n # 判断执行加盘操作后 Datastore 使用率是否超过 80%\\n if (((freespace / (1024**3)) - int(size)) / (max_freespace_ds.summary.capacity / (1024**3))) * 100 < 20:\\n logging.error(\\"加盘失败:加盘后 Datastore 使用率超过 80%\\")\\n return\\n \\n # 执行加盘操作\\n vm_obj = get_obj(vc_content, [vim.VirtualMachine], vmname)\\n if vm_obj is None:\\n logging.error(f\\"未找到虚拟机 {vmname}\\")\\n return\\n add_disk(vm_obj[0], size, max_freespace_ds)
# 连接到 VC 获取相关信息\\nvc_content = init_connection(vc_ip, username, password)\\n \\n# 获取该 VC 中对应的 cluster 对象\\ncluster_obj = get_obj(vc_content, [vim.ClusterComputeResource], cluster)\\nif cluster_obj is None:\\n logging.error(f\\"未找到目标 Cluster: {cluster}\\")\\n return\\nlogging.info(f\\"已连接至 VC: {vc_ip},并获取到 Cluster 对象\\")\\n \\n# 获取该 Cluster 中的所有宿主机\\nhost_list = cluster_obj[0].host\\nfor host in host_list:\\n logging.info(f\\"宿主机 {host.name} 开始配置\\")\\n \\n portgroup_spec = vim.host.PortGroup.Specification()\\n portgroup_spec.vswitchName = vswitch.strip()\\n portgroup_spec.name = vlanname.strip()\\n portgroup_spec.vlanId = vlanid\\n \\n network_policy = vim.host.NetworkPolicy()\\n network_policy.security = vim.host.NetworkPolicy.SecurityPolicy()\\n # network_policy.security.allowPromiscuous = False\\n # network_policy.security.macChanges = True\\n # network_policy.security.forgedTransmits = True\\n portgroup_spec.policy = network_policy\\n \\n host.configManager.networkSystem.AddPortGroup(portgroup_spec)\\n \\n logging.info(f\\"宿主机 {host.name} 配置完成\\")
# 连接到 VC 获取相关信息\\nvc_content = init_connection(vc_ip, username, password)\\n \\n# 获取该 VC 中所有的 host 对象\\nhost_obj = get_obj(vc_content, [vim.HostSystem], host)\\nif host_obj is None:\\n logging.error(f\\"未找到目标宿主机 {host}\\")\\n return\\nlogging.info(f\\"已连接至 VC: {vc_ip},并获取到 host 对象\\")\\n \\nobj = host_obj[0]\\nvswitch_spec = vim.host.VirtualSwitch.Specification()\\n \\nvswitch_spec.numPorts = 1024\\nvswitch_spec.mtu = 1450\\n \\nnetwork_policy = vim.host.NetworkPolicy()\\nnetwork_policy.security = vim.host.NetworkPolicy.SecurityPolicy()\\n# network_policy.security.allowPromiscuous = False\\n# network_policy.security.macChanges = True\\n# network_policy.security.forgedTransmits = True\\nvswitch_spec.policy = network_policy\\n \\nobj.configManager.networkSystem.AddVirtualSwitch(vswitch, vswitch_spec)\\nlogging.info(f\\"宿主机 {host} 添加 vSwitch {vswitch} 完成\\")