feat(cms): 内容管理闭环——站点单例三件套+文章视频字段
- A 站点文案单例:content/site/{home,about,opensource}.yaml +
src/lib/site-content.ts 访问层(字段级回落默认,默认值=原文案逐字
一致,零回归);接线主页 tagline(en 首页新增该块)、关于页 intro、
开源页叙述三段 + PINNED 参数化(projects.ts: loadProjects(pinned)
/orderPinned,PINNED_DEFAULT 仅缺省兜底)
- loader 语义修正(返工如实入档):Astro 7 的 file() 把 YAML 顶层对象
按 Object.entries 拆成多条目(id=key、data=value),单例字段必炸
schema;改 glob 精确到单文件 = 真单例语义,data 即文件根对象,
Sveltia 可编辑根字段零嵌套(比包 main: 层的方案后台体验更好)
- B 文章媒体:posts +video/+poster;Sveltia file 字段
(media_folder=public/media、public_folder=/media、hint ≤50MB);
正文后渲染 video controls preload=metadata playsinline,无则不渲染;
图片链路核对本已一致(./assets 走 Astro 管线);链接 markdown 原生
- docs/写作指南.md 补「后台上传」节(三类媒体 + 三单例操作 + 体积纪律)
Sveltia 集合:posts / moments / photos / site_pages(三单例)
车道:op-gitea(实测 tagline 改 yaml 产物联动、置空回落默认、pinned
改名排序变、视频临时件渲染全对、构建绿 + lint 45 页 0/0/0);收编
复验:构建 44 页 + E2E 37/37 + 三 yaml 扁平键就位
验证: pnpm astro build 44 页 exit 0;E2E 37/37 PASS;content-lint
0/0/0;播放器/单例/置顶 playwright 实测全过
Spec: none (内容管理闭环,车道级交付)
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
5be09b6f32
commit
264148f0a6
15 files changed
+346
-12
No files matched your search
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import LinkCardStyles from '../../components/geek/LinkCardStyles.astro';
|
||||
import PhotoZoom from '../../components/geek/PhotoZoom.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import ArtalkComments from '../../components/ArtalkComments.astro';
|
||||
@@ -63,6 +64,14 @@ const toc = headings.filter((h) => h.depth === 2 || h.depth === 3);
|
||||
</header>
|
||||
<div class="prose-reading">
|
||||
<Content />
|
||||
{entry.data.video && (
|
||||
<figure class="post-video">
|
||||
<video controls preload="metadata" playsinline poster={entry.data.poster}>
|
||||
<source src={entry.data.video} />
|
||||
你的浏览器不支持内嵌视频,<a href={entry.data.video}>点此下载观看</a>。
|
||||
</video>
|
||||
</figure>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
) : (
|
||||
@@ -83,6 +92,14 @@ const toc = headings.filter((h) => h.depth === 2 || h.depth === 3);
|
||||
<div class="tech-grid">
|
||||
<div class="prose-tech">
|
||||
<Content />
|
||||
{entry.data.video && (
|
||||
<figure class="post-video">
|
||||
<video controls preload="metadata" playsinline poster={entry.data.poster}>
|
||||
<source src={entry.data.video} />
|
||||
你的浏览器不支持内嵌视频,<a href={entry.data.video}>点此下载观看</a>。
|
||||
</video>
|
||||
</figure>
|
||||
)}
|
||||
</div>
|
||||
{toc.length > 0 && (
|
||||
<aside class="toc" aria-label="目录">
|
||||
@@ -117,6 +134,7 @@ const toc = headings.filter((h) => h.depth === 2 || h.depth === 3);
|
||||
</nav>
|
||||
|
||||
<ArtalkComments pageKey={`/archives/${entry.id}/`} pageTitle={entry.data.title} />
|
||||
<LinkCardStyles />
|
||||
<PhotoZoom />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -335,6 +353,17 @@ const toc = headings.filter((h) => h.depth === 2 || h.depth === 3);
|
||||
/* ---- T6 书评纸卡(08 裁定 §6-2):通栏 hairline 升圆角纸卡 ----
|
||||
--paper 三态已入 tokens(亮米白/暗暖棕夜灯),零新变量;7000px 长文
|
||||
左右边框恒为 1px hairline 防两根竖线感(§6-2 落地注意) */
|
||||
/* 文章附视频(cms 车道 B):等宽自适应、serve 源不在站内 */
|
||||
.post-video {
|
||||
margin: var(--space-4) 0 0;
|
||||
}
|
||||
.post-video video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
background: #000;
|
||||
display: block;
|
||||
}
|
||||
.prose-reading {
|
||||
border: 1px solid var(--paper-line);
|
||||
border-radius: 12px;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// /en/archives/{slug}:英文版文章页——postsEn 优先;缺译回退 zh 正文 + 醒目标注
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import LinkCardStyles from '../../../components/geek/LinkCardStyles.astro';
|
||||
import PhotoZoom from '../../../components/geek/PhotoZoom.astro';
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import ArtalkComments from '../../../components/ArtalkComments.astro';
|
||||
@@ -61,6 +62,14 @@ const minutes = Math.max(1, Math.ceil(words / 220));
|
||||
<AiTranslatedBadge />
|
||||
<div class={isReading ? 'prose-reading' : 'prose-tech'}>
|
||||
<Content />
|
||||
{entry.data.video && (
|
||||
<figure class="post-video">
|
||||
<video controls preload="metadata" playsinline poster={entry.data.poster}>
|
||||
<source src={entry.data.video} />
|
||||
Your browser cannot play embedded video — <a href={entry.data.video}>download it instead</a>.
|
||||
</video>
|
||||
</figure>
|
||||
)}
|
||||
</div>
|
||||
<nav class="pn">
|
||||
{prev && <a class="pn-item" href={`/en/archives/${prev.id}/`}>← {t('prev_post', 'en')} · {prev.data.title}</a>}
|
||||
@@ -68,6 +77,7 @@ const minutes = Math.max(1, Math.ceil(words / 220));
|
||||
</nav>
|
||||
<ArtalkComments pageKey={`/archives/${entry.id}/`} pageTitle={entry.data.title} />
|
||||
</article>
|
||||
<LinkCardStyles />
|
||||
<PhotoZoom />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -116,6 +126,17 @@ const minutes = Math.max(1, Math.ceil(words / 220));
|
||||
}
|
||||
|
||||
/* ---- T6 reading paper card (§6-2) ---- */
|
||||
/* 文章附视频:与 zh 版同构(cms 车道 B) */
|
||||
.post-video {
|
||||
margin: var(--space-4) 0 0;
|
||||
}
|
||||
.post-video video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
background: #000;
|
||||
display: block;
|
||||
}
|
||||
.prose-reading {
|
||||
border: 1px solid var(--paper-line);
|
||||
border-radius: 12px;
|
||||
|
||||
@@ -8,6 +8,7 @@ import PostList from '../../components/PostList.astro';
|
||||
import ProjectWall from '../../components/geek/ProjectWall.astro';
|
||||
import Heatmap from '../../components/geek/Heatmap.astro';
|
||||
import { t } from '../../i18n/ui';
|
||||
import { homeTagline } from '../../lib/site-content';
|
||||
|
||||
const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
|
||||
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
||||
@@ -17,10 +18,13 @@ const enTitle = Object.fromEntries(
|
||||
(await getCollection('postsEn')).map((e) => [e.id, e.data.title]),
|
||||
);
|
||||
const photos = (await getCollection('photos')).slice(0, 4);
|
||||
// 身份语与 zh 版同源(content/site/home.yaml 的 en 字段),缺值回落默认
|
||||
const tagline = await homeTagline('en');
|
||||
---
|
||||
|
||||
<BaseLayout title="wait4x" description="wait4x's blog: reading · code · photography">
|
||||
<IdCard>
|
||||
<p class="tagline">{tagline}</p>
|
||||
<div class="socials">
|
||||
<a
|
||||
class="tap social"
|
||||
@@ -103,6 +107,12 @@ const photos = (await getCollection('photos')).slice(0, 4);
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.tagline {
|
||||
font-size: 14px;
|
||||
color: var(--ink-3);
|
||||
margin-top: 4px;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.socials {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -3,15 +3,18 @@ import ProjectWall from '../../components/geek/ProjectWall.astro';
|
||||
import Heatmap from '../../components/geek/Heatmap.astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { t } from '../../i18n/ui';
|
||||
import { openSourceCopy } from '../../lib/site-content';
|
||||
|
||||
const copy = await openSourceCopy('en');
|
||||
---
|
||||
<BaseLayout title="Open Source · wait4x" description="My open-source activity: repos, contributions, and this site's source">
|
||||
<h1 class="page-title">Open Source</h1>
|
||||
<p class="blurb">This site is itself open source: <a class="link" href="https://git.wait4x.online/wait4x/blog">git.wait4x.online/wait4x/blog</a> — the Astro SSG, dual-writing workflow, and the full build & deploy pipeline live in the repo. Below are all my public repositories on self-hosted gitea and GitHub, plus contributions from the past year.</p>
|
||||
<p class="blurb">{copy.blurb}<a class="link" href={copy.repoUrl}>{copy.repoLabel}</a>{copy.blurbAfter}</p>
|
||||
<Heatmap labels={{
|
||||
title: t('heatmap_title', 'en'), summary: t('heatmap_summary', 'en'),
|
||||
less: t('heatmap_less', 'en'), more: t('heatmap_more', 'en'), note: t('heatmap_note', 'en'),
|
||||
}} />
|
||||
<ProjectWall limit={99} locale="en" />
|
||||
<ProjectWall limit={99} locale="en" pinned={copy.pinned} />
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -8,6 +8,7 @@ import ProjectWall from '../components/geek/ProjectWall.astro';
|
||||
import Heatmap from '../components/geek/Heatmap.astro';
|
||||
import { fetchHeartbeat, agoZh } from '../lib/heartbeat';
|
||||
import { t } from '../i18n/ui';
|
||||
import { homeTagline } from '../lib/site-content';
|
||||
|
||||
const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
|
||||
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
||||
@@ -15,11 +16,13 @@ const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.t
|
||||
// 最新影像取集合前 4 张;照片时间序由照片管线(exif 车道)治理,此处只裁切
|
||||
const photos = (await getCollection('photos')).slice(0, 4);
|
||||
const heartbeat = await fetchHeartbeat();
|
||||
// 身份语改由 content/site/home.yaml 供给(后台可改),缺值回落默认文案
|
||||
const tagline = await homeTagline('zh');
|
||||
---
|
||||
|
||||
<BaseLayout title="wait4x">
|
||||
<IdCard>
|
||||
<p class="tagline">写作者 · 读者 · 快门按压者,拥抱开源。</p>
|
||||
<p class="tagline">{tagline}</p>
|
||||
<div class="socials">
|
||||
<a
|
||||
class="tap social"
|
||||
|
||||
Reference in new issue
Block a user