Conversation
…e code block styling with custom font
…ions and metadata
…ត្តកែប នៅថ្ងៃទី ០៧ មីនា ២០២៦
📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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/readFileinside onetry.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 oncontent:♻️ 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
⛔ Files ignored due to path filters (1)
content/posts/example-heading-levels/cover.jpgis excluded by!**/*.jpg
📒 Files selected for processing (30)
content/posts/07-03-2026-kampot/index.mdxcontent/posts/08-03-2026-kep/index.mdxcontent/posts/basic-html-tutorial/index.mdxcontent/posts/building-rest-apis-nodejs/index.mdcontent/posts/css-flexbox-complete-guide/index.mdcontent/posts/example-heading-levels/index.mdxcontent/posts/git-workflow-best-practices/index.mdcontent/posts/javascript-testing-with-jest/index.mdcontent/posts/modern-javascript-es6-features/index.mdcontent/posts/responsive-web-design-principles/index.mdcontent/posts/typescript-for-javascript-developers/index.mdcontent/posts/understanding-react-hooks/index.mdcontent/posts/web-performance-optimization-techniques/index.mdcontent/posts/web-security-best-practices/index.mdcontent/projects/blog-post/index.mdxcontent/projects/blogs/index.mdxcontent/projects/cookies/index.mdxcontent/projects/leatsophat-me/index.mdxcontent/projects/nintrea-elibrary/index.mdxcontent/projects/nintrea-website/index.mdxcontent/projects/pphatdev-pphatdev/index.mdxcontent/projects/sample-nodejs-api/index.mdxcontent/projects/sessions/index.mdxnext.config.tssrc/app/api/posts/[id]/route.tssrc/app/layout.tsxsrc/components/ui/markdown-code-block.tsxsrc/components/ui/markdown-gallery.tsxsrc/components/ui/markdown-renderer.tsxsrc/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
There was a problem hiding this comment.
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), whilegetColumnsClassalways returns classes starting withsm:columns-2. Thesm:columns-2is redundant sincecolumns-2already sets 2 columns below thesmbreakpoint. Tailwind handles this gracefully, so it's not a bug, but you could simplify by removingcolumns-2from the base if the intent is to start with 1 column on mobile.If you want 1 column on mobile (<
smbreakpoint), 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
📒 Files selected for processing (3)
src/app/posts/[slug]/page.tsxsrc/components/ui/markdown-gallery.tsxsrc/components/ui/scroll-to-top-button.tsx
| <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" |
There was a problem hiding this comment.
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.
New Blog
Title
ហ្នឹងមែនៗ កំពត!! 😂
Desciptions
អានុស្សាវរីយ៍ដំណើរកំសាន្តជុំគ្នារបស់ពួកយើងនៅខេត្តកំពត និងខេត្តកែប នៅថ្ងៃទី ០៧ មីនា ២០២៦
Summary by CodeRabbit
New Features
Content Updates
Style