|
| 1 | +/* Blog content index — imports all markdown files and provides lookup by slug + language */ |
| 2 | + |
| 3 | +// English posts |
| 4 | +import enIntroducingOcr from './en/introducing-ocr-blog.md'; |
| 5 | + |
| 6 | +// Chinese posts |
| 7 | +import zhIntroducingOcr from './zh/introducing-ocr-blog.md'; |
| 8 | + |
| 9 | +// Japanese posts |
| 10 | +import jaIntroducingOcr from './ja/introducing-ocr-blog.md'; |
| 11 | + |
| 12 | +export type BlogSlug = |
| 13 | + | 'introducing-ocr-blog'; |
| 14 | + |
| 15 | +export interface BlogMeta { |
| 16 | + slug: BlogSlug; |
| 17 | + title: string; |
| 18 | + date: string; |
| 19 | + tags: string[]; |
| 20 | + summary: string; |
| 21 | + author: string; |
| 22 | +} |
| 23 | + |
| 24 | +const enPosts: Record<BlogSlug, string> = { |
| 25 | + 'introducing-ocr-blog': enIntroducingOcr, |
| 26 | +}; |
| 27 | + |
| 28 | +const zhPosts: Record<BlogSlug, string> = { |
| 29 | + 'introducing-ocr-blog': zhIntroducingOcr, |
| 30 | +}; |
| 31 | + |
| 32 | +const jaPosts: Record<BlogSlug, string> = { |
| 33 | + 'introducing-ocr-blog': jaIntroducingOcr, |
| 34 | +}; |
| 35 | + |
| 36 | +const blogMap: Record<string, Record<BlogSlug, string>> = { |
| 37 | + en: enPosts, |
| 38 | + zh: zhPosts, |
| 39 | + ja: jaPosts, |
| 40 | +}; |
| 41 | + |
| 42 | +function stripFrontmatter(md: string): string { |
| 43 | + if (md.startsWith('---')) { |
| 44 | + const end = md.indexOf('---', 3); |
| 45 | + if (end !== -1) { |
| 46 | + return md.slice(end + 3).trim(); |
| 47 | + } |
| 48 | + } |
| 49 | + return md; |
| 50 | +} |
| 51 | + |
| 52 | +function parseFrontmatter(slug: BlogSlug, raw: string): BlogMeta { |
| 53 | + const defaults: BlogMeta = { slug, title: slug, date: '', tags: [], summary: '', author: '' }; |
| 54 | + if (!raw.startsWith('---')) return defaults; |
| 55 | + const end = raw.indexOf('---', 3); |
| 56 | + if (end === -1) return defaults; |
| 57 | + const fm = raw.slice(3, end); |
| 58 | + |
| 59 | + const titleMatch = fm.match(/title:\s*(.+)/); |
| 60 | + const dateMatch = fm.match(/date:\s*(.+)/); |
| 61 | + const summaryMatch = fm.match(/summary:\s*(.+)/); |
| 62 | + const authorMatch = fm.match(/author:\s*(.+)/); |
| 63 | + const tagsMatch = fm.match(/tags:\s*\[([^\]]*)\]/); |
| 64 | + |
| 65 | + return { |
| 66 | + slug, |
| 67 | + title: titleMatch ? titleMatch[1].trim() : slug, |
| 68 | + date: dateMatch ? dateMatch[1].trim() : '', |
| 69 | + tags: tagsMatch ? tagsMatch[1].split(',').map(t => t.trim()).filter(Boolean) : [], |
| 70 | + summary: summaryMatch ? summaryMatch[1].trim() : '', |
| 71 | + author: authorMatch ? authorMatch[1].trim() : '', |
| 72 | + }; |
| 73 | +} |
| 74 | + |
| 75 | +function getRawContent(slug: BlogSlug, language: string): string { |
| 76 | + const langPosts = blogMap[language] || blogMap.en; |
| 77 | + return langPosts[slug] || enPosts[slug] || ''; |
| 78 | +} |
| 79 | + |
| 80 | +export function getBlogContent(slug: BlogSlug, language: string): string { |
| 81 | + return stripFrontmatter(getRawContent(slug, language)); |
| 82 | +} |
| 83 | + |
| 84 | +export function getBlogMeta(slug: BlogSlug, language: string): BlogMeta { |
| 85 | + return parseFrontmatter(slug, getRawContent(slug, language)); |
| 86 | +} |
| 87 | + |
| 88 | +export function getAllBlogMetas(language: string): BlogMeta[] { |
| 89 | + const slugs = Object.keys(blogMap.en) as BlogSlug[]; |
| 90 | + return slugs |
| 91 | + .map(slug => getBlogMeta(slug, language)) |
| 92 | + .sort((a, b) => b.date.localeCompare(a.date)); |
| 93 | +} |
| 94 | + |
| 95 | +export function getAllTags(language: string): string[] { |
| 96 | + const metas = getAllBlogMetas(language); |
| 97 | + const tagSet = new Set<string>(); |
| 98 | + metas.forEach(m => m.tags.forEach(t => tagSet.add(t))); |
| 99 | + return Array.from(tagSet).sort(); |
| 100 | +} |
| 101 | + |
| 102 | +export function searchBlog(query: string, language: string): { slug: BlogSlug; title: string; snippet: string }[] { |
| 103 | + if (!query.trim()) return []; |
| 104 | + const langPosts = blogMap[language] || blogMap.en; |
| 105 | + const results: { slug: BlogSlug; title: string; snippet: string }[] = []; |
| 106 | + const lowerQuery = query.toLowerCase(); |
| 107 | + const slugs = Object.keys(langPosts) as BlogSlug[]; |
| 108 | + for (const slug of slugs) { |
| 109 | + const raw = langPosts[slug] || enPosts[slug] || ''; |
| 110 | + const meta = getBlogMeta(slug, language); |
| 111 | + const content = stripFrontmatter(raw); |
| 112 | + const lowerContent = content.toLowerCase(); |
| 113 | + const lowerTitle = meta.title.toLowerCase(); |
| 114 | + const lowerSummary = (meta.summary || '').toLowerCase(); |
| 115 | + |
| 116 | + let snippet = ''; |
| 117 | + const contentIdx = lowerContent.indexOf(lowerQuery); |
| 118 | + if (lowerTitle.includes(lowerQuery)) { |
| 119 | + snippet = meta.title; |
| 120 | + } else if (lowerSummary.includes(lowerQuery)) { |
| 121 | + snippet = meta.summary || ''; |
| 122 | + } else if (contentIdx !== -1) { |
| 123 | + const start = Math.max(0, contentIdx - 30); |
| 124 | + const end = Math.min(content.length, contentIdx + query.length + 60); |
| 125 | + snippet = content.slice(start, end).replace(/[#*_`\[\]()]/g, '').replace(/\n/g, ' ').trim(); |
| 126 | + if (start > 0) snippet = '...' + snippet; |
| 127 | + if (end < content.length) snippet = snippet + '...'; |
| 128 | + } else { |
| 129 | + continue; |
| 130 | + } |
| 131 | + results.push({ slug, title: meta.title, snippet }); |
| 132 | + } |
| 133 | + return results; |
| 134 | +} |
0 commit comments