Browse Source

feat(public): post lists use front matter desc, strip FM in article body

Made-with: Cursor
main
npmrun 7 hours ago
parent
commit
660a2e35c0
  1. 8
      app/pages/@[publicSlug]/index.vue
  2. 17
      app/pages/@[publicSlug]/posts/[postSlug].vue
  3. 3
      app/pages/@[publicSlug]/posts/index.vue
  4. 57
      app/utils/markdown-front-matter.ts
  5. 14
      server/service/posts/index.ts

8
app/pages/@[publicSlug]/index.vue

@ -209,7 +209,7 @@ const bioHtml = computed(() =>
>
<NuxtLink
:to="`/@${slug}/posts/${encodeURIComponent(p.slug)}`"
class="block p-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-lg"
class="group block p-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-lg"
>
<time
v-if="p.publishedAt"
@ -222,6 +222,9 @@ const bioHtml = computed(() =>
<div v-if="p.excerpt" class="text-sm text-muted mt-1">
{{ p.excerpt }}
</div>
<div class="mt-1.5 text-sm font-medium text-primary group-hover:underline">
查看全文
</div>
</NuxtLink>
</li>
</ul>
@ -499,6 +502,9 @@ const bioHtml = computed(() =>
>
{{ p.excerpt }}
</p>
<p class="mt-2 text-sm font-medium text-primary group-hover:underline">
查看全文
</p>
</div>
</NuxtLink>
</li>

17
app/pages/@[publicSlug]/posts/[postSlug].vue

@ -1,5 +1,6 @@
<script setup lang="ts">
import { unwrapApiBody, type ApiResponse } from '../../../utils/http/factory'
import { extractFrontMatterDesc, stripFrontMatter } from '../../../utils/markdown-front-matter'
import { renderSafeMarkdown } from '../../../utils/render-markdown'
import { formatOccurredOnDisplay, occurredOnToIsoAttr } from '../../../utils/timeline-datetime'
import { useAuthSession } from '../../../composables/useAuthSession'
@ -33,7 +34,17 @@ const { data, pending, error } = await useAsyncData(
{ watch: [publicSlug, postSlug] },
)
const renderedBody = computed(() => (data.value ? renderSafeMarkdown(data.value.bodyMarkdown) : ''))
const renderedBody = computed(() =>
data.value ? renderSafeMarkdown(stripFrontMatter(data.value.bodyMarkdown)) : '',
)
const leadSummary = computed(() => {
if (!data.value) {
return ''
}
const fm = extractFrontMatterDesc(data.value.bodyMarkdown)
return (fm || data.value.excerpt || '').trim()
})
const publishedAtLabel = computed(() =>
data.value?.publishedAt != null ? formatOccurredOnDisplay(data.value.publishedAt) : '',
@ -102,8 +113,8 @@ const editPostHref = computed(() =>
<h1 class="text-2xl font-semibold">
{{ data.title }}
</h1>
<p v-if="data.excerpt" class="text-muted">
{{ data.excerpt }}
<p v-if="leadSummary" class="text-muted">
{{ leadSummary }}
</p>
<article
class="prose dark:prose-invert max-w-none prose-a:text-primary prose-img:rounded-lg"

3
app/pages/@[publicSlug]/posts/index.vue

@ -131,6 +131,9 @@ const { data, pending, error } = await useAsyncData(
>
{{ p.excerpt }}
</p>
<p class="mt-2 text-sm font-medium text-primary group-hover:underline">
查看全文
</p>
</div>
</NuxtLink>
</li>

57
app/utils/markdown-front-matter.ts

@ -0,0 +1,57 @@
/** 匹配以 --- 开头/结尾的 YAML front matter(全文开头) */
const FRONT_MATTER_RE = /^---[\t ]*\r?\n([\s\S]*?)\r?\n---[\t ]*(?:\r?\n|$)([\s\S]*)$/
export function splitFrontMatter(markdown: string): { front: string | null; body: string } {
const s = markdown.replace(/^\uFEFF/, "")
const m = FRONT_MATTER_RE.exec(s)
if (!m) {
return { front: null, body: markdown }
}
return { front: m[1].trim(), body: m[2] }
}
/** 正文渲染用:去掉 front matter,避免把元数据块排进 HTML */
export function stripFrontMatter(markdown: string): string {
return splitFrontMatter(markdown).body
}
function pickYamlScalar(block: string, key: string): string {
const lines = block.split(/\r?\n/)
const prefix = `${key}:`
for (const line of lines) {
const t = line.trim()
if (!t.startsWith(prefix)) {
continue
}
let v = t.slice(prefix.length).trim()
if (!v) {
return ""
}
if (
(v.startsWith('"') && v.endsWith('"')) ||
(v.startsWith("'") && v.endsWith("'"))
) {
return v.slice(1, -1)
}
return v
}
return ""
}
/** 列表摘要:优先 `desc`,其次 `description`(简单标量行) */
export function extractFrontMatterDesc(markdown: string): string {
const { front } = splitFrontMatter(markdown)
if (!front) {
return ""
}
const d = pickYamlScalar(front, "desc") || pickYamlScalar(front, "description")
return d.trim()
}
export function listCardSummaryFromPostBody(bodyMarkdown: string, dbExcerpt: string): string {
const fromFm = extractFrontMatterDesc(bodyMarkdown)
if (fromFm) {
return fromFm
}
return (dbExcerpt ?? "").trim()
}

14
server/service/posts/index.ts

@ -9,6 +9,7 @@ import { visibilitySchema, type Visibility } from "#server/constants/visibility"
import { normalizePublicListPage } from "#server/utils/public-pagination";
import { visibilityShareToken } from "#server/utils/share-token";
import { nextIntegerId } from "#server/utils/sqlite-id";
import { listCardSummaryFromPostBody } from "../../../app/utils/markdown-front-matter";
const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,98}[a-z0-9]$|^[a-z0-9]$/;
@ -136,6 +137,7 @@ const publicPostsListSelect = {
slug: posts.slug,
coverUrl: posts.coverUrl,
publishedAt: posts.publishedAt,
bodyMarkdown: posts.bodyMarkdown,
} as const;
function publicPostsListWhere(publicSlug: string) {
@ -157,7 +159,7 @@ export async function getPublicPostsPreviewBySlug(publicSlug: string): Promise<{
total: number;
}> {
const whereClause = publicPostsListWhere(publicSlug);
const [countRows, items] = await Promise.all([
const [countRows, rawItems] = await Promise.all([
dbGlobal
.select({ total: count() })
.from(posts)
@ -171,6 +173,10 @@ export async function getPublicPostsPreviewBySlug(publicSlug: string): Promise<{
.orderBy(desc(posts.publishedAt), desc(posts.id))
.limit(PUBLIC_PREVIEW_LIMIT),
]);
const items = rawItems.map(({ bodyMarkdown, excerpt, ...rest }) => ({
...rest,
excerpt: listCardSummaryFromPostBody(bodyMarkdown, excerpt),
}));
return { items, total: countRows[0]?.total ?? 0 };
}
@ -193,7 +199,7 @@ export async function getPublicPostsPageBySlug(
const pageSize = PUBLIC_LIST_PAGE_SIZE;
const offset = (page - 1) * pageSize;
const whereClause = publicPostsListWhere(publicSlug);
const [countRows, items] = await Promise.all([
const [countRows, rawItems] = await Promise.all([
dbGlobal
.select({ total: count() })
.from(posts)
@ -208,6 +214,10 @@ export async function getPublicPostsPageBySlug(
.limit(pageSize)
.offset(offset),
]);
const items = rawItems.map(({ bodyMarkdown, excerpt, ...rest }) => ({
...rest,
excerpt: listCardSummaryFromPostBody(bodyMarkdown, excerpt),
}));
return { items, total: countRows[0]?.total ?? 0, page, pageSize };
}

Loading…
Cancel
Save