Blog setup in my portfolio using ChatGpt
How I set up blog writing & displaying using Nextjs, MDX and many more. Dont forget the ChatGpt
Next.js
TypeScript
Personal
AI

Yes. Here is the final picture of the blog system we built for your Next.js portfolio, from content creation to production. Think of this as your revision/reference document.
Portfolio Blog — Complete Setup
Your blog is essentially a local MDX-based static blog.
The important architectural decision was:
Blog content lives inside the repository as MDX files instead of coming from a database or CMS.
That gives you a fast, simple setup with very little runtime infrastructure.
You write .mdx
│
▼
Frontmatter + MDX content
│
▼
Blog loader
│
├── Zod validation
├── Reading time
├── Tags
├── TOC
└── Metadata
│
▼
Next.js Server Components
│
├── /blog
├── /blog/[slug]
├── pagination
├── tags
├── RSS
└── sitemap
│
▼
Mostly static HTMLThat is the core architecture.

1. Technologies We Used
Your blog uses your existing portfolio stack plus a few blog-specific tools.
Next.js App Router
TypeScript
Tailwind CSS
MDX
gray-matter
Zod
reading-time
next-mdx-remote
rehype-shiki
Motion for ReactEach has a specific responsibility.
| Technology | Purpose |
|---|---|
| Next.js | Routing, rendering, SEO, static generation |
| TypeScript | Type safety |
| Tailwind | Styling |
| MDX | Writing articles |
| gray-matter | Reading frontmatter |
| Zod | Validating frontmatter |
| reading-time | Calculating reading time |
| next-mdx-remote | Rendering MDX |
| Shiki | Server-side code highlighting |
| Motion | Small search-result animations |
The important thing is that we didn't build the blog around a large client-side library.
2. Content directory
Your articles live somewhere similar to:
content/
└── blog/
├── nextjs-performance.mdx
├── redis-caching.mdx
└── useeffect-nextjs-request-loop.mdxEach file represents one article.
The filename becomes the slug.
For example:
useeffect-nextjs-request-loop.mdxbecomes:
/blog/useeffect-nextjs-request-loopKeep filenames clean:
good:
redis-caching.mdx
nextjs-server-components.mdx
mongodb-indexing.mdxAvoid:
My Blog.mdx
Redis!!!.mdx
article-final-NEW.mdxUse lowercase words separated by hyphens.
3. MDX article structure
Every article contains two major parts.
Frontmatter
+
Article contentExample:
---
title: "How a useEffect Caused Hundreds of Requests in My Next.js App"
description: "How I found and fixed a request loop in my Next.js blog."
publishedAt: "2026-08-04"
tags:
- Next.js
- React
- TypeScript
- Performance
published: true
---
While building my portfolio...
## The Problem
My Network tab started showing repeated requests.
```tsx
useEffect(() => {
// ...
}, [])
```Conclusion
The problem was caused by...
The frontmatter is machine-readable information.
The content underneath is what readers see.
---
# 4. Blog metadata
We created a metadata type/schema containing information similar to:
```ts
{
title
description
publishedAt
updatedAt?
image?
tags
published
}
The required information is roughly:
title
description
publishedAt
tags
publishedOptional:
updatedAt
imageThis lets you write:
updatedAt: "..."only when an article was actually updated.
5. Zod validation
We don't blindly trust MDX frontmatter.
It goes through:
MDX
↓
gray-matter
↓
unknown metadata
↓
Zod
↓
validated metadataConceptually:
const result =
blogPostMetadataSchema.safeParse(data)
if (!result.success) {
throw new BlogContentError(...)
}
const metadata = result.dataThis is important because this:
resultis the Zod result object.
This:
result.datais the actual validated metadata.
That was the TypeScript issue we fixed earlier.
6. Invalid articles should fail loudly
If you accidentally write:
title: ""or an invalid date/tag structure, the build should tell you.
We don't want:
Bad article
↓
silently disappearWe want:
Bad article
↓
Zod
↓
clear error
↓
pnpm build fails
↓
you fix articleThat's useful production behavior for repository-controlled content.
7. Draft system
We added:
published: trueor:
published: falseSo you can work on an article without publishing it.
published: true
↓
Public blog
published: false
↓
DraftDrafts should not appear in:
/blog
search
tags
RSS
sitemap
public article routes8. Blog types
We have types representing the different kinds of blog data.
For example:
BlogPostMetadata
BlogPost
SearchableBlogPost
BlogTag
TableOfContentsItem
ReadingTimeA full BlogPost contains more information.
Conceptually:
BlogPost {
slug
metadata
content
readingTime
tableOfContents
}But search doesn't need all that.
So we created a smaller:
SearchableBlogPostcontaining roughly:
{
slug
title
description
publishedAt
tags
readingTime
}This is important because we don't want to send full MDX articles to the browser just to perform search.
9. Blog loader
Your blog utility layer is responsible for reading the content.
Something similar to:
lib/
└── blog/
├── blog.ts
├── types.ts
├── schema.ts
└── errors.tsExact filenames aren't as important as the responsibilities.
The loader does:
Find MDX files
↓
Read file
↓
Parse frontmatter
↓
Validate with Zod
↓
Create slug
↓
Calculate reading time
↓
Generate TOC
↓
Return BlogPost10. Keep filesystem code server-only
The loader uses things such as:
import fs from "node:fs"
import path from "node:path"That code must never enter the browser.
We added:
import "server-only"to the server-side blog loader.
So:
filesystem
MDX reading
Zod parsing
content processingremain on the server/build side.
11. getAllPosts()
One of the main utilities is conceptually:
getAllPosts()It gives us all public posts.
Its responsibilities include:
load posts
validate
remove drafts
sort by date
return postsUsually newest first:
Newest
↓
Older
↓
OldestThis becomes the common source used by other blog features.
12. getPostBySlug()
For an individual article:
/blog/redis-cachingwe use:
getPostBySlug("redis-caching")which returns the appropriate article.
If there isn't one:
notFound()So:
/blog/does-not-existproperly becomes a 404.
13. Reading time
We calculate reading time automatically from the article content.
So you don't manually write:
readingTime: 7Instead:
MDX content
↓
reading-time
↓
7 min readThen the UI can display:
Aug 4, 2026 · 7 min read14. Table of contents
Article headings are extracted to generate a TOC.
For example:
## The Problem
## Understanding the Feedback Loop
### Why useEffect Repeated
## The Solutionbecomes something like:
On this page
The Problem
Understanding the Feedback Loop
Why useEffect Repeated
The SolutionThe TOC can be sticky on larger screens.
If an article has no useful headings:
items.length === 0we simply don't render an empty TOC.
15. MDX rendering
Individual article pages use MDX rendering.
Conceptually:
<MDXRemote source={post.content} components={mdxComponents} />This means normal Markdown works:
## Heading
Paragraph.
- Item
- Itemwhile MDX also allows custom components when needed.
16. Custom MDX components
We created/configured an MDX component mapping.
That allows you to control how things such as:
links
images
code
custom calloutsrender.
This gives your articles the same visual language as the rest of your portfolio.
But don't turn every paragraph into a custom React component.
Keep normal writing as Markdown whenever possible.
17. Syntax highlighting
Technical articles need code blocks.
We used Shiki through your rehype setup.
For example:
```ts
interface User {
id: string
name: string
}
```gets highlighted automatically.
You configured approximately:
themes: {
light: "github-light",
dark: "github-dark",
}So code works with both themes.
Most importantly:
Shiki stays server-side.
We don't ship a big syntax-highlighting engine to the browser just to color static code.
18. Tailwind Typography
Article content uses your prose styling:
className="
prose
prose-neutral
max-w-none
dark:prose-invert
prose-pre:bg-transparent
prose-pre:p-0
"This gives Markdown sensible typography for:
headings
paragraphs
lists
links
blockquote
code
tableswithout individually styling every MDX element.
19. Main /blog page
Your main page is:
/blogIt contains approximately:
Blog
Things I learn while building software.
[ Search articles... ]
[ All ] [ Next.js ] [ Performance ] [ TypeScript ]
Article
Description
Date · Reading time
Tags
Article
Description
Date · Reading time
Tags
...The main page remains mostly server-rendered.
20. Blog cards
Normal articles use:
<BlogCard />which displays:
Title
Description
Date
Reading time
TagsWe also fixed the visual inconsistency between normal posts and searched posts.
The important rule is:
Search results and normal results should visually use the same card design.
So title size, description size, metadata, tags and spacing remain consistent.
21. Tags
Articles have tags such as:
tags:
- Next.js
- TypeScript
- PerformanceWe derive tag information automatically.
Your tag type is:
export interface BlogTag {
name: string
slug: string
count: number
}So:
Next.js (5)
TypeScript (8)
Performance (3)can be generated from actual articles.
You don't manually maintain tag counts.
22. The [object Object] key bug
At one point we had:
key = { tag }but tag had become:
BlogTagrather than a string.
React converted the object into:
[object Object]which caused duplicate keys.
The correct solution became:
key={tag.slug}and:
{
tag.name
}This is why the structured tag model matters.
23. Tag filtering
Clicking:
Performancefilters posts containing that tag.
We use the tag's slug for URL/state:
performancewhile showing its readable name:
PerformanceThis separation is useful:
name → UI
slug → URL / identity
count → article count24. Search
Search runs against lightweight post information:
title
description
tagsConceptually:
const searchableText = [post.title, post.description, ...post.tags]
.join(" ")
.toLowerCase()Then:
searchableText.includes(normalizedQuery)For a portfolio with dozens of posts, this is perfectly reasonable.
We intentionally did not add:
Algolia
Elasticsearch
Fuse.js
database searchbecause you don't need them yet.
25. Search stays client-side
This is one of the few genuinely interactive parts.
So:
Blog page
↓
Server Component
BlogSearch
↓
Client ComponentBlogSearch uses:
"use client"because it needs:
useState
useEffect
useMemo
useSearchParams
useRouter
MotionBut the rest of the blog stays server-oriented.
26. URL-synchronized search
We made search/filter state shareable.
For example:
/blog?q=redisand:
/blog?tag=performanceand potentially:
/blog?q=redis&tag=performanceThis means refreshes and shared URLs can preserve useful filter state.
27. The URL synchronization bug we fixed
This was an important production bug.
Originally the URL-writing effect depended on:
searchParamswhile also calling:
router.replace(...)That created repeated RSC requests.
The Network tab showed:
/blog?tag=hello&_rsc=...
/blog?tag=hello&_rsc=...
/blog?tag=hello&_rsc=...
...The mental model was:
effect
↓
router.replace()
↓
navigation
↓
searchParams
↓
effect
↓
router.replace()
↓
...We fixed this by separating:
React state → URLfrom:
URL → React stateand, critically, checking whether navigation is actually necessary:
const nextSearch = params.toString()
const currentSearch = window.location.search.slice(1)
if (nextSearch === currentSearch) {
return
}So the application doesn't navigate to the URL it's already on.
28. Search debounce
We also improved search behavior.
The UI filters immediately:
User types "redis"
↓
results change immediatelybut URL synchronization waits roughly:
300msSo we don't navigate on every keystroke.
Conceptually:
query
│
├──────────────→ local search
│ immediate
│
└→ debounce
│
300ms
│
▼
URL updateTags don't really need typing-style debounce because clicking a tag is a single action.
29. Clear filters
When filters are active, we show:
Clear filterswhich resets:
setQuery("")
setSelectedTag(null)and ultimately returns the URL to:
/blog30. Empty state
If search finds nothing:
No articles found
Try a different search term or tag.
Clear filtersWe don't show an empty blank area.
This is a small but important UX improvement.
31. Search accessibility
We improved accessibility with things such as:
search label
role="group"
aria-label
aria-pressed
aria-live
role="status"
focus-visibleFor example, tag buttons can expose whether they're selected:
aria-pressed={isSelected}and result counts can be announced:
<p role='status' aria-live='polite'>
3 articles found
</p>32. Keyboard navigation
The blog should work without a mouse.
A user should be able to:
Tab
↓
Search
↓
All
↓
Next.js
↓
TypeScript
↓
Article links
↓
Paginationwith a visible focus state.
This is why we don't blindly remove:
outlinewithout providing a replacement focus indicator.
33. Motion
We intentionally use Motion sparingly.
Search/filter results can have a small transition:
opacity
+
a few pixels of vertical movementWe do not animate:
every article paragraph
code blocks
normal server-rendered cards
TOC
everything on scrollThat would work against the original performance goal.
34. Reduced motion
Interactive animations should respect:
prefers-reduced-motionusing:
useReducedMotion()So:
Normal preference
→ subtle transition
Reduced motion
→ no unnecessary movementAnimation remains enhancement rather than functionality.
35. Pagination
We chose:
POSTS_PER_PAGE = 10which is a good number for this portfolio.
Routes look like:
/blog
/blog/page/2
/blog/page/3We intentionally did not implement infinite scrolling.
Traditional pagination is:
simpler
faster
shareable
crawlable
accessibleand better suited to this blog.
36. Pagination edge cases
We handle bad pages correctly.
/blog/page/banana
→ 404
/blog/page/-1
→ 404
/blog/page/999
→ 404And:
/blog/page/1should redirect to:
/blogso there aren't two URLs representing the same first page.
37. Static generation
Because your content lives locally, Next.js can know your articles ahead of time.
For article routes we use:
generateStaticParams()Conceptually:
return posts.map((post) => ({
slug: post.slug,
}))We similarly generate known pagination/tag paths where appropriate.
This fits the blog extremely well because your content changes when you deploy new code/content.
38. SEO metadata
Each article can generate metadata from its frontmatter.
Conceptually:
title
description
canonical URL
OpenGraph
Twitter metadataSo:
title: "How a useEffect Caused Hundreds of Requests..."
description: "..."feeds both the page and SEO metadata.
You don't manually maintain duplicate SEO information somewhere else.
39. Canonical URLs
Each article should have one canonical URL:
/blog/useeffect-nextjs-request-loopThis helps search engines understand the preferred URL for the content.
Likewise, redirecting /blog/page/1 back to /blog avoids unnecessary duplication.
40. Open Graph
Article metadata can produce useful previews when shared.
For example:
Title
Description
Article image
URLfor platforms that understand Open Graph metadata.
If an article provides an optional:
image:use it.
Otherwise your portfolio can use its normal fallback sharing image.
41. Structured data
We also covered article structured data / JSON-LD.
Conceptually:
{
"@type": "BlogPosting",
"headline": "...",
"datePublished": "...",
"dateModified": "..."
}This gives search engines more explicit information about the article.
It should represent the same article data you already have rather than introducing another manually maintained source.
42. Sitemap
Your sitemap includes public article URLs.
Conceptually:
sitemap
│
├── /
├── /blog
├── /blog/article-1
├── /blog/article-2
└── ...Drafts should not appear.
The sitemap should reuse your public-post loader rather than independently discovering content.
43. Robots
Your portfolio has the appropriate robots setup so crawlers can discover the site and sitemap.
Again, this is server/static infrastructure.
There is no reason for client JavaScript to interact with it.
44. RSS
We built:
/rss.xmlRSS lets readers/feed applications subscribe to your writing.
It contains information such as:
Blog title
Description
Site URL
Last build date
Article
title
description
URL
publication datelastBuildDate belongs to the RSS channel/feed metadata.
The RSS feed should contain published articles only.
45. lastBuildDate
You specifically asked about this earlier.
Conceptually:
<channel>
<title>...</title>
<lastBuildDate>
...
</lastBuildDate>
<item>
...
</item>
</channel>It describes when the feed was last updated/generated.
46. Related articles
Individual articles can show related posts.
The relationship is based on useful article information such as tags.
For example:
Current article:
Next.js
Performance
TypeScript
↓
Related articles sharing relevant tagsIf there are no related articles:
render nothinginstead of showing an empty section.
47. Previous/next article navigation
Article pages can also expose navigation between posts.
Conceptually:
← Previous article Next article →The first/last article naturally has only one side.
We don't create fake:
href="#"links for missing destinations.
48. Images
When blog content/cards use images, prefer:
import Image from "next/image"rather than plain <img> where Next's image optimization is appropriate.
Images should have known dimensions or aspect ratios to avoid:
page loads
↓
image loads
↓
content suddenly jumpsAlso don't mark every image as priority.
Only genuinely critical above-the-fold images should be considered for eager/preloaded behavior.
49. Fonts
Your blog should reuse the portfolio's existing optimized fonts.
Don't load:
five additional font familiesjust because it's a blog.
Something like:
normal portfolio font
+
code fontis enough.
50. Performance strategy
The main philosophy was:
Do expensive work on the server/build side and ship as little JavaScript as possible.
So:
SERVER / BUILD
filesystem
gray-matter
Zod
MDX
Shiki
reading time
TOC
metadata
pagination
RSS
sitemapwhile the browser mainly gets:
rendered HTML
+
small BlogSearch client component
+
small Motion behavior51. Server memoization
We also looked at avoiding unnecessary repeated blog-loader work with React server memoization where appropriate:
cache(...)especially for functions such as:
getAllPosts()
getPostBySlug()because multiple server operations may ask for the same post information during one rendering workflow.
We did not add Redis or another external caching system for local MDX.
That would be unnecessary.
52. Things we intentionally did NOT build
This is just as important as what we built.
We avoided:
❌ Database for blog content
❌ CMS
❌ Redis blog cache
❌ Elasticsearch
❌ Algolia
❌ Infinite scroll
❌ Client-side MDX
❌ Client-side Shiki
❌ Heavy fuzzy-search library
❌ Loading spinners for local search
❌ Scroll animations everywhere
❌ useMemo/useCallback everywhere
❌ Complex runtime APIsFor a developer portfolio, these would mostly add complexity without solving a real problem.
53. Your final directory structure
The exact filenames may vary slightly in your project, but conceptually you now have:
portfolio/
│
├── content/
│ └── blog/
│ ├── article-one.mdx
│ ├── article-two.mdx
│ └── useeffect-nextjs-request-loop.mdx
│
├── src/
│ │
│ ├── app/
│ │ ├── blog/
│ │ │ ├── page.tsx
│ │ │ │
│ │ │ ├── [slug]/
│ │ │ │ └── page.tsx
│ │ │ │
│ │ │ └── page/
│ │ │ └── [page]/
│ │ │ └── page.tsx
│ │ │
│ │ └── ...
│ │
│ ├── components/
│ │ └── blog/
│ │ ├── blog-card.tsx
│ │ ├── blog-search.tsx
│ │ ├── blog-search-card.tsx
│ │ ├── blog-tag.tsx
│ │ ├── blog-pagination.tsx
│ │ ├── table-of-contents.tsx
│ │ └── ...
│ │
│ └── lib/
│ └── blog/
│ ├── blog.ts
│ ├── types.ts
│ ├── schema.ts
│ ├── errors.ts
│ └── ...
│
└── ...Plus your:
RSS
sitemap
robots
metadataroutes/configuration in their appropriate Next.js locations.
54. The complete request flow
This is the most useful diagram to remember.
Blog archive
Browser requests /blog
│
▼
Next.js Server Component
│
▼
getAllPosts()
│
▼
Read MDX
│
├── gray-matter
├── Zod
├── reading time
└── metadata
│
▼
Published posts
│
├───────────────┐
▼ ▼
10 BlogCards SearchableBlogPost[]
│ │
│ ▼
│ BlogSearch
│ │
└───────┬───────┘
▼
BrowserIndividual article
/blog/my-article
│
▼
[slug]
│
▼
getPostBySlug()
│
▼
MDX article
│
├── metadata
├── reading time
├── TOC
└── content
│
▼
MDXRemote
│
▼
Shiki
│
▼
Server HTML
│
▼
Browser55. What happens when you publish a new blog now?
This is the best part.
You no longer need to build infrastructure.
You create:
content/blog/my-new-post.mdxAdd:
---
title: "..."
description: "..."
publishedAt: "..."
tags:
- Next.js
- Performance
published: true
---
# Your content...Then your existing system handles the rest:
New MDX
↓
validation
↓
slug
↓
reading time
↓
blog listing
↓
search
↓
tags/counts
↓
pagination
↓
article page
↓
TOC
↓
syntax highlighting
↓
SEO metadata
↓
sitemap
↓
RSSThat's the point where a blog system is truly useful: publishing an article becomes a content task rather than a programming task.
Final architecture
Your portfolio now effectively has:
PORTFOLIO BLOG
│
┌────────────────┼────────────────┐
│ │ │
CONTENT SERVER CLIENT
│ │ │
▼ ▼ ▼
MDX Filesystem BlogSearch
│ │ │
Frontmatter Zod Search
Markdown gray-matter Filters
Code MDX URL sync
Images Shiki Motion
│ Reading time │
│ TOC │
└──────────────┬──────────────────┘
│
▼
UI
│
┌─────────────┼─────────────┐
▼ ▼ ▼
/blog /blog/[slug] Pagination
│ │
▼ ▼
Search Article
Tags TOC
Cards Code
│ Related
│ Prev/Next
│
└─────────────┬─────────────┐
▼ ▼
SEO RSS
│
Metadata
OpenGraph
JSON-LD
Sitemap
RobotsWhere I would stop
At this point, the blog infrastructure is done.
You do not need another 10 features. The highest-value work now is writing good technical articles based on things you genuinely build and debug—like the useEffect/RSC request-loop article we just created.
Before deployment, just run your final checks: pnpm build, test /blog and several article routes, test search/tag URLs while watching the Network tab, verify pagination/404s, open the RSS and sitemap, test keyboard/mobile layouts, and run Lighthouse against the production build.
After that, treat the blog system as finished unless a real requirement appears.