site-icon

星河絮语

浩瀚中的伟大,孤独间的渺小


Hello, Astro, 我带来了新鲜出炉的Astro-Blog-Plus

对于我目前想要建设的博客站来说,Astro的特性与相关功能支持是更加合适的选择。
发布时间:
最后一次更新于:

前言

大约两个月前,我开始思考除了xlog之外,自己是不是有必要再创建一个多语言的个人博客,用来发一发比较长的,三言两语讲不完一件事情的文章。虽然我很早之前就将自己的许多博文迁移到了xLog上,但是我总觉得自己需要有一个站点能和xLog形成主备,避免发生其中一个站点无法访问时,自己看不到博客文章的情况。

于是我开始思考自己的需求,最早搭建博客的时候我用过WordPress和Typecho,这两个博客系统一般来讲需要搭建在VPS或者虚拟主机上,服务器的费用就占了很多。后来即使使用serverless服务,由于数据库需要保持“心跳连接”,其实每月的账单费用对我来说也是比较高的。后来接触到Gridea,我才意识到我并不需要CMS,我只需要一个能将本地文章转换成静态网页的“博客生成器”,这样我在本地更新文章,在需要发布的时候推送到例如Github Pages这样的静态页面上就可以了。后来由于Gridea能自定义的部分太少了,我就将博客又迁到了Hexo上,用上了Kratos : Rebirth主题,说来也巧,我在看主题作者的博客时发现了xLog,于是也将博客迁了上去。

不过现在自己准备将博客面向更多的人,并且我希望自己尝试设计博客的样式,同时我还希望保持以前本地书写markdown的方式来写博客文章。于是我总结出来了自己的几个需求:

  • 能够将markdown转为静态网页
  • 能够支持多语言国际化
  • 能够完全自己设计博客主题样式
  • 能够接入自己曾经用过的各种插件(例如Waline评论等)
  • 能够适配移动端
  • 能切换日间模式和夜间模式

其中第一点是最重要最核心的需求,我需要这个博客框架能像Hexo一样将markdown文件的内容转换成对应的html页面。第二点、第四点和第五点是重要的特性,因为我打算像我的YouTube频道一样,将我的博客面向众多语言类型的用户,而选择沿用waline则是因为waline的国际化做得还不错,以及用waline可以方便自己把控一下评论内容的大方向。至于移动端适配,在我看来,现在大家访问一个网页,从移动端访问会比从PC端访问要多。而第三点和第六点是锦上添花的功能,优先级并不高,可以在做完前面的功能后再逐步完成。

定义好需求后,我发现现有的成熟建站工具总会有不满足的点,例如Hexo的多语言国际化,我看了好几篇文章,始终没有看懂是怎么把中文站和英文站揉到一起的。再比如VuePress,默认主题能够满足绝大部分需求,但是如果想自己做一套主题,重新上手主题制作感觉也需要很久的时间。兜兜转转,我又让自己的目光回到了Astro上。第一次接触Astro是因为偶然看到@liruifengv的推文,于是又干起了自己的老本行——本土化。在做翻译的同时,我试着跟着教程搭了他们的博客示例站,感受到了在vite的加持下Astro渲染和热更新的飞速,如今再次回过头去看Astro的文档,我突然发现我所需要的功能似乎Astro都能实现,并且Astro的blog模板采用的正是我喜欢的卡片式风格。所以我决定基于Astro官方的blog模板,扩展一些自己需要的页面、样式和功能,形成一套自己喜欢的博客模板,取名为Astro Blog Plus。

Astro-Blog-Plus的结构与特性

与Astro官方自带的博客样式相比,我将原有模板的目录结构做了些许调整以满足国际化的需求,并做了许多页面上和功能上的扩展,下面分别从目录结构和特性两部分讲一讲。

Astro-Blog-Plus的目录结构

Astro-Blog-Plus的结构如下图所示,其中{lang}对应各个语言的编码,例如“en”、“zh-hans”等等。默认仓库中已经生成了英语(en),简体中文(zh-hans),繁体中文(zh-hant)三种语言的各类页面、目录与RSS文件,在实际使用时根据所需要的语言建立对应的文件与文件夹。

│   astro.config.mjs //Astro的配置文件
│   package-lock.json
│   package.json
│   tsconfig.json
└───src
    │   env.d.ts
    ├───components
    │       BaseHead.astro //引入通用<head>内容
    │       BlogPostLicense.astro //文章底部的许可证说明
    │       Footer.astro // 博客页脚内容
    │       FormattedDate.astro //格式化日期
    │       Header.astro //页头图片与导航栏
    │       HeaderLink.astro //顶部导航链接
    │       LanguageSelector.astro //语言选择框
    │       MainBlogHead.astro //针对不同的页面引入不同CSS
    │       MobileMenu.astro //移动端的菜单栏
    │       SinglePageHead.astro //与MainBlogHead.astro作用相同
    │       ThemeIcon.astro //明亮&黑暗模式切换图标
    │       WalineComment.astro //waline评论组件     
    ├───content
    │   │   config.ts
    │   ├───draft //草稿箱
    │   └───{lang} //不同语言的博客文章
    ├───layouts
    │       BlogPost.astro  //博文页面布局
    ├───locales
    │   └───{lang}
    │           friends.json //各语言的友链配置
    │           translation.json //各语言的源文本和翻译文本 
    ├───pages
    │   │   index.astro
    │   ├───{lang}
    │   │   │   about.astro //关于我
    │   │   │   archives.astro //归档
    │   │   │   friends.astro //友链
    │   │   │   index.astro
    │   │   │   messageBoard.astro //留言板
    │   │   │   tags.astro //标签汇总
    │   │   │   [...slug].astro //文章页
    │   │   │   [page].astro  //分页样式
    │   │   └───tags 
    │   │           [tag].astro //每个标签包含的文章      
    │   ├───rss //RSS订阅源配置文件
    │           {lang}.xml.js          
    └───styles
            global.css
            main-blog.css
            single-page.css
  • components:用于存放公用组件,例如Waline评论、暗黑模式切换、格式化日期等,与Vue.js等前端框架类似,这些组件可以在其它页面中被import和直接使用。

  • content内容集合文件夹,用于存放markdown文件或mdx文件等,可以被Astro的getCollection API根据内容集合的名称检索。

  • layouts:用于存放通用布局,目前只有“博客文章”的页面布局

  • locales:用于存放国际化内容的json数据,包括页头、页脚等部分的翻译内容与友链信息两部分。

  • pages:用于存放各种页面文件,每个Astro项目必需的文件夹,page下的index文件是必需的,由于Astro使用的是基于文件的路由,通常设置对应语言的文件夹来代表不同语言的页面。

  • styles:用于存放一些全局样式和特定类型页面的通用样式。

Astro-Blog-Plus的新特性

相较于原有的Blog模板,Astro-Blog-Plus增加了以下全新特性:

  • 国际化:对原有Page和content做了改造,并在通用components中引入动态参数,根据路径识别对应语言环境并根据对应的翻译文件进行对应位置字符串的加载。要在Astro项目中应用国际化选项,首先需要在astro.config.mjs文件中增加下面的代码:
import { defineConfig } from "astro/config";
import mdx from "@astrojs/mdx";
import sitemap from "@astrojs/sitemap";

// https://astro.build/config
export default defineConfig({
  site: "https://example.com",
  integrations: [mdx(), sitemap()],
  i18n: { // 国际化相关配置
    defaultLocale: "zh-hans", //默认语言
    locales: ["zh-hans", "zh-hant", "en"], //支持的语言列表
    routing: {
      prefixDefaultLocale: true, //默认语言也具有路径前缀
      redirectToDefaultLocale: false //不自动重定向到默认语言
    }
  },
});

其中defaultLocale为默认语言,初次加载网站时,Astro会将页面重定向到默认语言对应的index页面下。locales为受支持的语言,我们需要根据locales的语言名称在pages内添加对应的文件夹。routing是国际化功能对应的路由设定,其中我将prefixDefaultLocale设置为true,使默认语言也在url中具有对应的前缀,同时将redirectToDefaultLocale设为false,目的是在直接使用例如 https://example.com 访问网站时,页面会显示pages目录下的index页面,而不是重定向到对应语言的index页面。这样的话我可以将pages目录下的index文件改造为自定义的重定向页,在用户访问时识别用户的系统语言,随后跳转到对应语言的主页上。

接着在locales文件夹中添加对应语言的文件夹与翻译文件,添加对应内容后,在每个需要被国际化的页面和组件中添加如下代码,识别对应语言并替换对应的翻译字符串。

type LanguageCollection = "zh-hans" | "zh-hant" | "en";

// 获取当前语言
const url = new URL(Astro.request.url);
const lang = url.pathname.split("/")[1] as LanguageCollection;

// 动态加载翻译文件
import translationsZhHans from "../locales/zh-hans/translation.json";
import translationsZhHant from "../locales/zh-hant/translation.json";
import translationsEn from "../locales/en/translation.json";
const translations = {
  "zh-hans": translationsZhHans,
  "zh-hant": translationsZhHant,
  en: translationsEn,
}[lang];

// 定义翻译函数
const t = (key: string, params: { [key: string]: any } = {}) => {
  const keys = key.split(".");
  let translation: any = keys.reduce(
    (acc, k) => (acc && (acc as { [key: string]: any })[k]) || null,
    translations as { [key: string]: any }
  );

  // 如果翻译不存在或不是字符串,返回 key
  if (typeof translation !== "string") return key;

  // 替换占位符为实际值
  Object.keys(params).forEach((paramKey) => {
    const placeholder = `{{${paramKey}}}`;
    translation = translation.replace(
      new RegExp(placeholder, "g"),
      params[paramKey]
    );
  });

  return translation;
};
  • 引入Waline评论系统:Waline分为客户端和服务端两部分,

  • 加入“友链”页面

  • 在首页中加入分页

  • 加入文章置顶:相较于原有的post已拥有的属性,我在config.ts中添加了一个新属性top用于表示文章是否置顶,top值越大,置顶的优先级越大。随后在文章分页时,我将文章按照置顶文章与非置顶文章进行分类,并且将置顶文章按照top的大小排序,将非置顶文章按照pubDate的时间先后排序,最后将它们组合成posts数组进行分页显示,详细代码如下:

export async function getStaticPaths({ paginate }: { paginate: PaginateFunction }) {
  const topPosts = (await getCollection("zh-hans")).filter(
    (post) => post.data.top !== undefined && post.data.top > 0 //区分置顶文章
  ).sort(
    (a, b) => (b.data.top ?? 0) - (a.data.top ?? 0)
  );
  const otherPosts = (await getCollection("zh-hans")).filter(
    (post) => post.data.top === undefined || post.data.top === 0
  ).sort(
    (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
  );
  const posts = topPosts.concat(otherPosts);
  return paginate(posts, { pageSize: 5 });
}

随后添加一个<div>用于标记置顶文章,为这个div设置对应的类,进而区分出哪些文章需要被标记出置顶,实现这一部分功能的代码如下:

CSS:

.isTop0 {
  display: none;
}
.isTop1 {
  display: block;
  position: absolute;
  top: 0;
  right: 0;
  background-color: #b692c2;
  color: #fff;
  padding: 0.5rem;
  z-index: 5;
  width: 5%;
  text-align: center;
  border-radius: 0 12px 0 0;
}
.dark .isTop1 {
  background-color: #333;
}

HTML:

<li>
  <div class=`isTop${post.data.top}`>TOP</div> //这一行用于TOP标注
  <a href={`/${lang}/${post.slug}/`}>
    <img
      width={720}
      height={360}
      src={post.data.heroImage}
      alt=""
    />
    <h4 class="title">{post.data.title}</h4>
    <p class="date">
      <div>
        <FormattedDate date={post.data.pubDate} />
      </div>
    </p>
  </a>
</li>

写在最后

这次基于Astro的Blog模板搭自己的博客,我能明显感觉自己

上手一个框架,最快的方式就是去看它的文档,再结合ChatGPT等AI工具辅助理解其中的一些API和框架结构,最后尝试将自己的构想一步步实现出来。

除另有说明外,本博客的所有文章均使用 CC BY-NC-SA 4.0 许可, 作者保留所有权利, 如需转载请注明出处。