在蓋「Moment:看電影看劇時,聽到喜歡的音樂」專案的時候,覺得輕量級的網站,用 Markdown 語法可以滿足快速、便於更新內容的需求,找到了 Astro 和 Remix。
Astro 目前以幾乎每天更新一次的頻率快速發展中,2022 年 8 月進入 1.0 穩定版本。這篇文章不會再講一次 Astro 文件提到的功能,而是文件上找不到參考,只好到處從社群爬文記下來的做法。
"astro": "^2.9.0"
- 網站:https://astro.build
- 文件:https://docs.astro.build/zh-tw/getting-started/
Moment 是搭配 Solid.js
,用來處理影片播放互動,後來 Astro 本身幾乎是隱形,跟框架整合得不錯。
首頁有 30 張,平均 24 KB 的 JPG 圖檔,依然可以在 Lighthouse 獲得 4 個 100 綠燈。(慶祝動畫怎麼不見了?)
如果是從 Astro 的 Blog Starter 範例進行修改,基礎是 preact
,使用的體感跟 React 幾乎沒有差別。
接下來的內容
Astro 2.9.0 之後遇到的問題
Astro 2.9.0 之前的心得/問題
根據文件路徑載入圖片
Astro
2.9.0
起的實驗功能 Assets
(3.0.0
之後成為正式功能),光是看文件,提到載入圖片的方法,會發現很多得不斷複製貼上的地方,不論是在 .astro
:
---
// /src/pages/[categories]/[...slug].astro
import { Image } from 'astro:assets';
import ovenMittImage from "../assets/utensils/oven-mitt/cover.jpg";
---
<Image src={ovenMitt} alt="圖片說明" />
或 markdown
、mdx
、markdoc
的 frontmatter
資訊裡:
---
# /src/content/utensils/oven-mitt.mdx
title: "標題名稱"
cover: "../../assets/utensils/oven-mitt/cover.jpg" # 對應到檔案 "src/assets//utensils/oven-mitt/cover.jpg"
coverAlt: "圖片說明"
---
文章內容
以上都無法從路徑與檔案名稱獲得對應圖片檔案,要不然就是得設定好一大串 import
。這使得在加入資料的時候,非常不方便。
還有,若把圖片放在 /public
,用 <Image />
帶入路徑:
---
// ❌ 在 <Image /> 載入 /public 資料夾裡的圖片
import { Image } from 'astro:assets';
---
<Image src={`/${categoryName}/${slug}/cover.jpg`} alt="圖片說明">
得到的是一般 <img>
語法,沒有 Astro
針對圖片載入效能的效果。
<!-- ❌ <Image /> 不會對來自 /public 的圖片進行最佳化 -->
<img src="/utensils/oven-mitt/cover.jpg" alt="圖片說明">
如果想要自動帶入路徑或檔名,又有 Astro
的最佳化效果,要從 Vite 4
的功能來下手:
---
// /src/pages/[categories]/[...slug].astro
import { Image } from 'astro:assets';
const { categories: categoryName, slug } = Astro.params;
---
<!-- ✅ 在 src 直接使用 import -->
<Image
src={import(`../../assets/${categoryName}/${slug}/cover.jpg`)}
alt="圖片說明"
/>
要注意的是,結尾必須是附檔名,不能放在變數裡面。
資料來源
Using Dynamic Filename Props in Astro Images
以變數載入 Content Collection
的型別設定
以 Content Collection
製作比較複雜的導覽介面時,遇到須以 Astro.props
或 Astro.params
來取得特定的文章列表或內容。
---
// /src/components/navigation/Slug.astro
import { getCollection } from 'astro:content';
const { categoryName } = Astro.props;
const slugItems = await getCollection(categoryName);
---
getCollection()
只能傳入已經設定好的 collection
名稱:
// /src/content/config.ts
import { defineCollection } from 'astro:content';
import { itemSchema } from '../schemas';
const tidyCollection = defineCollection({
type: 'content',
schema: itemSchema
});
const utensilsCollection = defineCollection({
type: 'content',
schema: itemSchema
});
export const collections = {
'tidy': tidyCollection,
'utensils': utensilsCollection
};
所以,在指定此處 prop
型別的時候,必須設定為 string literal types
。
以最偷懶的方法,是手動列出可能的 string
:
---
// /src/components/navigation/Slug.astro
import { getCollection } from 'astro:content';
type Props = {
categoryName: 'tidy' | 'utensils';
};
const { categoryName } = Astro.props;
const slugItems = await getCollection(categoryName);
---
只不過,如果 categoryName
有內容變動,就要記得回來這個檔案跟上,很不方便。
因此想要用一組陣列,既產生靜態網址,也可以定義 string literal types
。
使用的方法是把陣列指定為 const assertion
:
// constants.ts
export const categories = <const>
[
{
name: 'tidy',
label: '收納'
},
{
name: 'utensils',
label: '廚房清潔'
}
];
這可以用在靜態網址的 getStaticPaths()
:
---
// /src/pages/[categories].astro
import { categories } from '../../constants';
export async function getStaticPaths() {
return categories.map(({ name, label }) => ({
params: { categories: name },
props: { name, label }
}));
};
---
也能符合要傳入 getCollection()
的 props
型別設定:
---
// /src/layouts/Slug.astro
import SlugNavigation from '@components/navigation/Slugs.astro';
type Params = { categories: (typeof categories)[number]['name'] };
const { categories: categoryName } = Astro.params as Params;
---
<!-- ... -->
<SlugNavigation categoryName={categoryName} />
<!-- ... -->
---
// /src/components/navigation/Slugs.astro
type Props = { categoryName: (typeof categories)[number]['name'] };
const { categoryName } = Astro.props as Props;
const slugItems = await getCollection(categoryName);
---
<!-- ... -->
{
slugItems.map(({ slug, data }) => (
// ...
))
}
<!-- ... -->
Markdoc 設定檔的 build 型別錯誤
在 Astro 的 Markdoc 說明文件提到:設定檔名稱可以是 markdoc.config.mjs|ts
。如果用 .ts
附檔名,且跟著說明,把<article>
標籤省略。進行 build 的時候會出現錯誤:
pnpm build
> astro check && tsc --noEmit && astro build
09:06:48 PM [content] Types generated 257ms
...
markdoc.config.ts:15:7 - error TS2322: Type 'null' is not assignable to type 'Render | undefined'.
15 render: null
~~~~~~
Found 1 error in markdoc.config.ts:15
這錯誤只要檔名改成 markdoc.config.mjs
即可通過。
Astro v.s. Remix
Remix 的開發體驗確實很棒,社群也非常活絡。但畢竟是全端套件,如果只是要做靜態網站,以後會優先選擇 Astro。
Astro
- 可以發布到靜態網站服務:Github Pages、Vercel 或 Netlify 都行,也就是可以免費。
- 開發團隊建議使用 Tailwind、SCSS(或者更像 CSS Modules)和 PostCSS。
- 未正式支援
CSS-In-JS
,但社群有整理出可以使用的套件。
Remix
- 可以使用
CSS-In-JS
。 - 雖然同樣可以做成靜態網頁,但 MDX 放在路徑資料夾裡的時候,連路徑名稱都無法取得,實用程度不高,官方文件都說不推薦使用。
- 無法在(免費)靜態網站服務使用
mdx-bundler
的原因與考究,詳情請看這篇:初次使用 Remix,搭配 mdx-bundler。
區分 <body>
樣式
Astro 的 CSS 是 scoped
,不同頁的樣式不會混在一起。我想要首頁跟內文頁的外觀、版面不同。因此在各自頁面的 <body>
設定:
// pages/index.astro
body {
background-color: lavender;
}
// layouts/BlogPost.astro
body {
background-color: thistle;
}
發現首頁的 <body>
樣式,會被內頁的設定覆蓋:
詢問過 Astro 製作團隊,是他們刻意這樣設計,如果也有不同 <body>
樣式需求,有以下 3 個步驟:
在 <body>
加上屬性來區別頁面
<!-- pages/index.astro -->
<body data-body-style="home">
...
</body>
<!-- layouts/BlogPost.astro -->
<body data-body-style="post">
...
</body>
以 import
匯入全域 CSS 檔案
// Head.astro
import '~/styles/global.css';
設定個別樣式
// ~/styles/global.css
[data-body-style="home"] {
background-color: lavender;
}
[data-body-style="post"] {
background-color: thistle;
}
內容頁,或者說 /layouts
裡的樣式,就不會蓋掉首頁的。
設定 Markdown 語法的樣式
在 Astro 的 pages/
下,無論是使用 .md
或 .mdx
檔案,都可以有等同於 MDX 的功能,將元件輸入至內文。
// LyricSection.astro
<section class="lyricSection">
<slot />
</section>
---
# /pages/bohemian-rhapsody.mdx
layout: LyricSection from '../../components/LyricSection.astro';
---
<LyricSection>
Is this the real life?
Is this just fantasy?
Caught in a landslide
No escape from reality
</LyricSection>
這一段會產生 HTML 語法:
<!-- /bohemian-rhapsody -->
<section class="lyricSection astro-SNPOEJMT">
<p>Is this the real life?</p>
<p>Is this just fantasy?</p>
<p>Caught in a landslide</p>
<p>No escape from reality</p>
</section>
我想要設定 <section>
下面的 <p>
樣式,會發現也是因為 scoped
的緣故,沒辦法直接指定:
// LyricSection.astro
...
<style lang="scss">
.lyricSection {
display: grid;
row-gap: 16px;
p {
margin: 0;
color: hsl(var(--color-midnight1600));
font-size: 1.6rem;
line-height: 24px;
}
}
</style>
必須加上 :global()
才有效果:
// LyricSection.astro
.lyricSection {
...
:global(p) {
...
}
}
加入結構化資料 JSON LD 語法
按照直覺把結構化資料放在 <head> ... </head>
裡面時,會發現無法如預期輸出 JSON LD 語法:
// BaseHead.astro
---
const schema = JSON.stringify({
'@context': 'https://schema.org',
'@graph': [{
'@type': 'WebPage',
url: `${īmport.meta.env.PUBLIC_HOSTNAME}${canonicalPath}`,
name: title,
datePublished: published,
dateModified: modified
}]
});
// 要把 īmport 改為 import
---
<script type="application/ld+json">{schema}</script>
0.23.0
版開始,在 <script>
得要讓 HTML 有 Escape 過,才會出現有效的 JSON LD 語法:
// BaseHead.astro
<script type="application/ld+json" set:html={schema} />
遇到的 Bug
在程式碼區塊使用環境變數
在上一段內容,程式碼區塊裡有提到 Astro 的環境變數。
上傳到正式環境才發現:Markdown 區塊裡應該要直接顯示環境變數原始碼的地方,卻把它轉換了,或是出現令人不解的訊息,在使用 SSR 的情況下,甚至會無法 Build 成功。
如果是使用 MDX,已經在 @astrojs/mdx@0.11.2
修正。
然而,如果用了 Astro 最近新增的 Markdoc 格式,會造成編譯錯誤。
錯誤訊息是在該檔案的第 1 個空白行:
[error] Unexpected token (Note that you need plugins to import files that are not JavaScript)
這個問題已經回報,正等待解決。