Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Posts
public/posts.json

# Logs
logs
*.log
Expand Down
53 changes: 38 additions & 15 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'path';

const postsDir = './public/posts';
const distDir = './dist';

const allPosts = [];
const years = fs.readdirSync(postsDir);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Crash Risk: Add existence check before reading directory to prevent runtime crash when postsDir doesn't exist.

Suggested change
const years = fs.readdirSync(postsDir);
if (!fs.existsSync(postsDir)) {
console.error(`Error: Posts directory "${postsDir}" does not exist`);
process.exit(1);
}
const years = fs.readdirSync(postsDir);


years.forEach(year => {
Expand All @@ -12,23 +12,46 @@ years.forEach(year => {
const files = fs.readdirSync(yearPath);
files.forEach(file => {
if (file.endsWith('.md')) {
const slug = file.replace('.md', '');
const targetDir = path.join(distDir, year);

if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, {
recursive: true
});
}
fs.copyFileSync(path.join(distDir, 'index.html'), path.join(targetDir, `${slug}.html`));
const fileName = file.replace('.md', '');
const parts = fileName.split('-');
const date = parts.slice(0, 3).join('-');
const slug = parts.slice(3).join('-');
Comment on lines +16 to +18

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current method of parsing the date and slug from the filename by splitting on '-' is brittle. It assumes a fixed structure that can easily break if a filename doesn't strictly adhere to the YYYY-MM-DD-slug format. For instance, a file named my-new-post.md without a date prefix would lead to incorrect date and slug values. Using a regular expression to explicitly match and extract these parts would be far more robust and would also handle filenames that don't match the pattern gracefully.

Suggested change
const parts = fileName.split('-');
const date = parts.slice(0, 3).join('-');
const slug = parts.slice(3).join('-');
const match = fileName.match(/^(\d{4}-\d{2}-\d{2})-(.*)$/);
if (!match) return;
const [, date, slug] = match;


allPosts.push({
year,
date,
slug,
originalName: fileName,
title: slug.replace(/-/g, ' ')
});
}
});
}
});

fs.copyFileSync(
path.join(distDir, 'index.html'),
path.join(distDir, '404.html')
);
allPosts.sort((a, b) => b.date.localeCompare(a.date));

const postsData = JSON.stringify(allPosts, null, 2);
fs.writeFileSync('./public/posts.json', postsData);

if (fs.existsSync(path.join(distDir, 'index.html'))) {
fs.writeFileSync(path.join(distDir, 'posts.json'), postsData);

allPosts.forEach(post => {
const targetDir = path.join(distDir, post.year);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
fs.copyFileSync(
path.join(distDir, 'index.html'),
path.join(targetDir, `${post.slug}.html`)
);
});

fs.copyFileSync(
path.join(distDir, 'index.html'),
path.join(distDir, '404.html')
);
}

console.log('✅ Build HTML from Markdown Post');
console.log(`✅ Build ${allPosts.length} Posts Successfully`);
1 change: 1 addition & 0 deletions public/posts/2025/2025-12-13-Test7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Test7 file
1 change: 1 addition & 0 deletions public/posts/2026/2026-01-29-Test3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test3 file
1 change: 1 addition & 0 deletions public/posts/2026/2026-01-31-Test123.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test 123 file
1 change: 1 addition & 0 deletions public/posts/2026/2026-02-03-Test5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test5
File renamed without changes.
86 changes: 50 additions & 36 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,75 @@ import { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import Analytics from './Analytics';
import NotFound from './NotFound';
import Home from './Home';

const allPostFiles = import.meta.glob('/public/posts/**/*.md', { query: '?url', import: 'default' });

function App() {
export default function App() {
const [content, setContent] = useState('');
const [posts, setPosts] = useState([]);
const [status, setStatus] = useState('loading');

useEffect(() => {
const params = new URLSearchParams(window.location.search);
const redirectedPath = params.get('p');
const currentPath = redirectedPath || window.location.pathname;
let currentPath = redirectedPath || window.location.pathname;

if (redirectedPath) {
window.history.replaceState(null, '', redirectedPath);
}

if (currentPath === '/' || currentPath === '/index.html') {
setContent('# Welcome My Blog');
setStatus('success');
return;
}
fetch('/posts.json')
.then(res => res.json())
.then(data => {
setPosts(data);

const pathClean = currentPath.replace(/\.html$/, '');
const parts = pathClean.split('/').filter(Boolean);

const parts = currentPath.replace(/\.html$/, '').split('/').filter(Boolean);
const [year, slug] = parts;

if (year && slug) {
const expectedPath = `/public/posts/${year}/${slug}.md`;

if (allPostFiles[expectedPath]) {
fetch(`/posts/${year}/${slug}.md`)
.then(res => res.text())
.then(text => {
setContent(text);
setStatus('success');
})
.catch(() => setStatus('404'));
} else {
setStatus('404');
}
} else {
setStatus('404');
}
if (parts.length === 0 || (parts.length === 1 && parts[0] === 'index')) {
setStatus('home');
return;
}

if (parts.length === 2) {
const [year, slug] = parts;
const found = data.find(p => p.year === year && p.slug === slug);

if (found) {
fetch(`/posts/${year}/${found.originalName}.md`)
.then(res => res.text())
.then(text => {
setContent(text);
setStatus('post');
})
.catch(() => setStatus('404'));
} else {
setStatus('404');
}
} else {
setStatus('404');
}
})
.catch(() => setStatus('404'));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Logic Error: The fetch error handler sets status to '404', but this creates confusing user experience when the actual issue is network failure or server error, not a missing page. This misrepresents the error condition.

Suggested change
.catch(() => setStatus('404'));
.catch(() => setStatus('error'));

}, []);

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

return (
<>
<Analytics />
<article style={{ padding: '40px', maxWidth: '800px', margin: '0 auto' }}>
<ReactMarkdown>{content}</ReactMarkdown>
</article>
<div className="app-shell" style={{ padding: '40px', maxWidth: '800px', margin: '0 auto' }}>
{status === '404' ? (
<NotFound />
) : status === 'home' ? (
<Home posts={posts} />
) : (
Comment on lines +62 to +66

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add handling for the 'error' status state in the conditional rendering logic to properly display error messages instead of showing NotFound for network/server errors.

Suggested change
{status === '404' ? (
<NotFound />
) : status === 'home' ? (
<Home posts={posts} />
) : (
{status === '404' ? (
<NotFound />
) : status === 'error' ? (
<div>Error loading content. Please try again later.</div>
) : status === 'home' ? (
<Home posts={posts} />

<article>
<ReactMarkdown>{content}</ReactMarkdown>
<hr />
<a href="/" style={{ display: 'block', marginTop: '20px' }}>← Back to Home</a>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a standard <a> tag with href for navigation within a React application causes a full page reload, which negates the benefits of a single-page application (SPA). This leads to a slower, less fluid user experience. To fix this, you should handle navigation on the client side by preventing the default link behavior and updating the component's state to render the home view.

Suggested change
<a href="/" style={{ display: 'block', marginTop: '20px' }}>← Back to Home</a>
<a href="/" onClick={(e) => { e.preventDefault(); setStatus('home'); }} style={{ display: 'block', marginTop: '20px' }}>← Back to Home</a>

</article>
)}
</div>
</>
);
}

export default App;
}
37 changes: 37 additions & 0 deletions src/Home.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useState } from 'react';

const POSTS_PER_PAGE = 10;

export default function Home({ posts }) {
const [currentPage, setCurrentPage] = useState(0);

const startIndex = currentPage * POSTS_PER_PAGE;
const currentPosts = posts.slice(startIndex, startIndex + POSTS_PER_PAGE);
const hasNext = startIndex + POSTS_PER_PAGE < posts.length;
const hasPrev = currentPage > 0;
Comment on lines +5 to +11

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Crash Risk: Add validation for posts prop to prevent runtime crash when posts is undefined or null.

Suggested change
export default function Home({ posts }) {
const [currentPage, setCurrentPage] = useState(0);
const startIndex = currentPage * POSTS_PER_PAGE;
const currentPosts = posts.slice(startIndex, startIndex + POSTS_PER_PAGE);
const hasNext = startIndex + POSTS_PER_PAGE < posts.length;
const hasPrev = currentPage > 0;
export default function Home({ posts = [] }) {
const [currentPage, setCurrentPage] = useState(0);
const startIndex = currentPage * POSTS_PER_PAGE;
const currentPosts = posts.slice(startIndex, startIndex + POSTS_PER_PAGE);
const hasNext = startIndex + POSTS_PER_PAGE < posts.length;
const hasPrev = currentPage > 0;


return (
<div className="home-container">
<h1>Recent Posts</h1>
<ul className="post-list">
{currentPosts.map(post => (
<li key={post.originalName} className="post-item">
<span className="post-date">{post.date}</span>
<a href={`/${post.year}/${post.slug}.html`} className="post-link">
{post.title}
</a>
Comment on lines +20 to +22

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These post links use standard <a> tags, which cause a full page reload every time a user clicks on a post. In a Single-Page Application, this is inefficient and provides a poor user experience. You should handle this navigation on the client side.

To implement this, you could pass a navigation handler function from the App component down to the Home component. The <a> tag's onClick handler would then prevent the default behavior and call this function, allowing the App component to fetch the post data and update the view without a page refresh.

</li>
))}
</ul>

<nav className="pagination">
{hasPrev && (
<button onClick={() => setCurrentPage(p => p - 1)}>← Newer</button>
)}
{hasNext && (
<button onClick={() => setCurrentPage(p => p + 1)}>Older →</button>
)}
</nav>
</div>
);
}
Loading