Directus+Astro搭建博客网站
在本文开始之前你应该需要安装好Directus和Astro,若需帮助可参考:
Directus中需要建立一个名为articles的模型,并添加title、body及图片cover_image字段内容。
安装Directus SDK
为Astro安装Directus的JavaScript SDK,有了SDK后基于API的CRUD各种操作将变得非常简单
npm install @directus/sdk
配置Directus令牌
为了Api的安全考虑,我们需要在Directus后台配置一个被Astro使用的验证账号或者STATIC_TOKEN
打开directus后台,找到用户管理并添加一个用于API的认证的账号

#Option 为用户添加Token
个人更倾向于此方式,由于账号密码需要明文配置,所以Token更加安全,需要注意的是directus用户的token是一次性生成的且不可见的,所以我们生成出来token字符串的时候一定要复制下来

为Astro配置Directus SDK接口
在Astro目录下创建.env环境配置文件,根据实际情况修改
#Directus地址 PUBLIC_DIRECTUS_URL=http://localhost:8055 #认证账号 DIRECTUS_EMAIL= #认证密码 DIRECTUS_PASSWORD= #账号Token DIRECTUS_STATIC_TOKEN=
注意这里的账号密码和Token二选一填写,个人喜欢用token。
创建文件 /src/utils/get-asset-url.js
export function getAssetURL(id) {
if (!id) return null;
return `${import.meta.env.PUBLIC_DIRECTUS_URL}/assets/${id}`;
}创建文件 /src/utils/get-directus-client.js
import { Directus } from "@directus/sdk";
export const getDirectusClient = async () => {
const directus = new Directus(import.meta.env.PUBLIC_DIRECTUS_URL);
if (directus.auth.token) return directus;
if (import.meta.env.DIRECTUS_EMAIL && import.meta.env.DIRECTUS_PASSWORD) {
await directus.auth.login({
email: import.meta.env.DIRECTUS_EMAIL,
password: import.meta.env.DIRECTUS_PASSWORD,
});
} else if (import.meta.env.DIRECTUS_STATIC_TOKEN) {
await directus.auth.static(import.meta.env.DIRECTUS_STATIC_TOKEN);
}
return directus;
};这两个文件用于读取.env的配置信息。
创建文件 /src/utils/format-relative-time.js ,计算相对时间的函数
const UNITS = {
year: 24 * 60 * 60 * 1000 * 365,
month: (24 * 60 * 60 * 1000 * 365) / 12,
day: 24 * 60 * 60 * 1000,
hour: 60 * 60 * 1000,
minute: 60 * 1000,
second: 1000,
};
const LOCALE = "en";
const rtf = new Intl.RelativeTimeFormat(LOCALE, { numeric: "auto" });
export function formatRelativeTime(fromDate, toDate) {
const elapsed = fromDate - (toDate || new Date());
// "Math.abs" accounts for both "past" & "future" scenarios
for (let u in UNITS) {
if (Math.abs(elapsed) > UNITS[u] || u === "second")
return rtf.format(Math.round(elapsed / UNITS[u]), u);
}
return fromDate.toLocaleDateString(LOCALE);
}
内容展示组件
创建用于文章列表展示的component /src/components/Article.astro
---
import { getAssetURL } from '../utils/get-asset-url'
const { article } = Astro.props
---
<article>
<div class="article__topWrapper">
<div class="article__imageWrapper">
<img src={getAssetURL(article.cover_image)} alt="" loading="lazy" />
</div>
<span aria-hidden="true" class="tag">Writing</span>
</div>
<div class="article__bottomWrapper">
<h1 class="article__title">
<a href={`/articles/${article.id}`}>
{article.title}
</a>
</h1>
<div class="article__detail">
<div class="article__detailInner">
<div class="article__detailInnerTime">{article.publish_date}</div>
<div class="article__detailInnerCategory">Writing</div>
</div>
</div>
</div>
</article>在/src/pages/index.astro调用文章列表
---
import Layout from '../layouts/Layout.astro';
import Article from '../components/Article.astro'
import { getDirectusClient } from '../utils/get-directus-client'
import { formatRelativeTime } from '../utils/format-relative-time'
const directus = await getDirectusClient()
const response = await directus.items("articles").readByQuery({
fields: ["*"]
})
const formattedArticles = response.data.map((article) => {
return {
...article,
date_created: formatRelativeTime(new Date(article.date_created)),
}
})
const [ ...articles] = formattedArticles
---
<Layout title="BLOG">
<main>
<section class="main-content">
<div class="container">
<div class="articles-grid">
{articles.map((article) => (
<Article article={article} />
))}
</div>
</div>
</section>
</main>
</Layout>文章展示页 /src/pages/articles/[id].astro
---
import Layout from '../../layouts/Layout.astro';
import MoreArticles from '../../components/MoreArticles.astro'
import { getDirectusClient } from '../../utils/get-directus-client'
import { getAssetURL } from '../../utils/get-asset-url'
import { formatRelativeTime } from '../../utils/format-relative-time'
export async function getStaticPaths() {
const directus = await getDirectusClient();
const response = await directus.items("articles").readByQuery({
fields: ["*"],
filter: { status: { _eq: "published" } },
limit: -1,
})
let articles = response.data
for (const [index, article] of Object.entries(articles)) {
const moreArticlesResponse = await directus.items("item").readByQuery({
fields: ["*"],
filter: {
_and: [{ id: { _neq: article.id } }, { status: { _eq: "published" } }],
},
limit: 2,
})
const formattedMoreArticles = moreArticlesResponse.data.map(
(formattedMoreArticle) => {
return {
...formattedMoreArticle,
publish_date: formatRelativeTime(
new Date(formattedMoreArticle.date_created)
),
}
}
)
articles[index] = {
params: { id: article.id.toString() },
props: {
article: {
...article,
publish_date: formatRelativeTime(new Date(article.date_created)),
moreArticles: formattedMoreArticles,
}
}
}
}
return articles
}
const { article } = Astro.props
---
<Layout title="{article.title}">
<div class="current-article">
<section>
<div class="container">
<a href="/" class="current-article__backlink">
<span>Back to Articles</span>
</a>
<h1 class="current-article__title">{article.title}</h1>
<div class="current-article__detail">
<div class="current-article__wrapperOuter">
<div class="current-article__wrapperInner">
<div>
<div class="current-article__time">
{article.date_created}
</div>
</div>
</div>
</div>
<div class="current-article_coverImage">
<img src={getAssetURL(article.cover_image)} alt="" />
</div>
</div>
<div class="current-article__body">
<div class="current-article__bodyContent" set:html={article.body}></div>
</div>
</div>
</section>
<MoreArticles articles={article.moreArticles} />
</div>
</Layout>更多文章组件 /src/components/MoreArticles.astro
---
import Article from '../components/Article.astro'
const { articles } = Astro.props
---
<section class="more-articles">
<div class="container">
<h1 class="more-articles__title">More Articles</h1>
{articles?.length !== 0 && (
<div class="articles-grid">
{articles.map((article, index) => (
<Article article={article} bordered={index !==articles.length - 1} />
))}
</div>
)}
</div>
</section>