← Back to Notes

Medium Clone with NextJs, Convex, and Clerk

Hamed Bahram /
5 min read--- views

Are you ready to build a full-featured, real-time blogging platform similar to Medium? Look no further! In this blog post, we'll dive into an exciting video tutorial that walks you through the process of creating a Medium clone using cutting-edge technologies and techniques.

Source code

Enter your Github username and email to access the source code.

What You'll Learn

Our tutorial covers the creation of a modern blogging platform with real-time capabilities. You'll learn how to:

  1. Set up a Next.js project for a blogging platform
  2. Implement secure user authentication with Clerk
  3. Create a real-time database and file storage system using Convex
  4. Build a full-featured article publishing and reading experience
  5. Implement real-time likes and updates across all users

Tech Stack Overview

The tutorial leverages a powerful combination of technologies:

  • NextJs: A React framework for building server-side rendered and static web applications
  • Convex: A backend platform providing real-time database and file storage capabilities
  • Clerk: A complete user management and authentication solution

Key Features of the Medium Clone

  1. User Authentication: Secure sign-up and login functionality using Clerk
  2. Article Creation and Publishing: Allow users to write and publish articles
  3. Real-Time Updates: Instant updates for likes and new articles across all users
  4. File Storage: Handle image uploads for article content
  5. User Data Synchronization: Use Clerk webhooks to sync user data with Convex backend

Let's dive into some code examples to see how these features are implemented.

Deep Dive: Integrating Convex and Clerk

1. Setting up Convex Schema

First, let's define our Convex schema for articles:

// convex/schema.ts
 
import { defineSchema, defineTable } from 'convex/server'
import { v } from 'convex/values'
 
export default defineSchema({
  users: defineTable({
    email: v.string(),
    clerkUserId: v.string(),
    firstName: v.optional(v.string()),
    lastName: v.optional(v.string()),
    imageUrl: v.optional(v.string()),
    posts: v.optional(v.array(v.id('posts')))
  }).index('byClerkUserId', ['clerkUserId']),
  posts: defineTable({
    title: v.string(),
    slug: v.string(),
    excerpt: v.string(),
    content: v.string(),
    coverImageId: v.optional(v.id('_storage')),
    authorId: v.id('users'),
    likes: v.number()
  }).index('bySlug', ['slug'])
})

2. Implementing Article Creation

Here's how we might implement article creation using Convex:

// convex/articles.ts
 
import { v } from 'convex/values'
import { query, mutation } from './_generated/server'
import { getCurrentUserOrThrow } from './users'
 
export const createPost = mutation({
  args: {
    title: v.string(),
    slug: v.string(),
    excerpt: v.string(),
    content: v.string(),
    coverImageId: v.optional(v.id('_storage'))
  },
  handler: async (ctx, args) => {
    const user = await getCurrentUserOrThrow(ctx)
 
    const data = {
      ...args,
      authorId: user._id,
      likes: 0
    }
 
    await ctx.db.insert('posts', data)
    return data.slug
  }
})

3. Real-Time Article Fetching

To fetch articles in real-time, we can use Convex queries:

// convex/articles.ts
 
export const getPosts = query({
  args: {},
  handler: async ctx => {
    const posts = await ctx.db.query('posts').order('desc').collect()
    return Promise.all(
      posts.map(async post => {
        const author = await ctx.db.get(post.authorId)
 
        return {
          ...post,
          author,
          ...(post.coverImageId
            ? {
                coverImageUrl:
                  (await ctx.storage.getUrl(post.coverImageId)) ?? ''
              }
            : {})
        }
      })
    )
  }
})

4. Implementing Real-Time Likes

Here's how we can implement real-time likes using Convex mutations:

// convex/articles.ts
 
import { mutation } from './_generated/server'
 
export const likePost = mutation({
  args: { slug: v.string() },
  handler: async (ctx, { slug }) => {
    const user = await getCurrentUserOrThrow(ctx)
 
    const post = await ctx.db
      .query('posts')
      .withIndex('bySlug', q => q.eq('slug', slug))
      .unique()
 
    if (!post) {
      return null
    }
 
    if (post.authorId === user._id) {
      return null
    }
 
    await ctx.db.patch(post._id, { likes: post.likes + 1 })
  }
})

Syncing Clerk User Data with Convex

To keep user data in sync between Clerk and Convex, we'll use Convex's HTTP endpoints to create a POST endpoint that receives Clerk webhooks. This approach allows us to handle user creation, updates, and deletions directly within our Convex backend.

Here's how we implement this using Convex's http.route:

// convex/http.ts
 
import { httpRouter } from 'convex/server'
import { internal } from './_generated/api'
import { httpAction } from './_generated/server'
 
const http = httpRouter()
 
http.route({
  path: '/clerk-users-webhook',
  method: 'POST',
  handler: httpAction(async (ctx, request) => {
    const event = await validateRequest(request)
 
    if (!event) {
      return new Response('Error occurred', { status: 400 })
    }
 
    switch (event.type) {
      case 'user.created': // intentional fallthrough
      case 'user.updated':
        await ctx.runMutation(internal.users.upsertFromClerk, {
          data: event.data
        })
        break
      case 'user.deleted': {
        const clerkUserId = event.data.id!
        await ctx.runMutation(internal.users.deleteFromClerk, {
          clerkUserId
        })
        break
      }
      default:
        console.log('Ignored Clerk webhook event', event.type)
    }
 
    return new Response(null, { status: 200 })
  })
})
 
export default http

In this implementation:

  1. We create a POST route at /clerk-users-webhook to receive Clerk webhook events.
  2. The validateRequest function (not shown) would verify the webhook's authenticity.
  3. We handle three types of events:
    • user.created and user.updated: These events trigger the upsertFromClerk mutation to create or update user data in Convex.
    • user.deleted: This event triggers the deleteFromClerk mutation to remove the user from Convex.
  4. Any other event types are logged but not acted upon.

This approach allows for real-time synchronization of user data between Clerk and Convex, ensuring that your application always has the most up-to-date user information available for use in queries and mutations.

Conclusion

This video tutorial offers a comprehensive guide to building a modern, real-time blogging platform similar to Medium. The combination of Next.js, Convex, and Clerk creates a powerful system that allows for real-time updates, secure authentication, and seamless data management.

Whether you're building a personal blog or a large-scale publishing platform, the techniques and technologies covered in this tutorial will provide you with the tools you need to create a feature-rich, real-time, and user-friendly blogging experience.

Don't miss out on this opportunity to level up your full-stack development skills and create an impressive Medium clone. Watch the tutorial now and start building your next-generation blogging platform today!