Update blog CTA and blog content handling

This commit is contained in:
Haitao Pan 2025-12-22 19:55:50 +08:00
parent dbf50e68d5
commit 5dcdf9a747
8 changed files with 108 additions and 10 deletions

View File

@ -73,7 +73,6 @@ jobs:
matrix:
service:
- { name: dashboard, workdir: ., dockerfile: Dockerfile }
- { name: neurapress, workdir: ., dockerfile: packages/neurapress/docker/Dockerfile.prod }
steps:
# -------------------------------------------------------------

View File

@ -59,8 +59,8 @@ ensure-deps:
fi
dev: ensure-deps
@echo "🚀 Starting Next.js dev server (dashboard)..."
@echo " /editor is proxied to an external NeuraPress front-end at http://localhost:4000."
@echo "Starting Next.js dev server (dashboard)..."
@echo "/editor is proxied to an external NeuraPress front-end at http://localhost:4000."
yarn dev -p 3001
start:

BIN
public/icons/webchat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -6,6 +6,7 @@ import type { Metadata } from 'next'
import { getBlogPostBySlug } from '@lib/blogContent'
import { renderMarkdownContent } from '@server/render-markdown'
import BrandCTA from '@components/BrandCTA'
type PageProps = {
params: { slug: string | string[] }
@ -54,6 +55,7 @@ export default async function BlogPostPage({ params }: PageProps) {
}
const html = renderMarkdownContent(post.content)
const language: 'zh' | 'en' = /[\u4e00-\u9fff]/.test(`${post.title} ${post.content}`) ? 'zh' : 'en'
return (
<main className="flex min-h-screen flex-col bg-slate-50">
@ -86,10 +88,14 @@ export default async function BlogPostPage({ params }: PageProps) {
</header>
<article
className="prose prose-slate max-w-none prose-headings:scroll-mt-24 prose-a:text-brand prose-a:no-underline hover:prose-a:underline"
className="prose prose-slate max-w-none text-[15px] prose-headings:scroll-mt-24 prose-a:text-brand prose-a:no-underline hover:prose-a:underline"
dangerouslySetInnerHTML={{ __html: html }}
/>
<div className="mt-12">
<BrandCTA lang={language} />
</div>
<footer className="mt-16 border-t border-slate-200 pt-8">
<Link
href="/blog"

View File

@ -0,0 +1,46 @@
import Image from 'next/image'
type BrandCTAProps = {
lang?: 'zh' | 'en'
variant?: 'compact' | 'default'
}
const COPY = {
zh: {
main: '云原生实践 · 架构思考',
secondary: '获取更多信息,可通过右侧官方渠道',
},
en: {
main: 'Cloud-native practice · Architecture thinking',
secondary: 'For more information, see the official channels on the right',
},
}
export default function BrandCTA({ lang = 'en', variant = 'default' }: BrandCTAProps) {
const content = COPY[lang]
const isCompact = variant === 'compact'
const imageSize = isCompact ? 160 : 180
return (
<section className={`flex items-center border-t border-slate-200 ${isCompact ? 'pt-3' : 'pt-4'}`}>
<div className="flex-1 text-left">
<p className="text-sm font-medium text-slate-600">{content.main}</p>
{!isCompact && (
<>
<div className="h-3" aria-hidden="true" />
<p className="text-xs text-slate-500">{content.secondary}</p>
</>
)}
</div>
<div className="ml-6 flex justify-end">
<Image
src="/icons/webchat.jpg"
alt={lang === 'zh' ? 'Cloud-Neutral 微信二维码' : 'Cloud-Neutral WeChat QR code'}
width={imageSize}
height={imageSize}
className="h-auto w-auto"
/>
</div>
</section>
)
}

View File

@ -232,7 +232,7 @@ export default function Navbar() {
const labels = {
home: isChinese ? '首页' : 'Home',
docs: isChinese ? '文档' : 'Docs',
download: isChinese ? '下载' : 'Download',
download: isChinese ? '博客' : 'blog',
openSource: isChinese ? '开源项目' : 'Open source',
editor: isChinese ? '编辑器' : 'Editor',
moreServices: isChinese ? '更多服务' : 'More services',
@ -273,7 +273,7 @@ export default function Navbar() {
{ key: 'docs', label: labels.docs, href: '/docs' },
]
const downloadLink = { key: 'download', label: labels.download, href: '/download' }
const downloadLink = { key: 'blog', label: labels.download, href: '/blog' }
const editorLink = {
key: 'editor',

View File

@ -4,6 +4,7 @@ import { useMemo } from 'react'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import BrandCTA from '@components/BrandCTA'
import SearchComponent from '@components/search'
import type { BlogPost } from '@lib/blogContent'
@ -50,6 +51,15 @@ function buildCategoryCounts(posts: BlogPost[]) {
}, {})
}
function detectLanguage(posts: BlogPost[]): 'zh' | 'en' {
for (const post of posts) {
if (/[\u4e00-\u9fff]/.test(`${post.title} ${post.excerpt}`)) {
return 'zh'
}
}
return 'en'
}
export default function BlogList({ posts }: BlogListProps) {
const searchParams = useSearchParams()
const selectedCategory = searchParams.get('category')
@ -72,6 +82,7 @@ export default function BlogList({ posts }: BlogListProps) {
const startIndex = (currentPage - 1) * postsPerPage
const endIndex = startIndex + postsPerPage
const paginatedPosts = filteredPosts.slice(startIndex, endIndex)
const language = useMemo(() => detectLanguage(filteredPosts), [filteredPosts])
return (
<div className="bg-white text-slate-900">
@ -231,8 +242,13 @@ export default function BlogList({ posts }: BlogListProps) {
</Link>
</nav>
)}
</>
)}
<div className="mt-12">
<BrandCTA lang={language} variant="compact" />
</div>
</div>
</main>
</div>

View File

@ -73,14 +73,45 @@ function buildExcerpt(markdown: string): string {
return ''
}
function normalizeHeadingText(value: string): string {
return value
.replace(/[`*_]/g, '')
.replace(/\[(.*?)\]\((.*?)\)/g, '$1')
.trim()
}
function extractTitleFromContent(markdown: string): { title?: string; content: string } {
const lines = markdown.split(/\r?\n/)
let firstContentLine = 0
while (firstContentLine < lines.length && lines[firstContentLine].trim() === '') {
firstContentLine += 1
}
if (firstContentLine < lines.length) {
const match = lines[firstContentLine].match(/^#{1,6}\s+(.*)$/)
if (match && match[1]?.trim()) {
const title = normalizeHeadingText(match[1])
const content = [...lines.slice(0, firstContentLine), ...lines.slice(firstContentLine + 1)].join('\n')
return { title, content }
}
}
return { content: markdown }
}
function normalizePost(file: Awaited<ReturnType<typeof readBlogFiles>>[number]): BlogPost {
const title = typeof file.metadata.title === 'string' ? file.metadata.title : file.slug
const metadataTitle = typeof file.metadata.title === 'string' ? file.metadata.title : undefined
const { title: derivedTitle, content } = metadataTitle
? { title: undefined, content: file.content }
: extractTitleFromContent(file.content)
const title = metadataTitle ?? derivedTitle ?? file.slug
const author = typeof file.metadata.author === 'string' ? file.metadata.author : undefined
const date = typeof file.metadata.date === 'string' ? file.metadata.date : undefined
const tags = Array.isArray(file.metadata.tags)
? file.metadata.tags.filter((tag): tag is string => typeof tag === 'string')
: []
const excerpt = typeof file.metadata.excerpt === 'string' ? file.metadata.excerpt : buildExcerpt(file.content)
const excerpt = typeof file.metadata.excerpt === 'string' ? file.metadata.excerpt : buildExcerpt(content)
const categoryKey = typeof file.metadata.category === 'string' ? file.metadata.category : undefined
const categoryLabel = typeof file.metadata.categoryLabel === 'string' ? file.metadata.categoryLabel : categoryKey
const category =
@ -95,7 +126,7 @@ function normalizePost(file: Awaited<ReturnType<typeof readBlogFiles>>[number]):
date,
tags,
excerpt,
content: file.content,
content,
category,
}
}
@ -103,7 +134,7 @@ function normalizePost(file: Awaited<ReturnType<typeof readBlogFiles>>[number]):
export const getBlogPosts = cache(async (): Promise<BlogPost[]> => {
try {
const files = await readBlogFiles()
const posts = files.map(normalizePost)
const posts = files.map(normalizePost).filter((post) => post.content.trim().length > 0)
return posts
.map((post) => ({