Astro 建站小记

2023 年 9 月 16 日

简介

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

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

博客使用 astro 的内容集合和 markdownfrontmatter 字段管理文章, 通过 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

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

viewTransitions

客户端导航

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

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

prefetch

预获取 | Docs

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

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

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

暗黑模式

视图过渡动画 | Docs

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

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

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

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

动画

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

脚本与事件处理

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

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

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

内联脚本

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

假如我们有一个脚本用于给 posttoc 添加 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">

递归渲染

Astromarkdown 可以通过 frontmatterlayout 字段指定 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 引入是不会重新触发请求的