Content Management in the JAMStack By Shy Ruparel

Shy Ruparel Developer Evangelist | Contentful @ShyRuparel He/Him

3 Let’s talk about Content Management

COMMON CMS PROBLEMS PAGE-CENTRIC MODEL MONOLYTHIC SOLUTION SLOW ITERATIONS

COMMON CMS PROBLEMS WYSIWYG

DON'T BREAK SEPARATION OF CONCERNS Content vs. Look

Html

Html

Whenever you put HTML in your content you're Html breaking portability!

DISADVANTAGES OF WYSIWYG 01 02 03 04 Too flexible Mixes content and looks Too easy to mess up HTML is the goal

COMMON CMS PROBLEMS You can build your own CMS, but...

I don't want to do that!

I don't want to do that! I don't have time for that!

Don’t let a CMS get in the way of shipping software. Contentful provides a content infrastructure that enables teams to power content in any digital product.

CONTENT AT SCALE STRUCTURED CONTENT GOOD EDITING EXPERIENCE SPEEDY DEVELOPMENT

Our APIs Content Delivery API production Content Preview API staging Content Management API Images API automation assets

Let’s make a Website

So it’s time for a small tangent

www.ti4.nyc TI4.NYC

www.ti4.nyc Let’s dig into the About Page

I define a Content Model

Sean creates content

A JSON API { } "sys": { "contentType": { "sys": { "id": "component" } } }, "fields": { "title": "About", "slug": "about", "pageOrder": 1, "image": { "sys": { "id": "6BwngCuyHKkS28sKIimomM" } }, "copy": "Twilight Imperium is a monstrous game..." }

Our APIs Content Delivery API production Content Preview API staging Content Management API Images API automation assets

Our APIs Content Delivery API production

GATSBY-CONFIG.JS require("dotenv").config({ path: .env, }) module.exports = { plugins: [{ resolve: gatsby-source-contentful, options: { spaceId: process.env.spaceId, accessToken: process.env.accessToken, host: process.env.host }, }, ], }

Pull in the data

RENDER THE PAGE { this.props.cfData.map(componentEntry => ( <article id={componentEntry.node.slug} className={${this.props.article === componentEntry.node.slug ? "active" : ""} ${ this.props.articleTimeout ? "timeout" : "" }} style={{ display: "none" }} > <h2 className="major">{componentEntry.node.title}</h2> <span className="image main"> <img src={componentEntry.node.image.file.url} alt="" /> </span> <div dangerouslySetInnerHTML={{ __html: componentEntry.node.copy.childMarkdownRemark.html }} /> {close} </article> )); }

Build the site

Webhooks

Browser Static Site Host Build Server Content Infrastructure Sean Source Control Shy

Browser Static Site Host Build Server Content Infrastructure Editors Source Control Developers

www.ti4.nyc GITHUB.COM/SHY/ TI4.NYC-WEBSITE

Shy Ruparel Developer Evangelist | Contentful @ShyRuparel He/Him Slides: https://noti.st/shy Background Images: wocintechchat.com

Editor Workflows Developer Workflows

Production Preview Approval Editor Workflow QA Local Dev Dev Workflow Staging Draft Word / Google Docs

Production Preview Approval Editor Workflow QA Local Dev Dev Workflow Staging Draft Word / Google Docs

Improving the Editor Experience

Improving Sean’s Experience

My Editors want A Custom Field Type

Contentful doesn’t have a built in TI4 game tracker

So I built one

Let’s take a look at UI-Extensions

Let’s dig into The extension

STANDARD JSON OBJECT FIELD UI EXTENSION

Inspect in GraphiQL

CONFIGURE THE EXTENSION { } "id": "ti4MatchLog", "name": "Twilight Imperium 4th Edition Match Log", "srcdoc": "./index.html", "fieldTypes": ["Object"]

BUILD A FORM <body> <div id="content"> <div class="player"> <div class="cf-form-field inline"> <label>Players</label> <input type="text" value="Player 1 Name" class="cf-form-input inline" id="player1Name"> </div> <div class="cf-form-field inline"> <label>Points</label> <input type="integer" value="1" class="cf-form-input inline" id="player1Score"> </div> <div class="cf-form-field inline"> <label>Factions</label> <select class="cf-form-input" id="player1Faction"> <option value="Arborec" selected>Arborec</option> <option value="Barony">Barony of Letnev</option> </select> </div> </div>

SET THE DATA $(".cf-form-input").on("input", function() { extension.field.setValue(getGameData()); });

SET THE DATA function getGameData() { var data = {}; data["Players"] = []; var innerData = {}; var i = 0; for (; i < document.getElementsByClassName("cf-form-input").length; i += 1) { switch (i % 3) { case 0: innerData = {}; innerData["Name"] = document.getElementsByClassName( "cf-form-input")[i].value; break; case 1: innerData["Score"] = document.getElementsByClassName( "cf-form-input")[i].value; break; case 2: innerData["Faction"] = document.getElementsByClassName( "cf-form-input")[i].value; data["Players"].push(innerData); break; } } return (data); }

Push the code up to Contentful

Set a custom apparence for the JSON Object

And that’s it

RENDER THAT DATA {outerElement.node.games.map(innerElement => ( <React.Fragment> <h4>{innerElement.signTableNumber}</h4> <table> <thead> <tr> <th>Name</th> <th>Faction</th> <th>Score</th> </tr> </thead> <tbody> {innerElement.game.Players.map(playerElement => ( <tr> <td>{playerElement.Name}</td> <td> {playerElement.Faction} </td> <td>{playerElement.Score}</td> </tr> ))} </tbody> </table> </React.Fragment> ))}

www.ti4.nyc Game Log

www.ti4.nyc GITHUB.COM/SHY/ TI4.NYC-UI-EXTENSION

BE FRIENDS WITH OTHER VENDORS!

A rich ecosystem is indispensable in the world we live in

My Editors want Preview

COMMON CMS PROBLEMS WYSIWYG

Our APIs Content Delivery API production Content Preview API staging Content Management API Images API automation assets

Our APIs Content Preview API staging

Let’s take a look at Contentful Preview API

www.contentful.com PREVIEW.CONTENTFUL.COM

Preview API Key

Webhooks For Production

Webhooks For Preview

Create Draft Content

www.preview.ti4.nyc PREVIEW.TI4.NYC

YOUR EDITORS HAVE TO BE HAPPY (AND SAFE)

Improving the Developer Experience

Improving My Experience

www.ti4.nyc LET’S START WITH THE BACKGROUND IMAGE

Dealing with Assets

Our APIs Content Delivery API production Content Preview API staging Content Management API Images API automation assets

Our APIs Images API assets

3.2MB https:!//images.ctfassets.net/!!.../bg.jpg

140KB https:!//images.ctfassets.net/!!.../bg.jpg?w=800

30KB https:!//images.ctfassets.net/!!.../bg.jpg?w=800&fm=webp

17KB https:!//images.ctfassets.net/!!.../bg.jpg?w=800&fm=jpg&q=10

https:!//images.ctfassets.net/!!.../bg.jpg?w=800&h=200&fit=crop

Let’s talk about one of the laws of Software Engineering

Nobody gets it right the first time

When you can’t have perfection, the next best thing is change

Let's talk about the Contentful Migrations

Our APIs Content Delivery API production Content Preview API staging Content Management API Images API automation assets

Our APIs Content Management API automation

MIGRATION CLI CONTENT TYPE OPERATIONS 01 02 03 04 05 Create a content type Delete a content type Edit a content type Create/edit/delete fields Change a field ID

ALL IN JAVASCRIPT module.exports = function runMigration(migration) { const table = migration.editContentType("table"); Table .createField("winner") .name("winner") .type("Symbol") .required(false); return; };

CALLABLE VIA THE CONTENTFUL CLI

Let's do something A little more complicated

TRANSFORMING ENTRIES module.exports = function runMigration(migration) { const table = migration.editContentType("table"); table .createField("winner") .name("winner") .type("Symbol") .required(false); migration.transformEntries({ contentType: 'table', from: ['game'], to: ['winner'], transformEntryForLocale: function(fromFields, currentLocale) { var winner = ""; fromFields.game[currentLocale].Players.forEach(function(player) { if (player['Score'] == 10) { winner = player['Name']; } }); console.log(winner); return { winner: winner }; } }); return; };

Programatic Content Model Changes

MIGRATION CLI ADVANTAGES 01 Can be kept in VC 02 Includes sanity checks 03 Perfect for CI 04 Repeatable

CMS as Code (automation like a boss)

That’s Great

But what if I want to Discard Test or Preview a Migration?

125 Space Environments

MIGRATIONS + ENVIRONMENTS

GATSBY-CONFIG.JS require("dotenv").config({ path: .env, }) module.exports = { plugins: [{ resolve: gatsby-source-contentful, options: { spaceId: process.env.spaceId, accessToken: process.env.accessToken, host: process.env.host }, }, ], }

GATSBY-CONFIG.JS WITH ENVIRONMENTS require("dotenv").config({ path: .env, }) module.exports = { plugins: [{ resolve: gatsby-source-contentful, options: { spaceId: process.env.spaceId, accessToken: process.env.accessToken, host: process.env.host, environments: process.env.environments }, }, ], }

www.contentful.com COMBINE WITH NETLIFY FOR DEV PREVIEW

Combine with Netlify for Dev Preview

COMMON USAGE FOR SPACE ENVIRONMENTS Local development • Staging / QA • Continuous integration •

Let’s briefly dig into Continuous Integration

BASIC CI PIPELINE Build Test Deploy

MIGRATIONS + CI PIPELINE Build Test Deploy

MIGRATIONS + CI PIPELINE Build Create a new environment Test Deploy

MIGRATIONS + CI PIPELINE Build Create a new environment Migrate new environment Test Deploy

MIGRATIONS + CI PIPELINE Build Create a new environment Migrate new environment Test Deploy Migrate master environment

www.ti4.nyc GITHUB.COM/CONTENTFUL-LABS/ CONTINOUS-DELIVERYENVIRONMENTS-EXAMPLE

Editor Workflows Developer Workflows

Shy Ruparel Developer Evangelist | Contentful @ShyRuparel He/Him Slides: GitHub.com/shy/talks Background Images: wocintechchat.com