Update blog CTA and blog content handling
This commit is contained in:
parent
dbf50e68d5
commit
5dcdf9a747
1
.github/workflows/build-images.yml
vendored
1
.github/workflows/build-images.yml
vendored
@ -73,7 +73,6 @@ jobs:
|
||||
matrix:
|
||||
service:
|
||||
- { name: dashboard, workdir: ., dockerfile: Dockerfile }
|
||||
- { name: neurapress, workdir: ., dockerfile: packages/neurapress/docker/Dockerfile.prod }
|
||||
|
||||
steps:
|
||||
# -------------------------------------------------------------
|
||||
|
||||
4
Makefile
4
Makefile
@ -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
BIN
public/icons/webchat.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@ -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"
|
||||
|
||||
46
src/components/BrandCTA.tsx
Normal file
46
src/components/BrandCTA.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) => ({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user