<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Brantley Harris</title>
    <description>The latest articles on Forem by Brantley Harris (@deadwisdom).</description>
    <link>https://forem.com/deadwisdom</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F406434%2Faf81e8b7-cac5-4a25-a255-f9f6a9677ccf.jpg</url>
      <title>Forem: Brantley Harris</title>
      <link>https://forem.com/deadwisdom</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/deadwisdom"/>
    <language>en</language>
    <item>
      <title>11ty and Bun</title>
      <dc:creator>Brantley Harris</dc:creator>
      <pubDate>Fri, 06 Jun 2025 16:33:15 +0000</pubDate>
      <link>https://forem.com/deadwisdom/11ty-and-bun-3b1d</link>
      <guid>https://forem.com/deadwisdom/11ty-and-bun-3b1d</guid>
      <description>&lt;p&gt;In case you don't know &lt;a href="https://www.11ty.dev/" rel="noopener noreferrer"&gt;eleventy&lt;/a&gt; is pretty much the best way to make a static web site. And &lt;a href="http://bun.sh/" rel="noopener noreferrer"&gt;Bun&lt;/a&gt; is the fastest way I've found to compile typescript. Bun does a lot of things, actually, and one of those things is to replace all your webpacky rabbit holes with a super fast and focused bundler &lt;a href="https://bun.sh/docs/bundler" rel="noopener noreferrer"&gt;Bun.build&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But bun is a bit raw. So here's a system that works ace for me. &lt;/p&gt;

&lt;p&gt;This setup is made to focus on very healthy web development practices where we build, gasp, &lt;em&gt;HTML&lt;/em&gt; and very targeted web-components where we need extra functionality. It can build apps single-page, multi-page, or anything in-between without becoming a sticky ball of mud because we start simple and stay there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Libs
&lt;/h2&gt;

&lt;p&gt;First ask Claude to install &lt;a href="https://bun.sh/" rel="noopener noreferrer"&gt;Bun&lt;/a&gt;. Ask really nicely, and tip him well, you know he's dealing with your shit all day. If he goes rogue, spend a long time crafting the perfect prompt to tell him to just go to &lt;a href="https://bun.sh/" rel="noopener noreferrer"&gt;https://bun.sh/&lt;/a&gt; and follow the instructions.&lt;/p&gt;

&lt;p&gt;Next, install eleventy with bun. Let's go over Claude's head this time. You know, if you want that LLM money, you need to think like an LLM, and they would just spew things into your command line, so we can do that too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun add @11ty/eleventy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running 11ty
&lt;/h2&gt;

&lt;p&gt;Okay, so 11ty's command line specifies "node" at the top of it, so bun will dutifully execute in node, which isn't AT ALL THE POINT.&lt;/p&gt;

&lt;p&gt;So to execute bun correctly, we need to give bun the '-b' flag, which is '--bun' (I love it when cities have streets that are the name of the city) and that will intercept any calls to node and run them in bun:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun &lt;span class="nt"&gt;-b&lt;/span&gt; run eleventy &lt;span class="nt"&gt;--serve&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will serve at &lt;a href="https://0.0.0.0:8080" rel="noopener noreferrer"&gt;https://0.0.0.0:8080&lt;/a&gt;, but unless you are an overachiever, should give simply a 404 since we have no files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eleventy Basics
&lt;/h2&gt;

&lt;p&gt;Okay, so by default 11ty will do it's 11ty thing, you can create pages like &lt;code&gt;index.md&lt;/code&gt;, and save it. That will automatigically become your index.html. You can also give it a "layout", which will become the wrapper around your page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;index.md&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Yet&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Another&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;SaaS"&lt;/span&gt;
&lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;layout.html&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="gh"&gt;# {{ title }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;layout.html&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt; {{ content }} &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;".html" pages are actually, by default, liquid templates, which works but also you can change that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Layout
&lt;/h2&gt;

&lt;p&gt;Okay, so you do you, obviously, but here's a project layout that works for me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;_site&lt;/span&gt;            &lt;span class="c1"&gt;-- final built static content directory&lt;/span&gt;
&lt;span class="n"&gt;pages&lt;/span&gt;            &lt;span class="c1"&gt;-- where we put our content&lt;/span&gt;
  &lt;span class="n"&gt;_media&lt;/span&gt;         &lt;span class="c1"&gt;-- where we put our images/styles/etc&lt;/span&gt;
  &lt;span class="n"&gt;_layouts&lt;/span&gt;       &lt;span class="c1"&gt;-- where we put our layouts&lt;/span&gt;
     &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;  &lt;span class="c1"&gt;-- our default layout&lt;/span&gt;
  &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;       &lt;span class="c1"&gt;-- our index page&lt;/span&gt;
&lt;span class="n"&gt;components&lt;/span&gt;       &lt;span class="c1"&gt;-- where we will put our typescript / web-components&lt;/span&gt;
  &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;       &lt;span class="c1"&gt;-- the entrypoint for building&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eleventy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt;     &lt;span class="c1"&gt;-- our 11ty settings&lt;/span&gt;
&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt;         &lt;span class="c1"&gt;-- our typescript / web-components build script&lt;/span&gt;
&lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;     &lt;span class="c1"&gt;-- our package settings&lt;/span&gt;
&lt;span class="n"&gt;tsconfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;    &lt;span class="c1"&gt;-- our typescript settings&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Eleventy Config
&lt;/h2&gt;

&lt;p&gt;Now, 11ty has an evolving system for configs, but the latest and greatest is to put a &lt;code&gt;.eleventy.js&lt;/code&gt; in your project root. You can have multiple, and use &lt;code&gt;--config=&amp;lt;filename&amp;gt;&lt;/code&gt; to chose, but that's the default one.&lt;/p&gt;

&lt;p&gt;Within it, we export a default function that will be called when eleventy starts up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Build script for our components&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./build.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 11ty runs this function to configure itself&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eleventyConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Before 11ty runs, we will run our build script&lt;/span&gt;
    &lt;span class="nx"&gt;eleventyConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eleventy.before&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;directories&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;runMode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputMode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Build our components and put them in the js directory&lt;/span&gt;
        &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_site/js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error during build:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Our content is in the `pages` folder, this is our base&lt;/span&gt;
    &lt;span class="nx"&gt;eleventyConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInputDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pages&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Copy files from the `pages/_media` folder and put them in&lt;/span&gt;
    &lt;span class="c1"&gt;// `_site/media` in our final build&lt;/span&gt;
    &lt;span class="nx"&gt;eleventyConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addPassthroughCopy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_media&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;media&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Our layouts are in "pages/_layouts"&lt;/span&gt;
    &lt;span class="nx"&gt;eleventyConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setIncludesDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_layouts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Make a "layout.html" the default layout&lt;/span&gt;
    &lt;span class="nx"&gt;eleventyConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addGlobalData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;layout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;layout.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// If we make a change to our components, reload&lt;/span&gt;
    &lt;span class="nx"&gt;eleventyConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addWatchTarget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// This is using the default, but you can change this here&lt;/span&gt;
    &lt;span class="nx"&gt;eleventyConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOutputDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_site&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Short codes are little scripts you can embed, this is for &lt;/span&gt;
    &lt;span class="c1"&gt;// the copyright in your footer, which legally has to be&lt;/span&gt;
    &lt;span class="c1"&gt;// current, fyi&lt;/span&gt;
    &lt;span class="nx"&gt;eleventyConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addShortcode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;year&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; 
        &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Build
&lt;/h3&gt;

&lt;p&gt;Alright, so now eleventy will run our build script every time it runs, which includes when you make a file change and it automatically recompiles.&lt;/p&gt;

&lt;p&gt;Here is the build script I use, it makes a nice little table to explain what was done. We also set it to import css/html as files, split our files into shared chunks, minify the output, and add external source maps. We also set it to name our files basically like how they are in our &lt;code&gt;components&lt;/code&gt; directory with the same relative path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;filesize&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;filesize&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buildConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;watchDirs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;entrypoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./components/index.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;naming&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[dir]/[name].[ext]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;/// Building&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outdir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bun is not available. Please install Bun to use this feature. You might need to add -b after bun to make it intercept node.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;entrypoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;buildConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entrypoints&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;outdir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;outdir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;buildConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;naming&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;buildConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naming&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;splitting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;minify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sourcemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;external&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s1"&gt;----&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;❌ &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;art&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;art&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;art&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;📗 .&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;art&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loader&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;art&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;size&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;x1b[33m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;filesize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;art&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;x1b[0m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;✔️ &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;extraStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  More Example Stuff
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Entrypoint &lt;code&gt;components/index.ts&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./menu-toggle.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Anything else you want to build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Menu Toggle &lt;code&gt;components/menu-toggle.ts&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;LitElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;customElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lit/decorators.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;hamburgerIcon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="s2"&gt;`
&amp;lt;svg width="19" height="13" viewBox="0 0 19 13" fill="none" xmlns="http://www.w3.org/2000/svg"&amp;gt;
    &amp;lt;path d="M0.342712 0.743286H18.3427V2.74329H0.342712V0.743286ZM0.342712 5.74329H18.3427V7.74329H0.342712V5.74329ZM0.342712 10.7433H18.3427V12.7433H0.342712V10.7433Z" fill="black"/&amp;gt;
&amp;lt;/svg&amp;gt;
`&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;customElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;menu-toggle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MenuToggle&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;LitElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt;&lt;span class="s2"&gt;`
    :host {
        border: none;
        background: transparent;
        padding: var(--site-menu-toggle-padding, 0);
        cursor: pointer;
    }
  `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;connectedCallback&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connectedCallback&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;menu-open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hamburgerIcon&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layout &lt;code&gt;pages/_layouts/layout.html&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;http-equiv=&lt;/span&gt;&lt;span class="s"&gt;"X-UA-Compatible"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"IE=Edge,chrome=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;{{ title }}&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/js/index.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/media/base.css"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;nav&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;menu-toggle&amp;gt;&amp;lt;/menu-toggle&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"logo"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Yet Another SaaS&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/about"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;About Us&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;
        {{ content }}
    &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;footer&amp;gt;&lt;/span&gt;
        &lt;span class="ni"&gt;&amp;amp;copy;&lt;/span&gt; {{ year }} Extremely SaaS Solutions
    &lt;span class="nt"&gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Entrypoint &lt;code&gt;components/app.ts&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;You can create many entrypoints (see buildConfig in build.js),&lt;br&gt;
so keep your index/page bundle small. For complex apps make more entry points for the functionality you need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app-harness.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./data-sources.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./data-complex-things.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;thing&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;weirdLibrary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Project Orpheus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Credits
&lt;/h2&gt;

&lt;p&gt;My name is Brantley Harris and I do consulting work, you can find me on &lt;a href="https://www.linkedin.com/in/brantleyharris/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; for one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pexels.com/photo/close-up-photo-of-a-freshly-baked-bread-14800851/" rel="noopener noreferrer"&gt;Photo by Anete Lusina via Pexels&lt;/a&gt;&lt;/p&gt;

</description>
      <category>11ty</category>
      <category>bunjs</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>What if there was a CLI called "py" that got you working right away?</title>
      <dc:creator>Brantley Harris</dc:creator>
      <pubDate>Fri, 06 May 2022 15:21:24 +0000</pubDate>
      <link>https://forem.com/deadwisdom/what-if-there-was-a-cli-called-py-that-got-you-working-right-away-3cmn</link>
      <guid>https://forem.com/deadwisdom/what-if-there-was-a-cli-called-py-that-got-you-working-right-away-3cmn</guid>
      <description>&lt;p&gt;I'm tired of dealing with runtimes and virtual environments and remembering how to get everything set up for various projects, and I've been programming Python for 20 years now, I don't even understand how novice devs get along these days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaffolding
&lt;/h2&gt;

&lt;p&gt;In learning science, there is an idea of "scaffolding". This is a way of teaching that crosses the gap between providing rails or crutches, which make learners depend on the tools, often unable to move beyond them; and free form, which leaves learners out by themselves, often paralyzed by the possibilities. Scaffolding finds a good center where learners and pros-alike can get their bearings, pick comfortable directions, and feel supported along the way.&lt;/p&gt;

&lt;p&gt;Two great ways to perform scaffolding are one, to provide examples, which can be taken wholly at first, and then deviated from; and also through an emphasis on goal-setting, which lets learners see clearly their paths ahead of them.&lt;/p&gt;

&lt;p&gt;What does this have to do with a python CLI you might wonder? Well, that's exactly the issue: it doesn't usually. But what if we made a CLI that embraced scaffolding. And so here is my goal, to experiment with a different kind of CLI that works with goals and examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Py
&lt;/h2&gt;

&lt;p&gt;Of course, all of this is ephemeral at this point; I haven't created it yet, but I've begun experimenting and I'd love to know what people think.&lt;/p&gt;

&lt;p&gt;Py has two modes of running:&lt;/p&gt;

&lt;p&gt;Firstly, a user-friendly CLI to run python programs; setup/install projects, environments, and runtimes; run recipes of all sorts; and generally be a very simple hub of activity for any sort of python-related tasks. But most importunately does this while negotiating virtual environments and runtimes cleanly.&lt;/p&gt;

&lt;p&gt;This first mode would certainly step on some toes of some great projects like pyenv and poetry, but would rather use them rather than make them obsolete.&lt;/p&gt;

&lt;p&gt;The second mode of running would be if you just typed in "py". This is where the scaffolding comes into play. Instead of seeing a crufty help screen, you would see something of a screen like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oYlzropZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zh4e2ocfukxswapofoko.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oYlzropZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zh4e2ocfukxswapofoko.gif" alt="The PY command-line client opens and shows a menu of goals for the user to proceed with" width="600" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks to Rich for the wonderful interface API. With Rich, we can now create much more compelling terminal user experiences, which is something I wish to leverage here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Goals
&lt;/h3&gt;

&lt;p&gt;The goals you first get might be general, they might change based on your usage, or if you are in an existing project. And maybe your school or company could customize these based on common needs. Some initial goals might be:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initialize a new python project&lt;/strong&gt; - Create a new project by selecting a runtime environment, e.g. python 3.10.4, creating a virtual environment, and setting up a &lt;code&gt;pyproject.toml&lt;/code&gt; with dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check out an existing project and prepare to run it&lt;/strong&gt; - Clone or download an existing project and prepare your environments to run it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run the python shell&lt;/strong&gt; - Run the latest python shell, IDLE, or a notebook.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;View and modify your local python environments and installations&lt;/strong&gt; - See a list of runtime environments, and potentially upgrade or edit them.&lt;/p&gt;

&lt;p&gt;And finally &lt;strong&gt;Browse other goals and find more created by the community&lt;/strong&gt; - Which I imagine as a sort of mini-pypi user-submitted catalog of goals and recipes, browsable right here in the CLI. Imagine things like "Start a new data-science project" or even "Begin Sarah's machine learning tutorial #1".&lt;/p&gt;

&lt;h3&gt;
  
  
  Project
&lt;/h3&gt;

&lt;p&gt;So if this interests you, send me some feedback. I'd love to hear if you like the idea, obstacles, etc. I don't have a lot of free time to work on this, so also if you'd like to jump in, let's go.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>ux</category>
    </item>
    <item>
      <title>Web Components in Style</title>
      <dc:creator>Brantley Harris</dc:creator>
      <pubDate>Thu, 04 Mar 2021 22:40:37 +0000</pubDate>
      <link>https://forem.com/deadwisdom/web-components-in-style-426h</link>
      <guid>https://forem.com/deadwisdom/web-components-in-style-426h</guid>
      <description>&lt;p&gt;Web Components are really easy to style. You can share styles between them at nearly zero cost, and they can still be styled from outside. They are easy to set up and progressively enhance. There are a throng of frameworks that use Web Components, and if you got into one, you'd know all this naturally. But it's really hard to understand coming from React, Vue, Angular, etc. So let's talk about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Baseline
&lt;/h3&gt;

&lt;p&gt;Web Components are probably the most misunderstood technology right now on the web. It's no one's fault really, the tech is in an awkward place. The industry is caught in a React fever dream, and if not React, then surely Vue, and if not Vue then I guess Angular? All of these frameworks work in their own eco-system. And it's really hard to see what's going on outside of them.&lt;/p&gt;

&lt;p&gt;Web Components are about bringing everyone out of the closed-systems to share with each other. It's really easy to make your own walled-garden in tech, and in fact, once you've created it, you have incentives to keep it going. It's a whole other magnitude more difficult to do the work to create standards that everyone can agree on and that build interoperability.&lt;/p&gt;

&lt;p&gt;So let's go into what Web Components literally are. I'm obliged to go deep because of the misconceptions out there. There are three main browser features collectively called "Web Components":&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom Elements
&lt;/h3&gt;

&lt;p&gt;Being able to make your own elements, your own buttons, your own inputs whatever. The implementation is stupidly simple. You just create a new class extending HTMLElement, and then you register the name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class MyButton extends HTMLElement {
   connectedCallback() {
       super.connectedCallback();
       console.debug("I just appeared on the page!");
   }
}

customElements.register('my-button', MyButton);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et voila you can use &lt;code&gt;&amp;lt;my-button&amp;gt;&lt;/code&gt; anywhere in your HTML. Cool. BTW, you can use it in React and Vue also. It just works. Yes, we have to run JavaScript, but it's easy for this to be progressively enhanced because the browser treats it like a DIV until the javascript is loaded. You can still style it from the outside.&lt;/p&gt;

&lt;p&gt;This implementation of components is about the most straightforward, simple thing you could think of. It's wicked fast because it's using the browser's native object system already, and it's completely full-featured. Also, when you inspect it in the browser, you see it, not some weird &lt;code&gt;&amp;lt;div class="rg84823"&amp;gt;&lt;/code&gt;, not some huge pile of indecipherable elements. Just &lt;code&gt;&amp;lt;my-button&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTML Templates
&lt;/h3&gt;

&lt;p&gt;HTML Templates is a way to define fragments of HTML in the DOM without them having to actually run on the page. This lets you do declarative things without having to use JavaScript.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shadow DOM
&lt;/h3&gt;

&lt;p&gt;The Shadow DOM is a way of encapsulating and hiding chunks of HTML. Even though &lt;code&gt;&amp;lt;my-button&amp;gt;&lt;/code&gt; looks like a single element, it can have lots of sub-elements in its Shadow DOM. Just like the &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; element has its own inaccessible sub-elements like the dropdown arrow, your Custom Element can have its own little world.&lt;/p&gt;

&lt;p&gt;Yeah, it's kind of a mind-fudge. Because you're very used to the DOM tree as being, well a tree. Shadow DOM turns it into a hyper-tree. The nodes can have their own sub-trees off the main tree. That has a cost, mentally. And, honestly, any good developer will naturally want to turn off their brain here because the complexity doesn't seem to have an immediate payoff. But rather than giving up on the concept of Web Components entirely, let's first remember that we don't have to use Shadow DOM to use Custom Elements, and secondly, maybe we can keep trekking on over the conceptual hill, and see what's on the other side.&lt;/p&gt;

&lt;p&gt;One of the first things you'll find is that it lets you control how CSS affects the inner-workings of your component. This allows you to shield it from the mess above. Specifically, components become a boundary of the cascade, or in other words, the CSS for your component becomes scoped to it.&lt;/p&gt;

&lt;p&gt;There are multiple ways to pierce in and control the style within a component. The easiest, when you are writing the component is to have shared styles. This is a set of CSS styles that components all rely on, a style library. The browser caches it, but gives it to each component. In LitElement you'd do something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const buttonStyles = css`
  .icon { width: 32px; vertical-align: middle }
`;

class MyButton extends LitElement {
  static styles = [buttonStyles, myStyles];
}

class YourButton extends LitElement {
  static styles = [buttonStyles, yourStyles];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MyButton and YourButton will both use &lt;code&gt;buttonStyles&lt;/code&gt;, and whatever else they want, without being changed by the CSS coming from above them.&lt;/p&gt;

&lt;p&gt;Another is CSS variables, which if you aren't using, you really should. Components get the CSS variables from above them in the tree. It's common to define and publish with your component variables they support like &lt;code&gt;--my-button-color&lt;/code&gt;. It becomes simple to change it from above.&lt;/p&gt;

&lt;p&gt;We've also got CSS Parts, which lets the component author define pieces to alter.&lt;/p&gt;

&lt;p&gt;And finally, since it's a simple JavaScript class, you can extend whatever element you want and hand it new styles.&lt;/p&gt;

&lt;p&gt;With these systems, you'll find CSS to be much, much easier to manage. You are naturally pushed towards styling components separately, vs layout, utility, page-elements, etc. You'll find you aren't nesting nearly as much. And generally, you'll find it much easier to organize your styles. I don't even use sass/scss anymore, because I just don't need it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frameworks
&lt;/h3&gt;

&lt;p&gt;Let's talk about the really big misconception. People seem to think Web Components are a competitor to frameworks like React, Vue, Angular, etc. NO! Web Components build a path for these frameworks to provide components for you. Ideally, as an end developer, you would never have to worry about Custom Elements or ShadowDOM except to understand some of the details of styling and encapsulation.&lt;/p&gt;

&lt;p&gt;A lot of frameworks are doing this, but not the popular big three, who each have a vested interest in keeping themselves closed off. &lt;a href="https://webcomponents.dev/blog/all-the-ways-to-make-a-web-component/"&gt;Check out the many ways to make a webcomponent&lt;/a&gt;, all of these frameworks have different strategies and systems, but all of them can be used anywhere, not only in their own garden.&lt;/p&gt;

&lt;p&gt;Yes, you can use Web Components in a zero-dependency way, and that's a great option for things like date pickers or very specific one-offs. But it's not the main way you're supposed to consume or even create Web Components. It's just one of many options in an ever-expanding, open system.&lt;/p&gt;

&lt;p&gt;Please ask me anything, I'll do my best to help: &lt;a href="http://twitter.com/deadwisdom"&gt;@deadwisdom&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>css</category>
    </item>
    <item>
      <title>Firestore: What You Should Know</title>
      <dc:creator>Brantley Harris</dc:creator>
      <pubDate>Tue, 19 Jan 2021 22:54:33 +0000</pubDate>
      <link>https://forem.com/deadwisdom/firestore-what-you-should-know-50mf</link>
      <guid>https://forem.com/deadwisdom/firestore-what-you-should-know-50mf</guid>
      <description>&lt;p&gt;Here are my notes on Firestore, and what I wish I knew. This is a &lt;strong&gt;rundown&lt;/strong&gt;, so I've put bits in bold so you can scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Basics
&lt;/h2&gt;

&lt;p&gt;Firebase is one of the most popular "backendless" services. The whole idea is: let's provide everything we need for our applications as direct services that make setting up a backend server redundant. Firestore continues this: let's just connect right to our database from the client. The client here is our website, or a mobile app, a desktop client, whatever.&lt;/p&gt;

&lt;p&gt;Firestore is a &lt;strong&gt;document store&lt;/strong&gt; database, which means everything is an amorphous document that you set values on. It is &lt;strong&gt;schemaless&lt;/strong&gt;, meaning each document can have whatever fields you want on it– you aren't setting up columns or properties ahead of time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every document is in a collection&lt;/strong&gt;, kind of like a table, but you can think of it as a directory with the documents as files. In fact, they are named exactly like directory paths &lt;code&gt;/collection-name/document-id&lt;/code&gt;, and can keep going: you can have sub-collections like &lt;code&gt;/collection-name/document-id/subcollection-name/subdocument-id&lt;/code&gt;. In truth, there is absolutely no relation between the sub-document and the document, deleting the document won't even delete the sub-document. It merely serves as a way to organize.&lt;/p&gt;

&lt;p&gt;Again, you &lt;strong&gt;connect directly&lt;/strong&gt; to Firestore from the client / front end. You can update the value of a document, you can read a document, and you can query for multiple documents in a collection. Also, &lt;strong&gt;you can subscribe&lt;/strong&gt; to a document, collection, or collection query, and get real-time updates in the form of "snapshots".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queries are really simple&lt;/strong&gt;. We're not doing SQL queries here, just things like "is this field equal to this value" and "is this field bigger than this value". You can combine multiple query statements, but they require creating special indexes. Fortunately, when you attempt to use an index that doesn't exist, the library is smart enough to throw an error and give you an exact link to click on to create the query index. But probably 80% of the time, when I make an index, it turns out I don't actually need it. If you're making a lot of indexes in Firestore, it's a &lt;em&gt;red flag&lt;/em&gt; that you're laying out your data wrong.&lt;/p&gt;

&lt;p&gt;You might be thinking: if it connects directly and we read and write directly, how do we keep those reads and writes &lt;em&gt;secure&lt;/em&gt;? That's done with a magical system called Firebase "&lt;strong&gt;Rules&lt;/strong&gt;". You set up rules using a special domain-specific language that sort of resembles javascript. The rules dictate what requests can write and read to documents. In the rule statement you can reference the incoming request, the user, and the requested document.&lt;/p&gt;

&lt;p&gt;Generally, there is no processing on writes and reads, so whatever the client writes goes directly into the database. Whatever is in the database, the client reads. However, there is a way to &lt;strong&gt;trigger cloud functions&lt;/strong&gt; when a document is created or written. This lets you simulate processing but, importantly it is after the fact, so there's a quasi-state between writing and processing where things can be out of whack. This task gets scheduled somewhere by Firebase, in my tests it occurred a few seconds after the data was saved.&lt;/p&gt;

&lt;p&gt;And that's &lt;em&gt;basically it&lt;/em&gt;. Firestore is purposefully super simple. If you need more, you're using the wrong tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it shines
&lt;/h2&gt;

&lt;p&gt;Don't overthink Firestore. It exists in a larger offering called "Google Cloud Platform". It has a lot of friends to pick up its slack, and it's not trying too hard, so you shouldn't either.&lt;/p&gt;

&lt;p&gt;It's one of the fastest ways I know of going from zero to a full "backend" solution. It's amazing for prototyping, amazing for simple applications, and great as a front for a larger system, as a sort of caching layer. It's also pretty cheap, given the alternatives. Just make sure you aren't doing a lot of writes.&lt;/p&gt;

&lt;p&gt;Connecting directly from the client is as easy as you'd think it should be. &lt;strong&gt;It feels like cheating&lt;/strong&gt;, and it's worth the price of admission. Although the documentation can be pretty obtuse, once you figure out that you're mostly basing everything off of a document reference: &lt;code&gt;firestore.collection('collection-name').doc('doc-id')&lt;/code&gt;, you begin to fly. And once you figure out snapshots, you're golden.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's fast&lt;/strong&gt;. Special caching and indexing make it feel like real-time, especially when you subscribe to changes, they seem to come instantly.&lt;/p&gt;

&lt;p&gt;It has a live interface to view and edit the data. It's amazing for prototyping because you can see real-time changes. If your client is subscribing to the data, you can make the change in the interface and see it live. &lt;strong&gt;Absolute filth for live demos&lt;/strong&gt; to upper management. "Oh, you want to change this name? Already done."&lt;/p&gt;

&lt;p&gt;Another "feature" is that it forces you to &lt;strong&gt;simplify your data organization&lt;/strong&gt;. This can be bad and good, but I've found that it gives you all you need for most simple applications. So if you're struggling with data layout, it's a &lt;em&gt;red flag&lt;/em&gt; that you either need a separate solution for that feature, or you're thinking about it all wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it's dull
&lt;/h2&gt;

&lt;p&gt;The simplicity will quickly box you in, though. This isn't a relational database. It's a specialized non-relational database– you will have to learn a &lt;strong&gt;different way of thinking&lt;/strong&gt; that matches it, specifically.&lt;/p&gt;

&lt;p&gt;With no real way of doing relations, you're going to make &lt;strong&gt;a lot of reads and round-trips&lt;/strong&gt;. This is by design, and probably fine, but we're not able to do anything fancy with joins or graph queries. This also means being very, very aware of how you set up your data. In many places, you will be &lt;strong&gt;duplicating data&lt;/strong&gt; and will have to manage to update this duplicated data in a system that, frankly, doesn't want to help you.&lt;/p&gt;

&lt;p&gt;But easily &lt;strong&gt;the biggest problem with Firestore is the rules system&lt;/strong&gt;. This system is nasty. First of all, let's have a new language for you to learn that is not well documented and feels just close enough to javascript to make you feel like it should be, but no. Sorry, not sorry. It would be bad enough to leave it there, but there's almost no feedback. Your requests either work or don't. Debugging is practically impossible. They allow you to "test" the queries from their front-end, but it's buggy, and I don't recommend it. The biggest issue here is that you are simply left wondering about your security. Not a good place to be.&lt;/p&gt;

&lt;p&gt;Another big issue is that there is &lt;strong&gt;no schema&lt;/strong&gt;, at all. You cannot be sure of data consistency. That means you have to set up defenses in your client code when you read &lt;em&gt;and/or&lt;/em&gt; setup post-processing cloud function triggers to ensure sanity. A lot of &lt;code&gt;object.title || ''&lt;/code&gt; or use a schema validation library. It is really pushing the schema work to the client. This is a pretty awful indictment when you compare it to services offering things like GraphQL that handle this inherently.&lt;/p&gt;

&lt;p&gt;And given that a nefarious user could, with some console scripting, update &lt;em&gt;whatever they have access to with whatever data they wanted&lt;/em&gt;, you can't rely on any of it. Again, &lt;strong&gt;security becomes a big question&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Finally: &lt;strong&gt;pricing is weird&lt;/strong&gt;. On the surface, Firestore has a straightforward pricing model: you pay a certain amount per read and per write. But things &lt;a href="https://medium.com/swlh/use-firebase-and-go-directly-to-jail-48e5db9912dd"&gt;can get problematic&lt;/a&gt;. It's hard to figure out what special functions like subscribing to collection queries do, and it's hard to know what cloud functions might trigger either a read or a write. At best, you'll be able to estimate based on projected users and activity. At worst, like most cloud services, you'll have to do some linear regressions after you've run it for a while to see how much your app will cost when it scales. But, hey, this is why we have CFOs, right?&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategies
&lt;/h2&gt;

&lt;p&gt;Here are some strategies for success with Firestore:&lt;/p&gt;

&lt;h3&gt;
  
  
  Employ pricing limits.
&lt;/h3&gt;

&lt;p&gt;First thing, first thing: Set up pricing limits. Make something reasonable. Da pricing calculator don't care that your script went into an infinite loop, your credit card is still on the line. Big daddy Google ain't messing around. Though to be fair, I've heard of people messing this up and Google has been lenient. Still, put the limit in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep public and private data separated.
&lt;/h3&gt;

&lt;p&gt;As a general rule, separating public and private data is great. In Firebase it's even more greaterific. This lets you write public rules that are super simple, and focused, well crafted, private rules. It's also a good line on which to duplicate data, e.g. have private user profiles with all their information, and public profiles, in a separate collection, with only a subset of fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consider moving writes to a separate service.
&lt;/h3&gt;

&lt;p&gt;I abandoned making writes at all directly from the client. Instead, I set up an App Engine REST API to do this. You could easily do serverless functions, an API Gateway with Cloud Run, whatever. Yes, this throws out the idea of fully &lt;em&gt;backendless&lt;/em&gt; as you're literally making a backend. But I find it much, much easier to ensure security and data schema in custom functions. Best to do this after the prototyping phase. Get the functionality down, then move on to data consistency and security — batten down the hatches, so to speak.&lt;/p&gt;

&lt;p&gt;An alternative to this is to have special collections set up for writes, and then have serverless functions trigger and process them. This is a fine system, but you're essentially just doing the same thing as above.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consider using Firestore to augment your existing services.
&lt;/h3&gt;

&lt;p&gt;If you start thinking of Firestore as essentially a specialized, fast, subscribable read-only data source, it's easy to imagine gradually putting it in between your existing services and clients. Gradually put data into it and move clients to read from it. Slowly your backend becomes write-only, decreasing complexity and cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test your security rules with a deployment script.
&lt;/h3&gt;

&lt;p&gt;The rules are such little jerks, you need to be sure they are working. Don't leave it to the interface, it's just waiting to fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  On server-side, use transactions.
&lt;/h3&gt;

&lt;p&gt;The client bends backward to make every operation idempotent, so you can't get the database in a weird state. But on the server, you need to use transactions to make sure you don't create a weird state of the database. Also, it can save on pricing as a single transaction sort of bundles costs up. But, uh, don't quote me on that last part, it's not well defined.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watch the videos.
&lt;/h3&gt;

&lt;p&gt;Google has done an impressive job &lt;a href="https://www.youtube.com/c/firebase/videos"&gt;creating videos&lt;/a&gt; to help you understand how to organize your data. I've dealt with a lot of document store and non-relational databases and they still very much helped me think in Firestore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Welp, that's all I got. Overall, I really like Firestore. It lets me get building fast. But it's a special thing, and it's best to understand it before you jump in.&lt;/p&gt;

&lt;p&gt;It's well-executed; most of the issues are from trade-offs that I can't fault them on. Although the rules system really needs a lot of work.&lt;/p&gt;

&lt;p&gt;Going forward, the horizon looks good for some other backendless technologies. AWS is coming around the corner with Amplify, and any GraphQL service is going to have a huge benefit here since we're already giving up REST. But it remains to be seen if the simplicity, cost, and speed of Firebase will shine through.&lt;/p&gt;

&lt;p&gt;Cover image: Photo by burak kostak from Pexels&lt;/p&gt;

&lt;p&gt;Meet me on twitter: &lt;a href="https://twitter.com/deadwisdom"&gt;@deadwisdom&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm available for consulting services on architecture and web development.&lt;/p&gt;

</description>
      <category>backendless</category>
      <category>firebase</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Embracing JAMStack with Python: Generating a Static Website with Flask and Deploying to Netlify</title>
      <dc:creator>Brantley Harris</dc:creator>
      <pubDate>Mon, 26 Oct 2020 22:14:43 +0000</pubDate>
      <link>https://forem.com/deadwisdom/embracing-jamstack-with-python-generating-a-static-website-with-flask-and-deploying-to-netlify-4bge</link>
      <guid>https://forem.com/deadwisdom/embracing-jamstack-with-python-generating-a-static-website-with-flask-and-deploying-to-netlify-4bge</guid>
      <description>&lt;h3&gt;
  
  
  The Big Idea
&lt;/h3&gt;

&lt;p&gt;JAMStack completely changes how individuals and entire organizations alike iterate on a web app. It decouples your frontend and backend workflow, so you can focus on speed to your end-user, worry less about how it's served, keep separate iteration cycles, and best of all, enable easy feature branching.&lt;/p&gt;

&lt;p&gt;I'll show, step by step, how you can use the powerful, sophisticated tools of Flask and Python without bothering with specialized static-site generators.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contents:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;JAMStack: What it is and why it's awesome&lt;/li&gt;
&lt;li&gt;Generating Static Websites with Flask: A step-by-step tutorial&lt;/li&gt;
&lt;li&gt;Netlify: What it is and how to deploy your site&lt;/li&gt;
&lt;li&gt;Connecting a Bundler: Use with Rollup, Webpack, Parcel, etc&lt;/li&gt;
&lt;li&gt;Final Thoughts: Some tips and where to go from here&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Important Links:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/DeadWisdom/flask-static-tutorial" rel="noopener noreferrer"&gt;Github Repository for this Tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://flask-static-tutorial.netlify.app/" rel="noopener noreferrer"&gt;Project Example: Rare Pup Detective Agency&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://netlify.com" rel="noopener noreferrer"&gt;Netlify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://flask.palletsprojects.com/" rel="noopener noreferrer"&gt;Flask&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Before We Begin
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Black lives matter.&lt;/li&gt;
&lt;li&gt;We need diversity and women in tech.&lt;/li&gt;
&lt;li&gt;Trans rights are human rights.&lt;/li&gt;
&lt;li&gt;Reminder that we are quickly destroying the earth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All I ask for this content is that you are mindful about applying technology. It is not ours to fix everything, nor must we take on every cause as an individual, but we are the modern day wizards and we must remember that our actions &lt;em&gt;matter&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;And finally, if you struggle with this tutorial or the concepts herein, know that we all struggle. Tech is a deep, long journey, we all (at every level) get frustrated and doubt ourselves constantly. The best way is to keep going, ignore the haters, and reach out to others for help.&lt;/p&gt;

&lt;h1&gt;
  
  
  JAMStack
&lt;/h1&gt;

&lt;p&gt;JAMStack is a terrible name. It sounds like a local event for jarring preserves. But it's a great concept: Focus on a separation of Javascript, API, and Markup.&lt;/p&gt;

&lt;p&gt;The frontend and the backend decouple, with the Markup becoming a &lt;em&gt;static&lt;/em&gt; platform which the JavaScript builds on. Dynamic data is delivered from the API to the client via JavaScript. You can still do some dynamic Markup generation on the server, but that is now an edge-case and is done through the API.&lt;/p&gt;

&lt;p&gt;Since our client assets are now all static, it lends itself to serving directly from a Content Delivery Network. This ensures your site is delivered as fast and efficiently as possible. This is where Netlify comes in. It makes deploying from your repository to a website super slick. And a real fire and forget solution. More on that later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fflask-static-tutorial.netlify.app%2Fstatic%2Fjamstack.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fflask-static-tutorial.netlify.app%2Fstatic%2Fjamstack.webp" alt="A diagram showing JAMStack elements. JavaScript and Markup are static. JavaScript enhances Markup. API is dynamic. API and JavaScript send data to each other."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keeping your frontend and backend decoupled is an amazing thing once you set it up. It's easily worth the price of admission. Backend often requires a lot of tests, analysis, optimization, and generally way longer iteration cycles than frontend, which often wants to make changes and iterate by the minute with immediate results. When you are changing the border radius of a button, you'd rather not wait for a backend build that's often compiling a docker image, running tests, deploying, etc...&lt;/p&gt;

&lt;p&gt;Testing frontend feature branches, without needing to create a whole new backend environment is game changing. It revolutionizes everyone's experience, from the developer all the way to business stakeholders. Netlify gives you a unique URL &lt;em&gt;for every deploy&lt;/em&gt; meaning you can try out new features, review old deploys, and quickly test for production readiness.&lt;/p&gt;

&lt;p&gt;Lastly, keeping things decoupled also makes it easy to slot in serverless endpoints or backendless options like Firebase or AWS Amplify.&lt;/p&gt;

&lt;p&gt;Modularity... Composition... Wow! Who knew it was so great?&lt;/p&gt;

&lt;h3&gt;
  
  
  When not to use JAMStack
&lt;/h3&gt;

&lt;p&gt;JAMStack has one place where classic markup generation is better, and that's when you need to generate a lot of &lt;em&gt;dynamic content&lt;/em&gt;, i.e. Markup that changes depending on the user viewing or some other trip to the database.&lt;/p&gt;

&lt;p&gt;With something like a stock-ticker, that's still doable, because it comes from an API. But for something like a CMS, where the content often changes via a database, then classic Jinja rendering is still where it's at.&lt;/p&gt;

&lt;p&gt;Still, a lot of the promises of database-driven CMSs have yet to materialize, and people are increasingly finding it easier to simply change the content in the source code and redeploy, especially when it's done automatically and quickly.&lt;/p&gt;

&lt;p&gt;Further it's simple to put some content generation behind an API endpoint, if you don't need a lot of that.&lt;/p&gt;

&lt;h1&gt;
  
  
  Generating Static Websites with Flask
&lt;/h1&gt;

&lt;p&gt;This technique allows you to generate a static website in much the same way you'd make a classic Flask app. And this example parallels examples shown by generators like 11ty, Gatsby, and Jekyll, but in my opinion is better because it allows us to use Python, Flask and all the great tools that come with.&lt;/p&gt;

&lt;p&gt;Some benefits of using Flask instead of other static site generators:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;During development, we just get to use a Flask server, there's no compile step.&lt;/li&gt;
&lt;li&gt;No scaling problems when you get to lots of pages.&lt;/li&gt;
&lt;li&gt;We can use the same tools and mindset we use to build standard servers and API servers.&lt;/li&gt;
&lt;li&gt;No new domain-specific languages to do things like for-loops, and shoe-horn database querying into Markdown.&lt;/li&gt;
&lt;li&gt;We can still integrate with any database Flask can, any remote api, and generally do anything Python can do, which is a lot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, we could easily use Django, FastAPI, Starlette, or any other framework for this, but Flask has two extensions that make the process really easy: Frozen-Flask and Flask-FlatPages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tutorial
&lt;/h2&gt;

&lt;p&gt;We're going to break our endeavour into these goals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Setup / Install Our Dependencies and Setup Github&lt;/li&gt;
&lt;li&gt;Create Our Flask App&lt;/li&gt;
&lt;li&gt;Freeze It&lt;/li&gt;
&lt;li&gt;Add Pages / Content with Markdown&lt;/li&gt;
&lt;li&gt;Add JavaScript and Connect to an API&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then afterwards, I'll show you how to deploy with Netilify.&lt;/p&gt;

&lt;p&gt;Now to it!&lt;/p&gt;

&lt;h2&gt;
  
  
  Goal 1: Setup / Install our Dependencies and Setup Github
&lt;/h2&gt;

&lt;p&gt;First we will setup our Git repository. Using &lt;em&gt;Github&lt;/em&gt; here, cause it's a big old standard.&lt;/p&gt;

&lt;p&gt;Let's create &lt;a href="https://github.com/new" rel="noopener noreferrer"&gt;a new repository&lt;/a&gt;. I named it "flask-static-tutorial". I checked "Initialize this repository with a README", added a gitignore (Python), and a license (MIT). You do you.&lt;/p&gt;

&lt;p&gt;Once it's made, clone the repository locally.&lt;/p&gt;

&lt;p&gt;I'm going to assume you have Python 3.6+ on your system. I like using &lt;a href="https://github.com/pyenv/pyenv" rel="noopener noreferrer"&gt;Pyenv&lt;/a&gt; which manages the installation and selection of multiple Python versions. A great tutorial &lt;a href="https://realpython.com/intro-to-pyenv/" rel="noopener noreferrer"&gt;is available here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We're also going to use &lt;a href="https://pipenv-fork.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;Pipenv&lt;/a&gt; for this tutorial. It will manage our Python dependencies. I'm using it mostly because Netlify supports &lt;code&gt;Pipfile&lt;/code&gt; directly. So go ahead and &lt;a href="https://pipenv-fork.readthedocs.io/en/latest/install.html#installing-pipenv" rel="noopener noreferrer"&gt;install that&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now that we have those installed, we'll install our requirements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pipenv &lt;span class="nt"&gt;--python&lt;/span&gt; 3.7 &lt;span class="nb"&gt;install &lt;/span&gt;flask frozen-flask flask-flatpages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pipenv nicely creates a virtual environment for us, a &lt;code&gt;Pipfile&lt;/code&gt; , and &lt;code&gt;Pipfile.Lock&lt;/code&gt; , and installs our packages. We also tell it to use 3.7, because it is the default version for Netlify.&lt;/p&gt;

&lt;p&gt;Now let's commit it, and move on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s1"&gt;'project setup'&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Goal 2: Create Our Flask App
&lt;/h2&gt;

&lt;p&gt;This part is basically just following the flask tutorial. The only little change is that we're using Pipenv.&lt;/p&gt;

&lt;p&gt;Let's make &lt;code&gt;app.py&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;

&lt;span class="c1"&gt;# Create our app object, use this page as our settings (will pick up DEBUG)
&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# For settings, we just use this file itself, very easy to configure
&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# We want Flask to allow no slashes after paths, because they get turned into flat files
&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url_map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strict_slashes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="c1"&gt;# Create a route to our index page at the root url, return a simple greeting
&lt;/span&gt;&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, Flask&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Basic Flask stuff. Of course, you can do anything here, even talk to a database, but you don't want to add anything that is based on user interaction or state. Everything should respond to a simple GET request. Accessing the &lt;code&gt;request&lt;/code&gt; global is a red-flag here. It's &lt;em&gt;static&lt;/em&gt; content after all.&lt;/p&gt;

&lt;p&gt;Now we run our server, and this is where using Flask is great, because we can develop our site as we go, without having to rebuild or run some command to do so after changes. We just pretend we are making any old Flask site. And really, we are.&lt;/p&gt;

&lt;p&gt;First setup our environment, then run it with Pipenv.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;FLASK_DEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;True
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;FLASK_APP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app.py
&lt;span class="nv"&gt;$ &lt;/span&gt;pipenv run flask run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We tell &lt;code&gt;pipenv&lt;/code&gt; to &lt;code&gt;run flask&lt;/code&gt; and &lt;code&gt;flask&lt;/code&gt; to &lt;code&gt;run&lt;/code&gt; . Hope you follow. Alternatively you can create a &lt;a href="https://pipenv-fork.readthedocs.io/en/latest/advanced.html#custom-script-shortcuts" rel="noopener noreferrer"&gt;pipenv script&lt;/a&gt;. But &lt;em&gt;&lt;a href="https://twitter.com/iamtabithabrown" rel="noopener noreferrer"&gt;that's your bizzniss&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Now we can open our browser to &lt;a href="http://127.0.0.1:5000/" rel="noopener noreferrer"&gt;http://127.0.0.1:5000/&lt;/a&gt; and be greeted.&lt;/p&gt;

&lt;p&gt;Great work! But don't get arrogant, we have more to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Goal 3: Freeze It
&lt;/h2&gt;

&lt;p&gt;Did you know you can &lt;a href="https://www.realclearscience.com/blog/2018/07/09/how_people_created_ice_in_the_desert_2000_years_ago.html" rel="noopener noreferrer"&gt;make frozen ice cubes in the hot desert like the Persians thousands of years ago?&lt;/a&gt; Did you also know you can make a frozen Flask website in your computer? Even in the desert.&lt;/p&gt;

&lt;p&gt;Earlier we installed &lt;a href="https://pythonhosted.org/Frozen-Flask/" rel="noopener noreferrer"&gt;Frozen-Flask&lt;/a&gt;. To get started, all we need is another file &lt;code&gt;freeze.py&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask_frozen&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Freezer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;

&lt;span class="n"&gt;freezer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Freezer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;freezer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pipenv run python freeze.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see that it makes a directory &lt;code&gt;build&lt;/code&gt; and the file &lt;code&gt;index.html&lt;/code&gt; . Open it up and you'll see exactly what your browser gets when it goes to '&lt;a href="http://127.0.0.1:5000/" rel="noopener noreferrer"&gt;http://127.0.0.1:5000/&lt;/a&gt;' when our flask process is running.&lt;/p&gt;

&lt;p&gt;Frozen-Flask is really simple. It just runs your app, gets every root endpoint (ones without a path variable), copies the Markup, and saves it to a corresponding file. To find other pages, it tracks every response from &lt;code&gt;url_for()&lt;/code&gt; and adds them to its queue.&lt;/p&gt;

&lt;p&gt;To find pages that are outside of your root tree, &lt;a href="https://pythonhosted.org/Frozen-Flask/#finding-urls" rel="noopener noreferrer"&gt;read more about how it finds urls here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We are really close. Now we just need, you know content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Goal 4: Add Pages / Content with Markdown
&lt;/h2&gt;

&lt;p&gt;It is time. Time for you to look into your cold, frozen heart to see what kind of website you want to make. For me, it's simple: A site for a pet detective agency. But you do you.&lt;/p&gt;

&lt;p&gt;One of the things 11ty, Jekyll and basically every &lt;del&gt;blog creation framework&lt;/del&gt; static site generator does is easily let you create pages with markdown. We're going to do the same.&lt;/p&gt;

&lt;p&gt;This is where Flask-FlatPages comes in. It lets us create pages in any format we want, process them, and then deliver them as HTML.&lt;/p&gt;

&lt;h3&gt;
  
  
  App.py Changes
&lt;/h3&gt;

&lt;p&gt;Let's update our &lt;code&gt;app.py&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;render_template&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask_flatpages&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FlatPages&lt;/span&gt;

&lt;span class="c1"&gt;# Tell Flatpages to auto reload when a page is changed, and look for .md files
&lt;/span&gt;&lt;span class="n"&gt;FLATPAGES_AUTO_RELOAD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;FLATPAGES_EXTENSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.md&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;# Create our app object, use this page as our settings (will pick up DEBUG)
&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# For settings, we just use this file itself, very easy to configure
&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# We want Flask to allow no slashes after paths, because they get turned into flat files
&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url_map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strict_slashes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="c1"&gt;# Create an instance of our extension
&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FlatPages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Route to FlatPages at our root, and route any path that ends in ".html"
&lt;/span&gt;&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&amp;lt;path:path&amp;gt;.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Look for the page with FlatPages, or find "index" if we have no path
&lt;/span&gt;    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_or_404&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;index&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Render the template "page.html" with our page and title
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;render_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note, that we could make other routes to do whatever we want, but currently we just have one that works with Flask-FlatPages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Markdown Pages
&lt;/h3&gt;

&lt;p&gt;We've got Flask-FlatPages looking for pages in a &lt;code&gt;pages&lt;/code&gt; directory as default. So let's create some pages. You can click on each one to copy them or make your own content. Any file you add here that has an '.md' extension will turn into a page, provided you also link to it in another page with &lt;code&gt;url_for()&lt;/code&gt; .&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/DeadWisdom/flask-static-tutorial/blob/master/pages/" rel="noopener noreferrer"&gt;browse files&lt;/a&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pages/
  content.md
  index.md
  team.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;At the top of each page, you'll notice its "meta" section, which is YAML and looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Rare Pup Detective Agency&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;We sniff out the clues.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within the pages, you might notice some HTML. Don't forget, markdown lets us embed HTML! Use it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jinja Template
&lt;/h3&gt;

&lt;p&gt;Now let's create our Jinja template, that serves as the wrapper for our pages &lt;code&gt;templates/page.html&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;

    &amp;lt;meta charset="utf-8"&amp;gt;
    &amp;lt;title&amp;gt;{{ title }}&amp;lt;/title&amp;gt;
    &amp;lt;link rel="stylesheet" href="/static/base.css"&amp;gt;
    &amp;lt;meta name="viewport" content="width=device-width, initial-scale=1"&amp;gt;

  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;

    &amp;lt;header&amp;gt;
      &amp;lt;nav&amp;gt;
        &amp;lt;a href="/" class="logo" aria-hidden="true"&amp;gt;&amp;lt;/a&amp;gt;
        &amp;lt;a href="/" {% if page.path == "index" %}active{% endif %}&amp;gt;Home&amp;lt;/a&amp;gt;
        &amp;lt;a href="{{ url_for('page', path='team') }}" {% if page.path == "team" %}active{% endif %}&amp;gt;Team&amp;lt;/a&amp;gt;
        &amp;lt;a href="{{ url_for('page', path='contact') }}" {% if page.path == "contact" %}active{% endif %}&amp;gt;Contact&amp;lt;/a&amp;gt;
      &amp;lt;/nav&amp;gt;
    &amp;lt;/header&amp;gt;

    &amp;lt;main&amp;gt;
      &amp;lt;article&amp;gt;
      {% block content %}
          &amp;lt;h1&amp;gt;{{ page.meta.description }}&amp;lt;/h1&amp;gt;
          {{ page.html|safe }}
      {% endblock content %}
      &amp;lt;/article&amp;gt;
    &amp;lt;/main&amp;gt;

  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mostly we have a basic page here. There are a few things to note:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;a href="{{ url_for('page', path='team') }}" {% if page.path == "team" %}active{% endif %}&amp;gt;Team&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use flask's &lt;code&gt;url_for()&lt;/code&gt; method to get the final url of our "team" page, also we have add an 'active' attribute to the &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tag when we are on that page, for styling purposes.&lt;/p&gt;

&lt;p&gt;Also in our content block, we do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;h1&amp;gt;{{ page.meta.description }}&amp;lt;/h1&amp;gt;
{{ page.html|safe }}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We add an &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; with the description from the page meta. And finally, we grab the &lt;code&gt;page.html&lt;/code&gt; and run a &lt;code&gt;safe&lt;/code&gt; filter on it because it includes HTML that we want to render unescaped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Static assets
&lt;/h3&gt;

&lt;p&gt;Finally, any static assets like images, css, or js that we link to need to go into &lt;code&gt;/static&lt;/code&gt; . Frozen-Flask will automatically grab them if they are used. For this example, I'm only using a few, but you might many more:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/DeadWisdom/flask-static-tutorial/blob/master/static/" rel="noopener noreferrer"&gt;browse files&lt;/a&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;static/
  images/
    logo.png
  base.css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Develop &amp;amp; Freeze
&lt;/h3&gt;

&lt;p&gt;Alright, alright. That was a lot. Let's check out how we did, make changes, etc. During development we simply use the flask server, and it will auto-reload. We can pretend it's just a normal flask site:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pipenv run flask run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we're done, we can test the freeze, it should put all our pages and assets into the build directory:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;build/
  static/
    images/
      logo.png
    base.css
  content.html
  index.html
  team.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Goal 5: Add JavaScript and Connect to an API
&lt;/h2&gt;

&lt;p&gt;As a final step, we'll add some functionality to call an external API. In a real world scenario, you'd create the API, and host it yourself, or use a serverless option. Here we are just calling out to the wonderful service &lt;a href="https://dog.ceo/dog-api/" rel="noopener noreferrer"&gt;Dog CEO, Dog Api&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We'll add the script linked below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/DeadWisdom/flask-static-tutorial/blob/master/static/js/dogs.js" rel="noopener noreferrer"&gt;static/js/dogs.js&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It defines a custom element "dog-picture" which we've already spread through the site. If the user didn't have JavaScript, the browser just ignores all of the &lt;code&gt;&amp;lt;dog-picture&amp;gt;&lt;/code&gt; elements. Once this script is loaded, they spring to life. Custom Elements. Neat.&lt;/p&gt;

&lt;p&gt;We link it in &lt;code&gt;templates/page.html&lt;/code&gt; with a simple:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/static/js/dogs.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we freeze, you'll see, it gets added to &lt;code&gt;build/&lt;/code&gt; . If we need to compile our JavaScript with rollup, parcel, or webpack it's simple enough to run that before we do freeze.&lt;/p&gt;

&lt;p&gt;Only one more thing, let's deploy the thing...&lt;/p&gt;

&lt;h1&gt;
  
  
  Netlify
&lt;/h1&gt;

&lt;p&gt;Once we have the static content, we need somewhere to put it. Since it's all static, we can basically put it anywhere: behind nginx, on an S3, whatever. But it's best to put it behind a Content Delivery Network, where your data will live on multiple servers around the globe that are specially designed to cache and serve content quickly. The only trouble is managing that deployment process.&lt;/p&gt;

&lt;p&gt;And that's why we use Netlify. They have really figured out the process. Basically, it hooks into your repo, listens for updates, then runs a build command, puts it on a CDN, and routes to it.&lt;/p&gt;

&lt;p&gt;Every build gets served by a &lt;em&gt;unique url&lt;/em&gt;. This is key. It means we can create feature branches of our frontend without a thought.&lt;/p&gt;

&lt;p&gt;And, nicely, it's free for small projects like this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the Site
&lt;/h2&gt;

&lt;p&gt;First we make our account: &lt;a href="https://app.netlify.com/" rel="noopener noreferrer"&gt;https://app.netlify.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next we make our first site: &lt;a href="https://app.netlify.com/start" rel="noopener noreferrer"&gt;https://app.netlify.com/start&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Connect it to Github, and select your repository. For the build options, tell it the command "python freeze.py", and our build directory is "build".&lt;/p&gt;

&lt;p&gt;Note: Netlify's basic build environment will look at our &lt;code&gt;Pipfile&lt;/code&gt; to determine the python version, and requirements. Since it configures the base python environment we don't have to actually run it with pipenv.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fflask-static-tutorial.netlify.app%2Fstatic%2Fbuild-settings.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fflask-static-tutorial.netlify.app%2Fstatic%2Fbuild-settings.webp" alt="Screenshot of the "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now press "Deploy Site". You can watch the progress by clicking on the deploy item, it will give you a full output and status. If you get a big old "Deploy Failed", go into the deploy and scroll down in the page to see why.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fflask-static-tutorial.netlify.app%2Fstatic%2Ffailed-logs.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fflask-static-tutorial.netlify.app%2Fstatic%2Ffailed-logs.webp" alt="Screenshot of the logs for a failed deploy."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When it succeeds you will see "Published" at at the top of the 'Deploys' screen you'll see a link like "&lt;a href="https://infallible-beaver-cf7576.netlify.app" rel="noopener noreferrer"&gt;https://infallible-beaver-cf7576.netlify.app&lt;/a&gt;". Click on it to view it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fflask-static-tutorial.netlify.app%2Fstatic%2Fdeploy-success.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fflask-static-tutorial.netlify.app%2Fstatic%2Fdeploy-success.webp" alt="Screenshot of the deploy page for a successful deploy."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As awesome as "infallible-beaver-cf7576" is, you can rename your site by going to Settings &amp;gt; Domain Management. You can also bring in a custom domain.&lt;/p&gt;

&lt;p&gt;Now every time you commit, it will go live. You can also make it only publish a specific branch, setup triggers, etc. There's a million different options, so play around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some Netlify Extras
&lt;/h2&gt;

&lt;p&gt;Netlify has a decent redirect feature, allowing you to remap a request like /api/* to wherever you want, including a custom API endpoint. &lt;a href="https://docs.netlify.com/routing/redirects/" rel="noopener noreferrer"&gt;Read about Redirects and Rewrites&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Our &lt;code&gt;contact.md&lt;/code&gt; page has a form that doesn't go anywhere, but it has a &lt;code&gt;data-netlify="true"&lt;/code&gt; tag, which is automatically processed by Netlify. It gather all submissions, and you can read them in the site admin. Also, you can set up notifications for them. &lt;a href="https://docs.netlify.com/forms/setup/" rel="noopener noreferrer"&gt;Read about Netlify Forms&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Light authentication can be done pretty simply with &lt;a href="https://docs.netlify.com/visitor-access/identity/" rel="noopener noreferrer"&gt;Netlify's Authentication System&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Connecting a Bundler
&lt;/h1&gt;

&lt;p&gt;The great part about this approach is we don't need a build step, but sometimes you need JavaScript to bundle, by running Rollup, Webpack, Parcel, etc. There are two places you need this. One is during development when you make a change, and the other is during your publish step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bundle For Publishing
&lt;/h3&gt;

&lt;p&gt;One is to change your Netlify build command to something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; python freeze.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Have your package manager build right into your static folder like &lt;code&gt;/static/build&lt;/code&gt; , and then have your Flask template pickup the JavaScript file there. Flask-Freeze will then move it to the &lt;code&gt;/build&lt;/code&gt; directory for Netlify to use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bundle For Development
&lt;/h3&gt;

&lt;p&gt;I'll encourage you to use a resource like &lt;a href="https://open-wc.org" rel="noopener noreferrer"&gt;open-wc.org&lt;/a&gt; or &lt;a href="https://www.snowpack.dev/" rel="noopener noreferrer"&gt;Snowpack&lt;/a&gt; so that you &lt;em&gt;don't&lt;/em&gt; have to build anything for development. ES6 being what it is, you don't need to anymore.&lt;/p&gt;

&lt;p&gt;That said, sometimes we don't get to choose. In that case, we need to run our flask server and the bundler in watch mode.&lt;/p&gt;

&lt;p&gt;The simplest way is to just run two terminals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    Terminal 1: &lt;span class="nv"&gt;$ &lt;/span&gt;pipenv run flask run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    Terminal 2: &lt;span class="nv"&gt;$ &lt;/span&gt;npm run webpack &lt;span class="nt"&gt;--watch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bundle with Webpack
&lt;/h3&gt;

&lt;p&gt;If you're using Webpack, Andrew Montalenti (&lt;a href="https://twitter.com/amontalenti" rel="noopener noreferrer"&gt;@amontalenti&lt;/a&gt;) wrote a bridge between Webpack and Flask that leans on Flask-Static, check it out at his gist:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/amontalenti/ffeca0dce10f29d42a82e80773804355" rel="noopener noreferrer"&gt;https://gist.github.com/amontalenti/ffeca0dce10f29d42a82e80773804355&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;I hope I've shown how JAMStack can be an amazing way to develop, and how we don't need to leave all the glorious wealth we have from Python to get there. From here you can look into &lt;a href="https://github.com/Miserlou/Zappa" rel="noopener noreferrer"&gt;Zappa for serverless endpoints&lt;/a&gt;, deploying containers to &lt;a href="https://aws.amazon.com/fargate/" rel="noopener noreferrer"&gt;AWS Fargate&lt;/a&gt;, or using good old &lt;a href="https://cloud.google.com/appengine/" rel="noopener noreferrer"&gt;Google App Engine&lt;/a&gt;, for your API needs. I still prefer the latter for setting up quick Python APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some Tips
&lt;/h2&gt;

&lt;p&gt;Do remember that the JavaScript part of JAMStack should be your last resort. Too much these days developers are pushing giant React bundles into clients, and our poor devices just can't handle them.&lt;/p&gt;

&lt;p&gt;Progressive enhancement is still, after all these years, the goal. Think as close to Markup as possible, and then move outward as needed. So much of our time is spent implementing doomed features that are overly complex and JavaScript nightmares. Meanwhile what the user really needs is just some web form. JAMStack can help us here, but it can also lead us to Single Page React App nightmares.&lt;/p&gt;

&lt;p&gt;A great way to organize your apps is authenticated vs unauthenticated. Think of Markup as always anonymous, and then JavaScript adds functionality when they login. And think of your APIs as private and public endpoints. That helps you a lot when you get to caching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternatives to Netlify
&lt;/h2&gt;

&lt;p&gt;Netlify isn't really doing anything special here. There's nothing that necessitates it, and the adventurous might want to make their own deployment system. Static assets mean that you can put the hash in a filename and cache them indefinitely. Before Netlify I had a system that chunked my large JS bundles and placed them on S3 named by hash. When I made changes, clients only had to download the updated chunk files.&lt;/p&gt;

&lt;p&gt;You can also publish right to &lt;a href="https://pages.github.com/" rel="noopener noreferrer"&gt;GitHub Pages&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Host it on an &lt;a href="https://www.nginx.com/" rel="noopener noreferrer"&gt;NGinx&lt;/a&gt; server, and put &lt;a href="https://www.cloudflare.com/" rel="noopener noreferrer"&gt;CloudFare&lt;/a&gt; in front of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternatives to Flask
&lt;/h2&gt;

&lt;p&gt;Django has &lt;a href="https://github.com/fabiocaccamo/django-freeze" rel="noopener noreferrer"&gt;Django-Freeze&lt;/a&gt; to create static content in much the same way you'd do it here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.getpelican.com/" rel="noopener noreferrer"&gt;Pelican&lt;/a&gt; is a python based static site generator.&lt;/p&gt;

&lt;p&gt;Use &lt;a href="https://11ty.dev" rel="noopener noreferrer"&gt;11ty&lt;/a&gt; if you want a full-featured specialized static generator written in Javascript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Communicate With Me!
&lt;/h2&gt;

&lt;p&gt;If you spot any problems, have any questions, or want to request further tutorials create an issue in the project repo: &lt;a href="https://github.com/DeadWisdom/flask-static-tutorial/issues" rel="noopener noreferrer"&gt;https://github.com/DeadWisdom/flask-static-tutorial/issues&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Or hit me up on twitter: &lt;a href="https://twitter.com/deadwisdom" rel="noopener noreferrer"&gt;@deadwisdom&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also I am available as a consultant, primarily in helping organizations streamline their innovation and development cycles, especially where product, design, and development need to communicate.&lt;/p&gt;

&lt;p&gt;Thanks to those that helped me writing this, including &lt;a href="https://twitter.com/matteasterday" rel="noopener noreferrer"&gt;@matteasterday&lt;/a&gt;, &lt;a href="https://twitter.com/DanielReesLewis" rel="noopener noreferrer"&gt;@DanielReesLewis&lt;/a&gt;, and &lt;a href="https://twitter.com/amontalenti" rel="noopener noreferrer"&gt;@amontalenti&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And thank you!&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
