<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Posts on brege.org</title>
    <link>https://brege.org/post/</link>
    <description>Recent content in Posts on brege.org</description>
    <generator>Hugo</generator>
    <language>en</language>
    <copyright>Copyright (c) 2016-2026 Wyatt Brege</copyright>
    <atom:link href="https://brege.org/post/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Exploring my camera, screenshot, and image activity</title>
      <link>https://brege.org/post/image-activity/</link>
      <pubDate>Wed, 11 Feb 2026 16:09:29 -0500</pubDate>
      <guid>https://brege.org/post/image-activity/</guid>
      <description>A data exploration project around my personal image collection habits.</description>
      <content:encoded><![CDATA[<p>This project is <a href="/series/personal-data-explorations">part of a series of data exploration projects</a> around my personal computer usage.</p>
<p>GitHub link: <a href="https://github.com/brege/image-activity">github.com/brege/image-activity</a></p>
<h2 id="overview">Overview</h2>
<ul>
<li>Generate heatmaps and histograms of image saving activity over hours, days, and months</li>
<li>Use file timestamps, modified-times, EXIF, and regex parsing for refined image discovery</li>
<li>Add bands and markers for major life events</li>
</ul>
<h2 id="background">Background</h2>
<p>I wanted to determine if my image activity is dependent on major events and device purchases in my life.</p>
<ul>
<li>do I tend to take more pictures during certain times of year?</li>
<li>how has my screenshot usage evolved over the last 15 years?</li>
<li>do I have &ldquo;honeymoon&rdquo; periods after a device purchase?</li>
<li>in what ways has my camera and screenshot usage changed between being an academic, chef, and developer?</li>
</ul>
<p>I&rsquo;m not a social media person, although my <a href="https://mastodon.social/@brege">mastodon</a> did see an uptick of usage following my hip surgery, where I began hiking and foraging a lot.</p>
<p>My image activity fits in three main categories:</p>
<ol>
<li><strong>camera</strong>: storage of camera photos from my phone</li>
<li><strong>screenshots</strong>: screenshots on both my laptop and phone</li>
<li><strong>internet</strong>: pictures downloaded from the internet</li>
</ol>
<h2 id="gallery">Gallery</h2>
<p>I&rsquo;ve marked in these first line charts, <a href="#camera-usage">Camera Usage</a> and <a href="#image-capture-concurrency">Image Capture Concurrency</a>, times when I&rsquo;ve purchased a major device (a new phone or laptop) and a couple key periods of my life. These plots have all been normalized to a 0-100 photo count scale.</p>
<h3 id="camera-usage">Camera Usage</h3>
<p>From 2010 to 2017 I was a Physics TA and, following my 2014 physics prelims, a computational astrophysics doctoral researcher. I began attending conferences in 2015, exploring places around Pullman, WA during these researcher years there.</p>
<img src="img/combined/panel.png" width="100%">
<p>At the end of 2017, I left that life. I embraced my love of food and cooking and became a professional chef for a number of years thereafter, including the Covid-19 pandemic. This period of my life saw a greater number of photos taken: pictures of plates, menus, schedules, etc. My camera photos before this time were mostly non-work-related: travel, events, and pets drove image origination.</p>
<h3 id="image-capture-concurrency">Image Capture Concurrency</h3>
<img src="img/combined/sum.png" width="100%">
<h3 id="heatmaps">Heatmaps</h3>
<p>I only have one experience with online coursework: the data science bootcamp I attended in the fall of 2023. This period did not have a major impact on my screenshotting habits. There are three principal areas in which screenshot usage was more frequent:</p>
<ol>
<li>The creation of my website <a href="https://brege.org">brege.org</a> around August 2016.</li>
<li>As an executive chef, screenshotting is recurrent for scheduling, text message records, receipts/purchase dates, etc.</li>
<li>Agentic-driven coding workflows, beginning midway through 2025, saw a surge in screenshot usage. Screenshots have become a large part of my front-end debugging workflow for web app development&ndash;extending well beyond data-structured <a href="https://www.cypress.io">Cypress</a> end-to-end tests.</li>
</ol>
<p>I did not find my screenshot usage noticeably change during my brief stint with online coursework.</p>
<table>
  <tr>
    <td><img src="img/screenshot/heatmap-laptop.png" width="100%"></td>
    <td><img src="img/screenshot/heatmap-phone.png" width="100%"></td>
    <td><img src="img/camera/heatmap-phone.png" width="100%"></td>
  </tr>
</table>
<p>In general, it appears that I take more screenshots on desktop earlier in the week and in the afternoon (averaged over the last ~15 years). To my surprise, the heatmap for screenshots on my phone have nearly identical densities. I assumed this would be biased toward the weekend and closer to 17:00 because of sports and restaurant dinner service.</p>
<p>Camera usage frequency, on the other hand, is made distinct by day of week only on density during Thursday evening and Saturday afternoon.  It&rsquo;s especially featured in both my Chef days and post-op mobility.</p>
<h3 id="histograms">Histograms</h3>
<p>By device and source, then binned on hours of the day, day of the week, and month of the year, histograms provide a finer distribution in one dimension.</p>
<table>
  <tr>
    <td><img src="img/screenshot/hour.png" width="100%"></td>
    <td><img src="img/combined/hour.png" width="100%"></td>
  </tr>
</table>
<p>For the hourly concentration of all three photo habits, my activity roughly follows a Boltzmann distribution.</p>
<p>These distributions generally peak at two distinct hours:</p>
<ul>
<li>camera photos and screenshots center around 15:00</li>
<li>internet photos are generally concentrated around 20:00</li>
</ul>
<p>Each bin is averaged for each picture type over the last 15 years, regardless of timezone.</p>
<table>
  <tr>
    <td><img src="img/combined/day.png" width="100%"></td>
    <td><img src="img/combined/month.png" width="100%"></td>
  </tr>
</table>
<p>Image activity generally increases at the beginning and end of standard university semesters, which also include the height of summer and the holiday period when I am always always travelling. Screenshotting is highest in the fall to mid-winter.</p>
<p>In my experience, restaurants are historically busier between, roughly, Friendsgiving and Father&rsquo;s Day. Camera usage also largest during high summer. Beach. Hiking. Produce selection during chef years.</p>
]]></content:encoded>
    </item>
    <item>
      <title>20 years of email</title>
      <link>https://brege.org/post/email-analysis/</link>
      <pubDate>Mon, 01 Sep 2025 00:00:00 +0000</pubDate>
      <guid>https://brege.org/post/email-analysis/</guid>
      <description>Making my case that satisfaction surveys are the new email cancer.</description>
      <content:encoded><![CDATA[<p>I&rsquo;m not intentionally a data hoarder. I just haven&rsquo;t been an effective or aggressive email deleter or filter user. This has changed some in recent years, as the techniques for spam emails have evolved to covertly trojan &ldquo;survey&rdquo; subterfuge into my mailbox.</p>
<h2 id="i-have-survey-fatigue">I have survey fatigue</h2>
<p>Surveys are marketing emails. I can&rsquo;t believe I used to take the time to respond to some of them. An analysis on my email history has shown that my hunch on survey spam is correct. Around 2023, I began marking all surveys as spam, and I&rsquo;ve got the data to prove just how rampant companies have used surveys to get their brand in your inbox.</p>
<h2 id="emails-that-matter">Emails that matter</h2>
<p>My main goal in exploring my emails in-depth was to build a predictor of whether an email had any <strong>future usefulness</strong>.</p>
<p>Mail from friends and family, correspondence with students, receipts and financial records, etc. fit the <strong>binary of keep</strong>. After I manually processed this massive backlog of email (with great help of Thunderbird&rsquo;s filters), what I found I discarded most, besides spam, were surveys, newsletters, and other mass mailers.</p>
<p>What I found, qualitatively, was:</p>
<ul>
<li>imperfect spelling, capitalization, and grammar</li>
<li>little-to-no HTML markup</li>
<li>all emails meant only for me sans phishing and spam</li>
</ul>
<p>These traits defined true keepsakes.</p>
<h2 id="introducing-sanoma">Introducing sanoma</h2>
<p><a href="https://en.wiktionary.org/wiki/sanoma">en.wiktionary.org/wiki/sanoma</a></p>
<pre><code>sanoma (noun) Finnish 
  message, communication (a communication or the content of a physical
  message; also the message contained in some act or expression such as a
  work of art)
</code></pre>
<p><strong>sanoma</strong> (<a href="https://github.com/brege/sanoma">github.com/brege/sanoma</a>) uses YAML workflows to define multi-step analysis pipelines. The workflow runner automatically discovers and executes tools from the <code>sanoma/analysis/</code> and <code>sanoma/plot/</code> directories, making it easy to chain data extraction, filtering, analysis, and visualization into reproducible pipelines.</p>
<p>I developed this YAML workflow method in my Markdown-to-PDF project&ndash;<strong><a href="https://github.com/brege/oshea">oshea</a></strong>&ndash;where I realized comprehensive end-to-end tests were just manifest workflows. It&rsquo;s an intuitive way to string command line sequences together. The <em>pipeline</em> term in machine learning/data science is congruent to this system.</p>
<h2 id="data-mining">Data Mining</h2>
<p>While much of this can be done in a Jupyter notebook (far easier to refresh plots this way, although <code>:MarkdownPreview</code> in <strong>Neovim</strong> is sufficient), I built this project as a way to data-mine my own activity. I also want to create a visualization harness for many things on my computer:</p>
<ul>
<li>text message history</li>
<li>email history</li>
<li>screenshot frequency</li>
<li>browser history and bookmarks</li>
</ul>
<p>Because email is text-based, and because my first concept of &ldquo;AI&rdquo; was the need for combative spam filters that have been built over the last thirty years, email felt like a good starting point.</p>
<h2 id="grad-school-emails">Grad-school Emails</h2>
<p>The monthly timeline reveals the academic year rhythm: high volume during active semesters with dramatic drops during summer breaks and winter holidays. The 2016-2017 dip corresponds to the dissertation defense period, where militant email sanitation was a reprieve from LaTeX and simulation monitoring&ndash;hence the dip.</p>
<p>My personal dataset has about 35K emails between my grad-school emails and <a href="https://brege.org">my current website&rsquo;s</a> personal email. Not included are my Gmail and undergrad email(s). I plan on synchronizing those at a later date.</p>
<h3 id="grad-school-timeline-seasonality">Grad-school Timeline Seasonality</h3>
<p><img alt="Grad-school Emails (monthly)" loading="lazy" src="/post/email-analysis/img/wsu/timeline.png"></p>
<p>WSU&rsquo;s Okta system required changing passwords every 6 months, and some time after my defense my account died. I am thankful that I had a Thunderbird profile tucked away on a drive that allowed me to recover all of my university emails.</p>
<h3 id="grad-school-and-onward-histogram">Grad-school and onward Histogram</h3>
<p><img alt="Grad-school Emails (yearly)" loading="lazy" src="/post/email-analysis/img/wsu/histogram.png"></p>
<p>The year-over-year histogram demonstrates consistent academic seasonality, with September-April peaks and May &ndash; mid-August valleys across all years of graduate study. Even with teaching summer labs, the bureaucratic pressure in the summertime dies. I loved teaching in the summer.</p>
<h2 id="spam-marketing-and-surveys">Spam, Marketing, and Surveys</h2>
<p>The spam timeline shows minimal marketing emails pre-2010, followed by a sharp increase around university enrollment. By 2015, spam reached 60-80% of all emails and has remained consistently high. The GDPR implementation around 2018 created a spike in <code>unsubscribe</code> language as companies scrambled to comply with new regulations.</p>
<h3 id="marketing-spam-trends">Marketing Spam Trends</h3>
<p><img alt="Spam Timeline" loading="lazy" src="/post/email-analysis/img/spam/timeline.png"></p>
<p>The tail in the beginning of this timeline is presented for context.
It only includes a &ldquo;purified&rdquo; hotmail account mailbox from my teenage years that extended a bit into my undergrad years. Those years overlap with Gmail usage (not integrated into this data) and my GVSU university email.</p>
<h3 id="keyword-buckets">Keyword Buckets</h3>
<p><img alt="Spam Keywords" loading="lazy" src="/post/email-analysis/img/spam/keywords.png"></p>
<p>Another useful filter for spam emails is checking for keywords like <strong><code>unsubscribe</code></strong> in the message body.</p>
<p><code>unsubscribe_bait</code> dominates with over 12,500 matches, followed by <code>satisfaction</code> surveys (~8k) and direct &ldquo;survey&rdquo; requests (~4k). This reveals how modern marketing shifted from direct sales to engagement-focused tactics requesting feedback and reviews.</p>
<h3 id="conclusion-satisfaction-surveys-are-the-new-email-cancer">Conclusion: Satisfaction Surveys are the new email cancer</h3>
<p><img alt="Spam Heatmap" loading="lazy" src="/post/email-analysis/img/spam/heatmap.png"></p>
<p>The heatmap (filtered to post-2010) shows &ldquo;satisfaction&rdquo; spam as the most persistent threat, maintaining 20-25% frequency from 2012 onwards. Survey-based spam shows steady growth, intensifying after 2020, when both GDPR constraints pressured companies to invent new angles of attack, becoming increasingly desperate for customer &ldquo;feedback&rdquo; (attention) during the pandemic. <strong>Satisfaction feedback surveys are advertisements.</strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>How much of one single food do you need to eat in a day to satisfy protein demands?</title>
      <link>https://brege.org/post/protein-calculator/</link>
      <pubDate>Thu, 17 Apr 2025 16:54:44 -0400</pubDate>
      <guid>https://brege.org/post/protein-calculator/</guid>
      <description>A live protein calculator prototype for common high protein food sources.</description>
      <content:encoded><![CDATA[<p>I have now celebrated one year of resistance training. One of the benefits of consistent, hard exercise is that it naturally steers you toward a healthier, more informed diet—and makes it easier to keep the undesirable effects of indulgence at bay, if you try.</p>
<p>Most anyone in the gym has heard the adage to eat &ldquo;1g of protein per pound of (goal) bodyweight per day.&rdquo; Research has shown this figure isn&rsquo;t accurate; it&rsquo;s more like 60–70% of that (and that&rsquo;s at <strong>peak</strong> resistance training intensity) for ideal muscle mass gains <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>. For example, if you weigh 175 lbs, this guideline states you should eat somewhere between</p>
<p>$$
\begin{aligned}
\frac{\textrm{protein}}{\textrm{day}}
&amp;= \left[0.35,0.75\right]
\cdot \left( \text{g} / \text{lbs} / \text{day} \right)
\cdot 175 \textrm{lbs}  \\
&amp;= ~ \left[60, 140\right] \textrm{g/day}
\end{aligned}
$$</p>
<p>where we have used the shorthand</p>
<p>$$
\text{g} / \text{lbs} / \text{day}
\coloneqq \frac{\text{protein (g)}}{\text{bodyweight (lbs)} \cdot \text{day}}
$$</p>
<p>But for nearly everyone landing on these pages, the goal is fat loss. Higher protein (and fiber) intake will help you feel fuller for longer and can help people new to fitness and diet control their cravings.</p>
<p>Following the guidelines is also made more tedious by the near-constant arithmetic at the grocery store. While it&rsquo;s good to check the labels to learn what&rsquo;s actually in your food, keeping a running total and hitting your target each day becomes a challenge. You wind up relying on apps, breaking meals into chunks: &ldquo;10 grams here, 25g scoop there&hellip; how much protein is in a palm-sized chunk of chicken breast again??&rdquo; And then there’s the protein in bread, grains, bars. It’s nearly impossible to balance all that against metabolic calories if you’re even <em>trying</em> a little.</p>
<h2 id="compositional-thinking-strategy">Compositional Thinking Strategy</h2>
<p><em>Here&rsquo;s my take:</em> <strong>Ratio counting.</strong> As a chef, there&rsquo;s a reason the imperial, fractional system works: ratio. It&rsquo;s easier for me to think about doubling, tripling, or halving depending on what I&rsquo;m buying and who I&rsquo;m cooking for. Our days are divided into twelves and therefore 2&rsquo;s, 3&rsquo;s, and 4&rsquo;s. Naturally, our meals are spaced that way too. The value of our <code>base10</code> system cannot be overstated, but I have a difficult time what eating or preparing 10% less of a meal or recipe means vs doing a quarter or half.</p>
<p>For the 175 lbs individual, the ranges of protein consumption needed are pinned by three characteristic quantities:</p>
<ul>
<li>$\textbf{MAX} = $ 100% efficiency in protein synthesis $\approx 1.6 \text{g} / \text{kg} / \text{day}$</li>
<li><strong>0.75</strong> $\coloneqq 0.75 \times \text{MAX} \approx 1.2 \text{g} / \text{kg} / \text{day}$</li>
<li>$\textbf{RDA} \coloneqq 0.5 \times \text{MAX} \approx 0.8 \text{g} / \text{kg} / \text{day}$</li>
</ul>
<p>where $\text{RDA}$ is the <strong>Recommended Daily Allowance</strong>, and $\text{MAX}$ is the <strong>Maximum Efficient Intake</strong>. The &ldquo;$0.75 \times \text{MAX} \approx 1.2 \text{lbs}$&rdquo; is roughly the target for most people who weigh 175 lbs (80 kg), on average. <sup id="fnref1:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<details> <summary><b>Tap here to see the ranges for this weight...</b></summary> 
<br/>
For reference, the ranges of protein consumption needed for the the 175 lbs individual are:
<p>$$
\text{&lt;65yo + RE:} \quad \left[ 0.8,\ 1.6 \right] \ \text{g} / \text{kg} / \text{day}
$$</p>
<p>$$
\text{⩾65yo + RE:} \quad \left[ 1.1,\ 1.4 \right] \ \text{g} / \text{kg} / \text{day}
$$</p>
<p>where $\text{RE}$ means &ldquo;Resistance Exercise&rdquo;, or, in pounds:</p>
<p>$$
\text{&lt;65yo + RE:} \quad \left[ 0.35,\ 0.75 \right] \ \text{g} / \text{lbs} / \text{day}
$$</p>
<p>$$
\text{⩾65yo + RE:} \quad \left[ 0.55,\ 0.65 \right] \ \text{g} / \text{lbs} / \text{day}.
$$</p>
</details>
<h2 id="implementation-the-protein-efficiency-calculator">Implementation: The Protein Efficiency Calculator</h2>
<p>The table below answers a simple question: how much of one single food do you need to eat to hit your daily protein target? Input your weight, toggle metric or imperial, and adjust units per row—grams, ounces, scoops, each. It’ll show you the amount needed to hit benchmarks in the range of $\text{RDA}$ to the $(\text{MAX})$ intake.  Based on resistance training status and the latest research, a sliding scale in this range will allow you to estimate your required protein demands.</p>
<p><strong>Quantity:</strong> the number of units in the <strong>Unit/Size</strong> column you need to eat to acheive the chosen protein target, per day, for your input weight.</p>
<p>Adjusting the <strong>Protein target</strong> slider can be thought of as a direct conversion of protein to training intensity, from sedentary/$\text{RDA}$ all the way to the $\text{MAX}$ intensity threshold.</p>

<link rel="stylesheet" href="/css/calculator.css">

<div id="calculator">

  <div class="calculator">
    <label for="weight-input" class="form-label"><b>Body Weight:</b></label>
    <div class="input-group">
      <input type="number" id="weight-input" value="175" class="form-control weight-input" />
      <div class="unit-toggle">
        <label><input type="radio" name="unit" value="imperial" checked> lbs</label>
        <label><input type="radio" name="unit" value="metric"> kg</label>
      </div>
    </div>

    <label for="target-range" class="form-label mt-4"><b>Protein target:</b>
      <span id="target-label">0.8 g/lb (RDA) [140 g]</span>
    </label>
    <br>
    <input type="range" id="target-range" class="w-full mb-4" />
  </div>

  <table id="resultsTable">
    <thead>
      <tr>
        <th>Source</th>
        <th>Protein/Unit<br>(g/Unit)</th>
        <th id="target-column-label">Quantity <br> (Unit)</th>
        <th>Unit/Size</th>
        <th>Calories/day<br>(kcal)</th>
        <th>Cite</th>
      </tr>
    </thead>
    <tbody id="protein-tbody">
      
      <tr data-protein="84" data-calories="840">
        <td>Eggs, whole</td>
        <td class="protein-per-unit">84</td>
        <td class="col-target"></td>
        <td>
          <select class="unit-select">
            
            
              <option value="7" data-calories="70" >
                each
              </option>
            
              <option value="0.14" data-calories="1.4" >
                grams
              </option>
            
              <option value="84" data-calories="840" selected>
                dozen
              </option>
            
          </select>
        </td>
        <td class="calories-per-unit">840</td>
        <td class="citation">
          <a href="https://fdc.nal.usda.gov/food-details/1663084/nutrients" target="_blank" rel="noopener">[1]</a>
        </td>
      </tr>
      
      <tr data-protein="105" data-calories="735">
        <td>Chicken Breast, Raw</td>
        <td class="protein-per-unit">105</td>
        <td class="col-target"></td>
        <td>
          <select class="unit-select">
            
            
              <option value="105" data-calories="735" selected>
                pounds
              </option>
            
              <option value="6.6" data-calories="46" >
                ounces
              </option>
            
              <option value="0.25" data-calories="1.7" >
                grams
              </option>
            
              <option value="230" data-calories="1700" >
                kilograms
              </option>
            
          </select>
        </td>
        <td class="calories-per-unit">735</td>
        <td class="citation">
          <a href="https://fdc.nal.usda.gov/food-details/577951/nutrients" target="_blank" rel="noopener">[2]</a>
        </td>
      </tr>
      
      <tr data-protein="105" data-calories="975">
        <td>Beef, Ground (85/15)</td>
        <td class="protein-per-unit">105</td>
        <td class="col-target"></td>
        <td>
          <select class="unit-select">
            
            
              <option value="6.6" data-calories="61" >
                ounces
              </option>
            
              <option value="105" data-calories="975" selected>
                pounds
              </option>
            
              <option value="0.23" data-calories="2.2" >
                grams
              </option>
            
              <option value="232" data-calories="2150" >
                kilograms
              </option>
            
          </select>
        </td>
        <td class="calories-per-unit">975</td>
        <td class="citation">
          <a href="https://fdc.nal.usda.gov/food-details/2312134/nutrients" target="_blank" rel="noopener">[3]</a>
        </td>
      </tr>
      
      <tr data-protein="88" data-calories="600">
        <td>Greek Yogurt (2%)</td>
        <td class="protein-per-unit">88</td>
        <td class="col-target"></td>
        <td>
          <select class="unit-select">
            
            
              <option value="22" data-calories="150" >
                cups
              </option>
            
              <option value="2.7" data-calories="18" >
                ounces
              </option>
            
              <option value="0.1" data-calories="0.68" >
                grams
              </option>
            
              <option value="88" data-calories="600" selected>
                32oz tub
              </option>
            
          </select>
        </td>
        <td class="calories-per-unit">600</td>
        <td class="citation">
          <a href="https://fdc.nal.usda.gov/food-details/360742/nutrients" target="_blank" rel="noopener">[4]</a>
        </td>
      </tr>
      
      <tr data-protein="72" data-calories="600">
        <td>Cottage Cheese (2%)</td>
        <td class="protein-per-unit">72</td>
        <td class="col-target"></td>
        <td>
          <select class="unit-select">
            
            
              <option value="3.11" data-calories="23" >
                ounces
              </option>
            
              <option value="0.11" data-calories="0.81" >
                grams
              </option>
            
              <option value="24" data-calories="200" >
                cups
              </option>
            
              <option value="72" data-calories="600" selected>
                24oz tub
              </option>
            
          </select>
        </td>
        <td class="calories-per-unit">600</td>
        <td class="citation">
          <a href="https://fdc.nal.usda.gov/food-details/2658102/nutrients" target="_blank" rel="noopener">[5]</a>
        </td>
      </tr>
      
      <tr data-protein="23" data-calories="850">
        <td>Tree Nuts (Avg)</td>
        <td class="protein-per-unit">23</td>
        <td class="col-target"></td>
        <td>
          <select class="unit-select">
            
            
              <option value="4.6" data-calories="170" >
                ounces
              </option>
            
              <option value="0.165" data-calories="6.1" >
                grams
              </option>
            
              <option value="23" data-calories="850" selected>
                cups
              </option>
            
          </select>
        </td>
        <td class="calories-per-unit">850</td>
        <td class="citation">
          <a href="https://fdc.nal.usda.gov/food-details/2653755/nutrients" target="_blank" rel="noopener">[6]</a>
        </td>
      </tr>
      
      <tr data-protein="6" data-calories="100">
        <td>Peas, Frozen</td>
        <td class="protein-per-unit">6</td>
        <td class="col-target"></td>
        <td>
          <select class="unit-select">
            
            
              <option value="1.3" data-calories="22" >
                ounces
              </option>
            
              <option value="0.045" data-calories="0.78" >
                grams
              </option>
            
              <option value="6" data-calories="100" selected>
                cups
              </option>
            
          </select>
        </td>
        <td class="calories-per-unit">100</td>
        <td class="citation">
          <a href="https://fdc.nal.usda.gov/food-details/2427531/nutrients" target="_blank" rel="noopener">[7]</a>
        </td>
      </tr>
      
      <tr data-protein="30" data-calories="480">
        <td>Chili (Est.)</td>
        <td class="protein-per-unit">30</td>
        <td class="col-target"></td>
        <td>
          <select class="unit-select">
            
            
              <option value="15" data-calories="240" >
                cups
              </option>
            
              <option value="1.9" data-calories="28" >
                ounces
              </option>
            
              <option value="0.07" data-calories="1" >
                grams
              </option>
            
              <option value="70" data-calories="1000" >
                kilograms
              </option>
            
              <option value="30" data-calories="480" selected>
                bowls
              </option>
            
          </select>
        </td>
        <td class="calories-per-unit">480</td>
        <td class="citation">
          <a href="https://fdc.nal.usda.gov/food-details/2671587/nutrients" target="_blank" rel="noopener">[8]</a>
        </td>
      </tr>
      
      <tr data-protein="100" data-calories="1100">
        <td>Salmon, raw</td>
        <td class="protein-per-unit">100</td>
        <td class="col-target"></td>
        <td>
          <select class="unit-select">
            
            
              <option value="6.23" data-calories="65" >
                ounces
              </option>
            
              <option value="0.22" data-calories="2.3" >
                grams
              </option>
            
              <option value="100" data-calories="1100" selected>
                pounds
              </option>
            
              <option value="220" data-calories="2300" >
                kilograms
              </option>
            
          </select>
        </td>
        <td class="calories-per-unit">1100</td>
        <td class="citation">
          <a href="https://fdc.nal.usda.gov/food-details/2684441/nutrients" target="_blank" rel="noopener">[9]</a>
        </td>
      </tr>
      
      <tr data-protein="104" data-calories="760">
        <td>Pork Chop, Boneless</td>
        <td class="protein-per-unit">104</td>
        <td class="col-target"></td>
        <td>
          <select class="unit-select">
            
            
              <option value="6.2" data-calories="48" >
                ounces
              </option>
            
              <option value="0.22" data-calories="1.7" >
                grams
              </option>
            
              <option value="104" data-calories="760" selected>
                pounds
              </option>
            
              <option value="220" data-calories="1680" >
                kilograms
              </option>
            
          </select>
        </td>
        <td class="calories-per-unit">760</td>
        <td class="citation">
          <a href="https://fdc.nal.usda.gov/food-details/2646168/nutrients" target="_blank" rel="noopener">[10]</a>
        </td>
      </tr>
      
      <tr data-protein="25" data-calories="130">
        <td>Whey Protein Powder</td>
        <td class="protein-per-unit">25</td>
        <td class="col-target"></td>
        <td>
          <select class="unit-select">
            
            
              <option value="0.71" data-calories="3.5" >
                grams
              </option>
            
              <option value="25" data-calories="130" selected>
                scoops
              </option>
            
          </select>
        </td>
        <td class="calories-per-unit">130</td>
        <td class="citation">
          <a href="https://www.costco.com/kirkland-signature-whey-protein%2c-creamy-chocolate%2c-5.4lbs.product.4000287218.html" target="_blank" rel="noopener">[11]</a>
        </td>
      </tr>
      
      <tr data-protein="21" data-calories="150">
        <td>Vegan Protein Powder</td>
        <td class="protein-per-unit">21</td>
        <td class="col-target"></td>
        <td>
          <select class="unit-select">
            
            
              <option value="21" data-calories="150" selected>
                scoops
              </option>
            
              <option value="0.8" data-calories="5.7" >
                grams
              </option>
            
          </select>
        </td>
        <td class="calories-per-unit">150</td>
        <td class="citation">
          <a href="https://orgain.com/products/organic-protein-plant-based-protein-powder-vanilla-bean" target="_blank" rel="noopener">[12]</a>
        </td>
      </tr>
      
    </tbody>
  </table>
</div>

<script>
  let currentUnit = 'imperial';
  let userWeightKg = 175 / 2.205;
  let currentTarget = 1.2;

  function updateProteinNeeds(weightKg) {
    userWeightKg = weightKg;
    updateTableValues();
  }

  function updateTableValues() {
    document.querySelectorAll("tbody#protein-tbody tr").forEach(row => updateSingleRow(row));
  }

  function updateSingleRow(row) {
    const proteinPerUnit = parseFloat(row.dataset.protein);
    const caloriesPerUnit = parseFloat(row.dataset.calories);
    if (isNaN(proteinPerUnit) || proteinPerUnit === 0) return;

    const targetGrams = userWeightKg * currentTarget;
    const quantity = targetGrams / proteinPerUnit;
    const calories = quantity * caloriesPerUnit;

    row.querySelector(".col-target").textContent = quantity.toFixed(2);
    row.querySelector(".calories-per-unit").textContent = isNaN(calories) ? '-' : Math.round(calories);
  }

  function updateTargetRangeAttributes() {
    const range = document.querySelector("#target-range");
    if (currentUnit === 'metric') {
      range.min = 0.8;
      range.max = 1.7;
      range.step = 0.05;
      range.value = currentTarget;
    } else {
      range.min = (0.8 / 2.205).toFixed(3);
      range.max = (1.7 / 2.205).toFixed(3);
      range.step = (0.05 / 2.205).toFixed(3);
      range.value = (currentTarget / 2.205).toFixed(3);
    }
  }

  function updateTargetLabel() {
    const unitSuffix = currentUnit === "metric" ? "g/kg" : "g/lb";
    const displayTarget = currentUnit === "metric" ? currentTarget : currentTarget / 2.205;
    const label = document.querySelector("#target-label");

    const targetGrams = userWeightKg * currentTarget;
    const gramsRounded = Math.round(targetGrams);

    let baseText = `${displayTarget.toFixed(2)} ${unitSuffix}`;
    if (currentTarget <= 0.85) {
      baseText += " (RDA)";
    } else if (currentTarget >= 1.6) {
      baseText += " (MAX)";
    }

    label.textContent = `${baseText} [${gramsRounded} g]`;
  }

  document.querySelector("#weight-input").addEventListener("input", function () {
    const val = parseFloat(this.value);
    if (!isNaN(val)) {
      const weightInKg = currentUnit === "metric" ? val : val / 2.205;
      updateProteinNeeds(weightInKg);
    }
  });

  document.querySelectorAll("input[name='unit']").forEach(radio => {
    radio.addEventListener("change", function () {
      currentUnit = this.value;
      const val = parseFloat(document.querySelector("#weight-input").value);
      const weightInKg = currentUnit === "metric" ? val : val / 2.205;
      updateProteinNeeds(weightInKg);
      updateTargetRangeAttributes();
      updateTargetLabel();
    });
  });

  document.querySelector("#target-range").addEventListener("input", function () {
    currentTarget = currentUnit === 'metric' ? parseFloat(this.value) : parseFloat(this.value) * 2.205;
    updateTargetLabel();
    updateTableValues();
  });

  document.querySelectorAll("tbody#protein-tbody tr").forEach(row => {
    const unitSelect = row.querySelector(".unit-select");
    unitSelect.addEventListener("change", function () {
      const protein = parseFloat(this.value);
      const calories = parseFloat(this.selectedOptions[0].dataset.calories);
      row.dataset.protein = protein;
      row.dataset.calories = calories;
      row.querySelector(".protein-per-unit").textContent = protein.toFixed(2);
      updateSingleRow(row);
    });
  });

  updateTargetRangeAttributes();
  updateProteinNeeds(userWeightKg);
  updateTargetLabel();
</script>

<p>I&rsquo;ve also included the <strong>daily Calories</strong> you&rsquo;d likewise take on per food item.  You&rsquo;ll note that high carbohydrate (peas, chili) and especially high fat content (mixed nuts) greatly diminish the remaining calorie budget in the day.  Conversely, protein powders, including the vegan kind, substantially lower the overall caloric footprint.</p>
<h2 id="the-protein-efficiency-matrix">The Protein Efficiency Matrix</h2>
<p>While the above tool scales protein needs to an individual&rsquo;s body composition, it’s also worth viewing high-protein foods in a way that’s independent of bodyweight. The protein-to-calorie efficiency of a food source is a fixed property — it doesn’t change based on who’s eating it.</p>
<p>With that in mind, the <strong>Protein Efficiency Matrix</strong> below expresses the relative caloric density of each food source. It compares the efficiency of each against the others in the table.</p>
<style>
  .nowrap {
    white-space: nowrap;
  }
  .font-bold {
    font-weight: bold;
  }
  #protein-matrix .td-right {
    width: 3rem;
    min-width: 3rem;
    padding-left: 0.4rem;
    padding-right: 0.4rem;
  }
  #protein-matrix .matrix-col-header {
    position: relative;
    height: 10.5rem;
    width: 3rem;
    min-width: 3rem;
    max-width: 3rem;
    padding: 0;
    vertical-align: bottom;
    overflow: visible;
  }
  #protein-matrix .matrix-col-header > span {
    position: absolute;
    left: 0.35rem;
    bottom: 0.35rem;
    display: block;
    white-space: nowrap;
    line-height: 1;
    font-size: 0.92em;
    transform: rotate(-45deg);
    transform-origin: left bottom;
  }
  @media (max-width: 800px) {
    #protein-matrix .td-right {
      width: auto;
      min-width: auto;
    }
    #protein-matrix .matrix-col-header {
      height: auto;
      width: auto;
      min-width: auto;
      max-width: none;
      padding: 0.5rem 0.4rem;
    }
    #protein-matrix .matrix-col-header > span {
      position: static;
      display: inline;
      white-space: normal;
      line-height: inherit;
      transform: none;
      transform-origin: initial;
    }
  }
</style>

<div id="protein-matrix-container" class="mt-6">
  <div id="protein-matrix"></div>
</div>

<script>
  const proteinMatrixEl = document.querySelector("#protein-matrix");
  const tableBodyEl = document.querySelector("#protein-tbody");

  function getProteinDataRows() {
    return Array.from(tableBodyEl.querySelectorAll("tr")).map(row => {
      const source = row.querySelector("td:first-child").textContent.trim();
      const protein = parseFloat(row.dataset.protein);
      const calories = parseFloat(row.dataset.calories);
      return { source, protein, calories, calPer95g: (95 / protein) * calories };
    }).filter(x => !isNaN(x.calPer95g));
  }

  function getCellStyle(val, extraClass = "") {
    let color = "";
    let fontWeight = "";

    if (val <= 0.75) {
      color = "steelblue";
      fontWeight = "font-bold";
    } else if (val < 0.85) {
      color = "steelblue";
    } else if (val <= 1.15) {
      color = "seagreen";
    } else if (val < 2) {
      color = "lightcoral";
    } else {
      color = "lightcoral";
      fontWeight = "font-bold";
    }

    const classes = ["td-right"];
    if (extraClass) classes.push(extraClass);
    if (fontWeight) classes.push(fontWeight);

    return {
      className: classes.join(" "),
      style: `color: ${color};`
    };
  }

  function buildMatrix(data) {
    const n = data.length;
    const matrix = [];

    
    for (let i = 0; i < n; i++) {
      matrix[i] = [];
      for (let j = 0; j < n; j++) {
        matrix[i][j] = data[i].calPer95g / data[j].calPer95g;
      }
    }

    
    const rowAverages = matrix.map(row =>
      row.reduce((sum, val) => sum + val, 0) / n
    );

    const table = document.createElement("table");
    table.className = "table-style text-sm";

    
    const thead = document.createElement("thead");
    const headerRow = document.createElement("tr");

    headerRow.innerHTML = `
      <th class="th-left" aria-hidden="true"></th>
      <th class="th-top matrix-col-header"><span>Average</span></th>
      ${data.map(item => `<th class="th-top matrix-col-header"><span>${item.source}</span></th>`).join('')}
    `;
    thead.appendChild(headerRow);
    table.appendChild(thead);

    
    const tbody = document.createElement("tbody");
    for (let i = 0; i < n; i++) {
      const row = document.createElement("tr");

      const avg = rowAverages[i];
      const avgStyle = getCellStyle(avg, "bg-blue-50");

      row.innerHTML = `
        <th class="th-left nowrap">${data[i].source}</th>
        <td class="${avgStyle.className}" style="${avgStyle.style}">${avg.toFixed(2)}</td>
        ${matrix[i].map(val => {
          const cellStyle = getCellStyle(val);
          return `<td class="${cellStyle.className}" style="${cellStyle.style}">${val.toFixed(2)}</td>`;
        }).join('')}
      `;
      tbody.appendChild(row);
    }

    table.appendChild(tbody);
    return table;
  }

  function updateMatrixView() {
    const proteinData = getProteinDataRows();
    proteinMatrixEl.innerHTML = "";
    if (proteinData.length === 0) return;
    const matrixTable = buildMatrix(proteinData);
    proteinMatrixEl.appendChild(matrixTable);
  }

  const observer = new MutationObserver(updateMatrixView);
  observer.observe(tableBodyEl, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-protein', 'data-calories'] });

  document.querySelector("#target-range").addEventListener("input", updateMatrixView);
  document.querySelector("#weight-input").addEventListener("input", updateMatrixView);
  document.querySelectorAll("input[name='unit']").forEach(el => el.addEventListener("change", updateMatrixView));

  updateMatrixView();
</script>

<p>Your weight and training status don’t matter here — this table is the same for everyone, assuming equal daily protein intake. The first column shows a simple mean of each row’s relative efficiency compared to others — a rough estimate of how each food stacks up overall.
<strong>Smaller numbers are better.</strong>
High-fat items like salmon and nuts, and high-carb ones like peas and chili, form relative &ldquo;islands of inefficiency&rdquo; among high-protein sources.</p>
<p>You can even think of this matrix as a way to zone:
<span style="color: steelblue;">cutting</span> /
<span style="color: seagreen;">maintaining</span> /
<span style="color: lightcoral;">bulking</span>
cycles using
<span style="color: steelblue;">blues</span> /
<span style="color: seagreen;">greens</span> /
<span style="color: lightcoral;">reds</span>
as a visual cue for how your shopping list and pantry pars might shift depending on your current training status.</p>
<p>For example, Greek yogurt has over five times the protein density of tree nuts by volume. If you’re trying to control weight and meal prep, having this kind of visual measure might help you balance portions — say, between yogurt and nuts in your breakfast bowl.</p>
<p>Note also that the
<span style="color: lightcoral;">red, lower-in-protein efficiency</span>
items are still quite efficient. For example, gas station treats like trollies and doritos have relative efficiency values order(s) of magnitude higher.  Compared to greek yogurt, their protein efficiencies are astonomically bad
$\text{eff.}\left(\text{trollies}:\text{yogurt}\right)$
<span style="color: lightcoral;">
$\approx 32$
</span>
and
$\text{eff.}\left(\text{doritos}:\text{yogurt}\right)$
<span style="color: lightcoral;">
$\approx 13$.
</span>
<em>Abysmal.</em></p>
<h2 id="discussion">Discussion</h2>
<p>This lens is useful because it translates the quantities into <strong>grocery store</strong> units—like pack sizes. About a dozen eggs is one day; two pounds of beef and two pounds of chicken is about three days; a tub of cottage cheese (24oz) and a tub of Greek yogurt (1qt) is just under two days. Then 3–4 scoops of protein powder can help balance that week&rsquo;s diet.</p>
<p>There is also an upper limit to the amount of protein available on the planet. 70% of freshwater is already in use; 50% of global land space is already dedicated to agriculture. It’s a 60–40 split between plant- and animal-sourced proteins occupying this space—and both the global population and protein demand will increase by 20–25% in the next 25 years <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup>.</p>
<p>From a purely resource standpoint, there will need to be a lab-grown protein synthesis renaissance. And it&rsquo;s this fact alone that makes the &ldquo;$1 \text{g} \ \text{protein} / \text{lbs} / \text{day}$&rdquo; myth not only overly generous, but also inconsiderate of the planet’s biological limits. One-third of that is the target if you aren’t lifting. Half to two-thirds is more realistic if you’re engaged in moderate to <em>heavy</em> resistance training.</p>
<h2 id="references">References</h2>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Nunes, E. A., Colenso-Semple, L., McKellar, S. R., et al. (2022). <em>Systematic review and meta-analysis of protein intake to support muscle mass and function in healthy adults</em>. <em>Journal of cachexia, sarcopenia and muscle</em>. <a href="https://pubmed.ncbi.nlm.nih.gov/35187864/">https://pubmed.ncbi.nlm.nih.gov/35187864/</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Schoenfeld, B. J., &amp; Aragon, A. A. (2021). <em>The effect of protein timing on muscle strength and hypertrophy: A systematic review and meta-analysis</em>. <em>Journal of the International Society of Sports Nutrition</em>. <a href="https://link.springer.com/content/pdf/10.1186/1550-2783-10-53.pdf">https://link.springer.com/content/pdf/10.1186/1550-2783-10-53.pdf</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Phillips, S. M. (2014). <em>A brief review of critical processes in exercise-induced muscular hypertrophy</em>. <em>Sports Medicine</em>. <a href="https://link.springer.com/article/10.1007/s40279-014-0152-3">https://link.springer.com/article/10.1007/s40279-014-0152-3</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Moore, D. R., Atherton, P. J., Rennie, M. J., &amp; Phillips, S. M. (2011). <em>Resistance exercise enhances mTOR and MAPK signalling in human muscle over that seen at rest after bolus protein ingestion</em>. <em>Acta physiologica</em>. <a href="https://onlinelibrary.wiley.com/doi/abs/10.1111/j.1748-1716.2010.02187.x">https://onlinelibrary.wiley.com/doi/abs/10.1111/j.1748-1716.2010.02187.x</a>&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Paddon-Jones, D., &amp; Rasmussen, B. B. (2009). <em>Dietary protein recommendations and the prevention of sarcopenia</em>. <em>Current opinion in clinical nutrition &amp; metabolic care</em>. <a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC2760315/pdf/nihms111079.pdf">https://pmc.ncbi.nlm.nih.gov/articles/PMC2760315/pdf/nihms111079.pdf</a>&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Smith, K., Watson, A. W., Lonnie, M., Peeters, W. M., Oonincx, D., Tsoutsoura, N., &hellip; &amp; Corfe, B. M. (2024). <em>Meeting the global protein supply requirements of a growing and ageing population</em>. <em>European journal of nutrition</em>. <a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC11329409/">https://pmc.ncbi.nlm.nih.gov/articles/PMC11329409/</a>&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>Non-linear Weightlifting Progression Calculator</title>
      <link>https://brege.org/post/nonlinear-weightlifting-progression-scheme/</link>
      <pubDate>Tue, 25 Mar 2025 00:00:00 +0000</pubDate>
      <guid>https://brege.org/post/nonlinear-weightlifting-progression-scheme/</guid>
      <description>Using the Epley formula, and others, for estimating an effective pathway through weightlifting plateaus</description>
      <content:encoded><![CDATA[<p>Consider the Epley formula for estimating one-rep max weight:</p>
<p>$$
W_\text{1RM} = W \cdot \left(1 + \frac{R}{30}\right)
$$</p>
<p>To compute your one-rep max weight for a given lift, $W_{1\text{RM}}$, you simply input the weight lifted, $W$, and the number of reps performed before failure, $R$.</p>
<p>This formula has been empirically validated <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> and is a useful tool not just for estimating your max weight for a given weight-rep pair $\left(W, R\right)$, but also to compute the number of predictable reps you can do at any other given weight, relative to your one-rep max.</p>
<p>This is quite useful for plateaus. For example, if you get stuck progressing the Overhead Press to 115 lbs for 5 reps after successfully performing the OHP for 110 lbs for 5 reps, you may need to strategize. Using the Epley formula, to go from
$$
\left( 110\text{lb}, 5 \text{reps} \right) \rightarrow \left(115 \text{lb}, 5 \text{reps}\right),
$$
you are effectively increasing your one-rep max: $W_\text{1RM}: 128 \text{lbs} \rightarrow 134 \text{lbs}$. Our goal here is to effectively increment your $W_\text{1RM}$ in smaller steps:
$$
\small
\left( 110\text{lb}, 5 \text{reps} \right)
\rightarrow \left(115 \text{lb}, 4 \text{reps}\right)
\rightarrow \left(110 \text{lb}, 6 \text{reps}\right)
\rightarrow \left(105 \text{lb}, 8 \text{reps}\right)
\rightarrow \left(115 \text{lb}, 5 \text{reps}\right)
$$
because this is the same as
$$
W_{1\text{RM}}: 128.33 \text{lb}
\rightarrow 130.33 \text{lb}
\rightarrow 132.00 \text{lb}
\rightarrow 133.00 \text{lb}
\rightarrow 134.17 \text{lb} \\
W_{5\text{RM}}: 110.00 \text{lb}
\rightarrow 111.71 \text{lb}
\rightarrow 113.14 \text{lb}
\rightarrow 114.00 \text{lb}
\rightarrow 115.00 \text{lb}
$$
I refer to this as the <strong>Epley pathway</strong>.</p>
<p>Because the Epley formula is nonlinear, this requires an iterative approach. I&rsquo;ve made a calculator that will help you find this path over different $\left(W, R\right)$ pairs. It defaults to using a range of reps roughly half that of your initial number, $R_0$, and a few &ldquo;sweet spot&rdquo; rep values.</p>
<link rel="stylesheet" href="/css/calculator.css">

<div id="calculator">
  <label for="weight" class="form-label"><b>Initial Weight</b> (lbs):</label>
  <input type="number" id="weight" class="form-control" value="135" min="10" max="1000">

  <label for="reps" class="form-label"><b>Initial Reps</b>:</label>
  <input type="number" id="reps" class="form-control" value="5" min="1" max="30">

  <label for="repRange" class="form-label"><b>Rep Range Variation:</b></label>
  <select id="repRange" class="post-tags">
    <option value="0">hybrid: ±R₀/2 and (8, 10, 12, 15, 20, 30)</option>
    <option value="1">R₀±5 reps</option>
    <option value="2">±R₀</option>
    <option value="3">R₀±8 reps</option>
    <option value="4">R₀±10 reps</option>
    <option value="5">R₀±12 reps</option>
    <option value="6">R₀±15 reps</option>
    <option value="7">All reps: [0,..,30]</option>
  </select>

  <label for="formulaSelect" class="form-label"><b>1RM Formula:</b></label>
  <select id="formulaSelect" class="post-tags">
    <option value="epley">Epley</option>
    <option value="brzycki">Brzycki</option>
    <option value="lombardi">Lombardi</option>
    <option value="oconner">O’Conner</option>
  </select>

  
  <div class="calculator-explanation">
    <details open=true> 
<summary> Show/Hide extra notes </summary>
<p>You&rsquo;ll note that I&rsquo;ve generalized this calculator to also include different formulas that different apps use:</p>
<p>$$
W_{\text{1RM}} = f(W, R)
$$</p>
<p>Try changing the dropdown to see how different curves generate slightly different effective rep ranges.  To read more on the survey of different calculations, check out the <a href="https://en.wikipedia.org/wiki/One-repetition_maximum">One-repetition maximum article on Wikipedia</a>.</p>
</details>

  </div>
  

  <h3 id="formulaHeading">Epley Pathway </h3>
  <div id="formulaLatex" class="katex-block" style="margin-bottom: 1rem;"></div>

  <table id="resultsTable" class="table table-dark">
    <thead>
      <tr>
        <th>Weight (lbs)</th>
        <th>Reps</th>
        <th>1RM (lbs)</th>
        <th id="effectiveRMHeader">Effective <br> 5RM (lbs)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>

  <div id="chart-container"></div>

  <script>
    function calculate1RM(weight, reps) {
      const formula = document.getElementById('formulaSelect').value;
      switch (formula) {
        case 'epley': return weight * (1 + reps / 30);
        case 'brzycki': return weight / (1.0278 - 0.0278 * reps);
        case 'lombardi': return weight * Math.pow(reps, 0.10);
        case 'oconner': return weight * (1 + 0.025 * reps);
        default: return weight * (1 + reps / 30);
      }
    }

    function calculateEffectiveRM(estimated1RM, reps) {
      const formula = document.getElementById('formulaSelect').value;
      switch (formula) {
        case 'epley': return estimated1RM / (1 + reps / 30);
        case 'brzycki': return estimated1RM * (1.0278 - 0.0278 * reps);
        case 'lombardi': return estimated1RM / Math.pow(reps, 0.10);
        case 'oconner': return estimated1RM / (1 + 0.025 * reps);
        default: return estimated1RM / (1 + reps / 30);
      }
    }

    function updateFormulaHeading() {
      const formula = document.getElementById('formulaSelect').value;

      const headingMap = {
        epley: "Epley Pathway",
        brzycki: "Brzycki Pathway",
        lombardi: "Lombardi Pathway",
        oconner: "O’Conner Pathway"
      };

      const latexMap = {
        epley: "W_{\\text{1RM}} = W \\cdot \\left(1 + \\frac{R}{30}\\right)",
        brzycki: "W_{\\text{1RM}} = \\frac{W}{1.0278 - 0.0278R}",
        lombardi: "W_{\\text{1RM}} = W \\cdot R^{0.10}",
        oconner: "W_{\\text{1RM}} = W \\cdot (1 + 0.025R)"
      };

      document.getElementById('formulaHeading').textContent = headingMap[formula] || "Epley Pathway";

      const formulaLatex = latexMap[formula] || latexMap["epley"];
      const formulaLatexDiv = document.getElementById('formulaLatex');

      if (window.katex) {
        katex.render(formulaLatex, formulaLatexDiv, { throwOnError: false });
      } else {
        formulaLatexDiv.textContent = `\\(${formulaLatex}\\)`;
      }
    }

    function getRepRange(repVariation, R_0) {
      const reps = new Set();

      if (repVariation === 0) {
        
        const rStart = Math.floor(R_0 / 2);
        const rEnd = Math.floor(3 * R_0 / 2);
        for (let r = rStart; r <= rEnd; r++) reps.add(r);

        
        [8, 10, 12, 15, 20, 30].forEach(r => reps.add(r));
      } else if (repVariation === 1) {
        for (let r = Math.max(1, R_0 - 5); r <= Math.min(30, R_0 + 5); r++) reps.add(r);
      } else if (repVariation === 2) {
        for (let r = 0; r <= 2 * R_0; r++) reps.add(r);
      } else if (repVariation === 3) {
        for (let r = Math.max(1, R_0 - 8); r <= Math.min(30, R_0 + 8); r++) reps.add(r);
      } else if (repVariation === 4) {
        for (let r = Math.max(1, R_0 - 10); r <= Math.min(30, R_0 + 10); r++) reps.add(r);
      } else if (repVariation === 5) {
        for (let r = Math.max(1, R_0 - 12); r <= Math.min(30, R_0 + 12); r++) reps.add(r);
      } else if (repVariation === 6) {
        for (let r = Math.max(1, R_0 - 15); r <= Math.min(30, R_0 + 15); r++) reps.add(r);
      } else if (repVariation === 7) {
        for (let r = 0; r <= 30; r++) reps.add(r);
      }

      return Array.from(reps).filter(r => r >= 1 && r <= 30).sort((a, b) => a - b);
    }

    function calculateResults() {
      const W_0 = parseInt(document.getElementById('weight').value);
      const R_0 = parseInt(document.getElementById('reps').value);
      const repVariation = parseInt(document.getElementById('repRange').value);

      const repsToTry = getRepRange(repVariation, R_0);
      const lowerLimit = W_0;
      const upperLimit = W_0 + 5;

      document.getElementById('effectiveRMHeader').textContent = `Effective ${R_0}RM (lbs)`;

      const pairs = [];
      for (let W = 10; W <= 1000; W += 5) {
        for (const R of repsToTry) {
          const oneRM = calculate1RM(W, R);
          const effectiveRM = calculateEffectiveRM(oneRM, R_0);
          if (effectiveRM >= lowerLimit && effectiveRM <= upperLimit) {
            pairs.push({ W, R, oneRM, effectiveRM });
          }
        }
      }

      pairs.sort((a, b) => a.effectiveRM - b.effectiveRM);

      const tbody = document.querySelector("#resultsTable tbody");
      tbody.innerHTML = '';

      let finalRowAdded = false;
      for (const row of pairs) {
        const tr = tbody.insertRow();
        tr.insertCell(0).textContent = row.W;
        tr.insertCell(1).textContent = row.R;
        tr.insertCell(2).textContent = row.oneRM.toFixed(2);
        tr.insertCell(3).textContent = row.effectiveRM.toFixed(2);
        if (Math.abs(row.W - W_0) <= 5) tr.classList.add('bold-weight');
        if (row.W === W_0 + 5 && row.R === R_0) finalRowAdded = true;
      }

      if (!finalRowAdded) {
        const finalOneRM = calculate1RM(W_0 + 5, R_0);
        const finalEffective = calculateEffectiveRM(finalOneRM, R_0);
        const finalRow = tbody.insertRow();
        finalRow.insertCell(0).textContent = W_0 + 5;
        finalRow.insertCell(1).textContent = R_0;
        finalRow.insertCell(2).textContent = finalOneRM.toFixed(2);
        finalRow.insertCell(3).textContent = finalEffective.toFixed(2);
        finalRow.classList.add('bold-weight');
      }

      tbody.querySelectorAll("tr").forEach(tr => {
        tr.addEventListener("click", () => {
          tbody.querySelectorAll("tr").forEach(r => r.classList.remove("highlighted-row"));
          tr.classList.add("highlighted-row");
        });
      });

      updateChart(pairs);
    }


    document.getElementById('formulaSelect').addEventListener('change', () => {
      updateFormulaHeading();
      calculateResults();
    });

    document.getElementById('weight').addEventListener('input', calculateResults);
    document.getElementById('reps').addEventListener('input', calculateResults);
    document.getElementById('repRange').addEventListener('change', calculateResults);

    window.addEventListener('DOMContentLoaded', () => {
      updateFormulaHeading();
      calculateResults();
    });
  </script>

  <hr/>
</div>

<style>
  #resultsTable td {
    padding: 0.5rem !important;
    margin: 0 !important;
  }
  #resultsTable th {
    padding: 0.75rem !important;
  }
</style>


<p><em>More details of different, specific formulae for Epley <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> Brzycki <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>, Lombardi <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> and O&rsquo;Connor <sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>, and several complimentary research articles <sup id="fnref1:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> and <sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup>, are available.</em></p>
<hr />
<h3 id="my-progression-scheme">My Progression Scheme</h3>
<p>This makes it straightforward for me to also compute the warmup sets needed for my leading barbell exercise. Recently, I&rsquo;ve found good success in pyramiding warmups using my calculated $\left(30, 20, 10\right)\text{RM}$&rsquo;s for $\left(12, 8, 4\right) \text{reps}$, respectively.</p>
<p>In addition, another scheme I&rsquo;ve been using for strength-focused exercises has been, after 3–6 reps of heavy weight for three sets, to drop set into $\left(10, 20\right)\text{RM}$ for $\left(8, 12\ldots\text{amrap}\right)$, respectively, to increase my work volume without chewing up my joints, leaving $\sim \text{2}$ $\text{rir}$. Here, $\text{amrap} = $ &ldquo;as many reps as possible&rdquo; and $\text{rir} =$ &ldquo;reps in reserve&rdquo;. My thoughts here reflect the goal to cover warmup (3), strength (3–4), and hypertrophy (2), in a single leading compound exercise.</p>
<div class="progression-chart">
  <svg id="progressionChart" width="100%" height="auto" viewBox="0 0 800 400" preserveAspectRatio="xMidYMid meet"></svg>

  
    <div class="progression-caption"><b>Progression Chart Example: Set Breakdown for Leading Barbell Lift.</b> The width of the bins represents the number of reps, $R$, denoted inside the bins. The weights, $W_{N\text{RM}}$, are in lbs, represented as the height of the bins. From left to right, the weights in my scheme are: 30RM, 20RM, 10RM, 5RM (3x's), 10RM, 20RM. This provides a visual sense of volume in each zone of the exercise. <i>If you change the weights in the calculator, this plot will update. Because this is a hybrid model for strength training and hypertrophy, it will bind the rep ranges between 3–6 reps. The $y$-axis is truncated at ⅔ $W_{\text{30RM}}$.</i></div>
  
</div>

<script src="https://d3js.org/d3.v7.min.js"></script>

<script>
  let inputWeight =  135 ;
  let inputReps =  5 ;
  let chartData = [];

  function progression_calculate1RM(weight, reps) {
    return weight * (1 + reps / 30);
  }

  function progression_roundToNearest5(num) {
    return Math.round(num / 5) * 5;
  }

  function progression_calculateWeights(weight, reps) {
    const W_1RM = progression_roundToNearest5(progression_calculate1RM(weight, reps));
    const W_30RM = progression_roundToNearest5(W_1RM / (1 + 30 / 30));
    const W_20RM = progression_roundToNearest5(W_1RM / (1 + 20 / 30));
    const W_10RM = progression_roundToNearest5(W_1RM / (1 + 10 / 30));
    const W_5RM = progression_roundToNearest5(weight);
    return { W_30RM, W_20RM, W_10RM, W_5RM, W_1RM };
  }

  function progression_updateChart() {
    const constrainedReps = Math.max(3, Math.min(inputReps, 6));
    const { W_30RM, W_20RM, W_10RM, W_5RM, W_1RM } = progression_calculateWeights(inputWeight, constrainedReps);

    chartData = [
      { label: '12 reps (Warmup)', weight: W_30RM, reps: 12, zone: 'warmup' },
      { label: '8 reps (Warmup)', weight: W_20RM, reps: 8, zone: 'warmup' },
      { label: '4 reps (Warmup)', weight: W_10RM, reps: 4, zone: 'warmup' },
      { label: `${constrainedReps} reps (Workset 1)`, weight: inputWeight, reps: constrainedReps, zone: 'worksets' },
      { label: `${constrainedReps} reps (Workset 2)`, weight: inputWeight, reps: constrainedReps, zone: 'worksets' },
      { label: `${constrainedReps} reps (Workset 3)`, weight: inputWeight, reps: constrainedReps, zone: 'worksets' },
      { label: '8 reps (Dropset)', weight: W_10RM, reps: 8, zone: 'dropsets' },
      { label: '12 reps (Dropset)', weight: W_20RM, reps: 12, zone: 'dropsets' }
    ];

    const svg = d3.select("#progressionChart");
    svg.selectAll("*").remove();

    const margin = { top: 20, right: 20, bottom: 60, left: 40 };
    const width = 800 - margin.left - margin.right;
    const height = 400 - margin.top - margin.bottom;
    const svgGroup = svg
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom)
      .append("g")
      .attr("transform", `translate(${margin.left},${margin.top})`);

    const xScale = d3.scaleBand()
      .domain(chartData.map(d => d.label))
      .range([0, width])
      .padding(0.1);

    const yMin = chartData[0].weight / 1.5;
    const yMax = d3.max(chartData, d => d.weight) * 1.1;

    const yScale = d3.scaleLinear()
      .domain([yMin, yMax])
      .range([height, 0]);

    const barWidth = (reps) => reps * 10;
    const spacing = 5;
    let midPoints = [];
    let currentXPosition = 0;

    svgGroup.selectAll(".bar")
      .data(chartData)
      .enter()
      .append("rect")
      .attr("class", "bar")
      .attr("x", function(d, i) {
        const xPosition = currentXPosition;
        midPoints.push(xPosition + barWidth(d.reps) / 2);
        currentXPosition += barWidth(d.reps) + spacing;
        return xPosition;
      })
      .attr("y", d => yScale(d.weight))
      .attr("width", d => barWidth(d.reps))
      .attr("height", d => height - yScale(d.weight))
      .attr("fill", (d) => {
        if (d.zone === "warmup") return "seagreen";
        if (d.zone === "worksets") return "steelblue";
        return "lightcoral";
      });

    svgGroup.selectAll(".bar-weight")
      .data(chartData)
      .enter()
      .append("text")
      .attr("class", "bar-weight")
      .attr("x", (d, i) => midPoints[i])
      .attr("y", d => yScale(d.weight) - 10)
      .attr("text-anchor", "middle")
      .attr("font-size", "12px")
      .attr("fill", "white")
      .text(d => `${d.weight.toFixed(1)} lbs`)
      .style("fill", "var(--primary)");

    svgGroup.selectAll(".bar-reps")
      .data(chartData)
      .enter()
      .append("g")
      .attr("class", "bar-rep-group")
      .append("text")
      .attr("class", "bar-reps")
      .attr("x", (d, i) => midPoints[i])
      .attr("y", (d) => yScale(d.weight) + (height - yScale(d.weight)) / 2)
      .attr("text-anchor", "middle")
      .attr("font-size", "14px")
      .attr("fill", "var(--theme)")
      .attr("font-weight", "bold")
      .text(d => `${d.reps} reps`)
      .style("z-index", "10");

    const zoneMidpoints = {
      warmup: (midPoints[0] + midPoints[2]) / 2,
      worksets: (midPoints[3] + midPoints[5]) / 2,
      dropsets: (midPoints[6] + midPoints[7]) / 2
    };

    svgGroup.selectAll(".zone-label")
      .data(Object.keys(zoneMidpoints))
      .enter()
      .append("text")
      .attr("class", "zone-label")
      .attr("x", d => zoneMidpoints[d])
      .attr("y", height + 25)
      .attr("text-anchor", "middle")
      .attr("font-size", "14px")
      .text(d => d.charAt(0).toUpperCase() + d.slice(1))
      .style("fill", "var(--primary)");

    const totalWidth = chartData.reduce((acc, d) => acc + barWidth(d.reps) + spacing, 0);
    svgGroup.append("g")
      .attr("class", "x-axis")
      .attr("transform", `translate(0, ${height})`)
      .append("line")
      .attr("x1", 0)
      .attr("x2", totalWidth)
      .attr("y1", 0)
      .attr("y2", 0)
      .style("stroke", "var(--primary)")
      .style("stroke-width", "1px");

    svgGroup.append("g")
      .call(d3.axisLeft(yScale));

    svgGroup.selectAll(".axis text")
      .style("font-size", "14px")
      .style("fill", "var(--primary)");

    d3.select(".progression-chart")
      .style("background", "var(--code-bg)")
      .style("border", "1px solid var(--border)")
      .style("border-radius", "var(--radius)")
      .style("padding", "1rem");
  }

  document.getElementById('weight').addEventListener('input', (event) => {
    inputWeight = parseInt(event.target.value);
    progression_updateChart();
  });

  document.getElementById('reps').addEventListener('input', (event) => {
    inputReps = parseInt(event.target.value);
    progression_updateChart();
  });

  progression_updateChart();
</script>


<h3 id="possible-improvements">Possible Improvements</h3>
<ul>
<li>
<p>Create a similar pathway table for the goal of adding a rep, instead of adding $5 \text{lbs}$ to the bar.  Will take a good amount of effort.</p>
</li>
<li>
<p>Implement a pyramid scheme with broader rep ranges in the progression chart histogram. I haven&rsquo;t thought much about this yet, but when I can hit 7 reps on heavy compound lifts, I feel like I&rsquo;m entering a moderately safe 7–12 rep range. At that point, the effort level shifts to something different.</p>
</li>
<li>
<p><del>The Epley model is just one of many. I haven&rsquo;t explored the others yet because I could solve this with fractional arithmetic in my head while &ldquo;ape-brained&rdquo; in the gym. However, adding a dropdown toggle to choose different models, and referring to recent literature on advancements in curve fitting, would make this tool more robust.</del> Similar to how the Opus audio codec performs at different fidelities with various bitrates, I&rsquo;m sure that the pathway from 1RM to 30RM follows a more complex, multi-fitted approach.</p>
</li>
</ul>
<h3 id="references">References</h3>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://opensiuc.lib.siu.edu/cgi/viewcontent.cgi?article=1744&amp;context=gs_rp">https://opensiuc.lib.siu.edu/cgi/viewcontent.cgi?article=1744&amp;context=gs_rp</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="#fnref1:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://scholar.google.com/scholar?hl=en&amp;as_sdt=0%2C23&amp;q=+Epley%2C+Boyd+%281985%29.+%22Poundage+Chart%22.+Boyd+Epley+Workout.+Lincoln%2C+NE%3A+Body+Enterprises.+p.+86.+&amp;btnG=">https://scholar.google.com/scholar?hl=en&amp;as_sdt=0%2C23&amp;q=+Epley%2C+Boyd+%281985%29.+%22Poundage+Chart%22.+Boyd+Epley+Workout.+Lincoln%2C+NE%3A+Body+Enterprises.+p.+86.+&amp;btnG=</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="https://scholar.google.com/scholar?hl=en&amp;as_sdt=0%2C23&amp;q=+Brzycki%2C+Matt+%281998%29.+A+Practical+Approach+To+Strength+Training.+McGraw-Hill.+ISBN+978-1-57028-018-4.+&amp;btnG=">https://scholar.google.com/scholar?hl=en&amp;as_sdt=0%2C23&amp;q=+Brzycki%2C+Matt+%281998%29.+A+Practical+Approach+To+Strength+Training.+McGraw-Hill.+ISBN+978-1-57028-018-4.+&amp;btnG=</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p><a href="https://www.unm.edu/~rrobergs/478RMStrengthPrediction.pdf">https://www.unm.edu/~rrobergs/478RMStrengthPrediction.pdf</a>&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p><a href="https://www.medicalalgorithms.com/equation-of-oconnor-et-al-for-predicting-the-one-repetition-maximum-1-rm">https://www.medicalalgorithms.com/equation-of-oconnor-et-al-for-predicting-the-one-repetition-maximum-1-rm</a>&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p><a href="https://www.researchgate.net/profile/Marcelo-Silva-12/post/Prediction_of_1RM_in_muscular_strength_what_is_the_better_7-10_rep_or_5-7_rep/attachment/59d6235b79197b8077981b20/AS%3A306915098202113%401450185669112/download/478RMStrengthPrediction.pdf">https://www.researchgate.net/profile/Marcelo-Silva-12/post/Prediction_of_1RM_in_muscular_strength_what_is_the_better_7-10_rep_or_5-7_rep/attachment/59d6235b79197b8077981b20/AS%3A306915098202113%401450185669112/download/478RMStrengthPrediction.pdf</a>&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>Foraging Humanity</title>
      <link>https://brege.org/post/foraging-humanity/</link>
      <pubDate>Tue, 31 Oct 2023 15:52:39 -0400</pubDate>
      <guid>https://brege.org/post/foraging-humanity/</guid>
      <description>I talk about three mediums that capture the art of the lost and found</description>
      <content:encoded><![CDATA[<p>This is a small survey of a band, a magazine, and a show that each provide unique lenses into our world.  Absent celebrity, each of them forage their way through humanity&rsquo;s discards, revealing to us emotions that are truer to our spirit and more honest about our struggle.</p>
<h2 id="the-books">The Books</h2>
<p><a href="https://en.wikipedia.org/wiki/The_Books">The Books</a> were a musical duo that made music from found sound recordings.  Like a collage, bits of old cassette tapes discovered at garage sales, thrift stores, and thrown out are pieced together through melody with minimal instrumention to create emotional, sonically beautiful tracks.</p>
<style>
  .image-container {
    display: flex;
    justify-content: space-between;
  }

  .image-container img {
    width: 32%; 
  }
</style>
<div class="image-container">
  <img src="img/thought-for-food.jpg" alt="The Books: Though For Food (2002) cover art is an egg shape labeled 'thought' and a cavity labeled 'food'">
  <img src="img/lemon-of-pink.jpg" alt="The Books: The Lemon of Pink cover art">
  <img src="img/lost-and-safe.jpg" alt="The Books: Lost and Safe cover art features an abstract cube at the center of a spider web">
</div>
<p>This mosaic of found sound and vocal samples in large part created a music genre of its own.  Well, mostly.  I think most of us have heard those tracks that kickstart or bridge their songs with an audio clip taken from some movie or speech.  But The Books did this to the extreme, mixing these sounds into their songs as their own instrument.</p>
<p><div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube-nocookie.com/embed/-lr9WshYouk?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<a href="https://www.youtube.com/watch?v=-lr9WshYouk">The Books - Smells Like Content (3:42)</a></p>
<p>In fact, each track is filled with an unimaginable number of recordings&ndash;sounds that bleed with every emotion of the original, forgotten storyteller within them.  There are also
<a href="https://en.wikipedia.org/wiki/Aleatoric_music">aleatoric takes</a>
(re: &ldquo;by digitizing sound and thunder&rdquo;) that fold more natural sounds in, but not like
<a href="https://en.wikipedia.org/wiki/Stomp_(theatrical_show)">Stomp</a>
or <a href="https://en.wikipedia.org/wiki/Anathallo">Anathallo</a>.</p>
<p>What compelled me most was the unscripted essence of the recordings.  I wondered about the people that may have listened to some of those source tapes on long road trips, or recorded there.  Like archives of thoughts from carpeted basements, strung together from one fried thought to the next.</p>
<p><div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube-nocookie.com/embed/oD4anv60ow8?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<a href="https://www.youtube.com/watch?v=oD4anv60ow8">The Books - All Bad Ends All (2:42)</a></p>
<h2 id="found-magazine">Found Magazine</h2>
<p><a href="https://en.wikipedia.org/wiki/Found_Magazine">Found Magazine</a> was created by another duo, based in Ann Arbor, Michigan whose pages were each a collage of scrap notes and photos sent in to them.  In it, glimpses of everyday minutia are scanned into its pages, without continuity, often bearing the weather and stains obtained in journey from the trash.</p>
<p><img alt="4x2 grid of Found magazine covers over the years.  They all have a collage-like build quality to them." loading="lazy" src="/post/foraging-humanity/img/FOUND+Magazine+Covers.jpg">
<a href="https://images.squarespace-cdn.com/content/507dba43c4aabcfd2216a447/1377200982709-D8CFHDZBV9GAUZ38NUC9/FOUND+Magazine+Covers.jpg?format=1000w&amp;content-type=image%2Fjpeg">Source</a></p>
<p>Amid a probable banality, the subjects of these clippings range anywhere from odd grocery store requests to people dealing with heavy topics like abortion, divorce, and suicidal thoughts.  I was in my very early twenties when my friends introduced me to it, and while I had every opinion imaginable about the world, I certainly didn&rsquo;t suffer any of the experiences these notes conveyed.</p>
<p>There was sheer earnesty to them: a glimpse into worlds you couldn&rsquo;t ever possibly live.
People&rsquo;s experiences simultaneously made out to be both relatable and unlivable.</p>
<p><img alt="FOUND scrap note says: &ldquo;7th Grade. Dear Katie, I won&rsquo;t forgive you for that what happend Thursday ant I&rsquo;m no Longer being frie with you because you&rsquo;re annoying. from, Zyan&rdquo;" loading="lazy" src="/post/foraging-humanity/img/ANNOYING-crop.jpg">
<a href="http://foundmagazine.com/find/annoying/">Source</a></p>
<p>It is no wonder why this magazine was so popular in the US penal system.  Shy of living in it, it&rsquo;s these discards that are made to share our bond.  To me, there is an inner nature here lost in most documentaries (ugh, <em>Netflix</em>), let alone all reality TV shows: they are over prompted and epicly driven.  But the stories in <em>Found</em> were of a different species, one that was free of slant, rich in humor, tied deeply in emotion and all of our imperfections.</p>
<h2 id="how-to-with-john-wilson">How To with John Wilson</h2>
<p><img alt="Picture of How To with John Wilson card shows director at three angles in colage on a washed out background of New York" loading="lazy" src="/post/foraging-humanity/img/How-To-With-John-Wilson.avif">
<a href="https://static1.colliderimages.com/wordpress/wp-content/uploads/2022/01/How-To-With-John-Wilson.jpg">Source</a></p>
<p>Arguably the most inaccurately titled show ever, John Wilson sets out in New York City with nothing but his video camera, as he has done since he was a child, recording literally everything. In his docuseries <a href="https://en.wikipedia.org/wiki/How_To_with_John_Wilson">How To with John Wilson</a>, there are shots of spills and overflowing trash, candid public moments and private invitations, people spontaneously napping or ritualistic maneuvers that all walks of city life take.  But moreso, it&rsquo;s the profoundly deep cuts into the human experience accompanying the imagery that make it so special.</p>
<p><div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube-nocookie.com/embed/UUiRM7NOOow?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

YouTube: <a href="https://www.youtube.com/watch?v=UUiRM7NOOow">How To with John Wilson - Touch and Go</a></p>
<p>What I loved about this show, which thankfully made it to (but sadly ended in) three seasons, was the juxtaposition of Wilson&rsquo;s unpolished interview/questioning skills with a carefully well thought-out narrative.  There is an incredible mastery of using random, second-long bits to string together larger, much broader points about our experiences in the world.</p>
<p>The travelogues benefit from people living in tight proximity in an entirely manmade habitat: The City.  Even when journeying out West (I forgot why?) to fail in filming <strong>Burning Man</strong>, Wilson somehow winds up inside an underground, decommissioned nuclear launch bunker that feels just as claustrophobic and unnatural as the city he&rsquo;s escaped.</p>
<h2 id="conclusions">Conclusions</h2>
<p>While unfortunately these mediums aren&rsquo;t widely known nor around anymore, I think about them often when I forage for different things on my hikes.
So maybe I&rsquo;m intrigued by <em>The Books</em>, <em>Found</em>, and <em>John Wilson</em> for a similar lust, that it hits such a similar psycological mechanism is probably no accident.</p>
<p>I think about random notes I&rsquo;ve found, the content of which often surprised me.
I also imagine all the little notes I&rsquo;ve lost whose finder must have been just as confused upon reading.
I guess that&rsquo;s my larger wonder.
We don&rsquo;t always know who our audience is.  And I think that&rsquo;s actually quiet wonderful.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Some Sweet Thoughts on Sugar</title>
      <link>https://brege.org/post/sugar/</link>
      <pubDate>Sat, 10 Jun 2023 16:35:51 -0400</pubDate>
      <guid>https://brege.org/post/sugar/</guid>
      <description>How I think about sugar and sweetness as a chef from the perspectives of
history, technique, and natural science.</description>
      <content:encoded><![CDATA[<p>Chefs work with a wide variety of different sugars and syrups.  A small bit of sweetness in a savory dish can bring balance just as well as salt or acid do to sweet dishes.  Sometimes this can be accomplished via onion, carrot or tomato in a rich sauce, or via tree fruits, berries or wine in an aromatic one.</p>
<p>Sweetness is a part of the flavor balancing equation.  Sugar is energy dense.  It stores well.  It is the second fastest &ldquo;macro&rdquo; metabolized by our bodies.  Like the fastest (alcohol), we associate its taste with peak ripeness since it expresses itself in fruit through juiciness which elicits immediate opportunity modes within our survival brain.</p>
<p>One can practice a great deal of creativity with simple table sugar and their stove top range alone.  In addition to practically any sweet confection, sugar is melted into water for simple syrup in cocktails, into <strong>caramels</strong> and all its derivative forms (<strong>butterscotch</strong>, <strong>toffee</strong>, <strong>gastriques</strong> <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, etc.), and heated in puddings, pot de creme and creme anglaise.  All in all, understanding that sugar has a particular <em>behavior</em> when it melts carries over to practically every dessert sauce in existence.  Melting sugar to different temperatures and playing with ratios of milk fats and proteins explodes a whole catalog of dessert sauces.</p>
<p><strong>Molasses</strong> is the leftover byproduct from refining sugar from sugar cane, pomegranate, or beets, each of which imparts complex flavor profiles and adds many of its nutrients to the syrup.  Molasses is the brown in brown sugar.  Most barbecue sauces call for molasses and/or brown sugar.  I tend to not carry brown sugar in my kitchens because of its propensity to harden into an annoying brick&ndash;opting instead for molasses and white sugar always, which allows you to play with different types of molasseses and provides an easy to adjust <strong>bitterness</strong> knob.</p>
<p>You can also &ldquo;make&rdquo; brown sugar-adjacent substances by mixing honey, maple syrup or agave with granulated sugar (try it!).  In truth, I find there is little advantage keeping brown sugar on hand all together; its shelf life is an order of magnitude shorter than its two parent items, recipes often call for light or dark brown sugar ($\sim$ 1 or 2 Tbsp of molasses per cup of sugar, respectively), adding needless inventory.  Perhaps an issue is the variability of molasses spoonfuls, but then again brown sugar introduces <em>pack</em> as a new parameter.</p>
<p>Two especially irreplaceable sugar vehicles come in the form of <strong>honey</strong> and <strong>maple syrup</strong>, notably for breakfast items but just as essential for pastries, coffee drinks, and cocktails.  I use honey in every vinaigrette recipe.  Maple syrup has a high warming association not replaceable with spirits or bitters. Advocating for the use of maple, like honey, requires our agricultural industries to plant more trees and plow fewer fields, creating habitat for creatures that take flight.  As a sweetener, honey is better for tea and maple for coffee.  Flipped the other way, I find the flavor combination of coffee and honey just as odd as that of a vodka and coke! <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></p>
<p>With all that said: consuming less sugar is probably a good idea.  Any modern diet requires an elimination of sugar.  I have made an effort in previous kitchens to un-crutch myself from its use, and to always explore its substitution via honey or maple or juice reductions where ever I can (this is a great way to make friends with bartenders too, by the way).  Corn syrup is mostly employed because it doesn&rsquo;t crystalize, opacify, or dye candies, but is largely unnecessary otherwise.  Now that its production is tied to the energy sector&ndash;which has made the price of an ear of corn skyrocket in the last twenty years&ndash;its use in cooking and baking is, in my opinion, best avoided altogether.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><strong>Gastriques</strong> are made by combining vinegars and caramels, and are a fantastic way to use up expiring fruit and deglazing a cooking pan (like a rondeau used to caramelize onions) that pairs with steak, pork, or producing a glazing crostini toppings.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>In <a href="https://en.wikipedia.org/wiki/Ecce_Homo_(book)"><em>Ecce Homo</em></a>, Nietzsche says he does not drink coffee because it spreads darkness, yet drinks tea but only in the morning (p. 21).  For some reason, I tend to remember that when choosing honey or maple for sauces and cocktails and it <em>sort of</em> becomes a pneumonic for me that honey goes in lighter mediums and maple syrup in darker ones.  Usually.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>The Flavor Network</title>
      <link>https://brege.org/post/the-flavor-network/</link>
      <pubDate>Wed, 04 Jan 2023 04:04:49 -0500</pubDate>
      <guid>https://brege.org/post/the-flavor-network/</guid>
      <description>This tool allows you to explore the flavor network, a social graph for flavor profiles. The network is based on the &lt;a href=&#34;https://karenandandrew.com/books/the-flavor-bible/&#34;&gt;&lt;em&gt;Flavor Bible&lt;/em&gt;&lt;/a&gt; and soon the companion book &lt;a href=&#34;https://karenandandrew.com/books/what-to-drink-with-what-you-eat/&#34;&gt;&lt;em&gt;What to Drink with What You Eat&lt;/em&gt;&lt;/a&gt;.</description>
      <content:encoded><![CDATA[




<link rel="stylesheet" href="/css/network.css">

<div id="network-title">
  <div id="recipe-link"></div>
</div>
<div id="network"
  data-nodes-path=/data/flavor/nodes.json 
  data-edges-path=/data/flavor/edges.json
  data-sim-path=/data/flavor/similarity.json
></div>
<div id="network-settings">
  <div id="physics-toggle">
    <label for="physics">
      dynamics
    </label>
    <input type="checkbox" id="physics" checked>
  </div>
  <div id="scroll-toggle">
    <label for="click-to-use">
      zoom lock
    </label>
    <input type="checkbox" id="click-to-use" checked>
  </div>
  <div id="lenses-dropdown">
    <label for="lenses" title="used to see flavor combinations by 'social circle' or 'besties'">
      lens:
    </label>
    <select id="lenses">
      <option value="similarity" title="'friends list' - see ingredients that have similar overall 'friendships' as my recipe items" selected>
        similarity
      </option>
      <option value="affinity" title="'besties' - see ingredients that are 'best friends' with my recipe items">
        affinity
      </option>
      <option value="hybrid" title="uses a mix of 'friendships' and 'besties' to see flavor combinations">
        hybrid
      </option>
    </select>
  </div>
</div>

<script src="https://visjs.github.io/vis-network/standalone/umd/vis-network.min.js"></script>
<script src="/js/flavor-network.js"></script>




<link rel="stylesheet" href="/css/search-bar.css">
<div id="searchbox">
  <div id="search-form" data-search-path=/data/flavor/nodes.json>
    <input id="search-input" autofocus placeholder="Search.." aria-label="search" type="search" autocomplete="off">
  </div>
  <div id="search-results-container" aria-label="search results"></div>
</div>
<script src="/js/search-plots.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fuse.js/3.4.6/fuse.min.js" ></script>


<p>This tool allows you to explore the flavor network, a social graph for flavor profiles.
The network is based on the
<a href="https://karenandandrew.com/books/the-flavor-bible/"><em>Flavor Bible</em></a> and soon the companion book
<a href="https://karenandandrew.com/books/what-to-drink-with-what-you-eat/"><em>What to Drink with What You Eat</em></a>.</p>
<p>Search for an ingredient you like, and the graph will refine to give you a web of ingredients that share highly similar flavor profiles.
Then, click on a new ingredient in the network to add it to your recipe above the search box (or to remove it).
Clicking on a recipe item or a node has the same effect.
Search is not sorted by the flavor metric, it is instead sorted <a href="https://fusejs.io/">lexically</a>.</p>
<p>In this way, you can start building out recipes, menu items and tastings from a consensus of flavor combinations.</p>
<h2 id="overview">Overview</h2>
<p>What you are seeing:</p>
<ul>
<li>the nodes with color are your recipe ingredients</li>
<li>the suggested ingredients are determined by <a href="https://en.wikipedia.org/wiki/Jaccard_index">Jaccard <strong>similarity</strong></a> (default) or by one of the other options in the &lsquo;<strong>lens</strong>&rsquo; dropdown</li>
<li>if you choose the <strong>hybrid</strong> option, the suggested ingredients are fiducially split between:
<ul>
<li>the most similar ingredients in the flavor metric (<strong>similarity</strong>)</li>
<li>the most similar ingredients by text ranking (<strong>affinity</strong>)</li>
</ul>
</li>
<li>the edges from one ingredient to another are weighted by a consensus of chef and expert opinion <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></li>
<li>if your ingredient is missing, it was likely missing in the book (<em>quinoa</em>) or was pruned because its mentions were too sparse <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></li>
</ul>
<p>If a node is present without an edge, it means that the ingredient has a very good similarity with your recipe, but wasn&rsquo;t mentioned (connected) in its book-entry literally.
Reconstructing &lsquo;ghost&rsquo; entries and connections by training a model with listed affinities is one of the ultimate goals of this project.</p>
<p>The amount of suggestions gradually decreases as you add more ingredients to your recipe.
This is for performance reasons, as with the physics simulation disabling itself at destabilization.
When that happens, maybe you discovered a flavor affinity.</p>
<p>I have included autogenerated links from your recipe basket to a few popular recipe resources above the network graph, including a database for cocktail mixing. <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p>
<h2 id="why-and-how">Why and how</h2>
<p>Understanding why I chose this text for the dataset is probably already apparent to its readers, but the key thing to take away is that the authors did a fine job formatting something computer readable and human usable&ndash;a rare feat!
Most importantly, it is aggregated from <em>chefs</em>, from real humans in kitchens doing what works, what&rsquo;s delicious, and what&rsquo;s in season.
Recipe API&rsquo;s don&rsquo;t have this kind of granularity, many rely too heavily on user data to seed recommendations.
To my knowledge, this is the only dataset of this kind.</p>
<p>Technical tools only involve <a href="https://visjs.org/">vis.js</a> for visualization and <a href="https://www.crummy.com/software/BeautifulSoup/bs4/doc/">BeautifulSoup</a> for parsing.
The data is scraped from the <em>Flavor Bible</em>, and the similarity matrix is calculated using <a href="https://en.wikipedia.org/wiki/Jaccard_index">Jaccard similarity</a> for <a href="https://en.wikipedia.org/wiki/Pairwise_comparison">pairwise comparisons</a>.
I am working on cleaning up the initial data with some mix of modern techniques with some concoction of
<a href="https://www.nltk.org/">nltk</a>,
<a href="https://github.com/seatgeek/fuzzywuzzy">fuzzywuzzy</a>
and/or
<a href="https://huggingface.co/docs/transformers/model_doc/bert">Bert</a>.
The current form was done entirely with regexp/bs4 parsing.
The suggested nodes can be improved by using a weighted Jaccard probability distribution (<a href="https://arxiv.org/abs/1809.04052">arXiv</a>).
Source code for this part of the calculation (the text → dataset chain) is <a href="https://github.com/brege/flavor-project">available on GitHub</a>.</p>
<h2 id="inspiration">Inspiration</h2>
<p>In 2019, I was helping fellow chefs come up with new specials.
At this point in time, we were rolling about four-ten new specials as a team every week,
ranging from brunch, cocktails, lunch, football apps and our highly anticipated farm-to-fork pop-up dinners.
But sometimes you just get plain stuck.
A good trick, at least for creativity, is to set rules so you have some boundaries to push.
But if you are going to set rules, they should at least solve a few things:</p>
<ol>
<li>do something new</li>
<li>use something old</li>
<li>feature three things in season</li>
</ol>
<p>I hate having extra stuff around, but I love new stuff coming in, yet I don&rsquo;t like wasting things, but then I actually look forward to doing inventory.  Ah, Schrodinger&rsquo;s cook.</p>
<p>Specials:  we would work out new ideas together over the prep table.
Sometimes ideas required working things out on paper, usually butcher&rsquo;s,
and occoasionally crude graphs of our plate setups evolved.
These were sketches of sauce and protein layouts, heavy edges between ideas if their pairing &lsquo;sang&rsquo;, then as a guide hanging from the ticket rail on the night a feature debuted.</p>
<p>Karen and Andrew&rsquo;s book was gifted to me later that year, and it changed my game.
It finally put in words a mental ranking of flavor profiles based on ingredient query.
I had a good resource that gave answers, and especially new ideas, quickly.
And it was thorough enough to trust.</p>
<p>This method was so helpful, I started dreaming of a computer tool to help me sketch out this process. I remembered a reddit
<a href="https://www.reddit.com/r/datasets/comments/3bxlg7/i_have_every_publicly_available_reddit_comment/">post</a>
that
<a href="https://www.reddit.com/r/dataisbeautiful/comments/ae88pk/interactive_visualization_of_related_subreddits/">spurred others</a>
to lay out some of the underlying ideas here: overlapping communities :: compatible flavors.</p>
<h2 id="broader-thoughts">Broader thoughts</h2>
<p>I believe the impact of mathematical concepts to the broader culinary scope to be a major upgrade in our thoughtfulness about food.
To extend its application, in creativity and clarity, not abused in statistics to pressure a sale and disable the <em>creative mind</em>.
While I do see how a tool like this could provide immense practical application in the distribution world, my focus here is to empower chefs, bartenders, brewers, baristas, and sommeliers to create new things.</p>
<p>When it comes to tools available to chefs,
compared to musicians, writers, and artists,
chef&rsquo;s are unfortunately at a disadvantage creatively.
Yes, we have recipes, but those are instructions, and do little to help us build on <a href="https://ruhlman.com/ruhlmans-books/">ratio</a> or <a href="https://www.saltfatacidheat.com/">balance</a>.
What might be more helpful, I think, is a playground for putting new food ideas together.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>In the book, the weight of the pairing is given by the emphasis of the text:</p>
<ul>
<li>normal text means mentioned by at least one expert</li>
<li><strong>bold</strong> is recommended by many experts</li>
<li><strong>BOLD CAPS</strong> is highly recommended</li>
<li>*<strong>BOLD CAPS</strong> is the &ldquo;Holy Grail&rdquo; of pairings</li>
</ul>
<p>If the ingredient is not mentioned, it is given no weight (or edge) but it does not mean a flavor pairing doesn&rsquo;t exist.
This is part of the purpose of this tool! Lastly, there are a few dozen mentions of &ldquo;Avoid&rdquo;, and should be thought of as opposite charges.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>If you encounter a bug, please feel free to contact me by <a href="mailto:wyatt@brege.org">email</a>
or open an issue on
<a href="https://github.com/brege/flavor-project/issues">GitHub</a>!&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>When <em>What to Drink</em> has been parsed and merged with the network, the latter link in the recipe site list should become much more robust. How fun!&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>Les Miserables</title>
      <link>https://brege.org/post/les-miserables-graph-search/</link>
      <pubDate>Sat, 24 Dec 2022 05:30:47 -0500</pubDate>
      <guid>https://brege.org/post/les-miserables-graph-search/</guid>
      <description>A network graph of character connections from one of my favorite books
and authors of all time, Victor Hugo&amp;rsquo;s Les Miserables.</description>
      <content:encoded><![CDATA[<p><em>Les Miserables is one of my favorite books.  I read most of the original translation on a train ride to Portland, OR from Chicago, IL back in 2008 and enjoyed the remainder on the return trip back East.  It taught me compassion: when Valjean places the coin in Cosette&rsquo;s shoe.  Father Christmas always misses her.  There was an earlier passage of a man stepping on a coin in front of her, while she swept dressed in rags.</em></p>
<p>The graph may take a moment to load.</p>
<p>



<style>
  #network { height: 60vh; }   
</style>

<div id="network" data-nodes-path=data/nodes.json data-edges-path=data/edges.json></div>

<script src="https://visjs.github.io/vis-network/standalone/umd/vis-network.min.js"></script>
<script src="js/lesmis-network.js"></script>



<link rel="stylesheet" href="/css/search-bar.css">
<div id="searchbox">
  <div id="search-form" data-search-path=data/nodes.json>
    <input id="search-input" autofocus placeholder="Search.." aria-label="search" type="search" autocomplete="off">
  </div>
  <div id="search-results-container" aria-label="search results"></div>
</div>
<script src="/js/search-plots.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fuse.js/3.4.6/fuse.min.js" ></script>

</p>
<p>The search bar is the major addition to the graphing methods.
Nodes can be clicked and added to a subgraph builder.
You can continue to search for new node members in the search bar
(which has a rudimentary autofill that&rsquo;s a straight json query)
and clicking on them will add them to the builder.
Simultaneously, the graph will reduce to a graph containing only
all nodes with edges linked to nodes in the builder.</p>
<p>Items can be removed from the builder either by clicking the little builder tabs or re-clicking the node.  Clearing the builder bar completely will redraw the whole graph.</p>
<p>Testing and development was done on the mini pesto data set I made for <a href="/post/what-is-pesto/">What is Pesto?</a>.  Recipe builder coming soon(!)</p>
<p>Please email me at <a href="mailto:wyatt@brege.org">wyatt@brege.org</a> with any questions.</p>
<p>Dataset can be found here:</p>
<ul>
<li><a href="data/nodes.json"><code>nodes.json</code></a></li>
<li><a href="data/edges.json"><code>edges.json</code></a></li>
</ul>
<blockquote>
<p>Lingering annoyances:</p>
<ul>
<li>Slow</li>
<li>Javascript needs clean up</li>
<li>I have great fear running this on my 700x3000 dataset..</li>
</ul>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Network Graphs with Images</title>
      <link>https://brege.org/post/network-graphs-with-images/</link>
      <pubDate>Wed, 21 Dec 2022 02:15:04 -0500</pubDate>
      <guid>https://brege.org/post/network-graphs-with-images/</guid>
      <description>A followup to the Network Graphs in Hugo post, this time with avatars for
the nodes.</description>
      <content:encoded><![CDATA[<p>This is a follow-up to the previous post <a href="/post/network-graphs-in-hugo/">Network Graphs in Hugo</a>.
I&rsquo;m feeling fruity.  These aren&rsquo;t <em>all</em> tree fruits, but a few clusters organized by tree grafting compatibility.</p>




<style>
  #mynetwork {
    background-color: #EFEBE9;  
    border-radius: 10px;
    border: 1px solid #cccccc;
    margin: 5px 0 40px 0;
  }
</style>

<div id="mynetwork" data-nodes-path=data/nodes.json data-edges-path=data/edges.json></div>

<script src="https://visjs.github.io/vis-network/standalone/umd/vis-network.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="js/fruit-network.js"></script>

<ol>
<li>
<p>Data for the network is stored in two separate JSON files in this page bundle:</p>
<ul>
<li><a href="data/nodes.json"><code>nodes.json</code></a></li>
<li><a href="data/edges.json"><code>edges.json</code></a></li>
</ul>
</li>
<li>
<p>The shortcode and post-local javascript work together:</p>
<ul>
<li><code>fruit-network.html</code></li>
<li><a href="js/fruit-network.js"><code>fruit-network.js</code></a>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl">{{ $nodesPath := .Get &#34;nodesPath&#34; }}
</span></span><span class="line"><span class="cl">{{ $edgesPath := .Get &#34;edgesPath&#34; }}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">style</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">#</span><span class="nn">mynetwork</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">background-color</span><span class="p">:</span> <span class="mh">#f5f5f5</span><span class="p">;</span> <span class="c">/* a medium gray color */</span>
</span></span><span class="line"><span class="cl">    <span class="k">border-radius</span><span class="p">:</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">border</span><span class="p">:</span> <span class="mi">1</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#cccccc</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">margin</span><span class="p">:</span> <span class="mi">5</span><span class="kt">px</span> <span class="mi">0</span> <span class="mi">40</span><span class="kt">px</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">style</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;mynetwork&#34;</span> <span class="na">data-nodes-path</span><span class="o">=</span><span class="s">{{</span> <span class="err">$</span><span class="na">nodesPath</span> <span class="err">}}</span> <span class="na">data-edges-path</span><span class="o">=</span><span class="s">{{</span> <span class="err">$</span><span class="na">edgesPath</span> <span class="err">}}</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;https://visjs.github.io/vis-network/standalone/umd/vis-network.min.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;https://code.jquery.com/jquery-3.6.0.min.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;js/fruit-network.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span></code></pre></div></li>
</ul>
</li>
</ol>
<p>This will provide network graph physics where the nodes are images (all sourced from <a href="https://www.wikipedia.org/">Wikipedia</a>. Hugo template for completeness:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">{{&lt; fruit-network nodesPath=&#34;data/nodes.json&#34; edgesPath=&#34;data/edges.json&#34; scriptPath=&#34;js/fruit-network.js&#34; &gt;}}
</span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>What is Pesto?</title>
      <link>https://brege.org/post/what-is-pesto/</link>
      <pubDate>Sun, 11 Dec 2022 16:56:56 -0500</pubDate>
      <guid>https://brege.org/post/what-is-pesto/</guid>
      <description>A minimum specification of Pesto, earstwhile its relation to other green
sauces, and my view of pesto as a classifcation schema and not a rigid
recipe.</description>
      <content:encoded><![CDATA[<p><strong>Pesto*</strong> is a cold-prep green sauce made with herbs and alliums suspended in oil.  Most pestos call for some form of nut or cheese.  In the general sense, chefs may specify &ldquo;loose pesto&rdquo; when there are neither cheese nor nuts in the sauce, or when the sauce has an abundance of oil.  In general, pesto is made from herbs in the mint family, almost always from basil. The <strong>*</strong> distinguishes a generalization to the traditional meaning of the word.</p>
<h2 id="classification">Classification</h2>
<p><strong>Basil pesto</strong> is the common pesto.  Its recipe typically calls for five key ingredients:
$$
\begin{aligned}
\textrm{basil pesto} = \textrm{basil} &amp;+ \textrm{extra virgin olive oil} + \textrm{garlic} \\
&amp;+ \textrm{parmesan cheese} + \textrm{pine nuts}
\end{aligned}
$$</p>
<p>Chimichurri includes a vinegar, usually red wine vinegar, which pesto does not, although pesto can suspend acid through citrus.  Chimichurri often uses rough chopped or ground herbs, almost always oregano and parsley, but some modern applications will puree chimichurri as one does with pesto.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>Pesto is also different from gremolata, which like chimi is also made with parsley, so you may refer to carrot-family herb sauces as either gremolatas or chimis, and mint-family herb sauces as pestos, and chefs will know what you mean, even if they don&rsquo;t quite agree with the abstractions.</p>
<p>A table is provided as a summary of these three sauce&rsquo;s distinctions.</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Pesto</th>
          <th>Gremolata</th>
          <th>Chimichurri</th>
          <th><em>Spring Sauce</em></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Herb*</td>
          <td>Basil</td>
          <td>Parsley</td>
          <td>Parsley + Oregano</td>
          <td><em>Mint + Peas</em></td>
      </tr>
      <tr>
          <td>Oil</td>
          <td>Extra Virgin</td>
          <td>Extra Virgin</td>
          <td>Extra Virgin</td>
          <td><em>Walnut Oil</em></td>
      </tr>
      <tr>
          <td>Allium</td>
          <td>Garlic</td>
          <td>Garlic</td>
          <td>Garlic</td>
          <td><em>Green Onion</em></td>
      </tr>
      <tr>
          <td>Nut</td>
          <td>Pine nut</td>
          <td>-</td>
          <td>-</td>
          <td><em>Pistachio</em></td>
      </tr>
      <tr>
          <td>Acid</td>
          <td>-</td>
          <td>Citrus</td>
          <td>Red Wine Vinegar</td>
          <td><em>Pomelo</em></td>
      </tr>
      <tr>
          <td>Wow</td>
          <td>Parmesan</td>
          <td>n/a</td>
          <td>Chili Flakes</td>
          <td><em>Chevre</em></td>
      </tr>
  </tbody>
</table>
<blockquote>
<p>&ldquo;Green Sauces.  The author made spring sauce up, but it&rsquo;s very good on crostini.&rdquo;</p>
</blockquote>
<p>But there is a small catch, because some chefs may also say &lsquo;arugula pesto,&rsquo; which might have no mint-family herb mixed in.  This leafy sauce resembles pesto in process and appearence, and provides the effect of coating pastas in bright green colors, especially when suspended in stablized fat like reduced cream or mayonaisse.  A chef can employ arugula for its liquorice and peppercorn aspects, or baby spinach which adds tang (like nori) when cooked, but she will most likely apply it because of its food coloring properties (vibrancy).  Similarly, &lsquo;spring pea pesto&rsquo; is made with a puree of spring peas and mint, and on crostini is a fantastic crudité.</p>
<p>In this way, <strong>pesto*</strong> is generalized from its elements, describing a sauce of process and appearence while flexible in its flavor balancing rules.  This is pesto* as a <strong>category</strong>, not pesto as a set.  For all intents and purposes, chimis and gremolatas are pestos*.</p>
<h2 id="ingredients">Ingredients</h2>
<p>The key to most pestos is to use a motor driven device for pureeing the sauce.
The two best tools available and in common use both in commericial and residential kitchens are the <strong>vitamix</strong> or <strong>immersion blender</strong>.  Food processors and ninjas work just as well.</p>
<blockquote>
<p><strong>Hint:</strong> always add cheese last</p>
</blockquote>
<p>Puree everything besides cheese first.  You don&rsquo;t want to gum up your blender, so you can fold the proto-pesto with grated parmesan in a mixing bowl after pureeing the produce.  In restaurants, you can make a really fine proto-pesto (i.e., loose pesto) by pureeing the produce and oil for an extended period of time.  It is easier on the motor and on the blades.  You can be lazier picking your leaves on your basil or include more stems, because the cheese absent creates less friction.  <strong>Save the cheese for last.</strong></p>
<h3 id="greens">Greens</h3>
<p>Most herbs from either the carrot- or mint-families will work great, and many green tops of root vegetables (turnip, radish) are, while not interchangeable, usable and often complimentary in pestos.
Caution: while tarragon can be made into a pesto*, its deployment in cold-prep sauces is rare (marinades, green goddess dressing) and is typically utilized better in hot-prep sauces&ndash;especially béarnaise. Sage, rosemary, and lemon verbena require special practice.  Thyme is time.
Use edible garnishes. Keep away from the ornamental &ldquo;perfumes&rdquo;.</p>
<blockquote>
<p><strong>Recommend:</strong> basil, mint, parsley, oregano, marjoram, arugula<br>
<strong>Avoid:</strong> lavendar, tarragon, lemon verbena, rosemary</p>
</blockquote>

 
 
 <div id="mynetwork" data-nodes-path=data/nodes.json data-edges-path=data/edges.json></div>
 
 <script src="https://visjs.github.io/vis-network/standalone/umd/vis-network.min.js"></script>
 <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
 
 <script>
   
   const nodeStyles = {
     nodes: {
       color: {
         background: '#eaeaea'
       },
       shape: 'box',
       shapeProperties: {
 	      borderRadius: 3
       }
     }
   };
 
   function highlightNodes(matchingNodes) {
     
     matchingNodes.forEach(node => {
       $(`#${node.id}`).css('background-color', '#add8e6');
     });
   }
 
   
   const options = {
     width: "100%",
     height:   `${window.innerHeight * 0.6}px`,
   };
 
   
   const container = document.getElementById("mynetwork");
   const network = new vis.Network(container, {}, options);
 
   
   const nodesPath = $("#mynetwork").data("nodes-path");
   const edgesPath = $("#mynetwork").data("edges-path");
 
   
   $.getJSON(nodesPath, function(nodesData) {
     $.getJSON(edgesPath, function(edgesData) {
       
       const data = {
       	nodes: nodesData,
 	      edges: edgesData
       };
 
       
       network.setData(data);
       
       network.setOptions(nodeStyles);
 
       
       const nodes = data.nodes;
 
       network.on('select', function(params) {
         const matchingNodes = nodes.filter(node => selectedResults.includes(node.label));
           highlightNodes(matchingNodes);
       });
     });
   });
 
 </script>

<h3 id="cheese">Cheese</h3>
<p>Try to stick with a hard cheese that has longer room temperature stability.  Cheeses with hard interiors and developed rinds will impart the necessary fats and impactful aromas while also being more conservative in the funky bleu cheeses, chalky goat cheeses, or sour yet creamy compositions of farmer/cottage style cheeses&ndash;namely, ricotta, cotija or paneer. An old queso fresco that has been air dried in the fridge, however, is a mighty substitute to parmesan on a budget.</p>
<blockquote>
<p><strong>Recommend:</strong> parmesan, hard rind, feta, crumbly<br>
<strong>Avoid:</strong> soft, bleu</p>
</blockquote>
<h3 id="citrus">Citrus?</h3>
<p>Of course, pesto is also seasoned (sodiums, peppercorn) and lemon zest or lemon juice is an especially common addition.  Adding citrus tunes flavor, because it combats salinity (parmesan saltiness is not equal between products) and piquancy (garlic cloves grown in drought can be remarkably &lsquo;hot&rsquo;).  Because lemons are a hybrid of pomelo and citron, grapefruit juice is an equally applicable substitute.</p>
<blockquote>
<p><strong>Recommend:</strong> lemon zest, grapefruit juice, kombucha vinegars<br>
<strong>Avoid:</strong> orange, distilled vinegars</p>
</blockquote>
<h3 id="nuts">Nuts</h3>
<p>Pine nuts are the nut of choice in recipes from canonical sources.  People don&rsquo;t have pine  nuts in their house.  They do have other nuts leftover from baked good preperations.  In restaurants, pine nuts are toasted in hot ovens.  They are applied whole to pizzas and salads, so because they can live in a recipe and two auxillary dishes, they define themselves as roleplayers in menus and inventories.  However, pine nuts are expensive.  Because they are normally toasted, errant chefs may leave them in ovens for moments too long, destroying a product whose cost is equivalent to whole numbers of the average hourly wages.  This is not good for anyone.  I&rsquo;ve never done this before, I swear.</p>
<p>Instead, some nontropical tree nuts are immediate substitutes if you are concerned only with the context of pesto: <em>walnut,</em> <em>almond,</em> and <em>pistachio</em>.  This does not mean cashews are off limits, nor peanuts.  Consider  the role they play with basil and different oils in Thai cuisine.  But maybe we don&rsquo;t often think of peanut butter and parmesan together, so we avoid peanuts in pesto.  That doesn&rsquo;t mean you are violating international treaties by substituting deluxed mixed nuts from the grocery store in place of pine nuts.  Pine nuts are the most replaceable element of classical pesto.</p>
<blockquote>
<p><strong>Recommend:</strong> pine nut, walnut, almond, pistachio<br>
<strong>Avoid:</strong> peanut, cashew, macadamia, brazil, pecan</p>
</blockquote>
<h3 id="oils">Oils</h3>
<p>It is by no coincidence that extra virgin olive oil is the go to substrate (notice also olives and the alternative nuts previously mentioned grow adjacent to each other).  Its Mediterranean origin is the required consideration for almost all member dishes in its cuisine.  Some oils are better than others when olive oil is out: avocado, walnut, grapeseed.  Avoid soy and canola oils which are better suited for frying, and harm basil less in lite applications like salad (Italian) dressing.  Coconut oil can be used, but is not ideal because of its high melting point.  However, you can create a spreadable pesto by using coconut oil&ndash;if the proto-pesto wets are tempered into coconut oil carefully.  Coarser nuts in the spread make for a provocative but health-conscious party favor.</p>
<blockquote>
<p><strong>Recommend:</strong> extra virgin, walnut, avocado<br>
<strong>Avoid:</strong> canola, soy, peanut</p>
</blockquote>
<h3 id="alliums">Alliums</h3>
<p>Lastly, we generalized garlic to alliums above for good reason.  You can make garlic scape pesto, which uses the green tops grown above earth from the garlic bulb.  But, you can also use spring onions and/or shallot bulbs in place of garlic, which is especially more pleasant with lakefish and seafood.  Although, garlic <em>is</em> preferable to onion in the regime of shellfish and, notably, the mollusk.  You can also use radish bulbs, as well as radish or mustard greens in pesto, which simulates &ldquo;bite&rdquo;.  In place of garlic, this is a quite useful substitution when pestos are applied in dressings for leafy salads or pasta salads.</p>
<blockquote>
<p><strong>Recommend:</strong> fresh garlic, young onions, shallot, some pickled<br>
<strong>Avoid:</strong> large onions, leek, pearl onion, cooked garlic</p>
</blockquote>
<h2 id="recipe">Recipe</h2>
<p>I&rsquo;ve included a basic production recipe for <em>Basil Pesto</em> that creates a structurally &ldquo;loose&rdquo; pesto.  My application could be added to heavy mayo for basil aioli, brushed on bread for grilling, and thickened with reduced (50%) cream for penne pasta dishes (bechamel cheese sauces work there, too).
Cheese plays a more vital role when pesto is a topping where the goal is to gratinize, or in the presence of crackers.</p>
<div class="recipe border rounded is-embedded">
  <div class="content">
    <h2 id="basil-pesto">Basil Pesto</h2>
<p><strong>Chef:</strong> Wyatt Brege</p>
<p><strong>Yield:</strong> 1 Quart</p>
<h3 id="ingredients">Ingredients</h3>
<ul>
<li>2 Bunch Basil</li>
<li>8-10 clove Peeled Garlic</li>
<li>3 Cup Extra Virgin Olive Oil</li>
<li>1/4 Cup Seasonal Tree Nuts (shelled whole)</li>
<li>1/4 Cup Grated Parmesan</li>
<li>tt Salt and Black Pepper</li>
</ul>
<h3 id="directions">Directions</h3>
<p>In a medium sized mixing bowl, combine the ingredients (sans parmesan)
with the immersion blender.
Fold in the parmesan.
Store in 1 Quart deli container.</p>
<h3 id="notes">Notes</h3>
<p>It is important to use actual extra virgin olive oil and not pomace or
salad oil. The bite of the olive is a key flavor component of pesto.</p>

  </div>
</div>

<h2 id="what-is-sauce">What is sauce?</h2>
<p>When you play this game of generalizing sauces into irreducible primitives, you eventually question with good reason why the five mother sauces are the way that they are.</p>
<p>If postulated today, there would no doubt be endless criticism of its organization, lack of other fundamentally different sauces (gastriques?), exclusion of other world foods, and few would be convinced of any universality.
But Escoffier&rsquo;s postulate has historical importance defining the role of these hot-held sauces in a greater culinary scope. Less valuable now as theory, it is more so enforced as a performance lesson: a chef must learn how to make the mother sauces to be the best in her trade.</p>
<p>If you were to reduce every cold sauce into fundamentals (vinaigrette, aioli, coulis, etc), to what extent can you?
There is at least some argmunent: the preperation does require special work.
While the heated sauces have an additional thermal parameter (&lsquo;careful not to scald, to break..&rsquo;), the cold sauces spend energy kinetically: emulsify, puree, grind (&lsquo;careful not to overwhip, to break..&rsquo;).
In this sense, attention isn&rsquo;t voluntary, so it separates itself from chopping, fermenting, or stirring to mix, giving an argument of practicality some teeth in the need for order.  No, Ranch is not a mother cold sauce, but mayo does require special work and <em>is</em> an elemental part of Ranch.</p>
<p>In my mind, I do consider some categories of cold sauces as fundamental to the education of the garde (those mentioned, probably closer to seven in total).
So with liberty, let&rsquo;s for sake of argument hold salsas and pestos in different regard.  I cannot speak to the genre it belongs, but: if you have never tried blending tomatillo and basil together, you are missing out on one of life&rsquo;s best kept secrets.</p>
<div class="recipe border rounded is-embedded">
  <div class="content">
<h3 id="on-the-fly">On the fly</h3>
<ul>
<li>Arugula, Mint, Pea pesto $\approx$ pesto, sub herb</li>
<li>Gremolata $\sim$ pesto, sub herb, add citrus, minus cheese, minus nuts</li>
<li>Chimichurri $\sim$ gremolata, with vinegar, with chiles</li>
</ul>
  </div>
</div>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I often wonder what foods may have begun as mistakes, or because other times someone just had to make do.  Chimi mixed with pesto, then subtended with more red wine vinegar, leads to something quite close to Italian dressing.  Maybe this was an accident once, or maybe a quick hack.  Turning salvage and hacking into a creation presents a trade uniquely as an art.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>Network Graphs in Hugo</title>
      <link>https://brege.org/post/network-graphs-in-hugo/</link>
      <pubDate>Fri, 09 Dec 2022 23:02:42 -0500</pubDate>
      <guid>https://brege.org/post/network-graphs-in-hugo/</guid>
      <description>First crack at making a simple toy network graph in Hugo.</description>
      <content:encoded><![CDATA[<p>This is a simple toy to see how a network graph can be added in a Hugo article.  I&rsquo;ll be testing new features on it as I learn new things.</p>




<div id="mynetwork" data-nodes-path="data/nodes.json" data-edges-path="data/edges.json">
  <script src="https://visjs.github.io/vis-network/standalone/umd/vis-network.min.js"></script>
  <script src="js/toy-network.js"></script>
</div>

<p>Relative to the root of the Hugo website directory, here&rsquo;s some basic files to make this interactive.
Note that The JSON data and CSS is added inline here to make the scope of this tutorial focus on Hugo-specific structures.</p>
<ol>
<li>
<p>The javascript file lives in this page bundle:</p>
<ul>
<li><a href="js/toy-network.js"><code>toy-network.js</code></a></li>
</ul>
</li>
<li>
<p>This file accesses data for the nodes and edges from two JSON files in this page bundle:</p>
<ul>
<li><a href="data/nodes.json"><code>nodes.json</code></a></li>
<li><a href="data/edges.json"><code>edges.json</code></a></li>
</ul>
</li>
<li>
<p>In the shortcodes directory <code>/layouts/shortcodes/</code>:</p>
<ul>
<li><code>toy-network.html</code>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;mynetwork&#34;</span> <span class="na">data-nodes-path</span><span class="o">=</span><span class="s">&#34;data/nodes.json&#34;</span> <span class="na">data-edges-path</span><span class="o">=</span><span class="s">&#34;data/edges.json&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;https://visjs.github.io/vis-network/standalone/umd/vis-network.min.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;js/toy-network.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span></code></pre></div></li>
</ul>
</li>
<li>
<p>Do the normal way of making a post in Hugo, but invoke the shortcode within the body of your markdown:</p>
<ul>
<li><code>index.md</code>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">{{&lt; toy-network nodesPath=&#34;data/nodes.json&#34; edgesPath=&#34;data/edges.json&#34; scriptPath=&#34;js/toy-network.js&#34; &gt;}}
</span></span></code></pre></div></li>
</ul>
</li>
</ol>
<p>This will provide the simple network graph above.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Helpful Red Dead Redemption 2 Checklists</title>
      <link>https://brege.org/post/helpful-red-dead-redemption-2-checklist/</link>
      <pubDate>Sun, 06 Feb 2022 02:12:20 -0400</pubDate>
      <guid>https://brege.org/post/helpful-red-dead-redemption-2-checklist/</guid>
      <description>A compact, printable Google Drive spreadsheet for 100% Completion in
Rockstar&amp;rsquo;s Red Dead Redemption 2, including all missable items, compendia,
recipes, and more.</description>
      <content:encoded><![CDATA[<p>I&rsquo;ve put together a Google Drive spreadsheet for the avid Red Dead Redemption 2 completionist.  It&rsquo;s almost a 100% checklist, but mostly useful as a way to keep track of progress that plays nice in both print and mobile form.</p>
<p><a href="https://docs.google.com/spreadsheets/d/1wBuPPvN-Y4K115rBQrOP559tG2oCTfUjTCTBgZSJwRA/edit?usp=sharing">&#x1f517; RDR2 Completionist Checklist (Google Sheets)</a></p>
<p><img alt="png" loading="lazy" src="/post/helpful-red-dead-redemption-2-checklist/img/screenshot-1.png"></p>
<blockquote>
<h3 id="2025-note-on-google-drivesheets">2025 Note on Google Drive/Sheets</h3>
<p><strong>In order for you to edit this, you need to:</strong></p>
<ol>
<li>Open the spreadsheet in <a href="https://docs.google.com/spreadsheets/d/1wBuPPvN-Y4K115rBQrOP559tG2oCTfUjTCTBgZSJwRA/edit?usp=sharing">&#x1f517; <strong>Google Sheets</strong></a> while <strong>signed in</strong> with your account</li>
<li><strong>File &gt; Make a copy</strong></li>
</ol>
<p>This will add a completely editable copy in your Google Drive. I&rsquo;ve gotten a lot of requests to edit this spreadsheet!</p>
<p><em>If you do not have a Google account</em>: you can use this <a href="./data/rdr2-checklist.pdf"><strong>PDF version</strong></a> instead. On my last playthrough, I made sure the page breaks were well placed so I could print them out and mark them off as I played.</p>
</blockquote>
<p>While not a replacement by any means for the various user made maps and guides out there, I wanted everything consolodated in a form similar to what I had already been doing with a little notebook while I played.</p>
<ul>
<li><strong>Hunting</strong>: these are animals that you need for the Trapper, Mr. Pearson, Ms. Hobbs, and the legendaries.  Daggers † are reserved for the epilogue, officially, and have a grey background.</li>
<li><strong>Plants + Fish</strong>: combined on the same sheet so they can be printed together.  Also, there&rsquo;s some markers in there that indicate challenges, recipe usage/bait types, and some other information available :on hover:</li>
<li><strong>Horses</strong>: this was actually a major pain to put together! With Rockstar updates and online content being incosistent, there is a definite need out there for these to be consolodated and made tidy.</li>
<li><strong>Missables</strong>: sorted first by character; this includes:
<ul>
<li>camp requests</li>
<li>companion activities</li>
<li>missable compendium weapons and &ldquo;unique item&rdquo; satchel contents</li>
</ul>
</li>
<li><strong>Collectables</strong>: a grid to help demarcate collectable discovery (again, some have grey backgrounds if inaccesible for much of the story)</li>
<li><strong>Weapons</strong>: see key for more details</li>
</ul>
<p><img alt="png" loading="lazy" src="/post/helpful-red-dead-redemption-2-checklist/img/rdr2-sample-checklist.png"></p>
<p>This guide can be used to checkoff <em>discovered</em> recipes and items, avoiding purchase from the fence, but mostly to fully complete the compendium (and sometimes it takes too many clicks and scrolls in the pause menu).  Items that are <strong>bolded</strong> are uniquely obtained, some of which are missable, or can be sold and never obtained again.</p>
<p>There are page breaks to make printing easier.  Saving a copy of the Google Sheet will also allow you to use it on your phone quite well.  Or, here&rsquo;s the full
<a href="./data/rdr2-checklist.pdf"> PDF Checklist &#x2b07;&#xfe0f; </a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Hockey Catch-all Statistics versus Salary Cap</title>
      <link>https://brege.org/post/hockey-fgvt-and-salary-caps/</link>
      <pubDate>Tue, 07 Nov 2017 11:11:52 -0800</pubDate>
      <guid>https://brege.org/post/hockey-fgvt-and-salary-caps/</guid>
      <description>&lt;p&gt;This project &lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; is motivated by the &amp;ldquo;&lt;a href=&#34;https://en.wikipedia.org/wiki/Wins_Above_Replacement&#34;&gt;WAR&lt;/a&gt;&amp;rdquo; stat in baseball, where I have adopted the &amp;ldquo;Goals vs. Threshold&amp;rdquo; (GVT) statistic from &lt;a href=&#34;https://web.archive.org/web/20130407214751/http://hockeyprospectus.com/article.php?articleid=236&#34;&gt;Tom Awad&lt;/a&gt;.  Here, I only consider the Offensive GVT for forward skaters and defensemen (OGVT).&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>This project <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> is motivated by the &ldquo;<a href="https://en.wikipedia.org/wiki/Wins_Above_Replacement">WAR</a>&rdquo; stat in baseball, where I have adopted the &ldquo;Goals vs. Threshold&rdquo; (GVT) statistic from <a href="https://web.archive.org/web/20130407214751/http://hockeyprospectus.com/article.php?articleid=236">Tom Awad</a>.  Here, I only consider the Offensive GVT for forward skaters and defensemen (OGVT).</p>
<p>I take as input the spreadsheet provided by <a href="http://www.hockeyabstract.com/testimonials/nhl2016-17playerdata">Robert Vollman</a>, which has not been updated with GVT data yet.  I made minor modifications to his spreadsheet in LibreOffice Calc to make it export to the CSV file format well.   The code calculates OGVT by player, which is weighted against his own team&rsquo;s Threshold Offensive Contribution by forwards ($TOC_F$), or defensemen ($TOC_D$), per minute, rather than league wide.</p>
<p>To get an estimate of how good a goal is compared to an assist, we estimate that a goal scored contributes 1.5 times as much as an assist contributes to a goal.  Therefore, the calculated goal value (or assist) scored by an entity $x$ is
$$
\begin{aligned}
GV_x &amp;= \frac{1.5 G_x}{A_x + 1.5 G_x}, \\
AV_x &amp;= \frac{GV_x}{1.5}
\end{aligned}
$$
where $G_x$ is goals scored by either an individual, $x=i$, team, $x=T$, or the league as a whole, $x=L$, and $A_x$ are the assists scored by those subcategories.</p>
<p>The total offensive contribution of all forwards, $TOC_F$, is determined by</p>
<p>$$ TOC_F = \frac{\sum_{f \in T} G_f \times GV_T + A_f \times AV_T}{\sum_{f \in T} MP_f} \times OTV$$
where $MP_f$ is the minutes by forward, and the offensive threshold value is $OTV = 0.75$ via Tom Awad or $0.58$ via Alan Ryder (I chose the former).  I chose an uppercase $F$ so that one may distinguish this value, which applies to <em>all</em> forwards on the team, from an individual forward, $f$.</p>
<p>The final formula to calculate $OGVT$ for each forward $f$ is, according to Awad, then
$$
OGVT = G_f \times GV_f + A_f \times AV_f - MP_f \times TOC_F
$$</p>
<p>Additionally, I wanted to get a sense for one player&rsquo;s value to the team  in relation to his salary cap hit.  Here, I show from the 2016-17 NHL regular season $OGVT$ versus Salary Cap for the Stanley Cup Champion Pittsburgh Penguins, the cap-troubled Detroit Red Wings, and the young Edmonton Oilers with generational talent Connor McDavid (only forward skaters).</p>


<div id="scatterplot" data-csv-path="data/NHL_PIT-DET-EDM_OGVT_2016-17.csv"></div>
<style> 
#scatterplot {
  margin: 10px 0;
  width: 100%;
  height: 500px;
  background: var(--code-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem;
}

</style>
<script type="text/javascript" src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script> 
<script type="text/javascript" src="/js/scatterplot.js"></script>

<p>However, in debugging my code, something seemed strange to me.  This first term in the $OGVT$ expression, with some math, reduces to the number of goals by that individual:
$$
\begin{aligned}
G_f \times GV_f + A_f \times AV_f
&amp;= G_f \times GV_f + A_f \times \frac{GV_f}{1.5} \\
&amp;= \left( G_f  + \frac{A_f}{1.5}\right )
\times GV_f \\
&amp;= \left( G_f  + \frac{A_f}{1.5}\right )
\times \left( \frac{1.5 G_f}{A_f + 1.5 G_f} \right) \\
&amp;= \left( 1.5 G_f  + A_f \right)
\times \left( \frac{G_f}{A_f + 1.5 G_f} \right) \\
&amp;= G_f.
\end{aligned}
$$
So, unless I&rsquo;m misunderstanding Tom Awad&rsquo;s definition of terms here:</p>
<blockquote>
<p>A player&rsquo;s OGVT is therefore:</p>
<p>OGVT = (G x GV) + (A x AV) - (MP x TOC)</p>
<p>Where G is the player&rsquo;s goals, A his assists, MP his minutes played, GV his goal value, AV his assist value, and TOC the Threshold offensive contribution value for his position.</p>
</blockquote>
<p>I don&rsquo;t quite understand how this first set of terms is relevant, as it essentially removes the direct value of a skater&rsquo;s assists in the calculation of this catch-all offensive statistic.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Actually, I mostly wanted to get some experience with <a href="https://d3js.org/">D3</a> and using publically accesible data.  I&rsquo;m still investigating why the axes titles aren&rsquo;t showing on my plot.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>]]></content:encoded>
    </item>
    <item>
      <title>Updated Nginx configuration with Let&#39;s Encrypt headers</title>
      <link>https://brege.org/post/updated-nginx-conf-with-letsencrypt-headers/</link>
      <pubDate>Mon, 03 Apr 2017 15:06:38 -0700</pubDate>
      <guid>https://brege.org/post/updated-nginx-conf-with-letsencrypt-headers/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve added a new &lt;code&gt;security_headers.conf&lt;/code&gt; file in &lt;code&gt;/etc/nginx/&lt;/code&gt; to keep all the HTTPS headers in one place:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I&rsquo;ve added a new <code>security_headers.conf</code> file in <code>/etc/nginx/</code> to keep all the HTTPS headers in one place:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl">    <span class="k">ssl_session_timeout</span> <span class="s">1d</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">ssl_session_cache</span> <span class="s">shared:SSL:50m</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">ssl_session_tickets</span> <span class="no">off</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># intermediate configuration. tweak to your needs.
</span></span></span><span class="line"><span class="cl">    <span class="k">ssl_protocols</span> <span class="s">TLSv1</span> <span class="s">TLSv1.1</span> <span class="s">TLSv1.2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">ssl_ciphers</span> <span class="s">&#39;ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">ssl_prefer_server_ciphers</span> <span class="no">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># to score better grades on
</span></span></span><span class="line"><span class="cl">    <span class="c1">#  https://observatory.mozilla.org/analyze.html?host=brege.org
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">add_header</span> <span class="s">Strict-Transport-Security</span> <span class="s">&#34;max-age=15768000</span><span class="p">;</span> <span class="k">includeSubDomains</span><span class="p">;</span> <span class="k">preload&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">add_header</span> <span class="s">X-Frame-Options</span> <span class="s">&#34;SAMEORIGIN&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">add_header</span> <span class="s">X-Content-Type-Options</span> <span class="s">nosniff</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">add_header</span> <span class="s">X-XSS-Protection</span> <span class="s">&#34;1</span><span class="p">;</span> <span class="k">mode=block&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">add_header</span> <span class="s">X-Permitted-Cross-Domain-Policies</span> <span class="s">none</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">add_header</span> <span class="s">Content-Security-Policy</span> <span class="s">&#34;default-src</span> <span class="s">&#39;self&#39;</span><span class="p">;</span><span class="k">&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">add_header</span> <span class="s">Referrer-Policy</span> <span class="s">&#34;no-referrer,</span> <span class="s">strict-origin-when-cross-origin&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># OCSP Stapling ---
</span></span></span><span class="line"><span class="cl">    <span class="c1"># fetch OCSP records from URL in ssl_certificate and cache them
</span></span></span><span class="line"><span class="cl">    <span class="k">ssl_stapling</span> <span class="no">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">ssl_stapling_verify</span> <span class="no">on</span><span class="p">;</span>
</span></span></code></pre></div><p>We include the headers we obtained in the <a href="/post/getting-an-a-plus-on-mozilla-observatory/">last post</a> in here as well.  The main configuration file <code>/etc/nginx/brege.org</code> contains:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span> <span class="s">*.brege.org</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">return</span> <span class="s">http://brege.org/</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="mi">80</span> <span class="s">default_server</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="s">[::]:80</span> <span class="s">default_server</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span>  <span class="s">brege.org</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># enforce https
</span></span></span><span class="line"><span class="cl">    <span class="kn">return</span> <span class="mi">301</span> <span class="s">https://</span><span class="nv">$host$request_uri</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">if</span> <span class="s">(</span><span class="nv">$scheme</span> <span class="s">!=</span> <span class="s">&#34;https&#34;)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">return</span> <span class="mi">301</span> <span class="s">https://</span><span class="nv">$host$request_uri</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="c1"># managed by Certbot
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="mi">443</span> <span class="s">default</span> <span class="s">ssl</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="s">[::]:443</span> <span class="s">default</span> <span class="s">ssl</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">root</span> <span class="s">/usr/share/nginx/brege.org</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">index</span> <span class="s">index.html</span> <span class="s">index.htm</span> <span class="s">index.php</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span> <span class="s">brege.org</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">ssl_certificate</span> <span class="s">/etc/letsencrypt/live/brege.org/fullchain.pem</span><span class="p">;</span> <span class="c1"># managed by Certbot
</span></span></span><span class="line"><span class="cl">    <span class="kn">ssl_certificate_key</span> <span class="s">/etc/letsencrypt/live/brege.org/privkey.pem</span><span class="p">;</span> <span class="c1"># managed by Certbot
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">include</span> <span class="s">security_headers.conf</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">include</span> <span class="s">letsencrypt.conf</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">location</span> <span class="s">/</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># First attempt to serve request as file, then
</span></span></span><span class="line"><span class="cl">        <span class="c1"># as directory, then fall back to displaying a 404.
</span></span></span><span class="line"><span class="cl">        <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri/</span> <span class="p">=</span><span class="mi">404</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">location</span> <span class="p">~</span> <span class="sr">^/(?:feed|feeds|rss)</span>  <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">return</span> <span class="mi">301</span> <span class="s">/index.xml</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Note that first server block does not work as expected.  On a separate instance, I tested this and confirmed that <a href="http://abc.mysite.org">http://abc.mysite.org</a> does get redirected to <a href="https://mysite.org">https://mysite.org</a>, but I think that since my site has been <a href="https://hstspreload.org/?domain=brege.org">HSTS Preloaded</a>, this doesn&rsquo;t allow <a href="http://abc.brege.org">http://abc.brege.org</a> to redirect to <a href="https://brege.org">https://brege.org</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>Getting an A&#43; on Mozilla&#39;s HTTP Observatory</title>
      <link>https://brege.org/post/getting-an-a-plus-on-mozilla-observatory/</link>
      <pubDate>Mon, 09 Jan 2017 18:29:50 -0500</pubDate>
      <guid>https://brege.org/post/getting-an-a-plus-on-mozilla-observatory/</guid>
      <description>&lt;p&gt;After I learned about &lt;a href=&#34;https://github.com/mozilla/http-observatory&#34;&gt;Mozilla&amp;rsquo;s tool&lt;/a&gt; to test how secure your site is, I ran it on my site &lt;a href=&#34;https://observatory.mozilla.org/analyze.html?host=brege.org&#34;&gt;https://observatory.mozilla.org/analyze.html?host=brege.org&lt;/a&gt; and received an &amp;ldquo;&lt;strong&gt;F&lt;/strong&gt;&amp;rdquo;.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>After I learned about <a href="https://github.com/mozilla/http-observatory">Mozilla&rsquo;s tool</a> to test how secure your site is, I ran it on my site <a href="https://observatory.mozilla.org/analyze.html?host=brege.org">https://observatory.mozilla.org/analyze.html?host=brege.org</a> and received an &ldquo;<strong>F</strong>&rdquo;.</p>
<p>After some trial &amp; error and searching around, I came up with the following to be placed in my Nginx <code>server{...}</code> block:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl">    <span class="k">add_header</span> <span class="s">Strict-Transport-Security</span> <span class="s">&#34;max-age=15768000</span><span class="p">;</span> <span class="k">includeSubDomains</span><span class="p">;</span> <span class="k">preload&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">add_header</span> <span class="s">X-Frame-Options</span> <span class="s">&#34;SAMEORIGIN&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">add_header</span> <span class="s">X-Content-Type-Options</span> <span class="s">nosniff</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">add_header</span> <span class="s">X-XSS-Protection</span> <span class="s">&#34;1</span><span class="p">;</span> <span class="k">mode=block&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">add_header</span> <span class="s">X-Permitted-Cross-Domain-Policies</span> <span class="s">none</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">add_header</span> <span class="s">Content-Security-Policy</span> <span class="s">&#34;default-src</span> <span class="s">&#39;self&#39;</span><span class="p">;</span><span class="k">&#34;</span><span class="p">;</span>
</span></span></code></pre></div><p>Now I am receiving an &ldquo;<strong>A+</strong>&rdquo; from the observatory!</p>
<p>I also went to <a href="https://hstspreload.org/">Google&rsquo;s submission page</a> to have your site put on the <a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security">HSTS</a> preloaded list, so that all users of Google Chrome access your site through HTTPS by default.</p>]]></content:encoded>
    </item>
    <item>
      <title>New way forward for deploying brege.org</title>
      <link>https://brege.org/post/new-way-forward-for-deploying-brege.org/</link>
      <pubDate>Tue, 18 Oct 2016 16:35:01 -0700</pubDate>
      <guid>https://brege.org/post/new-way-forward-for-deploying-brege.org/</guid>
      <description>&lt;p&gt;ssh&amp;rsquo;ing into my Digital Ocean droplet has become rather annoying for maintaining this website.  Particularly annoying is dealing with images on two different filesystems.  In the &lt;a href=&#34;https://github.com/brege/brege.org/blob/master/README.md&#34;&gt;README&lt;/a&gt; (commit &lt;a href=&#34;https://github.com/brege/brege.org/commit/1a0ee5a1a946bf5ee574a4593e4e6b22d35607e3&#34;&gt;1a0ee5a&lt;/a&gt;), I describe the steps I have been doing to publish an article to &lt;a href=&#34;https://brege.org&#34;&gt;https://brege.org&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I tried implementing &lt;a href=&#34;https://www.digitalocean.com/community/tutorials/how-to-deploy-a-hugo-site-to-production-with-git-hooks-on-ubuntu-14-04&#34;&gt;this method&lt;/a&gt;, but it is very long and requires many modifications to the post-receive hook to get it working on a per-user basis.&lt;/p&gt;
&lt;p&gt;Here I outline a way to do this in a more conservative manner.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>ssh&rsquo;ing into my Digital Ocean droplet has become rather annoying for maintaining this website.  Particularly annoying is dealing with images on two different filesystems.  In the <a href="https://github.com/brege/brege.org/blob/master/README.md">README</a> (commit <a href="https://github.com/brege/brege.org/commit/1a0ee5a1a946bf5ee574a4593e4e6b22d35607e3">1a0ee5a</a>), I describe the steps I have been doing to publish an article to <a href="https://brege.org">https://brege.org</a>.</p>
<p>I tried implementing <a href="https://www.digitalocean.com/community/tutorials/how-to-deploy-a-hugo-site-to-production-with-git-hooks-on-ubuntu-14-04">this method</a>, but it is very long and requires many modifications to the post-receive hook to get it working on a per-user basis.</p>
<p>Here I outline a way to do this in a more conservative manner.</p>
<h2 id="installation-on-development-machine">Installation on development machine</h2>
<ol>
<li>On a development laptop, <a href="https://getfedora.org/">Fedora</a>, I installed <a href="https://golang.org/">Go</a> and <a href="https://gohugo.io">Hugo</a> via</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo dnf install golang
</span></span><span class="line"><span class="cl">mkdir -p ~/build/go
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">GOPATH</span><span class="o">=</span><span class="nv">$HOME</span>/build/go
</span></span><span class="line"><span class="cl">go get -v github.com/spf13/hugo
</span></span></code></pre></div><ol start="2">
<li>Clone repository</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> ~/build/
</span></span><span class="line"><span class="cl">git clone git://github.com/brege/brege.org.git
</span></span></code></pre></div><h2 id="new-workflow-for-publishing-an-article">New workflow for publishing an article</h2>
<ol>
<li>I once again use Hugo to create my article</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> ~/build/brege.org
</span></span><span class="line"><span class="cl">hugo new post/new-way-forward-for-deploying-brege.org.md
</span></span></code></pre></div><ol start="2">
<li>
<p>Add content, run <code>hugo server</code>, then preview http://localhost:1313 in a browser to determine what needs to be edited.  The cool thing about this is you can edit/add a file while the test server is running. You don&rsquo;t even need to refresh the browser page whenever you save the file!</p>
</li>
<li>
<p>Run <code>rm -r public/ ; hugo</code> to remove the old and create the new <code>public/</code> directory on the development machine.</p>
</li>
<li>
<p>Ship the changes to the production server (DO droplet) with rsync:</p>
</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rsync -avP --delete --exclude<span class="o">=</span><span class="s2">&#34;.well-known&#34;</span> public/ brege.org:/usr/share/nginx/brege.org/
</span></span></code></pre></div><p>The new article will now show up on the <a href="https://brege.org/post/new-way-forward-for-deploying-brege/">https://brege.org/post/new-way-forward-for-deploying-brege/</a></p>
<ol start="5">
<li>Finally, commit the new article to GitHub:</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git add content/post/new-way-forward-for-deploying-brege.org.md
</span></span><span class="line"><span class="cl">git commit -m <span class="s2">&#34;Add article&#34;</span>
</span></span><span class="line"><span class="cl">git push -u origin master
</span></span></code></pre></div><p>Much nicer way to go about it.  Images and other assets can be more readily copied and modified.  I can edit a webpage on the fly before publishing layout-bugs and typos to the world (or, at least to a lesser extent).</p>]]></content:encoded>
    </item>
    <item>
      <title>Adding desktop entries in GNOME 3</title>
      <link>https://brege.org/post/adding-desktop-entries-in-gnome-3/</link>
      <pubDate>Mon, 17 Oct 2016 20:28:15 +0000</pubDate>
      <guid>https://brege.org/post/adding-desktop-entries-in-gnome-3/</guid>
      <description>&lt;p&gt;Firefox Developer Edition has newer features than what was available from my distribution&amp;rsquo;s package manager (&lt;a href=&#34;https://getfedora.org&#34;&gt;Fedora&lt;/a&gt;).  Whereas I&amp;rsquo;ve been using it as my daily driver, I&amp;rsquo;ve had to do so through a terminal to launch it.  With an alias in my &lt;code&gt;~/.bashrc&lt;/code&gt; such as&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;dfox&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;~/Preview/firefox-developer/firefox --profile ~/.mozilla/firefox/7ahl24yk.default/&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I was able to launch Firefox Developer quickly enough.  Unfortunately, I found it annoying that I couldn&amp;rsquo;t just type [Super Key] + &amp;ldquo;Fire&amp;rdquo; in GNOME and be on my way to browsing the web.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Firefox Developer Edition has newer features than what was available from my distribution&rsquo;s package manager (<a href="https://getfedora.org">Fedora</a>).  Whereas I&rsquo;ve been using it as my daily driver, I&rsquo;ve had to do so through a terminal to launch it.  With an alias in my <code>~/.bashrc</code> such as</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">dfox</span><span class="o">=</span><span class="s2">&#34;~/Preview/firefox-developer/firefox --profile ~/.mozilla/firefox/7ahl24yk.default/&#39;
</span></span></span></code></pre></div><p>I was able to launch Firefox Developer quickly enough.  Unfortunately, I found it annoying that I couldn&rsquo;t just type [Super Key] + &ldquo;Fire&rdquo; in GNOME and be on my way to browsing the web.</p>
<h2 id="adding-a-launcher-icon-for-firefox-developer">Adding a launcher icon for Firefox Developer</h2>
<p>I wanted to add a desktop entry/launcher icon for Firefox Developer.  I searched and found the Alacarte package, which is the GUI-way of doing this.  Alas, adding entries in Alacarte led to the following problem:</p>
<p><strong>Problem 1:</strong> Alacarte didn&rsquo;t allow me to actually click &ldquo;OK&rdquo;.  Both with the version from the Fedora repositories, <code>alacarte-3.11.91-4.fc24</code> via <code>sudo dnf install alacarte</code>, as well as a version built from source, <code>1bb265f7</code> via <code>git clone git://git.gnome.org/alacarte</code>, did I have these issues.</p>
<figure>
    <img loading="lazy" src="img/alacarte-disabled-ok-button.png"/> 
</figure>

<p><strong>Solution:</strong> Create the .desktop file by hand.  (Also, probably submit a bug report.)</p>
<p>In <code>~/.local/share/applications/firefox.desktop</code>, I added the following lines:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Desktop Entry]</span>
</span></span><span class="line"><span class="cl"><span class="na">Comment</span><span class="o">=</span><span class="s">Browse the World Wide Web</span>
</span></span><span class="line"><span class="cl"><span class="na">Terminal</span><span class="o">=</span><span class="s">false</span>
</span></span><span class="line"><span class="cl"><span class="na">Name</span><span class="o">=</span><span class="s">Firefox Developer Browser</span>
</span></span><span class="line"><span class="cl"><span class="na">Exec</span><span class="o">=</span><span class="s">~/Preview/firefox-developer/firefox --profile ~/.mozilla/firefox/xxxxxxxx.default %U</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">Application</span>
</span></span><span class="line"><span class="cl"><span class="na">Icon</span><span class="o">=</span><span class="s">~/Preview/firefox-developer/browser/icons/mozicon128.png</span>
</span></span><span class="line"><span class="cl"><span class="na">Categories</span><span class="o">=</span><span class="s">Network;</span>
</span></span></code></pre></div><p>where <code>xxxxxxxx.default</code> is my default profile directory. My starting point was browsing a few .desktop files in <code>~/.local/share/applications/</code> (user installed applications) and <code>/usr/share/applications/</code> (system-wide applications), for reference.</p>
<p>I ran into an issue, however:</p>
<p><strong>Problem 2:</strong> If I added Firefox to my Favorites (the dock on the left where openened and manually added applications sit), I would get a duplicated menu entry.</p>
<p><strong>Solution:</strong> Add <code>StartupWMClass=Firefox</code> to <code>~/.local/share/applications/firefox.desktop</code></p>
<p>I came acrossed a proper solution <a href="https://askubuntu.com/questions/403766/duplicate-icons-for-manully-created-gnome-launcher-items#635839">here</a>.  In short, I fired up Firefox Developer, ran <code>xprop WM_CLASS</code> in the terminal, clicked the Firefox window, and added the entry to <code>firefox.desktop</code></p>
<p>My final file looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Desktop Entry]</span>
</span></span><span class="line"><span class="cl"><span class="na">Comment</span><span class="o">=</span><span class="s">Browse the World Wide Web</span>
</span></span><span class="line"><span class="cl"><span class="na">Terminal</span><span class="o">=</span><span class="s">false</span>
</span></span><span class="line"><span class="cl"><span class="na">Name</span><span class="o">=</span><span class="s">Firefox Developer Browser</span>
</span></span><span class="line"><span class="cl"><span class="na">Exec</span><span class="o">=</span><span class="s">~/Preview/firefox-developer/firefox --profile ~/.mozilla/firefox/xxxxxxxx.default %U</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">Application</span>
</span></span><span class="line"><span class="cl"><span class="na">Icon</span><span class="o">=</span><span class="s">~/Preview/firefox-developer/browser/icons/mozicon128.png</span>
</span></span><span class="line"><span class="cl"><span class="na">Categories</span><span class="o">=</span><span class="s">Network;</span>
</span></span><span class="line"><span class="cl"><span class="na">StartupWMClass</span><span class="o">=</span><span class="s">Firefox</span>
</span></span></code></pre></div><h2 id="exercise---repeating-the-process-for-earlybird-thunderbird-alpha">Exercise - Repeating the process for Earlybird (Thunderbird Alpha)</h2>
<ol>
<li>Go to the <a href="https://www.mozilla.org/en-US/thunderbird/channel/">Thunderbird Release Channel</a> and grab the package for Earlybird (or the the Beta, if you choose):
<figure>
        <img loading="lazy" src="img/thunderbird-release-channel.png"/> 
    </figure>

Or, from the command line:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> ~/Preview<span class="sb">`</span>
</span></span><span class="line"><span class="cl">wget https://ftp.mozilla.org/pub/thunderbird/nightly/latest-comm-aurora/thunderbird-51.0a2.en-US.linux-x86_64.tar.bz2<span class="sb">`</span>
</span></span><span class="line"><span class="cl">tar -xvf thunderbird-51.0a2.en-US.linux-x86_64.tar.bz2 -C thunderbird
</span></span></code></pre></div></li>
</ol>
<p>If you use Thunderbird already, determine your profile: <code>ls ~/.thunderbird/*.default</code></p>
<ol start="2">
<li>Create the desktop entry, <code>~/.local/share/applications/thunderbird.desktop</code>, with the following contents:</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Desktop Entry]</span>
</span></span><span class="line"><span class="cl"><span class="na">Name</span><span class="o">=</span><span class="s">Thunderbird</span>
</span></span><span class="line"><span class="cl"><span class="na">Comment</span><span class="o">=</span><span class="s">Send and receive mail with Thunderbird</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">Application</span>
</span></span><span class="line"><span class="cl"><span class="na">Exec</span><span class="o">=</span><span class="s">~/Preview/thunderbird/thunderbird --profile ~/.thunderbird/xxxxxxxx.default %u</span>
</span></span><span class="line"><span class="cl"><span class="na">Icon</span><span class="o">=</span><span class="s">~/Preview/thunderbird/chrome/icons/default/default256.png</span>
</span></span><span class="line"><span class="cl"><span class="na">Categories</span><span class="o">=</span><span class="s">Network;</span>
</span></span><span class="line"><span class="cl"><span class="na">StartupWMClass</span><span class="o">=</span><span class="s">Thunderbird</span>
</span></span></code></pre></div><p>Replace <code>xxxxxxxx.default</code> with your profile.</p>
<figure>
    <img loading="lazy" src="img/thunderbird-launcher.png"/> 
</figure>

<p>For any other application like this, I would just copy <code>firefox.desktop</code> to <code>myapplication.desktop</code> and adjust the contents to suit your needs.  Then, in general, launch the application and run <code>xprop WM_CLASS</code> to determine the value to put in the <code>StartupWMClass=</code> field.</p>]]></content:encoded>
    </item>
    <item>
      <title>brege.org now on GitHub</title>
      <link>https://brege.org/post/brege.org-now-on-github/</link>
      <pubDate>Wed, 12 Oct 2016 00:32:21 +0000</pubDate>
      <guid>https://brege.org/post/brege.org-now-on-github/</guid>
      <description>&lt;p&gt;Earlier I added the hugo source files for this website on GitHub.  Take a look!&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/brege/brege.org&#34;&gt;https://github.com/brege/brege.org&lt;/a&gt;&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Earlier I added the hugo source files for this website on GitHub.  Take a look!</p>
<p><a href="https://github.com/brege/brege.org">https://github.com/brege/brege.org</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Nginx rewrite for RSS</title>
      <link>https://brege.org/post/nginx-rewrite-for-rss/</link>
      <pubDate>Tue, 11 Oct 2016 07:01:47 +0000</pubDate>
      <guid>https://brege.org/post/nginx-rewrite-for-rss/</guid>
      <description>&lt;p&gt;Add the following to the main server block in your nginx config:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-nginx&#34; data-lang=&#34;nginx&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;location&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;~&lt;/span&gt; &lt;span class=&#34;sr&#34;&gt;^/(?:feed|feeds|rss)&lt;/span&gt;  &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;         &lt;span class=&#34;kn&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;301&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;/post/index.xml&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now people can go to &lt;a href=&#34;https://brege.org/rss&#34;&gt;https://brege.org/rss&lt;/a&gt; to find the RSS feed.
This rewrite rule could be expanded to match other common feed URI guesses.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Add the following to the main server block in your nginx config:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl">    <span class="k">location</span> <span class="p">~</span> <span class="sr">^/(?:feed|feeds|rss)</span>  <span class="p">{</span>
</span></span><span class="line"><span class="cl">         <span class="kn">return</span> <span class="mi">301</span> <span class="s">/post/index.xml</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span></code></pre></div><p>Now people can go to <a href="https://brege.org/rss">https://brege.org/rss</a> to find the RSS feed.
This rewrite rule could be expanded to match other common feed URI guesses.</p>]]></content:encoded>
    </item>
    <item>
      <title>Automatic renewal of Let&#39;s Encrypt Certificates</title>
      <link>https://brege.org/post/automatic-renewal-of-letsencrypt/</link>
      <pubDate>Tue, 11 Oct 2016 00:15:22 +0000</pubDate>
      <guid>https://brege.org/post/automatic-renewal-of-letsencrypt/</guid>
      <description>&lt;p&gt;Let&amp;rsquo;s Encrypt certificates need to be renewed every three months.
&lt;a href=&#34;https://wiki.archlinux.org/index.php/Let%E2%80%99s_Encrypt#Automatic_renewal&#34;&gt;The Arch Wiki&lt;/a&gt; has good documentation on automating the renewal process with systemd.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Let&rsquo;s Encrypt certificates need to be renewed every three months.
<a href="https://wiki.archlinux.org/index.php/Let%E2%80%99s_Encrypt#Automatic_renewal">The Arch Wiki</a> has good documentation on automating the renewal process with systemd.</p>
<ul>
<li>In <code>/etc/systemd/system/certbot.service</code>:</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Let&#39;s Encrypt renewal</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/bin/certbot renew --quiet --agree-tos</span>
</span></span></code></pre></div><ul>
<li>In <code>/etc/systemd/system/certbot.timer</code>:</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Daily renewal of Let&#39;s Encrypt&#39;s certificates</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Timer]</span>
</span></span><span class="line"><span class="cl"><span class="na">OnCalendar</span><span class="o">=</span><span class="s">daily</span>
</span></span><span class="line"><span class="cl"><span class="na">RandomizedDelaySec</span><span class="o">=</span><span class="s">1day</span>
</span></span><span class="line"><span class="cl"><span class="na">Persistent</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">timers.target</span>
</span></span></code></pre></div><ul>
<li>Enable and start the timer:</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> certbot.timer
</span></span><span class="line"><span class="cl">sudo systemctl start certbot.timer
</span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>Building brege.org</title>
      <link>https://brege.org/post/building-brege-dot-org/</link>
      <pubDate>Mon, 10 Oct 2016 19:47:14 +0000</pubDate>
      <guid>https://brege.org/post/building-brege-dot-org/</guid>
      <description>&lt;h2 id=&#34;overview&#34;&gt;Overview&lt;/h2&gt;
&lt;p&gt;brege.org is deployed with the following services:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://www.namecheap.com/&#34;&gt;Namecheap&lt;/a&gt; - DNS Registrar&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://letsencrypt.org&#34;&gt;Let&amp;rsquo;s Encrypt&lt;/a&gt; - Free SSL certificates&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.digitalocean.com&#34;&gt;Digital Ocean&lt;/a&gt; - Droplets for $5 a month&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;brege.org makes use of the following tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://nginx.org&#34;&gt;Nginx&lt;/a&gt; - Fast reverse proxy&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://debian.org&#34;&gt;Debian Jessie&lt;/a&gt; - Operating system running on our DO droplet&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://gohugo.io&#34;&gt;Hugo&lt;/a&gt; - Static site generator&lt;/li&gt;
&lt;/ul&gt;</description>
      <content:encoded><![CDATA[<h2 id="overview">Overview</h2>
<p>brege.org is deployed with the following services:</p>
<ul>
<li><a href="https://www.namecheap.com/">Namecheap</a> - DNS Registrar</li>
<li><a href="https://letsencrypt.org">Let&rsquo;s Encrypt</a> - Free SSL certificates</li>
<li><a href="https://www.digitalocean.com">Digital Ocean</a> - Droplets for $5 a month</li>
</ul>
<p>brege.org makes use of the following tools:</p>
<ul>
<li><a href="https://nginx.org">Nginx</a> - Fast reverse proxy</li>
<li><a href="https://debian.org">Debian Jessie</a> - Operating system running on our DO droplet</li>
<li><a href="https://gohugo.io">Hugo</a> - Static site generator</li>
</ul>
<h2 id="hosting">Hosting</h2>
<p>First thing I did was register my domain on Namecheap.
It was around $12 to register brege.org.</p>
<p>Next bit was deciding on a hosting provider.
I gave Amazon S3 some consideration, since it&rsquo;s practically free for a static site, but after playing around with their tools it seemed far too confusing to use.
I thought I&rsquo;d try Digital Ocean.
Their tutorials on deploying various web apps have been quite helpful in previous searches, and they host good things like <a href="https://lkml.org">lkml.org</a>.
The $5 droplet service offers 512 MB RAM, 20 GB SSD, and 1 TB of bandwidth.
Good enough.</p>
<p>I selected the debian installation on the &ldquo;Create Droplet&rdquo; page and created a new ssh key:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh-keygen -t rsa -b <span class="m">4096</span> -C <span class="s2">&#34;wyatt@brege.org&#34;</span>
</span></span></code></pre></div><p>I like to have a different key for each remote service I use (GitHub, Clusters, Servers, etc).
So for the bit about saving the key, I just substituted the default <code>id_rsa</code> with <code>do_rsa</code>, chose a password to unlock my key, and uploaded <code>do_rsa.pub</code> in the DO web interface.</p>
<p>This gets added to /root/.ssh/authorized_keys on your DO droplet.
I added another user on my droplet so I wouldn&rsquo;t always be logging in as root.
You can copy this key from your workstation to the user&rsquo;s home directory on your droplet via</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh-copy-id -i ~/.ssh/do_rsa newuser@ip.of.my.droplet
</span></span></code></pre></div><p>DNS settings can be confusing. Setting &ldquo;Custom DNS&rdquo; in Namecheap and setting the nameservers to DO&rsquo;s nameservers was simple enough.  Setting records on namecheap is kind of annoying.  On DO it is really intuitive to set up records.</p>
<h3 id="nginx">Nginx</h3>
<p>I&rsquo;ve been using nginx on my personal home server and quite like its configuration and speed.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt-get install nginx
</span></span></code></pre></div><p>I&rsquo;m not exactly sure what the consensus is on where your site should live in your debian install.
<code>/usr/share/nginx/</code>? <code>/var/www/</code>? The <code>html/</code> directory in either of those?
The way I treat it is that my static site will live in <code>/usr/share/nginx/brege.org/</code>.
If I want to password protect any section of my site, my <code>.htpasswd</code> will live in user <code>www-data</code>&rsquo;s home directory, <code>/var/www/</code>.
If I want to host some other site for <code>abc.brege.org</code>, it will live in <code>/usr/share/nginx/abc.brege.org/</code>.</p>
<p>I removed the default site configuration file installed on debian.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rm /etc/nginx/sites-available/default
</span></span><span class="line"><span class="cl">rm /etc/nginx/sites-enabled/default
</span></span></code></pre></div><p>I made a new configuration file <code>/etc/nginx/sites-available/brege.org</code> with the following settings:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span> <span class="s">*.brege.org</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">return</span> <span class="s">http://brege.org/</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="s">[::]:80</span> <span class="s">default_server</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span>  <span class="s">brege.org</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># enforce https
</span></span></span><span class="line"><span class="cl">    <span class="kn">return</span> <span class="mi">301</span> <span class="s">https://</span><span class="nv">$server_name$request_uri</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="mi">443</span> <span class="s">default</span> <span class="s">ssl</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="s">[::]:443</span> <span class="s">default_server</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">root</span> <span class="s">/usr/share/nginx/brege.org</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">index</span> <span class="s">index.html</span> <span class="s">index.htm</span> <span class="s">index.php</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span> <span class="s">brege.org</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">ssl_certificate</span> <span class="s">/etc/letsencrypt/live/brege.org/fullchain.pem</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">ssl_certificate_key</span> <span class="s">/etc/letsencrypt/live/brege.org/privkey.pem</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">include</span> <span class="s">letsencrypt.conf</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">location</span> <span class="s">/</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># First attempt to serve request as file, then
</span></span></span><span class="line"><span class="cl">        <span class="c1"># as directory, then fall back to displaying a 404.
</span></span></span><span class="line"><span class="cl">        <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri/</span> <span class="p">=</span><span class="mi">404</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>You can split each <code>server{ ... }</code> block into its own config file if you like.
One redirects all <code>*.brege.org</code> requests to <code>brege.org</code>, including <code>www.brege.org</code>.
The second one redirects all requests from <a href="http://brege.org">http://brege.org</a> to <a href="https://brege.org">https://brege.org</a>.
The third is the main configuration for the site.
This is where you put reverse proxies that redirect internal ports to subdirectories on your site.</p>
<p>Link the new configuration file and restart nginx:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo ln -s /etc/nginx/sites-available/brege.org /etc/nginx/sites-enabled/
</span></span><span class="line"><span class="cl">sudo systemctl restart nginx.service
</span></span></code></pre></div><h3 id="lets-encrypt">Let&rsquo;s Encrypt</h3>
<p>In <code>/etc/nginx/letsencrypt.conf</code>, I have:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">location</span> <span class="s">^~</span> <span class="s">/.well-known/acme-challenge</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">alias</span> <span class="s">/var/lib/letsencrypt/.well-known/acme-challenge</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">default_type</span> <span class="s">&#34;text/plain&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="p">=</span><span class="mi">404</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>After making any changes to your nginx configuration, restart the service:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl restart nginx.service
</span></span></code></pre></div><p>We need to generate a free SSL certificate from Let&rsquo;s Encrypt.
I followed the instructions on <a href="https://certbot.eff.org/#debianjessie-nginx">https://certbot.eff.org/#debianjessie-nginx</a> to install certbot on Debian Jessie.</p>
<p>As root, I ran</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">certbot certonly --webroot -w /usr/share/nginx/brege.org -d brege.org -d www.brege.org
</span></span></code></pre></div><p>and completed the wizard. YMWV</p>
<h3 id="hugo">Hugo</h3>
<p>Since I&rsquo;ve been getting into the habit of writing my notes and personal documentation in <a href="https://daringfireball.net/projects/markdown/">Markdown</a>, I wanted to be able to quickly copy and paste snippets to-and-from this website.
I found <a href="https://gohugo.io">Hugo</a>, which is written in <a href="https://golang.org/">Go</a>.
It is very fast to build a static website under.
Every post is written in markdown, and running the <code>hugo</code> command only takes ~50ms to generate all the html/css/js files.
No more having to fiddle with html/css/js.</p>
<p>First, the version of <a href="https://golang.org/">Go</a> required to run Hugo must be 1.5 or greater at the time of writing.
I went to <a href="https://golang.org/dl/">https://golang.org/dl/</a> and grabbed the latest release:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> ~/
</span></span><span class="line"><span class="cl">wget https://storage.googleapis.com/golang/go1.7.1.linux-amd64.tar.gz
</span></span><span class="line"><span class="cl">tar xvf go1.7.1.linux-amd64.tar.gz
</span></span><span class="line"><span class="cl">rm go1.7.1.linux-amd64.tar.gz
</span></span><span class="line"><span class="cl">sudo mv go /opt/
</span></span><span class="line"><span class="cl">sudo chown -R newuser:newuser go/
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> go/ 
</span></span></code></pre></div><p>I added the following to <code>/etc/profile</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="nv">$PATH</span>:/usr/local/go/bin
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="nv">$PATH</span>:/opt/go/bin
</span></span></code></pre></div><p>Next, install Hugo via git:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">GOPATH</span><span class="o">=</span>/opt/go/bin
</span></span><span class="line"><span class="cl">go get -v github.com/spf13/hugo
</span></span></code></pre></div><p>More information available <a href="https://gohugo.io/overview/installing/">here</a>.</p>
<p>Checkout <a href="https://themes.gohugo.io/">https://themes.gohugo.io/</a> to see if there&rsquo;s a theme that you like.
I thought the <a href="https://github.com/gcushen/hugo-academic">academic theme</a> looked pretty nice.
I followed the instructions in the README.md to create my site.</p>
<p>The workflow is typically:</p>
<ol>
<li>Creating a new post</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hugo new post/hello-world.md
</span></span></code></pre></div><ol start="2">
<li>
<p>Adding what I want to say with simple markdown format in <code>content/post/hellow-world.md</code> after the <code>+++ ... +++</code> front matter.</p>
</li>
<li>
<p>Running <code>hugo</code>, which creates all the files for your site.
It may be wise to remove the <code>public/</code> directory and the <code>/usr/share/nginx/brege.org/*</code> subdirectories to prune any orphaned articles.</p>
</li>
<li>
<p>Copying the generated <code>public/</code> folder over to the document root.</p>
</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo rsync -avP public/ /usr/share/nginx/brege.org/
</span></span></code></pre></div><ol start="5">
<li>Visit brege.org in my browser.</li>
</ol>
<p>In fact, there is probably no need to run go/hugo on my DO droplet at all.
The proper way is probably to do everything on a local machine, add a new article and check at localhost:1313, then rsync the contents of <code>public/</code> to your document root on your droplet.</p>
<p>Putting the source of this site on github might be even better.
I&rsquo;m already keeping track of my changes with a local git repository, so I might as well make it available on GitHub.  Plus it seems worth it just for the sake of writing articles locally.</p>
<p><strong>Update</strong>: I am getting closer to implementing my initial thoughts here: <a href="https://brege.org/post/new-way-forward-for-deploying-brege/">https://brege.org/post/new-way-forward-for-deploying-brege/</a></p>]]></content:encoded>
    </item>
  </channel>
</rss>
