+
+ {t('nav.blog' as any, lang)}
+
{t('privacy' as any, lang)}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index eba5f65..cd09a22 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -150,5 +150,15 @@
"update.title": "App Updated",
"update.message": "Updated to version {version}",
"update.close": "Close",
- "update.showChangelog": "View Release Notes"
+ "update.showChangelog": "View Release Notes",
+ "blog.title": "Blog",
+ "blog.subtitle": "Tips and guides for using ObatLog",
+ "blog.categoryGuide": "Guides",
+ "blog.categoryTips": "Tips",
+ "blog.readMore": "Read more",
+ "blog.backToList": "← Back to blog",
+ "blog.publishedAt": "Published",
+ "blog.author": "Author",
+ "blog.relatedPosts": "Related posts",
+ "nav.blog": "Blog"
}
diff --git a/src/i18n/id.json b/src/i18n/id.json
index 053b000..276697c 100644
--- a/src/i18n/id.json
+++ b/src/i18n/id.json
@@ -150,5 +150,15 @@
"update.title": "Aplikasi Diperbarui",
"update.message": "Diperbarui ke versi {version}",
"update.close": "Tutup",
- "update.showChangelog": "Lihat Catatan Rilis"
+ "update.showChangelog": "Lihat Catatan Rilis",
+ "blog.title": "Blog",
+ "blog.subtitle": "Tips dan panduan menggunakan ObatLog",
+ "blog.categoryGuide": "Panduan",
+ "blog.categoryTips": "Tips",
+ "blog.readMore": "Baca selengkapnya",
+ "blog.backToList": "← Kembali ke blog",
+ "blog.publishedAt": "Diterbitkan",
+ "blog.author": "Penulis",
+ "blog.relatedPosts": "Artikel terkait",
+ "nav.blog": "Blog"
}
diff --git a/src/i18n/ja.json b/src/i18n/ja.json
index d58aeee..e48c6c9 100644
--- a/src/i18n/ja.json
+++ b/src/i18n/ja.json
@@ -150,5 +150,15 @@
"update.title": "アップデートしました",
"update.message": "バージョン {version} に更新されました",
"update.close": "閉じる",
- "update.showChangelog": "更新履歴を見る"
+ "update.showChangelog": "更新履歴を見る",
+ "blog.title": "ブログ",
+ "blog.subtitle": "ObatLogの使い方やコツをご紹介",
+ "blog.categoryGuide": "使い方ガイド",
+ "blog.categoryTips": "Tips",
+ "blog.readMore": "続きを読む",
+ "blog.backToList": "← ブログ一覧に戻る",
+ "blog.publishedAt": "公開日",
+ "blog.author": "著者",
+ "blog.relatedPosts": "関連記事",
+ "nav.blog": "ブログ"
}
diff --git a/src/lib/blog.ts b/src/lib/blog.ts
new file mode 100644
index 0000000..a6e64ae
--- /dev/null
+++ b/src/lib/blog.ts
@@ -0,0 +1,152 @@
+import fs from 'fs';
+import path from 'path';
+import matter from 'gray-matter';
+import { remark } from 'remark';
+import html from 'remark-html';
+import remarkGfm from 'remark-gfm';
+
+const contentDirectory = path.join(process.cwd(), 'content/blog');
+
+export interface BlogPost {
+ slug: string;
+ title: string;
+ category: 'guide' | 'tips';
+ description: string;
+ publishedAt: string;
+ author: string;
+ content: string;
+}
+
+export interface BlogPostMetadata {
+ slug: string;
+ title: string;
+ category: 'guide' | 'tips';
+ description: string;
+ publishedAt: string;
+ author: string;
+}
+
+/**
+ * Validate frontmatter fields - throws error if invalid
+ */
+function validateFrontmatter(slug: string, data: any): void {
+ const required = ['title', 'category', 'description', 'publishedAt', 'author'];
+ const missing = required.filter(field => !data[field]);
+
+ if (missing.length > 0) {
+ throw new Error(
+ `Invalid frontmatter in ${slug}.md: missing required fields: ${missing.join(', ')}`
+ );
+ }
+
+ if (!['guide', 'tips'].includes(data.category)) {
+ throw new Error(
+ `Invalid frontmatter in ${slug}.md: category must be 'guide' or 'tips', got '${data.category}'`
+ );
+ }
+
+ // Convert Date object to string if needed
+ const dateStr = data.publishedAt instanceof Date
+ ? data.publishedAt.toISOString().split('T')[0]
+ : String(data.publishedAt);
+
+ // Validate date format
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
+ throw new Error(
+ `Invalid frontmatter in ${slug}.md: publishedAt must be in YYYY-MM-DD format, got '${dateStr}'`
+ );
+ }
+
+ // Update the data object with string date
+ data.publishedAt = dateStr;
+}
+
+/**
+ * Get all blog post slugs
+ */
+export function getAllPostSlugs(): string[] {
+ if (!fs.existsSync(contentDirectory)) {
+ return [];
+ }
+
+ const files = fs.readdirSync(contentDirectory);
+ return files
+ .filter(file => file.endsWith('.md'))
+ .map(file => file.replace(/\.md$/, ''));
+}
+
+/**
+ * Get metadata for all blog posts (without content)
+ */
+export function getAllPostsMetadata(): BlogPostMetadata[] {
+ const slugs = getAllPostSlugs();
+
+ const posts = slugs.map(slug => {
+ const fullPath = path.join(contentDirectory, `${slug}.md`);
+ const fileContents = fs.readFileSync(fullPath, 'utf8');
+ const { data } = matter(fileContents);
+
+ validateFrontmatter(slug, data);
+
+ return {
+ slug,
+ title: data.title,
+ category: data.category,
+ description: data.description,
+ publishedAt: data.publishedAt,
+ author: data.author,
+ };
+ });
+
+ // Sort by publishedAt descending (newest first)
+ return posts.sort((a, b) =>
+ new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
+ );
+}
+
+/**
+ * Get a single blog post by slug (with content)
+ */
+export async function getPostBySlug(slug: string): Promise
{
+ try {
+ const fullPath = path.join(contentDirectory, `${slug}.md`);
+
+ if (!fs.existsSync(fullPath)) {
+ return null;
+ }
+
+ const fileContents = fs.readFileSync(fullPath, 'utf8');
+ const { data, content } = matter(fileContents);
+
+ validateFrontmatter(slug, data);
+
+ // Convert markdown to HTML
+ const processedContent = await remark()
+ .use(remarkGfm)
+ .use(html, { sanitize: false })
+ .process(content);
+
+ const contentHtml = processedContent.toString();
+
+ return {
+ slug,
+ title: data.title,
+ category: data.category,
+ description: data.description,
+ publishedAt: data.publishedAt,
+ author: data.author,
+ content: contentHtml,
+ };
+ } catch (error) {
+ console.error(`Error reading blog post ${slug}:`, error);
+ throw error;
+ }
+}
+
+/**
+ * Get posts by category
+ */
+export function getPostsByCategory(category: 'guide' | 'tips'): BlogPostMetadata[] {
+ const allPosts = getAllPostsMetadata();
+ return allPosts.filter(post => post.category === category);
+}
diff --git a/tailwind.config.ts b/tailwind.config.ts
index d007b24..4ca01bc 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -6,7 +6,9 @@ const config: Config = {
'./src/components/**/*.{ts,tsx}',
],
theme: { extend: {} },
- plugins: [],
+ plugins: [
+ require('@tailwindcss/typography'),
+ ],
};
export default config;