{"id":1367,"date":"2024-03-22T09:53:17","date_gmt":"2024-03-22T15:53:17","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=1367"},"modified":"2024-03-22T09:53:19","modified_gmt":"2024-03-22T15:53:19","slug":"building-a-live-preview-with-eleventy-contentful-and-liquid-templating","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/building-a-live-preview-with-eleventy-contentful-and-liquid-templating\/","title":{"rendered":"Building a Live Preview with Eleventy, Contentful, and Liquid Templating"},"content":{"rendered":"\n<p>As a part of the marketing team at <a href=\"https:\/\/heyflow.com\/\">Heyflow<\/a>, I collaborate with people who work on the company\u2019s website. Although all team members are technically acquainted, sometimes they struggle to update the website. The struggle is not being able to visualize what will change on the page when updating the content. Saving the updates and waiting for the staging environment to build is inefficient (even though our site build is less than a minute\u2026 still). As a result, the team requested a <em>live<\/em> preview of our pages.<\/p>\n\n\n\n<p>We\u2019re using <a href=\"https:\/\/www.11ty.dev\/\">Eleventy<\/a> to build the site, and <a href=\"https:\/\/www.contentful.com\/\">Contentful<\/a> to manage the content. Here\u2019s a video of the solution I came up with working:<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='316' src='https:\/\/videopress.com\/embed\/UoIIqnZY?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1674852142'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p>After successfully implementing it on our company\u2019s website, I built an <a href=\"https:\/\/www.11ty.dev\/docs\/starter\/\">Eleventy starter project<\/a> and a <a href=\"https:\/\/11ty-llp.netlify.app\/\">demo site<\/a> showing how it works.<\/p>\n\n\n\n<p class=\"learn-more\">Disclaimer: This article describes the live preview without the live editing option. That means that you can\u2019t see instant page updates, but instead you need to click on the Refresh button to pull the latest updates.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Plan<\/h2>\n\n\n\n<p>Contentful guides for building a live preview usually require using React, which I\u2019m trying to avoid.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>The live preview SDK works with JavaScript and has optimized integration for any React.js framework (like Next.js).<\/p>\n<\/blockquote>\n\n\n\n<p>So I\u2019ve built a serverless function that renders the whole Liquid template on request without React.js. The code is available on <a href=\"https:\/\/github.com\/maliMirkec\/11ty-liquid-live-preview\">GitHub<\/a>, and the demo is available at <a href=\"https:\/\/11ty-llp.netlify.app\/\">https:\/\/11ty-llp.netlify.app\/<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A little appreciation for templating languages<\/h2>\n\n\n\n<p>Allow me a moment to express my admiration for templating engines, especially <a href=\"https:\/\/shopify.github.io\/liquid\/\">Liquid<\/a>. I remember being amazed when I started using <a href=\"https:\/\/mustache.github.io\/\">Mustache<\/a> with PHP almost ten years ago. Outputting variables with Mustache tags made so much sense to me. It was much more readable than echoing PHP variables. I loved it and soon discovered other templating engines.<\/p>\n\n\n\n<p>As my back-end career transformed into a front-end area, I discovered <a href=\"https:\/\/handlebarsjs.com\/\">Handlebars<\/a>, <a href=\"https:\/\/github.com\/twigjs\">Twig<\/a>, <a href=\"https:\/\/pugjs.org\/api\/getting-started.html\">Pug<\/a>, and Liquid. Pug, in particular, was the choice for my site around five years ago. I thought it was the right choice, but it didn\u2019t stick. The main reasons were other projects I\u2019ve been part of. These projects were Jekyll and Shopify, the two most prominent frameworks that used Jekyll as their templating engines. Since working with Liquid daily, I learned many ways to work around its limitations. So, it made perfect sense to use it on my site later when I migrated from <a href=\"https:\/\/hexo.io\/\">Hexo<\/a> to <a href=\"https:\/\/www.11ty.dev\/\">Eleventy<\/a>.<\/p>\n\n\n\n<p>What I never did was use it in Node.js to render files. In this project, that is exactly what I needed to do to make a live preview happen. Hooray for learning new things!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The website<\/h2>\n\n\n\n<p>The demo website uses Contentful, Eleventy, and Liquid \u2014 my favorite combination for building a static site. The Contentful content model is based on pages and components. Here\u2019s how it looks <a href=\"https:\/\/www.contentful.com\/help\/visual-modeler\/\">in Visual Modeler<\/a>.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/paper-attachments.dropboxusercontent.com\/s_DCDB6F2DA26A3F054F94FAE3064EEC0A53131E1015BEBF3538F17EF6B8B10DC3_1710877631885_contentful-content-model.png?ssl=1\" alt=\"\"\/><\/figure>\n\n\n\n<p>The pages consist of components that could include other components. For example, the homepage has a hero component with a call-to-action button (CTA), which is also a component.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"667\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/03\/page.png?resize=1024%2C667&#038;ssl=1\" alt=\"\" class=\"wp-image-1381\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/03\/page.png?resize=1024%2C667&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/03\/page.png?resize=300%2C195&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/03\/page.png?resize=768%2C500&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/03\/page.png?w=1182&amp;ssl=1 1182w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>To fetch the data from Contentful, I\u2019m using <a href=\"https:\/\/www.contentful.com\/developers\/docs\/references\/content-delivery-api\/\">Content Delivery API<\/a> to fetch every entity separately. That means I\u2019m fetching pages, hero components, and call-to-action (CTA) components separately, which allows me to handle components individually and reuse the data throughout the site.<\/p>\n\n\n\n<p>For example, here\u2019s how to fetch the pages from Contentful by using the <a href=\"https:\/\/www.11ty.dev\/docs\/data-js\/\">JavaScript data file<\/a> in Eleventy. The following code snippet is placed inside the <code>_data\/pages.js<\/code> file. Notice how I use only the transformed component object\u2019s <code>id<\/code> and <code>type<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">\n<span class=\"hljs-keyword\">const<\/span> contentful = <span class=\"hljs-built_in\">require<\/span>(<span class=\"hljs-string\">\"contentful\"<\/span>)\n\n<span class=\"hljs-keyword\">const<\/span> client = contentful.createClient({\n  <span class=\"hljs-attr\">space<\/span>: process.env.CONTENTFUL_SPACE_ID,\n  <span class=\"hljs-attr\">accessToken<\/span>: process.env.CONTENTFUL_ACCESS_TOKEN_DELIVERY,\n  <span class=\"hljs-attr\">environment<\/span>: process.env.CONTENTFUL_ENVIRONMENT,\n  <span class=\"hljs-attr\">host<\/span>: <span class=\"hljs-string\">'cdn.contentful.com'<\/span>\n})\n\n<span class=\"hljs-built_in\">module<\/span>.exports = <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">return<\/span> client.getEntries({ <span class=\"hljs-attr\">content_type<\/span>: <span class=\"hljs-string\">'page'<\/span> })\n    .then(<span class=\"hljs-function\">(<span class=\"hljs-params\">response<\/span>) =&gt;<\/span> response.items.map(<span class=\"hljs-function\"><span class=\"hljs-params\">item<\/span> =&gt;<\/span> {\n      <span class=\"hljs-keyword\">return<\/span> {\n        ...item.fields,\n        <span class=\"hljs-attr\">components<\/span>: item.fields.components.map(<span class=\"hljs-function\"><span class=\"hljs-params\">component<\/span> =&gt;<\/span> {\n          <span class=\"hljs-keyword\">return<\/span> {\n            <span class=\"hljs-attr\">id<\/span>: component.sys.id,\n            <span class=\"hljs-attr\">type<\/span>: component.sys.contentType.sys.id\n          }\n        })\n      }\n    })\n    .catch(<span class=\"hljs-built_in\">console<\/span>.error)\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Here\u2019s how to include the page components dynamically inside the Liquid template, <code>pages.liquid<\/code>. Notice how I pass the component\u2019s <code>id<\/code> parameter to the Liquid partial and use the <code>type<\/code> parameter to determine the path of the included component.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\">{%- for component in page.components -%}\n  {%- assign includePath = 'partials\/' | append: component.type -%}\n  {%- include includePath, id: component.id -%}\n{%- endfor -%}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Here\u2019s how to fetch hero sections from Contentful in the <code>_data\/hero.js<\/code> file. Notice how I transform the CTA components object using only its <code>id<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> contentful = <span class=\"hljs-built_in\">require<\/span>(<span class=\"hljs-string\">\"contentful\"<\/span>)\n\n<span class=\"hljs-keyword\">const<\/span> client = contentful.createClient({\n  <span class=\"hljs-attr\">space<\/span>: process.env.CONTENTFUL_SPACE_ID,\n  <span class=\"hljs-attr\">accessToken<\/span>: process.env.CONTENTFUL_ACCESS_TOKEN_DELIVERY,\n  <span class=\"hljs-attr\">environment<\/span>: process.env.CONTENTFUL_ENVIRONMENT,\n  <span class=\"hljs-attr\">host<\/span>: <span class=\"hljs-string\">'cdn.contentful.com'<\/span>\n})\n\n<span class=\"hljs-built_in\">module<\/span>.exports = <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">return<\/span> client.getEntries({ <span class=\"hljs-attr\">content_type<\/span>: <span class=\"hljs-string\">'hero'<\/span> })\n    .then(<span class=\"hljs-function\">(<span class=\"hljs-params\">response<\/span>) =&gt;<\/span> response.items.map(<span class=\"hljs-function\"><span class=\"hljs-params\">item<\/span> =&gt;<\/span> {\n      <span class=\"hljs-keyword\">return<\/span> {\n        ...item.fields,\n        <span class=\"hljs-attr\">id<\/span>: item.sys.id,\n        <span class=\"hljs-attr\">cta<\/span>: item.fields.cta.map(<span class=\"hljs-function\"><span class=\"hljs-params\">cta<\/span> =&gt;<\/span> cta.sys.id)\n      }\n    })\n    .catch(<span class=\"hljs-built_in\">console<\/span>.error)\n})<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Here\u2019s how I search for the hero component I need. Notice how I use the <code>id<\/code> parameter previously passed from the Liquid template page.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\">{%- assign componentHero = hero | where: 'id', id | first -%}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Here\u2019s how to add the CTA components to the hero component in the <code>_includes\/partials\/hero.liquid<\/code> Liquid partial. Notice how I pass the CTA\u2019s <code>id<\/code> parameter to the Liquid <code>cta<\/code> partial.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\">{%- if componentHero.cta -%}\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"hero__action\"<\/span>&gt;<\/span>\n    {%- for ctaId in componentHero.cta -%}\n      {%- include 'partials\/cta', id: ctaId -%}\n    {%- endfor -%}\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n{%- endif -%}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Here\u2019s how to fetch CTA components from Contentful in the <code>_data\/cta.js<\/code> file.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> contentful = <span class=\"hljs-built_in\">require<\/span>(<span class=\"hljs-string\">\"contentful\"<\/span>)\n\n<span class=\"hljs-keyword\">const<\/span> client = contentful.createClient({\n  <span class=\"hljs-attr\">space<\/span>: process.env.CONTENTFUL_SPACE_ID,\n  <span class=\"hljs-attr\">accessToken<\/span>: process.env.CONTENTFUL_ACCESS_TOKEN_DELIVERY,\n  <span class=\"hljs-attr\">environment<\/span>: process.env.CONTENTFUL_ENVIRONMENT,\n  <span class=\"hljs-attr\">host<\/span>: <span class=\"hljs-string\">'cdn.contentful.com'<\/span>\n})\n\n<span class=\"hljs-built_in\">module<\/span>.exports = <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">return<\/span> client.getEntries({ <span class=\"hljs-attr\">content_type<\/span>: <span class=\"hljs-string\">'cta'<\/span> })\n    .then(<span class=\"hljs-function\">(<span class=\"hljs-params\">response<\/span>) =&gt;<\/span> response.items.map(<span class=\"hljs-function\"><span class=\"hljs-params\">item<\/span> =&gt;<\/span> {\n      <span class=\"hljs-keyword\">return<\/span> {\n        ...item.fields,\n        <span class=\"hljs-attr\">id<\/span>: item.sys.id\n      }\n    })\n    .catch(<span class=\"hljs-built_in\">console<\/span>.error)\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And here\u2019s how to find and display the CTA component in the <code>_includes\/partials\/cta.liquid<\/code> Liquid partial. Notice how I use the <code>id<\/code> parameter previously passed from the hero Liquid template.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\">{%- assign componentCta = cta | where: 'id', id | first -%}\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">a<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"cta\"<\/span> <span class=\"hljs-attr\">href<\/span>=<span class=\"hljs-string\">\"{{ componentCta.url }}\"<\/span>&gt;<\/span>{{ componentCta.text }}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">a<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now that we know how our page template works let\u2019s see how to set up the live preview.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Live Preview<\/h2>\n\n\n\n<p>I\u2019m using Netlify Functions, the <a href=\"https:\/\/liquidjs.com\/\">LiquidJS<\/a> package, and its <a href=\"https:\/\/liquidjs.com\/tutorials\/render-file.html\">render file<\/a> method for live previewing. This approach has limitations\u2014live editing and in-page changes are unavailable.<\/p>\n\n\n\n<p>First, I need the Liquid page, where I can parse the URL parameters and make requests for the Netlify function. Here\u2019s the code for the <code>preview.html<\/code> Liquid page.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\">---\ntitle: Preview page\nlayout: default\npermalink: \"\/preview\/\"\n---\n\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">script<\/span>&gt;<\/span><span class=\"javascript\">\n<span class=\"hljs-keyword\">const<\/span> preview = <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">const<\/span> urlParams = <span class=\"hljs-keyword\">new<\/span> URLSearchParams(<span class=\"hljs-built_in\">window<\/span>.location.search)\n  <span class=\"hljs-keyword\">const<\/span> id = urlParams.get(<span class=\"hljs-string\">'id'<\/span>)\n  <span class=\"hljs-keyword\">const<\/span> response = <span class=\"hljs-keyword\">await<\/span> fetch(<span class=\"hljs-string\">`\/.netlify\/functions\/preview-page\/?pageId=<span class=\"hljs-subst\">${id}<\/span>`<\/span>)\n  <span class=\"hljs-built_in\">document<\/span>.body.appendChild(<span class=\"hljs-keyword\">await<\/span> response.text())\n}\npreview();\n<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">script<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Next, I need the Netlify Function. I placed it under the <code>netlify\/functions<\/code> folder and named it <code>preview-page.cjs<\/code>.<\/p>\n\n\n\n<p class=\"learn-more\"><code>.cjs<\/code> means we\u2019re using the CommonJS module for Node.js.<\/p>\n\n\n\n<p>First, I need to include LiquidJS and initialize it (after installing it).<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> liquid = <span class=\"hljs-string\">'liquidjs'<\/span>\n<span class=\"hljs-keyword\">const<\/span> path = <span class=\"hljs-string\">'path'<\/span>\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-keyword\">async<\/span> (req) =&gt; {\n  <span class=\"hljs-keyword\">const<\/span> engine = <span class=\"hljs-keyword\">new<\/span> liquid.Liquid({\n    <span class=\"hljs-attr\">root<\/span>: path.resolve(__dirname, <span class=\"hljs-string\">'..\/..\/site\/_includes\/'<\/span>),\n  })\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>I need to fetch the data from Contentful using the Contentful Preview API. The difference between the Contentful Delivery API and the Contentful Preview API is that the preview will return draft and changed content.<\/p>\n\n\n\n<p>I can reuse the code for fetching the Contentful content like explained in the previous section, but I need to make sure to use <strong>the preview token<\/strong> this time to fetch the unpublished changes. This is how my <code>netlify\/functions\/data\/pages.js<\/code> file looks like.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> contentful = <span class=\"hljs-built_in\">require<\/span>(<span class=\"hljs-string\">\"contentful\"<\/span>)\n\n<span class=\"hljs-keyword\">const<\/span> client = contentful.createClient({\n  <span class=\"hljs-attr\">space<\/span>: process.env.CONTENTFUL_SPACE_ID,\n  <span class=\"hljs-attr\">accessToken<\/span>: process.env.CONTENTFUL_ACCESS_TOKEN_PREVIEW,\n  <span class=\"hljs-attr\">environment<\/span>: process.env.CONTENTFUL_ENVIRONMENT,\n  <span class=\"hljs-attr\">host<\/span>: <span class=\"hljs-string\">'preview.contentful.com'<\/span>\n})\n\n<span class=\"hljs-built_in\">module<\/span>.exports = <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">return<\/span> client.getEntries({ <span class=\"hljs-attr\">content_type<\/span>: <span class=\"hljs-string\">'page'<\/span> })\n    ...\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>I can import all content types in my serverless function now.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> pages = <span class=\"hljs-built_in\">require<\/span>(<span class=\"hljs-string\">'.\/data\/pages'<\/span>)\n<span class=\"hljs-keyword\">const<\/span> components = <span class=\"hljs-built_in\">require<\/span>(<span class=\"hljs-string\">'.\/data\/components'<\/span>)\n<span class=\"hljs-keyword\">const<\/span> cta = <span class=\"hljs-built_in\">require<\/span>(<span class=\"hljs-string\">'.\/data\/cta'<\/span>)<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Next, I need to parse the parameters and find the requested page by matching the requested page\u2019s <code>id<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-keyword\">async<\/span> (req, context) =&gt; {\n  <span class=\"hljs-keyword\">const<\/span> urlParams = <span class=\"hljs-keyword\">new<\/span> URLSearchParams(req.url.split(<span class=\"hljs-string\">'?'<\/span>).pop())\n  <span class=\"hljs-keyword\">const<\/span> id = urlParams.get(<span class=\"hljs-string\">'pageId'<\/span>)\n  <span class=\"hljs-keyword\">const<\/span> pagesArray = <span class=\"hljs-keyword\">await<\/span> pages()\n  <span class=\"hljs-keyword\">const<\/span> page = pagesArray.find(<span class=\"hljs-function\"><span class=\"hljs-params\">page<\/span> =&gt;<\/span> page.pageId === id)\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now, I can render the page template by passing the <code>page<\/code>, <code>component<\/code>, and <code>cta<\/code> data from Contentful. Finally, I return the rendered HTML as a string.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-keyword\">async<\/span> (req, context) =&gt; {\n  <span class=\"hljs-keyword\">const<\/span> l = <span class=\"hljs-keyword\">await<\/span> engine\n    .renderFile(<span class=\"hljs-string\">\"helpers\/page\"<\/span>, {\n    <span class=\"hljs-string\">'page'<\/span>: page,\n    <span class=\"hljs-string\">'components'<\/span>: <span class=\"hljs-keyword\">await<\/span> components(),\n    <span class=\"hljs-string\">'cta'<\/span>: <span class=\"hljs-keyword\">await<\/span> cta()\n  })\n\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">new<\/span> Response(<span class=\"hljs-keyword\">await<\/span> l)\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Here\u2019s how the whole serverless function looks.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> liquid = <span class=\"hljs-built_in\">require<\/span>(<span class=\"hljs-string\">'liquidjs'<\/span>)\n<span class=\"hljs-keyword\">const<\/span> pages = <span class=\"hljs-built_in\">require<\/span>(<span class=\"hljs-string\">'.\/data\/pages'<\/span>)\n<span class=\"hljs-keyword\">const<\/span> components = <span class=\"hljs-built_in\">require<\/span>(<span class=\"hljs-string\">'.\/data\/components'<\/span>)\n<span class=\"hljs-keyword\">const<\/span> cta = <span class=\"hljs-built_in\">require<\/span>(<span class=\"hljs-string\">'.\/data\/cta'<\/span>)\n<span class=\"hljs-keyword\">const<\/span> path = <span class=\"hljs-built_in\">require<\/span>(<span class=\"hljs-string\">'path'<\/span>)\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-keyword\">async<\/span> (req, context) =&gt; {\n  <span class=\"hljs-keyword\">const<\/span> urlParams = <span class=\"hljs-keyword\">new<\/span> URLSearchParams(req.url.split(<span class=\"hljs-string\">'?'<\/span>).pop())\n  <span class=\"hljs-keyword\">const<\/span> id = urlParams.get(<span class=\"hljs-string\">'pageId'<\/span>)\n  <span class=\"hljs-keyword\">const<\/span> pagesArray = <span class=\"hljs-keyword\">await<\/span> pages()\n  <span class=\"hljs-keyword\">const<\/span> page = pagesArray.find(<span class=\"hljs-function\"><span class=\"hljs-params\">page<\/span> =&gt;<\/span> page.pageId === id)\n\n  <span class=\"hljs-keyword\">if<\/span> (page) {\n    <span class=\"hljs-keyword\">const<\/span> engine = <span class=\"hljs-keyword\">new<\/span> liquid.Liquid({\n      <span class=\"hljs-attr\">root<\/span>: path.resolve(__dirname, <span class=\"hljs-string\">'..\/..\/site\/_includes\/'<\/span>),\n      <span class=\"hljs-attr\">extname<\/span>: <span class=\"hljs-string\">'.html'<\/span>\n    })\n\n    <span class=\"hljs-keyword\">const<\/span> l = <span class=\"hljs-keyword\">await<\/span> engine\n      .renderFile(<span class=\"hljs-string\">\"helpers\/page\"<\/span>, {\n      <span class=\"hljs-string\">'page'<\/span>: page,\n      <span class=\"hljs-string\">'components'<\/span>: <span class=\"hljs-keyword\">await<\/span> components(),\n      <span class=\"hljs-string\">'cta'<\/span>: <span class=\"hljs-keyword\">await<\/span> cta()\n    })\n\n    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">new<\/span> Response(<span class=\"hljs-keyword\">await<\/span> l)\n  } <span class=\"hljs-keyword\">else<\/span> {\n    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">new<\/span> Response(<span class=\"hljs-string\">`&lt;div style=\"margin:auto\"&gt;\n      &lt;p&gt;Couldn't fetch this page.&lt;\/p&gt;\n      &lt;p&gt;Please check the &lt;code&gt;id&lt;\/code&gt;.&lt;\/p&gt;\n      &lt;br&gt;\n      &lt;p&gt;&lt;code&gt;id: <span class=\"hljs-subst\">${id}<\/span>&lt;\/code&gt;&lt;\/p&gt;\n    &lt;\/div&gt;`<\/span>)\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>To test our function, I used the <a href=\"https:\/\/docs.netlify.com\/cli\/get-started\/\">Netlify CLI<\/a>. After running the <code>netlify dev<\/code> to run the serverless function locally, I\u2019ve opened the <code>localhost:8888\/preview\/?id=XYZ<\/code> and this is what I got:<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='316' src='https:\/\/videopress.com\/embed\/oRIA7bVq?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1674852142'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>The live preview is convenient for all team members, including me. In the future, I plan to add live preview templates for other headless CMS platforms, like <a href=\"https:\/\/strapi.io\/\">Strapi<\/a> and WordPress.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>As a part of the marketing team at Heyflow, I collaborate with people who work on the company\u2019s website. Although all team members are technically acquainted, sometimes they struggle to update the website. The struggle is not being able to visualize what will change on the page when updating the content. Saving the updates and [&hellip;]<\/p>\n","protected":false},"author":16,"featured_media":1376,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[137,136,138],"class_list":["post-1367","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-contentful","tag-eleventy","tag-liquid"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/03\/live-preview-thumb.jpg?fit=1000%2C500&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/1367","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/16"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=1367"}],"version-history":[{"count":4,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/1367\/revisions"}],"predecessor-version":[{"id":1385,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/1367\/revisions\/1385"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/1376"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=1367"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=1367"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=1367"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}