How to Build a Blog Subscription Wall with Clerk and ContentLayer for Your MDX Blog
In this article, we will build a subscription wall for your MDX blog using Clerk, an easy-to-use auth provider, and integrate it into our blog using MDX and ContentLayer.
Felix Vemmer
April 9, 2024
I began this blog on Ghost, a popular blogging platform. A standout feature of Ghost that I appreciated is the option to share some content publicly and keep other content private for subscribers.
After moving to my custom blog using Next.js and MDX through ContentLayer, I aimed to include a subscription wall to begin growing my audience.
To secure access to my blog, I chose Clerk because it's a simple and intuitive authentication provider that works very well with Next.js. Below, you'll find a post where I compared Clerk to Supabase Auth and delved a bit more into detail on why I prefer Clerk.
The easiest way to get started with Clerk is to use their Next.js integration.
Since their tutorial is well done, I will not delve into detail on how to set up Clerk. I'll just provide a step-by-step guide on creating a subscription wall.
By the end of following the quickstart, you should have set up Clerk in your Next.js app with the following files:
For the UI, depending on the mode and sign-up/sign-in state, we render either a sign-up, sign-in, or verification code form.
If the user has signed up or signed in, we display a verification code form where the user can enter the verification code that was sent to their email address.
Finally, we need to handle the form submission when a user tries to sign up/sign in or enters the verification code. For each route, I created two separate functions onSubmit and onVerify.
First, we check if the sign-up or sign-in API is loaded.
If it is, we check if the mode is sign-up or sign-in and handle the sign-up or sign-in flow accordingly.
If the completeSignUp.status === 'complete', we set an active session for the user.
const onVerify = async (values: TSubscribe) => { if (!isLoadedSignUp) { return } if (!values.code) { toast.error('Please enter the verification code sent to your email.') return } if (form.watch('mode') === 'sign-up') { try { const completeSignUp = await signUp.attemptEmailAddressVerification({ code: values.code, }) if (completeSignUp.status !== 'complete') { /* investigate the response, to see if there was an error or if the user needs to complete more steps.*/ console.log(JSON.stringify(completeSignUp, null, 2)) } if (completeSignUp.status === 'complete') { await setActive({ session: completeSignUp.createdSessionId }) return } } catch (err: any) { const message = err?.errors[0]?.message || 'There was an error verifying your email code.' toast.error(message) console.error(JSON.stringify(err, null, 2)) return } } else { if (!signIn) return if (!values.code) { form.setError('code', { message: 'Please enter the verification code sent to your email.', }) return } try { // Use the code provided by the user and attempt verification const completeSignIn = await signIn.attemptFirstFactor({ strategy: 'email_code', code: values.code, }) // This mainly for debuggin while developing. // Once your Instance is setup this should not be required. if (completeSignIn.status !== 'complete') { console.error(JSON.stringify(completeSignIn, null, 2)) return } // If verification was completed, create a session for the user if (completeSignIn.status === 'complete') { await setActive({ session: completeSignIn.createdSessionId }) return } } catch (err: any) { const message = err?.errors[0]?.message || 'There was an error verifying your email code.' toast.error(message) console.error(JSON.stringify(err, null, 2)) return } }}
Since we want to show a teaser of the content to the user before they are authenticated, we need a way to modify the showContentTeaser state. For ContentLayer, I did not manage to easily pass props to the ProtectedContent component, so I created a custom hook using Jotai:
use-content-teaser.ts
import { atom, useAtom } from 'jotai'const showContentTeaser = atom(false)export function useContentTeaser() { return useAtom(showContentTeaser)}
Now that we have the ProtectedContent component ready, we can use it to protect our content in any MDX file by simply wrapping the content in the ProtectedContent component.
# Protected Content Dummy ExamplePublic Content<ProtectedContent> Subscriber Only Content</ProtectedContent>
To ensure that this works for Google, you can utilize the Rich Results Test at https://search.google.com/test/rich-results to determine if Google can successfully access your 'ProtectedContent' component.
Testing it on this blog post reveals that I could view the protected content:
Google ought to index this.
within this ProtectedContent component.
The subsequent step involves crafting a card component that will display to users who are not logged in.<ProtectedContent>Google should be able to index this.</ProtectedContent>
This concludes the guide on how to create a subscription wall for your MDX blog using Clerk.
I hope this guide has been helpful to you. I'm eager to hear your feedback or see you as a new subscriber in my Clerk dashboard. For those interested in adding a guest book to their website, Brian has written an excellent post on integrating a guestbook using Clerk, Neon, and Netlify Functions.
If you're curious about how my blog subscribers are growing, follow me on X so you don't miss out.
Just built and deployed a new feature with @ClerkDev creating a custom contentlayer component which creates a sort of "Subscription Wall" for blog content. 馃 Kinda same what you see on @Medium and @Ghost I have now on my own blog 馃コ