aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authormihna123 <mihailonvojinovic@gmail.com>2026-06-08 14:19:32 +0200
committermihna123 <mihailonvojinovic@gmail.com>2026-06-08 14:19:32 +0200
commit67eb91ad6dccf2a14e3f8d3b285c75d251322973 (patch)
tree648fc2de5568ef8f541e6c7033e3c1a0a953a92e /src
parent3f513363524c7cfd9d7fb573454090d2ba01bda9 (diff)
downloadinvoice-67eb91ad6dccf2a14e3f8d3b285c75d251322973.tar.gz
invoice-67eb91ad6dccf2a14e3f8d3b285c75d251322973.zip
feat(blogs): implement blogging functionality
- Implement static blog generation from markdown files
Diffstat (limited to 'src')
-rw-r--r--src/app/blog/page.tsx7
-rw-r--r--src/app/blogs/[slug]/page.tsx36
-rw-r--r--src/app/blogs/page.tsx39
-rw-r--r--src/app/globals.css29
-rw-r--r--src/components/header.tsx2
-rw-r--r--src/lib/blogs/api.ts42
6 files changed, 147 insertions, 8 deletions
diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx
deleted file mode 100644
index ff5c415..0000000
--- a/src/app/blog/page.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export default function BlogPage() {
- return (
- <section>
- <h1>Blogs</h1>
- </section>
- );
-}
diff --git a/src/app/blogs/[slug]/page.tsx b/src/app/blogs/[slug]/page.tsx
new file mode 100644
index 0000000..3372bea
--- /dev/null
+++ b/src/app/blogs/[slug]/page.tsx
@@ -0,0 +1,36 @@
+import { getBlogFromSlug, markdownToHtml } from '@/lib/blogs/api';
+
+type Params = {
+ params: Promise<{ slug: string }>;
+};
+
+export default async function Blog(props: Params) {
+ const params = await props.params;
+ const blog = getBlogFromSlug(params.slug);
+ const htmlContent = await markdownToHtml(blog.content);
+
+ return (
+ <section className="mt-10 flex justify-center">
+ <div className="w-5xl space-y-4">
+ <h1 className="m-0 mb-2 text-4xl font-bold">{blog.title}</h1>
+ <p>
+ by{' '}
+ <a
+ href={blog.author.social}
+ target="_blank"
+ rel="noopener noreferrerer"
+ className="link"
+ >
+ {blog.author.name}
+ </a>{' '}
+ ~ {blog.date.toLocaleDateString()}
+ </p>
+ <p className="mb-10 text-lg">{blog.excerpt}</p>
+ <article
+ className="markdown"
+ dangerouslySetInnerHTML={{ __html: htmlContent }}
+ />
+ </div>
+ </section>
+ );
+}
diff --git a/src/app/blogs/page.tsx b/src/app/blogs/page.tsx
new file mode 100644
index 0000000..8fa3c42
--- /dev/null
+++ b/src/app/blogs/page.tsx
@@ -0,0 +1,39 @@
+import { type Blog, getAllBlogs } from '@/lib/blogs/api';
+import Link from 'next/link';
+
+const BlogCard = ({ blog }: { blog: Blog }) => {
+ return (
+ <div className="space-y-2">
+ <Link
+ href={`/blogs/${blog.slug}`}
+ className="link text-3xl font-semibold"
+ >
+ {blog.title}
+ </Link>
+ <p>
+ by {blog.author.name} ~ {blog.date.toLocaleDateString()}
+ </p>
+ <p className="text-lg">{blog.excerpt}</p>
+ <Link href={`/blogs/${blog.slug}`} className="link">
+ READ MORE &gt;
+ </Link>
+ </div>
+ );
+};
+
+export default function BlogPage() {
+ const blogs = getAllBlogs();
+ // newest first
+ blogs.sort((a, b) => b.date.getTime() - a.date.getTime());
+
+ return (
+ <section className="mt-10 flex justify-center">
+ <div className="w-5xl space-y-4">
+ <h1 className="text-4xl font-bold">Free Invoice Generator Blogs</h1>
+ {blogs.map((b) => (
+ <BlogCard key={b.slug} blog={b} />
+ ))}
+ </div>
+ </section>
+ );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index eb0ee21..ff84294 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -17,3 +17,32 @@ body {
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
+
+.link,
+.markdown a {
+ @apply text-blue-900 underline hover:bg-blue-900 hover:text-white;
+}
+
+.markdown {
+ @apply space-y-4 text-lg leading-relaxed;
+}
+
+.markdown h1 {
+ @apply text-3xl font-bold;
+}
+
+.markdown h2 {
+ @apply text-2xl font-semibold;
+}
+
+.markdown h3 {
+ @apply text-xl font-semibold;
+}
+
+.markdown ul {
+ @apply list-disc pl-8;
+}
+
+.markdown ol {
+ @apply list-decimal pl-8;
+}
diff --git a/src/components/header.tsx b/src/components/header.tsx
index d54d957..0637fc2 100644
--- a/src/components/header.tsx
+++ b/src/components/header.tsx
@@ -9,7 +9,7 @@ export const Header = () => {
</Link>
<ul className="flex gap-2">
<li>
- <Link href="/blog">Blog</Link>
+ <Link href="/blogs">Blogs</Link>
</li>
<li>
<Link href="/help">Help</Link>
diff --git a/src/lib/blogs/api.ts b/src/lib/blogs/api.ts
new file mode 100644
index 0000000..3fcdd15
--- /dev/null
+++ b/src/lib/blogs/api.ts
@@ -0,0 +1,42 @@
+import path from 'path';
+import fs from 'fs';
+import matter from 'gray-matter';
+import { remark } from 'remark';
+import html from 'remark-html';
+
+const blogsDir = path.join(process.cwd(), '_blogs');
+
+export interface Blog {
+ author: {
+ name: string;
+ social: string;
+ };
+ date: Date;
+ title: string;
+ slug: string;
+ excerpt: string;
+ content: string;
+}
+
+export const getBlogFromSlug = (slug: string): Blog => {
+ const fullPath = path.join(blogsDir, `${slug}.md`);
+ const file = fs.readFileSync(fullPath, 'utf8');
+ const { data, content } = matter(file);
+ return {
+ ...data,
+ date: new Date(data.date),
+ content,
+ slug,
+ } as Blog;
+};
+
+export const getAllBlogs = (): Blog[] => {
+ const rawMdFiles = fs.readdirSync(blogsDir);
+ const slugs = rawMdFiles.map((file) => file.replace(/\.md$/, ''));
+ return slugs.map(getBlogFromSlug);
+};
+
+export const markdownToHtml = async (markdown: string): Promise<string> => {
+ const htmlResult = await remark().use(html).process(markdown);
+ return htmlResult.toString();
+};