Skip to content

New Blog: ហ្នឹងមែនៗ កំពត!! 😂#70

Merged
pphatdev merged 11 commits intomainfrom
static
Mar 12, 2026
Merged

New Blog: ហ្នឹងមែនៗ កំពត!! 😂#70
pphatdev merged 11 commits intomainfrom
static

Conversation

@pphatdev
Copy link
Owner

@pphatdev pphatdev commented Mar 12, 2026

New Blog

Title

ហ្នឹងមែនៗ កំពត!! 😂

Desciptions

អានុស្សាវរីយ៍ដំណើរកំសាន្តជុំគ្នារបស់ពួកយើងនៅខេត្តកំពត និងខេត្តកែប នៅថ្ងៃទី ០៧ មីនា ២០២៦

Summary by CodeRabbit

  • New Features

    • Responsive image gallery support in posts
    • Direct serving of post image assets
    • Automatic heading IDs for in-page navigation
    • Multiple monospace code fonts applied
    • Scroll-to-top button on post pages
  • Content Updates

    • Added new travel posts (Kampot, Kep)
    • Removed several older tutorial/articles
  • Style

    • Improved code block typography via updated fonts and head metadata

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

📝 Walkthrough

Walkthrough

Adds two new MDX travel posts, removes multiple older educational posts, implements asset-serving in the posts API with rewrites, introduces a Markdown gallery component and renderer refactor, adds a scroll-to-top button, font link, code font styling, and small SEO script and script helpers.

Changes

Cohort / File(s) Summary
New Blog Posts
content/posts/07-03-2026-kampot/index.mdx, content/posts/08-03-2026-kep/index.mdx
Added two new MDX travel posts with frontmatter, long-form narratives, and embedded gallery HTML blocks referencing external and local images.
Modified Blog Posts
content/posts/basic-html-tutorial/index.mdx, content/posts/example-heading-levels/index.mdx
TOC anchors updated, code fence languages switched to html, sample HTML adjusted, thumbnail path switched to /api/post?slug=..., and published flags toggled.
Deleted Blog Posts
content/posts/...
content/posts/building-rest-apis-nodejs/index.md, content/posts/css-flexbox-complete-guide/index.md, content/posts/git-workflow-best-practices/index.md, content/posts/javascript-testing-with-jest/index.md, content/posts/modern-javascript-es6-features/index.md, content/posts/responsive-web-design-principles/index.md, content/posts/typescript-for-javascript-developers/index.md, content/posts/understanding-react-hooks/index.md, content/posts/web-performance-optimization-techniques/index.md, content/posts/web-security-best-practices/index.md
Removed ten educational posts (front matter and content). Review for any downstream links or references to these slugs.
Next.js Rewrites
next.config.ts
Added rewrites returning two rules: query-based /api/post?slug=.../api/posts/:slug and path-based /api/post/:slug/api/posts/:slug.
Post Assets API
src/app/api/posts/[id]/route.ts
GET handler extended to serve post assets via ?asset=...; adds path validation, file existence checks, binary read, MIME detection helper, Cache-Control header, and 404/500 handling. Pay attention to path traversal protection and caching semantics.
Markdown Rendering & Gallery
src/components/ui/markdown-renderer.tsx, src/components/ui/markdown-gallery.tsx, src/components/ui/markdown-code-block.tsx
Refactored renderer to factory createMarkdownComponents() with stable heading IDs, improved code vs inline handling, and gallery recognition; added MarkdownGallery component (responsive columns, optional captions) and applied CODE_FONT_FAMILY to code elements. Review rendering edge cases for legacy HTML gallery blocks.
Layout / Fonts
src/app/layout.tsx
Added a combined Google Fonts stylesheet link (monospace fonts) into document head.
UI Components
src/components/ui/scroll-to-top-button.tsx, src/app/posts/[slug]/page.tsx
Added client-side ScrollToTopButton component and rendered it in post page. Check client-only behavior and SSR boundaries.
SEO Script Helpers
src/scripts/seo-google-indexing-fix.ts
Introduced ServiceNote type, resolveBaseUrl, and getServiceNotes to centralize base URL resolution and service note generation for indexing flow.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant NextAPI as Next.js API<br/>/api/posts/[id]
    participant FS as File System<br/>content/posts/
    participant Response as HTTP Response

    Client->>NextAPI: GET /api/posts/{slug}?asset=cover.jpg
    NextAPI->>NextAPI: Validate slug and asset name<br/>(prevent path traversal)
    alt asset is valid and exists
        NextAPI->>FS: Read file binary
        FS-->>NextAPI: File bytes
        NextAPI->>NextAPI: Determine MIME type<br/>set headers (Content-Type, Cache-Control)
        NextAPI->>Response: 200 OK with binary body
        Response-->>Client: File bytes
    else missing or invalid asset
        NextAPI->>Response: 404 Not Found
        Response-->>Client: 404
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through posts and tidy code,
Galleries stitched in travel mode,
Rewrites paved paths for asset calls,
Headings, fonts, and scrolling walls,
A little rabbit cheers the blend—onwards we go! 🌿

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title references a new blog post about a Cambodian trip and includes the post title in Khmer script, accurately reflecting the main change of adding new travel blog content to the repository.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch static

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/app/api/posts/[id]/route.ts (1)

42-58: Avoid sync filesystem calls in the request path.

Lines 47-51 do three synchronous filesystem operations per asset request. That blocks the Node worker for every image fetch and keeps a small check-then-read race in place. Prefer fs.promises.stat/readFile inside one try.

Suggested change
         if (assetName) {
             const safeAssetName = path.basename(assetName);
             const postDir = path.join(process.cwd(), 'content', 'posts', post.slug);
             const assetPath = path.join(postDir, safeAssetName);

-            if (safeAssetName !== assetName || !fs.existsSync(assetPath) || !fs.statSync(assetPath).isFile()) {
+            if (safeAssetName !== assetName) {
                 return NextResponse.json({ error: 'Asset not found' }, { status: 404 });
             }

-            const fileBuffer = fs.readFileSync(assetPath);
+            let fileBuffer: Buffer;
+            try {
+                const stats = await fs.promises.stat(assetPath);
+                if (!stats.isFile()) {
+                    return NextResponse.json({ error: 'Asset not found' }, { status: 404 });
+                }
+                fileBuffer = await fs.promises.readFile(assetPath);
+            } catch {
+                return NextResponse.json({ error: 'Asset not found' }, { status: 404 });
+            }

             return new NextResponse(new Uint8Array(fileBuffer), {
                 headers: {
                     'Content-Type': getMimeType(assetPath),
                     'Cache-Control': 'public, max-age=31536000, immutable',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/posts/`[id]/route.ts around lines 42 - 58, Replace synchronous fs
calls with async fs.promises inside a try/catch: compute safeAssetName with
path.basename(assetName) and assetPath from postDir as before, then await
fs.promises.stat(assetPath) and verify stat.isFile() and that safeAssetName ===
assetName; if any of these checks or the stat/readFile fails, return the 404
response. Read the file with await fs.promises.readFile(assetPath) and construct
the NextResponse using the returned Buffer/Uint8Array and existing headers
(getMimeType, Cache-Control). Ensure all uses reference the same symbols:
assetName, safeAssetName, postDir, assetPath, getMimeType, and NextResponse so
the request path is non-blocking and race conditions are reduced.
src/components/ui/markdown-renderer.tsx (1)

139-141: Consider memoizing the components factory.

Calling createMarkdownComponents() on every render creates new component references, which may cause unnecessary re-renders in react-markdown. Since the heading counter state needs to be fresh per content, you could memoize based on content:

♻️ Optional optimization
 export function MarkdownRenderer({ content, className }: MarkdownRendererProps) {
-    const markdownComponents = createMarkdownComponents();
+    const markdownComponents = React.useMemo(() => createMarkdownComponents(), [content]);

This ensures stable component references when content hasn't changed, while still resetting heading counts when content updates.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/markdown-renderer.tsx` around lines 139 - 141, The
MarkdownRenderer calls createMarkdownComponents() on every render causing new
component references and extra re-renders; wrap the factory in React.useMemo
inside MarkdownRenderer (e.g., const markdownComponents = useMemo(() =>
createMarkdownComponents(), [content])) so component references are stable
between renders but regenerate when the content prop changes (this preserves a
fresh heading counter per content and reduces unnecessary re-renders by
react-markdown).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@content/posts/07-03-2026-kampot/index.mdx`:
- Around line 71-73: The post's gallery <div class="blog-gallery"> contains
<img> elements (e.g., the img with className="max-h-96 w-full object-cover" and
the other gallery/story images referenced on the same page) that are missing
meaningful alt text; update each <img> tag (including those around lines 72,
86–93, and 98–123) to include descriptive alt attributes that convey the content
or purpose of the photo (avoid empty strings), e.g., brief captions describing
the scene or subject so screen readers can present the images to visually
impaired users.

In `@content/posts/08-03-2026-kep/index.mdx`:
- Around line 2-4: The frontmatter title and slug are still referencing the
07-03 Kampot post (title and slug fields), which mismatches this file's identity
(08-03-2026-kep); update the frontmatter by changing the title, slug (and
description if needed) to reflect the 08-03-2026 Kep post so routing/SEO/admin
lookups use the correct identifiers (edit the title and slug keys in this file's
frontmatter).

In `@content/posts/example-heading-levels/index.mdx`:
- Line 11: Update the frontmatter "thumbnail" value to use the canonical asset
endpoint instead of the compatibility alias; replace the current string
"/api/post?slug=example-heading-levels&asset=cover.jpg" in the thumbnail field
with the direct canonical asset route for this post's cover (i.e. the real
/api/assets/... path for example-heading-levels/cover.jpg) so the post points at
the primary asset endpoint rather than the rewrite alias.

In `@next.config.ts`:
- Around line 127-138: The rewrite rule inside the rewrites async function that
maps source '/api/post' to destination '/api/posts/:slug' currently uses a
capture group (?<slug>.*) which permits empty slugs; update the regex for the
has.query entry (key: 'slug') to require at least one character by using
(?<slug>[^/]+) so requests like /api/post?slug= are not rewritten to /api/posts/
with an empty slug; keep the same has/destination structure but replace the
pattern in the slug capture.

---

Nitpick comments:
In `@src/app/api/posts/`[id]/route.ts:
- Around line 42-58: Replace synchronous fs calls with async fs.promises inside
a try/catch: compute safeAssetName with path.basename(assetName) and assetPath
from postDir as before, then await fs.promises.stat(assetPath) and verify
stat.isFile() and that safeAssetName === assetName; if any of these checks or
the stat/readFile fails, return the 404 response. Read the file with await
fs.promises.readFile(assetPath) and construct the NextResponse using the
returned Buffer/Uint8Array and existing headers (getMimeType, Cache-Control).
Ensure all uses reference the same symbols: assetName, safeAssetName, postDir,
assetPath, getMimeType, and NextResponse so the request path is non-blocking and
race conditions are reduced.

In `@src/components/ui/markdown-renderer.tsx`:
- Around line 139-141: The MarkdownRenderer calls createMarkdownComponents() on
every render causing new component references and extra re-renders; wrap the
factory in React.useMemo inside MarkdownRenderer (e.g., const markdownComponents
= useMemo(() => createMarkdownComponents(), [content])) so component references
are stable between renders but regenerate when the content prop changes (this
preserves a fresh heading counter per content and reduces unnecessary re-renders
by react-markdown).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f58484d2-6887-43b8-88ce-de80b958f22e

📥 Commits

Reviewing files that changed from the base of the PR and between 42bcaaf and 31c6999.

⛔ Files ignored due to path filters (1)
  • content/posts/example-heading-levels/cover.jpg is excluded by !**/*.jpg
📒 Files selected for processing (30)
  • content/posts/07-03-2026-kampot/index.mdx
  • content/posts/08-03-2026-kep/index.mdx
  • content/posts/basic-html-tutorial/index.mdx
  • content/posts/building-rest-apis-nodejs/index.md
  • content/posts/css-flexbox-complete-guide/index.md
  • content/posts/example-heading-levels/index.mdx
  • content/posts/git-workflow-best-practices/index.md
  • content/posts/javascript-testing-with-jest/index.md
  • content/posts/modern-javascript-es6-features/index.md
  • content/posts/responsive-web-design-principles/index.md
  • content/posts/typescript-for-javascript-developers/index.md
  • content/posts/understanding-react-hooks/index.md
  • content/posts/web-performance-optimization-techniques/index.md
  • content/posts/web-security-best-practices/index.md
  • content/projects/blog-post/index.mdx
  • content/projects/blogs/index.mdx
  • content/projects/cookies/index.mdx
  • content/projects/leatsophat-me/index.mdx
  • content/projects/nintrea-elibrary/index.mdx
  • content/projects/nintrea-website/index.mdx
  • content/projects/pphatdev-pphatdev/index.mdx
  • content/projects/sample-nodejs-api/index.mdx
  • content/projects/sessions/index.mdx
  • next.config.ts
  • src/app/api/posts/[id]/route.ts
  • src/app/layout.tsx
  • src/components/ui/markdown-code-block.tsx
  • src/components/ui/markdown-gallery.tsx
  • src/components/ui/markdown-renderer.tsx
  • src/scripts/seo-google-indexing-fix.ts
💤 Files with no reviewable changes (10)
  • content/posts/web-performance-optimization-techniques/index.md
  • content/posts/modern-javascript-es6-features/index.md
  • content/posts/css-flexbox-complete-guide/index.md
  • content/posts/responsive-web-design-principles/index.md
  • content/posts/building-rest-apis-nodejs/index.md
  • content/posts/web-security-best-practices/index.md
  • content/posts/javascript-testing-with-jest/index.md
  • content/posts/typescript-for-javascript-developers/index.md
  • content/posts/understanding-react-hooks/index.md
  • content/posts/git-workflow-best-practices/index.md

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/ui/markdown-gallery.tsx (1)

43-44: Minor redundancy in column classes.

The base class includes columns-2 (applies at all breakpoints), while getColumnsClass always returns classes starting with sm:columns-2. The sm:columns-2 is redundant since columns-2 already sets 2 columns below the sm breakpoint. Tailwind handles this gracefully, so it's not a bug, but you could simplify by removing columns-2 from the base if the intent is to start with 1 column on mobile.

If you want 1 column on mobile (< sm breakpoint), consider:

♻️ Optional: Remove base columns-2 for mobile-first single column
 className={cn(
-    'not-prose my-8 columns-2 [column-gap:0.75rem] md:[column-gap:1rem]',
+    'not-prose my-8 columns-1 [column-gap:0.75rem] md:[column-gap:1rem]',
     getColumnsClass(dataColumns),
     className
 )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/markdown-gallery.tsx` around lines 43 - 44, The base class
list currently contains a hardcoded "columns-2" which conflicts with the
responsive classes returned by getColumnsClass(dataColumns); remove "columns-2"
from the base class array in the JSX so mobile (<sm) will be single-column and
rely on getColumnsClass(dataColumns) to apply "sm:columns-2" and other
responsive column classes; update the class array near where getColumnsClass is
used in the MarkdownGallery component to only include the non-column utility
classes and keep responsive behavior driven by getColumnsClass.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/ui/scroll-to-top-button.tsx`:
- Around line 31-48: The button remains focusable when isVisible is false;
update the ScrollToTop component so hidden buttons are not in the tab
order—either conditionally don't render the Button when isVisible is false
(unmount it) or add accessibility attributes to the Button (set
aria-hidden="true", tabIndex={-1} and disabled) when isVisible is false; change
the logic around the wrapper (cn(...) containing isVisible) and the Button
component (props on the Button that calls scrollToTop) to implement one of these
fixes.

---

Nitpick comments:
In `@src/components/ui/markdown-gallery.tsx`:
- Around line 43-44: The base class list currently contains a hardcoded
"columns-2" which conflicts with the responsive classes returned by
getColumnsClass(dataColumns); remove "columns-2" from the base class array in
the JSX so mobile (<sm) will be single-column and rely on
getColumnsClass(dataColumns) to apply "sm:columns-2" and other responsive column
classes; update the class array near where getColumnsClass is used in the
MarkdownGallery component to only include the non-column utility classes and
keep responsive behavior driven by getColumnsClass.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a90ce593-a346-46a1-80d3-8761027ea7f6

📥 Commits

Reviewing files that changed from the base of the PR and between 31c6999 and d3ebaa1.

📒 Files selected for processing (3)
  • src/app/posts/[slug]/page.tsx
  • src/components/ui/markdown-gallery.tsx
  • src/components/ui/scroll-to-top-button.tsx

Comment on lines +31 to +48
<div
className={cn(
'fixed bottom-5 right-5 z-50 transition-all duration-300',
'sm:bottom-6 sm:right-6',
isVisible
? 'translate-y-0 opacity-100 pointer-events-auto'
: 'translate-y-4 opacity-0 pointer-events-none',
className
)}
>
<div className="bg-foreground/5 ring-1 ring-foreground/10 rounded-full p-1 backdrop-blur-sm">
<Button
type="button"
size="icon"
variant="ghost"
onClick={scrollToTop}
aria-label="Scroll to top"
className="rounded-full p-2 hover:ring ring-foreground/20 hover:bg-foreground/10"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Prevent focus on the hidden button.

When isVisible is false, Lines 35-37 only hide the wrapper visually. The Button on Lines 42-48 still stays in the tab order, so keyboard users can focus an invisible control. Either unmount it while hidden or make it non-focusable/non-interactive.

♿ Proposed fix
         <div
+            aria-hidden={!isVisible}
             className={cn(
                 'fixed bottom-5 right-5 z-50 transition-all duration-300',
                 'sm:bottom-6 sm:right-6',
                 isVisible
                     ? 'translate-y-0 opacity-100 pointer-events-auto'
@@
                 <Button
                     type="button"
                     size="icon"
                     variant="ghost"
                     onClick={scrollToTop}
                     aria-label="Scroll to top"
+                    tabIndex={isVisible ? 0 : -1}
+                    disabled={!isVisible}
                     className="rounded-full p-2 hover:ring ring-foreground/20 hover:bg-foreground/10"
                 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/scroll-to-top-button.tsx` around lines 31 - 48, The button
remains focusable when isVisible is false; update the ScrollToTop component so
hidden buttons are not in the tab order—either conditionally don't render the
Button when isVisible is false (unmount it) or add accessibility attributes to
the Button (set aria-hidden="true", tabIndex={-1} and disabled) when isVisible
is false; change the logic around the wrapper (cn(...) containing isVisible) and
the Button component (props on the Button that calls scrollToTop) to implement
one of these fixes.

@pphatdev pphatdev merged commit c502e29 into main Mar 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant