A presentation at JAMstack Berlin Meetup #13 by Yu Ling Cheng
Making the most of Contentful to launch duolab.com JAMstack Berlin #13 —27 Avril 2020 Yu Ling Cheng @yulingec
Yu Ling Cheng Lead developer at Theodo #DevUx forever (devux.tech) Just joined JAMstack Paris Meetup organisation #StayAtHome #KnittingMyFirstPullover Follow me @yulingec
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
@yulingec
@yulingec
Skin diagnosis @yulingec
Products recommendation @yulingec
E-commerce Store @yulingec
Static content to introduce the brand @yulingec
Frontend @yulingec
Frontend content Headless CMS @yulingec
Frontend users content Headless CMS contributors @yulingec
@yulingec
Learnings and takeaways @yulingec
Learnings and takeaways The project stakes UX/CX Time to market Sustainability User experience and contributor experience Ease of implementation Developer experience @yulingec
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
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
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
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
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
Product information Data model @yulingec
Product information Data model @yulingec
Product information Data model @yulingec
Product information Data model @yulingec
Product information Data model @yulingec
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
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
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
Data model @yulingec
Data model @yulingec
Data model @yulingec
Data model @yulingec
Page with static content Data model @yulingec
Page with static content Data model @yulingec
Page with static content Data model @yulingec
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
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
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
Data model @yulingec
Data model @yulingec
Data model @yulingec
Content block Data model @yulingec
Content block Data model @yulingec
Content block Data model @yulingec
Content block Data model @yulingec
Content block Data model @yulingec
Content block Pros: Flexibility Cons: Contributor experience is less clear Variant: List Item (carousel) UX/CX Data model TTM Sustainability @yulingec
Data model @yulingec
Data model @yulingec
Translations (Copies) Data model @yulingec
Translations (Copies) Data model @yulingec
Translations (Copies) Data model @yulingec
Translations (Copies) Pros: Easy MVP to setup Cons: Experience is less clear for translators (lack of context) UX/CX Data model TTM Sustainability @yulingec
Data model Product information Page with static content Content block Translations (Copies) Data model @yulingec
Advanced model example Quiz Data model @yulingec
Advanced model example: Quiz Step 1: Define the quiz logic Data model @yulingec
Advanced model example: Quiz Step 2: 2 models—Question and Answer Data model @yulingec
Advanced model example: Quiz Step 2: 2 models—Question and Answer Data model @yulingec
Advanced model example: Quiz Step 3: Fill in content Data model @yulingec
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
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
Learnings and takeaways Data model Services integration Developer Experience @yulingec
Frontend content Headless CMS Services integration @yulingec
Frontend generate Recommendation engine content Headless CMS Services integration @yulingec
Frontend generate Recommendation engine content Headless CMS product mapping Services integration @yulingec
Frontend redirection E-commerce store content Headless CMS Services integration @yulingec
Frontend redirection E-commerce store content Headless CMS product mapping Services integration @yulingec
Data mapping Services integration @yulingec
Data mapping Services integration @yulingec
Data mapping Services integration @yulingec
Data mapping Services integration @yulingec
Data mapping Services integration @yulingec
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
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
Data mapping Pros: Easy to implement Cons: Hard to configure several environments UX/CX Services integration TTM Sustainability @yulingec
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
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
Learnings and takeaways Data model Services integration Developer Experience @yulingec
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
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
Learnings and takeaways VS CaaS API-based Directus on-premise open-source VS CaaS Git-based UX/CX Time to market Sustainability Price @yulingec
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
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
L’Occitane group launched a new premium brand in February this year—Duolab—that offers customised skincare. The Duolab tech team needed to create a tailored and polished online experience for users while leaving editors control over the content. This seemed to be a perfect use case for a headless CMS! As part of this team, I’ll tell you the story of the many choices we made—defining content models, integrating Shopify headless commerce, plugging into our database to generate bespoke recommendations—along with our difficulties and takeaways.
The following resources were mentioned during the presentation or are useful additional information.
The content and the logic of that quiz is managed in Contentful. It’s a side project I’ve been working on.
Here’s what was said about this presentation on social media.
First talk @jamstackberlin digital edition by @YuLingEC about @contentful and @nuxt_js pic.twitter.com/29lpJ5RcVi
— Khaled Garbaya (@khaled_garbaya) April 27, 2020
Happy Friday everyone! Join us this Monday for the 1st remote JAMstack Meetup 🎉. @YuLingEC and @robinpokorny are joining us for 2 talks on "Making the most of Contentful to launch https://t.co/bbCBUDFQ5n" and "@NetlifyCMS outside @Netlify. RSVP here :https://t.co/y64y8TWc4c
— Contentful (@contentful) April 24, 2020