主页美化
最近的博客文章
主页加了一个最近的博客文章,显示三篇近期文章。
实现方式如下。
// 获取博客集合
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位删除内容,避免草稿出现
<!--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中添加
password: z.string().optional(),
在src/components/PasswordWrapper.astro中添加
---
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添加
---
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。代码如下:
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添加
<PasswordWrapper password="{blog.data.password}">
<content />
</PasswordWrapper>
把内容用PasswordWrapper弄上。
最后的最后,我使用的博客模板RSS会泄漏内容,需要把RSS修改一下,不要返回博客内容。
自动保存密码
改改上文的prepare函数,把password存到localStorage里面:
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
---
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中添加
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!