📄 Huge intl feature
Some checks failed
Some checks failed
This commit was merged in pull request #10.
This commit is contained in:
@@ -33,7 +33,7 @@ export function calculateReadingTime(content: string): number {
|
||||
return Math.ceil(words / wordsPerMinute)
|
||||
}
|
||||
|
||||
export function validateFrontmatter(data: any): FrontMatter {
|
||||
export function validateFrontmatter(data: any, locale?: string): FrontMatter {
|
||||
if (!data.title || typeof data.title !== 'string') {
|
||||
throw new Error('Invalid title')
|
||||
}
|
||||
@@ -60,15 +60,19 @@ export function validateFrontmatter(data: any): FrontMatter {
|
||||
author: data.author,
|
||||
category: data.category,
|
||||
tags: data.tags,
|
||||
locale: data.locale || locale || 'en',
|
||||
image: data.image,
|
||||
draft: data.draft || false,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPostBySlug(slug: string | string[]): Promise<Post | null> {
|
||||
export async function getPostBySlug(
|
||||
slug: string | string[],
|
||||
locale: string = 'en'
|
||||
): Promise<Post | null> {
|
||||
const slugArray = Array.isArray(slug) ? slug : slug.split('/')
|
||||
const sanitized = slugArray.map(s => sanitizePath(s))
|
||||
const fullPath = path.join(POSTS_PATH, ...sanitized) + '.md'
|
||||
const fullPath = path.join(POSTS_PATH, locale, ...sanitized) + '.md'
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
return null
|
||||
@@ -76,7 +80,7 @@ export async function getPostBySlug(slug: string | string[]): Promise<Post | nul
|
||||
|
||||
const fileContents = fs.readFileSync(fullPath, 'utf8')
|
||||
const { data, content } = matter(fileContents)
|
||||
const frontmatter = validateFrontmatter(data)
|
||||
const frontmatter = validateFrontmatter(data, locale)
|
||||
|
||||
const processed = await remark()
|
||||
.use(remarkGfm)
|
||||
@@ -85,13 +89,14 @@ export async function getPostBySlug(slug: string | string[]): Promise<Post | nul
|
||||
publicDir: 'public/blog',
|
||||
currentSlug: sanitized.join('/'),
|
||||
})
|
||||
.use(remarkInternalLinks)
|
||||
.use(remarkInternalLinks, { locale })
|
||||
.process(content)
|
||||
|
||||
const processedContent = processed.toString()
|
||||
|
||||
return {
|
||||
slug: sanitized.join('/'),
|
||||
locale,
|
||||
frontmatter,
|
||||
content: processedContent,
|
||||
readingTime: calculateReadingTime(processedContent),
|
||||
@@ -99,8 +104,14 @@ export async function getPostBySlug(slug: string | string[]): Promise<Post | nul
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllPosts(includeContent = false): Promise<Post[]> {
|
||||
export async function getAllPosts(locale: string = 'en', includeContent = false): Promise<Post[]> {
|
||||
const posts: Post[] = []
|
||||
const localeDir = path.join(POSTS_PATH, locale)
|
||||
|
||||
if (!fs.existsSync(localeDir)) {
|
||||
console.warn(`Locale directory not found: ${localeDir}`)
|
||||
return []
|
||||
}
|
||||
|
||||
async function walkDir(dir: string, prefix = ''): Promise<void> {
|
||||
const files = fs.readdirSync(dir)
|
||||
@@ -114,7 +125,7 @@ export async function getAllPosts(includeContent = false): Promise<Post[]> {
|
||||
} else if (file.endsWith('.md')) {
|
||||
const slug = prefix ? `${prefix}/${file.replace(/\.md$/, '')}` : file.replace(/\.md$/, '')
|
||||
try {
|
||||
const post = await getPostBySlug(slug.split('/'))
|
||||
const post = await getPostBySlug(slug.split('/'), locale)
|
||||
if (post && !post.frontmatter.draft) {
|
||||
posts.push(includeContent ? post : { ...post, content: '' })
|
||||
}
|
||||
@@ -125,20 +136,22 @@ export async function getAllPosts(includeContent = false): Promise<Post[]> {
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync(POSTS_PATH)) {
|
||||
await walkDir(POSTS_PATH)
|
||||
}
|
||||
await walkDir(localeDir)
|
||||
|
||||
return posts.sort(
|
||||
(a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime()
|
||||
)
|
||||
}
|
||||
|
||||
export async function getRelatedPosts(currentSlug: string, limit = 3): Promise<Post[]> {
|
||||
const currentPost = await getPostBySlug(currentSlug)
|
||||
export async function getRelatedPosts(
|
||||
currentSlug: string,
|
||||
locale: string = 'en',
|
||||
limit = 3
|
||||
): Promise<Post[]> {
|
||||
const currentPost = await getPostBySlug(currentSlug, locale)
|
||||
if (!currentPost) return []
|
||||
|
||||
const allPosts = await getAllPosts(false)
|
||||
const allPosts = await getAllPosts(locale, false)
|
||||
const { category, tags } = currentPost.frontmatter
|
||||
|
||||
const scored = allPosts
|
||||
@@ -155,8 +168,13 @@ export async function getRelatedPosts(currentSlug: string, limit = 3): Promise<P
|
||||
return scored.slice(0, limit).map(({ post }) => post)
|
||||
}
|
||||
|
||||
export function getAllPostSlugs(): string[][] {
|
||||
export function getAllPostSlugs(locale: string = 'en'): string[][] {
|
||||
const slugs: string[][] = []
|
||||
const localeDir = path.join(POSTS_PATH, locale)
|
||||
|
||||
if (!fs.existsSync(localeDir)) {
|
||||
return []
|
||||
}
|
||||
|
||||
function walkDir(dir: string, prefix: string[] = []): void {
|
||||
const files = fs.readdirSync(dir)
|
||||
@@ -173,9 +191,26 @@ export function getAllPostSlugs(): string[][] {
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync(POSTS_PATH)) {
|
||||
walkDir(POSTS_PATH)
|
||||
}
|
||||
walkDir(localeDir)
|
||||
|
||||
return slugs
|
||||
}
|
||||
|
||||
export async function getAvailableLocales(slug: string): Promise<string[]> {
|
||||
const locales = ['en', 'ro']
|
||||
const available: string[] = []
|
||||
|
||||
for (const locale of locales) {
|
||||
const post = await getPostBySlug(slug, locale)
|
||||
if (post) {
|
||||
available.push(locale)
|
||||
}
|
||||
}
|
||||
|
||||
return available
|
||||
}
|
||||
|
||||
export async function getPostCount(locale: string): Promise<number> {
|
||||
const posts = await getAllPosts(locale, false)
|
||||
return posts.length
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ interface LinkNode extends Node {
|
||||
children: Node[]
|
||||
}
|
||||
|
||||
interface Options {
|
||||
locale?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects internal blog post links:
|
||||
* - Relative paths (no http/https)
|
||||
@@ -24,11 +28,11 @@ function isInternalBlogLink(url: string): boolean {
|
||||
|
||||
/**
|
||||
* Transforms internal .md links to blog routes:
|
||||
* - tech/article.md → /blog/tech/article
|
||||
* - article.md#section → /blog/article#section
|
||||
* - nested/path/post.md?ref=foo → /blog/nested/path/post?ref=foo
|
||||
* - tech/article.md → /[locale]/blog/tech/article
|
||||
* - article.md#section → /[locale]/blog/article#section
|
||||
* - nested/path/post.md?ref=foo → /[locale]/blog/nested/path/post?ref=foo
|
||||
*/
|
||||
function transformToBlogPath(url: string): string {
|
||||
function transformToBlogPath(url: string, locale: string = 'en'): string {
|
||||
// Split into path, hash, and query
|
||||
const hashIndex = url.indexOf('#')
|
||||
const queryIndex = url.indexOf('?')
|
||||
@@ -50,17 +54,19 @@ function transformToBlogPath(url: string): string {
|
||||
// Remove .md extension
|
||||
const cleanPath = path.replace(/\.md$/, '')
|
||||
|
||||
// Build final URL
|
||||
return `/blog/${cleanPath}${query}${hash}`
|
||||
// Build final URL with locale prefix
|
||||
return `/${locale}/blog/${cleanPath}${query}${hash}`
|
||||
}
|
||||
|
||||
export function remarkInternalLinks() {
|
||||
export function remarkInternalLinks(options: Options = {}) {
|
||||
const locale = options.locale || 'en'
|
||||
|
||||
return (tree: Node) => {
|
||||
visit(tree, 'link', (node: Node) => {
|
||||
const linkNode = node as LinkNode
|
||||
|
||||
if (isInternalBlogLink(linkNode.url)) {
|
||||
linkNode.url = transformToBlogPath(linkNode.url)
|
||||
linkNode.url = transformToBlogPath(linkNode.url, locale)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
24
lib/tags.ts
24
lib/tags.ts
@@ -25,8 +25,8 @@ export function slugifyTag(tag: string): string {
|
||||
.replace(/^-|-$/g, '')
|
||||
}
|
||||
|
||||
export async function getAllTags(): Promise<TagInfo[]> {
|
||||
const posts = await getAllPosts()
|
||||
export async function getAllTags(locale: string = 'en'): Promise<TagInfo[]> {
|
||||
const posts = await getAllPosts(locale)
|
||||
const tagMap = new Map<string, number>()
|
||||
|
||||
posts.forEach(post => {
|
||||
@@ -46,8 +46,8 @@ export async function getAllTags(): Promise<TagInfo[]> {
|
||||
.sort((a, b) => b.count - a.count)
|
||||
}
|
||||
|
||||
export async function getPostsByTag(tagSlug: string): Promise<Post[]> {
|
||||
const posts = await getAllPosts()
|
||||
export async function getPostsByTag(tagSlug: string, locale: string = 'en'): Promise<Post[]> {
|
||||
const posts = await getAllPosts(locale)
|
||||
|
||||
return posts.filter(post => {
|
||||
const tags = post.frontmatter.tags?.filter(Boolean) || []
|
||||
@@ -55,18 +55,18 @@ export async function getPostsByTag(tagSlug: string): Promise<Post[]> {
|
||||
})
|
||||
}
|
||||
|
||||
export async function getTagInfo(tagSlug: string): Promise<TagInfo | null> {
|
||||
const allTags = await getAllTags()
|
||||
export async function getTagInfo(tagSlug: string, locale: string = 'en'): Promise<TagInfo | null> {
|
||||
const allTags = await getAllTags(locale)
|
||||
return allTags.find(tag => tag.slug === tagSlug) || null
|
||||
}
|
||||
|
||||
export async function getPopularTags(limit = 10): Promise<TagInfo[]> {
|
||||
const allTags = await getAllTags()
|
||||
export async function getPopularTags(locale: string = 'en', limit = 10): Promise<TagInfo[]> {
|
||||
const allTags = await getAllTags(locale)
|
||||
return allTags.slice(0, limit)
|
||||
}
|
||||
|
||||
export async function getRelatedTags(tagSlug: string, limit = 5): Promise<TagInfo[]> {
|
||||
const posts = await getPostsByTag(tagSlug)
|
||||
export async function getRelatedTags(tagSlug: string, locale: string = 'en', limit = 5): Promise<TagInfo[]> {
|
||||
const posts = await getPostsByTag(tagSlug, locale)
|
||||
const relatedTagMap = new Map<string, number>()
|
||||
|
||||
posts.forEach(post => {
|
||||
@@ -107,8 +107,8 @@ export function validateTags(tags: any): string[] {
|
||||
return validTags
|
||||
}
|
||||
|
||||
export async function getTagCloud(): Promise<Array<TagInfo & { size: 'sm' | 'md' | 'lg' | 'xl' }>> {
|
||||
const tags = await getAllTags()
|
||||
export async function getTagCloud(locale: string = 'en'): Promise<Array<TagInfo & { size: 'sm' | 'md' | 'lg' | 'xl' }>> {
|
||||
const tags = await getAllTags(locale)
|
||||
if (tags.length === 0) return []
|
||||
|
||||
const maxCount = Math.max(...tags.map(t => t.count))
|
||||
|
||||
@@ -5,12 +5,14 @@ export interface FrontMatter {
|
||||
author: string
|
||||
category: string
|
||||
tags: string[]
|
||||
locale: string
|
||||
image?: string
|
||||
draft?: boolean
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
slug: string
|
||||
locale: string
|
||||
frontmatter: FrontMatter
|
||||
content: string
|
||||
readingTime: number
|
||||
|
||||
Reference in New Issue
Block a user