Skip to content
intermediatePhase 94 · Early Intermediate

Blog Platform

Build a complete blog with posts, comments, search, pagination, rich text editing, and user authentication.

4h
0 problems
Topic Progress0%

Project Overview

Blog Platform

Build a complete blogging platform with author and reader roles.

Features

Author:

  • Create, edit, delete posts
  • Rich text editor (Markdown or WYSIWYG)
  • Draft/publish workflow
  • Image upload for featured images
  • Post analytics (views, comments)

Reader:

  • Browse posts with pagination
  • Search by title and content
  • Filter by category and tag
  • Read posts with comments
  • Like posts

Architecture

React Frontend
  ↓
Express API (REST)
  ↓
MongoDB (posts, users, comments, categories)

What You Will Build

  1. Multi-model data design
  2. Rich text content handling
  3. Image upload and storage
  4. Search with text indexes
  5. Pagination (cursor and offset)
  6. Nested comments

Architecture — What Goes Where and Why

Project Architecture

Before writing any code, let's understand what files we need and where they go. Think of this like a blueprint for a house — you don't start building without a plan.

The File Tree

Every project needs a folder structure. Here is ours:

├── server.js
├── config/database.js
├── models/Post.js
├── models/Comment.js
├── routes/posts.js
├── routes/comments.js
├── middleware/auth.js
├── public/index.html
├── public/post.html
├── public/css/styles.css
├── public/js/app.js

What Each File Does

Let me explain each file like you're 5 years old:

server.js — Express server entry point

Starts the server, connects to database, loads all routes.

config/database.js — Database connection

Connects to PostgreSQL using a connection pool for efficiency.

models/Post.js — Blog post model

Defines what a blog post looks like: title, content, author, tags, dates.

models/Comment.js — Comment model

Defines what a comment looks like: text, author, post reference.

routes/posts.js — Post API routes

Handles GET all posts, GET single post, POST new post, PUT update, DELETE.

routes/comments.js — Comment API routes

Handles adding comments to posts and listing comments.

middleware/auth.js — Authentication check

Verifies JWT tokens before allowing write operations.

public/index.html — Blog homepage

Lists all blog posts with titles, excerpts, and dates.

public/post.html — Single post page

Shows full post content with comment section below.

public/css/styles.css — Blog typography

Clean, readable fonts and spacing for long-form content.

public/js/app.js — Frontend JavaScript

Fetches posts from API, renders them, handles comments.

Why This Structure?

You might wonder: why use folders at all? Why not put everything in one place?

Because as projects grow, you might have:

  • Multiple CSS files
  • Many JavaScript files
  • Images, fonts, and other assets

If they're all in one folder, it's chaos. Folders keep things organized.

// BAD — everything in one folder
index.html
styles.css
main.css
dark.css
app.js
utils.js
helper.js

// GOOD — organized in folders
index.html
css/
  styles.css
  main.css
js/
  app.js
  utils.js

The Golden Rule

When you start a project, ALWAYS:

  1. Create the folder structure FIRST
  2. Then fill in the files one by one
  3. Never write everything in one giant file

This is how professional developers work. Always.

Step 1 — Project Setup (Creating the Empty House)

Setting Up the Project

We are going to create an empty project from scratch. This is the exact process professional developers use every single day.

Open Your Terminal

Your terminal (also called "command line" or "console") is where you type commands to talk to your computer.

  • Mac: Open "Terminal" app
  • Windows: Open "PowerShell" or "Command Prompt"
  • VS Code: Press Ctrl+` (backtick) to open the built-in terminal

Step-by-Step Commands

Type each of these commands, one at a time, pressing Enter after each:

mkdir blog-platform && cd blog-platform && git init && npm init -y && npm install express pg bcryptjs jsonwebtoken && mkdir config models routes middleware public public/css public/js

What Just Happened?

Let me explain each command like you're 5:

mkdir [folder] — "Make a new box." This creates a folder on your computer.

cd [folder] — "Go inside the box." Now when you type commands, they happen inside this folder.

git init — "Start keeping a diary." Git will now track every change you make. If you break something, you can go back in time.

npm init -y — "Create an ID card for this project." It creates package.json with default settings. The -y means "yes to all questions."

touch [file] — "Create an empty file." Think of it as a blank piece of paper.

Verify It Worked

Type this command to see your files:

ls -la

The .gitignore File

Open .gitignore and add this:

node_modules/
.DS_Store
*.log
.env

Why? Because node_modules can be HUGE — thousands of files. We don't want to save all of them in Git. We can always recreate them with npm install.

Your First Git Commit

Now save everything to Git:

git add .
git commit -m "Initial project setup"

Congratulations! You just set up a project like a real developer.

Step 2 — Building the Core (Step by Step)

Building the Application

Now let's build the application step by step. Follow each step in order.

Build Steps

  1. Set up PostgreSQL database with posts and comments tables
  2. Create config/database.js with connection pool
  3. Create Post and Comment models
  4. Create routes/posts.js with full CRUD
  5. Create routes/comments.js for post comments
  6. Add auth middleware for protected routes
  7. Build public/index.html with post list
  8. Build public/post.html with full post view
  9. Style with clean blog typography
  10. Write app.js to fetch and render posts
  11. Add markdown support for post content
  12. Add pagination for post list
  13. Test all operations, commit, deploy

How Each Step Works

Step 1 — Create the entry point

Every application starts with an entry file. This is the file that runs first when someone opens your app.

Think of it like the front door of a house. Everything starts when you walk through it.

Step 2 — Set up the structure

Before writing features, we set up the basic layout. This is like putting up the walls before painting them.

Step 3 — Add the main functionality

Now we add what makes the app actually DO something. This is the "brain" of the application.

Step 4 — Connect everything

We wire the pieces together. The HTML connects to CSS. The JavaScript connects to HTML. Everything talks to each other.

Step 5 — Test and fix

We try everything, find bugs, and fix them. Real developers spend 30% of their time testing.

Save and Test

After each step, save your files and refresh the browser. If something breaks, check the browser console (F12 → Console tab).

Commit after each major step:

git add .
git commit -m "Describe what you just built"

Pro Tip

Build in small steps. Don't write 100 lines and then test. Write 10 lines, test, fix, then write 10 more. This catches errors early when they're easy to fix.

Data Design

Database Design

Models

// Post
const postSchema = new mongoose.Schema({
  title: { type: String, required: true, trim: true },
  slug: { type: String, unique: true },
  content: { type: String, required: true },
  excerpt: { type: String, maxlength: 200 },
  author: { type: ObjectId, ref: 'User', required: true },
  category: { type: ObjectId, ref: 'Category' },
  tags: [String],
  featuredImage: String,
  status: { type: String, enum: ['draft', 'published'], default: 'draft' },
  views: { type: Number, default: 0 },
  likes: [{ type: ObjectId, ref: 'User' }]
}, { timestamps: true });

postSchema.index({ title: 'text', content: 'text' });
postSchema.index({ slug: 1 });
postSchema.index({ author: 1, createdAt: -1 });

// Comment
const commentSchema = new mongoose.Schema({
  post: { type: ObjectId, ref: 'Post', required: true },
  author: { type: ObjectId, ref: 'User', required: true },
  content: { type: String, required: true },
  parentComment: { type: ObjectId, ref: 'Comment' },
  likes: [{ type: ObjectId, ref: 'User' }]
}, { timestamps: true });

// Category
const categorySchema = new mongoose.Schema({
  name: { type: String, required: true, unique: true },
  slug: { type: String, unique: true },
  description: String
});

Slug Generation

import slugify from 'slugify';

postSchema.pre('save', function(next) {
  if (this.isModified('title')) {
    this.slug = slugify(this.title, { lower: true, strict: true }) + '-' + Date.now().toString(36);
  }
  next();
});

Relationships

User ──< Post ──< Comment
  │        │        └── parentComment (self-reference)
  │        └── Category
  └── likes (ref arrays on Post and Comment)

API Design

REST API Design

Endpoints

POST   /api/auth/register       — Register
POST   /api/auth/login          — Login

GET    /api/posts               — List posts (published)
POST   /api/posts               — Create post (auth)
GET    /api/posts/:slug         — Get post by slug
PATCH  /api/posts/:id           — Update post (auth, owner)
DELETE /api/posts/:id           — Delete post (auth, owner)

GET    /api/posts/:id/comments  — Get comments
POST   /api/posts/:id/comments  — Add comment (auth)
DELETE /api/comments/:id        — Delete comment (auth, owner)

POST   /api/posts/:id/like      — Toggle like (auth)
GET    /api/categories          — List categories
GET    /api/posts/search?q=...  — Search posts

Pagination

// Cursor-based pagination
router.get('/', async (req, res) => {
  const { limit = 10, cursor, category, tag } = req.query;
  const query = { status: 'published' };

  if (cursor) query.createdAt = { $lt: new Date(cursor) };
  if (category) query.category = category;
  if (tag) query.tags = tag;

  const posts = await Post.find(query)
    .sort({ createdAt: -1 })
    .limit(parseInt(limit) + 1)
    .populate('author', 'name avatar')
    .populate('category', 'name slug');

  const hasMore = posts.length > parseInt(limit);
  const items = hasMore ? posts.slice(0, -1) : posts;
  const nextCursor = hasMore ? items[items.length - 1].createdAt : null;

  res.json({ posts: items, nextCursor, hasMore });
});

Search

router.get('/search', async (req, res) => {
  const { q, page = 1 } = req.query;
  const posts = await Post.find(
    { $text: { $search: q }, status: 'published' },
    { score: { $meta: 'textScore' } }
  )
  .sort({ score: { $meta: 'textScore' } })
  .skip((page - 1) * 10)
  .limit(10);
  res.json(posts);
});

Frontend Architecture

React Frontend

Route Structure

/                    — Home (latest posts)
/posts/:slug        — Single post view
/posts/new          — Create post (auth)
/posts/:id/edit     — Edit post (auth, owner)
/categories/:slug   — Category posts
/search?q=...       — Search results
/login              — Login
/register           — Register
/dashboard          — Author dashboard

Rich Text Editor

import ReactMarkdown from 'react-markdown';
import { useState } from 'react';

function PostEditor({ initialContent, onChange }) {
  const [content, setContent] = useState(initialContent || '');
  const [preview, setPreview] = useState(false);

  return (
    <div className="editor">
      <div className="editor-toolbar">
        <button onClick={() => setPreview(false)}>Write</button>
        <button onClick={() => setPreview(true)}>Preview</button>
      </div>
      {preview ? (
        <div className="preview">
          <ReactMarkdown>{content}</ReactMarkdown>
        </div>
      ) : (
        <textarea
          value={content}
          onChange={e => { setContent(e.target.value); onChange(e.target.value); }}
          placeholder="Write your post in Markdown..."
        />
      )}
    </div>
  );
}

Image Upload

async function uploadImage(file) {
  const formData = new FormData();
  formData.append('image', file);
  const res = await fetch('/api/upload', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${token}` },
    body: formData
  });
  return res.json();
}

Common Mistakes (What Goes Wrong)

Mistakes Everyone Makes

Every developer makes these mistakes. Knowing them ahead of time saves hours of debugging.

Mistake 1: Forgetting to Connect Files

<!-- WRONG — CSS is not linked, page looks ugly -->
<link rel="stylesheet" href="style.css">

<!-- RIGHT — file path matches actual location -->
<link rel="stylesheet" href="css/styles.css">

How to check: Right-click the page → Inspect → Network tab → look for red (failed) resources.

Mistake 2: JavaScript Before HTML

<!-- WRONG — JavaScript runs before HTML exists -->
<script src="app.js"></script>
<body>...</body>

<!-- RIGHT — JavaScript runs after HTML loads -->
<body>...</body>
<script src="app.js"></script>

Mistake 3: Not Using preventDefault()

// WRONG — page reloads when you submit
form.addEventListener('submit', () => {
  // This code runs, but then the page reloads!
});

// RIGHT — preventDefault stops the reload
form.addEventListener('submit', (e) => {
  e.preventDefault(); // Now the page doesn't reload
});

Mistake 4: Modifying State Without Re-rendering

// WRONG — changes data but screen stays the same
todos.push(newTodo);

// RIGHT — always update the screen after changing data
todos.push(newTodo);
render(); // Now the screen shows the new todo

Mistake 5: Not Handling Empty Input

// WRONG — adds empty tasks
addTodo(""); // Adds a blank todo

// RIGHT — check for empty input
if (text.trim()) {
  addTodo(text);
}

Mistake 6: Ignoring Errors

How to check for errors:

  1. Press F12 in your browser
  2. Click "Console" tab
  3. Look for red text
  4. Click on it to see what went wrong

If you see red text, DON'T IGNORE IT. That's the computer telling you something is broken.

Debugging Checklist

When something doesn't work:

  1. Check the console — F12 → Console (red text = error)
  2. Check the Network — F12 → Network (red lines = failed requests)
  3. Check file paths — Are CSS/JS files in the right folder?
  4. Check spelling — Typos in IDs, class names, function names
  5. Check the basics — Is the server running? Is the URL correct?

The 5-Minute Rule

If you've been stuck for 5 minutes:

  1. Stop
  2. Re-read the error message
  3. Search the error on Google/Stack Overflow
  4. If still stuck, ask for help

Don't waste hours on something that might be a simple typo.

Testing Everything Works

Testing Your App

A real developer NEVER assumes code works. They TEST it.

Why Test?

  • Users will find every bug you missed
  • Bugs found later cost 10x more to fix
  • Testing gives you confidence to make changes

Manual Testing Checklist

Go through this checklist for EVERY feature:

□ Page loads without errors (check browser console — F12)
□ All buttons work when clicked
□ All forms submit correctly
□ Input validation works (empty, special characters)
□ Data saves and loads correctly
□ Responsive on mobile (resize browser)
□ No console errors
□ All links work
□ Loading states appear during API calls
□ Error messages show when things fail

How to Check for Errors

Press F12 in your browser. Click the "Console" tab.

  • Empty = Good! No errors.
  • Red text = Bad. Click it to see what went wrong.

Edge Cases to Test

Real users do weird things. Test these:

  1. Empty input — What happens if you submit nothing?
  2. Very long input — Paste a 1000-character string
  3. Special characters — Type < > & " ' / 4. Rapid clicking — Click a button 10 times fast
  4. No internet — Turn off WiFi and try
  5. Refresh mid-action — Reload while something is happening

Browser Testing Tools

Tool How to Open What It Shows
Console F12 → Console JavaScript errors and logs
Network F12 → Network API requests and responses
Elements F12 → Elements Live HTML and CSS
Device Mode Ctrl+Shift+M Mobile screen simulation

Git Commit

After testing, commit your work:

git add .
git commit -m "Complete [project name] with testing"

What You Just Learned

Summary — Skills You Now Have

Technical Skills

  1. Project Structure — You know how to organize files in folders
  2. Git — You can initialize, add, commit, and push
  3. Version Control — You can track changes and go back in time
  4. HTML/CSS/JavaScript — You can build interactive web pages
  5. DOM Manipulation — You can change what's on screen with code
  6. Event Handling — You can respond to clicks, inputs, and other actions
  7. State Management — You can track and update application data
  8. Debugging — You can find and fix errors using browser tools
  9. Testing — You can verify your app works correctly

Thinking Skills

  1. Architecture — You planned the project BEFORE writing code
  2. Problem Solving — You broke a big problem into small steps
  3. Debugging — You systematically found and fixed errors
  4. Documentation — You wrote notes for future you (and other developers)

The Pattern

Every frontend application follows this pattern:

State (data) → Render (draw) → Event (respond) → Update State → Render again

React, Vue, Angular, Svelte — they ALL do this. You just learned the fundamental pattern that powers the entire web.

What's Next?

Look at the next project in the roadmap. Each new project builds on what you learned here. The skills compound — every project makes you a better developer.

Remember: every expert was once a beginner who didn't give up.

Quick Reference

Quick Reference

File Overview

  • server.js: Express server entry point
  • config/database.js: Database connection
  • models/Post.js: Blog post model
  • models/Comment.js: Comment model
  • routes/posts.js: Post API routes
  • routes/comments.js: Comment API routes
  • middleware/auth.js: Authentication check
  • public/index.html: Blog homepage
  • public/post.html: Single post page
  • public/css/styles.css: Blog typography
  • public/js/app.js: Frontend JavaScript

Setup Commands

mkdir blog-platform && cd blog-platform && git init && npm init -y && npm install express pg bcryptjs jsonwebtoken && mkdir config models routes middleware public public/css public/js

Common Patterns

// Select an element
const el = document.getElementById('myId');

// Add event listener
el.addEventListener('click', (e) => {
  e.preventDefault();
});

// Update the DOM
el.innerHTML = '<p>New content</p>';
el.textContent = 'Text only';

Debugging

F12 → Console     → Check for errors
F12 → Network     → Check API requests
F12 → Elements    → Inspect HTML/CSS
Ctrl+Shift+M      → Mobile view

Git Commands

git init                  # Start a repo
git add .                 # Stage all changes
git commit -m "message"   # Save changes
git push                  # Upload to GitHub

Quiz

1. Why use cursor-based pagination over offset-based?

Question 1 options

2. What is a text index in MongoDB?

Question 2 options

Flashcards

Question

What is a slug?

Answer

A URL-friendly version of a title (e.g., "my-first-post-abc123") used in place of IDs

Question

What is cursor-based pagination?

Answer

Using a pointer (cursor) to the last item to fetch the next page, avoiding offset issues

Revision Notes

Key Takeaways

  • 1.Multi-model design requires careful relationship planning
  • 2.Text indexes enable search without external services
  • 3.Cursor pagination is more reliable than offset
  • 4.Rich text editing needs Markdown or WYSIWYG
  • 5.Image upload requires multipart form handling

Interview Tips

  • Design the blog API endpoints
  • Explain pagination tradeoffs
  • Discuss search implementation options

Cheat Sheet

Blog Platform

Slug

slugify(title, { lower: true, strict: true }) + '-' + Date.now().toString(36);

Text Search

Post.find({ $text: { $search: query } }, { score: { $meta: 'textScore' } })
  .sort({ score: { $meta: 'textScore' } });

Cursor Pagination

query.createdAt = { $lt: new Date(cursor) };