
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title><![CDATA[ The Cloudflare Blog ]]></title>
        <description><![CDATA[ Get the latest news on how products at Cloudflare are built, technologies used, and join the teams helping to build a better Internet. ]]></description>
        <link>https://blog.cloudflare.com</link>
        <atom:link href="https://blog.cloudflare.com/" rel="self" type="application/rss+xml"/>
        <language>en-us</language>
        <image>
            <url>https://blog.cloudflare.com/favicon.png</url>
            <title>The Cloudflare Blog</title>
            <link>https://blog.cloudflare.com</link>
        </image>
        <lastBuildDate>Wed, 08 Apr 2026 04:44:15 GMT</lastBuildDate>
        <item>
            <title><![CDATA[We rebuilt Cloudflare's developer documentation - here's what we learned]]></title>
            <link>https://blog.cloudflare.com/new-dev-docs/</link>
            <pubDate>Fri, 27 May 2022 12:55:54 GMT</pubDate>
            <description><![CDATA[ In this blog post, we’ll cover the history of Cloudflare’s developer docs, why we made this recent transition, and why we continue to dogfood Cloudflare’s products as we develop applications internally ]]></description>
            <content:encoded><![CDATA[ <p></p><p>We recently updated <code>developers.cloudflare.com</code>, the Cloudflare Developers documentation website, to a new version of our custom documentation engine. This change consisted of a significant migration from Gatsby to Hugo and converged a collection of Workers Sites into a single Cloudflare Pages instance. Together, these updates brought developer experience, performance, and quality of life improvements for our engineers, technical writers, and product managers.</p><p>In this blog post, we’ll cover the history of Cloudflare’s developer docs, why we made this recent transition, and why we continue to <a href="https://en.wikipedia.org/wiki/Eating_your_own_dog_food">dogfood</a> Cloudflare’s products as we develop applications internally.</p>
    <div>
      <h3>What are Cloudflare’s Developer Docs?</h3>
      <a href="#what-are-cloudflares-developer-docs">
        
      </a>
    </div>
    <p>Cloudflare’s Developer Docs, which are <a href="https://github.com/cloudflare/cloudflare-docs/">open source on GitHub</a>, comprise documentation for all of Cloudflare’s products. The documentation is written by technical writers, product managers, and engineers at Cloudflare. Like many open source projects, contributions to the docs happen via Pull Requests (PRs). At time of writing, we have 1,600 documentation pages and have accepted almost 4,000 PRs, both from Cloudflare employees and external contributors in our community.</p><p>The underlying documentation engine we’ve used to build these docs has changed multiple times over the years. Documentation sites are often built with static site generators and, at Cloudflare, we’ve used tools like Hugo and Gatsby to <a href="https://blog.cloudflare.com/markdown-for-agents/">convert thousands of Markdown pages</a> into HTML, CSS, and JavaScript.</p><p>When we released the first version of our Docs Engine in mid-2020, we were excited about the facelift to our Developer Documentation site and the inclusion of dev-friendly features like dark mode and proper code syntax highlighting.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5UuLEAsx9i6dC7g3ai0LA9/b564d7bf89af2483abcf0f521f0a353f/image1-60.png" />
            
            </figure><p>Most importantly, we also used this engine to transition <i>all</i> of Cloudflare’s products with documentation onto a single engine. This allowed all Cloudflare product documentation to be developed, built, and deployed using the same core foundation. But over the next eighteen months and thousands of PRs, we realized that many of the architecture decisions we had made were not scaling.</p><p>While the user interface that we had made for navigating the documentation continued to receive great feedback from our users and product teams, decisions like using client-side rendering for docs had performance implications, especially on resource-constrained devices.</p><p>At the time, our decision to dogfood Workers Sites — which served as a precursor to Cloudflare Pages — meant that we could rapidly deploy our documentation across all of Cloudflare’s network in a matter of minutes. We implemented this by creating a separate Cloudflare Workers deployment for each product’s staging and production instances. Effectively, this meant that more than a hundred Workers were regularly updated, which caused significant headaches when trying to understand the causes and downstream effects of any failed deployments.</p><p>Finally, we struggled with our choice of underlying static site generator, Gatsby. We still think Gatsby is a great tool of choice for certain websites and applications, but we quickly found it to be the wrong match for our content-heavy documentation experience. Gatsby inherits many dependency chains to provide its featureset, but running the dependency-heavy toolchain locally on contributors’ machines proved to be an incredibly difficult and slow task for many of our documentation contributors.</p><p>When we did get to the point of deploying new docs changes, we began to be at the mercy of Gatsby’s long build times – in the worst case, almost an entire hour – just to compile Markdown and images into HTML. This negatively impacted our team’s ability to work quickly and efficiently as they improved our documentation. Ultimately, we were unable to find solutions to many of these issues, as they were core assumptions and characteristics of the underlying tools that we had chosen to build on — it was time for something new.</p><p>Built using Go, <a href="https://gohugo.io">Hugo</a> is incredibly fast at building large sites, has an active community, and is easily installable on a variety of operating systems. In our early discovery work, we found that Hugo would build our docs content in mere seconds. Since performance was a core motive for pursuing a rewrite, this was a significant factor in our decision.</p>
    <div>
      <h3>How we migrated</h3>
      <a href="#how-we-migrated">
        
      </a>
    </div>
    <p>When comparing frameworks, the most significant difference between Hugo and Gatsby – <i>from a user’s standpoint</i> – is the allowable contents of the Markdown files themselves. For example, Gatsby makes heavy use of <a href="https://mdxjs.com/">MDX</a>, allowing developers to author and import React components within their content pages. While this can be effective, MDX unfortunately is not CommonMark-compliant and, in turn, this means that its flavor of Markdown is <i>required</i> to be very flexible and permissive. This posed a problem when migrating to any other non-MDX-based solution, including Hugo, as these frameworks don’t grant the same flexibilities with Markdown syntax. Because of this, the largest technical challenge was converting the existing 1,600 markdown pages from MDX to a stricter, more standard Markdown variant that Hugo (or almost any framework) can interpret.</p><p>Not only did we have to convert 1,600 Markdown pages so that they’re rendered correctly by the new framework, we had to make these changes in a way that minimized the number of merge conflicts for when the migration itself was ready for deployment. There was a lot of work to be done as part of this migration – and work takes time! We could not stall or block the update cycles of the Developer Documentation repository, so we had to find a way to rename or modify <i>every single file</i> in the repository without gridlocking deployments for weeks.</p><p>The only way to solve this was through automation. We created <a href="https://github.com/cloudflare/cloudflare-docs/pull/3609/commits/2b16cd220f79c7cfd64d80f4a4592b73abcf0753">a migration script</a> that would apply all the necessary changes on the morning of the migration release day. Of course, this meant that we had to identify and apply the changes manually and then record that in JavaScript or Bash commands to make sweeping changes for the entire project.</p><p>For example, when migrating Markdown content, the migrator needs to take the file contents and parse them into an abstract syntax tree (AST) so that other functions can access, traverse, and modify a collection of standardized objects <i>representing</i> the content instead of resorting to a sequence string manipulations… which is scary and error-prone.</p><p>Since the project started with MDX, we needed a MDX-compatible parser which, in turn, produces its own AST with its own object standardizations. From there, one can “walk” – aka traverse – through the AST and add, remove, and/or edit objects and object properties. With the updated AST and a final traversal, a “stringifier” function can convert each object representation back to its string representation, producing updated file contents that differ from the original.</p><p>Below is an example snippet that utilizes <a href="https://www.npmjs.com/package/mdast-util-from-markdown"><code>mdast-util-from-markdown</code></a> and <a href="https://www.npmjs.com/package/mdast-util-to-markdown"><code>mdast-util-to-markdown</code></a> to create and stringify, respectively, the MDX AST and <a href="https://github.com/lukeed/astray"><code>astray</code></a> to traverse the AST with our custom modifications. For this example, we’re looking for <code>heading</code> and <code>anchor</code> nodes – both names are provided by the <code>mdast-*</code> utilities – so that we can read the primary header () text and ensure that all internal Developer Documentation links are consistent:</p>
            <pre><code>import * as fs from 'fs';
import * as astray from 'astray';
import { toMarkdown } from 'mdast-util-to-markdown';
import { fromMarkdown } from 'mdast-util-from-markdown';

/**
 * @param {string} file The "*.md" file path.
 */
export async function modify(file) {
  let content = await fs.promises.read(file, 'utf8');
  let AST = fromMarkdown(content);
  let title = '';

  astray.walk(AST, {
    /**
     * Read the page's &lt;h1&gt; to determine page's title.
     */
    heading(node) {
      // ignore if not &lt;h1&gt; header
      if (node.depth !== 1) return;

      astray.walk(node, {
        text(t: MDAST.Text) {
          // Grab the text value of the H1
          title += t.value;
        },
      });

      return astray.SKIP;
    },
    
    /**
     * Update all anchor links (&lt;a&gt;) for consistent internal linking.
     */
    link(node) {
      let value = node.url;
      
      // Ignore section header links (same page)
      if (value.startsWith('#')) return;

      if (/^(https?:)?\/\//.test(value)) {
        let tmp = new URL(value);
        // Rewrite our own "https://developers.cloudflare.com" links
        // so that they are absolute, path-based links instead.
        if (tmp.origin === 'https://developers.cloudflare.com') {
          value = tmp.pathname + tmp.search + tmp.hash;
        }
      }
      
      // ... other normalization logic ...
      
      // Update the link's `href` value
      node.url = value;
    }
  });
  
  // Now the AST has been modified in place.
  // AKA, the same `AST` variable is (or may be) different than before.
  
  // Convert the AST back to a final string.
  let updated = toMarkdown(AST);
  
  // Write the updated markdown file
  await fs.promises.writeFile(file, updated);
}</code></pre>
            <p><a href="https://gist.github.com/lukeed/d63a4561ce9859765d8f0e518b941642#file-cfblog-devdocs-0-js">https://gist.github.com/lukeed/d63a4561ce9859765d8f0e518b941642#file-cfblog-devdocs-0-js</a></p><p>The above is an abbreviated snippet of the modifications we needed to make during our migration. You may find all the AST traversals and manipulations we created as part of our migration <a href="https://github.com/cloudflare/cloudflare-docs/blob/2b16cd220f79c7cfd64d80f4a4592b73abcf0753/migrate/normalize.ts">on GitHub</a>.</p><p>We also took this opportunity to analyze the thousands and thousands of code snippets we have throughout the codebase. These serve an important role as they are crucial aides in reference documentation or are presented alongside tutorials as recipes or examples. So we added a <a href="https://github.com/cloudflare/cloudflare-docs/blob/ce64f4d28a6bff7de914d54623046384545e0048/bin/format.ts">code formatter script</a> that utilizes <a href="https://prettier.io/">Prettier</a> to apply a consistent code style across all code snippets. As a bonus side effect, Prettier would throw errors if any snippets had invalid syntax for their given language. Any of these were fixed manually and the `format` script has been added as part of our own CI process to ensure that all JavaScript, TypeScript, Rust, JSON, and/or C++ code we publish is syntactically valid!</p><p>Finally, we <a href="https://github.com/cloudflare/cloudflare-docs/blob/2b16cd220f79c7cfd64d80f4a4592b73abcf0753/migrate/Makefile#L7">created a Makefile</a> that coordinated the series of Node scripts and <code>git</code> commands we needed to make. This orchestrated the entire migration, boiling down all our work into a single <code>make run</code> command.</p><p>In effect, the majority of the <a href="https://github.com/cloudflare/cloudflare-docs/pull/3609">migration Pull Request</a> was the result of automated commits – over one million changes were applied across nearly 5,000 files in less than two minutes. With the help of product owners, we reviewed the newly generated documentation site and applied any fine-tuning adjustments where necessary.</p><p>Previously, with the Gatsby-based Workers Sites architecture, each Cloudflare product needed to be built and deployed as its own individual documentation site. These sites would then be managed and proxied by an umbrella Worker, listening on <code>developers.cloudflare.com</code>, which ensured that all requests were handled by the appropriate product-specific Worker Site. This worked well for our production needs, but made it complicated for contributors to replicate a similar setup during local development. With the move to Hugo, we were able to merge everything into a single project – in other words, 48 moving pieces became 1 single piece! This made it extremely easy to build and develop the entire Developer Docs locally, which is a big confidence booster when working.</p><p>A unified Hugo project also means that there’s only one build command and one deployable unit… This allowed us to move the Developer Docs to Cloudflare Pages! With Pages attached and configured for the GitHub repository, we immediately began making use of <a href="https://developers.cloudflare.com/pages/platform/preview-deployments/">preview deployments</a> as part of our PR review process and our <code>production</code> branch commits automatically queued new production deployments to the live site.</p>
    <div>
      <h3>Why we’re excited</h3>
      <a href="#why-were-excited">
        
      </a>
    </div>
    <p>After all the changes were in place, we ended up with a near-identical replica of the Developer Documentation site. However, upon closer inspection, a number of major improvements had been made:</p><ol><li><p>Our application now has fewer moving pieces for development <i>and</i> deployment, which makes it significantly easier to understand and onboard other contributors and team members.</p></li><li><p>Our development flow is a lot snappier and fully replicated the production behavior. This hugely increased our iteration speed and confidence.</p></li><li><p>Our application was now built as an HTML-first static site. Even though it was always a content site, we are now shipping 90% less JavaScript bytes, which means that our visitors’ browsers are doing less work to view the same content.</p></li></ol><p>The last point speaks to our web pages’ performance, which has real-world implications. These days, websites with faster page-load times are preferred over competitor sites with slower response times. This is true for human and bot users alike! In fact, this is so true that Google now <a href="https://developer.chrome.com/blog/search-ads-speed/">takes page speed into consideration</a> when ranking search results and offers tools like WebMasters and Lighthouse to help site owners track and improve their scores. Below, you can see the before-after comparison of our previous JS-first site to our HTML-first replacement:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1FaGWaH2XlN9riYuoufZAQ/5978df6bf6a6299b62e75f1606058e9d/image2-55.png" />
            
            </figure>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/60vizdQG71GVdLRHE12TyQ/f832833af15e2bd6200f07a9033a94dd/image3-40.png" />
            
            </figure><p>Here you can see that our <code>Performance</code> grade has significantly improved! It’s this figure, which is a weighted score of the Metrics like First Contentful Paint, that is tied to Page Speed. While this <i>does</i> have SEO impact, the <code>SEO</code> score in a Lighthouse report has to do with Google Crawler’s ability to parse and understand the page’s metadata. This remains unchanged because the content (and its metadata) were not changed as part of the migration.</p>
    <div>
      <h3>Conclusion</h3>
      <a href="#conclusion">
        
      </a>
    </div>
    <p>Developer documentation is incredibly important to the success of any product. At Cloudflare, we believe that technical docs are a product – one that we can continue to iterate on, improve, and make more useful for our customers.</p><p>One of the most effective ways to improve documentation is to make it easier for our writers to contribute to them. With our new Documentation Engine, we’re giving our product content team the ability to validate content faster with instantaneous local builds. Preview links via Cloudflare Pages allows stakeholders like product managers and engineering teams the ability to quickly review what the docs will <i>actually</i> look like in production.</p><p>As we invest more into our build and deployment pipeline, we expect to further develop our ability to validate both the content and technical implementation of docs as part of review – tools like automatic spell checking, link validation, and visual diffs are all things we’d like to explore in the future.</p><p>Importantly, our documentation continues to be 100% open source. If you read Cloudflare’s developer documentation, and have feedback, feel free to <a href="https://github.com/cloudflare/cloudflare-docs/">check out the project on GitHub</a> and submit suggestions!</p> ]]></content:encoded>
            <category><![CDATA[Technical Writing]]></category>
            <category><![CDATA[Developer Documentation]]></category>
            <guid isPermaLink="false">4o47pQSmXUYk7med7nhGAM</guid>
            <dc:creator>Kristian Freeman</dc:creator>
            <dc:creator>Luke Edwards</dc:creator>
        </item>
        <item>
            <title><![CDATA[An Open-Source CMS on the Cloudflare Stack: Introductory Post]]></title>
            <link>https://blog.cloudflare.com/production-saas-intro/</link>
            <pubDate>Fri, 19 Nov 2021 13:59:24 GMT</pubDate>
            <description><![CDATA[ We are developing an example feature-complete SaaS application that will be built entirely on the Cloudflare stack. ]]></description>
            <content:encoded><![CDATA[ <p></p><p>The <a href="https://developers.cloudflare.com/">Cloudflare documentation</a> is a great resource when learning concepts, reviewing API usage notes, or when you’re in need of a concise snippet to illustrate those APIs or concepts. But, as comprehensive as it is, new users to the Cloudflare Workers platform must bridge a large gap to go from the introductory example snippets to a real, production-ready application. While some of this may be specific to Workers (as with any platform), developers <i>everywhere</i> are figuring out how applications should be built in a serverless world. Building large serverless applications entails a learning curve journey, regardless of a developer’s experience level.</p><p>At Cloudflare, we’re intimately aware of this because we also had to go through the same transition. Our engineers are world-class and expertfully design and craft products that complement the distributed paradigm… but experts aren’t born overnight! We have been there, and we want to help jumpstart and aid others’ understanding.</p><p>With this in mind, we decided to do something unique to the industry: we are developing an example feature-complete SaaS application that will be built entirely on the Cloudflare stack. It is and will continue to be completely free, <a href="https://github.com/cloudflare/production-saas">open-sourced on GitHub</a>, and developed in public. This will be an incredible time as it can be used as a template for launching your own SaaS applications, too! In fact, you can clone the GitHub repository, update a few service tokens, and deploy the pre-built application to your own Cloudflare account within minutes!</p><p>Of course, examples and templates are great, but technologies and best practices never stop evolving. Cloudflare is no exception and is constantly iterating and introducing new products and product features. By extension, this requires the SaaS application to be a <i>living example</i> that evolves alongside the Workers platform — and this is part of our commitment.</p><p>|| <b>Don’t miss out!</b> Watch the <a href="https://github.com/cloudflare/production-saas">project on GitHub</a> to track its development progress and stay current with our latest changes and recommendations.</p>
    <div>
      <h2>Application Overview</h2>
      <a href="#application-overview">
        
      </a>
    </div>
    <p>Now, aside from actually building the application, we needed to find a balance between picking an example SaaS application that is both complex enough to serve as a convincing case study and simple — or self-contained — enough so that developers can quickly dive in, follow the source, and understand the components involved and the reasons why and/or how they are used.</p><p>Ultimately we decided to build an example content management system (CMS) which, as an application archetype, has also transformed over the years. Traditionally, a CMS operated on rented hardware, which was home to a long-lived server that handled incoming requests and queried an SQL-like database in order to retrieve the requested content, render it to an HTML page, and repeat the process over and over again. <a href="https://wordpress.org/">WordPress</a> was — and still is — a very common example of this approach.</p><p>Naturally, this application architecture was improved over the years: layers of caching were introduced, database schemas were redesigned to minimize the number of rows processed, and some frameworks began to skip the database entirely, preferring a build-step to render all content upfront as static HTML pages. (This is now known as “static-site generation” and is still a very popular approach.)</p><p>Today, in the serverless era, there are a number of “headless CMS” options available. These are made “headless” because they are not monolithic web servers that render HTML for each request. Instead, they offer API endpoints that will return the content as raw JSON data. This allows web developers to build completely custom templates for their website using whatever tools and/or frameworks they prefer. This approach grants an enormous amount of flexibility to the developer without losing the ability to organize their content, image assets, etc. WordPress, the seasoned veteran, is one of few that is able to offer a headless <i>and</i> a “headful” mode. Other headless choices, like <a href="https://www.sanity.io/">Sanity.io</a> and <a href="https://www.contentful.com/">Contentful</a>, are also quite popular.</p><p>The CMS application model is a great case study for our open-source example. One of the primary tenets of an edge-first design is that content should be made available as close as physically possible to the users asking for it. And the serverless architecture means that there’s no longer — or should not be — a single point of failure. These both directly benefit the CMS archetype and, when implemented, will yield clear performance gains.</p>
    <div>
      <h2>Current Progress</h2>
      <a href="#current-progress">
        
      </a>
    </div>
    <p>Before diving into the roadmap and explaining how this project will progress, it’s important to call out that this project <i>has already been</i> — and will continue to be — an ongoing effort! Today, you can find the <a href="https://github.com/cloudflare/production-saas">project on GitHub</a> and inspect the work that’s already been completed. As of now, the application already combines Workers, Workers KV, Cloudflare for SaaS, and Rate Limiting with Pages and Durable Objects additions to come in later milestones.</p><p><b>Phase 1</b> (see below) is nearing completion and, when finished, will mark the end of a very significant milestone. A new update to this blog post series will be issued, covering the highlights and technical overview of the project so far. This is important and immediately useful because <i>on its own,</i> <b>Phase 1</b> qualifies for a successful, full-stack application.</p>
    <div>
      <h2>Development Milestones</h2>
      <a href="#development-milestones">
        
      </a>
    </div>
    <p>The CMS application will progress in milestones. We have already released the project and will continue to build upon it in accordance with the roadmap (below). <a href="https://github.com/cloudflare/production-saas">GitHub stargazers</a> will be able to keep tabs on its progress, or at the very least, only subscribe to updates for the milestones they’re interested in following.</p><p>Each milestone is a sizable checkpoint on its own. As you’ll see below, the project roadmap is planned in a way such that each phase adds a considerable amount of new features and/or integrates with a new Cloudflare product. At every point, the application will remain functional and maintain a live, interactive demo to immediately demonstrate the latest functionality to passersby.</p><p>This format is chosen because it’s how real applications — and real products — are developed. Our goal is to ensure that the GitHub repository is <b>never</b> out of date. And, because of the development structure, one may <i>always</i> traverse the list of past milestones to review the changes that were necessary to migrate X or how product Y was integrated.</p><p><b>|| Note:</b> Visit the <a href="https://github.com/cloudflare/production-saas/milestones">GitHub milestones</a> to view more details and to subscribe for updates. There is so much more than can be listed here.</p>
    <div>
      <h3>Phase 1 – JSON API</h3>
      <a href="#phase-1-json-api">
        
      </a>
    </div>
    <p>The project must begin with some API endpoints to start managing and manipulating data. Using Workers and Workers KV, the work within this milestone will focus on building a robust JSON API that handles the core functionality that the rest of the application will need.</p><p>There is no HTML, CSS, or client-side JavaScript involved in this phase. Instead, work here should focus solely on the data: how it’s accessed, how models relate to one another, and how best to structure and store these relationships within Workers KV. For example, individuals should be able to create and manage workspaces that belong to their personal user accounts or to the organization(s) that they belong to.</p><p>Additionally, when creating content, the document should be validated against an existing schema that the document was assigned. This feature is critical in any CMS platform that plans to handle thousands of documents within a workspace. Without it, there’s very little confidence that your contents’ JSON representation is consistently structured.</p><p>A number of other features are planned — subscription management and invoicing through <a href="https://stripe.com/">Stripe</a>, sending transactional emails through <a href="https://sendgrid.com/">SendGrid</a>, and assigning vanity domains to workspaces through <a href="/cloudflare-for-saas-for-all-now-generally-available/">Cloudflare for SaaS</a>. Finally, of course, the standard house-keeping tasks will be set up. This includes continuous integration (CI) with API testing and automated, continuous deployments (CD).</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/50epXtqsrSebofS2ulnpsu/124d13127e4db7bd3fe160db00b563b0/image2-20.png" />
            
            </figure><p>By the end of this phase, the project will exist as a collection of API endpoints that, on its own, is a complete application. While it may only be accessible through <code>curl</code> commands — or any other preferred method for manually constructing HTTP requests, the completion of <b>Phase 1</b> already qualifies the project as a full-stack application and <i>could</i> serve a real-world SaaS product.</p><p>Additionally, the repository will include all the best practices for writing tests, automating deployments, and organizing the source in a way for long-term growth and maintenance. And, because we started with the JSON API, the project is immediately useful and capable of integrating with your existing build tools and frameworks. In other words, <a href="https://github.com/cloudflare/production-saas">stargazers</a> could deploy the project to their own accounts as their own personalized Headless CMS. Perhaps some of you will build Gatsby or Eleventy plugins — if you do, please let us know!</p>
    <div>
      <h3>Phase 2 — Dashboard UI</h3>
      <a href="#phase-2-dashboard-ui">
        
      </a>
    </div>
    <p>As fun as <code>curl</code> may be, most people prefer some form of visual interface they can interact with. This phase will be all about assembling a frontend to serve as the CMS application’s dashboard.</p><p>We will use <a href="https://svelte.dev/">Svelte</a>, a JavaScript framework for building user interfaces. While not everyone may enjoy or agree with this decision, the templating syntax resembles standard HTML markup, which will allow non-frontend developers to follow along and gauge what’s going on.</p><p>Svelte will be paired with <a href="https://tailwindcss.com/">Tailwind CSS</a> for the design system. Tailwind is a very popular, utility-first CSS framework that allows developers to compose styles through predefined, reusable HTML <code>”class”</code> names.</p><p>The result will be a single-page application (SPA) and will be hosted on Cloudflare Pages. This means that, out of the box, the dashboard will be able to take advantage of <a href="https://developers.cloudflare.com/pages/platform/preview-deployments#customizing-preview-deployments-access">Access-protected preview deployments</a>, <a href="https://developers.cloudflare.com/pages/platform/rollbacks">instant rollbacks</a>, automated deployments, comprehensive analytics, and more.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7tr1PMuybglLESZjeCxqvS/621ceb9d180069a2d499c94d870257d4/image1-53.png" />
            
            </figure><p>Finally, <a href="/cloudflare-pages-goes-full-stack/">now that Pages integrates with Workers directly</a>, the JSON API from <b>Phase 1</b> will migrate into a new repository structure. While this may seem like an innocent refactor, it actually unlocks an incredible set of features for the JSON API: <a href="https://developers.cloudflare.com/pages/platform/preview-deployments#customizing-preview-deployments-access">Access-protected preview deployments</a>, <a href="https://developers.cloudflare.com/pages/platform/rollbacks">instant rollbacks</a>, and automated deployments. Yes — these are the same Pages features mentioned above! This is amazing because it means that our API is continuously and atomically versioned, allowing its development to continue safely <i>alongside</i> the client dashboard that depends on it. In other words, there is zero risk of the API and the dashboard diverging, which would have allowed their expectations of one another to misalign. Instant rollbacks will also apply to the API since the <i>entire</i> application operates as a single Pages unit.</p><p>The previous phase will have built the core SaaS product functionality, but completing this phase will make it feel like a real-world product that can be launched and used on a daily basis. In fact, the end of <b>Phase 2</b> marks the application as a possible contender in the Headless CMS service space.</p>
    <div>
      <h3>Phase 3 — Article Edge-Rendering</h3>
      <a href="#phase-3-article-edge-rendering">
        
      </a>
    </div>
    <p>The previous phases are focused on assembling a minimum viable Headless CMS product, but <b>Phase 3</b> looks to grow outside this archetypal box. This will happen by allowing the application to render HTML web pages by injecting the JSON content into predefined templates.</p><p>Like WordPress, the CMS application should allow its users to choose whether they want to continue using the “headless” feature or enlist the complete template engine. Should they opt for HTML output, the Cloudflare project will only include a few premade templates that a user may select from — but, of course, this can be customized in your own projects.</p><p>Even though this phase reintroduces the monolithic CMS archetype, it’s a significantly safer, faster, and more resilient architecture than the single, all-in-one server of yesteryear. The CMS contents will still be distributed around the world, close to the customers’ readers — but now, the content can be <i>rendered</i> from anywhere in the world at extremely low latencies, too.</p>
    <div>
      <h3>Phase 4 — Feature Upgrades</h3>
      <a href="#phase-4-feature-upgrades">
        
      </a>
    </div>
    <p>At this stage, the application is — for the most part — complete. It’s functional, looks nice, performs well globally, and can be used in two very distinct ways.</p><p>In the context of a real SaaS product, development begins to shift towards adding new features that excite users or towards maintenance health of the project. For example, <b>Phase 4</b> will utilize <a href="/durable-objects-ga/">Durable Objects</a> to introduce a document editor that allows multiple users to edit the same document in a real-time, collaborative environment.</p><p>It’s also very likely that <a href="/introducing-r2-object-storage/">Cloudflare R2 Storage</a> will be introduced as a backend for media assets, allowing users to upload and manage images within a workspace. Or perhaps we decide to use <a href="/announcing-cloudflare-images/">Cloudflare Images</a> for this and R2 is used for importing and exporting content backups.</p><p>As you may expect, this milestone is full of unknowns, but that’s because the future holds unlimited possibilities. The project will continue to evolve and expand with Cloudflare and with time.</p><p>Of course, if you have ideas or suggestions for features, start a discussion with us on GitHub. We would love to hear from you!</p>
    <div>
      <h2>Next Steps</h2>
      <a href="#next-steps">
        
      </a>
    </div>
    <p>This was the introductory post of (what will be) an ongoing series. When each milestone is completed, we will publish a new post in this series with a retrospective and with technical walkthroughs of key aspects from that chapter’s work.</p><p>We’re at the beginning of an exciting journey, and we hope you’re as interested as we are!</p><p>You can show your support by starring or <a href="https://github.com/cloudflare/production-saas">following the project on GitHub</a>. All releases, discussions, and milestone tracking will reside within the repository. The next generation of SaaS applications will be built on Cloudflare — subscribe and dive in early!</p> ]]></content:encoded>
            <category><![CDATA[Full Stack Week]]></category>
            <category><![CDATA[Open Source]]></category>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Developers]]></category>
            <category><![CDATA[Developer Platform]]></category>
            <guid isPermaLink="false">24256QbRQUElgLBxTBRMfa</guid>
            <dc:creator>Luke Edwards</dc:creator>
        </item>
        <item>
            <title><![CDATA[Building the Cloudflare Summer Challenge Application]]></title>
            <link>https://blog.cloudflare.com/building-the-cloudflare-summer-challenge-application/</link>
            <pubDate>Fri, 13 Aug 2021 12:59:56 GMT</pubDate>
            <description><![CDATA[ If you haven’t already heard, we’re hosting the Cloudflare Summer Developer Challenge, a contest for the Cloudflare community at large. Anybody – yes, including you – can sign up for free and compete for a chance to win one of 300 available prizes.  ]]></description>
            <content:encoded><![CDATA[ 
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5iFPcUKI0TUH6QFrr6P95R/f94f5801d55670bb76140f257c8ffa98/Developer-Challenge.png" />
            
            </figure><p>If you haven’t already heard, we’re hosting the <a href="/developer-summer-challenge">Cloudflare Summer Developer Challenge</a>, a contest for the Cloudflare community at large. Anybody – yes, including you – can sign up for free and compete for a chance to win one of 300 available prizes. To submit you need to use  at least two products from the <a href="https://developers.cloudflare.com/">Cloudflare developer platform</a> — which makes this contest a great opportunity to give them a try if you haven’t already! The top 300 submissions will receive a box of our most popular swag, so you should give it a go!</p><p>Coincidentally, the Cloudflare Summer Developer Challenge’s landing page and signup workflow qualifies as a valid project submission (so meta), so if you’re looking for some inspiration, this walkthrough will shed some light on how it was built.</p>
    <div>
      <h2>Overview</h2>
      <a href="#overview">
        
      </a>
    </div>
    <p>At its core, the application is a series of static HTML pages, most of which have a form to submit, with a backend API to handle those submissions, and a storage layer to persist the data. In a Cloudflare lens, this would point towards using Pages, a Worker, and Workers KV. And while this <i>should be</i> the preferred stack for a project like this, truthfully, this “application” was originally intended to be a single HTML page with a single form, but its list of requirements grew over time, as things tend to do. So instead, this project began as–and remains–a Workers Site project, comprised of a single Worker and a single Workers KV namespace.</p><p>Workers Sites, the precursor to our <a href="https://pages.dev/">Pages</a> product, is a pattern where your Worker handles all the requests for your site’s assets. While doing this, your Worker Site can still include backend-y things, like offering a collection of JSON API endpoints. Basically, Workers Sites is a coined term for building monoliths within a Worker, but without the negative associations that the word “monolith” can bring. Given that a Workers Site is still a Worker, this means your monolith is deployed globally – tough to beat!</p><p>As with all Workers Sites, routing is the primary concern. For this, I used the <a href="https://github.com/lukeed/worktop"><code>worktop</code></a> web framework, which includes a router among many other utilities. <i>(Disclosure: I am also the author of worktop.)</i> This allowed me to quickly structure the layout of the entire application:</p>
            <pre><code>import { Router } from 'worktop';
import * as Cache from 'worktop/cache';

const API = new Router;

API.add('GET', '/', (req, res) =&gt; {
  res.send(200, 'TODO: send HTML for landing page');
});

API.add('GET', '/rules', (req, res) =&gt; {
  res.send(200, 'TODO: send HTML for terms &amp; conditions');
});

API.add('POST', '/signup', (req, res) =&gt; {
  res.send(201, 'TODO: parse &amp; save initial registration');
});

API.add('GET', '/submit', (req, res) =&gt; {
  res.send(200, 'TODO: render the unique submission form');
});

API.add('POST', '/submit', (req, res) =&gt; {
  res.send(201, 'TODO: parse, validate, save submission data');
});

// init; w/ Cache API
Cache.listen(API.run);</code></pre>
            <p>At this point, nothing useful is happening, but having an application skeleton laid out like this is my preferred format for a TODO list. It’s very satisfying to go through and fill out the handler bodies as development progresses. Additionally, the <code>Cache.listen</code> helper at the bottom of the file integrates the entire application with the Cache API, which I know I’ll want since most of the requests will be for the static HTML pages anyway.</p>
    <div>
      <h2>Building and Optimizing the Client pages</h2>
      <a href="#building-and-optimizing-the-client-pages">
        
      </a>
    </div>
    <p>Historically, deploying a Workers Site meant uploading all of your assets into a KV namespace. Then you would include something like <a href="https://github.com/cloudflare/kv-asset-handler"><code>@cloudflare/kv-asset-handler</code></a> into your Worker so that incoming requests would seamlessly route to keys within the namespace. However, I chose to go a different route.</p><p>Knowing that each of my static pages would – at most – have <i>one</i> CSS stylesheet and sometimes only <i>one</i> JavaScript file, I thought it would be pretty nifty to include a build system that would <i>inline</i> these assets into the built HTML page. This would mean that my static HTML pages would have <i>absolutely zero</i> network requests for additional resources, which is generally good news for performance.</p><blockquote><p><i>And while I would love to say that I did this purely for performance reasons, I must also admit that the lazy-me appreciated that I didn’t have to set up additional URL routing, deal with KV asset uploading, or deal with additional Cache lifespans. A win-win in this case!</i></p></blockquote><p>The trouble is: avoiding any external assets is not a common goal. In fact, this is very much a side quest I bestowed upon myself. And since no frameworks (that I know of, at least) can do this, I had to assemble my own miniature toolkit to accommodate my needs.</p><p>In the end, it proved to be a fun detour and didn’t take very long at all <a href="https://github.com/cloudflare/developer-summer-challenge/blob/master/bin/html.js">to put together</a>. I incorporated <a href="https://stylus-lang.com/">Stylus</a>, my preferred CSS preprocessor, and came up with a rather simple convention to inline CSS and/or JS files where needed. Instead of fancy AST parsers and transformers, I opted to simply read the HTML file contents as strings and search for HTML comments that matched the <code>&lt;!-- inject:(path) --&gt;</code> format:</p>
            <pre><code>&lt;!-- submit/index.html --&gt;
&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="utf-8"/&gt;
    &lt;title&gt;Submit Project | Cloudflare Developer Summer Challenge&lt;/title&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="icon" type="image/png" href="https://www.cloudflare.com/favicon-128.png"&gt;
    &lt;!-- inject:submit/index.styl --&gt;
    &lt;!-- inject:index.js --&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;!-- ... --&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
            <p>In this example, the <code>submit/index.html</code> file is injecting the <code>submit/index.styl</code>, which is its own stylesheet, and the <code>index.js</code> script, which does not live within the `submit` directory because it’s used by other pages. The toolkit looks at both asset paths, converts the Stylus to plain CSS, and then embeds the contents into the appropriate <code>&lt;script&gt;</code> or <code>&lt;style&gt;</code> HTML tags.</p><p>Finally, for production builds, the setup will pass the final HTML source through a minifier, which compresses the entire document, including any CSS or JavaScript that was injected. This step is optional, but it never hurts to send fewer bytes down the wire.</p><p>Once these pages were built, I was satisfied with the Network Activity panel when loading the main page:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/51lvxbE7cmM4R9fbrSheCC/b917f8dd5dc1d3f9f078ca5f26a6bf53/network.png" />
            
            </figure><p>You can see how the <code>localhost</code> document loads, only dispatching a single request for the <code>favicon-128.png</code> file, which is hosted externally. The three <code>data:image/*</code> requests are <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob URLs</a> and don’t actually transfer network packets. All in all, this means that the HTML document is fully self-contained.</p>
    <div>
      <h2>Including HTML into the Worker</h2>
      <a href="#including-html-into-the-worker">
        
      </a>
    </div>
    <p>Workers can send anything in a Response. Of course, this includes a HTML string. If I wanted to make things incredibly difficult for myself, I could have skipped the <code>/src</code> directory with its own build system, and instead written the HTML, CSS, and JS entirely within a JS string. This would certainly work, but it would be a nightmare to maintain and (for me, at least) be extremely error prone:</p>
            <pre><code>API.add('GET', '/', (req, res) =&gt; {
  // Note: Worktop APIs
  res.setHeader('Content-Type', 'text/html;charset=utf-8');
  res.send(200, `
    &lt;!doctype html&gt;
    &lt;html lang="en"&gt;
      &lt;head&gt;
        &lt;title&gt;Demo | Insanity&lt;/title&gt;
        &lt;style&gt;
          body {
            background: #fff;
            color: #424242;
          }
          /* more */
        &lt;/style&gt;
        &lt;script&gt;
          $('form').onsubmit = function (ev) {
            ev.preventDefault();
            // ...
          });
        &lt;/script&gt;
      &lt;/head&gt;
      &lt;body&gt;
        &lt;!-- my entire page content --&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  `);
});</code></pre>
            <p>Thankfully, I planned ahead and already have a build system that produces <i>better</i> HTML files anyway. So now I just needed a way to load those built outputs into my Worker code.</p><p>Now for the <i>second half</i> of this project’s toolkit; I find it perfectly acceptable to have a two-step build pipeline. Here, this means that the static site should be built first, followed by building the Worker. I was planning to use TypeScript to author my Worker anyway, which meant I was already going to need a build step – the only change here is that these build steps would now have to be sequential and ordered.</p><p>The Worker is built using <a href="https://esbuild.github.io/">esbuild</a>, which is an extremely quick JavaScript bundler <i>and</i> compiler that is capable of translating TypeScript, too. It also has its own plugin system, which allowed me the opportunity to add the “inline my HTML files” behavior I needed. The Worker’s <a href="https://github.com/cloudflare/developer-summer-challenge/blob/master/bin/worker.js">build script</a> actually isn’t too intimidating and allows the Worker to `import` HTML files directly. This allows the insanity from above can be safely replaced with this pattern:</p>
            <pre><code>import { Router } from 'worktop';
import * as Cache from 'worktop/cache';

// loaded via esbuild plugin
import LANDING from 'index.html';
import RULES from 'rules/index.html';

API.add('GET', '/', (req, res) =&gt; {
  res.setHeader('Content-Type', 'text/html;charset=utf-8');
  res.setHeader('Cache-Control', 'public,max-age=60');
  res.send(200, LANDING);
});

API.add('GET', '/rulees', (req, res) =&gt; {
  res.setHeader('Content-Type', 'text/html;charset=utf-8');
  res.setHeader('Cache-Control', 'public,max-age=1800');
  res.send(200, RULES);
});

// ...

// init; w/ Cache API
Cache.listen(API.run);</code></pre>
            <p>Of course, this is much cleaner and sensible in the long-run. Clarity makes it easier to identify and extract common patterns into utility functions. I took the opportunity to introduce a <code>render</code> function, the first of many reusable helpers this project would encounter:</p>
            <pre><code>// worker/utils.ts
import type { ServerResponse } from 'worktop/response';

export function render(res: ServerResponse, template: string) {
  res.setHeader('Content-Type', 'text/html;charset=UTF-8');
  res.send(200, template);
}

// worker/index.ts
import * as utils from './utils';

API.add('GET', '/', (req, res) =&gt; {
  res.setHeader('Cache-Control', 'public,max-age=60');
  return utils.render(res, LANDING);
});

API.add('GET', '/rulees', (req, res) =&gt; {
  res.setHeader('Cache-Control', 'public,max-age=1800');
  return utils.render(res, RULES);
});</code></pre>
            <p>Finally, most of the pages need to dynamically insert values into the HTML markup. For example, the submission form should render with the participant’s name and email address and the landing page is required to reflect the current value of remaining prizes. Much like any other monolithic application, the Worker Site is fully aware – and capable – of injecting these values where needed.</p><p>To do this, I standardized the <code>{{ variable }}</code> syntax in my project’s HTML. Each of these variables would be replaced <i>during the Worker request</i> with the appropriate value. Of course, it also requires that each endpoint actually provide the correct information to make the substitutions. With this in mind, I modified the `render` utility and updated the landing page’s route handler:</p>
            <pre><code>// worker/utils.ts
import type { KV } from 'worktop/kv';
import type { ServerResponse } from 'worktop/response';

// TypeScript placeholder
// Defines the `DATA` KV binding
declare const DATA: KV.Namespace;

export function render(res: ServerResponse, template: string, values: Record&lt;string, string&gt; = {}) {
  for (let key in values) {
    template = template.replace('{{ ' + key + ' }}', values[key]);
  }
  res.setHeader('Content-Type', 'text/html;charset=UTF-8');
  res.send(200, template);
}
  
export function toCount(): Promise&lt;string&gt; {
  return DATA.get('::remain', 'text').then(v =&gt; v || '300+');
}
  
// worker/index.ts
import * as utils from './utils';

API.add('GET', '/', async (req, res) =&gt; {
  // Get the "::remain" count from KV
  const count = await utils.toCount();
  
  // Short-term TTL for remaining swag updates
  res.setHeader('Cache-Control', 'public,max-age=60');
  
  // Render the HTML, passing in `count` variable
  return utils.render(res, LANDING, { count });
});</code></pre>
            <p>With these changes, the landing page will always check the KV namespace for the latest <code>::remain</code> value and inject it into the correct location. If you’re interested in checking out the project’s <a href="https://github.com/cloudflare/developer-summer-challenge/blob/master/worker/index.ts">source code</a>, you’ll find that this pattern is used in nearly every HTML response.</p>
    <div>
      <h2>Accepting Form Submissions</h2>
      <a href="#accepting-form-submissions">
        
      </a>
    </div>
    <p>As expected, this application made heavy use of form submissions. Luckily, the Fetch API offers a variety of <a href="https://fetch.spec.whatwg.org/#body">built-in body parsers</a> to make retrieval of the data trivial. Additionally, <code>worktop</code> offers a convenience function that will automatically invoke the correct parser based on the request’s <code>Content-Type</code> header. It’s aptly named <code>req.body()</code>.</p><p>It’s easy to parse and retrieve user data, but it still has to be validated. There are a number of ways to do this, all of which boil down to an input object, a group of rules, and a loop through those rules, collecting any error messages into an <code>errors</code> object. This is precisely what my <code>utils.validate</code> helper does, allowing me to clearly define and manage my rules inline.</p><p>Let’s see how this looks within the <code>POST /submit</code> handler, which accepts the initial registration form:</p>
            <pre><code>// worker/index.ts
import * as utils from './utils';

API.add('POST', '/signup', async (req, res) =&gt; {
  try {
    var input = await req.body&lt;Entry&gt;();
  } catch (err) {
    return toError(res, 400, 'Error parsing input');
  }

  let { email, firstname, lastname } = input || {};
  firstname = String(firstname||'').trim();
  lastname = String(lastname||'').trim();
  email = String(email||'').trim();

  let { errors, invalid } = utils.validate({
    email, firstname, lastname
  }, {
    email(val: string) {
      if (val.length &lt; 1) return 'Required';
      return utils.isEmail(val) || 'Invalid email address';
    },
    firstname(val: string) {
      return val.length &gt; 1 || 'Required';
    },
    lastname(val: string) {
      return val.length &gt; 1 || 'Required';
    }
  });

  if (invalid) {
    return res.send(422, errors);
  }
      
  // The `input` is valid!
  
  return res.send(200, 'TODO: finish me');
});</code></pre>
            <p>Only after the data is considered valid can data be stored into KV for future use. For the initial registration, a number of things need to happen:</p><ol><li><p>Ensure that the <code>input.email</code> hasn’t already been registered;</p></li><li><p>Persist the new registration using the `input` values, identifying each document with the <code>user:&lt;email&gt;</code> key;</p></li><li><p>Generate and save a unique code for the registration, which will be used later to ensure (a) that unregistered persons cannot submit projects and (b) that a registrant can only submit once;</p></li><li><p>Send the user an email, containing their unique submission link; and</p></li><li><p>Render a confirmation page, reminding the user to check their inbox for their link.</p></li></ol><p>It can seem like a lot, but after piecing together a few utility helpers and abstractions, it can actually feel quite approachable:</p>
            <pre><code>// worker/index.ts
import * as utils from './utils';
import * as Sparkpost from './emails';
import * as Signup from './signup';
import * as Code from './code';

function toError(res: ServerResponse, status: number, reason: string) {
  return res.send(status, { status, reason });
}

API.add('POST', '/signup', async (req, res) =&gt; {
  try {
    var input = await req.body&lt;Entry&gt;();
  } catch (err) {
    return toError(res, 400, 'Error parsing input');
  }
  
  let { email, firstname, lastname } = input || {};
  firstname = String(firstname||'').trim();
  lastname = String(lastname||'').trim();
  email = String(email||'').trim();
  
  // truncated: validation
  
  // Ensure email is not already in use
  let exists = await Signup.find(email);
  if (exists) return toError(res, 400, 'You have already signed up');

  // Generate new `Entry` record
  let entry = Signup.prepare({ email, firstname, lastname });

  // create "user:&lt;unique email&gt;" document
  let isOK = await Signup.save(entry);
  if (!isOK) return toError(res, 500, 'Error persisting entry');

  // create "code:&lt;unique value&gt;" document
  isOK = await Code.save(entry);
  if (!isOK) return toError(res, 500, 'Error saving unique code');

  // dispatch "We received your registration" email
  let sent = await Sparkpost.confirm(entry);
  if (!sent) return toError(res, 500, 'Error sending confirmation email');

  // render "Thank you, check your {{ email }} for next steps" page
  return utils.render(res, CONFIRM, { email: entry.email });
});</code></pre>
            <p>A full HTML response is returned, which means that the client-side form handler should be able to see this content and render it directly in the browser window. This can be seen in the following <code>index.js</code> snippet, which was referenced earlier in the <code>submit/index.html</code> as an injected asset:</p>
            <pre><code>// (client) index.js

$('form').onsubmit = async function (ev) {
  ev.preventDefault();

  var form = ev.target;
  var res = await fetch(form.action, {
    method: form.method || 'POST',
    body: new FormData(form),
  });

  // truncate: clear existing errors

  if (res.ok) {
    form.reset();
    // Receive HTML response
    let html = await res.text();
    // Force-write the new HTML into this window
    document.documentElement.innerHTML = html;
  } else {
    // truncate: render errors
  }
};</code></pre>
            <blockquote><p><b><i>BONUS:</i></b><i> Because a full HTML response is returned, and all the client-side </i><code><i>&lt;form&gt;</i></code><i> elements are semantically correct, the form submission workflow will work with JavaScript disabled! The client-side validation will remain functional, but be a degraded experience – the error dialog won’t popup and any error messages will not appear beneath their respective form inputs.</i></p></blockquote>
    <div>
      <h2>Sending Transactional Emails</h2>
      <a href="#sending-transactional-emails">
        
      </a>
    </div>
    <p>It should (hopefully) come as no surprise that programmatically sending an email is pretty straightforward these days. We chose to use SparkPost, but practically every service has the same API mechanics:</p><ul><li><p>Obtain an API Token</p></li><li><p>Send a POST request to an endpoint with:</p><ul><li><p>your API Token as an <code>Authorization</code> header</p></li><li><p>your recipient, sender identity, and text and/or HTML content as the POST body</p></li></ul></li><li><p>Wait for a 200-level response, or deal with any API errors</p></li></ul><p>Most email-as-a-service providers allow you to define templates, which allow you to replace variables with unique values per email – essentially the same thing our <code>utils.render</code> function is doing with our HTML contents. The benefit of this is that you only have to worry about writing your emails once; then you’re just POST’ing new values to the API endpoint.</p><p>SparkPost allows templates to be referenced by a custom name rather than a randomly generated identifier, which makes it easy to track and debug templates over time.</p>
            <pre><code>// worker/emails.ts
import type { Entry } from './signup';

// wrangler secret
// @see https://developers.sparkpost.com/api/#header-authentication
declare const SPARKPOST_KEY: string;

/**
 * Assemble the POST request for all SparkPost email triggers
 * @see https://developers.sparkpost.com/api/transmissions/#transmissions-post-send-a-template
 */
async function send(
  templateid: string,
  recipient: Entry,
  values?: Record&lt;string, string&gt;
): Promise&lt;boolean&gt; {
  const res = await fetch('https://api.sparkpost.com/api/v1/transmissions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': SPARKPOST_KEY,
    },
    body: JSON.stringify({
      content: {
        template_id: templateid,
      },
      recipients: [{
        address: {
          email: recipient.email,
          name: recipient.firstname + ' ' + recipient.lastname,
        },
        substitution_data: values || {},
      }]
    })
  });

  let data = await res.json() as {
    results: {
      id: string;
      total_rejected_recipients: number;
      total_accepted_recipients: number;
    }
  };

  return res.ok &amp;&amp; data.results.total_accepted_recipients === 1;
}
    
/**
 * Confirming user's signup
 * Sending unique submission form
 */
export function confirm(entry: Entry): Promise&lt;boolean&gt; {
  return send('devchallenge-confirm', entry, {
    firstname: entry.firstname,
    code: entry.code,
  });
}</code></pre>
            <p>The above snippet includes the <i>entire</i> <code>POST</code> request formatter – there’s nearly more type-hinting than there is code! Also shown is an example <code>confirm</code> method, which is responsible for sending the unique submission link to the newly-registered user. You’ll notice that <code>firstname</code> and <code>code</code> are the injected variables, required by the “devchallenge-confirm” template.</p>
    <div>
      <h2>Overall Performance</h2>
      <a href="#overall-performance">
        
      </a>
    </div>
    <p>I’d call this a success!</p><p>Even though this certainly wasn’t my first Worker project – and definitely won’t be my last – I’m consistently amazed how much the Workers runtime lets me get away with. I mean, if you could only take away two points from this article, they should be:</p><ol><li><p>I was able to build a moderately complex application, from scratch, while incorporating a Cache layer, a globally-replicated storage layer, and a super-performant JS runtime, all of which live under the same roof.</p></li><li><p>I (probably) spent more time fussing with a custom client-side build pipeline than I did piecing together the mission-critical API form handlers.</p></li></ol><p>The cherry on top: Should this contest go viral and lure in millions of visitors, I’d only be paying a couple of dollars at the end of the month. Obviously I have a bias here, but it’s pretty amazing really.</p><p>Finally, performance-wise, this may justify the time spent fiddling with the HTML build output:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3irhqabprRjLez9WXuHyi6/08a866800702d3aa1d6cab0e5706f04b/lighthouse.png" />
            
            </figure>
    <div>
      <h2>Lessons Learned</h2>
      <a href="#lessons-learned">
        
      </a>
    </div>
    <p>As I alluded to earlier, if I were to rebuild this application, or if I were to add more to it down the road, I would replace the Workers Site architecture with a Pages project and deploy a Worker in front of it for my API requirements and dynamic KV injections.</p><p>Since the static assets would no longer be embedded into the Worker’s source, I would need to replace the `utils.render` approach for another utility that fetches the URL from Pages (which becomes my “origin server”) and then uses <a href="https://developers.cloudflare.com/workers/runtime-apis/html-rewriter"><code>HTMLRewriter</code></a> to inject the variables. Also, not that I was anywhere near the 1MB size limit, the largest contributor to my Worker’s bytesize would disappear.</p><p>But, more significantly, this refactor would also reduce my total tooling since the <i>majority</i> of the project’s complexity lies in the custom build system for the frontend assets. In other words, the entire <code>/src</code> directory could have been built and deployed like a normal static website, which would allow me to make use of existing frameworks and/or toolkits instead of taking my self-imposed detour. There would have been no need to create a custom frontend toolkit <i>and</i> its bridge to get the static assets loaded into my Worker.</p><p>However, none of this is to say that Workers Sites was a bad approach for this application. It’s quite the contrary! This is all to highlight the flexibility of Worker Sites – and the Workers platform at large. Cloudflare Pages exists so that I, the developer, can lean into existing, well-traveled paths and let the experts worry about toolkits, build pipelines, and deployments… But that doesn’t prevent you, the resident expert, from customizing every aspect if that’s your desire.</p>
    <div>
      <h2>Resources</h2>
      <a href="#resources">
        
      </a>
    </div>
    <ul><li><p><a href="https://github.com/cloudflare/developer-summer-challenge">Source Code on GitHub</a></p></li><li><p><a href="https://developers.cloudflare.com/workers/platform/sites">Workers Site: Quickstarts</a></p></li><li><p><a href="https://developers.cloudflare.com/pages/get-started">Pages: Get Started Guide</a></p></li></ul> ]]></content:encoded>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Serverless]]></category>
            <category><![CDATA[Developers]]></category>
            <guid isPermaLink="false">4DqYEgzFEeSL3YkaaYUcF3</guid>
            <dc:creator>Luke Edwards</dc:creator>
        </item>
    </channel>
</rss>