容查詢與渲染指南:用 getEmDashCollection / getEmDashEntry 構(gòu)建 Astro 驅(qū)動(dòng)的內(nèi)容站點(diǎn))
CMS后端前端插件系統(tǒng)【免費(fèi)下載鏈接】emdashEmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress項(xiàng)目地址https://gitcode.com/gh_mirrors/emdas/emdash點(diǎn)擊查看免費(fèi)下載EmDash 是一個(gè)基于 Astro 構(gòu)建的全棧 TypeScript CMSWordPress 的現(xiàn)代后繼者其核心能力之一是把 CMS 內(nèi)容無(wú)縫暴露給前端頁(yè)面在.astro頁(yè)面中通過(guò)emdash包提供的查詢函數(shù)按集合collection取數(shù)、渲染 Portable Text 富文本、輸出 CMS 圖片并開(kāi)啟可視化編輯。本篇指南以 querying-and-rendering.md 為骨架逐項(xiàng)講解內(nèi)容查詢 API、緩存集成、富文本與圖片渲染、分頁(yè)、SEO 元數(shù)據(jù)與常見(jiàn)頁(yè)面模式并結(jié)合 query.ts 等源碼說(shuō)明底層實(shí)現(xiàn)幫助你在自己的 EmDash 站點(diǎn)上寫出類型安全、可緩存、可點(diǎn)選編輯的頁(yè)面。內(nèi)容查詢 API 總覽EmDash 的查詢函數(shù)統(tǒng)一從emdash包導(dǎo)入它們?cè)趦?nèi)部包裝 Astro 的getLiveCollection/getLiveEntry并附加類型過(guò)濾與內(nèi)容水合bylines、taxonomy terms源碼入口見(jiàn) index.ts 的 re-export 與 query.ts。核心函數(shù)有兩個(gè)getEmDashCollection(type, filter?)—— 獲取一個(gè)集合的多條條目返回{ entries, error, cacheHint, nextCursor }getEmDashEntry(type, id, options?)—— 按 slug或數(shù)據(jù)庫(kù) ID獲取單條條目返回{ entry, error, isPreview, cacheHint }。兩個(gè)函數(shù)都遵循“錯(cuò)誤不拋出、隨結(jié)果返回”的 Astro 風(fēng)格集合查詢?cè)诔鲥e(cuò)時(shí)返回空entries數(shù)組并附帶error字段單條查詢?cè)谖凑业綍r(shí)entry為null這不屬于錯(cuò)誤只有真正的數(shù)據(jù)庫(kù)異常才會(huì)設(shè)置error。因此頁(yè)面里可以直接用if (!post) return Astro.redirect(/404)處理不存在的情況。集合查詢getEmDashCollection最基本的用法是不帶任何過(guò)濾條件取回整個(gè)集合import { getEmDashCollection } from emdash; // Basic const { entries: posts } await getEmDashCollection(posts);帶選項(xiàng)的用法如下// With options const { entries: posts, cacheHint } await getEmDashCollection(posts, { status: published, limit: 10, orderBy: { published_at: desc }, where: { category: news }, });選項(xiàng)說(shuō)明與源碼中 CollectionFilterBase 的定義一致選項(xiàng)類型說(shuō)明statusdraft \| published \| archived按內(nèi)容狀態(tài)過(guò)濾limitnumber返回的最大條目數(shù)cursorstring不透明游標(biāo)用于 keyset 分頁(yè)把上一次結(jié)果的nextCursor傳進(jìn)來(lái)即可翻頁(yè)offsetnumber偏移分頁(yè)的跳過(guò)條數(shù)與cursor互斥同時(shí)傳入在編譯期報(bào)錯(cuò)orderBy{ field: asc \| desc }排序字段與方向默認(rèn){ created_at: desc }也支持多字段如{ published_at: desc, title: asc }whereRecordstring, WhereValue按字段值、taxonomy 詞條或 byline 過(guò)濾數(shù)組表示 OR 語(yǔ)義如{ category: [news, featured] }還支持日期范圍{ published_at: { gte: 2024-01-01, lt: 2025-01-01 } }localestring配置了 i18n 時(shí)按語(yǔ)言過(guò)濾如en/frwhere的細(xì)節(jié)值得展開(kāi)taxonomy 名稱會(huì)被自動(dòng)識(shí)別并走 JOIN 過(guò)濾如{ category: news }過(guò)濾歸類到該詞條的條目保留鍵byline會(huì)通過(guò)_emdash_content_bylines中間表按署名過(guò)濾含合著條目{ byline: [01HXYZ..., 01HABC...] }表示任一署名匹配其余鍵則作為內(nèi)容表的列過(guò)濾。見(jiàn) query.ts 的注釋與示例。單條查詢getEmDashEntryimport { getEmDashEntry } from emdash; const { entry: post, cacheHint } await getEmDashEntry(posts, slug); if (!post) { return Astro.redirect(/404); }getEmDashEntry接受可選的{ locale }選項(xiàng)。在配置了 i18n 時(shí)它會(huì)沿著“請(qǐng)求語(yǔ)言 → fallback 語(yǔ)言 → 默認(rèn)語(yǔ)言”的 fallback 鏈解析命中的條目會(huì)在結(jié)果中帶出fallbackLocale字段預(yù)覽_previewtoken與編輯模式由中間件通過(guò) AsyncLocalStorage 注入請(qǐng)求上下文查詢函數(shù)自動(dòng)讀取無(wú)需額外傳參見(jiàn) query.ts 的 locale 鏈與 draft 分支邏輯。Entry 結(jié)構(gòu)與“兩個(gè) id”的陷阱查詢返回的每條 entry 都是統(tǒng)一的ContentEntryT形狀interface ContentEntryT { id: string; // The slug (used in URLs) data: T; // All fields, including system fields edit: EditProxy; // Visual editing attributes (spread onto elements) }data中既包含系統(tǒng)字段也包含你在 CMS 里定義的自定義字段。以一篇 post 為例interface PostData { id: string; // Database ULID (use for taxonomy lookups, etc.) slug: string; status: string; title: string; featured_image?: { id: string; src?: string; alt?: string; width?: number; height?: number; }; content?: PortableTextBlock[]; createdAt: Date; updatedAt: Date; publishedAt: Date | null; // Bylines (eagerly loaded) byline: BylineSummary | null; // Primary author bylines: ContentBylineCredit[]; // All credits (with roleLabel, source) // ... your custom fields }最重要的一個(gè)約定entry.id是 slug用于拼 URLentry.data.id才是數(shù)據(jù)庫(kù) ULID用于調(diào)用getEntryTerms等 API。千萬(wàn)不要混用二者URL 用entry.id如/posts/${post.id}而需要按數(shù)據(jù)庫(kù)主鍵做查詢例如取 taxonomy terms時(shí)必須傳entry.data.id。從源碼看entryDatabaseId正是讀取data.idquery.ts。另外注意bylines/byline是**急切水合eagerly hydrated**的字段——查詢返回時(shí)已附帶署名數(shù)據(jù)不需要額外請(qǐng)求taxonomy terms 則被水合到entry.data.terms按 taxonomy 名稱分組的TaxonomyTerm[]對(duì)象這意味著列表頁(yè)循環(huán)條目時(shí)無(wú)需再逐條調(diào)用getEntryTerms造成 N1 查詢見(jiàn) query.ts 的批量 JOIN 實(shí)現(xiàn)。緩存永遠(yuǎn)調(diào)用 Astro.cache.set(cacheHint)查詢結(jié)果都帶有cacheHint用于 Astro 的 Route Caching內(nèi)容變更時(shí)自動(dòng)失效緩存--- const { entries: posts, cacheHint } await getEmDashCollection(posts); Astro.cache.set(cacheHint); ---請(qǐng)務(wù)必調(diào)用Astro.cache.set(cacheHint)——它讓頁(yè)面在 CMS 內(nèi)容更新時(shí)自動(dòng)失效并重建是生產(chǎn)站點(diǎn)緩存一致性的前提。從實(shí)現(xiàn)看集合與單條查詢都經(jīng)過(guò)請(qǐng)求級(jí)緩存同一渲染周期內(nèi)相同(type, filter)的重復(fù)查詢只執(zhí)行一次與分布式對(duì)象緩存L2按collection filter 有效 locale緩存 JSON 快照見(jiàn) query.tscacheHint攜帶的tags/lastModified就是給路由緩存做失效判斷的依據(jù)。此外對(duì)于limit小于 10 的小型“最近 N 篇”組件查詢層會(huì)把 limit 歸并到共享桶bucket以合并重復(fù)取數(shù)所以多個(gè)側(cè)邊欄小組件同時(shí)渲染也不會(huì)各自查庫(kù)query.ts。渲染 Portable TextCMS 里的富文本字段如content是 Portable Text 塊數(shù)組用emdash/ui的PortableText組件渲染--- import { PortableText } from emdash/ui; --- PortableText value{post.data.content} /它內(nèi)置支持標(biāo)準(zhǔn)塊段落、標(biāo)題、列表、引用、代碼塊、圖片與行內(nèi)標(biāo)記加粗、斜體、代碼、刪除線、鏈接。組件源碼見(jiàn) PortableText.astro并從 components/index.ts 統(tǒng)一導(dǎo)出。自定義塊類型營(yíng)銷頁(yè)通常需要自定義塊如 hero、features 等 CMS 區(qū)塊。通過(guò)componentsprop 傳入類型到組件的映射--- import { PortableText } from emdash/ui; import Hero from ./blocks/Hero.astro; import Features from ./blocks/Features.astro; const customTypes { marketing.hero: Hero, marketing.features: Features, }; --- PortableText value{page.data.content} components{{ type: customTypes }} /每個(gè)自定義組件會(huì)收到該 block 的數(shù)據(jù)作為 props你可以在組件內(nèi)部讀取字段并自行排版。Image 組件CMS 圖片字段是對(duì)象不是字符串CMS 中的圖片字段一律是對(duì)象含id、src、alt、width、height等必須使用 EmDash 的Image組件渲染。正確與錯(cuò)誤的寫法對(duì)比--- import { Image } from emdash/ui; --- {/* Correct -- passes the image object */} Image image{post.data.featured_image} / {/* Also works with explicit props */} {post.data.featured_image?.src ( img src{post.data.featured_image.src} alt{post.data.featured_image.alt || } / )}{/* WRONG -- image is an object, not a string */} img src{post.data.featured_image} /把對(duì)象直接塞給原生img src是新手最常見(jiàn)的錯(cuò)誤渲染結(jié)果是[object Object]。Image組件源碼見(jiàn) Image.astro 與 EmDashImage.astro處理對(duì)象并輸出正確的img屬性如果確實(shí)要用原生標(biāo)簽務(wù)必像上面的“顯式 props”寫法那樣取.src與.alt??梢暬庉嬚归_(kāi) entry.edit 屬性每條 entry 都攜帶edit代理對(duì)象把它展開(kāi)到展示對(duì)應(yīng)字段的元素上即可啟用“點(diǎn)擊即編輯”h1 {...post.edit.title}{post.data.title}/h1 p {...post.edit.excerpt}{post.data.excerpt}/p div {...post.edit.featured_image} Image image{post.data.featured_image} / /div當(dāng)管理員登錄并瀏覽站點(diǎn)時(shí)這些屬性會(huì)附加編輯標(biāo)注實(shí)現(xiàn)行內(nèi)點(diǎn)選編輯普通訪客拿到的是 no-op 版本展開(kāi)后不產(chǎn)生任何副作用。實(shí)現(xiàn)上createEditable/createNoopvisual-editing/editable.ts 的引入處會(huì)根據(jù)請(qǐng)求上下文是否編輯模式?jīng)Q定附加真實(shí)代理還是空操作Portable Text 數(shù)組還會(huì)被貼上非枚舉的編輯元數(shù)據(jù)tagEditableFields見(jiàn) query.ts讓富文本字段同樣可被編輯定位。常見(jiàn)頁(yè)面模式列表頁(yè)如/posts/index.astro--- import { getEmDashCollection, getEntryTerms } from emdash; import { Image } from emdash/ui; import Base from ../../layouts/Base.astro; const { entries: posts, cacheHint } await getEmDashCollection(posts, { orderBy: { published_at: desc }, }); Astro.cache.set(cacheHint); const sortedPosts posts.toSorted((a, b) { const dateA a.data.publishedAt?.getTime() ?? 0; const dateB b.data.publishedAt?.getTime() ?? 0; return dateB - dateA; }); --- Base titlePosts {sortedPosts.map(post ( article {post.data.featured_image Image image{post.data.featured_image} /} a href{/posts/${post.id}}{post.data.title}/a {post.data.excerpt p{post.data.excerpt}/p} /article ))} /Base注意publishedAt是Date對(duì)象所以手動(dòng)排序要用getTime()比較鏈接一律使用post.idslug。詳情頁(yè)如/posts/[slug].astro--- import { getEmDashEntry, getEntryTerms, getSeoMeta } from emdash; import { Image, PortableText } from emdash/ui; import Base from ../../layouts/Base.astro; const { slug } Astro.params; if (!slug) return Astro.redirect(/404); const { entry: post, cacheHint } await getEmDashEntry(posts, slug); if (!post) return Astro.redirect(/404); Astro.cache.set(cacheHint); const seo getSeoMeta(post, { siteTitle: My Blog, siteUrl: Astro.url.origin, path: /posts/${slug}, }); const tags await getEntryTerms(posts, post.data.id, tag); --- Base title{seo.title} description{seo.description} article {post.data.featured_image ( div {...post.edit.featured_image} Image image{post.data.featured_image} / /div )} h1 {...post.edit.title}{post.data.title}/h1 PortableText value{post.data.content} / {tags.length 0 ( div {tags.map(t a href{/tag/${t.slug}}{t.label}/a)} /div )} /article /Base這里示范了三個(gè)要點(diǎn)getSeoMeta導(dǎo)出自 seo/index.ts根據(jù) entry 生成 SEO 標(biāo)題與描述getEntryTerms(posts, post.data.id, tag)必須傳數(shù)據(jù)庫(kù) ULIDpost.data.id而非 slug可視化編輯屬性與Image/PortableText組合使用。分類歸檔頁(yè)如/category/[slug].astro--- import { getTerm, getEmDashCollection } from emdash; import Base from ../../layouts/Base.astro; const { slug } Astro.params; const term slug ? await getTerm(category, slug) : null; if (!term) return Astro.redirect(/404); const { entries: posts } await getEmDashCollection(posts, { where: { category: term.slug }, orderBy: { published_at: desc }, }); --- Base title{${term.label} posts} h1{term.label}/h1 {posts.map(post ( a href{/posts/${post.id}}{post.data.title}/a ))} /BasegetTerm按(taxonomy, slug)取單個(gè)詞條含 label、slug、children 等并支持 locale fallback 鏈與可選的可見(jiàn)條目計(jì)數(shù)includeCounts: false可跳過(guò)計(jì)數(shù)查詢實(shí)現(xiàn)見(jiàn) taxonomies/index.ts。歸檔頁(yè)隨后用where: { category: term.slug }過(guò)濾出該分類下的條目。RSS 源如/rss.xml.tsimport type { APIRoute } from astro; import { getEmDashCollection } from emdash; const siteTitle My Site; export const GET: APIRoute async ({ url }) { const siteUrl url.origin; const { entries: posts } await getEmDashCollection(posts, { orderBy: { published_at: desc }, limit: 20, }); const items posts .filter((p) p.data.publishedAt) .map((post) { const postUrl ${siteUrl}/posts/${post.id}; return item title${escapeXml(post.data.title)}/title link${postUrl}/link guid isPermaLinktrue${postUrl}/guid pubDate${post.data.publishedAt!.toUTCString()}/pubDate description${escapeXml(post.data.excerpt || )}/description /item; }) .join(\n); return new Response( ?xml version1.0 encodingUTF-8? rss version2.0 xmlns:atomhttp://www.w3.org/2005/Atom channel title${escapeXml(siteTitle)}/title link${siteUrl}/link atom:link href${siteUrl}/rss.xml relself typeapplication/rssxml/ languageen-us/language lastBuildDate${new Date().toUTCString()}/lastBuildDate ${items} /channel /rss, { headers: { Content-Type: application/rssxml; charsetutf-8, Cache-Control: public, max-age3600, }, }, ); }; function escapeXml(s: string): string { return s .replace(//g, amp;) .replace(//g, lt;) .replace(//g, gt;) .replace(//g, quot;) .replace(//g, apos;); }要點(diǎn)過(guò)濾掉未發(fā)布的條目p.data.publishedAt為 null 的過(guò)濾掉、用entry.id拼 URL、用toUTCString()輸出 RFC 822 時(shí)間并對(duì)所有動(dòng)態(tài)文本做 XML 轉(zhuǎn)義。404 頁(yè)面/404.astro--- import Base from ../layouts/Base.astro; --- Base titleNot Found h1Page not found/h1 pThe page youre looking for doesnt exist./p a href/Go home/a /Base空狀態(tài)當(dāng)集合還沒(méi)有內(nèi)容時(shí)展示一個(gè)友好的空狀態(tài)引導(dǎo)去后臺(tái)創(chuàng)建第一篇內(nèi)容{posts.length 0 ? ( section h2No posts yet/h2 pCreate your first post in the admin panel./p a href/_emdash/admin/content/posts/newCreate a post/a /section ) : ( /* ... render posts ... */ )}分頁(yè)cursor 與 offset 兩種方式getEmDashCollection內(nèi)置兩種互斥的分頁(yè)方式keyset 游標(biāo)分頁(yè)cursor與偏移分頁(yè)offset。源碼中的類型定義保證了二者不能同時(shí)傳入同時(shí)提供在編譯期報(bào)錯(cuò)見(jiàn) query.ts。游標(biāo)分頁(yè)推薦用于“下一頁(yè)”翻頁(yè)把上一次結(jié)果的nextCursor傳入下一次請(qǐng)求的cursor即可nextCursor為undefined表示沒(méi)有更多結(jié)果--- const cursor Astro.url.searchParams.get(cursor) ?? undefined; const { entries, nextCursor, cacheHint } await getEmDashCollection(posts, { limit: 10, cursor, orderBy: { published_at: desc }, }); Astro.cache.set(cacheHint); --- {entries.map(post ( a href{/posts/${post.id}}{post.data.title}/a ))} {nextCursor a href{?cursor${nextCursor}}Next page/a}游標(biāo)是不透明字符串由查詢層按“排序值 數(shù)據(jù)庫(kù) ID”編碼encodeEntryCursor見(jiàn) query.ts編碼時(shí)對(duì)日期列優(yōu)先使用原始存儲(chǔ)字符串以避免時(shí)區(qū)/精度損耗因此翻頁(yè)穩(wěn)定、不受新插入數(shù)據(jù)影響。偏移分頁(yè)用于/page/2這類編號(hào)歸檔結(jié)果中的hasMore表示是否還有后續(xù)條目適合渲染“下一頁(yè)”鏈接而無(wú)需計(jì)算總數(shù)const perPage 20; const { entries, hasMore } await getEmDashCollection(posts, { limit: perPage, offset: (page - 1) * perPage, orderBy: { published_at: desc }, });兩種方式下只要傳了limit結(jié)果都會(huì)附帶hasMore字段默認(rèn)limit 1探測(cè)法判斷見(jiàn) query.ts。日期格式化查詢返回的日期字段createdAt、updatedAt、publishedAt都是Date對(duì)象直接用toLocaleDateString或Intl.DateTimeFormat格式化const formatted post.data.publishedAt?.toLocaleDateString(en-US, { year: numeric, month: long, day: numeric, });注意publishedAt可能為null未發(fā)布取值時(shí)要先判空如可選鏈?.。實(shí)戰(zhàn)要點(diǎn)小結(jié)所有查詢函數(shù)從emdash導(dǎo)入類型由生成到站點(diǎn)里的emdash-env.d.ts提供EmDashCollections接口擴(kuò)展后集合名與字段自動(dòng)獲得類型推斷見(jiàn) query.ts拼錯(cuò)集合名或字段名會(huì)得到編譯期錯(cuò)誤每次查詢后都調(diào)用Astro.cache.set(cacheHint)保證內(nèi)容變更自動(dòng)失效緩存URL 用entry.idslug數(shù)據(jù)庫(kù) API如getEntryTerms用entry.data.idULIDCMS 圖片必須用Image組件傳對(duì)象不要直接src{對(duì)象}富文本用PortableText自定義 CMS 區(qū)塊通過(guò)components{{ type: {...} }}注入管理端登錄后把post.edit.*展開(kāi)到元素上即獲得行內(nèi)點(diǎn)選編輯能力列表、詳情、歸檔、RSS、404、空狀態(tài)、分頁(yè)、日期格式化可直接復(fù)制上面的代碼模式改造到自己的站點(diǎn)。贊分享CMS后端前端插件系統(tǒng)【免費(fèi)下載鏈接】emdashEmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress項(xiàng)目地址https://gitcode.com/gh_mirrors/emdas/emdash點(diǎn)擊查看免費(fèi)下載相關(guān)推薦EmDash 內(nèi)容查詢與渲染實(shí)戰(zhàn)從 getEmDashCollection 到 Portable Text 的完整指南EmDash 內(nèi)容查詢與渲染實(shí)戰(zhàn)從 getEmDashCollection 到 Portable Text 的完整指南 這篇技術(shù)指南圍繞 EmDash 前端模CMS后端前端插件系統(tǒng)EmDash 內(nèi)容查詢與渲染實(shí)戰(zhàn)指南getEmDashCollection、Portable Text 與可視化編輯EmDash 內(nèi)容查詢與渲染實(shí)戰(zhàn)指南getEmDashCollection、Portable Text 與可視化編輯 EmDash 是一個(gè)基于 Astro 的CMS后端前端插件系統(tǒng)EmDash 內(nèi)容查詢與渲染實(shí)戰(zhàn)指南getEmDashCollection、PortableText 與常見(jiàn)頁(yè)面模式EmDash 內(nèi)容查詢與渲染實(shí)戰(zhàn)指南getEmDashCollection、PortableText 與常見(jiàn)頁(yè)面模式 導(dǎo)讀 本文基于 EmDash 官方站點(diǎn)構(gòu)CMS后端前端插件系統(tǒng)創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考