TL;DR View the demo View the source code Install the Premium Content Blog You have great ideas. Your head is overflowing with content that you people will pay to read. So where do you start? You’re likely inclined to choose a trusted platform like Wordpress, but since you want to offer paid content to your users you now face the problem of making a cumbersome solution even more so. know Instead of tweaking a platform to work with for us, we want a simple, straightforward solution with no more functionality than we need. This, of course, means we’re going to build it ourselves. In the spirit of a simple, straight forward solution that works exactly the way we want it to, we’ll be using to host our blog, manage it’s users, and store it’s content. Cosmic JS Our project will be a play in two parts. First, we’ll build our blog with Express and Cosmic JS and use Stripe to handle the blog’s payments and subcriptions. Then, we’ll leverage Cosmic JS’s Extension feature to build a dashboard that gives us an overview of our business’ backend. Part 1: Building the Blog 1. Boilerplate Setup To save time on boilerplate, we’ll use Yeoman and the Express Generator (which builds on Express’ official generator) to get started. If you don’t have Yeoman installed, run . Then install the generator with and run it with . Follow the instructions to set up your project under a new directory (say ), install the version, and use Handlebars for your view engine. npm i -g yo npm i -g generator-express yo express CosmicUserBlog Basic Your directory structure is now this: CosmicUserBlog||--bin| |--www|--(node_modules)|--public|--routes| |--users.js|--index.js|--views|--layouts|--partials|--error.handlebars|--index.handlebars|--.bowerrc|--.gitignore|--app.js|--bower.json|--gruntfile.js|--package.json 2. Installations We’ll be using the following packages: Async — A powerful async utilities library Axios — Simple, promise based http requests Cors — Standard cors middleware brcypt — For password hashing. (If you’re on Windows read ) these notes CosmicJs — The official client Express Session — So our users can log in dateformat — An intuitive date formatter that we’ll use with posts Stripe — The official client TruncateHTML — for post blurbs You could install these with npm, but I advocate for Yarn. It’s significantly faster and we have no time to waste. So install Yarn (on macOS we’ll do a ) then run . We're almost ready to start building. brew install yarn yarn add async axios cors bcrypt cosmicjs expres-session dateformat stripe truncate-html 3. Set Up Cosmic JS Before we start building, we’ll need to work out the schema for our Cosmic Bucket. We want to store , , and (to edit site configurations on the fly). Posts Users Configs Those three object types will have the following matafields (all of type , given by their ): text Title Post: MetafieldValuePremium or true false User: MetafieldValueFirst namestringLast namestringPasswordHashed StringEmailstringStripe IdstringSubsription Typestring Config: : | Metafield | Value | | — — — — — — — — | — — -: | | Monthly Price | string | | Quarterly Price | string | | Yearly Price | string | | Cancellations | string | Object: Subscriptions : | Metafield | Value | | — — — — — | — — -: | | Site Title | string | | Domain | String | Object: Site Once you’ve added your , , and object types and created your and Config objects, we'll get ourselves set up with Stripe. Post User Config Subscriptions Site 4. Configure Stripe Since we’ll be charging users for their premium subscriptions we’ll need a payment processor. With a robust API, fair pay-as-you-go pricing, and proven security, using Stripe is a no brainer. Moving foward, we’ll need both a “Publishable” and a “Secret” key for Stripe’s API and we need to setup Subscription plans for Monthly, Yearly, and Quarterly subscriptions. Follow Stripe’s instructions to create these subscriptions and give them the ID’s , , and accordingly. subscription-monthly subscription-quarterly subscription-yearly 5. Configure the Express App We have our packages installed, we worked out our data schema, and we’ve set up a a Stripe account. Now we need to configure our Express backend. The boilerplate Express code is pre-ES5, so for a consistent style we’ll the packages we need. require At the top of the Express app add: // app.jsvar session = require('express-session')var dateFormat = require('date-format')var truncate = require('truncate-html')var cors = require('cors') When we deploy our app, Cosmic JS will provide our Bucket keys as well as any custom keys we provide via . Below the require statements, go ahead make those accessible throughout the app by storing them in process.env app.locals //app.js var config = {bucket: {slug: process.env.COSMIC_BUCKET,read_key: process.env.COSMIC_READ_KEY,write_key: process.env.COSMIC_WRITE_KEY}}app.locals.config = configapp.locals.stripeKeyPublishable = process.env.STRIPE_PUBLISHABLE_KEYapp.locals.stripeKeySecret = process.env.STRIPE_SECRET_KEY The last step is then to connect the and middleware like so: cors session //app.js app.locals.stripeKeySecret = process.env.STRIPE_SECRET_KEY app.use(session({secret: 'sjcimsoc',resave: false,saveUninitialized: true,cookie: { secure: false }}))app.use(cors()) At this point we have a solid base to build our app on and can start drafting out it’s views. 6. Sculpt Out the Views Having a model of how we want our blog to look and feel will help us think about how to wire up it’s routes. We’ll start with the main layout. The Main Layout: In handlebars, every page will render inside the tag of a default layout. In our boilerplate, this is . body /views/layouts/main.handlebars We need to make three alterations. in the tag, swap out for , which we'll pass via later. title {{title}} {{config.site_title}} res.locals Before the end of the tag add . This is Stripe's browser client. We only need this for the checkout form, however Stripe reccomends including it on every page to aid in fraud detection. head <script src="https://js.stripe.com/v3/"></script> Include Bootstrap. Somewhere in the tag add and right before the end of the tag add head <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" /> body <script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script><script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> The Main layout now looks like this: <!-- views/layouts/main.handlebars --><!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><title>{{config.site_title}}</title><link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />{{#if ENV_DEVELOPMENT}}<script src="http://localhost:35729/livereload.js"></script>{{/if}}<script src="https://js.stripe.com/v3/"></script> </head><body> {{{body}}} <scriptsrc="https://code.jquery.com/jquery-3.2.1.min.js"integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="crossorigin="anonymous"></script><script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script></body></html> The Header Partial: Handlebars expects partials to be found in the directory by default so we'll make there. It will look like this: /views/partials header.handlebars <header><div class="container"><div class="row"><div class="col-xs-12 text-center"><h1>{{config.site_title}}</h1></div></div></div></header> <nav class="navbar navbar-default"><div class="container-fluid"><div class="navbar-header"><button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse-header" aria-expanded="false"><span class="sr-only">Toggle navigation</span><span class="icon-bar"></span><span class="icon-bar"></span><span class="icon-bar"></span></button></div> <div class="collapse navbar-collapse" id="navbar-collapse-header"> <ul class="nav navbar-nav"> <li {{#if route\_posts}}class="active"{{/if}}><a href="/posts">Posts<span class="sr-only">(current)</span></a></li> <li><a href="/premium">Premium Content</a></li> </ul> <ul class="nav navbar-nav navbar-right"> {{#if logged\_in}} <li class="navbar-text text-center">Welcome Back, {{user.first\_name}}!</li> <li><a href="/logout">Logout</a></li> {{/if}} {{#unless logged\_in}} <li {{#if route\_login}}class="active"{{/if}}><a href="/login">Login</a></li> <li {{#if route\_signup}}class="active"{{/if}}><a href="/plans">Sign Up</a></li> {{/unless}} </ul> </div> </div></nav> In the header partial we take advantage of Handlebars’ built in and helpers to make parts of our code route-specific. The we'll pass the relative booleans later in the route handlers. Note: if unless The Posts View On the Posts page we want to: Display the header Show an error message if the user is trying to access premium posts without an account Abstract the post display logic to it’s own partial to make our code cleaner and modular. For the error message, we’ll rely on Handlebars’ helper as we did before. For showing summaries of the posts, we'll be passing the posts to the view as an array of post objects. This lets us use the Handlebars block helper to iterate over that array (each post being accessible as ). Our Posts view will then look like this: if each this {{> header}}<div class="container"><div class="row">{{#if error}}<div class="alert alert-danger" role="alert">{{error}}</div>{{/if}}{{#each posts}}{{> post-container this}}{{/each}}</div></div> The Post Container partial: The next obvious step is to build the container to show summaries of each post on the Posts page. Beyond displaying the post’s title and content (which is easily accessed with , etc.) we want to show the user if the post is premium, as well as a truncation of the post's body and it's creation date. this.title We’ll again turn to the helper to show a star beside the post if returns , which is simple enough. For the blurb and creation date, we need to modify the and properties of the Post object; in the first place, to shorten it, in the latter, because Cosmic stores the date in ISO datetime format. To keep display logic out of our views, Handlebars provides us with functionality to write own helpers. if this.metadata.premium true content created_at First, get the view code in place: <!-- /views/partials/post-container.handlebars --> <div style="border-bottom: 3px solid #337ab7" class="col-xs-12 col-md-8 col-md-offset-2"><h2>{{#if this.metadata.premium}}<span style="font-size: 0.5em" class="glyphicon glyphicon-star"></span>{{/if}}<a href="/post/{{this.slug}}">{{this.title}}</a></h2><p style="margin: 35px 40px">{{truncateText this.content 20}} <a href="/post/{{this.slug}}">Read more</a></p><em style="margin: 20px 0" class="pull-right">{{date this.created_at}}</em></div> (Hint: our handlebars helpers will be named and .) truncateText date Open and find the snippet of code that sets Handlebars as the view engine. is a reference to and the object passed to it contains the parameters used to instantiate the engine. We need to add the property to that object. The property will then point to the and methods as follows: app.js exphbs express-handlebars helpers helpers date truncateText // app.js // etc...app.engine('handlebars', exphbs({defaultLayout: 'main',partialsDir: ['views/partials/'],helpers: {date: function(date) {return dateFormat(new Date(date), "dddd, mmmm dS, yyyy")},truncateText: function(text, length) {return truncate(text, length, { stripTags: true, byWords: true })}}}));app.set('views', path.join(__dirname, 'views'));app.set('view engine', 'handlebars');// etc... To illustrate, tells Handlebars to render the result of . {{truncateText this.content 20}} truncate(this.content, 20, {...} ) The Post View: Our users need to be able to read individual posts, so we’ll build a simple view that get’s passed an array that’s one post long. (In the future, outside the scope of this guide, we may find it useful to use the view to display multiple full posts in succession.) We’ll also display an error if the post isn’t found. Create your post view like so: <!-- /views/post.handlebars --> {{> header}}{{#if not_found}}<div class="container"><div class="row text-center"><div class="col-xs12 col-md-6 col-md-offset-3"><div class="alert alert-danger">Post Not Found!</div><a href="/posts">See All Posts</a></div></div></div>{{/if}} {{#each post}}<article class="container"><div class="row"><div class="col-xs-12 col-sm-8 col-sm-offset-2"><h1>{{this.title}}</h1><div class="lead" style="font-size: 1.5em">{{{this.content}}}</div><em class="pull-right"><time>{{date this.created_at}}</time></em></div></div></article>{{/each}} The Login Page: The login page will be the simplest yet — a basic form that POSTs to a login route. <!-- /views/login.handlebars --> {{> header}} <div class="container"><div class="row"><div class="col-xs-12 col-sm-4 col-sm-offset-4"><h1>Log In</h1><p class="text-muted">Log in to view premium content.</p> <form method="post"> <input class="form-control" type="email" name="email" placeholder="Email" required/> <input type="password" class="form-control" name="password" placeholder="Password" required /> <button class="btn btn-lg btn-primary btn-block submit-btn" type="submit">Log in</button> </form> </div> </div></div> <style>form > input, button {margin-top: 12px;}</style> The Plans Page: Obviously, our users will need to be able to signup before they can login, but since we’re giving them the option to choose one of three subscription plans we’ll build out a view that shows them their options just before checking out. Later, we’ll need to pass the Subscriptions object to this view so we can set the plan prices from Cosmic, rather than hard coding them. The view will look like this: <!-- /views/plans.handlebars --> {{> header}} <div class="container"><div class="row text-center lead"><h1>Choose a Plan to Read Premium Content</h1><p>Sign up to view Premium Content on {{config.site_title}}</p></div> <div class="row"><div class="col-sm-4"><h3 class="text-center text-muted">Monthly</h3><p class="text-center"><strong>Billed every month</strong></p><h1 class="text-center text-success">{{subscriptions.monthly_price}}</h1 class="text-center text-success"><ul class="list-unstyled lead" style="padding: 0 20px"><li><span class="glyphicon glyphicon-ok text-success"></span>Here's a good benefit</li><li><span class="glyphicon glyphicon-ok text-success"></span>A reason to buy</li><li><span class="glyphicon glyphicon-ok text-success"></span>Why you have to have it</li><li><span class="glyphicon glyphicon-ok text-success"></span>Why you shouldn't miss out</li><li><span class="glyphicon glyphicon-ok text-success"></span>Believe it.</li></ul><a href="/signup?plan=monthly"><button class="btn btn-block btn-default btn-lg">Sign Up</button></a></div><div class="col-sm-4" style="border: 2px solid #3c763d"><h3 style="background: #3c763d;color: white;padding: 7px 0" class="text-center text-success">Yearly</h3><p class="text-center"><strong>Billed every 12 months</strong></p><h1 class="text-center text-success">{{subscriptions.yearly_price}}</h1><ul class="list-unstyled lead" style="padding: 0 20px"><li><span class="glyphicon glyphicon-ok text-success"></span>Here's a good benefit</li><li><span class="glyphicon glyphicon-ok text-success"></span>A reason to buy</li><li><span class="glyphicon glyphicon-ok text-success"></span>Why you have to have it</li><li><span class="glyphicon glyphicon-ok text-success"></span>Why you shouldn't miss out</li><li><span class="glyphicon glyphicon-ok text-success"></span>Believe it.</li></ul><a href="/signup?plan=yearly"><button class="btn btn-block btn-default btn-lg">Sign Up</button></a></div><div class="col-sm-4"><h3 class="text-center text-muted">Quarterly</h3><p class="text-center"><strong>Billed every 3 months</strong></p><h1 class="text-center text-success">{{subscriptions.quarterly_price}}</h1 class="text-center text-success"><ul class="list-unstyled lead" style="padding: 0 30px"><li><span class="glyphicon glyphicon-ok text-success"></span>Here's a good benefit</li><li><span class="glyphicon glyphicon-ok text-success"></span>A reason to buy</li><li><span class="glyphicon glyphicon-ok text-success"></span>Why you have to have it</li><li><span class="glyphicon glyphicon-ok text-success"></span>Why you shouldn't miss out</li><li><span class="glyphicon glyphicon-ok text-success"></span>Believe it.</li></ul><a href="/signup?plan=quarterly"><button class="btn btn-block btn-default btn-lg">Sign Up</button></a></div></div></div> <style>form > input, button {margin-top: 12px;}ul.lead {margin-top: 40px}.lead > li {margin: 8px 0}.glyphicon {margin-right: 12px}.btn {margin: 35px 0px;}</style> Notice that each Sign Up button links to the _signup_ route, passing a query paramter associated with the plan selected. I.e. _/signup?plan={:plan}_ The Signup Page: Last, but certainly not least — the money maker. We’ve saved our most complicated view for last. These are our requirements: We need to pass the plan chosen on the page before in . (Later we'll do this in a URL query string) planName We need to use the Stripe Elements API for collecting credit card information. This is what we included in the Main layout earlier. At the end of our checkout form we need two s; one ID'd and the other ID'd for to inject into after the initial DOM rendering. Stripe.js div card-element card-errors Stripe.js We need to pass our publishable stripe key to make it all work. Starting with the HTML we have: <!-- /views/signup.handlebars -->{{>header}} <div class="container"><div class="row"><form method="post" id="payment-form"><div class="col-xs-12 text-center"><h4 class="lead"><em>You're one step away from a <u>{{planName}}</u> subscription to {{config.site_title}}!</em></h4></div></div><div class="row" style="margin-top: 30px"><div class="col-md-8 col-md-offset-2"><h4>Enter your account details and payment information</h4> <label for="first\_name">First name:</label> <input type="text" name="first\_name" class="form-control" placeholder="First name" required /> <label for="last\_name">Last name:</label> <input type="text" name="last\_name" class="form-control" placeholder="Last name" required /> <label for="email">Email:</label> <input type="email" name="email" class="form-control" placeholder="Email" required /> <label for="password">Password</label> <input type="password" name="password" class="form-control" placeholder="Password" required /> <label for="card-element">Credit or debit card</label> <div class="form-control" id="card-element"></div> <div id="card-errors" role="alert"></div> <button id="submit-button" class="btn-success btn btn-lg btn-block btn-default" >Submit Payment</button> </div> </div> </form> </div> Then, for simplicity, we’ll follow this with an inline script that integrates Stripe: <!-- views/signup.handlebars --> <script>var stripe = Stripe('{{stripeKeyPublishable}}')var elements = stripe.elements()var card = elements.create('card')var style = {base: {color: '#32325d',lineHeight: '24px',fontFamily: '"Helvetica Neue", Helvetica, sans-serif',fontSmoothing: 'antialiased',fontSize: '16px','::placeholder': {color: '#aab7c4'}},invalid: {color: '#fa755a',iconColor: '#fa755a'}}; card.mount('#card-element', {style: style}) // Handle real-time validation errors from the card Element.card.addEventListener('change', function(event) {var displayError = document.getElementById('card-errors');if (event.error) {displayError.textContent = event.error.message;} else {displayError.textContent = '';}}); // Handle form submissionvar form = document.getElementById('payment-form');var submitButton = document.getElementById('submit-button');form.addEventListener('submit', function(event) {event.preventDefault();submitButton.disabled=truestripe.createToken(card).then(function(result) {if (result.error) {// Inform the user if there was an errorvar errorElement = document.getElementById('card-errors');errorElement.textContent = result.error.message;submitButton.disabled=false} else {// Send the token to your serverstripeTokenHandler(result.token);}});}); function stripeTokenHandler(token) {// Insert the token ID into the form so it gets submitted to the servervar form = document.getElementById('payment-form');var hiddenInput = document.createElement('input');hiddenInput.setAttribute('type', 'hidden');hiddenInput.setAttribute('name', 'stripeToken');hiddenInput.setAttribute('value', token.id);form.appendChild(hiddenInput);// Submit the formform.submit();}</script> <style>label,button {margin-top: 22px;}</style> Here’s what’s going on: We instantiate Stripe with the Publishable key we passed, assign its library to it's own variable, use that to create a element, and finally mount that to the we created earlier. The card object handles card validation, comes packaged with good UX features, and reports errors back to the user in real time. elements card <div id="card-element"> We attach an event listener to the card object that responds to any change in either the user-inputted card number, CCV, or expiration date. It mounts that error on <div id="card-errors"> We handle the form submission manually. First, we prevent the default action and disable multiple submissions. Then, barring no errors, we attach a hidden field to the form that contains Stripe’s validation token, provided by , and submit the form to the route. Stripe.js then Signup 7. Build the Routes With our views built, we know exactly what routes our application needs. Namely: Posts Post Premium Login Logout Signup Plans By default, your app has a route and an route. Delete and make redirect to like so: Users Index Users Index Posts // /routes/index.js var express = require('express')var router = express.Router() router.get('/', function(req, res) {res.redirect('/posts')}); module.exports = router The Posts Route: Posts will use to string together a series of async functions: one using the Cosmic JS client to get our Posts, the next using Cosmic to get the site config and render the view, passing in the relevant locals. If the user is not in an authenticated session, we'll use to filter out the posts returned from Cosmic which been labelled premium (and are therefore free to read). We'll pass the Posts, Config, and route-specific view data via . async posts lodash have not res.locals var express = require('express');var router = express.Router();var cosmic = require('cosmicjs');var async = require('async');var _ = require('lodash') router.get('/', function(req, res) {async.series([function(cb) {cosmic.getObjectType(req.app.locals.config, { type_slug: 'posts' }, function(err, response) {(req.session.user) ? res.locals.posts = response.objects.all : res.locals.posts = _.filter(response.objects.all, function(post) {return !post.metadata.premium})cb()})},function(cb) {cosmic.getObject(req.app.locals.config, { slug: 'site' }, function(err, response) {res.locals.config = response.object.metadatares.locals.user = req.session.userres.locals.route_posts = trueif (req.session.user) res.locals.logged_in = truereturn res.render('posts.handlebars')})=}])}); module.exports = router; The Post Route: Having a route for all posts, we’ll need a companion route for a singular post that takes the post slug as a URL parameter and returns that post if it’s found, utilizing similar logic as the route. Posts // routes/post.js var express = require('express');var router = express.Router();var cosmic = require('cosmicjs');var async = require('async');var _ = require('lodash') router.get('/:slug', function(req, res) {async.series([function(cb) {cosmic.getObjectType(req.app.locals.config, { type_slug: 'posts' }, function(err, response) {res.locals.post = _.filter(response.objects.all, function(post) {return post.slug === req.params.slug})if (!res.locals.post) res.locals.not_found = truecb()})},function(cb) {cosmic.getObject(req.app.locals.config, { slug: 'site' }, function(err, response) {res.locals.config = response.object.metadatares.locals.user = req.session.userreturn res.render('post.handlebars')})}])}); module.exports = router The Login Route: To let users have access to premium posts, we’ll need to collect their login information, hash their password with bcrypt, and check it against the password associated with the email address stored in Cosmic. A GET request will render the login form. We then handle the form’s POST request by retrieiving all users from Cosmic and iteratating over all users with a series of two async functions: one using bcrypt to compare password hashes, the next saving the user’s data into a session if they’re found. // routes/login.js var express = require('express');var router = express.Router();var cosmic = require('cosmicjs');var async = require('async');var _ = require('lodash')var bcrypt = require('bcrypt') router.get('/', function(req, res) {async.series([function(cb) {cosmic.getObject(req.app.locals.config, { slug: 'site' }, function(err, response) {res.locals.config = response.object.metadatares.locals.route_login = truereturn res.render('login.handlebars')})}])}); router.post('/', function(req, res) {cosmic.getObjectType(req.app.locals.config, { type_slug: 'users' }, function (err, response) {if (err) res.status(500).json({ status: 'error', data: response })else {async.eachSeries(response.objects.all, function (user, eachCb) {if (!_.find(user.metafields, { key: 'email', value: req.body.email.trim().toLowerCase() }))return eachCb()const stored_password = _.find(user.metafields, { key: 'password' }).valuebcrypt.compare(req.body.password, stored_password, function (err, correct) {if (correct) res.locals.user_found = usereachCb()})}, function () {if (res.locals.user_found) {req.session.user = {first_name: res.locals.user_found.metafield.first_name.value,last_name: res.locals.user_found.metafield.last_name.value,email: res.locals.user_found.metafield.email.value}req.session.save()return res.redirect('/posts')}return res.status(404).json({ status: 'error', message: 'User not found' })})}})}) module.exports = router The Plans Route: Before a user can log in, we need a sign up form so we can have users in the first place. As said before, we’ll show them their subscription options right before the signup form. This does nothing more than pass the Site and Subscription configs before rendering the view: var express = require('express');var router = express.Router();var cosmic = require('cosmicjs');var async = require('async') router.get('/', function(req, res) {async.series([function(cb) {cosmic.getObject(req.app.locals.config, { slug: 'site' }, function(err, response) {res.locals.config = response.object.metadatares.locals.route_signup = truecb()})},function(cb) {cosmic.getObject(req.app.locals.config, { slug: 'subscriptions' }, function(err, response) {res.locals.subscriptions = response.object.metadatares.render('plans.handlebars')})}])}); module.exports = router; The Signup Route: As you might have guessed, the signup route has the most going on out of them all. Here’s what we need to implement: On a GET request, render the signup form and pass our publishable Stripe key through for Stripe.js to work. res.locals On a Post request: Instantiate Stripe server-side with our secret key If a user’s already logged in, redirect them. Run a series of two async functions (so we can use their return values after both have completed) to fetch our subscription data from Cosmic and hash the password. named Having completed Step 3, we use the Stripe API to create a new customer, associating their payment method via the Stripe token we passed from the signup form. We then charge that customer based on the plan selected and create a new subscription (again, via Stripe) so recurring payments are processed automatically. We create a new User object based on our Cosmic schema, add that to our bucket, and once that’s succesful we create a new session for the user and redirect them to the route, where they'll now be able to view premium content. Posts All complete, it will look like this: // routes/signup.js var express = require('express');var router = express.Router();var cosmic = require('cosmicjs');var async = require('async');var bcrypt = require('bcrypt') router.get('/', function(req, res) {if (req.session.user) res.redirect('/')async.series([function(cb) {cosmic.getObject(req.app.locals.config, { slug: 'site' }, function(err, response) {res.locals.config = response.object.metadatares.locals.route_signup = truecb()})},function(cb) {cosmic.getObject(req.app.locals.config, { slug: 'subscriptions' }, function(err, response) {res.locals.subscriptions = response.object.metadatares.locals.stripeKeyPublishable = req.app.locals.stripeKeyPublishableres.locals.planName = req.query.planres.render('signup.handlebars')})}])}); router.post('/', function(req, res) {var stripe = require('stripe')(req.app.locals.stripeKeySecret)if (req.session.user) res.redirect('/') async.series({subscriptions: function(callback) {cosmic.getObject(req.app.locals.config, { slug: 'subscriptions' }, function(err, response) {callback(null, response.object.metadata)})},hash: function (callback) {bcrypt.hash(req.body.password, 10, function (err, hash) {callback(null, hash)})}}, function (err, results) { stripe.customers.create({ email: req.body.email, source: req.body.stripeToken }).then(function (customer) { return stripe.charges.create({ amount: results.subscriptions\[req.query.plan + "\_price"\].replace(/\[$\]/,'') + '00', currency: "usd", customer: customer.id }) }).then(function (charge) { stripe.subscriptions.create({ customer: charge.customer, items: \[ { plan: 'subscription-' + req.query.plan } \] }) var object = { type\_slug: 'users', title: req.body.first\_name + ' ' + req.body.last\_name, metafields: \[ { title: 'First name', key: 'first\_name', type: 'text', value: req.body.first\_name }, { title: 'Last name', key: 'last\_name', type: 'text', value: req.body.last\_name }, { title: 'Password', key: 'password', type: 'text', value: results.hash }, { title: 'Email', key: 'email', type: 'text', value: req.body.email }, { title: 'Stripe Id', key: 'stripe\_id', type: 'text', value: charge.customer }, { title: 'Subscription Type', key: 'subscription\_type', type: 'text', value: req.query.plan } \] } if (req.app.locals.config.bucket.write\_key) object.write\_key = req.app.locals.config.bucket.write\_key cosmic.addObject(req.app.locals.config, object, function (err, reponse) { if (err) res.status(500).json({ data: reponse }) else { req.session.user = { first\_name: req.body.first\_name, last\_name: req.body.last\_name, email: req.body.email } req.session.save() res.redirect('/posts') } }) }) })}) module.exports = router The Logout Route: To be user friendly we’ll need to give our users a chance to log out. All this requires is a POST request to and a quick call. /logout session.destroy() var express = require('express')var router = express.Router() /* GET home page. */ router.get('/', function(req, res) {req.session.destroy()return res.redirect('/')}); module.exports = router Wiring Them All Together: Having all of our routes built and ready to work as we need them to, we’ll them all in our app and point their associated enpoints to them via require app.use() // app.js var routes = require('./routes/index');var posts = require('./routes/posts');var post = require('./routes/post')var login = require('./routes/login')var logout = require('./routes/logout')var signup = require('./routes/signup')var plans = require('./routes/plans')var premium = require('./routes/premium')var api = require('./routes/api') app.use('/', routes);app.use('/post', post)app.use('/posts', posts)app.use('/login', login)app.use('/logout', logout)app.use('/signup', signup)app.use('/plans', plans)app.use('/premium', premium)app.use('/api', api) Moving On to the Extension: If you’ve done everything right up until this point, your blog now works exactly as you’d expect it to. To test, go ahead and run , create a few posts in Cosmic and verify that they're being fetched. Then create a dummy account and make sure it's being stored in Cosmic and registered by Stripe. Then, we'll build our dashboard extension for Cosmic. npm start Part 2: Building the Extension Stripe provides us with an impressive amount of analytics, however we want a central location to get a quick glance at a list of all of our users, what subscription plan they’re on, and three key metrics about our blog: revenue to date, active subscriptions, and cancellations to date. Cosmic JS gives us the ability to do this by utilizing it’s extension feature, which lets us upload a SPA with as an entry point that gets loaded into a frame in our Cosmic dashboard. Bucket keys are then provided to it via URL query strings. index.html We’ll be building the extension with React, namely because our extension only requires a view layer. Setup To keep ourselves organized we’ll store our app under our directory. Our tree will look like this: CosmicUserBlog CosmicUserBlog||--extensions| |--subscription-management| | |--client| | | |--components| | | |--index.js| | | |--index.html| | |--dist Once you have your directory structure in place, run and we'll move onto installaitons. yarn init Installations We need these packages: async axios babel-preset-2015 Babel-preset-2016 cosmicjs html-webpack-plugin — for generating our html with webpack lodash query-string — an easy way to parse bucket keys path react react-dom react-loading webpack babel-core babel-loader babel-preset-react Run , then we'll dive in. yarn add async axios babel-preset-2015 babel-preset-2016 cosmicjs html-webpack-plugin lodash query-string path react react-dom react-loading webpack babel-core babel-loader-babel-preset-react Configure Webpack and Babel First, make in the root folder and tell it how to transpile our code: .babelrc // .babelrc {"presets": ["es2016", "es2015", "react"]} Then, again under , make so we can tell Webpack how to package our modules. CosmicUserBlog/extensions/subscription-management webpack.config.js const path = require('path')const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = {entry: './client/index.js',output: {path: path.resolve('dist'),filename: 'index_bundle.js'},module: {loaders: [{ test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }]},plugins: [new HtmlWebpackPlugin({template: './client/index.html',filename: 'index.html',inject: 'body'})]} will contain all of our output files (those being and ) and we'll ultimately compress to upload as our extension. html-webpack-plugin will take our html template from and link to our compiled javascript in upon building. dist index_bundle.js index.html dist client/index.html dist/index.html Create an Entry File We want to use Boostrap and we need a (which we'll ID as ) for our React App to mount onto. should look like this: div root client/index.html <!DOCTYPE html><html><head><link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" /></head><body><div id="root"><scriptsrc="https://code.jquery.com/jquery-3.2.1.min.js"integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="crossorigin="anonymous"></script><script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script></body></html> Build the React App 1. Create an Entry Point We have everything in place to start getting our hands dirty. We’ll use as an entry point for our React app. We'll import React, set it as a global, pass our cosmic keys to it in it's props, and mount the App component (which we'll make next) to . client/index.js <div id="root"> import React from 'react'import ReactDom from 'react-dom'import App from './components/App'import QueryString from 'query-string' window.React = Reactconst url = QueryString.parse(location.search) const cosmic = { bucket: {slug: url.bucket_slug,write_key: url.write_key,read_key: url.read_key}} ReactDom.render(<App cosmic={cosmic}/>,document.getElementById('root')) 2. Build the App Component To keep our app modular, we’ll have a tiered component structure that looks like this: components||--App.js|--Header.js|--SubscriberData| || |--SubscriberContainer.js| |--Loader.js| |--StatsContainer.js| |--StatTicker.js| |--UserList.js and will be immediate children of . , , and will all be immediate children of . Finally, will be composed of s. Header SubscriberContainer App Loader UserList StatsContainer SubcriberContainer StatsContainer StatTicker Aside from keeping an organized project, this structure allows us to maximize our number of which are not React classes and also happen to be fast. stateless functional components Starting at the top of the heirarchy, we’ll build an App component that stores our Cosmic keys in it’s state and renders a and a . Header SubscriberContainer // components/App.js import { Component } from 'react'import Header from './Header'import SubscriberContainer from './SubscriberData/SubscriberContainer' export default class App extends Component { constructor(props) {super(props)this.state = {cosmic: this.props.cosmic}} render() {return (<div><Headerbucket={this.state.cosmic.bucket.slug} /><SubscriberContainercosmic={this.state.cosmic} /></div>)}} The next obvious step is to build out the Header component. Build the Header: Header will be our first stateless functional component, taking only our bucket slug as a prop: const Header = ({ bucket }) =><nav className="navbar navbar-default"><div className="container-fluid"><ul className="nav navbar-nav"><li className="navbar-text"><strong>Managing Subscriptions for: </strong><em>{bucket}</em></li></ul></div></nav> export default Header Build the Subscriber Container: will handle all of the logic associated with our subscriber data and render and to display the data it processes. SubscriberContainer StatsContainer UserList will be a stateful React class containing the following: SubscriberContainer A constructor that initializes 's state to contain our Cosmic keys (which we've passed as props) and reflect our subscriber data. We intialize the Revenue, Users, and Cancellations stats to and their loading state to . SubscriberContainer 'Loading…' true An override for to get our component to fetch the data we need after it mounts to the DOM, and then refresh that data every minute. componentDidMount() A method to fetch (in an admittedly hack-ish way) our revenue by iterating over the subscription types of our active users from Cosmic and storing the calculated revenue in the state. getRevenue() A method to fetch all of our users from Cosmic and store them in an array in the state, as well as their total. getUsers() A method to grab the amount of cancelled subscriptions from our config object. (Later we'll be updating that number with a webhook from Stripe.) getCancellations() Subscriptions A method to render our component (only if we're fetching data), , and . render() Loader StatsContainer UserList All put together, we have this: import { Component } from 'react'import Cosmic from 'cosmicjs'import async from 'async'import _ from 'lodash'import StatsContainer from './StatsContainer'import Loader from './Loader'import UserList from './UserList' const formatter = new Intl.NumberFormat('en-US', {style: 'currency',currency: 'USD',minimumFractionDigits: 2}) export default class App extends Component { constructor(props) {super(props)this.state = {cosmic: this.props.cosmic,stats: {revenue: 'Loading...',users: 'Loading...',cancellations: 'Loading...'},users: [],fetchingRevenue: true,fetchingUsers: true,}} fetchData() {this.getRevenue();this.getUsers();this.getCancellations()} componentDidMount() {this.fetchData()setInterval(() => {this.fetchData()}, 60000)} getRevenue(cosmic) {this.setState({ fetchingRevenue: true})async.series([callback => {Cosmic.getObject(this.state.cosmic, { slug: 'subscriptions' }, (err, response) => {callback(null, response.object)})},callback => {Cosmic.getObjectType(this.state.cosmic, { type_slug: 'users' }, (err, response) => {callback(null, response.objects.all)})}], (err, results) => {let subscriptions = results[0], users = results[1];let currentStats = this.state.statscurrentStats.revenue = formatter.format(users.map(user =>parseInt(subscriptions.metadata[`${user.metadata.subscription_type}_price`].replace('$', ''))).reduce((sum, val) => sum + val))this.setState({ stats: currentStats })this.setState({ fetchingRevenue: false })})} getUsers(cosmic) {this.setState({ fetchingUsers: true })Cosmic.getObjectType(this.state.cosmic, { type_slug: 'users' }, (err, response) => {if (err) {currentStats = this.state.statscurrentStats.users = 'Error'this.setState({ stats: currentStats })} else {let currentStats = this.state.statscurrentStats.users = isNaN(response.total) ? 0 : response.totalthis.setState({ stats: currentStats })this.setState({ users: response.objects.all })this.setState({ fetchingUsers: false })}})} getCancellations(cosmic) {this.setState({ fetchingCancellations: true})Cosmic.getObject(this.state.cosmic, { slug: 'subscriptions' }, (err, response) => {if (err) {currentStats = this.state.statscurrentStats.users = 'Error'this.setState({ stats: currentStats })} else {let currentStats = this.state.statscurrentStats.cancellations = isNaN(response.object.metadata.cancellations) ? 0: response.object.metadata.cancellationsthis.setState({ stats: currentStats })this.setState({ fetchingCancellations: false })}})} render() {return (<div className="container"><Loader loadingState={this.state.fetchingUsers || this.state.fetchingRevenue || this.state.fetchingCancellations} /><StatsContainer stats={this.state.stats} /><UserList users={this.state.users}/></div>)}} We’re now left with four stateless functional components to build out. These are: 1. Loader: import ReactLoading from 'react-loading' const Loader = ({ loadingState }) =><div className="row" style={{display: loadingState ? 'block' : 'none' }}><div className="col-xs-12"><div className="pull-right"><ReactLoading height='20px' width='20px' type="spin" color="#444" /></div></div></div> export default Loader Which makes use of the handy package. react-loading 2. StatsContainer: import StatTicker from './StatTicker' const StatsContainer = ({ stats }) =><div className="row">{Object.keys(stats).map((key, index) =><div key={index} className="col-md-4 text-center"><StatTicker name={key} value={stats[key]} /></div>)}</div> export default StatsContainer 3. StatTicker: const StatTicker = ({ name, value }) =><div><h3 className="lead text-muted">{name}</h3><h1 className="text-primary">{value}</h1></div> export default StatTicker and finally… 4. UserList: const UserList = ({ users, deleteUser }) =><div style={{marginTop: 50 + 'px'}} className="row"><div className="col-xs-12"><h4 className="pull-left lead">All Users:</h4><table className="table table-responsive table-hover"><thead><tr><th>Stripe ID</th><th>First Name</th><th>Last Name</th><th>Email</th></tr></thead><tbody>{users.map((user, index) =><tr key={index}><td>{user.metadata.stripe_id}</td><td>{user.metadata.first_name}</td><td>{user.metadata.last_name}</td><td>{user.metadata.email}</td></tr>)}</tbody></table></div></div> export default UserList Integrate Stripe Webhooks To get our cancelled subscriber count, we’ll udpate a metafield in our Cosmic object. To do this, we'll receive a webhook from Stripe through our Express app every time we delete a subscription on Stripe. cancellations Subscriptions Set up webhooks in Stripe and point them to the domain your Express app is deployed to. Create an route at and it in . api CosmicUserBlog/routes/api.js require App.js Handle POST requests with a statement acting on . When Stripe sends us a subscription cancellation webhook, will be . switch req.body req.body.type customer.subscription.deleted Delete the User object from Cosmic, get the Subscription object from Cosmic, shallow copy the object, increment , then use Cosmic's REST API to push the changes to the object. metadata.cancellations Respond with a code so Stripe can confirm receipt of the webhook. 200 Here’s the finshed product: var express = require('express')var router = express.Router()var cosmic = require('cosmicjs')var axios = require('axios') router.post('/', function(req, res) {event = req.bodyswitch (event.type) {case 'customer.subscription.deleted':cosmic.deleteObject(req.app.locals.config, { slug: 'user', write_key: req.app.locals.config.bucket.slug }, function (err, response) {cosmic.getObject(req.app.locals.config, { slug: 'subscriptions' }, function (err, response) {var currentObject = response.objectcurrentObject.metadata.cancellations = currentObject.metadata.cancellations + 1currentObject.metafield.cancellations.value = currentObject.metadata.cancellations + 1currentObject.write_key = req.app.locals.config.bucket.write_keyaxios({method: 'put',url: `https://api.cosmicjs.com/v1/${req.app.locals.config.bucket.slug}/edit-object`,data: currentObject}).then(function (axRes) {console.log('Success')}).catch(function (axError) {console.log('Error')})})})return res.json({ received: true})break;default:return res.json({ received: false })}}); module.exports = router Deploy To tell Cosmic what your extension is, you’ll need to add to your folder. We'll configure our extension like this: extension.json dist // dist/extension.json {"title": "Subscription Management","font_awesome_class": "fa-gears","image_url": ""} Conclusion Using Cosmic JS, Express, Stripe, and React, we’ve built both a monetizable blog that lets our readers subscribe to read premium content and a convenient dashboard to view data about our blog. We’ve integrated Stripe for secure payments and we’ve built an app that does as much as we want it to do with room to grow. With how quickly we’ve been able to build our app and with the simplicity of deploying and maintaining it, it’s clear that Cosmic JS is one of a kind in its API first approach to content management. Clearly, CosmisJS is a money maker. This article originally appears on the Cosmic JS Blog . . Matt Cain builds smart web applications and writes about the tech used to build them. You can learn more about him on his portfolio
Share Your Thoughts