A full stack blogging platform: write in a rich text editor, publish or keep it a draft, and let readers comment, reply, like, bookmark and follow.
Developer documentation · Features · Getting started
Dark mode · View this page in light mode
- Why this project matters
- Where it came from
- Screenshots
- Responsive layout
- Features
- Roles
- The post lifecycle
- Getting started
- Contributors
- Contributing
- License
A blog is the standard "full stack" exercise, and most versions of it stop at create, read, update and delete over a posts table. The parts that make a blog actually usable are the ones that get skipped, and they are where the interesting decisions live.
A draft has to be genuinely invisible: not just filtered from listings, but returning 404 to anyone who guesses the URL, while still loading for its author in the editor. A published permalink has to survive its title being edited, or every link anyone shared breaks. A rich text editor writes HTML into a database that later feeds dangerouslySetInnerHTML, so there is exactly one place the sanitiser can go and exactly one way to get it wrong. A comment thread needs a depth rule, because "reply to a reply to a reply" has no natural end. Deleting a post on MongoDB means deleting its replies before its comments, because there are no cascades to lean on.
BlogNest is built around getting those right, and the reasoning for each is written down in DEVDOC.md rather than left in the code for the next person to reverse engineer.
It started as the standard Next.js blog tutorial and kept going past the point where the tutorial stops.
The turns that changed it were all the same shape: a feature that looks like one field turns out to be a rule. Adding drafts meant deciding what a draft URL does for a stranger. Adding an editor meant deciding where HTML gets sanitised, and the answer is on the way in, once, rather than on every render. Adding likes and comment counts meant deciding whether to count rows on every request or denormalise a counter and keep it honest, and the counter won because sorting by "most discussed" is a query you cannot write against a count you compute in application code.
The result is a blog you can actually run, with the awkward cases handled rather than avoided.
Every image is a real 1440x900 viewport render against a seeded local instance. This page shows dark mode; the same gallery in light mode is at README-light.md.
Each image is its own device viewport. The header collapses to a menu, the grid drops to one column, and the sidebar moves below the content.
Home |
All posts |
Home |
Reading
- Home page with a featured story, category strip, recent posts and a most-read sidebar
- Full library at
/blog: search titles, body and tags, filter by category, sort four ways - Search-as-you-type in the header, showing the top six matches before you press enter
- Category pages, tag pages and author profiles, each paginated
- Related posts under every article, same category first and shared tags second
- Reading time, views, likes and comment count on every card and article
- RSS feed at
/feed.xml, plussitemap.xmlandrobots.txt
Writing
- Rich text editor: headings, bold, italic, underline, strikethrough, quotes, code blocks, lists, links and inline images
- Cover image by upload or by pasting a URL you already host
- Category, up to eight tags, and an optional excerpt generated from the body if left blank
- Publish now or save as a draft; a draft URL returns 404 to everyone but its author
- Unsaved new posts autosave to your browser, so a refresh does not lose the draft
- Unpublishing moves a post back to drafts without losing its comments or likes
Discussion and reactions
- Comment on any published post, and reply once deep into a thread
- Edit or delete your own comments; admins can moderate anyone's
- Like a post, with a public count that drives most-liked ordering
- Bookmark a post privately, collected in your dashboard
- Follow an author from their profile or from any byline
Your account
- Email and password, or GitHub and Google when the site owner has configured them
- Dashboard with published, drafts, views, likes, comments received, followers and bookmarks
- Editable profile: name, handle, bio, website and picture. Your handle is your public URL
- Change your password, or set one for the first time after signing up through a provider
- Delete your account, which removes your posts, comments, likes, bookmarks and uploads
Everywhere
- Light and dark themes, remembered per browser, with no flash of the wrong one on load
- Responsive from phone to desktop
- Keyboard reachable, with a skip link, focus rings and labelled controls
Write in the header, or New post from the dashboard.
Give it a title, write the body, pick a category and up to eight tags. Add a cover image by uploading one or pasting a URL. Leave the excerpt blank and one is generated from the first part of the body.
Then either Publish or Save as draft. A draft is visible only to you and to admins: it is excluded from every listing, the feed and the sitemap, and its URL returns 404 to anyone else. Editing a draft's title also changes its slug; once published, the slug is frozen so shared links keep working.
If you close the tab before saving a new post, the draft is restored from your browser next time.
Comments are open on any published post to anyone signed in. Replies go one level deep: a reply to a reply attaches to the same root, because an unbounded thread has no sensible layout at 390px wide.
You can edit or delete your own comments. Deleting a comment that has replies takes the replies with it, and the post's comment count is adjusted by the number of rows that actually go away rather than by one.
Like is public and drives the most-liked ordering. Save is a private bookmark, collected under Dashboard → Bookmarks. Follow an author from their profile or from any byline, and their follower count updates immediately.
Dashboard shows published count, drafts, total views, likes, comments received, followers and bookmarks saved.
My posts is the table: per-post numbers with inline publish, unpublish, edit and delete. Bookmarks is what you saved. Settings holds your profile, your handle, and the password form.
Your handle is your public URL at /authors/<handle>, so changing it changes where your profile lives.
The header search shows the top six matches as you type. Press enter for the full library, where you can search titles, body text and tags together, filter by category, and sort by newest, oldest, most read or most discussed.
| Role | Can do |
|---|---|
| Visitor | Read published posts, search, browse categories, tags and author profiles, subscribe to the feed |
| User | Everything a visitor can, plus write, publish, edit and delete their own posts, comment, reply, like, bookmark and follow |
| Admin | Everything a user can, plus edit or delete any post, moderate any comment, and create categories |
An account becomes an admin by having its email listed in ADMIN_EMAILS at the time it signs up.
Save as draft Publish
(writing) ---------------------> DRAFT ---------> PUBLISHED
^ |
+--------------------+
Unpublish
- Draft is visible only to its author and to admins. No
publishedAt, excluded from every listing, feed and sitemap, and its URL returns 404 to anyone else. Editing the title also changes the slug. - Published is listed, searchable, indexable and open for comments.
publishedAtis stamped once and never rewritten, so unpublishing and republishing does not move the post to the top of the page. The slug is frozen so existing links keep working. - Deleted removes the post along with its comments, replies, likes and bookmarks.
You need Node 18 or newer and a MongoDB replica set. Prisma needs a replica set for transactions; Atlas gives you one by default, and DEVDOC.md has the local Docker one-liner.
git clone https://github.com/Dileepadari/BlogNest.git
cd BlogNest
npm install
cp .env.example .env # set DATABASE_URL and NEXTAUTH_SECRET
npm run db:push # create the collections and indexes
npm run db:seed # 4 authors, 12 posts, comments, likes, bookmarks
npm run dev # http://localhost:3000npm run db:seed creates four authors with a term of posts, comments, likes, bookmarks and follows between them.
| Role | Password | |
|---|---|---|
| Admin | demo@example.com |
DemoPass123! |
| User | maya@example.com |
DemoPass123! |
| User | tomas@example.com |
DemoPass123! |
| User | reader@example.com |
DemoPass123! |
Every address is under example.com, which RFC 2606 reserves for exactly this.
npm run dev # in one terminal
./scripts/e2e.sh # 50 checks against the real HTTP APIThe script signs in through NextAuth, exercises the guards, writes and deletes its own throwaway account, and is safe to run repeatedly. Do not point it at production.
Dileep Adari Author and maintainer |
Issues and pull requests are welcome on the repository.
Before opening a PR, run what CI runs:
npm run lint
npm run build
./scripts/e2e.sh # needs a running instance and a seeded databaseConventions: single-line commit messages, no em dashes and no literal emoji anywhere, and update DEVDOC.md in the same change if you add a route, a model field or an environment variable.
Two rules worth knowing before touching the data layer. HTML is sanitised on the way in, once, in the API route, so dangerouslySetInnerHTML reads only what the sanitiser already passed. And on MongoDB an absent field and a null one are different states, so a filter meaning "has no parent" has to say both.
MIT © Dileep Adari










