I rebuilt mehd.ir on Next.js and Payload CMS so the site could be both a portfolio and a real publishing system. Static markdown was fine for a few pages. It fell over once I wanted drafts, scheduled posts, media, SEO fields, and an admin UI I would actually use.
This is the stack, the content model choices, and the sharp edges I hit shipping it.
Why Next.js plus Payload
Next.js covers the public site: App Router, React Server Components, image optimization, and deployment on Vercel without a separate frontend ops story. Payload covers the editorial side: TypeScript collections, a local admin, access control, and Lexical body content stored in Postgres.
I wanted one repo. Frontend routes and CMS schema live together. When a field changes, TypeScript complains in both places.
Content model before chrome
I defined collections before polishing the homepage.
- Posts: title, slug, Lexical content, hero image, publishedAt, meta title/description, categories, authors, _status.
- Categories / users / media: thin supporting collections.
- Pages only where the layout is not a post.
The mistake I almost made was stuffing the homepage into one giant CMS body field. Instead, structured blocks and relations keep listing pages fast and keep SEO fields first-class.
App Router wiring
Public routes read published posts through Payload Local API or cached queries. The admin stays on Payload's panel. Draft preview matters: editors need to see an unpublished post without flipping _status to published.
For listings I select only card fields (title, slug, date, description, hero). I do not pull full Lexical JSON for index pages. That single habit keeps Postgres and TTFB honest as the archive grows.
Media and SEO
Every post gets:
- A hero used in the article layout and OG image path
- meta.title / meta.description distinct from the H1 when needed
- Stable slugs; no casual renames after publish
Images go through Next.js Image with explicit sizes. Layout shift from unbound heroes is an easy CLS foot-gun on a portfolio.
Deploy and env
Production needs POSTGRES_URL, PAYLOAD_SECRET, and the public server URL. I publish and revalidate by slug so ISR/CDN caches do not serve stale bodies after a CMS update. A successful publish is a live URL returning HTTP 200 with the expected title, a green admin save alone.
What I would repeat
- Schema-first, UI second.
- Separate card queries from article queries.
- Treat revalidation as part of publish, not an afterthought.
- Keep the admin boring so writing stays easy.
What I would avoid
- One unstructured body for the entire site
- Client-fetching the whole archive on the homepage
- Renaming slugs after they rank
- Skipping meta fields because "the title is enough"
Next.js and Payload are a strong personal-site pair when you treat the CMS as product infrastructure. The stack is not the point. A site you can update safely on a Friday afternoon is.