Skip to content

Commit 38e7fef

Browse files
authored
blog: list posts on the home page (#39)
1 parent e588589 commit 38e7fef

9 files changed

Lines changed: 132 additions & 51 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Posts
2+
public/posts.json
3+
14
# Logs
25
logs
36
*.log

build.js

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from 'path';
33

44
const postsDir = './public/posts';
55
const distDir = './dist';
6-
6+
const allPosts = [];
77
const years = fs.readdirSync(postsDir);
88

99
years.forEach(year => {
@@ -12,23 +12,46 @@ years.forEach(year => {
1212
const files = fs.readdirSync(yearPath);
1313
files.forEach(file => {
1414
if (file.endsWith('.md')) {
15-
const slug = file.replace('.md', '');
16-
const targetDir = path.join(distDir, year);
17-
18-
if (!fs.existsSync(targetDir)) {
19-
fs.mkdirSync(targetDir, {
20-
recursive: true
21-
});
22-
}
23-
fs.copyFileSync(path.join(distDir, 'index.html'), path.join(targetDir, `${slug}.html`));
15+
const fileName = file.replace('.md', '');
16+
const parts = fileName.split('-');
17+
const date = parts.slice(0, 3).join('-');
18+
const slug = parts.slice(3).join('-');
19+
20+
allPosts.push({
21+
year,
22+
date,
23+
slug,
24+
originalName: fileName,
25+
title: slug.replace(/-/g, ' ')
26+
});
2427
}
2528
});
2629
}
2730
});
2831

29-
fs.copyFileSync(
30-
path.join(distDir, 'index.html'),
31-
path.join(distDir, '404.html')
32-
);
32+
allPosts.sort((a, b) => b.date.localeCompare(a.date));
33+
34+
const postsData = JSON.stringify(allPosts, null, 2);
35+
fs.writeFileSync('./public/posts.json', postsData);
36+
37+
if (fs.existsSync(path.join(distDir, 'index.html'))) {
38+
fs.writeFileSync(path.join(distDir, 'posts.json'), postsData);
39+
40+
allPosts.forEach(post => {
41+
const targetDir = path.join(distDir, post.year);
42+
if (!fs.existsSync(targetDir)) {
43+
fs.mkdirSync(targetDir, { recursive: true });
44+
}
45+
fs.copyFileSync(
46+
path.join(distDir, 'index.html'),
47+
path.join(targetDir, `${post.slug}.html`)
48+
);
49+
});
50+
51+
fs.copyFileSync(
52+
path.join(distDir, 'index.html'),
53+
path.join(distDir, '404.html')
54+
);
55+
}
3356

34-
console.log('✅ Build HTML from Markdown Post');
57+
console.log(`✅ Build ${allPosts.length} Posts Successfully`);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Test7 file
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test3 file
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test 123 file
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test5
File renamed without changes.

src/App.jsx

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,61 +2,75 @@ import { useState, useEffect } from 'react';
22
import ReactMarkdown from 'react-markdown';
33
import Analytics from './Analytics';
44
import NotFound from './NotFound';
5+
import Home from './Home';
56

6-
const allPostFiles = import.meta.glob('/public/posts/**/*.md', { query: '?url', import: 'default' });
7-
8-
function App() {
7+
export default function App() {
98
const [content, setContent] = useState('');
9+
const [posts, setPosts] = useState([]);
1010
const [status, setStatus] = useState('loading');
1111

1212
useEffect(() => {
1313
const params = new URLSearchParams(window.location.search);
1414
const redirectedPath = params.get('p');
15-
const currentPath = redirectedPath || window.location.pathname;
15+
let currentPath = redirectedPath || window.location.pathname;
1616

1717
if (redirectedPath) {
1818
window.history.replaceState(null, '', redirectedPath);
1919
}
2020

21-
if (currentPath === '/' || currentPath === '/index.html') {
22-
setContent('# Welcome My Blog');
23-
setStatus('success');
24-
return;
25-
}
21+
fetch('/posts.json')
22+
.then(res => res.json())
23+
.then(data => {
24+
setPosts(data);
25+
26+
const pathClean = currentPath.replace(/\.html$/, '');
27+
const parts = pathClean.split('/').filter(Boolean);
2628

27-
const parts = currentPath.replace(/\.html$/, '').split('/').filter(Boolean);
28-
const [year, slug] = parts;
29-
30-
if (year && slug) {
31-
const expectedPath = `/public/posts/${year}/${slug}.md`;
32-
33-
if (allPostFiles[expectedPath]) {
34-
fetch(`/posts/${year}/${slug}.md`)
35-
.then(res => res.text())
36-
.then(text => {
37-
setContent(text);
38-
setStatus('success');
39-
})
40-
.catch(() => setStatus('404'));
41-
} else {
42-
setStatus('404');
43-
}
44-
} else {
45-
setStatus('404');
46-
}
29+
if (parts.length === 0 || (parts.length === 1 && parts[0] === 'index')) {
30+
setStatus('home');
31+
return;
32+
}
33+
34+
if (parts.length === 2) {
35+
const [year, slug] = parts;
36+
const found = data.find(p => p.year === year && p.slug === slug);
37+
38+
if (found) {
39+
fetch(`/posts/${year}/${found.originalName}.md`)
40+
.then(res => res.text())
41+
.then(text => {
42+
setContent(text);
43+
setStatus('post');
44+
})
45+
.catch(() => setStatus('404'));
46+
} else {
47+
setStatus('404');
48+
}
49+
} else {
50+
setStatus('404');
51+
}
52+
})
53+
.catch(() => setStatus('404'));
4754
}, []);
4855

4956
if (status === 'loading') return <div>Loading...</div>;
50-
if (status === '404') return <NotFound />;
5157

5258
return (
5359
<>
5460
<Analytics />
55-
<article style={{ padding: '40px', maxWidth: '800px', margin: '0 auto' }}>
56-
<ReactMarkdown>{content}</ReactMarkdown>
57-
</article>
61+
<div className="app-shell" style={{ padding: '40px', maxWidth: '800px', margin: '0 auto' }}>
62+
{status === '404' ? (
63+
<NotFound />
64+
) : status === 'home' ? (
65+
<Home posts={posts} />
66+
) : (
67+
<article>
68+
<ReactMarkdown>{content}</ReactMarkdown>
69+
<hr />
70+
<a href="/" style={{ display: 'block', marginTop: '20px' }}>← Back to Home</a>
71+
</article>
72+
)}
73+
</div>
5874
</>
5975
);
60-
}
61-
62-
export default App;
76+
}

src/Home.jsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useState } from 'react';
2+
3+
const POSTS_PER_PAGE = 10;
4+
5+
export default function Home({ posts }) {
6+
const [currentPage, setCurrentPage] = useState(0);
7+
8+
const startIndex = currentPage * POSTS_PER_PAGE;
9+
const currentPosts = posts.slice(startIndex, startIndex + POSTS_PER_PAGE);
10+
const hasNext = startIndex + POSTS_PER_PAGE < posts.length;
11+
const hasPrev = currentPage > 0;
12+
13+
return (
14+
<div className="home-container">
15+
<h1>Recent Posts</h1>
16+
<ul className="post-list">
17+
{currentPosts.map(post => (
18+
<li key={post.originalName} className="post-item">
19+
<span className="post-date">{post.date}</span>
20+
<a href={`/${post.year}/${post.slug}.html`} className="post-link">
21+
{post.title}
22+
</a>
23+
</li>
24+
))}
25+
</ul>
26+
27+
<nav className="pagination">
28+
{hasPrev && (
29+
<button onClick={() => setCurrentPage(p => p - 1)}>← Newer</button>
30+
)}
31+
{hasNext && (
32+
<button onClick={() => setCurrentPage(p => p + 1)}>Older →</button>
33+
)}
34+
</nav>
35+
</div>
36+
);
37+
}

0 commit comments

Comments
 (0)