Making the most of Contentful to launch Duolab.com

A presentation at JAMstack Berlin Meetup #13 in April 2020 in by Yu Ling Cheng

Slide 1

Slide 1

Making the most of Contentful to launch duolab.com JAMstack Berlin #13 —27 Avril 2020 Yu Ling Cheng @yulingec

Slide 2

Slide 2

Yu Ling Cheng Lead developer at Theodo #DevUx forever (devux.tech) Just joined JAMstack Paris Meetup organisation #StayAtHome #KnittingMyFirstPullover Follow me @yulingec

Slide 3

Slide 3

What is Duolab? A new brand from L’Occitane A tailor-made skin care The result of 6 years of research and innovation Launched on February, 6th with a Pop-up Store in London @yulingec

Slide 4

Slide 4

@yulingec

Slide 5

Slide 5

@yulingec

Slide 6

Slide 6

Skin diagnosis @yulingec

Slide 7

Slide 7

Products recommendation @yulingec

Slide 8

Slide 8

E-commerce Store @yulingec

Slide 9

Slide 9

Static content to introduce the brand @yulingec

Slide 10

Slide 10

Frontend @yulingec

Slide 11

Slide 11

Frontend content Headless CMS @yulingec

Slide 12

Slide 12

Frontend users content Headless CMS contributors @yulingec

Slide 13

Slide 13

@yulingec

Slide 14

Slide 14

Learnings and takeaways @yulingec

Slide 15

Slide 15

Learnings and takeaways The project stakes UX/CX Time to market Sustainability User experience and contributor experience Ease of implementation Developer experience @yulingec

Slide 16

Slide 16

Flexibilité Célérité Maintenabilité Learnings and takeaways Expérience Utilisateurs & Expérience Contributeurs Rapidité de mise en place Expérience de développement Focus Data models Services integration Developer Experience @yulingec

Slide 17

Slide 17

Model Content Product Produit Produit Entry Name: Text Name: Light base Description: Long text Description: Blabla Bla bla bla, bla bla blabla blablabla Data model Images: Files list Images: Linked products: List of products Linked products: Night base @yulingec

Slide 18

Slide 18

Model Content Produi Produi tt Product Entry Name: Text Name: Light base Description: Long text Description: API Blabla Bla bla bla, bla bla blabla blablabla Images: Files list Images: Linked products: List of products Linked products: Data model Night base @yulingec

Slide 19

Slide 19

Model Content Produi Produi tt Product Entry Name :Text Name: Text Name :Light Name: Lightbase base Description :Long Description: Longtext text Description : Description: API Blabla Bla bla bla, bla bla blabla blablabla Images :Files Images: Fileslist list Images : Images: Linked products: products :List Listof of products Linked products: products : Data model Night base @yulingec

Slide 20

Slide 20

Model Content Produi Produi tt Product Entry Name :Text Name: Text Name :Light Name: Lightbase base Description :Long Description: Longtext text Description : Description: API Blabla Bla bla bla, bla bla blabla blablabla Images :Files Images: Fileslist list Images : Images: Linked products: products :List Listof of products Linked products: products : Data model Night base @yulingec

Slide 21

Slide 21

Product information Data model @yulingec

Slide 22

Slide 22

Product information Data model @yulingec

Slide 23

Slide 23

Product information Data model @yulingec

Slide 24

Slide 24

Product information Data model @yulingec

Slide 25

Slide 25

Product information Data model @yulingec

Slide 26

Slide 26

Product information Tips: It’s more than just creating a content model • Understand contributors expectations and needs to define field types and relationships • Take some time to define helpers and adjust naming to help contributors fill in content • Configure a preview to ease contribution Data model @yulingec

Slide 27

Slide 27

Product information Tips : • Bien comprendre les attentes des contributeurs pour définir les types des champs et relations • Prendre le temps de définir les helpers pour aider les contributeurs à bien remplir le contenu • Configurer une preview pour faciliter la contribution Data model @yulingec

Slide 28

Slide 28

Product information Tips: It’s more than just creating a content model • Understand contributors expectations and needs to define field types and relationships • Take some time to define helpers and adjust naming to help contributors fill in content • Configure a preview to ease contribution UX/CX Data model TTM Sustainability @yulingec

Slide 29

Slide 29

Data model @yulingec

Slide 30

Slide 30

Data model @yulingec

Slide 31

Slide 31

Data model @yulingec

Slide 32

Slide 32

Data model @yulingec

Slide 33

Slide 33

Page with static content Data model @yulingec

Slide 34

Slide 34

Page with static content Data model @yulingec

Slide 35

Slide 35

Page with static content Data model @yulingec

Slide 36

Slide 36

Page with static content Pros: A unique place to manage the content of a page Cons: • Structure lacks flexibility • Multiply single entry models Data model @yulingec

Slide 37

Slide 37

Page with static content Avantage : Un seul endroit pour contrôler le contenu d’une page Inconvénients : • Structure peu flexible • Multiplication de modèles à entrée unique Data model @yulingec

Slide 38

Slide 38

Page with static content Pros: A unique place to manage the content of a page Cons: • Structure lacks flexibility • Multiply single entry models UX/CX Data model TTM Sustainability @yulingec

Slide 39

Slide 39

Data model @yulingec

Slide 40

Slide 40

Data model @yulingec

Slide 41

Slide 41

Data model @yulingec

Slide 42

Slide 42

Content block Data model @yulingec

Slide 43

Slide 43

Content block Data model @yulingec

Slide 44

Slide 44

Content block Data model @yulingec

Slide 45

Slide 45

Content block Data model @yulingec

Slide 46

Slide 46

Content block Data model @yulingec

Slide 47

Slide 47

Content block Pros: Flexibility Cons: Contributor experience is less clear Variant: List Item (carousel) UX/CX Data model TTM Sustainability @yulingec

Slide 48

Slide 48

Data model @yulingec

Slide 49

Slide 49

Data model @yulingec

Slide 50

Slide 50

Translations (Copies) Data model @yulingec

Slide 51

Slide 51

Translations (Copies) Data model @yulingec

Slide 52

Slide 52

Translations (Copies) Data model @yulingec

Slide 53

Slide 53

Translations (Copies) Pros: Easy MVP to setup Cons: Experience is less clear for translators (lack of context) UX/CX Data model TTM Sustainability @yulingec

Slide 54

Slide 54

Data model Product information Page with static content Content block Translations (Copies) Data model @yulingec

Slide 55

Slide 55

Advanced model example Quiz Data model @yulingec

Slide 56

Slide 56

Advanced model example: Quiz Step 1: Define the quiz logic Data model @yulingec

Slide 57

Slide 57

Advanced model example: Quiz Step 2: 2 models—Question and Answer Data model @yulingec

Slide 58

Slide 58

Advanced model example: Quiz Step 2: 2 models—Question and Answer Data model @yulingec

Slide 59

Slide 59

Advanced model example: Quiz Step 3: Fill in content Data model @yulingec

Slide 60

Slide 60

Advanced model example: Quiz Step 4: Implement frontend <script> import Vue from “vue”; import Question from “~/components/Quiz/Question”; import Answer from “~/components/Quiz/Answer”; import { getQuestions } from “~/services/quiz.js”; export default Vue.extend({ name: “Quiz”, components: { Question, Answer, }, asyncData({ app, error }) { return getQuestions(app, error); }, computed: { currentQuestion() { return this.$store.state.quiz.currentQuestion; }, }, }); </script> Data model <template> <div class=”quiz-container”> <!—Display the current question —> <Question :question=”currentQuestion” /> <!-Display available answers. Choosing an answer will trigger the update of: - the score - the state currentQuestion —> <Answer :v-for=”answer in currentQuestion.answers” :key=”answer.id” :answer=”answer” /> </div> </template> @yulingec

Slide 61

Slide 61

Advanced model example Quiz Step 1: Step 2: Define the quiz logic 2 models—Question and Answer Step 3: Step 4: Fill in content Implement frontend UX/CX Data model TTM Sustainability @yulingec

Slide 62

Slide 62

Learnings and takeaways Data model Services integration Developer Experience @yulingec

Slide 63

Slide 63

Frontend content Headless CMS Services integration @yulingec

Slide 64

Slide 64

Frontend generate Recommendation engine content Headless CMS Services integration @yulingec

Slide 65

Slide 65

Frontend generate Recommendation engine content Headless CMS product mapping Services integration @yulingec

Slide 66

Slide 66

Frontend redirection E-commerce store content Headless CMS Services integration @yulingec

Slide 67

Slide 67

Frontend redirection E-commerce store content Headless CMS product mapping Services integration @yulingec

Slide 68

Slide 68

Data mapping Services integration @yulingec

Slide 69

Slide 69

Data mapping Services integration @yulingec

Slide 70

Slide 70

Data mapping Services integration @yulingec

Slide 71

Slide 71

Data mapping Services integration @yulingec

Slide 72

Slide 72

Data mapping Services integration @yulingec

Slide 73

Slide 73

Data mapping <!doctype html> <html lang=”en”> <head> <meta charset=”UTF-8”> <title>Products</title> <link rel=”stylesheet” href=”https://unpkg.com/contentfului-extensions-sdk@3/dist/cf-extension.css”> <script src=”https://unpkg.com/contentful-ui-extensionssdk@3”></script> <script src=”https://unpkg.com/jquery@3”></script> <style> .-warning { color: #d9453f; margin-top: 4px; display: none; } </style> </head> <body> <div class=”cf-form-field”> <select id=”products” class=”cf-form-input”> <option value=”-1” disabled>Select…</option> </select> <div class=”cf-form-hint”>Please select the desired product.</div> <div class=”cf-form-hint -warning” data-mkto-warning>⚠ Error in this extension.</div> <div class=”cf-form-hint -warning” data-lambda-warning>⚠ Error fetching data.</div> </div>

<script type=”text/javascript”> window.contentfulExtension.init(function(api) { api.window.startAutoResizer(); var value = api.field.getValue(); var selectField = $(“#products”); var forms = []; $.ajax({ url: ‘https://jsonplaceholder.typicode.com/users’, type: “GET”, crossDomain: true, dataType: “json”, success: function(response) { forms = response; forms.forEach(function(value, index) { var option = document.createElement(“option”); option.setAttribute(“value”, index); option.innerText = value.name; }); var value = api.field.getValue(); var id = value && value.id; var index = forms.findIndex(function(form) { return form.id === id; }); selectField.val(index); if (id && index < 0) { $(“[data-mkto-warning]”).show(); } <script type=« text/javascript »>/****/</script> </body> </html>

selectField.append(option); }); }, error: function(xhr, status) { $(“[data-lambda-warning]”).show(); console.log(“Error fetching data. Status:”, status); } selectField.on(“input”, function() { api.field.setValue(forms[this.value]); }); }); </script> Services integration @yulingec

Slide 74

Slide 74

Data mapping <!doctype html> <html lang=”en”> <head> <meta charset=”UTF-8”> <title>Products</title> <link rel=”stylesheet” href=”https://unpkg.com/contentfului-extensions-sdk@3/dist/cf-extension.css”> <script src=”https://unpkg.com/contentful-ui-extensionssdk@3”></script> <script src=”https://unpkg.com/jquery@3”></script> <style> .-warning { color: #d9453f; margin-top: 4px; display: none; } </style> </head> <body> <div class=”cf-form-field”> <select id=”products” class=”cf-form-input”> <option value=”-1” disabled>Select…</option> </select> <div class=”cf-form-hint”>Please select the desired product.</div> <div class=”cf-form-hint -warning” data-mkto-warning>⚠ Error in this extension.</div> <div class=”cf-form-hint -warning” data-lambda-warning>⚠ Error fetching data.</div> </div>

<script type=”text/javascript”> window.contentfulExtension.init(function(api) { api.window.startAutoResizer(); var value = api.field.getValue(); var selectField = $(“#products”); var forms = []; $.ajax({ url: ‘https://jsonplaceholder.typicode.com/users’, type: “GET”, crossDomain: true, dataType: “json”, success: function(response) { forms = response; forms.forEach(function(value, index) { var option = document.createElement(“option”); option.setAttribute(“value”, index); option.innerText = value.name; }); var value = api.field.getValue(); var id = value && value.id; var index = forms.findIndex(function(form) { return form.id === id; }); selectField.val(index); if (id && index < 0) { $(“[data-mkto-warning]”).show(); } <script type=« text/javascript »>/****/</script> </body> </html>

selectField.append(option); }); }, error: function(xhr, status) { $(“[data-lambda-warning]”).show(); console.log(“Error fetching data. Status:”, status); } selectField.on(“input”, function() { api.field.setValue(forms[this.value]); }); }); </script> Services integration @yulingec

Slide 75

Slide 75

Data mapping Pros: Easy to implement Cons: Hard to configure several environments UX/CX Services integration TTM Sustainability @yulingec

Slide 76

Slide 76

Content fetching /* nuxt.config.js / plugins: [ // … ‘~/plugins/contentful’, ], / plugins > contentful.js */ const contentful = require(‘contentful’) const contentfulClient = contentful.createClient({ space: ‘XXXXXXXXXXXXX’, accessToken: ‘XXXXXXXXXXXXX’ }) contentfulClient.getTranslationEntries = async (translationKeys) => { const translationEntries = await contentfulClient.getEntries({ content_type: ‘content’, ‘fields.key’: translationKeys, select: ‘fields.key,fields.value’ }) return parseTranslationEntries(translationEntries) } export const parseTranslationEntries = (translationEntries) => translationEntries.items.reduce((trans, translationEntry) => { if ( translationEntry.fields && translationEntry.fields.key && translationEntry.fields.value ) { return { …trans, [translationEntry.fields.key]: translationEntry.fields.value } } }, {}) export default (_context, inject) => { inject(‘cms’, contentfulClient) } Intégration de services

<!— pages > faq > index.vue —> <script> import FAQFooter from ‘~/components/Faq/FAQFooter’ export default { components: { FAQFooter }, layout: ‘page’, async asyncData({ app, error }) { try { // Get FAQ list from Contentful const faq = await app.$cms.getEntries({ content_type: ‘faq’, order: ‘fields.title’ }) const translations = await app.$cms.getTranslationEntries([ ‘faq.footer.see_more’, ‘faq.footer.ask_question’ ]) return { faqList: faq.items, translations } } catch (e) { // log error with Sentry app.$sentry.captureException(new Error(‘FAQ not found’)) error({ statusCode: 404, message: ‘FAQ not found’ }) } } } </script>

@yulingec

Slide 77

Slide 77

Content fetching Pros: Contentful SDK makes it easy to use Cons: Should we validate the data we receive? Watch out for soft delete—could lead to bugs TTM Intégration de services Sustainability @yulingec

Slide 78

Slide 78

Learnings and takeaways Data model Services integration Developer Experience @yulingec

Slide 79

Slide 79

Manage environments Mono-Environment: Viable but with limitations when integrating to external services Avoid bugs with clear development/contribution process Multi-Environments: Possible but the feature is still early staged Onboarding cost to manage content and content types with migration scripts TTM Developer experience Sustainability @yulingec

Slide 80

Slide 80

Learnings and takeaways Conclusion UX/CX Time to Market Sustainability Simple interface to manage +/- complex models Easy to deploy (installation, SDK, extensions…) Point of attention: managing environnements @yulingec

Slide 81

Slide 81

Learnings and takeaways VS CaaS API-based Directus on-premise open-source VS CaaS Git-based UX/CX Time to market Sustainability Price @yulingec

Slide 82

Slide 82

Making the most of Contentful to launch duolab.com JAMstack Berlin 27 Avril 2020 Vielen Dank! Yu Ling Cheng #DevUx forever (devux.tech) Follow me @yulingec

Slide 83

Slide 83

Making the most of Contentful to launch duolab.com JAMstack Berlin 27 Avril 2020 Vielen Dank! Learnings following Q&A: It’s possible to compose content blocs from a rich text field, instead of assembling many blocs—a nice possibility to explore! @yulingec