博客搭建笔记

最近把博客迁移了,顺便学习一下Astro框架

周一 4月 07 2025
838 字 · 8 分钟

主页美化

最近的博客文章

主页加了一个最近的博客文章,显示三篇近期文章。

实现方式如下。

JAVASCRIPT
// 获取博客集合
let blogs = await getCollection("blog");
for (let i = 0; i < blogs.length; i++) {
  if (blogs[i].data.draft) {
    blogs.splice(i, 1);
    i--;
  }
}
blogs = blogs.sort((a, b) => {
  return new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime();
});

这段实现了博客文章的获取,splice函数从第i位删除内容,避免草稿出现

HTML
<!--Recent Blog -->
<section class="py-4">
  <h2 class="text-2xl font-bold mb-6 flex items-center gap-2">
    <Icon name="lucide:folder" class="w-6 h-6 text-primary" />
    <span>最近的博客文章</span>
  </h2>
  <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
    <IndexRecentBlog
      title={blogs[0].data.title}
      description={blogs[0].data.description}
      icon="lucide:book-open"
      href={`/blog/${blogs[0].slug}`}
    />
    <IndexRecentBlog
      title={blogs[1].data.title}
      description={blogs[1].data.description}
      icon="lucide:book-open"
      href={`/blog/${blogs[1].slug}`}
    />
    <IndexRecentBlog
      title={blogs[2].data.title}
      description={blogs[2].data.description}
      icon="lucide:book-open"
      href={`/blog/${blogs[2].slug}`}
    />
  </div>
</section>

博客

添加密码

先在src/content/config.ts中添加

PLAINTEXT
password: z.string().optional(),

src/components/PasswordWrapper.astro中添加

JAVASCRIPT
---
import Encrypt from "@components/Encrypt.astro";
export interface Props {
  password?: string;
}
const password = Astro.props.password;
---
{
  !password ? (
    <slot />
  ) : (
    <Encrypt password={password}>
      <slot />
    </Encrypt>
  )
}

在 Astro 中,<slot /> 用于插入父组件传入的内容,这类似于其他前端框架(例如 React 的 props.children 或 Vue 的插槽)的作用。

在这里,即为下文

此外,文件中引用了Encrypt.astro,并根据有无密码实现是否加密

src/components/Encrypt.astro添加

JAVASCRIPT
---
import { encrypt } from "@utils/encrypt";
export interface Props {
  password: string;
}
const html = await Astro.slots.render("default");
const encryptedHtml = await encrypt(html, Astro.props.password);
---
<meta name="encrypted" content={encryptedHtml} />
<div>
  <input
    id="password"
    class="w-auto rounded border border-skin-fill
border-opacity-40 bg-skin-fill p-2 text-skin-base
placeholder:italic placeholder:text-opacity-75
focus:border-skin-accent focus:outline-none"
    placeholder="Enter password"
    type="text"
    autocomplete="off"
    autofocus
  />
  <button
    id="password-btn"
    class="bg-skin-full rounded-md
    border border-skin-fill border-opacity-50 p-2
    text-skin-base
    hover:border-skin-accent"
  >
    Submit
  </button>
</div>
<script is:inline data-astro-rerun>
  async function decrypt(data, key) {
    key = key.padEnd(16, "0");

    const decoder = new TextDecoder();
    const dataBuffer = new Uint8Array(
      atob(data)
        .split("")
        .map((c) => c.charCodeAt(0)),
    );
    const keyBuffer = new TextEncoder().encode(key);

    const cryptoKey = await crypto.subtle.importKey("raw", keyBuffer, { name: "AES-CBC", length: 256 }, false, [
      "decrypt",
    ]);

    const iv = dataBuffer.slice(0, 16);
    const encryptedData = dataBuffer.slice(16);
    const decryptedData = await crypto.subtle.decrypt({ name: "AES-CBC", iv }, cryptoKey, encryptedData);

    return decoder.decode(decryptedData);
  }

  function prepare() {
    const encrypted = document.querySelector("meta[name=encrypted]")?.getAttribute("content");
    const input = document.getElementById("password");
    const btn = document.getElementById("password-btn");
    const article = document.querySelector("#content");

    btn?.addEventListener("click", async () => {
      const password = input.value;
      try {
        const html = await decrypt(encrypted, password);
        article.innerHTML = html;
      } catch (e) {
        alert(e);
      }
    });
  }

  prepare();
  document.addEventListener("astro:after-swap", prepare);
</script>

上述文件实现了输入密码的前端,在这个文件里面引用了encrypt。代码如下:

TYPESCRIPT
export async function encrypt(data: string, key: string): Promise<string> {
  key = key.padEnd(16, "0");

  const dataBuffer = Buffer.from(data);
  const keyBuffer = Buffer.from(key);

  const cryptoKey = await crypto.subtle.importKey(
    "raw",
    keyBuffer,
    { name: "AES-CBC", length: 256 },
    false,
    ["encrypt"],
  );

  const iv = crypto.getRandomValues(new Uint8Array(16));

  console.warn("iv", iv);
  const encryptedData = await crypto.subtle.encrypt(
    { name: "AES-CBC", iv },
    cryptoKey,
    dataBuffer,
  );
  console.warn("encryptedData", new Uint8Array(encryptedData));
  const combinedData = new Uint8Array(iv.length + encryptedData.byteLength);
  combinedData.set(iv);
  combinedData.set(new Uint8Array(encryptedData), iv.length);
  return Buffer.from(combinedData).toString("base64");
}

加密部分使用AES-CBC,填充方式为零填充。

最后记得在src/pages/blog/[…slug].astro添加

HTML
<PasswordWrapper password="{blog.data.password}">
  <content />
</PasswordWrapper>

把内容用PasswordWrapper弄上。

最后的最后,我使用的博客模板RSS会泄漏内容,需要把RSS修改一下,不要返回博客内容。

自动保存密码

改改上文的prepare函数,把password存到localStorage里面:

JAVASCRIPT
function prepare() {
  const encrypted = document.querySelector("meta[name=encrypted]")?.getAttribute("content");
  const input = document.getElementById("password");
  const btn = document.getElementById("password-btn");
  const article = document.querySelector("#content");

  async function decryptAndDisplay() {
    const password = input.value;
    try {
      const html = await decrypt(encrypted, password);
      article.innerHTML = html;
      localStorage.setItem("password", password);
    }
    catch (e) {
      alert("密码错误");
      localStorage.removeItem("password");
    }
  }
  const savedPassword = localStorage.getItem("password");
  if (savedPassword) {
    input.value = savedPassword;
    decryptAndDisplay();
  }

  btn?.addEventListener("click", async () => {
    decryptAndDisplay();
  });
}

渲染界面添加一个小锁

改一下Heading.astro

JAVASCRIPT
---
import { Icon } from "astro-icon/components";

const { url, title, isLocked } = Astro.props;
---

<a href={url} target="_self" class="block hover:-translate-y-0.5 transition-transform duration-300">
  <h2 id={title} class="frosti-heading">
    {title}
    {isLocked && <Icon name="lucide:lock" class="inline-block ml-1 text-sm text-gray-500" />}
  </h2>
</a>

根据isLocked判断是否加上锁。此外需要自行修改一下PostData类型等。

[…page].astro中添加

HTML
page.data.map((blog: Post) => (
  <PostCard
    title={blog.data.title}
    image={blog.data.image}
    description={blog.data.description}
    url={"/blog/" + blog.slug}
    pubDate={blog.data.pubDate}
    badge={blog.data.badge}
    categories={blog.data.categories}
    tags={blog.data.tags}
    word={blog.remarkPluginFrontmatter.totalCharCount}
    time={blog.remarkPluginFrontmatter.readingTime}
    isLocked={!!blog.data.password}
  />
))
}

此外Index.astro也需要改一下,一样的操作。


Thanks for reading!

博客搭建笔记

周一 4月 07 2025
838 字 · 8 分钟