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
34 changes: 34 additions & 0 deletions build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import fs from 'fs';
import path from 'path';

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

const years = fs.readdirSync(postsDir);

years.forEach(year => {
const yearPath = path.join(postsDir, year);
if (fs.lstatSync(yearPath).isDirectory()) {
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`));
}
});
}
});

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

console.log('✅ Build HTML from Markdown Post');
159 changes: 159 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"clean": "rm -rf dist",
"prebuild": "bun run clean",
"build": "vite build && bun build.js",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0"
"react-dom": "^19.2.0",
"react-markdown": "^10.1.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
Expand Down
16 changes: 16 additions & 0 deletions public/posts/2026/Test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Hello React 19

This is a post read from `public/posts/2026/Test.md`.

## Testing Functionality

* **Automatic Path**: Accessed via `/2026/Test.html`

* **GA Tracing**: `Analytics.jsx` is enabled

* **Environment**: Driven by **Bun**

### Code Example
```javascript
console.log("Hello from 2026!");
```

Choose a reason for hiding this comment

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

medium

It's a good practice for text files to end with a newline character. Some tools and systems expect this and may behave unexpectedly without it.

56 changes: 53 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,62 @@
import { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import Analytics from './Analytics';
import NotFound from './NotFound';

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

function App() {
const [content, setContent] = 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;

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

if (currentPath === '/' || currentPath === '/index.html') {
setContent('# Welcome My Blog');

Choose a reason for hiding this comment

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

medium

For better maintainability, it's good practice to avoid hardcoding strings directly in the logic. Consider extracting this welcome message into a constant, e.g., const HOME_PAGE_CONTENT = '# Welcome My Blog'; at the top of the file.

setStatus('success');
return;
}

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())

Choose a reason for hiding this comment

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

high

The fetch API does not reject on HTTP error statuses (like 404 or 500). This means that if the markdown file is not found on the server, the .then block will still execute on an error page's content. You should check if the response was successful before processing it to ensure you show your custom 404 page correctly.

For example:

.then(res => {
  if (!res.ok) {
    throw new Error(`HTTP error! status: ${res.status}`);
  }
  return res.text();
})

.then(text => {
setContent(text);
setStatus('success');
})
.catch(() => setStatus('404'));
Comment on lines +34 to +40

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 catches all errors and displays a 404 page, which will mislead users when network errors or server errors occur. Users experiencing connection issues will see "Page Not Found" instead of an appropriate error message.

Suggested change
fetch(`/posts/${year}/${slug}.md`)
.then(res => res.text())
.then(text => {
setContent(text);
setStatus('success');
})
.catch(() => setStatus('404'));
fetch(`/posts/${year}/${slug}.md`)
.then(res => {
if (!res.ok) {
setStatus('404');
return;
}
return res.text();
})
.then(text => {
if (text) {
setContent(text);
setStatus('success');
}
})
.catch(() => setStatus('error'));

} else {
setStatus('404');
}
} else {
setStatus('404');
}
}, []);

if (status === 'loading') return <div>Loading...</div>;
if (status === '404') return <NotFound />;
Comment on lines +49 to +50

Choose a reason for hiding this comment

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

🛑 Logic Error: Missing status condition handler. The code sets status to 'error' for network failures but only checks for 'loading' and '404' states, causing the component to render nothing when network errors occur.

Suggested change
if (status === 'loading') return <div>Loading...</div>;
if (status === '404') return <NotFound />;
if (status === 'loading') return <div>Loading...</div>;
if (status === '404') return <NotFound />;
if (status === 'error') return <div>Network error. Please try again later.</div>;


function App() {
return (
<>
<Analytics />
<h1 className="main-title">Hello World</h1>
<article style={{ padding: '40px', maxWidth: '800px', margin: '0 auto' }}>

Choose a reason for hiding this comment

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

medium

Using inline styles can make the component harder to maintain and reuse, especially as the application grows. Consider moving these styles to a dedicated CSS file or using CSS Modules for better separation of concerns and reusability.

<ReactMarkdown>{content}</ReactMarkdown>
</article>
</>
);
}
}
Comment on lines +8 to +60

Choose a reason for hiding this comment

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

medium

This component currently handles routing, data fetching, and rendering. This can make it harder to maintain. Consider these refactors:

  • Extract a custom hook: The logic inside useEffect for routing and fetching post data could be extracted into a custom hook (e.g., usePost()).
  • Create a Layout component: <Analytics /> is included here and also in NotFound.jsx. To avoid duplication, you could create a Layout component that renders <Analytics /> and then the specific page content. This would also ensure analytics are always active, even on the loading screen.


export default App;
17 changes: 17 additions & 0 deletions src/NotFound.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Analytics from './Analytics';

export default function NotFound() {
return (
<>
<Analytics />
<div style={{ textAlign: 'center', padding: '10vh 20px' }}>
<h1 style={{ fontSize: '3rem', color: '#ff4d4f' }}>404</h1>
<h2>Page Not Found</h2>
<p>Sorry, the article or page you are looking for seems to have moved or no longer exists.</p>
<a href="/" style={{ color: '#1890ff', textDecoration: 'none' }}>
← Return to homepage
</a>
</div>
Comment on lines +7 to +14

Choose a reason for hiding this comment

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

medium

Similar to App.jsx, these inline styles could be extracted to a CSS file or CSS Module. This centralizes styling, making it easier to manage and maintain a consistent look and feel across the application.

</>
);
}

Choose a reason for hiding this comment

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

medium

It's a good practice for files to end with a newline character. Please add one here.

Suggested change
}
}

6 changes: 6 additions & 0 deletions vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
build: {
emptyOutDir: true,
},

plugins: [react()],

base: '/',

server: {
open: true,
port: 3000,
Expand Down
Loading