Making the most of Contentful to launch duolab.com JAMstack Berlin #13 —27 Avril 2020 Yu Ling Cheng @yulingec
A presentation at JAMstack Berlin Meetup #13 in April 2020 in 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