<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Jacob Pretorius</title><link href="http://world.optimizely.com" /><updated>2025-07-21T09:45:17.0000000Z</updated><id>https://world.optimizely.com/blogs/jacob-pretorius/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>Build a headless blog with Astro and Optimizely SaaS CMS Part 4 - SSR &amp; Visual Builder Experiences</title><link href="https://world.optimizely.com/blogs/jacob-pretorius/dates/2025/7/build-a-headless-blog-with-astro-and-optimizely-saas-cms-part-4---ssr--visual-builder-experience/" /><id>&lt;p&gt;For those new to the series, you may want to read &lt;a href=&quot;/link/033891ed66d94136a8b66d0e00ee3619.aspx&quot;&gt;parts one&lt;/a&gt;, &lt;a href=&quot;/link/59abecef9d6642b9816d424de5a0a99b.aspx&quot;&gt;two&lt;/a&gt;, and &lt;a href=&quot;/link/6d4fd38dc1254a36855db99902ac4f17.aspx&quot;&gt;three&lt;/a&gt; first.&lt;/p&gt;
&lt;p&gt;At the end of my last post I mentioned I had two more ideas to explore:&lt;/p&gt;
&lt;ol&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ol&gt;
&lt;li style=&quot;font-style: italic;&quot;&gt;&lt;em&gt;Render some purely Visual Builder content&lt;/em&gt;&lt;/li&gt;
&lt;li style=&quot;font-style: italic;&quot;&gt;&lt;em&gt;Render the whole thing dynamically rather than statically.&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&#39;s do that.&lt;/p&gt;
&lt;h2&gt;From Static to Server-Side Rendered (SSR)&lt;/h2&gt;
&lt;p&gt;I started &lt;a href=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo/tree/main/ssr&quot;&gt;a new server-side rendered version&lt;/a&gt; of the demo site that builds on everything added in the &lt;a href=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo/tree/main/static-site&quot;&gt;static site version&lt;/a&gt;. This means content changes in the CMS now appear on the SSR website version nearly instantly, without needing to trigger a new build on Vercel. For sites with lots of content changes, this will be a much more suitable approach compared to the webhook process we used with the static site version.&lt;/p&gt;
&lt;p&gt;The core change was changing &lt;a href=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo/blob/main/ssr/astro.config.mjs&quot;&gt;astro.config.mjs&lt;/a&gt; from static output to `output: &#39;server&#39;`. This change tells Astro to stop pre-building all the pages into HTML files and instead render each page on the server when a user requests it.&lt;/p&gt;
&lt;p&gt;I also had to install the appropriate &lt;a href=&quot;https://docs.astro.build/en/guides/on-demand-rendering/#server-adapters&quot;&gt;Server Adapter&lt;/a&gt;, in this case `@astrojs/vercel` since this demo is running on Vercel.&lt;/p&gt;
&lt;p&gt;This is great for content-heavy sites where updates are frequent, changes reflect as soon as they are indexed in Graph.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;TypeScript&lt;/h2&gt;
&lt;p&gt;In the last post, I mentioned that the GraphQL integration could be improved by automatically creating TypeScript types. I thought given that I&#39;m making more changes in the new version, I might as well add that too.&lt;/p&gt;
&lt;p&gt;By installing the `@graphql-codegen/cli` package and setting up a &lt;a href=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo/blob/main/ssr/codegen.ts&quot;&gt;codegen.ts &lt;/a&gt;config file, we can now run a single command: `npm run codegen`.&lt;/p&gt;
&lt;p&gt;This command connects to our Optimizely Graph endpoint, scans our queries in&amp;nbsp;&lt;a href=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo/blob/main/ssr/src/api/apollo-client.ts&quot;&gt;apollo-client.ts&lt;/a&gt;, and automatically generates TypeScript types for them. When developing, this means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Full type-safety:&lt;/strong&gt; No more guessing what fields are available on a content item.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Intelligent autocompletion:&lt;/strong&gt; The code editor knows our schema, which makes writing queries and using the data much faster and less error-prone.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I updated all the page and component layouts to use the imports from our generated types e.g.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;import type { StartPage, ArticlePage as ArticlePageType } from &#39;../gql/graphql&#39;;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo/blob/main/images/4/types.png?raw=true&quot; alt=&quot;Code editor showing a string type for the article.Heading property&quot; width=&quot;848&quot; height=&quot;477&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a nice improvement to the developer experience and something you&amp;rsquo;d want in any real-world project.&lt;/p&gt;
&lt;h2&gt;Adding Experiences and Visual Builder&lt;/h2&gt;
&lt;p&gt;The SSR version now properly supports Optimizely&#39;s Visual Builder with Experiences.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo/blob/main/images/4/experience-setup.png?raw=true&quot; alt=&quot;Optimizely SaaS CMS showing the setup of an Experience Hero component with Description, Heading, and Image properties&quot; width=&quot;1680&quot; height=&quot;945&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I&#39;ve included three to get started:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ExperienceHero:&lt;/strong&gt; A simple, reusable full-width hero banner with a background image and text overlay.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ExperienceCarousel:&lt;/strong&gt; For creating responsive image grids.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ExperienceRTE:&lt;/strong&gt; A standard Rich Text Editor for displaying any kind of formatted text content.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can find them in the components folder in the repo, e.g. &lt;a href=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo/blob/main/ssr/src/components/ExperienceHero.astro&quot;&gt;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo/blob/main/ssr/src/components/ExperienceHero.astro&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Content editors can now compose these with Experience page type inside the CMS and see them &lt;a href=&quot;https://opti-saas-astro-ssr-demo.vercel.app/experience/&quot;&gt;rendered on page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo/blob/main/images/4/experience-cms.png?raw=true&quot; alt=&quot;Optimizely SaaS CMS showing an Experience Page with the three Experience components&quot; width=&quot;1680&quot; height=&quot;945&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Visual builder has come a long way already.&lt;/p&gt;
&lt;h2&gt;Caching&lt;/h2&gt;
&lt;p&gt;Graph responses seem to be relatively quick, but as you might expect, a server-side rendered page that has to load page content from an external source for every request won&#39;t be as performant as a static site that can instantly start streaming the response HTML.&lt;/p&gt;
&lt;p&gt;Thankfully, in-memory caching on the server is still possible. So I added a 2-minute cache for all normal page loads (non-preview mode). Only the first new Graph request will go out to Graph every 2 minutes. You can see and change the cache configuration to your needs in&amp;nbsp;&lt;a href=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo/blob/main/ssr/src/api/apollo-client.ts&quot;&gt;apollo-client.ts&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This new SSR version is a much more robust and dynamic foundation for building with Astro and Optimizely SaaS CMS. It brings the power of instant content updates and a true visual editing experience to the forefront, while making the development process more efficient with type generation.&lt;/p&gt;
&lt;p&gt;It upgrades my example from a simple blog (&lt;a href=&quot;https://jacob.earth/blog/&quot;&gt;like mine&lt;/a&gt;) into something you could use for a more complex and content-rich enterprise site.&lt;/p&gt;
&lt;p&gt;Next time, I&#39;ll look at integrating &lt;a href=&quot;https://github.com/episerver/content-js-sdk&quot;&gt;the new beta Optimizely content-js-sdk&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Check out the live demo and the code for yourself&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Live SSR Demo: &lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;a class=&quot;ng-star-inserted&quot; href=&quot;https://opti-saas-astro-ssr-demo.vercel.app&quot;&gt;https://opti-saas-astro-ssr-demo.vercel.app&lt;/a&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Github Repo (SSR Folder): &lt;a href=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo/tree/main/ssr&quot;&gt;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo/tree/main/ssr&lt;/a&gt;&amp;nbsp;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;See you next time &#128521;&lt;/p&gt;</id><updated>2025-07-21T09:45:17.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Visualise Cloudflare Edge Logs with Grafana</title><link href="https://world.optimizely.com/blogs/jacob-pretorius/dates/2025/4/visualise-cloudflare-edge-logs-with-grafana/" /><id>&lt;p&gt;Optimizely currently has an &lt;a href=&quot;https://www.optimizely.com/beta/&quot;&gt;open beta&lt;/a&gt; for DXP customers to get access to their Cloudflare Edge logs. This beta has been around for a while, but I haven&#39;t seen that much posted about it, perhaps because edge logs can be kind of &lt;em&gt;boring&lt;/em&gt;?&lt;/p&gt;
&lt;p&gt;Well, not if we visualise them with fancy dashboards!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/jacobpretorius/Opti.Edge.Logs.Grafana/blob/main/images/example-dashboard.png?raw=true&quot; alt=&quot;Grafana dashboard showing Cloudflare Edge Logs&quot; width=&quot;1922&quot; height=&quot;1080&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Tech Stack&lt;/h2&gt;
&lt;p&gt;How this all works (at least during the beta phase) is that the Cloudflare edge logs are pushed to an Azure Blob Storage container owned by Optimizely, which you get a SAS link for. What you do with the logs &lt;em&gt;from&lt;/em&gt; that storage container is up to you.&lt;/p&gt;
&lt;p&gt;I&#39;ve chosen to use &lt;a href=&quot;https://grafana.com/grafana/&quot;&gt;Grafana&lt;/a&gt; as it&#39;s a popular choice among the homelab crowd (guilty) because it makes it easy to visually represent pretty much anything. In order to get Grafana to &quot;understand&quot; our logs they need to go to &lt;a href=&quot;https://grafana.com/oss/loki/&quot;&gt;Loki&lt;/a&gt; first, which you can think of as the underlying database engine.&lt;/p&gt;
&lt;p&gt;The last thing we need is to get the actual log files &lt;em&gt;from&lt;/em&gt; the Azure storage container &lt;em&gt;into&lt;/em&gt; Loki. There may be ways to do that automatically, such as with an ingestion pipeline from Blob Storage to Azure Log Analytics, but I like building my own stuff so I&#39;ve done that instead.&lt;/p&gt;
&lt;p&gt;I&#39;ve wrapped everything we need in a Docker stack to make working with it super easy.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/jacobpretorius/Opti.Edge.Logs.Grafana/blob/main/images/tech.png?raw=true&quot; alt=&quot;Tech stack: Cloudflare logs -&amp;gt; Azure storage -&amp;gt; nodejs ingest engine -&amp;gt; Loki -&amp;gt; Grafana&quot; width=&quot;1546&quot; height=&quot;477&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;p&gt;Getting this going locally should be fairly simple.&lt;/p&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Get access to the &lt;a href=&quot;https://www.optimizely.com/beta-signup/?beta=Edge+logs+for+DXP+cloud+services&quot;&gt;edge logs beta by applying for it&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Install Docker.&lt;/li&gt;
&lt;li&gt;Once approved for the beta, go to the &lt;a href=&quot;https://paasportal.episerver.net/&quot;&gt;PaaS portal &lt;/a&gt;and create an API key for the approved project. Make sure to select &quot;Edge Logs&quot; as the required permission.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Project Setup&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jacobpretorius/Opti.Edge.Logs.Grafana/tree/main&quot;&gt;Clone the git repo&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Create an `/.env` file based on my&amp;nbsp;&lt;a href=&quot;https://github.com/jacobpretorius/Opti.Edge.Logs.Grafana/blob/main/example.env&quot;&gt;example.env,&lt;/a&gt; replace with your values where needed. Consider changing the default Grafana username and password while you are at it.&lt;/li&gt;
&lt;li&gt;Open a terminal in the project directory and start the docker stack with `docker compose up --build -d`&lt;/li&gt;
&lt;li&gt;Grafana should now be running at http://localhost:3000&lt;/li&gt;
&lt;li&gt;Login as user / pass configured, or default admin/admin&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You should have the stack running in Docker.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/jacobpretorius/Opti.Edge.Logs.Grafana/blob/main/images/docker-stack.png?raw=true&quot; alt=&quot;The stack running in Docker&quot; width=&quot;1920&quot; height=&quot;1080&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If you need to debug anything with the log ingestion, check the container for it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/jacobpretorius/Opti.Edge.Logs.Grafana/blob/main/images/log-ingest.png?raw=true&quot; alt=&quot;log ingestor container logs in Docker&quot; width=&quot;1920&quot; height=&quot;1080&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Once done, shut down with `docker compose down` in the same project terminal to bring the stack down.&lt;/p&gt;
&lt;h3&gt;Adding a Dashboard&lt;/h3&gt;
&lt;p&gt;You can import the dashboard from my example above with the Grafana UI using the &lt;a href=&quot;https://github.com/jacobpretorius/Opti.Edge.Logs.Grafana/blob/main/example-dashboard.json&quot;&gt;example dashboard JSON file&lt;/a&gt;. Give it a couple of minutes on initial startup to get enough data to fill all the panels.&lt;/p&gt;
&lt;h2&gt;Useful Links&lt;/h2&gt;
&lt;p&gt;If you want to explore the edge logs more the &lt;a href=&quot;https://developers.cloudflare.com/logs/reference/log-fields/zone/http_requests/#cachecachestatus&quot;&gt;Cloudflare Edge Logs docs&lt;/a&gt; are a good reference for what the values mean so you can create your own dashboard panels from there.&lt;/p&gt;
&lt;p&gt;If you run into any issues with access to the logs you may need to check &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/logging-options#edge-logs-streaming-using-cdn-beta&quot;&gt;the documentation from Optimizely&lt;/a&gt;. I&#39;ve taken care of what the EpiCloud module does in my code to get the SAS link automatically as that&#39;s just easier to have it auto-refresh the SAS link when it expires.&lt;/p&gt;
&lt;p&gt;And in case you want to contribute or raise issues: &lt;a href=&quot;https://github.com/jacobpretorius/Opti.Edge.Logs.Grafana&quot;&gt;https://github.com/jacobpretorius/Opti.Edge.Logs.Grafana&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I&#39;m really interested to see what visualisations people come up with. So far, I&#39;ve found the Traffic Type and CF Block Actions to be very nice as those aren&#39;t things we&#39;re usually exposed to.&lt;/p&gt;
&lt;p&gt;Please share your custom dashboards and panels with us &#128519;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Presentation&lt;/h2&gt;
&lt;p&gt;I spoke about this at the London dev meetup, watch the talk below.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://youtu.be/yqtleljncDg?t=5159&quot;&gt;https://youtu.be/yqtleljncDg?t=5159&lt;/a&gt;&lt;/p&gt;</id><updated>2025-04-30T13:10:08.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Notes on Optimizely Self-Optimizing Block</title><link href="https://world.optimizely.com/blogs/jacob-pretorius/dates/2025/4/notes-on-optimizely-self-optimizing-block/" /><id>&lt;p&gt;While the free &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/optimizely-ab-testing&quot;&gt;A/B Testing might be dead&lt;/a&gt;, the &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/optimizely-self-optimizing-block&quot;&gt;Self-Optimizing Block&lt;/a&gt; is still alive and kicking. Here&#39;s some notes for those debugging it.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Installation&lt;/h2&gt;
&lt;p&gt;You can install the self-optimising block with the non-obvious package name `&lt;span class=&quot;padding-right-8 repos-line-content removed&quot;&gt;EPiServer.&lt;span class=&quot;removed-content&quot;&gt;Cms.AddOns.Blocks&lt;/span&gt;&lt;/span&gt;` from &lt;a href=&quot;https://nuget.optimizely.com/package/?id=EPiServer.Cms.AddOns.Blocks&quot;&gt;https://nuget.optimizely.com/package/?id=EPiServer.Cms.AddOns.Blocks&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Ensure you have `app.UseSessions()` in your `Startup.cs` (&lt;a href=&quot;/link/6b52e01dc86e4b8b95d9561c17ebc70e.aspx&quot;&gt;thanks Arjan!&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;You will then be able to create instances of &quot;Self Optimizing Block&quot; using the CMS.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Missing renderer&lt;/h2&gt;
&lt;p&gt;If for some reason you have a block called &quot;&lt;span class=&quot;epi-previewableTextBox-text dojoxEllipsis dijitInline&quot;&gt;&lt;span&gt;OptimizingBlock&lt;/span&gt;&lt;/span&gt;&quot; in your CMS it likely means the &quot;Self Optimizing Block&quot; package is no longer installed. You can verify this by going to the CMS admin &amp;gt; Content Types and searching for it to see if it is From code: No&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/2ae072a18962419aaebad3f8dcc2ee9f.aspx&quot; alt=&quot;&quot; width=&quot;444&quot; height=&quot;250&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If you install the package it should spring back to action and restore itself as &quot;Self Optimizing Block&quot;.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</id><updated>2025-04-29T09:41:58.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Build a headless blog with Astro and Optimizely SaaS CMS Part 3</title><link href="https://world.optimizely.com/blogs/jacob-pretorius/dates/2025/1/build-a-headless-blog-with-astro-and-optimizely-saas-cms-part-3/" /><id>&lt;p&gt;It is finally time to explore my basic blog example powered by &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt; and Opti SaaS CMS. For those new to the series, you may want to read &lt;a href=&quot;/link/033891ed66d94136a8b66d0e00ee3619.aspx&quot;&gt;parts one&lt;/a&gt; and &lt;a href=&quot;/link/59abecef9d6642b9816d424de5a0a99b.aspx&quot;&gt;two&lt;/a&gt; first.&lt;/p&gt;
&lt;h2&gt;Project Structure&lt;/h2&gt;
&lt;p&gt;I&#39;ve mostly kept to the standard project structure as initiated by the Astro blog template so it should be familiar to most people who have worked with the popular front-end frameworks.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/images/3/structure.png&quot; alt=&quot;The project folder structure&quot; width=&quot;1920&quot; height=&quot;1080&quot; /&gt;&lt;/p&gt;
&lt;p&gt;First, on the right, we can see the two main configuration files: `astro.config.mjs` is quite barebones and imports some additional functionality (automatic sitemap &amp;amp; Vercel platform support) and we also enable view transitions for browsers that support it. `.env` contains some SaaS CMS environment variables.&lt;/p&gt;
&lt;p&gt;Looking at the left of the image, we can follow the process of an incoming web request to a specific page URL:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;First, load the relevant logic for the specific URL path from the `pages` folder and execute the logic found. Similar to what we can think of as a &#39;controller&#39; in MVC terms. In this example, I&#39;ve set a catch-all route based on Astro &lt;a href=&quot;https://docs.astro.build/en/guides/routing/&quot;&gt;naming conventions&lt;/a&gt; with `[...slug].astro`.&amp;nbsp; It then loads the as-yet-unknown &#39;model for this URL by calling &lt;a href=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo/blob/main/static-site/src/api/apollo-client.ts&quot;&gt;apollo-client.ts&lt;/a&gt; which talks to Optimizely Graph via GraphQL (more on this later). &lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/images/3/route.png&quot; alt=&quot;Showing the catch-all route logic&quot; width=&quot;1920&quot; height=&quot;1080&quot; /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;The route then uses the &#39;model&#39; to populate common shared `components` like the header and footer, and most importantly the `TemplateResolver.astro` (also commonly referred to as TemplateCoordinator) to figure out what page type this &#39;model&#39; is&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/images/3/templateresolver.png&quot; alt=&quot;Showing the processing in TemplateResolver and the header component&quot; width=&quot;1920&quot; height=&quot;1080&quot; /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Once we have the page type figured out, we load and populate its exact layout from `layouts` so we can process the rendering of the &#39;view&#39;, here of the StartPage&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/images/3/layout.png&quot; alt=&quot;Showing the layout view redering&quot; width=&quot;1920&quot; height=&quot;1080&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It&#39;s all very familiar to those of us coming from the back-end world of doing this with PaaS in .NET. I&#39;m sure there are other (better?) ways to structure a solution like this, but given the audience here I think this is easiest to follow.&lt;/p&gt;
&lt;h2&gt;Did Someone Say Graph?&lt;/h2&gt;
&lt;p&gt;In `apollo-client.ts` I have all the GraphQL queries needed to render &lt;em&gt;everything&lt;/em&gt; for this site from the Optimizely Graph.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/images/3/gql.png&quot; alt=&quot;Showing how apollo-client is setup&quot; width=&quot;1920&quot; height=&quot;1080&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We&#39;re also caching the results using the built-in `InMemoryCache`, not that it matters too much for a statically generated site, but we might as well ensure our build times are quick when it is so easy to enable &#128519;&lt;/p&gt;
&lt;p&gt;Admittedly, I&#39;ve gone for the &quot;quick demo&quot; approach and built &lt;em&gt;one big query to rule them all&lt;/em&gt;&lt;span class=&quot;BxUVEf ILfuVd&quot;&gt;&lt;span class=&quot;hgKElc&quot;&gt;&lt;strong&gt;&amp;trade;&lt;/strong&gt;&lt;/span&gt;&lt;/span&gt; to resolve content by URL rather than what you&#39;d actually want which is more finely scoped queries for each unique page/content type, but the approach is largely the same you just end up with multiple chained requests to the graph.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/images/3/bigquery.png&quot; alt=&quot;Showing part of the big graphql query to load basically everything&quot; width=&quot;1920&quot; height=&quot;1080&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This could be further improved by using a package to automatically create TypeScript models based on what is indexed in the graph, as in the &lt;a href=&quot;https://github.com/episerver/cms-saas-vercel-demo&quot;&gt;Optimizely Nextjs Demo&lt;/a&gt;, but I&#39;ve not looked into that for this demo. It&#39;s standard GraphQL / Astro functionality here so shouldn&#39;t be too difficult.&lt;/p&gt;
&lt;h2&gt;CMS Editing&lt;/h2&gt;
&lt;p&gt;I didn&#39;t want to publish this post before I had time to wrap this up, and thanks to the holiday season I finally could! Optimizely provide &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/v1.0.0-CMS-SaaS/docs/enable-live-preview-saas&quot;&gt;documentation&lt;/a&gt; on the full process.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/images/3/editmode.png&quot; alt=&quot;CMS Edit mode is back&quot; width=&quot;1920&quot; height=&quot;1080&quot; /&gt;&lt;/p&gt;
&lt;p&gt;For this, I created an additional catch-all route in the `pages` directory, placing it under a new child `preview` folder. This means we now have a route registered for any request under `/preview` so that we can execute edit-only preview logic differently.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/images/3/edit1.png&quot; alt=&quot;The edit-only preview route&quot; width=&quot;1920&quot; height=&quot;1080&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As on-page editing / visual builder is an interactive experience, this won&#39;t quite work for our statically generated website. So we tell Astro to not prerender this page route - thereby making it dynamic. Then we follow the guidance from Optimizely for what we can expect as query parameters of the /preview URL when editors view the page in on-page editing. Most importantly we need to use the provided preview token for our existing graph queries so that we can load unpublished/draft content based on content version rather than URL (as the page may not have a URL yet).&lt;/p&gt;
&lt;p&gt;For the rendering side, we need to load the provided communication script as instructed by Opti, and setup a client-side event listener to detect the content saved event and refresh the on page editing experience for the editor.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/images/3/edit2.png&quot; alt=&quot;The wrapping script and browser saved event listener&quot; width=&quot;1920&quot; height=&quot;1080&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Finally, we need to wrap our editable layout elements with `data-epi-edit` HTML tags matching the property name as indexed in Graph. Example here for a page `MainBody`&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;span data-epi-edit=&#39;MainBody&#39;&amp;gt;
  &amp;lt;div set:html={pageContent.MainBody.html} /&amp;gt;
&amp;lt;/span&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And just like that, we have on-page editing and preview support &#128513;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;We&#39;ve finally reached what I had in mind for this project when I originally thought of it. We have a simple, yet solid, base template for a SaaS CMS site with Astro that mostly focuses on content that doesn&#39;t change all that often. I could 100% use this to power &lt;a href=&quot;https://jcpretorius.com/blog/&quot;&gt;my personal blog&lt;/a&gt; by swopping out the markdown files I currently use and instead pull content from Graph as we just saw for this demo.&lt;/p&gt;
&lt;p&gt;I have two more ideas in mind to build on:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Render some purely Visual Builder content&lt;/li&gt;
&lt;li&gt;Render the whole thing dynamically rather than statically.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;See you next time &#128521;&lt;/p&gt;
&lt;p&gt;As always, code on Github: &lt;a href=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo&quot;&gt;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/link/ad0ab559cb97490491e9c4d448f232ca.aspx&quot;&gt;Click here to read part four now&lt;/a&gt;.&lt;/p&gt;</id><updated>2025-01-06T13:47:07.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Build a headless blog with Astro and Optimizely SaaS CMS Part 2</title><link href="https://world.optimizely.com/blogs/jacob-pretorius/dates/2024/10/build-a-headless-blog-with-astro-and-optimizely-saas-cms-part-2/" /><id>&lt;p&gt;&lt;em&gt;Well, this took a while. Sorry! I got busy, and we had lots of Opticon content to cover. Now back to the regular programming.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Back in May, I shared &lt;a href=&quot;/link/033891ed66d94136a8b66d0e00ee3619.aspx&quot;&gt;part one&lt;/a&gt; of building a super simple static blog website using Optimizely headless SaaS and Astro. Let&#39;s go through the setup steps so you can get it running for yourself.&lt;/p&gt;
&lt;h2&gt;Get The CMS Ready&lt;/h2&gt;
&lt;p&gt;First, you&#39;ll want to &lt;a href=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/static-site/cms-data/ExportedFile.episerverdata&quot;&gt;download the CMS export&lt;/a&gt; of the sample content I&#39;ve modelled from GitHub.&lt;/p&gt;
&lt;p&gt;Then you should go to a SaaS CMS instance and import the export there. Choose the Root folder as the destination.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/images/ImportZeExport.png&quot; alt=&quot;Import the downloaded file&quot; width=&quot;1280&quot; height=&quot;720&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If all goes to plan you should have some content in your CMS.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/images/imported.png&quot; alt=&quot;Sample content inported in the CMS&quot; width=&quot;1280&quot; height=&quot;720&quot; /&gt;&lt;/p&gt;
&lt;p&gt;While we&#39;re here, make sure to setup the &quot;Applications&quot; with a hostname where it will be available (the &lt;em&gt;expected&lt;/em&gt; Vercel URL). Also run the &quot;Optimizely Graph Full Synchronization&quot; scheduled job so everything gets pushed to Graph.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/images/Index.png&quot; alt=&quot;Run the scheduled job to index to Graph&quot; width=&quot;1280&quot; height=&quot;720&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Finally, go to the CMS dashboard page and take note of your Graph keys as we will need those later.&lt;/p&gt;
&lt;h2&gt;Copy The Code&lt;/h2&gt;
&lt;p&gt;Head over to &lt;a href=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo&quot;&gt;the repo&lt;/a&gt; and download/fork it for your site.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The repo should be fairly familiar if you&#39;ve worked with any of the modern front-end frameworks. All the fun stuff is in the `/static-site` directory. &lt;em&gt;This repo is a bit ambitious and I hope to someday add a non-static variant with more bells and whistles.&amp;nbsp;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/images/repo.png&quot; alt=&quot;The github repo&quot; width=&quot;1280&quot; height=&quot;720&quot; /&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You will want to update the &lt;a href=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo/blob/main/static-site/.env&quot;&gt;`.env` file&lt;/a&gt; with your Graph&#39;s Delivery Keys &quot;Single key&quot; from the CMS dashboard.&lt;/p&gt;
&lt;p&gt;Then you should update &lt;a href=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo/blob/main/static-site/astro.config.mjs&quot;&gt;astro.config.mjs&lt;/a&gt; with the same site URL you configured as a hostname.&lt;/p&gt;
&lt;p&gt;That&#39;s it really, save and push the code to a private repo of your choice.&lt;/p&gt;
&lt;h2&gt;Setup Vercel&lt;/h2&gt;
&lt;p&gt;The Vercel setup couldn&#39;t be easier, all you need to do is create a Vercel account and link your git repo. If you use GitHub this is seamless. Make sure to select the static-site folder as your root directory and it should do all the magic from there.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/images/vercel.png&quot; alt=&quot;Setup the Vercel deployment&quot; width=&quot;1280&quot; height=&quot;720&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;Setup a Deployment Webhook&lt;/h2&gt;
&lt;p&gt;With this incredibly simple use case, I&#39;m not doing anything fancy in terms of having Astro serve static content while doing a cache revalidation in the background or anything like that. At build time in Vercel, it pulls all relevant content from Graph and creates static HTML as output to get hosted on the edge.&lt;/p&gt;
&lt;p&gt;But I still think it&#39;s worth some automation, as we don&#39;t want to have to remember to trigger a deployment in Vercel every time content is updated in the CMS.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/images/WebhookDeployed.png&quot; alt=&quot;Vercel deployment webook in action&quot; width=&quot;1280&quot; height=&quot;720&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Thankfully, that&#39;s quite easy to do with a &lt;a href=&quot;https://docs.developers.optimizely.com/platform-optimizely/v1.4.0-optimizely-graph/docs/use-cases&quot;&gt;Vercel to Optimizely Webhook&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I won&#39;t copy all the steps from there so just follow the wiki from Optimizely, but I will tell you &lt;strong&gt;how to authorise with the Optimizely webhook API&lt;/strong&gt; so you can register the webhook as it is not mentioned anywhere! &lt;em&gt;(at time of writing, it has now been updated, thank you docs team)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In your API client of choice (Bruno, &lt;span style=&quot;text-decoration: line-through;&quot;&gt;Insomnia, Postman&lt;/span&gt;) set the authentication scheme to &quot;Basic Auth&quot;, then use your Graph Management Keys &quot;App Key&quot; as the username and the &quot;Secret&quot; as the password (you can get them from the CMS dashboard page). This took me ages to figure out.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/refs/heads/main/images/brunoAuth.png&quot; alt=&quot;How to register the webhook using the correct Auth scheme&quot; width=&quot;1280&quot; height=&quot;720&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Example webhook for on-published events, replace the &quot;URL&quot; with your webhook URL from the Vercel settings.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;POST https://cg.optimizely.com/api/webhooks
{
  &quot;disabled&quot;: false,
  &quot;request&quot; : {
    &quot;url&quot; : &quot;https://api.vercel.com/v1/integrations/deploy/secret/secret&quot;,
    &quot;method&quot;: &quot;post&quot;
  },
  &quot;topic&quot;: [&quot;*.*&quot;],
  &quot;filters&quot;: [
     { &quot;status&quot;: { &quot;eq&quot;: &quot;Published&quot; } }
   ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And with that, you should have a working website running in Vercel, with auto deployments when you make changes and publish it in the CMS.&lt;/p&gt;
&lt;p&gt;That&#39;s all for this post, &lt;a href=&quot;/link/6d4fd38dc1254a36855db99902ac4f17.aspx&quot;&gt;next time&lt;/a&gt; we can look at some code!&lt;/p&gt;</id><updated>2024-10-24T15:53:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Build a headless blog with Astro and Optimizely SaaS CMS</title><link href="https://world.optimizely.com/blogs/jacob-pretorius/dates/2024/5/build-a-headless-blog-with-astro-and-optimizely-saas-cms/" /><id>&lt;p&gt;&lt;span&gt;I&amp;rsquo;m a big fan of using the right tool for the right job. &lt;/span&gt;&lt;span&gt;I&amp;rsquo;m also a big fan of &lt;a href=&quot;https://astro.build&quot;&gt;Astro&lt;/a&gt;, for the right use case.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Let&#39;s explore Astro to see what it&#39;s all about and how to build an Optimizely SaaS CMS site using it.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;strong&gt;What is Astro&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span&gt;Astro calls itself &amp;ldquo;The web framework for content-driven websites&amp;rdquo;, which sounds very fancy and highly relevant to us as we enter the wonderful headless content delivery world with the Optimizely SaaS CMS. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;I&amp;rsquo;d imagine that many of the headless sites we&amp;rsquo;re going to be building over the next couple of years are going to lean quite strongly into not just being &amp;ldquo;content-driven&amp;rdquo; (that&amp;rsquo;s all websites?), but &amp;ldquo;content heavy&amp;rdquo;.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/main/images/astro.png&quot; width=&quot;1280&quot; alt=&quot;Screenshot of Astro website&quot; height=&quot;720&quot; /&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;&lt;span&gt;&lt;span class=&quot;mce-nbsp-wrap&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;The team behind Astro aren&amp;rsquo;t trying to build a javascript framework for all web experiences ever, instead, they go for a very specific kind of website. That gives them the luxury of making opinionated calls with the framework and the optimisations it can ship straight out of the box.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;The &lt;a href=&quot;https://docs.astro.build/en/concepts/why-astro/&quot;&gt;Why Astro&lt;/a&gt; &lt;/span&gt;&lt;span&gt;page has some great information, most notably:&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: bold; display: inline-block;&quot;&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/framework-components/&quot;&gt;UI-agnostic&lt;/a&gt;&lt;/span&gt;&lt;span style=&quot;font-weight: bold;&quot;&gt;:&lt;/span&gt;&lt;span&gt; Supports React, Preact, Svelte, Vue, Solid, Lit, HTMX, web components, and more.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: bold; display: inline-block;&quot;&gt;&lt;a href=&quot;https://docs.astro.build/en/basics/rendering-modes/&quot;&gt;Server-first&lt;/a&gt;&lt;/span&gt;&lt;span style=&quot;font-weight: bold;&quot;&gt;:&lt;/span&gt;&lt;span&gt; Moves expensive rendering off of your visitors&amp;rsquo; devices.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: bold; display: inline-block;&quot;&gt;&lt;a href=&quot;https://docs.astro.build/en/basics/astro-components/&quot;&gt;Zero JS, by default&lt;/a&gt;&lt;/span&gt;&lt;span style=&quot;font-weight: bold;&quot;&gt;:&lt;/span&gt;&lt;span&gt; Less client-side JavaScript to slow your site down.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;&lt;span class=&quot;mce-nbsp-wrap&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;However, the Astro team know the lines blur really quickly between static site, SPA, PWA, and everything else content-driven you can build with other frameworks. So they have this &lt;a href=&quot;https://docs.astro.build/en/concepts/islands/&quot;&gt;Islands&lt;/a&gt; &lt;/span&gt;&lt;span&gt;concept, where you can use your React/Vue/Svelte &amp;ldquo;components&amp;rdquo; inside your Astro project, and at build time they get rid of all the overhead from those frameworks and just ship the javascript needed to make your dynamic components work.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;If you think about it, these optimisations start to open many doors.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;I&amp;rsquo;ll wrap up this section with their &lt;strong&gt;design principles&lt;/strong&gt;, which I think help explain their biases clearly&lt;/span&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: bold; display: inline-block;&quot;&gt;&lt;a href=&quot;https://docs.astro.build/en/concepts/why-astro/#content-driven&quot;&gt;Content-driven&lt;/a&gt;&lt;/span&gt;&lt;span style=&quot;font-weight: bold;&quot;&gt;:&lt;/span&gt;&lt;span&gt; Astro was designed to showcase your content.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: bold; display: inline-block;&quot;&gt;&lt;a href=&quot;https://docs.astro.build/en/concepts/why-astro/#server-first&quot;&gt;Server-first&lt;/a&gt;&lt;/span&gt;&lt;span style=&quot;font-weight: bold;&quot;&gt;:&lt;/span&gt;&lt;span&gt; Websites run faster when they render HTML on the server.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: bold; display: inline-block;&quot;&gt;&lt;a href=&quot;https://docs.astro.build/en/concepts/why-astro/#fast-by-default&quot;&gt;Fast by default&lt;/a&gt;&lt;/span&gt;&lt;span style=&quot;font-weight: bold;&quot;&gt;:&lt;/span&gt;&lt;span&gt; It should be impossible to build a slow website in Astro.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: bold; display: inline-block;&quot;&gt;&lt;a href=&quot;https://docs.astro.build/en/concepts/why-astro/#easy-to-use&quot;&gt;Easy to use&lt;/a&gt;&lt;/span&gt;&lt;span style=&quot;font-weight: bold;&quot;&gt;:&lt;/span&gt;&lt;span&gt; You don&amp;rsquo;t need to be an expert to build something with Astro.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: bold; display: inline-block;&quot;&gt;&lt;a href=&quot;https://docs.astro.build/en/concepts/why-astro/#developer-focused&quot;&gt;Developer-focused&lt;/a&gt;&lt;/span&gt;&lt;span style=&quot;font-weight: bold;&quot;&gt;:&lt;/span&gt;&lt;span&gt; You should have the resources you need to be successful.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;strong&gt;What about Nextjs&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span&gt;Nextjs seems like it could be the default implementation choice for most Optimizely SaaS implementations, the Optimizely &lt;a href=&quot;https://github.com/episerver/cms-saas-vercel-demo&quot;&gt;demo implementation&lt;/a&gt;&lt;/span&gt;&lt;span&gt; also use next. As Nextjs itself is funded by Vercel and built with the React ecosystem you&amp;rsquo;re in relatively stable hands.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/main/images/next.png&quot; width=&quot;1280&quot; alt=&quot;Next website screenshot&quot; height=&quot;720&quot; /&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;&lt;span&gt;&lt;span class=&quot;mce-nbsp-wrap&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;If you&amp;rsquo;re somewhat familiar with Nextjs then you&amp;rsquo;re probably already thinking these two frameworks have a lot in common, and you&amp;rsquo;re right. As always, it comes down to the use case.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;I&amp;rsquo;m not going to argue the pros and cons here (maybe in a future post?), but I would like to explore the most simple of use cases where I think you could do well with either option. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;strong&gt;Let&amp;rsquo;s build a blog&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span&gt;What could be more simple than a static site with a few pages and some articles? I can&amp;rsquo;t think of anything exciting enough to build and share here.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Thankfully, building a decent blog with Astro is &lt;em&gt;ridiculously &lt;/em&gt;easy. I&amp;rsquo;m not joking. I moved my &lt;a href=&quot;https://jcpretorius.com/blog&quot;&gt;personal blog&lt;/a&gt; over from a custom CMS I built myself to Astro in less than a day, and it got even better in the process.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;I spun up the Astro blog template and hooked it up to my SaaS CMS Optimizely Graph and what do you know we have a working website.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;&lt;span&gt;&lt;img src=&quot;https://raw.githubusercontent.com/jacobpretorius/Opti.SaaS.Astro.Demo/main/images/sample-site.png&quot; width=&quot;1280&quot; alt=&quot;Screenshot of sample site&quot; height=&quot;720&quot; /&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;&lt;span&gt;&lt;span class=&quot;mce-nbsp-wrap&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Check it out here &lt;/span&gt;&lt;span style=&quot;display: inline-block;&quot;&gt;&lt;a href=&quot;https://opti-saas-astro-static-demo.vercel.app&quot;&gt;https://opti-saas-astro-static-demo.vercel.app&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;In the &lt;a href=&quot;/link/59abecef9d6642b9816d424de5a0a99b.aspx&quot;&gt;next post&lt;/a&gt; I&amp;rsquo;ll walk through the code a bit and share how it all fits together, but for the eager you can find it here &lt;/span&gt;&lt;span style=&quot;display: inline-block;&quot;&gt;&lt;a href=&quot;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo&quot;&gt;https://github.com/jacobpretorius/Opti.SaaS.Astro.Demo&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;</id><updated>2024-05-28T17:41:49.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>How to go beyond web A/B testing with Optimizely Feature Experimentation</title><link href="https://world.optimizely.com/blogs/jacob-pretorius/dates/2023/2/how-to-go-beyond-web-ab-testing-with-optimizely-feature-experimentation/" /><id>&lt;p&gt;&lt;span&gt;In &lt;a href=&quot;https://www.dotcentric.co.uk/news-and-insight/optimizely-feature-experimentation/&quot;&gt;the latest article&lt;/a&gt;, which follows on from our previous&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.dotcentric.co.uk/news-and-insight/optimizely-web-experimentation/&quot;&gt;Web Experimentation article&lt;/a&gt;&lt;span&gt;, we explain how Feature Experimentation goes beyond A/B testing to enable product teams to deliver higher quality releases, run safer tests and validate new features at scale.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Feel free to read &lt;a href=&quot;https://www.dotcentric.co.uk/news-and-insight/optimizely-feature-experimentation/&quot;&gt;How to go beyond web A/B testing with Optimizely Feature Experimentation&lt;/a&gt; on the dotcentric blog.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Happy Friday!&lt;/span&gt;&lt;/p&gt;</id><updated>2023-02-17T09:05:30.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>How Optimizely Web Experimentation enables fast validation of design and UX changes</title><link href="https://world.optimizely.com/blogs/jacob-pretorius/dates/2023/1/how-optimizely-web-experimentation-enables-fast-validation-of-design-and-ux-changes/" /><id>&lt;p&gt;&lt;span&gt;Ever wondered how to increase conversion rates and quickly validate design/UX alternatives to find the most impactful changes?&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span class=&quot;css-901oao&amp;#32;css-16my406&amp;#32;r-poiln3&amp;#32;r-bcqeeo&amp;#32;r-qvutc0&quot;&gt;I wrote a practical article showing how to do exactly that &lt;a href=&quot;https://www.dotcentric.co.uk/news-and-insight/optimizely-web-experimentation/&quot;&gt;using Optimzely Web Experimentation now available on the dotcentric blog&lt;/a&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;</id><updated>2023-01-31T15:18:17.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Three key takeaways from Opticon London 2022</title><link href="https://world.optimizely.com/blogs/jacob-pretorius/dates/2022/11/three-key-takeaways-from-opticon-london-2022/" /><id>&lt;p&gt;Myself and members of the dotcentric team spent the day at Opticon London 2022 learning all about the latest happenings in the Optimizely world.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.dotcentric.co.uk/news-and-insight/three-key-takeaways-from-opticon-2022/&quot;&gt;Head on over to the dotcentric blog&lt;/a&gt; to read about my three key takeaways from the event.&lt;/p&gt;
&lt;p&gt;See you next year!&lt;/p&gt;</id><updated>2022-11-10T16:45:35.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Waving To Advance Over the Sunset</title><link href="https://world.optimizely.com/blogs/jacob-pretorius/dates/2022/9/waving-to-advance-over-the-sunset/" /><id>&lt;p&gt;While doing some maintenance work for a client we noticed that on solution startup there were some DNS failure messages for the URL &amp;ldquo;emea.epcc.episerver.net&amp;rdquo; and looking at their appSettings.config it is clear where this comes from.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;&amp;lt;!-- EPiServer Advance --&amp;gt;
&amp;lt;add key=&quot;episerver:RecommendationServiceKey&quot; value=&quot;secret&quot; /&amp;gt;
&amp;lt;add key=&quot;episerver:RecommendationServiceSecret&quot; value=&quot; secret&quot; /&amp;gt;
&amp;lt;add key=&quot;episerver:RecommendationServiceUri&quot; value=&quot;https://emea.advance.episerver.net/&quot; /&amp;gt;
&amp;lt;add key=&quot;episerver:CmsCloudSynchronizationKey&quot; value=&quot; secret&quot; /&amp;gt;
&amp;lt;add key=&quot;episerver:CmsCloudSynchronizationSecret&quot; value=&quot; secret&quot; /&amp;gt;
&amp;lt;add key=&quot;episerver:CmsCloudSynchronizationUri&quot; value=&quot;https://emea.epcc.episerver.net/&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, getting to the bottom of these turned out to be quite a rabbit hole. Join me as we explore it.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Optimizely product names and package names in &lt;strong&gt;bold&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size:&amp;#32;24pt;&quot;&gt;&lt;strong&gt;Episerver Advance&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Optimizely support let us know that &lt;strong&gt;Advance&lt;/strong&gt; has been sunset, and pointed us to this document of retired products &lt;a href=&quot;/link/c6edeabaacf14b62876d5287e97717c3.aspx&quot;&gt;https://world.optimizely.com/contentassets/c1670edf938343f6b1b1076ece6abea6/episerver-discontinued-services-and-products---20200701-distro.pdf&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/9dA03Vg.jpg&quot; width=&quot;868&quot; alt=&quot;Some&amp;#32;sunset&amp;#32;Episerver&amp;#32;products&amp;#32;(Including&amp;#32;Advance&amp;#32;-&amp;#32;December&amp;#32;2020)&quot; height=&quot;433&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Okay great, we can get rid of it. But what &lt;em&gt;was&lt;/em&gt; it and which packages were a part of it? Of course, there isn&amp;rsquo;t a nice and easy Episerver.Advance package to uninstall and be done with it.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Advance&lt;/strong&gt; product announcement post in World by Joakim mentions installing the packages &lt;strong&gt;Episerver.Personalization.CMS&lt;/strong&gt; and &lt;strong&gt;EPiServer.Tracking.PageView&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/link/6a57ae88bcb54e819fef3e2c2ef30b11.aspx&quot;&gt;https://world.optimizely.com/blogs/joakim-platbarzdis/dates/2018/4/episerver-advance-launched/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;We can also see in this article which is in a &amp;ldquo;CHECKED&amp;rdquo; category, &lt;em&gt;whatever that means&lt;/em&gt;, that it needs the same packages &lt;a href=&quot;/link/8cee0891fd0b47aea4c1a742c21d761f.aspx&quot;&gt;https://world.optimizely.com/documentation/developer-guides/archive/personalization/checked/advance-api/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/RD7Yx4J.jpg&quot; width=&quot;1083&quot; alt=&quot;&quot; height=&quot;288&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s now drill into the packages a bit. There doesn&amp;rsquo;t seem to be any existing proper documentation around these anymore so time to spin up the decompiler.&lt;/p&gt;
&lt;p&gt;First starting with the packages for &lt;strong&gt;Advance.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size:&amp;#32;18pt;&quot;&gt;&lt;strong&gt;Episerver.Personalization.CMS &amp;ndash; &lt;/strong&gt;safe to remove&lt;/span&gt;&lt;br /&gt;This uses the connections strings matching the URL section &amp;ldquo;&lt;a href=&quot;https://atpscan.global.hornetsecurity.com/index.php?atp_str=gvTI2oXDUu5o4xxGQ8_4tYmuMJVw60Q4C3GlalJumgN3qe9fDg2qD9mjHD1CtJ7N2hgyrD7JK0587DrKhvWSYVCFjxFU92phv8m89SXl93LgtKbDycnE--znZjO11KvS1SKj8DRjZ3UaY3DS5-qLUZJiSa55I5FoFlejayMdtPl0JYinXynNGOtz0KqzzywIkV1PJc_GX8vwybuH92FvMoYrgfucSONT1mHdHMzIrqPOD6ulD6WLia167JnGXjm4GGR6eFBsHjo_FNIIPFDTJcArZh9fJxb0Sexz7TwIFSVQf4103ujGX5fyIaugtLCxupH97KZBIzo6Izj4akH-NnMNozY6tCM6OiMRkth3H7QfpnytT4zthGel&quot;&gt;emea.advance.episerver.net&lt;/a&gt;&amp;rdquo;. This URL does not have DNS mappings and is now non-functional.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;namespace EPiServer.Personalization.CMS
{
  [InitializableModule]
  [ModuleDependency(typeof (ServiceContainerInitialization))]
  public class InitializationModule : IConfigurableModule, IInitializableModule
  {
    public void ConfigureContainer(ServiceConfigurationContext context)
    {
      context.Services.AddSingleton&amp;lt;IHttpClientFactory, HmacHttpClientFactory&amp;gt;();
      string applicationKey = ConfigurationManager.AppSettings.Get(&quot;episerver:RecommendationServiceKey&quot;);
      if (string.IsNullOrEmpty(applicationKey))
        throw new ConfigurationErrorsException(string.Format(&quot;AppSetting &#39;{0}&#39; is required for recommendation service&quot;, (object) &quot;episerver:RecommendationServiceKey&quot;));
      string secret = ConfigurationManager.AppSettings.Get(&quot;episerver:RecommendationServiceSecret&quot;);
      if (string.IsNullOrEmpty(secret))
        throw new ConfigurationErrorsException(string.Format(&quot;AppSetting &#39;{0}&#39; is required for recommendation service&quot;, (object) &quot;episerver:RecommendationServiceSecret&quot;));
      string baseUri = ConfigurationManager.AppSettings.Get(&quot;episerver:RecommendationServiceUri&quot;);
      if (string.IsNullOrEmpty(baseUri))
        throw new ConfigurationErrorsException(string.Format(&quot;AppSetting &#39;{0}&#39; is required for recommendation service&quot;, (object) &quot;episerver:RecommendationServiceUri&quot;));
      // Snip
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size:&amp;#32;18pt;&quot;&gt;&lt;strong&gt;Episerver.Personalization.CMS.UI &amp;ndash; &lt;/strong&gt;safe to remove&lt;/span&gt;&lt;br /&gt;Same as above.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;namespace EPiServer.Personalization.CMS.UI
{
  [InitializableModule]
  [ModuleDependency(typeof (ServiceContainerInitialization))]
  public class InitializationModule : IConfigurableModule, IInitializableModule
  {
    private void SetupStrategyConfiguration(ServiceConfigurationContext context)
    {
      string applicationKey = ConfigurationManager.AppSettings.Get(&quot;episerver:RecommendationServiceKey&quot;);
      if (string.IsNullOrWhiteSpace(applicationKey))
        throw new ConfigurationErrorsException(&quot;AppSetting &#39;episerver:RecommendationServiceKey&#39; is required for recommendation service&quot;);
      string secret = ConfigurationManager.AppSettings.Get(&quot;episerver:RecommendationServiceSecret&quot;);
      if (string.IsNullOrWhiteSpace(secret))
        throw new ConfigurationErrorsException(&quot;AppSetting &#39;episerver:RecommendationServiceSecret&#39; is required for recommendation service&quot;);
      string baseUri = ConfigurationManager.AppSettings.Get(&quot;episerver:RecommendationServiceUri&quot;);
      if (string.IsNullOrWhiteSpace(baseUri) || !Uri.IsWellFormedUriString(baseUri, UriKind.Absolute))
        throw new ConfigurationErrorsException(&quot;AppSetting &#39;episerver:RecommendationServiceUri&#39; is required for recommendation service&quot;);
      string s = ConfigurationManager.AppSettings.Get(&quot;episerver:RecommendationServiceCacheDuration&quot;);
      // Snip
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size:&amp;#32;18pt;&quot;&gt;&lt;strong&gt;Episerver.Personalization.CMS.Core &amp;ndash; &lt;/strong&gt;safe to remove&lt;/span&gt;&lt;br /&gt;Same as above.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;namespace EPiServer.Personalization.CMS.Core
{
  public static class Constants
  {
    public const string ApplicationKey = &quot;episerver:RecommendationServiceKey&quot;;
    public const string Secret = &quot;episerver:RecommendationServiceSecret&quot;;
    public const string BaseUri = &quot;episerver:RecommendationServiceUri&quot;;
    public const string CacheDuration = &quot;episerver:RecommendationServiceCacheDuration&quot;;
    public const int DefaultStrategyCacheDuration = 5;
    public const string DefaultStrategyValue = &quot;251679C7-088F-45EE-8B39-238F73C7C5C6&quot;;
    public const int RecommendationItemBufferNumber = 5;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-size:&amp;#32;18pt;&quot;&gt;&lt;strong&gt;&lt;img src=&quot;data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; /&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size:&amp;#32;18pt;&quot;&gt;&lt;strong&gt;EPiServer.Tracking.PageView - &lt;/strong&gt;keep&lt;strong&gt;!&lt;/strong&gt;&lt;/span&gt;&lt;br /&gt;This is an interesting one. While as part of the &lt;strong&gt;Advance &lt;/strong&gt;installation steps they recommended installing this package, it doesn&amp;rsquo;t relate directly to the &lt;strong&gt;Advance &lt;/strong&gt;product. Instead, it relies on the &lt;strong&gt;Profile Store&lt;/strong&gt; product internally.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/G4FF0FV.jpg&quot; width=&quot;482&quot; alt=&quot;Profile&amp;#32;store&amp;#32;is&amp;#32;a&amp;#32;dependency&amp;#32;of&amp;#32;EpiServer.Tracking.PageView&quot; height=&quot;219&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Which in turn uses a different set of connection strings.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;namespace EPiServer.Profiles.Client
{
  [ServiceConfiguration(Lifecycle = ServiceInstanceScope.Scoped, ServiceType = typeof (IProfilesTrackingConfiguration))]
  public class ProfilesTrackingConfiguration : IProfilesTrackingConfiguration
  {
    private const string TrackingApiBaseUrlKey = &quot;episerver:profiles.TrackingApiBaseUrl&quot;;
    private const string SubscriptionIdKey = &quot;episerver:profiles.TrackingApiSubscriptionKey&quot;;
    private const string ProfileStoreTrackingEnabledKey = &quot;episerver:profiles.ProfileStoreTrackingEnabled&quot;;
    // Snip
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Profile Store&lt;/strong&gt; is crucial for good recommendations so this package should be kept along with the PageView tracking package.&lt;/p&gt;
&lt;p&gt;Removing the three personalisation packages above is required when removing the connection string values otherwise there is a runtime error preventing startup, as these packages need the connection strings in place to allow the solution to run.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size:&amp;#32;18pt;&quot;&gt;&lt;strong&gt;Cloud Synchronization&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Now for the URL we see in the startup error logs, &lt;a href=&quot;https://emea.epcc.episerver.net/&quot;&gt;https://emea.epcc.episerver.net/&lt;/a&gt; which is used by &lt;strong&gt;EPiServer.Cms.CloudSynchronization&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I can find no indication online what this package did, or why it was installed! It is like it never existed. The only trace of it online is as an entry in Knipe&amp;rsquo;s NuGet feed explorer&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;https://i.imgur.com/kBBm7oE.jpg&quot; width=&quot;886&quot; alt=&quot;&quot; height=&quot;57&quot; /&gt;&lt;br /&gt;&lt;a href=&quot;https://www.david-tec.com/optimizely-nuget-feed-explorer/?q=EPiServer.Cms.EPiServer.Cms.CloudSynchronization&quot;&gt;https://www.david-tec.com/optimizely-nuget-feed-explorer/?q=EPiServer.Cms.EPiServer.Cms.CloudSynchronization&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&amp;nbsp;however, it is not available in the official Optimizely NuGet feed so seems like it is no longer supported&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;https://i.imgur.com/aGMYJQg.jpg&quot; width=&quot;756&quot; alt=&quot;&quot; height=&quot;160&quot; /&gt;&lt;br /&gt;&lt;a href=&quot;https://nuget.optimizely.com/package?id=EPiServer.Cms.CloudSynchronization&quot;&gt;https://nuget.optimizely.com/package?id=EPiServer.Cms.CloudSynchronization&lt;/a&gt; &lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;However, I can see the package uses these connection strings we have&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;namespace EPiServer.Cms.CloudSynchronization
{
  public static class ConfigurationSettings
  {
    public const string ApplicationKey = &quot;episerver:CmsCloudSynchronizationKey&quot;;
    public const string Secret = &quot;episerver:CmsCloudSynchronizationSecret&quot;;
    public const string ServiceLocation = &quot;episerver:CmsCloudSynchronizationUri&quot;;
    // Snip
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;I also found by exploring the code for it that it added the &amp;ldquo;Cloud Provisioning&amp;rdquo; scheduled job&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;namespace EPiServer.Cms.CloudSynchronization.Internal
{
  [ScheduledPlugIn(Description = &quot;Connects to a cloud service and uploads entities such as content, categories, sites etc. Job should be run one time upon installation and should not be scheduled since changes are incrementally synchronized as soon as they occur.&quot;, DisplayName = &quot;Cloud Provisioning&quot;, GUID = &quot;3FAF1A3C-DF44-4FB3-9F47-B9C3B2A36020&quot;)]
  [ServiceConfiguration(IncludeServiceAccessor = false)]
  public class CloudProvisioningJob : ScheduledJobBase
  {
    public const string JobId = &quot;3FAF1A3C-DF44-4FB3-9F47-B9C3B2A36020&quot;;
    private static readonly ILogger Logger = LogManager.GetLogger();
    private readonly IEnumerable&amp;lt;IProvisioningJob&amp;gt; _syncJobs;
    private CancellationTokenSource _cancellationTokenSource;

    public CloudProvisioningJob(IEnumerable&amp;lt;IProvisioningJob&amp;gt; syncJob)
    {
      this._syncJobs = syncJob;
      this.IsStoppable = true;
    }
    // Snip
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Running it leads to an error since the URL also does not have DNS mappings anymore.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;https://i.imgur.com/bswVqbF.jpg&quot; width=&quot;850&quot; alt=&quot;&quot; height=&quot;258&quot; /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;It should be safe to remove this along with the three &lt;strong&gt;Advance &lt;/strong&gt;personalisation packages and the related connection strings for both products.&amp;nbsp;(&lt;em&gt;Please leave a comment with what it did if you know as I&#39;m burning to find out&lt;/em&gt;)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size:&amp;#32;18pt;&quot;&gt;&lt;strong&gt;Content Recommendations&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Advance &lt;/strong&gt;has been superseded by the &lt;strong&gt;Content Recommendations&lt;/strong&gt; product. Which uses the package &lt;strong&gt;EPiServer.Personalization.Content.UI&lt;/strong&gt; and is working well in our client&amp;rsquo;s solution. Connection strings below for interest sake.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;namespace EPiServer.Personalization.Content.UI.Configuration
{
  [Options]
  public class ContentPersonalizationOptions
  {
    public string Environment { get; set; }
    public string ClientId { get; set; }
    public string ClientName { get; set; }
    public string ApiToken { get; set; }
    public bool DisableDefaultTracking { get; set; }
    internal void Validate()
    {
      if (string.IsNullOrEmpty(this.Environment) || string.IsNullOrEmpty(this.ClientId) || string.IsNullOrEmpty(this.ApiToken) || string.IsNullOrEmpty(this.ClientName))
        throw new ConfigurationErrorsException(&quot;Episerver Content Personalization requires the following properties to be set: episerver:personalization.content.Environment, episerver:personalization.content.ClientId, episerver:personalization.content.ClientName and episerver:personalization.content.ApiToken are set in the appSettings section.&quot;);
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-size:&amp;#32;18pt;&quot;&gt;&lt;img src=&quot;data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size:&amp;#32;18pt;&quot;&gt;Conclusion&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;I hope this helps anyone having to do the same kind of maintenance/clean-up work as it was quite shocking to me how little information I could find about these packages online.&lt;/p&gt;</id><updated>2022-09-12T08:59:54.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Unpublish Content the Intuitive Way</title><link href="https://world.optimizely.com/blogs/jacob-pretorius/dates/2022/4/unpublish-content-the-intuitive-way/" /><id>&lt;p&gt;Being someone with a technical background who is also &quot;client-facing&quot; means, quite often, that &lt;em&gt;you &lt;/em&gt;are the person who has to explain a lot of the platform functionality to the &quot;business&quot; minded folk.&lt;/p&gt;
&lt;p&gt;That&#39;s all fun and games as quite often people with no past experience with Optimizely directly, or even CMS/Commerce solutions as a whole, bring with them lots of weird and wonderful questions. Often with even weirder answers leading to interesting observations.&lt;/p&gt;
&lt;p&gt;One that I&#39;ve been asked more times than I can count is &quot;How do I unpublish this content?&quot;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Oh, it&#39;s super simple. See you&#39;re in preview mode now so scroll up so that hidden grey menu appears. Then click the &quot;Tools&quot; dropdown. Now click &quot;Manage Expiration and Archiving&quot;. Then on that popup click the &quot;now&quot; next to &quot;Expire date&quot;. Ok now click &quot;Save&quot;. See super easy!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&quot;That&#39;s.. weird &#129300;&quot;&lt;/p&gt;
&lt;p&gt;Actually, come to think of it; yes, yes it is.&lt;/p&gt;
&lt;p&gt;So I made a plugin for CMS 12 that makes it a bit more intuitive and puts the &quot;Unpublish&quot; functionality right where you (okay maybe not &lt;em&gt;you&lt;/em&gt;, but the business person signing off our invoices) would expect it to be.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/dotcentric/dotcentric.Optimizely.Unpublish/main/images/unpublish.png&quot; width=&quot;468&quot; alt=&quot;&quot; height=&quot;270&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/JOTo6Dk.gif&quot; width=&quot;800&quot; alt=&quot;&quot; height=&quot;483&quot; /&gt;&lt;/p&gt;
&lt;p&gt;All you need to do is install it via the &lt;a href=&quot;https://nuget.optimizely.com/package/?id=dotcentric.Optimizely.Unpublish&quot;&gt;Optimizely NuGet feed&lt;/a&gt; and then add the below to your &lt;code&gt;Startup.cs&lt;/code&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public void ConfigureServices(IServiceCollection services)
{
    // The usual stuff omitted

    // using dotcentric.Optimizely.Unpublish;
    services.AddUnpublish();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As usual, the code for this plugin can also be found &lt;a href=&quot;https://github.com/dotcentric/dotcentric.Optimizely.Unpublish&quot;&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;*Update*&lt;/h2&gt;
&lt;p&gt;Seems like this tiny change resonated with more people than I thought it would and I&#39;ve had a few requests to see if we can get it added to the CMS core product. If you&#39;d like to help make that happen please vote for it here &lt;a href=&quot;https://feedback.optimizely.com/ideas/CMS-I-280&quot;&gt;https://feedback.optimizely.com/ideas/CMS-I-280&lt;/a&gt;&lt;/p&gt;</id><updated>2022-04-19T10:58:12.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Quick Episerver Custom Login Background</title><link href="https://world.optimizely.com/blogs/jacob-pretorius/dates/2019/8/quick-episerver-custom-login-background/" /><id>&lt;p&gt;I&#39;m sure we&#39;ve all had the same request many times, &quot;Could you change the CMS login background image for us?&quot;. It&#39;s not a difficult thing to do or something that takes much time.&lt;/p&gt;
&lt;p&gt;The other day I was creating three different new Episerver solutions and I as I was doing the same thing for each of them it struck me that I&#39;ve never actually checked to see if there is a NuGet package that does this.&lt;/p&gt;
&lt;p&gt;I had a look around and could not find anything that is also customizable if I didn&#39;t like the particular new background image the plugin creator chose for me.&lt;/p&gt;
&lt;p&gt;With that in mind, I&#39;ve created &lt;code&gt;Zone.Epi.BlankLoginBackground&lt;/code&gt; for a one-click customizable CMS login background change.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/zone/Zone.Epi.BlankLoginBackground/master/img/Login_Usage.PNG&quot; width=&quot;800&quot; alt=&quot;&quot; height=&quot;594&quot; /&gt;&lt;/p&gt;
&lt;p&gt;All you need to do is install it via the &lt;a href=&quot;https://nuget.episerver.com/package/?id=Zone.Epi.BlankLoginBackground&quot;&gt;Episerver NuGet feed&lt;/a&gt; and you are good to go. On the next CMS login you will see a more generic login background image.&lt;/p&gt;
&lt;p&gt;If you would like to use any other image all you need to do is replace &lt;code&gt;login-background.jpg&lt;/code&gt; in the newly created project directory &lt;code&gt;\Static\Images\CMS\&lt;/code&gt; and it will be used automatically.&lt;/p&gt;
&lt;p&gt;As usual, the code for this plugin can also be found &lt;a href=&quot;https://github.com/zone/Zone.Epi.BlankLoginBackground&quot;&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;</id><updated>2019-08-14T10:27:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>How to easily copy content between pages</title><link href="https://world.optimizely.com/blogs/jacob-pretorius/dates/2019/7/how-to-easily-copy-content-between-pages/" /><id>&lt;p&gt;Working with large Episerver builds often comes with unique challenges both from the content management side and from a business rule perspective.&lt;/p&gt;
&lt;p&gt;Recently one of our clients at Zone decided they wanted to update and restructure large parts of their site layout. Based on other external factors, this would create some extra editor overhead to keep everything running smoothly.&lt;/p&gt;
&lt;p&gt;Occasionally the editors will need to manually copy content between pages to align marketing strategies. I saw this as an interesting problem that we could make as painless as possible with a custom admin plugin.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/zone/Zone.Epi.ContentCopy/master/img/Content_Copy_Usage.png&quot; width=&quot;600&quot; alt=&quot;&quot; height=&quot;606&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The plugin allows editors to choose which language they want, and if they only want culture specific properties shown. To make it as intuitive as possible they have two content trees, one for the &#39;source&#39; and one for the &#39;destination&#39; page.&lt;/p&gt;
&lt;p&gt;Based on the source page selection and culture-specific setting, the available properties for that page type will be loaded automatically.&lt;/p&gt;
&lt;p&gt;The plugin isn&#39;t sensitive to which types of pages are selected; the only requirement is that the selected properties are the same type and have the same name on both source and destination page types.&lt;/p&gt;
&lt;p&gt;By default the plugin will create a new draft on the &#39;destination&#39; page if there aren&#39;t any issues with the copy process. If there is an outstanding draft on the &#39;destination&#39; page already, the plugin will let the editor know and not make any further changes to that draft.&lt;/p&gt;
&lt;p&gt;Editors can also choose to skip the draft process and overwrite any outstanding drafts by copying the content and publishing the changes immediately (assuming there are no errors during copying). To prevent misuse of this I&#39;ve decided to make the plugin an admin plugin and not put it in the CMS editor interface.&lt;/p&gt;
&lt;p&gt;All in all, I think this plugin is very useful for managing complex content strategies with ease.&lt;/p&gt;
&lt;p&gt;This plugin can be installed using NuGet from the &lt;a href=&quot;https://nuget.episerver.com/package/?id=Zone.Epi.ContentCopy&quot;&gt;Episerver feed&lt;/a&gt; by searching for &lt;code&gt;Zone.Epi.ContentCopy&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As usual the code for this plugin can also be found &lt;a href=&quot;https://github.com/zone/Zone.Epi.ContentCopy&quot;&gt;on GitHub&lt;/a&gt; if you want to change any of the standard features or build it out more. Thanks to Raffaele Millo for helping determine exactly how it should work.&lt;/p&gt;</id><updated>2019-07-01T17:16:46.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Introducing the A/B Test List Gadget</title><link href="https://world.optimizely.com/blogs/jacob-pretorius/dates/2019/5/introducing-the-ab-test-list-gadget/" /><id>&lt;p&gt;Episerver A/B testing has been around for a while, and editors can easily use it to get some real-world metrics that measure how different content versions compare. However with complex websites, it can be difficult to get the right message across as efficiently as possible on all devices &amp;ndash; A/B testing is a powerful tool to make sure our sites do just that.&lt;/p&gt;
&lt;p&gt;Unfortunately, it can be rather difficult for editors to keep track of exactly what is being tested, how far along the tests are and which changes seem to be doing the best. On larger builds with multiple sites and many different editorial teams it can quite easily become a &quot;who knows?&quot; type problem.&lt;/p&gt;
&lt;p&gt;Out of the box you could go to CMS edit mode and get some info on the &quot;Tasks&quot; tab, but that list feels very hidden and leaves a lot to be desired.&lt;/p&gt;
&lt;p&gt;With this in mind, we at Zone decided to create a CMS dashboard gadget which gives editors a list of running A/B tests, owners, results, views, participation percentage and a direct link to the detailed test overview page. This list can also be filtered based on the test site directly from the component interface.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/5iF3PSF.png&quot; width=&quot;714&quot; alt=&quot;&quot; height=&quot;436&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We think this is a much better way to get an overview of exactly what is going on with A/B testing across the whole build and allows editors to get the most of this powerful testing tool.&lt;/p&gt;
&lt;p&gt;The component can be installed using NuGet from the &lt;a href=&quot;https://nuget.episerver.com/package/?id=Zone.Epi.ABTestListGadget&quot;&gt;Episerver feed&lt;/a&gt; by searching for &lt;code&gt;Zone.Epi.ABTestListGadget&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The code for the component can also be found &lt;a href=&quot;https://github.com/zone/Zone.Epi.ABTestListGadget&quot;&gt;on GitHub&lt;/a&gt; if you want to customise and expand it more &amp;ndash; please let me know if you do as we are interested in seeing where this could go.&lt;/p&gt;</id><updated>2019-05-20T16:33:17.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Autofill Episerver Forms fields from Profile Store</title><link href="https://world.optimizely.com/blogs/jacob-pretorius/dates/2019/5/autofill-episerver-forms-fields-from-profile-store/" /><id>&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;As a user, it can be frustrating to repeatedly fill in the same information, especially if the system already has that data recorded. So why not use all the info from Profile Store to autofill forms and save some time?&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;The Episerver Profile Store can be a great option for tracking visitor behaviour and getting personalisation going quickly. Once you combine the Profile Store with Insight, Forms and David Knipe&#39;s &lt;/span&gt;&lt;span class=&quot;&amp;#32;md-link&quot;&gt;&lt;a href=&quot;https://github.com/davidknipe/InsightFormFieldMapper&quot;&gt;&lt;span class=&quot;md-plain&quot;&gt;Insight Form Field Mapper&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&quot;&gt;, you can easily extract a great deal of value from the service.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&amp;#32;md-expand&quot;&gt;At Zone, we&#39;ve recently been investigating how we can extend the Episerver platform to provide the best user experience possible. One of the stories we have is the ability to autofill Episerver Forms elements. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;With this idea in mind I started digging around to see how we could achieve exactly that. Fortunately, I found the &lt;/span&gt;&lt;span class=&quot;&amp;#32;md-link&quot;&gt;&lt;a href=&quot;/link/51e6228df528414eb2964adf40fba9ae.aspx&quot;&gt;&lt;span class=&quot;md-plain&quot;&gt;developer guides for the Forms Autofill API&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&quot;&gt; and even though it&amp;rsquo;s still in beta, it doesn&#39;t look like there has been much new development going on around it. I decided to give it a go.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/awDm73x.gif&quot; width=&quot;745&quot; alt=&quot;&quot; height=&quot;453&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;For this demo we show a user providing their details in a standard custom form during checkout. That info is then stored to the Profile Store and retrieved to autofill the Forms elements on the homepage. Sharing information between different forms is also supported as long as they are mapped to push and pull data from the Profile Store in the CMS.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;All we really need to do to get the Forms elements hooked up to our data source (Profile Store) is implement &lt;/span&gt;&lt;span&gt;&lt;code&gt;IExternalSystem&lt;/code&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&quot;&gt; and &lt;/span&gt;&lt;span&gt;&lt;code&gt;IAutofillProvider&lt;/code&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&quot;&gt;. &lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;&lt;span class=&quot;md-plain&quot;&gt;Implementing the interfaces&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;Both interfaces can be implemented in the same class as there is some overlap between them. I&#39;ve called my implementing class &lt;/span&gt;&lt;span&gt;&lt;code&gt;ProfileStoreAutofillFields&lt;/code&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&quot;&gt;. The &lt;/span&gt;&lt;span&gt;&lt;code&gt;IExternalSystem&lt;/code&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&quot;&gt; interface is used to register a custom data source and drives the CMS drop-down list.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public virtual string Id
{
    get { return &quot;ProfileStoreAutofillFields&quot;; }
}

public virtual IEnumerable&amp;lt;IDatasource&amp;gt; Datasources
{
    get
    {
        // Register the Profile Store as a data source
        var profileStoreDataSource = new Datasource()
        {
            Id = &quot;ProfileStoreDataSource&quot;,
            Name = &quot;Profile Store Data Source&quot;,
            OwnerSystem = this,
            Columns = new Dictionary&amp;lt;string, string&amp;gt; {
                // &quot;Name of mapped field&quot;, &quot;friendly name in CMS&quot;
                { &quot;profilestoreemail&quot;, &quot;Email&quot; },
                { &quot;profilestorename&quot;, &quot;Name&quot; },
                { &quot;profilestorecity&quot;, &quot;City&quot; },
                { &quot;profilestoremobile&quot;, &quot;Mobile&quot; },
                { &quot;profilestorephone&quot;, &quot;Phone&quot; }
            }
        };

        return new[] { profileStoreDataSource };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;Up next, the &lt;/span&gt;&lt;span&gt;&lt;code&gt;IAutofillProvider&lt;/code&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&quot;&gt; is where the magic happens and we use &lt;/span&gt;&lt;span&gt;&lt;code&gt;GetSuggestedValues&lt;/code&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&quot;&gt; to provide our &quot;suggested&quot; values from the Profile Store based on the device GUID as set in the Profile Store Cookie. The drop-down list in the CMS needs to match here for a value to be suggested.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Returns a list of suggested values by field mapping key. This will be called automatically by the GetAutofillValues() function in DataElementBlockBase for each field
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;returns&amp;gt;Collection of suggested values&amp;lt;/returns&amp;gt;
public virtual IEnumerable&amp;lt;string&amp;gt; GetSuggestedValues(IDatasource selectedDatasource, IEnumerable&amp;lt;RemoteFieldInfo&amp;gt; remoteFieldInfos,
ElementBlockBase content, IFormContainerBlock formContainerBlock, HttpContextBase context)
{
    if (selectedDatasource == null || remoteFieldInfos == null)
        return Enumerable.Empty&amp;lt;string&amp;gt;();

    // Make sure the Data sources are for this system
    if (!this.Datasources.Any(ds =&amp;gt; ds.Id == selectedDatasource.Id)
        || !remoteFieldInfos.Any(mi =&amp;gt; mi.DatasourceId == selectedDatasource.Id))
    {
        return Enumerable.Empty&amp;lt;string&amp;gt;();
    }

    // We also need to make sure that we have some tracking info to auto fill
    // _madid is the default Episerver Profile Store tracking cookie, see https://world.episerver.com/documentation/developer-guides/tracking/episerver-cookies/
    var userDeviceId = context.Request.Cookies[&quot;_madid&quot;]?.Value;

    // Because this gets called with EVERY FIELD it is suggested to cache the response elsewhere
    var userProfile = ProfileStoreApiService.GetProfileByDeviceId(userDeviceId);
    if (userProfile == null)
    {
        return Enumerable.Empty&amp;lt;string&amp;gt;();
    }
    
    // Unpack the info object
    var info = userProfile[&quot;Info&quot;];

    // Get the field details
    var activeRemoteFieldInfo = remoteFieldInfos.FirstOrDefault(mi =&amp;gt; mi.DatasourceId == selectedDatasource.Id);
    switch (activeRemoteFieldInfo.ColumnId)
    {
        // Suggest the data from the Profile Store user profile
        case &quot;profilestoreemail&quot;:
            return new List&amp;lt;string&amp;gt; {
                (string)(info[&quot;Email&quot;] ?? info[&quot;Email&quot;]?.ToString())
            };

        case &quot;profilestorename&quot;:
            return new List&amp;lt;string&amp;gt; {
                (string)(userProfile[&quot;Name&quot;] ?? userProfile[&quot;Name&quot;]?.ToString())
            };

        case &quot;profilestorecity&quot;:
            return new List&amp;lt;string&amp;gt;{
                (string)(info[&quot;City&quot;] ?? info[&quot;City&quot;]?.ToString())
            };

        case &quot;profilestorephone&quot;:
            return new List&amp;lt;string&amp;gt;{
                (string)(info[&quot;Phone&quot;] ?? info[&quot;Phone&quot;]?.ToString())
            };

        case &quot;profilestoremobile&quot;:
            return new List&amp;lt;string&amp;gt;{
                (string)(info[&quot;Mobile&quot;] ?? info[&quot;Mobile&quot;]?.ToString())
            };

        default:
            return Enumerable.Empty&amp;lt;string&amp;gt;();
    } 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;I have only added a handful of fields to demonstrate how it works. To add more, simply update the implementation of both interfaces with the fields available in the Profile Store API - &lt;/span&gt;&lt;span class=&quot;&amp;#32;md-link&quot;&gt;&lt;a href=&quot;/link/cba0a11a3ed44e37b9319475791d819f.aspx&quot;&gt;&lt;span class=&quot;md-plain&quot;&gt;all of which you can see here&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&quot;&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;&lt;span class=&quot;md-plain&quot;&gt;Getting the user profile from the Profile Store&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;We get all the user info by querying the Profile Store API. There are some ongoing improvements to provide a wrapper instead of manually having to implement the API calls, such as the &lt;/span&gt;&lt;span class=&quot;&amp;#32;md-link&quot;&gt;&lt;a href=&quot;https://www.david-tec.com/2019/05/episerver-profile-store-.net-client/&quot;&gt;&lt;span class=&quot;md-plain&quot;&gt;Episerver Profile Store .NET client&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&quot;&gt;; however, to keep things flexible I&#39;ve provided a (mostly unchanged) version of the one call we need here from the client by David Knipe.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using Newtonsoft.Json.Linq;
using RestSharp;

public static class ProfileStoreApiService
{
    // This should come from app settings really
    private static readonly string apiRootUrl = &quot;https://somesecret.profilestore.episerver.net&quot;; // Get from Insight / Profile store developer portal
    private static readonly string subscriptionKey = &quot;somesecret&quot;; // Get from Insight / Profile store developer portal

    public static JToken GetProfileByDeviceId(string deviceId)
    {
        // Set up the request
        var client = new RestClient(apiRootUrl);
        var request = new RestRequest(&quot;api/v1.0/Profiles&quot;, Method.GET);
        request.AddHeader(&quot;Ocp-Apim-Subscription-Key&quot;, subscriptionKey);

        // Filter the profiles based on the current device id
        request.AddParameter(&quot;$filter&quot;, &quot;DeviceIds eq &quot; + deviceId);

        // Execute the request to get the profile
        var getProfileResponse = client.Execute(request);
        var getProfileContent = getProfileResponse.Content;

        // Get the results as a JArray object
        if (!string.IsNullOrWhiteSpace(getProfileContent))
        {
            var profileResponseObject = JObject.Parse(getProfileContent);
            var profileArray = (JArray)profileResponseObject[&quot;items&quot;];

            // Expecting an array of profiles with one item in it
            return profileArray.First;
        }

        return null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;It should be noted that in production you would want to build out this &lt;/span&gt;&lt;span&gt;&lt;code&gt;ProfileStoreApiService&lt;/code&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&quot;&gt; to include some form of response caching. The &lt;/span&gt;&lt;span&gt;&lt;code&gt;GetSuggestedValues&lt;/code&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&quot;&gt; function we have calls &lt;/span&gt;&lt;span&gt;&lt;code&gt;GetProfileByDeviceId&lt;/code&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&quot;&gt; (aka the Profile Store API) &lt;/span&gt;&lt;span class=&quot;&quot;&gt;&lt;strong&gt;&lt;span class=&quot;md-plain&quot;&gt;for every Forms element&lt;/span&gt;&lt;/strong&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&quot;&gt; we have mapped. Personally, I would parse the response JToken object and cache a strongly typed user profile object, but there are various ways to go about it. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;With all that done it&#39;s time to spin up the site and go update some Forms.&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;&lt;span class=&quot;md-plain&quot;&gt;Forms configuration&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;First, you need to update the Form Container to map our new data source.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;md-image&amp;#32;md-img-loaded&quot;&gt;&lt;img src=&quot;https://i.imgur.com/xuWsGew.png[&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;Next, we need to edit the Form Elements to link the mappings.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;md-image&amp;#32;md-img-loaded&quot;&gt;&lt;img src=&quot;https://i.imgur.com/WXKcPU9.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;Make sure it&#39;s mapped to push responses to the correct Profile Store/Insight fields.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/a3vrCMk.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;And finally, map to the Profile Store data source to retrieve the stored fields.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/IJJVSXh.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;That&#39;s all there is to it! You should now have Forms elements that fill themselves out as the user information gets updated.&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;&lt;span class=&quot;md-plain&quot;&gt;Future work&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;This should be seen as more of a POC to get things going. You need to decide if and when you want to &quot;trust&quot; that a user profile is actually who we think they are (such as only for logged-in users) to make sure you don&#39;t expose personal user data.&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;&lt;span class=&quot;md-plain&quot;&gt;Useful resources&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span class=&quot;md-plain&quot;&gt;Check out the Forms &lt;/span&gt;&lt;span class=&quot;&amp;#32;md-link&quot;&gt;&lt;a href=&quot;https://github.com/episerver/EPiServer.Forms.Demo&quot;&gt;&lt;span class=&quot;md-plain&quot;&gt;demo project&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&quot;&gt; and the &lt;/span&gt;&lt;span class=&quot;&amp;#32;md-link&quot;&gt;&lt;a href=&quot;https://github.com/davidknipe/InsightFormFieldMapper/&quot;&gt;&lt;span class=&quot;md-plain&quot;&gt;Episerver Insight Form Field Mapper&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&quot;&gt; to see examples of what else is currently possible. The sample code for this can be found on &lt;/span&gt;&lt;span class=&quot;&amp;#32;md-link&quot;&gt;&lt;a href=&quot;https://github.com/jacobpretorius/Episerver.Autofill.Forms.Sample&quot;&gt;&lt;span class=&quot;md-plain&quot;&gt;Github&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class=&quot;md-plain&amp;#32;md-expand&quot;&gt;.&lt;/span&gt;&lt;/p&gt;</id><updated>2019-05-10T11:10:38.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>