配置

把插件选项和可选的 Vite 设置放进 sitelo.config.js。不需要 vite.config.js

export default {
  site: 'https://example.com',
  rss: {
    site: 'https://example.com',
    title: '我的博客',
    description: '最新文章',
    routePrefix: '/blog',
  },
  vite: {
    publicDir: 'static',
    build: {
      emptyOutDir: true,
      outDir: 'public',
    },
    server: {
      port: 8888,
    },
  },
}

插件选项

开发工具栏

sitelo(dev)运行期间,每个页面底部都有一条小工具栏,显示页面文件、参数,以及页面上有多少个服务端区块。用视口按钮在 Desktop / Tablet / Mobile 预览之间切换(通过 iframe,因此媒体查询能正确匹配),Copy 则会给出一段便于提 issue 的调试信息。它绝不会出现在 sitelo build 的产物里。

// sitelo.config.js
export default {
  devToolbar: false, // 为本项目的所有人隐藏
}

Vite 选项

vite 之下的一切都会合并进 Vite 的配置。命令行选项(例如 --port)优先级高于两者。

已有 vite.config.js 的情况

依然支持 —— 要么只写 Vite 选项,要么完全自行掌控插件:

// 仅 Vite 选项;sitelo 仍会注入插件
export default {
  publicDir: 'static',
  server: { port: 8888 },
}
// 自行注册插件
import htmlPages from 'sitelo'

export default {
  plugins: [htmlPages({
    site: 'https://example.com',
  })],
}

如果插件已经在你的 Vite 配置里,插件选项就写在那里 —— 不要同时又在 sitelo.config.js 里作为插件选项写一遍(sitelo 会报错)。

Sitemap 与 RSS

设置 site 即可产出 dist/sitemap.xml

RSS:

export default {
  rss: {
    site: 'https://example.com',
    title: '我的博客',
    description: '最新文章',
    routePrefix: '/blog',
  },
}

会生成 dist/rss.xml,为 routePrefix 下的每个页面各生成一条。

链接检查

失效的 <script src> 和样式表 href 已经会让构建失败(见 missingAssets)。linkCheck 负责另一半:指向不存在页面的内部 <a href> 链接。

export default {
  linkCheck: true,   // 'warn'(默认)、'error',或一个选项对象
}

sitelo build 之后,产物中的每个内部链接都会被解析,凡是背后没有页面的都会被报告,并按其所在页面分组:

[sitelo] 3 个失效的内部链接

  index.html
    ../escape           -> 超出了输出目录
    /abuot              -> 没有这个页面
    /blog/missing-post  -> 没有这个页面

链接如何解析

检查是针对产出的站点进行的,而不是路由表 —— 所以它会考虑 cleanUrls、路由分组、mapOutputPath、从 public/ 复制过来的文件,以及动态路由生成的页面。它还会在图片优化和 Pagefind 之后运行,因此看到的正是最终发布的内容。当有真实文件能响应某个链接时它才算有效,尝试顺序与静态托管一致:

相对链接(../about)相对于所在页面解析,跑出输出目录的会被报告。解析时会忽略查询串 —— /about?utm=x 检查的是 /about

外部链接永远不会被请求。https://、协议相对的 //cdn.example.commailto:tel: 以及其他协议都会被完全跳过。

选项

选项默认值说明
mode'warn''warn' 记录日志并继续;'error' 让构建失败 —— 在 CI 中很有用
exclude[]要跳过的 href 的 glob 或正则
checkFragmentsfalse同时校验被链接页面中是否存在 #fragment 目标
export default {
  linkCheck: {
    mode: 'error',                    // 遇到死链时让构建失败
    checkFragments: true,             // 同时校验 #fragment 目标
    exclude: ['/api/**', /^\/legacy\//],
  },
}

checkFragments 默认关闭,因为由客户端 JavaScript 添加的 id 并不在构建出的 HTML 里,会被误报为缺失。id 和早期的锚点 name 属性都算作 fragment 目标。

部署在 base 之下的站点

如果你的站点部署在某个 base 之下(比如 GitHub Pages 的项目站点),任何没有带上该 base 的根相对链接都会被报告。部署在 /repo/ 的站点上写 /about 会把浏览器带到主机根目录,而不是你的站点里 —— 若仍按产物去解析它,反而会掩盖掉最值得发现的错误。如果这是有意为之,请使用 exclude

Lighthouse 审计

sitelo lighthouseLighthouse 审计最终构建。它是可选的 peer 依赖:

npm install -D lighthouse

sitelo 会像 sitelo preview 那样提供 dist/,让无头 Chrome 逐页打开并打印分数:

[sitelo] lighthouse mobile - 3 pages

  page           perf  a11y  best   seo
  /                98   100   100   100
  /docs            95   100   100   100
  /docs/routing    97   100   100   100

[sitelo] lighthouse audited 3 pages in 31.4s

每个页面都按站点实际链接的 URL 审计 —— /docs,而不是 dist/docs.html,所以 cleanUrls、路由组和 base 都已覆盖。设置阈值后,报告就变成检查:任何低于阈值的分数都会让命令失败。

export default {
  lighthouse: {
    exclude: ['404.html'],   // 404 页面通常不需要审计
    thresholds: {
      performance: 90,
      accessibility: 100,
      'best-practices': 95,
      seo: 100,
    },
  },
}

分数按 Lighthouse 显示的方式书写(0100);它自己的 01 小数同样可用。用 mode: 'warn' 只记录、不失败。

export default {
  lighthouse: {
    formFactor: 'desktop',   // Lighthouse 的桌面预设
    runs: 3,                 // 每页运行三次,取中位数
    output: true,            // 完整报告写入 .sitelo/lighthouse/
    onBuild: true,           // 在 sitelo build 结束时也审计一次
    flags: { throttlingMethod: 'provided' },
  },
}

Lighthouse 需要真实的 Chrome:在运行审计的机器上装一个,或用 CHROME_PATH 指向可执行文件。性能分数在多次运行之间会浮动,给它设阈值前先用 runs: 3

Pagefind 搜索

Pagefind 提供的可选静态搜索,它是一个可选的 peer 依赖。需要搜索时先安装它,然后开启索引、标记内容、挂载界面,再运行 sitelo build

npm install -D pagefind

1. 开启索引

设置 pagefind: truesitelo build 之后你会得到 dist/pagefind/,并且默认还会在 public/pagefind/ 留一份副本,好让下一次 sitelo(dev)或 sitelo preview 无需重新构建就能提供 /pagefind/

export default {
  pagefind: true,
}

2. 标记内容并加一个挂载点

在主内容上加 data-pagefind-body,这样导航和页脚就不会被索引。再留一个空元素给搜索界面:

import {
  html, head, title, link, script, body, header, a, div, main, h1, p,
} from 'javascript-to-html'

export default () =>
  html({ lang: 'zh' },
    head(
      title('我的网站'),
      link({ rel: 'stylesheet', href: '/styles.css' }),
      script({ type: 'module', src: '/main.js' }),
    ),
    body(
      header(
        a({ href: '/' }, '首页'),
        div({ id: 'search' }),
      ),
      main({ 'data-pagefind-body': '' },
        h1('你好'),
        p('只有这个区域会被索引。'),
      ),
    ),
  )

3. 挂载 Pagefind 界面

在客户端脚本里加载 /pagefind/pagefind-ui.jspagefind-ui.css(只在构建产生索引之后):

async function initSearch() {
  const mount = document.querySelector('#search')
  if (!mount) return

  // 索引只在 `sitelo build` 之后存在(默认同步到 public/pagefind)
  try {
    const probe = await fetch('/pagefind/pagefind-ui.js', { method: 'HEAD' })
    if (!probe.ok) return
  } catch {
    return
  }

  const style = document.createElement('link')
  style.rel = 'stylesheet'
  style.href = '/pagefind/pagefind-ui.css'
  document.head.appendChild(style)

  await new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.src = '/pagefind/pagefind-ui.js'
    script.onload = resolve
    script.onerror = reject
    document.body.appendChild(script)
  })

  new window.PagefindUI({
    element: '#search',
    showImages: false,
  })
}

initSearch()

4. 构建,并忽略同步过去的那份副本

sitelo build
# 然后:sitelo preview — 或使用 public/pagefind 的 sitelo (dev)
public/pagefind/

进阶选项(syncPublicglob、语言、选择器,…)写在一个对象里:pagefind: { syncPublic: false, glob: '**/*.html' }。完整的界面选项见 pagefind.app

404

创建 src/404.ht.js 即可得到 dist/404.html。否则会生成一个简洁的默认页面。