Astro 建站小记

简介

这是一个使用 obsidian 作为 backend 的个人博客, 虽然这么说, 其实并没有为 obsidian 的 api 做出单独的渲染优化, 所以很多插件大概率是不兼容的

因为我对 obsidian 的用途只是一个单纯的 markdown 编辑器, 如果您有上述需求, 可能官方的托管服务更适合您

博客使用 astro 的内容集合和 markdown 的 frontmatter 字段管理文章, 通过 obsidian 的模板快速插入相关信息

模板大概类似这样

---
pubDate:
  - "{{date}}"
tags: []
featured: false
draft: true
---

Obsidian 配置

设置-选项-文件与链接

  • 始终更新内部链接
    • 开启
  • 新建笔记的存放位置
    • 选择指定的附件文件夹,路径参考 /todo
  • 内部链接类型
    • 选择 基于当前笔记的相对路径
  • 使用 Wiki 链接
    • 关闭
  • 附件默认存放路径
    • 选择 指定的附件文件夹,路径参考 /assets

第三方插件配置

  • Paste image rename

    从外部复制图片到 Obsidian 中,文件名中默认包含空格,而 Astro 暂不支持在 Markdown 中导入的文件名中包含空格或者转义字符的图片,所以需要此插件自动修改文件名

    • 配置 Pattern 第一个选项,配置参考 {{DATE:x}}
    • 开启 Auto rename

样式

astro 内置了组件级别的 style, 但是使用 markdown.render() 返回的内容是没有样式的, 因为它不属于 astro 组件的一部分, 你必须创建一个全局的类, 然后在上面修改样式, 这一点的工作原理和 tailwind 类似

一个好的想法可能是单独使用一个组件用于渲染 markdown

markdown

使用 tailwind 的 prose 插件可能不是一个好主意, 预置的样式有点多了, 不方便修改, 建议在 global.css 中写一个自用的类

viewTransitions

客户端导航

使用 viewTransitions 会开启客户端导航模式

在客户端导航中, 我们使用 astro:page-load 代替 DOMcontentLoad 事件

prefetch

预获取 | Docs

astro 的 prefetch 在客户端导航期间默认是 prefetchALl, 默认行为仍然是 hover

要注意的是, 如果你的网站响应速度不够快, 最好不要修改默认策略为 load 或者 viewport, 如果访客网速很低, 这些行为最终会 fallback 到 tap 而不是 hover, 这可能会导致更糟糕的体验

特别是在客户端导航期间, 浏览器在点击后没有反馈, 又没有设置 before-prepration 的动画的时候, 界面会出现假死

暗黑模式

视图过渡动画 | Docs

暗黑模式应该在客户端导航生命周期的 before-swap 周期被添加, 以避免页面背景闪烁或者重置

页面刷新的时候仍然会白屏, 因为此时 dark 类还没有被添加到 <head> 上, 建议在 <viewTransition/> 之前添加一个内联脚本, 用于从 localstorge 中读取 theme 变量并决定是否添加 dark

注意, 这和 before-swap 并不冲突

一个是负责页面刷新后, 重新获取 document, 此时的 document 的 head 是没有 dark 的, 所以会白屏, 一个是负责客户端导航期间添加 dark 类

动画

viewTransitions 的退出动画效果目前和部分浏览器 (chromium 系) 的兼容并不太好, 建议只使用进入动画, 退出动画设置为 0s, 替代方案可以是手动添加动画类

脚本与事件处理

以下讨论均在客户端导航的前提下

脚本应该在合适的时机被执行, 以免阻塞渲染, 也就是说, 绝大多数脚本都应该在 astro:page-load 事件中完成

只有少数, 例如添加暗黑模式需要在 before-swap 事件中完成

内联脚本

在绝大多数情况下, 我们不需要内联脚本

假如我们有一个脚本用于给 post 的 toc 添加 click 事件用于显示或者隐藏目录,

第一个想法可能是在 toc 组件中写一个内联脚本, 其实也可以写一个条件判断当前路径是否在 post 的目录下, 再添加相应的事件

<script is:inline>
  // 将会被直接插入 HTML,不会有任何变化!
  // 本地导入并不会被解析,也不会生效。
  // 如果组件被多次使用,脚本会出现多次。
</script>

托管

Cloudflare

之前是在 netlify 上托管的, 最近我的网站都迁移到 cloudflare 上了, 其实对于静态博客网站来说, netlify 还是很慷慨的

同样要配置缓存标头, cloudflare 还会给每个 pages 分配一个 dev 的域名, 也可以排除在搜索引擎之外, 要放到 /public/_headers 里

https://wunhao-com.pages.dev/*
  X-Robots-Tag: noindex
https://wunhao.com/*
  Cache-Control: max-age=86400
缓存

Netlify , Vercel 等平台为了更灵活/广泛的应用, 默认的 HTTP 标头使用的是 Cache-control: max-age=0 must-revalite , 这意味着尽管缓存会被存放在本地, 但是会立即过期, 在下次导航时, 必须先向服务器发出请求 if-none-match, 在接受到 304 之后, 才可以重用缓存, 我们可以配置自定义标头采用更激进的缓存策略

在项目的根目录创建一个 netlify.toml ,并设置过期时间为一天或者更久


[[headers]]

for = "/*"

[headers.values]

cache-control = "max-age=86400"
重定向

Redirect options | Netlify Docs

启用漂亮 URL 后,Netlify 会将 /about 等路径转发到 /about/ (静态网站和单页应用程序中的常见做法),并将 /about.html 等路径重写为 /about/ 。

netlify 默认会开启 pretty url , 例如, 当你访问 /about 的时候, 会被重定向到 /about/ , 这样的重定向会在导航的时候带来一次额外的往返, 如果你距离代理服务器过远 (>500ms), 还是会影响体验的

astro 在构建的时候也会把 /about.astro 构建为 /about/index.html

在导航链接的时候, 你应该填写 /about/ 而不是 /about, 总的来说, 只要你的 astro 文件在构建后会生成一个同名目录以及 index.html, 你就应该在末尾加上 /, 或者说 tailing splash ,这样就可以避免重定向

注意: 这里只是个细枝末节的优化, 你很难把控到请求响应的每一部分, 例如, 本站为了兼容 obsidian , post 的链接选择了以 md 结尾, 就意味着一定会发生重定向

post 目录

视图过渡动画 | Docs

post 目录中的内容可能会被多次点击, 理想的做法是不应该将同一页面点击的链接也加入历史记录, 否则用户在访问的时候可能需要返回很多次才能回 上一页面

tags 页面上的组件也同理, 最好也添加上

<a href="/main" data-astro-history="replace">

递归渲染

在 Astro 中 markdown 可以通过 frontmatter 的 layout 字段指定 layout, 并单独作为页面, layout 可以在 astro. props 中接收到 heading 数组

我的 markdown 文件是通过 obsidain 管理的, 所以选择了使用内容集合的方式

无论如何, 首先你要获取到 heading 数组

总体思路就是, 将 1 维的 heading 数组, 转换为嵌套数组, 然后利用 astro.self 递归渲染组件, 因此我们需要两个组件

组件

  • 第一个组件是入口, 用来处理数据
---
// github.com/rezahedi/rezahedi.dev/blob/main/src/components/TOC.astro
import NestedList from "./NestedList.astro";
const { headings } = Astro.props;

function getNested(data) {
  let arr = [];
  for (let i = 0; i < data.length; i++) {
    const nextIndex = data
      .slice(i + 1)
      .findIndex((item) => item.depth <= data[i].depth);

    if (nextIndex > -1) {
      data[i].children = getNested(data.slice(i + 1, i + nextIndex + 1));

      arr.push(data[i]);
      i += nextIndex;
    } else {
      // 如果没有找到满足条件的索引,则将剩余的元素作为当前元素的 children 属性
      data[i].children = getNested(data.slice(i + 1))
      // 推送当前元素到结果数组中
      arr.push(data[i]);
      // 结束循环
      i = data.length;
    }
  }
  return arr;
}
const nestedArr = getNested(headings);
---

<div>
  <NestedList items={nestedArr} />
</div>

  • 第二个组件用来递归渲染
---
const { items } = Astro.props;
---
<ul>
  {items.map((item) => (
    item.depth < 6 && (
      <li >
        <a href={`#${item.slug}`} >
         {item.text}
        </a>
        {Array.isArray(item.children) && item.children.length > 0 ? (
          <Astro.self items={item.children} />
        ) : null}
      </li>
    )
  ))}
</ul>

网站分析

本站使用自托管的 Umami 进行网站分析

注意, netlify 的 nextjs rumtime 目前 (2024-04-23) 有问题, /setting 页面打不开, 可以在本地跑, 然后拿到 data-id, 修改一下域名就可以.

netlify 默认生成的 build 命令是错误的, 不是 yarn run build-app ,应该修改为 yarn build

收集数据

对于 astro 项目, 在 baselayout 的 </body> 结束之前插入一个 inline 内联脚本就可以保证脚本在每一页都运行, 记得要加上 defer

没有选择在 <head> 引入主要是考虑用户体验, 以及, 如果是客户端导航, 在 head 引入是不会重新触发请求的