<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://ritviknag.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ritviknag.com/" rel="alternate" type="text/html" /><updated>2026-08-17T23:55:21-04:00</updated><id>https://ritviknag.com/feed.xml</id><title type="html">Ritvik Nag</title><subtitle>Senior Software Engineer (automation, backend). M.S. (AI/ML). Runner pursuing a half marathon in all 51 states.</subtitle><author><name>Ritvik Nag</name><email>me@ritviknag.com</email></author><entry><title type="html">Why I Stopped Writing (And Why I Started Again)</title><link href="https://ritviknag.com/blog/why-i-stopped-writing-and-why-i-started-again/" rel="alternate" type="text/html" title="Why I Stopped Writing (And Why I Started Again)" /><published>2026-08-17T22:56:51-04:00</published><updated>2026-08-17T22:56:51-04:00</updated><id>https://ritviknag.com/blog/why-i-stopped-writing-and-why-i-started-again</id><content type="html" xml:base="https://ritviknag.com/blog/why-i-stopped-writing-and-why-i-started-again/"><![CDATA[<h2 id="winter-thoughts-on-return">Winter Thoughts on Return</h2>

<p><em>I am resurgent.</em></p>

<p>I am dormant, asleep, until needed.<br />
Like a bear in winter’s embrace, I wait.</p>

<p>Patient.</p>

<p>Still.</p>

<p>This is not the end;
it is only a pause.</p>

<p>Once I was,
and soon I will be again.</p>

<hr />

<p><em>I am resurgent.</em></p>

<p>I am dormant, asleep, until I am called upon.
Like a bear, I hibernate through winter, gathering strength.</p>

<p>Once I was.</p>

<p>And I shall rise again.</p>

<h2 id="hail-to-the-new-year">Hail to the New Year</h2>

<p>Winter has always seemed to understand something about disappearance.</p>

<p>Trees shed everything ornamental. The landscape empties itself. Animals vanish underground. Lakes freeze over and pretend, convincingly, that nothing moves beneath them.</p>

<p>And yet very little is actually dead.</p>

<p>It is waiting.</p>

<p>Perhaps that is why I have always liked winter.</p>

<p>There is an honesty to a season that makes no effort to appear hospitable.</p>

<p>Cold is cold.</p>

<p>Darkness arrives early.</p>

<p>The trees do not apologize for being bare.</p>

<p>For a while, I stopped writing.</p>

<p>There wasn’t a dramatic decision behind it. I simply began to wonder what the point was.</p>

<p>Words accumulate. Pages disappear into archives. The internet swallows another essay and asks for another million before breakfast.</p>

<p>Eventually, the blank page became easier.</p>

<p>So I left it blank.</p>

<p>For a while.</p>

<p>Then it snowed.</p>

<p>Not much. Just enough to alter the familiar outlines of things.</p>

<p>There is something strangely theatrical about snowfall. The same road, the same buildings, the same branches — and suddenly somebody has redrawn the world overnight.</p>

<p>I stood watching it and thought about dormancy.</p>

<p>And return.</p>

<h2 id="creep">Creep</h2>

<p>Around the same time, I found myself listening again to Radiohead’s <em>Creep</em>.</p>

<p>It is a strange song to survive as long as it has.</p>

<p>Its basic ingredients are hardly obscure: admiration, alienation, envy, desire, self-consciousness, the uncomfortable distance between the person you are and the person you imagine you ought to be.</p>

<p>Maybe that is precisely why it survives.</p>

<p>There is a particular kind of beauty that can make a person suddenly conscious of themselves.</p>

<p>Not merely romantic beauty.</p>

<p>Talent can do it.</p>

<p>Confidence can do it.</p>

<p>Someone walking into a room apparently possessing whatever invisible quality you have spent years unsuccessfully trying to manufacture can do it.</p>

<p>For a moment, they seem illuminated.</p>

<p>And you become unusually aware of the shadows.</p>

<p>That tension sits at the center of <em>Creep</em>: the yearning to be extraordinary coupled with the suspicion that perhaps you are not.</p>

<p>I think most people understand that feeling better than they admit.</p>

<p>We spend tremendous amounts of energy constructing acceptable versions of ourselves.</p>

<p>Faster.</p>

<p>Smarter.</p>

<p>Funnier.</p>

<p>Stronger.</p>

<p>More attractive.</p>

<p>More successful.</p>

<p>More interesting.</p>

<p>Special.</p>

<p>And underneath all of that improvement lies an inconvenient question:</p>

<p>What happens if the finished product still doesn’t feel like enough?</p>

<h2 id="the-heretic">The Heretic</h2>

<p>That question followed me into <em>Heretic</em>.</p>

<p>I watched the film twice.</p>

<p>Hugh Grant’s Mr. Reed is fascinating because he is both intellectually magnetic and deeply unsettling. He speaks with the confidence of someone who has spent a very long time arranging the universe into a system only he fully understands.</p>

<p>The result is dangerous.</p>

<p>But also compelling.</p>

<p>What interested me most wasn’t whether his arguments about religion were ultimately correct. The more interesting idea was his obsession with originality, repetition and control.</p>

<p>How many supposedly new ideas are old ideas wearing different clothes?</p>

<p>How many convictions are inherited rather than discovered?</p>

<p>How much of what we call identity is simply an elaborate remix?</p>

<p>The film uses <em>Creep</em> as part of that discussion, and the choice works almost too well.</p>

<p>A song about feeling fundamentally out of place appears inside a story populated by people attempting to explain precisely where human beings belong.</p>

<p>Religion.</p>

<p>Doubt.</p>

<p>Control.</p>

<p>Belief.</p>

<p>Fear.</p>

<p>The desire to understand the machinery behind everything.</p>

<p>Perhaps that is why the film stayed with me.</p>

<p>Not because I found some grand answer inside it.</p>

<p>Quite the opposite.</p>

<p>It reminded me how seductive grand answers can be.</p>

<h2 id="a-world-made-of-mirrors">A World Made of Mirrors</h2>

<p>There are moments when reality feels extraordinarily solid.</p>

<p>Cold pavement beneath your shoes.</p>

<p>A steering wheel beneath your hands.</p>

<p>The first sting of winter air entering your lungs.</p>

<p>Then there are other moments when everything seems slightly artificial.</p>

<p>A stage set.</p>

<p>A simulation.</p>

<p>A world made mostly of mirrors.</p>

<p>I have always liked stories such as <em>The Matrix</em> for that reason.</p>

<p>The fantasy isn’t merely that reality is false.</p>

<p>The fantasy is that somewhere there is a curtain you can pull aside.</p>

<p>A red pill.</p>

<p>A hidden door.</p>

<p>One moment of revelation after which everything finally makes sense.</p>

<p>Reality, unfortunately, is less cooperative.</p>

<p>Even if the universe is a simulation, the alarm still goes off tomorrow morning.</p>

<p>Gravity remains surprisingly committed to the bit.</p>

<p>Bills retain their remarkable sense of purpose.</p>

<p>And winter remains cold.</p>

<p>Perhaps knowing the true architecture of existence would change less than we imagine.</p>

<h2 id="midnight">Midnight</h2>

<p>The arrival of 2025 found me driving.</p>

<p>There was no dramatic countdown.</p>

<p>No champagne.</p>

<p>No great declaration about becoming a new person at midnight.</p>

<p>Just roads.</p>

<p>Traffic lights.</p>

<p>Long stretches of darkness.</p>

<p>A handful of cars moving through intersections as one year quietly became another.</p>

<p>I noticed the clock change.</p>

<p>11:59.</p>

<p>12:00.</p>

<p>2025.</p>

<p>That was it.</p>

<p>I kept driving.</p>

<p>There was something oddly appropriate about entering a new year in motion.</p>

<p>We place enormous importance on midnight, as though time itself recognizes the boundary.</p>

<p>But the road didn’t change.</p>

<p>The traffic lights continued their indifferent cycle.</p>

<p>Red.</p>

<p>Green.</p>

<p>Yellow.</p>

<p>Red again.</p>

<p>Eventually I stopped somewhere playing karaoke.</p>

<p>Someone sang <em>Creep</em>.</p>

<p>Later came <em>Pink Pony Club</em>.</p>

<p>I stayed for a while and listened.</p>

<p>Sometimes participation is overrated.</p>

<p>There are evenings when simply observing the strange parade of humanity is entertainment enough.</p>

<p>Then I stepped back outside into January.</p>

<p>The year had begun without asking whether anyone was ready for it.</p>

<h2 id="why-write">Why Write?</h2>

<p>That brings me back to writing.</p>

<p>I used to think writing required an audience.</p>

<p>Otherwise, what exactly was the point?</p>

<p>No one pays you by the paragraph.</p>

<p>The internet certainly isn’t suffering from a shortage of opinions.</p>

<p>And there are approximately seventeen billion more productive things one could theoretically be doing.</p>

<p>But that’s an unnecessarily transactional way of thinking about it.</p>

<p>Writing is useful because thought is messy.</p>

<p>Inside the mind, an idea can remain shapeless indefinitely.</p>

<p>On a page, it has nowhere to hide.</p>

<p>You have to give it a beginning.</p>

<p>Then another sentence.</p>

<p>Eventually you discover whether the thing you believed made any sense at all.</p>

<p>That alone seems worthwhile.</p>

<p>There is another reason too.</p>

<p>Memory is an unreliable archivist.</p>

<p>Entire years compress themselves into a handful of scenes.</p>

<p>A road at midnight.</p>

<p>Snow falling beneath a streetlight.</p>

<p>A song heard at exactly the right moment.</p>

<p>A movie that leaves behind a question you weren’t expecting.</p>

<p>Writing pins a few of those moments in place.</p>

<p>Not everything needs preserving.</p>

<p>Probably very little does.</p>

<p>But occasionally something deserves a page.</p>

<p>Perhaps that is what this is.</p>

<h2 id="resurgent">Resurgent</h2>

<p>I don’t know whether I am “returning to writing.”</p>

<p>That sounds too ceremonial.</p>

<p>Maybe I’ll write frequently.</p>

<p>Maybe I’ll disappear again.</p>

<p>Winter doesn’t promise spring on a particular schedule.</p>

<p>It simply waits.</p>

<p>There is freedom in that.</p>

<p>Not everything absent is lost.</p>

<p>Not everything silent is finished.</p>

<p>Sometimes the machinery is still running somewhere beneath the snow.</p>

<p>And sometimes, after a long enough winter, something begins moving again.</p>

<p>So I’ll leave this where I began.</p>

<blockquote>
  <p>I am resurgent.<br />
I am dormant, asleep, until needed.<br />
Like a bear in winter’s embrace, I wait patiently.</p>

  <p>This is not the end;<br />
it is only a pause.</p>

  <p>Once I was,<br />
and soon I will be again.</p>

  <p><em>[Brief interlude of silence]</em></p>

  <p>I am resurgent.<br />
I am dormant, asleep, until I am called upon.<br />
Like a bear, I hibernate through winter, gathering strength.</p>

  <p>Once I was.</p>

  <p>And I shall rise again.</p>
</blockquote>]]></content><author><name>Ritvik Nag</name><email>me@ritviknag.com</email></author><category term="blog" /><category term="writing" /><category term="reflection" /><category term="winter" /><category term="resurgence" /><category term="film" /><summary type="html"><![CDATA[Winter Thoughts on Return]]></summary></entry><entry><title type="html">Why I Run</title><link href="https://ritviknag.com/blog/why-i-run/" rel="alternate" type="text/html" title="Why I Run" /><published>2026-08-17T22:42:10-04:00</published><updated>2026-08-17T22:42:10-04:00</updated><id>https://ritviknag.com/blog/why-i-run</id><content type="html" xml:base="https://ritviknag.com/blog/why-i-run/"><![CDATA[<p>People sometimes ask why I run so hard.</p>

<p>There are the obvious answers. I like racing. I like getting faster. I like finding out what happens when the body starts negotiating and you decide not to listen.</p>

<p>But there are other reasons.</p>

<p>Running has always provided a peculiar kind of escape. Not necessarily from a place, or even from a person. Sometimes you just want to put some distance between yourself and whatever happens to be occupying your head.</p>

<p>I’ve had periods when I’ve felt remarkably invisible. You can be surrounded by people and still somehow feel like a ghost among them. You talk. You make jokes. You show up. Yet occasionally you leave wondering whether you were really there at all.</p>

<p>Rejection is strange that way. After enough of it, the individual instances aren’t necessarily what bother you anymore. You start looking for a pattern.</p>

<p>And patterns are dangerous things to look for when you’re unhappy.</p>

<p>The mind is exceptionally good at turning <em>this happened again</em> into <em>this will always happen</em>. A bad afternoon becomes evidence about your entire life. Someone’s indifference becomes a referendum on your worth. Eventually you’re prosecuting yourself with evidence you collected yourself, before a jury consisting entirely of yourself.</p>

<p>So I run.</p>

<p>There is something wonderfully uncomplicated about physical exhaustion. The road doesn’t care who likes you. A stopwatch doesn’t ghost you. A hill doesn’t reject you. There is only the next mile, the increasingly unreasonable demands from your legs, and the little voice suggesting that perhaps this would be an excellent time to stop.</p>

<p>Sometimes I listen.</p>

<p>Often I don’t.</p>

<p>That’s the contradiction.</p>

<p>My legs have carried me away from some of the worst places my mind has taken me. They stand me up, quite literally. And my way of thanking them has frequently been to ask for another mile.</p>

<p>And another.</p>

<p>And another.</p>

<p>There is probably a lesson in there somewhere.</p>

<p>I thought about this after watching <em>The Iron Claw</em>. It’s a film about wrestling, ostensibly, but what stayed with me was something broader: what happens when identity, suffering, love and self-destruction become tangled together. What happens when the thing that makes you feel alive is also capable of consuming you?</p>

<p>Running isn’t wrestling, obviously. My life isn’t that movie. But good stories don’t need to resemble your life exactly to make you recognize something in yourself.</p>

<p>I don’t want running merely to be an escape hatch.</p>

<p>I want to run because I love running. Because a fast mile is exhilarating. Because there’s another road I haven’t seen, another race I haven’t run, another absurd number on a clock that I’d like to make slightly smaller.</p>

<p>Maybe that’s part of getting older: realizing that the things which carried you through difficult periods don’t have to remain attached to those periods forever.</p>

<p>Maybe running has always contained a little of the same idea I’ve written about elsewhere: dormancy, endurance, and eventually, resurgence.</p>

<p>I put it this way elsewhere in <a href="/blog/why-i-stopped-writing-and-why-i-started-again/#winter-thoughts-on-return"><em>Winter Thoughts on Return</em></a>:</p>

<blockquote>
  <p>I am resurgent.<br />
I am dormant, asleep, until needed.<br />
Like a bear in winter’s embrace, I wait patiently.</p>

  <p>This is not the end;<br />
it is only a pause.</p>

  <p>Once I was,<br />
and soon I will be again.</p>

  <p><em>[Brief interlude of silence]</em></p>

  <p>I am resurgent.<br />
I am dormant, asleep, until I am called upon.<br />
Like a bear, I hibernate through winter, gathering strength.</p>

  <p>Once I was.</p>

  <p>And I shall rise again.</p>
</blockquote>

<p>I still run hard.</p>

<p>Probably too hard sometimes.</p>

<p>But these days, I’d rather be running toward something.</p>]]></content><author><name>Ritvik Nag</name><email>me@ritviknag.com</email></author><category term="blog" /><category term="running" /><summary type="html"><![CDATA[A reflection on ambition, escape, endurance, and why running has come to mean more to me than simply putting miles on a clock.]]></summary></entry><entry><title type="html">Half Marathons by State</title><link href="https://ritviknag.com/blog/half-marathons-by-state/" rel="alternate" type="text/html" title="Half Marathons by State" /><published>2026-04-16T23:10:28-04:00</published><updated>2026-04-16T23:10:28-04:00</updated><id>https://ritviknag.com/blog/half-marathons-by-state</id><content type="html" xml:base="https://ritviknag.com/blog/half-marathons-by-state/"><![CDATA[<p><strong>Progress:</strong> 18 states completed<br />
⭐ = Personal Record (PR)</p>

<p>I’m working toward running a half marathon in every U.S. state and DC.</p>

<p>Before each race, I keep a simple tradition: carb-loading the night before — usually pasta. Whenever possible, I’ll also share memorable pre-race spots worth checking out.</p>

<p>For detailed destination race guides – including lodging, restaurants, airports, parking, weather, elevation, and race-morning logistics – visit <a href="https://races.ritviknag.com/">Race Across 50 States</a>.</p>

<p>This page is a running log of races so far — times, courses, travel notes, and a few thoughts along the way.</p>

<hr />

<h1 id="1-virginia">1. Virginia</h1>

<h2 id="reston-half-marathon-4212024">Reston Half Marathon (4/21/2024)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Reston, VA</td>
      <td><strong>1:36:41</strong> ⭐</td>
      <td>7:23/mi</td>
      <td>5</td>
      <td>1</td>
    </tr>
  </tbody>
</table>

<p>First in AG. I was expecting at least 3rd — a nice surprise.</p>

<p>In the words of a volunteer on the sidelines:</p>

<blockquote>
  <p>“Gravity is your friend!”</p>
</blockquote>

<hr />

<h2 id="richmond-half-marathon-11152025">Richmond Half Marathon (11/15/2025)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>11</td>
      <td>Richmond, VA</td>
      <td><strong>1:23:37</strong> ⭐</td>
      <td>6:23/mi</td>
      <td>124</td>
      <td>30</td>
    </tr>
  </tbody>
</table>

<div class="review-card">
  <h4>Pre-Race: Casa Italiana</h4>
  <p>
    Family Owned Italian Restaurant Specializing in Home Made Pasta.
  </p>
  <p>
    <a href="https://www.yelp.com/biz/casa-italiana-richmond?hrid=OVT_Zwlg0z0F6Mi5YeyZ3g" rel="nofollow noopener">
      Read full review on Yelp
    </a>
  </p>
</div>

<p>Fast downhill finish. Strong close over the final stretch.</p>

<p>Great course, great crowd — solid weekend.</p>

<hr />

<h2 id="sentara-shamrock-half-marathon-3222026">Sentara Shamrock Half Marathon (3/22/2026)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>15</td>
      <td>Virginia Beach, VA</td>
      <td><strong>1:18:37</strong> ⭐</td>
      <td>6:00/mi</td>
      <td>15</td>
      <td>3</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>The Sentara Shamrock Half Marathon is flat and fast, making it the perfect place for a PR.</p>
</blockquote>

<p>Flat and fast was accurate.</p>

<hr />

<h1 id="2-connecticut">2. Connecticut</h1>

<h2 id="62nd-annual-john--jessie-kelley-half-marathon-832024">62nd Annual John &amp; Jessie Kelley Half Marathon (8/3/2024)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2</td>
      <td>New London, CT</td>
      <td><strong>1:31:53</strong> ⭐</td>
      <td>7:01/mi</td>
      <td>31</td>
      <td>4</td>
    </tr>
  </tbody>
</table>

<div class="review-card">
  <h4>Pre-Race: Consiglio's Restaurant</h4>
  <p>
    Classic Italian spot in New Haven — cozy atmosphere, solid pasta, and a great pre-race option.
  </p>
  <p>
    <a href="https://www.yelp.com/biz/consiglios-restaurant-new-haven-2?hrid=e-tC1gcwIPzPfuk7ZnQqig" rel="nofollow noopener">
      Read full review on Yelp
    </a>
  </p>
</div>

<p>Summer heat kept me from breaking 1:30.</p>

<p>Well-organized race despite tough conditions. Volunteers spraying water along the course made a huge difference — grabbing water and dumping it on my head was necessary in that heat.</p>

<hr />

<h1 id="3-west-virginia">3. West Virginia</h1>

<h2 id="new-river-gorge-wilds-trail-run-1052024">New River Gorge Wilds Trail Run (10/5/2024)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>3</td>
      <td>Oak Hill, WV</td>
      <td><strong>1:50:03</strong></td>
      <td>8:24/mi</td>
      <td>3</td>
      <td>?</td>
    </tr>
  </tbody>
</table>

<p>Top 3 at ACE Adventure Resort.</p>

<p>A true trail run — and I wore the wrong shoes.</p>

<p>Scenic course, and a ~5-hour drive from NoVA that was worth it.</p>

<p>First trail half marathon. Racing with a dead watch, no phone, no gear, and limited water was a good lesson in what to do better next time.</p>

<hr />

<h1 id="4-north-carolina">4. North Carolina</h1>

<h2 id="charlotte-half-marathon-11162024">Charlotte Half Marathon (11/16/2024)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>4</td>
      <td>Charlotte, NC</td>
      <td><strong>1:24:35</strong> ⭐</td>
      <td>6:28/mi</td>
      <td>38</td>
      <td>6</td>
    </tr>
  </tbody>
</table>

<p>First sub-1:25. Also the debut of the Nike Vaporfly 3.</p>

<hr />

<h1 id="5-arizona">5. Arizona</h1>

<h2 id="run-sedona-212025">Run Sedona (2/1/2025)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>5</td>
      <td>Sedona, AZ</td>
      <td><strong>1:31:02</strong></td>
      <td>6:57/mi</td>
      <td>16</td>
      <td>3</td>
    </tr>
  </tbody>
</table>

<p>Two hours of sleep, hills, and elevation — still happy with the result.</p>

<hr />

<h1 id="6-washington-dc">6. Washington, DC</h1>

<h2 id="dc-rock-n-roll-half-3152025">DC Rock ‘N Roll Half (3/15/2025)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>6</td>
      <td>Washington, DC</td>
      <td><strong>1:23:54</strong></td>
      <td>6:24/mi</td>
      <td>146</td>
      <td>22</td>
    </tr>
  </tbody>
</table>

<p>A good summary of this race:</p>

<blockquote>
  <p>Monster Hill 👹 @ mile 8<br />
completely destroyed me,<br />
had me thinking I’d just hold on for sub-1:30,<br />
and somehow still managed a slight PR.</p>
</blockquote>

<hr />

<h1 id="7-new-york">7. New York</h1>

<h2 id="nycruns-half--brooklyn-ice-cream-social-8232025">NYCRUNS Half — Brooklyn Ice Cream Social (8/23/2025)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>7</td>
      <td>Brooklyn, NY</td>
      <td><strong>1:24:53</strong></td>
      <td>6:29/mi</td>
      <td>9</td>
      <td>4</td>
    </tr>
  </tbody>
</table>

<p>Four laps around Prospect Park.</p>

<hr />

<h1 id="8-colorado">8. Colorado</h1>

<h2 id="boulderthon-9282025">Boulderthon (9/28/2025)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>8</td>
      <td>Boulder, CO</td>
      <td><strong>1:24:17</strong></td>
      <td>6:25/mi</td>
      <td>62</td>
      <td>10</td>
    </tr>
  </tbody>
</table>

<p>Happy to break 1:25 at altitude.</p>

<p>Hard race. ~5,300 ft — much higher than what I’m used to in NoVA.</p>

<p>Did a 10.5-mile shakeout on Davidson Mesa beforehand to acclimate. Still — they said “no hills”… not true.</p>

<hr />

<h1 id="9-new-hampshire">9. New Hampshire</h1>

<h2 id="smuttynose-rockfest-half-marathon-1052025">Smuttynose Rockfest Half Marathon (10/5/2025)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>9</td>
      <td>Hampton Beach, NH</td>
      <td><strong>1:20:25</strong> ⭐</td>
      <td>6:09/mi</td>
      <td>21</td>
      <td>6</td>
    </tr>
  </tbody>
</table>

<hr />

<h1 id="10-tennessee">10. Tennessee</h1>

<h2 id="nashville-half-marathon-10252025">Nashville Half Marathon (10/25/2025)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>10</td>
      <td>Nashville, TN</td>
      <td><strong>1:23:47</strong></td>
      <td>6:23/mi</td>
      <td>97</td>
      <td>11</td>
    </tr>
  </tbody>
</table>

<p>Started late (~7:20 AM) due to long bib pickup lines.</p>

<p>Hilly course — but the downhills helped. Strong energy downtown, and a unique finish inside a baseball stadium.</p>

<p>~10-hour trip from NoVA — worth it.</p>

<hr />

<h1 id="11-hawaii">11. Hawaii</h1>

<h2 id="maui-oceanfront-marathon-1182026">Maui Oceanfront Marathon (1/18/2026)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>11</td>
      <td>Wailuku, HI</td>
      <td><strong>1:23:24</strong></td>
      <td>6:22/mi</td>
      <td>4</td>
      <td>1</td>
    </tr>
  </tbody>
</table>

<p>First time in Hawaii — traveled with family.</p>

<hr />

<h1 id="12-california">12. California</h1>

<h2 id="bay-breeze-half-marathon-2142026">Bay Breeze Half Marathon (2/14/2026)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>12</td>
      <td>San Leandro, CA</td>
      <td><strong>1:21:15</strong></td>
      <td>6:12/mi</td>
      <td>3</td>
      <td>2</td>
    </tr>
  </tbody>
</table>

<p>Flat, but brutal — gravel, sand, bridges, wind, and bugs.</p>

<p>Cool temps (~47–50°F), but didn’t feel like it.</p>

<hr />

<h1 id="13-arkansas">13. Arkansas</h1>

<h2 id="little-rock-half-marathon-312026">Little Rock Half Marathon (3/1/2026)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>13</td>
      <td>Little Rock, AR</td>
      <td><strong>1:24:42</strong></td>
      <td>6:28/mi</td>
      <td>17</td>
      <td>2</td>
    </tr>
  </tbody>
</table>

<div class="review-card">
  <h4>Pre-Race: Ristorante Capeo</h4>
  <p>
    Arkansas' Premier Italian Restaurant
  </p>
  <p>
    <a href="https://www.yelp.com/biz/ristorante-capeo-north-little-rock-2?hrid=mMfOurCpQ978ID06xg0IKw" rel="nofollow noopener">
      Read full review on Yelp
    </a>
  </p>
</div>

<p>Hilly course, but a great race overall. Ran alongside another runner for much of it — strong finish and great crowd support.</p>

<h1 id="14-kentucky">14. Kentucky</h1>

<h2 id="kentucky-derby-festival--humana-mini-marathon-4252026">Kentucky Derby Festival – Humana Mini-Marathon (4/25/2026)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>16</td>
      <td>Louisville, KY</td>
      <td><strong>1:21:53</strong></td>
      <td>6:15/mi</td>
      <td>42</td>
      <td>10</td>
    </tr>
  </tbody>
</table>

<div class="review-card">
  <h4>Pre-Race: The Old Spaghetti Factory</h4>
  <p>
      Classic Italian spot in downtown Louisville with generous portions and solid pre-race pasta options.
  </p>
  <p>
    <a href="https://www.yelp.com/biz/the-old-spaghetti-factory-louisville?hrid=go0y-AT0ufKtpkleD0L6OA" rel="nofollow noopener">
      Read full review on Yelp
    </a>
  </p>
</div>

<p>A scenic and energetic first race in Louisville with strong crowd support throughout the course. Cool morning weather and a solid pre-race dinner helped set up a fast day and a new state completed on the half marathon journey.</p>

<h1 id="15-ohio">15. Ohio</h1>

<h2 id="flying-pig-half-marathon-532026">Flying Pig Half Marathon (5/3/2026)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>17</td>
      <td>Cincinnati, OH</td>
      <td><strong>1:29:33</strong></td>
      <td>6:51/mi</td>
      <td>165</td>
      <td>27</td>
    </tr>
  </tbody>
</table>

<div class="review-card">
  <h4>Pre-Race: Pepp &amp; Dolores</h4>
  <p>
      Popular Cincinnati Italian restaurant with flavorful pasta and a relaxed atmosphere.
  </p>
  <p>
    <a href="https://www.yelp.com/biz/pepp-and-dolores-cincinnati-5?hrid=Rav2UsxFeg2937in1eNA8Q" rel="nofollow noopener">
      Read full review on Yelp
    </a>
  </p>
</div>

<p>First time racing in Cincinnati at one of the most well-known half marathons in the Midwest. Cold conditions and heavy congestion early on made the race challenging, but the course atmosphere, riverfront views, and crowd energy still made it a memorable experience.</p>

<hr />

<h1 id="16-wyoming">16. Wyoming</h1>

<h2 id="grand-teton-half-marathon-662026">Grand Teton Half Marathon (6/6/2026)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>18</td>
      <td>Jackson, WY</td>
      <td><strong>1:34:05</strong></td>
      <td>7:10/mi</td>
      <td>12</td>
      <td>2</td>
    </tr>
  </tbody>
</table>

<div class="review-card">
  <h4>Pre-Race: Calico Restaurant &amp; Bar</h4>
  <p>
    Fantastic pasta just minutes from packet pickup at Stilson Lot. One of my favorite pre-race dinners of the entire 50-state journey.
  </p>
  <p>
    <a href="https://www.yelp.com/biz/calico-restaurant-and-bar-wilson-2?hrid=w6wt8mUrTG5q0CrN3j1WGg" rel="nofollow noopener">
      Read full review on Yelp
    </a>
  </p>
</div>

<p>Beautiful mountain scenery and my first podium finish at altitude. Racing above 6,300 feet felt dramatically harder than the course profile suggested, but the views of the Tetons made every mile worthwhile.</p>

<p><a href="https://races.ritviknag.com/races/grand-teton-half">Read the full Grand Teton Half race guide →</a></p>

<hr />

<h1 id="17-montana">17. Montana</h1>

<h2 id="yellowstone-half-marathon-6132026">Yellowstone Half Marathon (6/13/2026)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>19</td>
      <td>West Yellowstone, MT</td>
      <td><strong>1:37:41</strong></td>
      <td>7:27/mi</td>
      <td>9</td>
      <td>2</td>
    </tr>
  </tbody>
</table>

<div class="review-card">
  <h4>Pre-Race: Madison Crossing Lounge</h4>
  <p>
    Excellent pasta just minutes from the race start and one of my favorite pre-race dinners.
  </p>
  <p>
    <a href="https://www.yelp.com/biz/madison-crossing-lounge-west-yellowstone-6?hrid=VTRn6Ij5d3GchndGpUYk4w" rel="nofollow noopener">
      Read full review on Yelp
    </a>
  </p>
</div>

<p>High-altitude trail race with roughly 671 feet of climbing, rocky footing, and temperatures around 38°F at the start. One of the toughest half marathons I’ve run, but an unforgettable destination race.</p>

<p><a href="https://races.ritviknag.com/races/yellowstone-half">Read the full Yellowstone Half race guide →</a></p>

<hr />

<h1 id="18-idaho">18. Idaho</h1>

<h2 id="boise-half-marathon-6202026">Boise Half Marathon (6/20/2026)</h2>

<table>
  <thead>
    <tr>
      <th>Race</th>
      <th>Location</th>
      <th>Time</th>
      <th>Pace</th>
      <th>Overall</th>
      <th>AG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>20</td>
      <td>Boise, ID</td>
      <td><strong>1:28:48</strong></td>
      <td>6:47/mi</td>
      <td>10</td>
      <td>2</td>
    </tr>
  </tbody>
</table>

<div class="review-card">
  <h4>Pre-Race: Àlavita</h4>
  <p>
    Handmade pasta within walking distance of the race start—one of the best pre-race restaurants in Boise.
  </p>
  <p>
    <a href="https://www.yelp.com/biz/%C3%A0lavita-boise-2?hrid=HTlRwW36jUtTuKEinV3ljg" rel="nofollow noopener">
      Read full review on Yelp
    </a>
  </p>
</div>

<p>A rainy race morning replaced the sunny forecast, but the downtown course was fast, walkable, and well organized. After two weeks of racing at much higher elevations, running at Boise’s lower elevation felt refreshingly comfortable.</p>

<p><a href="https://races.ritviknag.com/races/boise-half">Read the full Boise Half race guide →</a></p>]]></content><author><name>Ritvik Nag</name><email>me@ritviknag.com</email></author><category term="blog" /><category term="half-marathon" /><category term="running" /><category term="race-recaps" /><category term="50-states" /><summary type="html"><![CDATA[My ongoing project to run a half marathon in every state, with race results, travel notes, and lessons from the road.]]></summary></entry><entry><title type="html">How I Automated Parking Permit Purchases at GMU with Python</title><link href="https://ritviknag.com/tech-tips/how-i-automated-parking-permit-purchases-at-gmu-with-python/" rel="alternate" type="text/html" title="How I Automated Parking Permit Purchases at GMU with Python" /><published>2025-09-04T23:04:34-04:00</published><updated>2025-09-04T23:04:34-04:00</updated><id>https://ritviknag.com/tech-tips/how-i-automated-parking-permit-purchases-at-gmu-with-python</id><content type="html" xml:base="https://ritviknag.com/tech-tips/how-i-automated-parking-permit-purchases-at-gmu-with-python/"><![CDATA[<p class="notice--warning">This article has been cross-posted on my <a href="https://medium.com/@ritviknag">Medium</a> and <a href="https://dev.to/ritvik-nag">Dev.to</a>.
<br /><br />
If you found this post useful, please consider <em>following me</em> on those platforms as I would be really grateful for the support. Thanks!</p>
<!-- {: .notice--info} -->

<hr />

<p>As a grad student at <strong>George Mason University (GMU)</strong>, I commute to Fairfax once a week for evening classes. Like many commuters, I need parking — but only for a few hours, usually <strong>4–7 PM</strong>.</p>

<p>Buying a semester-long permit didn’t make sense for my schedule (or wallet). Instead, the <strong>daily parking permits</strong> turned out to be cheaper and more flexible.</p>

<p>The problem? I always procrastinated. I’d remember to buy a permit at the last minute, sometimes right before class. It was stressful, repetitive, and just annoying.</p>

<p>So, being a CS student, I did what came naturally:</p>

<p>👉 <strong>I automated the entire process.</strong></p>

<h2 id="the-idea">The Idea</h2>

<p>Every week, I needed to:</p>

<ol>
  <li>Go to the GMU parking portal.</li>
  <li>Log in with my credentials.</li>
  <li>Navigate through a bunch of menus.</li>
  <li>Select the right permit.</li>
  <li>Enter payment details.</li>
</ol>

<p>It was the same steps every time. Why not let Python handle it?</p>

<h2 id="the-tool">The Tool</h2>

<p>I built a small open-source tool in Python that:</p>

<ul>
  <li>Automates login to the GMU parking portal up to the <strong>Duo Mobile 2FA step</strong> (which still requires approval on your phone).</li>
  <li>Selects the correct <strong>daily parking permit</strong>.</li>
  <li>Completes the checkout flow.</li>
  <li>Emails me a confirmation when done.</li>
</ul>

<p>That’s it — no more last-minute scrambling.</p>

<p>📽️ <strong>Demo Video:</strong> <a href="https://youtu.be/9X5lc2zZq-k">YouTube</a></p>

<p>💻 <strong>Source Code:</strong> <a href="https://github.com/rnag/GMU-Daily-Permit-Automation">GitHub – GMU Daily Permit Automation</a></p>

<h2 id="tech-breakdown">Tech Breakdown</h2>

<p>At its core, this project uses:</p>

<ul>
  <li><strong><a href="https://www.selenium.dev/">Selenium</a></strong> – to control a browser and interact with the GMU parking site.</li>
  <li><strong>Headless mode</strong> – so it runs quietly in the background.</li>
  <li><strong>Config + Environment variables</strong> – to keep credentials and payment info safe.</li>
  <li><strong>Task scheduling (optional)</strong> – you <em>could</em> use <code class="language-plaintext highlighter-rouge">cron</code> (Linux/Mac) or Task Scheduler (Windows) to kick off the script automatically. I usually just run it manually before class.</li>
  <li><strong>Duo Mobile 2FA</strong> – you’ll still need to approve the login from your phone. The script pauses at this step until Duo is confirmed.</li>
</ul>

<p>Here’s a simplified example of what the login step looks like in Python with Selenium:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">selenium</span> <span class="kn">import</span> <span class="n">webdriver</span>
<span class="kn">from</span> <span class="nn">selenium.webdriver.common.by</span> <span class="kn">import</span> <span class="n">By</span>
<span class="kn">from</span> <span class="nn">selenium.webdriver.common.keys</span> <span class="kn">import</span> <span class="n">Keys</span>

<span class="c1"># Start the browser (headless mode can be enabled too)
</span><span class="n">driver</span> <span class="o">=</span> <span class="n">webdriver</span><span class="p">.</span><span class="n">Chrome</span><span class="p">()</span>

<span class="c1"># Go to GMU parking login page
</span><span class="n">driver</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"https://gmu.t2hosted.com/Account/Portal"</span><span class="p">)</span>

<span class="c1"># Enter username
</span><span class="n">username_input</span> <span class="o">=</span> <span class="n">driver</span><span class="p">.</span><span class="n">find_element</span><span class="p">(</span><span class="n">By</span><span class="p">.</span><span class="n">ID</span><span class="p">,</span> <span class="s">"username"</span><span class="p">)</span>
<span class="n">username_input</span><span class="p">.</span><span class="n">send_keys</span><span class="p">(</span><span class="s">"myusername"</span><span class="p">)</span>

<span class="c1"># Enter password
</span><span class="n">password_input</span> <span class="o">=</span> <span class="n">driver</span><span class="p">.</span><span class="n">find_element</span><span class="p">(</span><span class="n">By</span><span class="p">.</span><span class="n">ID</span><span class="p">,</span> <span class="s">"password"</span><span class="p">)</span>
<span class="n">password_input</span><span class="p">.</span><span class="n">send_keys</span><span class="p">(</span><span class="s">"mypassword"</span><span class="p">)</span>
<span class="n">password_input</span><span class="p">.</span><span class="n">send_keys</span><span class="p">(</span><span class="n">Keys</span><span class="p">.</span><span class="n">RETURN</span><span class="p">)</span>

<span class="c1"># (From here, the script navigates menus and purchases the permit)
</span></code></pre></div></div>

<p>The real project handles edge cases, payment, and confirmation, but this snippet shows the basic automation flow.</p>

<h2 id="lessons-learned">Lessons Learned</h2>

<p>A few interesting takeaways while building this:</p>

<ul>
  <li>University systems often don’t have APIs, so <strong>web automation</strong> is sometimes the only option.</li>
  <li><strong>Security matters</strong> – never hardcode sensitive info; use config files or environment variables.</li>
  <li>Even small automations can save you from recurring stress (and are great coding practice).</li>
</ul>

<h2 id="try-it-yourself">Try It Yourself</h2>

<p>If you’re at GMU (or curious about automation), you can try it out:</p>

<p>👉 <a href="https://github.com/rnag/GMU-Daily-Permit-Automation">GitHub Project</a></p>

<p>The <code class="language-plaintext highlighter-rouge">README</code> has setup instructions. Just remember: <strong>use responsibly</strong> — the tool isn’t affiliated with GMU Parking Services, and you should only automate your own account.</p>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>This project started as a way to save myself from procrastination, but it turned into a fun automation experiment. It’s one of those small quality-of-life improvements that adds up.</p>

<p>If you’re a fellow GMU commuter, hopefully this helps you too. And if you’re a developer, maybe it’ll inspire you to automate some of your own daily annoyances.</p>

<p>While Duo Mobile means this isn’t 100% hands-free, it still saves me from repeating the same web clicks every week.</p>]]></content><author><name>Ritvik Nag</name><email>me@ritviknag.com</email></author><category term="tech-tips" /><category term="python" /><category term="automation" /><category term="selenium" /><category term="productivity" /><category term="student-life" /><summary type="html"><![CDATA[A Python + Selenium tool to automate GMU daily parking permits. It’s open source, semi-automated with Duo Mobile 2FA, and saves me from last-minute stress.]]></summary></entry><entry><title type="html">Sleep Mode: Automating Wi-Fi &amp;amp; Bluetooth Off at Night on Apple Devices</title><link href="https://ritviknag.com/tech-tips/sleep-mode-automating-wi-fi-&-bluetooth-off-at-night-on-apple-devices/" rel="alternate" type="text/html" title="Sleep Mode: Automating Wi-Fi &amp;amp; Bluetooth Off at Night on Apple Devices" /><published>2025-05-22T00:26:24-04:00</published><updated>2025-05-22T00:26:24-04:00</updated><id>https://ritviknag.com/tech-tips/sleep-mode-automating-wi-fi-&amp;-bluetooth-off-at-night-on-apple-devices</id><content type="html" xml:base="https://ritviknag.com/tech-tips/sleep-mode-automating-wi-fi-&amp;-bluetooth-off-at-night-on-apple-devices/"><![CDATA[<p class="notice--warning">This article has been cross-posted on my <a href="https://medium.com/@ritviknag">Medium</a> and <a href="https://dev.to/ritvik-nag">Dev.to</a>.
<br /><br />
If you found this post useful, please consider <em>following me</em> on those platforms as I would be really grateful for the support. Thanks!</p>
<!-- {: .notice--info} -->

<hr />

<p>As a runner and fitness enthusiast, sleep is essential for my recovery — and yet, it’s something I’m constantly working to improve. I aim for 8 hours a night, but my Apple Watch reports an average closer to 6.5 hours over the past few months, with some nights dipping as low as 5. Being a light sleeper doesn’t help either. I’ve experimented with wax earplugs to block out noise and blackout curtains to keep light at bay.</p>

<p>One thing I’ve wanted to automate for a while now is disabling Wi-Fi and Bluetooth on the devices near my bed — including both my personal and work MacBooks, my iPhone, and ideally even my Apple Watch (which I wear to track sleep). These devices all stay in my bedroom overnight, so minimizing potential sleep disruptors has become a personal experiment.</p>

<p>You might ask: why go through the trouble of shutting off Wi-Fi and Bluetooth automatically? It’s a fair question. <a href="https://sleepreviewmag.com/sleep-health/parameters/quality/study-raises-concerns-wi-fi-device-radiation-sleep-quality/">This study</a> raises concerns that Wi-Fi radiation may impact sleep quality and increase the risk of insomnia. And this <a href="https://neurologicwellnessinstitute.com/can-wifi-affect-sleep-quality/">video explanation</a> discusses the potential relationship between wireless radiation and disrupted sleep. While scientific research on the topic is still largely inconclusive, I figured that if there’s even a slight chance of improving sleep quality by reducing exposure, it’s worth exploring.</p>

<p>So for the past several months, I’ve had an automation in place that turns off Wi-Fi and Bluetooth on my Apple devices around bedtime — and in this post, I’ll share how I set that up in case you’re looking to try something similar.</p>

<h2 id="creating-the-shortcuts">Creating the Shortcuts</h2>

<p>The first step involves opening the Shortcuts app on a Mac laptop or device. The easiest way is to hit <code class="language-plaintext highlighter-rouge">Cmd + Space</code> and search for <code class="language-plaintext highlighter-rouge">Shortcuts</code>.</p>

<p>The icon should look like this:</p>

<p><img src="https://help.apple.com/assets/6712D663A5C9C17B38070C34/6712D668A5C9C17B38070C3A/en_US/d230a25cb974f8908871af04caad89a1.png" width="30%" /></p>

<p>I’ve created two shortcuts. One called “<strong>Start Morning</strong>” to enable Wi-Fi/Bluetooth, and another called “<strong>Night Time</strong>” to disable Wi-Fi/Bluetooth and put the display to sleep, as shown below. They both use the “<em>Set Wi-Fi</em>” and “<em>Set Bluetooth</em>” actions, which you can find using the Action Library on the right.</p>

<p><img src="/assets/images/1_Shortcuts-StartMorning.png" alt="Start Morning" /></p>

<p><img src="/assets/images/2_Shortcuts-NightTime.png" alt="Night Time" /></p>

<h2 id="creating-automator-scripts">Creating Automator Scripts</h2>

<p>Next, I opened up the <strong>Automator</strong> app — the <code class="language-plaintext highlighter-rouge">Cmd + Space</code> works here too — and go to <em>File &gt; New</em> to create a new automation. I need to do this twice, once for each action.</p>

<p>Search for the “<strong>Run Shell Script</strong>”  action. Then in the settings, select <strong>/bin/bash</strong> as the Shell, and enter this code in the bash code block:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>shortcuts run "Start Morning"
</code></pre></div></div>

<p>Here, <code class="language-plaintext highlighter-rouge">"Start Morning"</code> is the name of your first shortcut. If you named it something else, update the name here accordingly. The executable command <code class="language-plaintext highlighter-rouge">shortcuts</code> can also be run from the Mac Terminal app, and is simply a handy, automated way to interact with your shortcuts on the Shortcuts app.</p>

<p>Here’s a screenshot of this — you can name this <code class="language-plaintext highlighter-rouge">StartMorning.app</code> and save it to your <code class="language-plaintext highlighter-rouge">/Applications</code> folder, but here you can see I’ve saved it to under my <code class="language-plaintext highlighter-rouge">iCloud Drive &gt; Automator</code> , for an important reason I’ll cover later:</p>

<p><img src="/assets/images/3_Automator-StartMorning.png" alt="" width="200%" /></p>

<p>Lastly, go to <em>File &gt; New</em> again and search for the “<strong>Run Shell Script</strong>”  action. Then in the settings, select <strong>/bin/bash</strong> as the Shell as last time, and enter this code in the bash code block:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>shortcuts run "Night Time"
</code></pre></div></div>

<p>Replacing <code class="language-plaintext highlighter-rouge">"Night Time"</code> if you chose to use a different name for the shortcut. I’ve again saved this to my iCloud Drive, as shown below:</p>

<p><img src="/assets/images/4_Automator-NightTime.png" alt="" width="200%" /></p>

<p>Now, there’s a few way to schedule these two automations on a Mac. You can either set up a cron job or, in my case, I chose to use the Calendar app. I got all these steps from an article a long time ago, but now for the life of me I can’t seem to find that article — I guess that’s one reason to bookmark an interesting site that you come across.</p>

<h2 id="scheduling-with-calendar">Scheduling with Calendar</h2>

<p>Regardless, the next step is to open the <strong>Calendar</strong> app on your Mac.</p>

<p>Now, remember how I saved the Automator scripts to iCloud Drive, inside an “Automator” folder? There’s a reason for that. If you’re using just one Mac — say, a personal laptop — saving the scripts to <code class="language-plaintext highlighter-rouge">/Applications</code> works fine. But in my case, I use both a <em>MacBook Air</em> (personal) and a <em>MacBook Pro</em> (work). I wanted the Wi-Fi and Bluetooth toggling to sync across both devices.</p>

<p>After some trial and error, I found that Calendar events sync automatically across Apple devices — but local file paths like <code class="language-plaintext highlighter-rouge">/Applications</code> do not. So, if the script lives in <code class="language-plaintext highlighter-rouge">/Applications</code> on one Mac, the synced calendar event on the second Mac will break. By storing the script in iCloud Drive, both Macs can access the same path, and I only have to set up the Calendar automation once (in theory, at least).</p>

<p>First thing I did was hit the <strong>+</strong> button at the top to create a new Calendar event. I called the first one “<strong>Start Morning</strong>”, set it to <strong>repeat daily</strong> at <strong>8:00 AM</strong>, and the important bit here is to expand the event, click on “Add Alert, Repeat, or Travel Time”, and then to go to where you see <code class="language-plaintext highlighter-rouge">alert: None</code>, click the dropdown, and go to <code class="language-plaintext highlighter-rouge">Custom...</code>, choose <code class="language-plaintext highlighter-rouge">Open File</code> and then in the second dropdown choose <code class="language-plaintext highlighter-rouge">Other...</code> . In the Finder window, locate and select the file location for the Automator script <code class="language-plaintext highlighter-rouge">StartMorning.app</code>  that you saved in previous step. Next, though technically not needed, I clicked the dropdown to alert <strong>15 Minutes before</strong> and changed it to <strong>At time of event</strong>.</p>

<p>You can see the event which turns on Wi-Fi/Bluetooth here:</p>

<p><img src="/assets/images/5_Calendar-StartMorning.png" alt="Start Morning Calendar Event" width="50%" /></p>

<p>Lastly, do the same for the “<strong>Night Time</strong>” event! I set it to <strong>repeat daily</strong> at <strong>11:00 PM</strong> based on my sleep schedule, but feel free to adjust accordingly based on yours! I followed the same steps as above. I chose <code class="language-plaintext highlighter-rouge">Open File</code> and then in the second dropdown choose <code class="language-plaintext highlighter-rouge">Other...</code> . In the Finder window, locate and select the file location for the Automator script <code class="language-plaintext highlighter-rouge">NightTime.app</code>  that you saved in previous step.</p>

<p>You can see the final event which switches off Wi-Fi/Bluetooth here:</p>

<p><img src="/assets/images/6_Calendar-NightTime.png" alt="Night Time Calendar Event" width="50%" /></p>

<p>And that’s it! All my Mac devices will effectively go into Airplane Mode at my sleep time at 11 PM, and wake up from Airplane Mode at 8 AM the next day automatically. Problem solved!</p>

<h2 id="scheduling-on-iphone">Scheduling on iPhone</h2>

<p>Is it possible to schedule the same on an iPhone?</p>

<p>You betcha! It’s actually a bit easier, since you only have to set Airplane Mode, rather than Wi-Fi and Bluetooth individually.</p>

<p>I opened the “<strong>Shortcuts</strong>” app on my iPhone. I created my first automation, which runs on a schedule — at <strong>11:00 PM, daily</strong> in my case — and the action I chose is <strong>Set Airplane Mode &gt; ON</strong>. This is shown below:</p>

<p><img src="/assets/images/7_iPhone_Shortcuts.jpeg" alt="" width="50%" /></p>

<p>Lastly, rather than turn on my iPhone in the morning at a specific time, I chose to create an automation with a trigger that runs when my phone is disconnected from power — since I wirelessly charge it overnight — and the action I chose is <strong>Set Airplane Mode &gt; OFF</strong>. This is shown below:</p>

<p><img src="/assets/images/8_iPhone_Shortcuts.jpeg" alt="" width="50%" /></p>

<h2 id="scheduling-on-apple-watch">Scheduling on Apple Watch</h2>

<p>The final piece of the puzzle is my Apple Watch. Since I wear it every night to track sleep, I’d hoped to schedule Wi-Fi shutoff there as well. Unfortunately, even with the Shortcuts app, there doesn’t seem to be a way to automate Wi-Fi toggling on the Watch. For now, I’ve resorted to doing it manually: unlock the Watch, press the side button, and tap the blue Wi-Fi icon. Not ideal, but a small inconvenience — especially since everything else is automated.</p>

<p>Whether or not Wi-Fi exposure truly impacts sleep quality remains an open question. But for me, this setup offers peace of mind. And that, in itself, helps me sleep better.</p>

<p>Hope this was helpful — and if you end up building something similar, I’d love to hear how it goes.</p>

<p>Thanks for reading.</p>]]></content><author><name>Ritvik Nag</name><email>me@ritviknag.com</email></author><category term="tech-tips" /><category term="sleep" /><category term="automation" /><category term="apple" /><category term="health" /><category term="productivity" /><summary type="html"><![CDATA[How I automated turning off Wi-Fi and Bluetooth on my Apple devices at night to reduce distractions and improve sleep quality — step by step.]]></summary></entry><entry><title type="html">How to Cross the Finish Line on Top</title><link href="https://ritviknag.com/blog/how-to-cross-the-finish-line-on-top/" rel="alternate" type="text/html" title="How to Cross the Finish Line on Top" /><published>2024-10-20T08:56:46-04:00</published><updated>2024-10-20T08:56:46-04:00</updated><id>https://ritviknag.com/blog/how-to-cross-the-finish-line-on-top</id><content type="html" xml:base="https://ritviknag.com/blog/how-to-cross-the-finish-line-on-top/"><![CDATA[<p>2024 has turned into a surprisingly consequential year for my running.</p>

<p>I ran my first half marathon. Then another. Then another. I broke four miles in Orangetheory’s Catch Me If You Can benchmark, finished on the podium at my first trail half marathon, and somehow lowered my mile PR to 4:38.</p>

<p>There are worse problems to have than too many good races to write about.</p>

<p>I originally wanted to document every detail of every one of them. But memories don’t necessarily become more meaningful just because every detail survives. Sometimes the better record is the shorter one: what happened, what stayed with you, and what you learned before moving on to the next starting line.</p>

<p>So, here are a few finish lines from the year.</p>

<h2 id="learning-the-half-marathon">Learning the Half Marathon</h2>

<p>I ran my first half marathon on April 21 in Reston, Virginia, finishing in roughly 1:37.</p>

<p>It was also my introduction to an important principle of distance running:</p>

<p><strong>Hills are evil.</strong></p>

<p>The course had plenty of them.</p>

<p>I’d run 13.1 miles before in training, but racing the distance felt different. A half marathon is long enough to punish early mistakes and short enough to tempt you into making them anyway.</p>

<p>It also introduced me to the surprisingly complicated art of drinking from an aid station while continuing to run.</p>

<p>My early technique consisted primarily of:</p>

<ol>
  <li>grabbing a cup,</li>
  <li>spilling part of it on myself,</li>
  <li>attempting to swallow the remainder without drowning.</li>
</ol>

<p>There has been improvement since.</p>

<p>The race also taught me something I’ve carried into almost every half since: the first few miles can lie to you.</p>

<p>When everything feels easy, running substantially faster seems perfectly reasonable. Several miles later, your body may submit a formal objection.</p>

<p>I would relearn that lesson in Connecticut.</p>

<h2 id="connecticut">Connecticut</h2>

<p>My second half marathon took me to New London for the Ocean Beach/John &amp; Jessie Kelley Half Marathon.</p>

<p>I had recently bought a red Tesla Model 3, and Connecticut became its first proper road trip. The drive north was long enough to teach me a few things about EV road trips, charging stops, and just how much longer a six-hour trip can become once reality gets involved.</p>

<p>We made a larger trip out of it and explored parts of Connecticut and New England before and after the race.</p>

<p>But race morning was the important part.</p>

<p>The Kelley Half started at Ocean Beach Park in New London on August 3.</p>

<p>Unlike my first half, much of the course was relatively flat.</p>

<p>Unfortunately, August had brought another opponent: heat.</p>

<p>As the morning progressed, the sun became increasingly difficult to ignore. Volunteers sprayed runners with hoses along portions of the course, and I became a very willing participant.</p>

<p>Aid stations became increasingly important too. At some point I discovered a technique other runners had already mastered: take a water bottle, drink what you need, dump some over your head, and move on.</p>

<p>Civilization advances through accumulated knowledge.</p>

<p>My larger problem was pacing.</p>

<p>I started aggressively and held something near my 10K pace much longer than I probably should have. It felt excellent.</p>

<p>Until it didn’t.</p>

<p>Somewhere after the halfway point, the bill arrived.</p>

<p>The second half turned into an increasingly stubborn argument between what I wanted my legs to do and what they were interested in doing. I slowed substantially and briefly walked more than once.</p>

<p>Eventually, though, the finish line appeared.</p>

<p>That tends to restore one’s enthusiasm.</p>

<p>I found enough left for a final kick and crossed in just under <strong>1:32</strong>, a new personal best by several minutes.</p>

<p>Not the cleanest race I’ve ever run.</p>

<p>Still one of the most educational.</p>

<p>And Connecticut became another state checked off the list.</p>

<h2 id="four-miles">Four Miles</h2>

<p>One of my favorite Orangetheory benchmarks is <strong>Catch Me If You Can</strong>, a 22-minute treadmill challenge that gradually eliminates runners who don’t reach increasing distance checkpoints.</p>

<p>Once you survive the final checkpoint, the remaining objective is simple:</p>

<p>Run.</p>

<p>I’d wanted to break four miles for quite a while.</p>

<p>The mathematics are unforgiving. Four miles in 22 minutes requires averaging a little under 11 mph for the entire effort.</p>

<p>Previous attempts had convinced me that four miles might remain tantalizingly out of reach.</p>

<p>On August 22, I tried again.</p>

<p>I spent much of the run alternating around 11–11.2 mph, treating 11 mph as “recovery,” which is a somewhat questionable definition of the word.</p>

<p>Twenty-two minutes later:</p>

<p><strong>4.08 miles.</strong></p>

<p>Four miles was no longer hypothetical.</p>

<p>I also finished first on my studio leaderboard.</p>

<p>There’s something satisfying about finally crossing an arbitrary numerical boundary you’ve assigned enormous importance to for no particularly rational reason.</p>

<p>That is, admittedly, much of running.</p>

<h2 id="a-different-kind-of-half-marathon">A Different Kind of Half Marathon</h2>

<p>By October, I was looking for another state.</p>

<p>A trail race in West Virginia caught my attention: the New River Gorge Wilds Trail Run at ACE Adventure Resort.</p>

<p>I had several possible races that weekend, but this one offered something the others didn’t.</p>

<p>I had never raced a half marathon on a real trail.</p>

<p>That sounded interesting.</p>

<p>It also sounded like an excellent opportunity to discover several new forms of suffering.</p>

<p>I signed up.</p>

<p>My previous half marathons had at least given me a decent understanding of road racing. Trail racing immediately discarded much of that confidence.</p>

<p>The terrain demanded constant attention. Pace became much less meaningful. There were climbs, descents, uneven surfaces, and stretches where maintaining rhythm mattered less than simply finding the next good place to put a foot.</p>

<p>It was brutal.</p>

<p>It was also fantastic.</p>

<p>About halfway through, I took the energy gel I’d carried with me and became very thankful that Past Ritvik had possessed the foresight to bring it.</p>

<p>Eventually, after considerably more trail than my legs would have preferred, I reached the finish.</p>

<p><strong>1:50:03.</strong></p>

<p>That would have been much slower than my road half marathon PR.</p>

<p>It was also good enough for <strong>third overall</strong>.</p>

<p><img src="/assets/images/finish_wild_gorge.jpeg" alt="Crossing the Finish Line" /></p>

<p>That result reinforced something I’ve gradually learned about racing: a finishing time tells you very little without the course attached to it.</p>

<p>A 1:50 trail half can require far more from you than a much faster road race.</p>

<p>More importantly, I had finished my first trail half marathon and put West Virginia on the map.</p>

<p>I decided 13.1 miles was also approximately the maximum distance for which I wanted to make friends with that particular terrain.</p>

<p>For now.</p>

<h2 id="438">4:38</h2>

<p>Then came the mile.</p>

<p>The mile is a wonderfully unpleasant distance.</p>

<p>It’s too long to sprint and too short to settle in.</p>

<p>I had previously run exactly 5:00 while traveling. Since then, I’d lowered my PR further, but I wanted another shot.</p>

<p>On October 16, Orangetheory held the benchmark again.</p>

<p>For this attempt, I wore a relatively new pair of Nike Vaporfly 3s.</p>

<p>Carbon-plated racing shoes are peculiar things. They’re light, aggressive and slightly ridiculous, and the Vaporflys immediately felt much faster than my ordinary trainers.</p>

<p>Race strategy was simple enough.</p>

<p>Start fast.</p>

<p>Hold on.</p>

<p>Discover whether this was a mistake later.</p>

<p>I opened around 13 mph.</p>

<p>For the first several minutes, everything remained under control—or at least maintained a convincing appearance of being under control.</p>

<p>Then the usual negotiations began.</p>

<p>I dropped the pace slightly when my legs started objecting, tried several times to bring it back up, and saved whatever remained for the final stretch.</p>

<p>By then, the mile had ceased to be an athletic activity and become mostly an exercise in refusing to press the slower-speed button.</p>

<p>Finally, the treadmill crossed one mile.</p>

<p><strong>4:38.</strong></p>

<p>A nine-second PR.</p>

<p>And first on the studio leaderboard.</p>

<p>One comparison put the effort into perspective afterward: the pace I’d barely maintained for one mile was roughly in the neighborhood of the pace Eliud Kipchoge sustained during his famous sub-two-hour marathon exhibition.</p>

<p>I survived it for a little over four and a half minutes.</p>

<p>He continued for another twenty-five miles.</p>

<p>Running has a reliable way of making an achievement feel impressive and humbling at exactly the same time.</p>

<h2 id="the-finish-line-keeps-moving">The Finish Line Keeps Moving</h2>

<p>There were other milestones this year.</p>

<p>I ran about 120 treadmill miles during Orangetheory’s Marathon Month and finished first at my studio. I substantially improved my DriTri time. There were training runs, good workouts, terrible workouts, small PRs and probably several things I’ve already forgotten.</p>

<p>That’s part of why I wanted to write this.</p>

<p>Not because every workout needs to become a permanent artifact on the internet.</p>

<p>Most probably shouldn’t.</p>

<p>But some moments are worth marking.</p>

<p>My first half marathon.</p>

<p>My first destination half.</p>

<p>My first trail podium.</p>

<p>Four miles in Catch Me If You Can.</p>

<p>A 4:38 mile.</p>

<p>Individually, they’re just numbers and races.</p>

<p>Together, they tell me something more interesting: the ceiling keeps moving.</p>

<p>Every so often I reach a number that once seemed absurd, enjoy it briefly, and almost immediately wonder what the next one might be.</p>

<p>Perhaps that’s the real trick to crossing the finish line on top.</p>

<p>Enjoy the view.</p>

<p>Then find another starting line.</p>]]></content><author><name>Ritvik Nag</name><email>me@ritviknag.com</email></author><category term="blog" /><category term="run" /><category term="running" /><category term="race" /><category term="half-marathon" /><category term="trail-running" /><summary type="html"><![CDATA[2024 has turned into a surprisingly consequential year for my running.]]></summary></entry><entry><title type="html">On My First Race and Running</title><link href="https://ritviknag.com/blog/on-my-first-race-and-running/" rel="alternate" type="text/html" title="On My First Race and Running" /><published>2024-04-28T01:11:40-04:00</published><updated>2024-04-28T01:11:40-04:00</updated><id>https://ritviknag.com/blog/on-my-first-race-and-running</id><content type="html" xml:base="https://ritviknag.com/blog/on-my-first-race-and-running/"><![CDATA[<p>I ran my first half marathon this April. It wasn’t perfect — I got lost (twice), spilled Gatorade on myself mid-stride, and battled dehydration, rogue hills, and tired legs. But somehow, I crossed the finish line — first in my age group, and fifth overall.</p>

<p>This is a story about more than just a race. It’s about rediscovering a long-lost joy, learning on the go, and pushing the body beyond what it thinks it can do. If you’ve ever wondered what it’s like to run 13.1 miles for the first time — the chaos, the adrenaline, the tiny victories — read on.</p>

<hr />

<p>This month, on Sunday, April 21st, I participated in my first half marathon race.</p>

<p>There’s a lot to unpack there and talk about, and not sure if I’ll have the time or dedication to do it. But sometimes you gotta start somewhere, so here I go.</p>

<p>I signed up for the race on March 27th, about a whole month before the race. I paid about $100 for the registration, which certainly feels like I’m getting milked for all I’m worth.</p>

<p>The flyer for the race was being displayed at my gym for the longest time, and I figured, why not, it’s close to home and I’ve been thinking about when and if I’ll ever do a marathon lately, wouldn’t it be easier to train for and start with a half marathon first?</p>

<p>So anyway, that’s the story of how I signed up for my first half marathon.</p>

<p>A large impetus for signing up for my first half, was the passion for running that’s been rekindled in me recently. Ever since I started going back to gym, since I started Orangetheory, a little over a year ago now, the joy (and spark) of running has been alive in me again. It’s like re-learning how to use an extraneous muscle. Like riding a bike again. You never forget how that used to be. You just learn more about it, dive deeper into the sport, and introspect further and learn how your body handles it, and adapts to it.</p>

<p>Anyway, consistency is key, and I would give credit where credit is due - the gym I go, Orangetheory Fitness, has especially been inspiring me to helping me to stay consistent and motivated. Further, they have motivational work outs and benchmark challenges every now and then. For example, the 1 mile run, a 12-minute timed tread for distance, and a longer 22-minute run. More recently, they have something called Tread 50 workouts, which is just a 45-minute block where you have access to the treadmill. Which is great to try out something new, or to prep for an upcoming work out or challenge. It’s all up to you, but the idea here is that motivation is never lacking.</p>

<p>If anything, the benchmark challenges for running are fun, and well — <em>challenging</em> — and they inspire me to put my best foot forward.</p>

<p>They also re-kindle in me once again, the joy for running. This is actually a large part of the reason why I signed up for my first half marathon. To fully embrace my newfound (or re-discovered?) joy of running, and share that with others. I do love running, and I hope to continue it going forward.</p>

<p>One other thing. Running on the treadmill, believe it or not, is sub-optimal to preparing for a half marathon. For training purposes, it is indeed sub-optimal. So while I have been signing up for Tread 50 and challenging myself to run at base or base-push the whole 45-minute block, without stopping, it is lacking in something. Don’t get me wrong, it keeps me on my toes, keeps me active and in shape. It builds endurance and willpower.</p>

<p>What the treadmill does not build, is leg strength, or real-world experience. Experience with hills and just the feel of the stones and rough asphalt under your feet. That you’ll only ever get with outdoor running. Freed from the metaphorical chains, without the treadmill running belt to help you, you are effectively at the mercy of Mother Nature and the elements. The biting cold. Rain pouring down. Wind gusts battering you at over 10 MPH, slowing down your progress and grinding it to a near halt. Making your very bones shake. Causing your mind to falter in its resolution and drive. This is the sort of thing that one would only ever get exposed to by choosing to embrace the elements, and run outdoors.</p>

<p>So slowly, I have been building a routine in order to prepare myself and train for my first race. At the end of March, I ran roughly a 10K with a running group. An average pace around 8:30/mile. Cadence of 167 SPM.</p>

<p>Between March and April, I have been running at least a 10K outdoors, every week — and I hope to continue this routine, and this <em>consistency</em>, moving forward. Because if there’s anything that Orangetheory and working out has taught me, it’s that consistency is key.</p>

<p>Anyway — the next week, start of April, I knocked out another 10K outdoors, and averaged an 8:00/mile pace. Cadence of around 172 SPM. So overall, slightly improved from last time.</p>

<p>The following day, for the very first time, I did a longer run outdoors. I am talking over 10 miles. Prior to this, I had never run this sort of distance before, in my entire life.</p>

<p>I ran outside at the WO &amp; D trail, which is close to me and was recommended to me by friends and family both. While I did 16 miles total, it looks like I only ran about 12 miles of that. I remember the reason why that was. I had got my new waist pack which was a gift from the fam. It had holders for mini- plastic bottles with little plastic sippy-heads (microplastics, yay!) and pockets which I could put my keys and phone in.</p>

<p>There were only two annoying things with my first long run outdoors:</p>

<ul>
  <li>I had no idea how to use Strava, and I wanted to stream the run to Strava, which is some new app for runners. It took me too long to figure out.</li>
  <li>The strap for the waist pack was too loose. I tried to tighten it but it kept falling off and it was mighty frustrating.</li>
</ul>

<p>Regarding the first point, I walked the first 2 miles because I was fiddling around with Strava and trying to record the exercise from within that app. I also started the outdoor activity on my Apple Watch. In hindsight, one thing I learned — Strava can auto-import workouts from the Apple Watch, so you only realistically need to record it on there, don’t need to do it twice as I did.</p>

<p>Regarding the second point, adjusting the waist pack was an eternal struggle, and I don’t know if I ever got it fully correct that whole 16 mile walk-run effort. After the first ten minutes, I wanted to just remove my waist pack and toss it to the ground, and stamp on it. I was that frustrated. But thankfully, after I did some last-minute adjusting, it stayed on, and didn’t fall off at least, even if it was kind of loose. So <em>shrug</em>, better than nothing, so I shrugged it off and started the run.</p>

<p>Plus, I needed the waist pack. For my AirPods case, and keys. Phone so that I could listen to music, since my watch didn’t have 3G or cellular. So given that my phone was bulky, it wouldn’t be possible to fit everything in my pants pocket and run with it. At least, it would be uncomfortable to run like that. So, in that sense the waist pack really delivered and came through. That’s not even considering the water. Water really helps to stay hydrated on a longer (over 10 miles) run outdoors.</p>

<p>So anyway, that first long run outdoors, my average pace was 10:57/mile, which was bad because I walked about 4 miles of that. Looks like I burned almost 2000 calories from that (1600 active calories), so there is a silver lining with that. All in all, I was out of breath near the end, and my legs were essentially destroyed after a run of 12 miles. This is why I had to walk the last 2 or 3 miles. Well, the reason I was forced to walk the last distance, actually is that I overshot when I was back-tracking or looping back. Went too far, guess you could say. So I needed to back-track yet again to get to where I had parked my car. In order to do that, my legs and knee was totaled, so literally I could not run, I could only walk (or limp) back. I contemplated more than once on taking an Uber back, but ultimately I just walked the half hour or so back. It wasn’t the end of the world. At least, mission accomplished there with that long run. Longest run I’ve run in my entire life leading up to that, was on that day.</p>

<p>Roughly 10 days after that, because I was having a hard time finding time, I went for a run at 7AM on a weekday. Yeah, that’s right. I wanted to wrap up around 9AM. So I ran pretty fast due to that. Here are the results in the screenshot below.</p>

<p><img src="/assets/images/half-training.jpeg" alt="Half Marathon Training Results" width="70%" /></p>

<p>Looks like my best half-marathon time was 1:31:08, as Apple Watch claims. I don’t know if this is accurate or not. If so, it would be my fastest half marathon time ever, which is great.</p>

<p>Anyway, very tired after that, so I went back, drank electrolytes, whey protein to recover, and iced my knees.</p>

<p>On Sunday, April 21st, I had my first half marathon race, so here’s how that went.</p>

<h2 id="first-half-marathon">First Half Marathon</h2>

<p>On Sunday, April 21st, I had my first race ever, a half marathon.</p>

<p>The day before, I had the carbo-load meal at Davio’s in Reston. Portions were small, but it tasted great. They charged me close to $50 for that. But there was pasta, and there was carbs abounding. So not much to complain.</p>

<p>That day I went to sleep early like around 11am.</p>

<p>The day of the race, I woke up about 5:30am. Got ready, drank some matcha, had early morning banana, apple, and oatmeal with berries. Also had a honey protein bar. Then I got dressed, laced up my bright orange Hoka Mach 5 shoes, which a subreddit on Reddit had convinced me to wear, and off to the race I went.</p>

<p>It was less than a 15 minute drive for me to the race’s gathering point.</p>

<p>One thing I had desired in my first race, my first half for that matter, is a good distance for commute purposes. I didn’t want to fly, didn’t want to drive 4 hours for a race. So having a first half marathon so close to home, was a godsend for this reason. I didn’t have to worry about commute. I honestly could eliminate that issue as otherwise it would be another stress point for me.</p>

<p>Fast forward to the start of the race. If I remember, I joined the middle of the pack. I wanted to take it somewhat easy, but not too easy. I still wanted to challenge myself. So I tried to stay with some folks who seems like they were maintaining a 7:30-8:00/mile pace, which is a pretty respectable, but easy enough, pace to be running at.</p>

<p>Here is a photo of me on that race:</p>

<p><img src="/assets/images/RMR_half_on_race_1.jpg" alt="RMR Half Marathon - On Race #1" width="70%" /></p>

<p>Most of what I remember from that race, my very first, is that I probably had ate a bit much, and also I seemed to lack energy, since potentially I had not carb-loaded properly. It’s always hard to estimate these things, especially for one’s first race. Also, I was for some reason thirsty, all the time. I didn’t carry my waist pack with me on this run, in an attempt to slash on unnecessary weight, and potentially gain speed and pick up the pace as a result.</p>

<p>That reminds me, it was about 45 degrees out that morning, but I was wearing a t-shirt and shorts, and I saw lot of others wearing similar attire, so thankfully I was not alone. Some folks were even wearing sleeveless. Then again I saw lot of others in long-sleeved and even pants. Because 45 degrees is understandably chilly for some.</p>

<p>Anyway, going back to hydration. At around mile 3 or 5, some volunteers were on the sidelines offering water and gatorade, so I snagged one quick. I tried to drink while jogging, a mistake, as I spilled some on my shirt. I tossed it after I’d drunk most of it, but the damage was done. My shirt was soaked and I wasn’t too well hydrated. On top of that I had lost on pacing slightly, since I had slowed down. So turns out, bad idea overall.</p>

<p>Most of what I remember on that run, on my first half marathon, basically boils down to five things. One, it was a scenic run, as promised, lot of green grass and vegetation, and trees, and overall just a pleasant run. Two, the weather was perfect and it was a little bit chilly but that’s much preferred over humid any day of the week. Three, so many hills, and I was not prepared for them. Four, did I mention dehydration, as it always seemed as my throat was parched. Five, my lungs and mind were fighting me constantly, but my legs only after mile 10.</p>

<p>Let’s dive a little bit deeper into some points. Starting with the hills. The uphills were especially brutal, and even though there were hills on the WO&amp;D trail, they were nothing like these. The uphill jog efforts really snuck up on you, and took their toll on me. The downhills were much better.</p>

<p>In the words of a volunteer on the sidelines —</p>

<blockquote>
  <p>Gravity is your friend!</p>
</blockquote>

<p>That basically is a nod to the fact that downhill, running is so much easier. Actually gravity carries your legs faster and further than you could normally run unaided. This is actually the reason why some world records for the mile run were discounted — because the track in those cases were downhill, which is not fair since other competitors didn’t have that edge. Make no doubt, running downhill certainly gives you an edge, at least compared to running on flat ground, and most certainly when compared to running uphill. Long story short, running uphill sucks, running downhill is a godsend.</p>

<p>More on the dehydration aspect. I felt thirsty, like my throat was parched, constantly. At mile 3, I drank water. I drank gatorade again at mile 6, and same as last time I tried to drink while running, and spilled the fluids on myself again. At mile 11, I snagged gatorade, and gave in to my mind telling me to take a break, also because my legs were completely beat, and I just stood still and finished that cup of gatorade. Wasting a lot of time and crashing my pace, since I was immobile in one spot, but what can you do, when both your legs and mind are fighting you over your sustained efforts. Anyway, I estimate a total of 3-4 water or gatorade stops on my part. Too many frequent breaks I suppose, but for a first half marathon I suppose it can be forgiven.</p>

<p>Lastly, about my lungs and mind fighting me. I did try to keep up with the group, in order to pace myself better, and mostly I stuck with this one dude until about mile 7. He was a pretty good pacer, and we were both averaging about 7:20/mile. Certainly under 7:30/mile. It felt good to have someone to pace with, to run with, so I just decided to have a relaxed run in that manner, us alternating taking the lead, and the lungs issue was tabled for a while.</p>

<p>At around mile 7, I broke up and sped off in an (unrealistic) effort to catch up with the frontrunners, and also to challenge myself to push the pace, and get a decent finish time.</p>

<p>Another photo, ostensibly taken when I decided to “fly solo” at around mile 7-8:</p>

<p><img src="/assets/images/RMR_half_on_race_2.jpg" alt="RMR Half Marathon - On Race #2" width="70%" /></p>

<p>This is more or less where I started having a “consistent” pacing issue, and at the same time started having a breakdown of my lungs and mind unanimously. The hard part was not starting at the distance readout on my Apple Watch every few minutes. Seeing those mile plaques or posters or whatever, on the race, pop up every mile as a benchmark, was actually more disheartening than reassuring. It felt like how much distance I’d achieved, and how much my lungs were burning and screaming at me, were totally at odds.</p>

<p>At around mile 9, my lungs were almost at my limit and I just wanted to throw in the towel. At mile 10, it felt like an eternity in coming, but at the same time, that’s when you’re supposed to realize that it’s only a 5K distance left. That is, 3.1 miles remaining. So I was <em>supposed</em> to speed up, because it’s only a 5K race now, but in reality I couldn’t. My lungs had been fighting me every mile of the way, and apparently now it was my legs’ turn to take over the battle.</p>

<p>It seemed unfair, for the legs to refuse to move at the same pace, to slacken and grow tired, but that’s exactly what ended up happening. Rather than speeding up for the last 5K distance, I struggled even with maintaining what I assumed was my 7:30/mile pace that I had going on. Maintaining that pace was the hardest part. Shouting down my mind that told me to walk it out, and punishing my lungs for fighting me by continuing the battle with them, seemed especially cruel but necessary.</p>

<p>Anyway, near the end, I almost got lost twice. Once when I didn’t know if I should take a right or go straight. Some volunteer sitting in a pickup truck told me to go straight after I asked. But again, that questioning wasted no small amount of time.</p>

<p>Finally, at last leg of the race, once I got on the track again, where the race was supposed to start and end, I again got lost. I saw “Finish” and supposed I should go left. I was shooed by volunteers and told to go right. Then I stopped prematurely, assuming I was done, only to be told to keep going, I have to go all the way to the “Finish” line.</p>

<p>If you’d ask me, this last part was not coordinated or handled well. Why is it so easy to get lost on that last home stretch, while on the track and you see the words reading “Finish”? I don’t know. It seems like it shouldn’t be so easy to get lost, but it was, for me at least.</p>

<p>Running the whole length of that track to the Finish line seemed like it took an eternity by itself.</p>

<p>You can see how tired and out of breath I was, and must have been, by seeing how I looked right before I punched through the finish line:</p>

<p><img src="/assets/images/RMR_half_finish_1.jpg" alt="RMR Half Marathon - Finish #1" width="67%" /></p>

<p>By getting lost twice and stopping for breaks to hydrate, I must have wasted about 2 or 3 minutes easily by that itself. But again, sometimes it’s the journey that matters more than the destination. Given that it was my first race ever, and first half marathon, I really was not expecting to break a PR or get first place overall — that would have been unrealistic at any rate.</p>

<p>You can see the joy etched on my face, of having finally finished that dreadful half marathon, and returning power back to – and releasing the noose around – my lungs and my blessed legs, that carried me so far. 13.1 miles, all the way to the finish line.</p>

<p><img src="/assets/images/RMR_half_finish_2.jpg" alt="RMR Half Marathon - Finish #2" width="55%" /></p>

<p><img src="/assets/images/RMR_half_finish_3.jpg" alt="RMR Half Marathon - Finish #3" width="55%" /></p>

<p>Anyway, I finished first in my age group, and fifth overall in the race.</p>

<p>I got a finish time of 1:36:41 for my first half marathon.</p>

<p>Here’s a splendid, dashing photo of me basking in my glory and victory, after the conclusion of the race:</p>

<p><img src="/assets/images/RMR_half_after_race.jpg" alt="RMR Half Marathon - After Race" width="70%" /></p>

<p>Can I say it was worth it?</p>

<p>Absolutely, for the experience along. Even though that first half marathon, felt like a <em>full</em> marathon to me, it taught me some valuable lessons on endurance and leg strength especially, which I hope to fully utilize and learn from going forward.</p>

<p>Going forward, I will plan to incorporate some amount of long runs (at least 10 miles) outdoors, at a frequency of 2-3 weeks. This is just so that I maintain the endurance and leg strength that my first half marathon race has imparted to me.</p>

<p>One thing I enjoyed about my first half marathon is it enabled me to display and embrace my passion of running, and even after finishing it, the passion and respect for running has only increased, it has not reduced or stayed the same.</p>

<p>Going to cut it short here. I am proud of my achievement of my first half marathon, and hope to continue incorporating lessons I have gained from it, such as improved endurance and fitness. I aim to shoot for running at least a 10K distance outdoors once a week, and a 10+ miles outdoor run at least 1-2 times a month.</p>

<p>Participating in my first half marathon has imparted to me much-needed endurance and leg strength, and I hope to continue running, pushing myself, and next time around I hope to improve on my half marathon finish time. Also, here’s to hoping I don’t get lost or get frazzled by those hydration breaks in my next half marathon 😜.</p>]]></content><author><name>Ritvik Nag</name><email>me@ritviknag.com</email></author><category term="blog" /><category term="half marathon" /><category term="run" /><category term="running" /><category term="endurance" /><category term="fitness" /><category term="first race" /><summary type="html"><![CDATA[My first half marathon: hills, hydration mishaps, and a finish line I almost missed — but somehow, I still placed first in my age group.]]></summary></entry><entry><title type="html">Navigating Medium API with JavaScript in 2024</title><link href="https://ritviknag.com/tech-tips/navigating-medium-api-with-javascript-in-2024/" rel="alternate" type="text/html" title="Navigating Medium API with JavaScript in 2024" /><published>2024-02-17T19:07:00-05:00</published><updated>2024-02-17T19:07:00-05:00</updated><id>https://ritviknag.com/tech-tips/navigating-medium-api-with-javascript-in-2024</id><content type="html" xml:base="https://ritviknag.com/tech-tips/navigating-medium-api-with-javascript-in-2024/"><![CDATA[<p class="notice--warning">This article has been cross-posted on my <a href="https://medium.com/@ritviknag">Medium</a> and <a href="https://dev.to/ritvik-nag">Dev.to</a>.
<br /><br />
If you found this post useful, please consider <em>following me</em> on those platforms as I would be really grateful for the support. Thanks!</p>
<!-- {: .notice--info} -->

<hr />

<h2 id="background">Background</h2>

<p>I’m trying to integrate Medium blogging into my personal site at <em>myname.com</em>.</p>

<p>That is, I’m trying to automate the process of publishing an article on my personal blog, and have the contents of the Jekyll-based markdown post – a <code class="language-plaintext highlighter-rouge">.md</code> file – be posted to the two other blogging sites I’m currently signed up on, <a href="https://medium.com"><code class="language-plaintext highlighter-rouge">medium.com</code></a> and <a href="https://dev.to"><code class="language-plaintext highlighter-rouge">dev.to</code></a>.</p>

<p>Towards that end, I tried to search online to see if there is such as a thing as a Medium API that allows me to:</p>

<ul>
  <li>Publish and/or update a post for a user</li>
  <li>Retrieve a list of posts for a user</li>
</ul>

<p>Turns out, only the first point is currently possible via the <a href="https://developers.medium.com/">Official Medium API</a>, and that too, it’s only <em>partially</em> satisfied – there is currently no route via the API to update a post on a Medium. One can only <strong>create</strong> a new post on Medium via the API, but that’s about it.</p>

<p>With the Official <a href="https://github.com/Medium/medium-api-docs"><strong>Medium API</strong></a> as it stands – which is no longer supported or maintained by the Medium team – one can only retrieve user info and create a post, they cannot do anything much more useful than that, for example it is not possible to retrieve a list of a user’s posts via the API.</p>

<h3 id="how-to-auto-update-a-medium-post">How to Auto-Update a Medium Post</h3>

<p>This is slightly tangential to the matter, but I haven’t expressed my discontent with not being able to automate the updating of a post on Medium via an API. I fooled around this with their API – for example using an HTTP <code class="language-plaintext highlighter-rouge">PUT</code> request - for way too long, and didn’t get anywhere with that at all. It’s a real shame that we can only publish new posts via the API, and that it’s not possible to update an existing post via an API.</p>

<p>In fact, this proved the impetus or drive for me to publish an online web tool that easily translates posts from <em>markdown</em> to <em>medium</em>, which I have published on my website:</p>

<blockquote>
  <p><a href="https://ritviknag.com/markdown-to-medium">https://ritviknag.com/markdown-to-medium</a></p>
</blockquote>

<p>I made this above tool for myself, so I can take my personal blog posts, written in <code class="language-plaintext highlighter-rouge">.md</code> files, paste them in there and copy the output to Medium when using the editor to update an existing post.</p>

<hr />

<p>Anyway, slight detour there…</p>

<p>All this to say that I was not able to find a solution via the API that worked for me, to list all the posts under a given user. The reason for this is purely for automation purposes, such that whenever I change a blog post – or <code class="language-plaintext highlighter-rouge">.md</code> file – on my website, I want to publish a new post to Medium, but <em>only</em> if there is not already an existing post with that same title. So basically, I want to avoid re-publishing a post every time I make an update to it – if that’s even possible!</p>

<h2 id="my-search">My Search</h2>

<p>My search led me to Google search for something along the lines of “<strong>medium api get all user posts</strong>”, which led me more or less to this somewhat informative article on StackOverflow:</p>

<blockquote>
  <p><a href="https://stackoverflow.com/q/36097527/10237506">https://stackoverflow.com/q/36097527/10237506</a></p>
</blockquote>

<p>As the accepted answer says, the Medium API is only intended for <em>Publishing</em> and is not intended to retrieve posts.</p>

<p>A workaround that is suggested is to simply use the RSS feed that Medium offers, as such:</p>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">https://medium.com/feed/@username</code></p>
</blockquote>

<p>For example, here is the RSS feed for my user:</p>

<blockquote>
  <p><a href="https://medium.com/feed/@ritviknag">https://medium.com/feed/@ritviknag</a></p>
</blockquote>

<p>One can simply get the RSS feed via <code class="language-plaintext highlighter-rouge">GET</code>, then if it’s needed in JSON format just use an NPM module like <code class="language-plaintext highlighter-rouge">rss-to-json</code> and we’re good to go.</p>

<p>However, there is an issue with this approach, as another answer in the same SO post confirms:</p>

<blockquote>
  <p>[…] has a limit of the <strong>latest 10 posts</strong>.</p>
</blockquote>

<p>So, there is a limit of only retrieving 10 posts for a given user using the RSS feed option.</p>

<p>Looks like we need another solution!</p>

<h2 id="unofficial-medium-api">Unofficial Medium API</h2>

<p>I would be remiss if I didn’t mention an alternate approach.</p>

<p>There is an <strong>Unofficial</strong> Medium API that someone has come up with. This is the link to it below:</p>

<blockquote>
  <p><a href="https://mediumapi.com">https://mediumapi.com</a></p>
</blockquote>

<p>Unfortunately, it’s not an insignificant commitment to sign up with that API. Take a look below:</p>

<p><img src="/assets/images/medium-api-sub.png" alt="Unofficial Medium API Subscription" /></p>

<p>As noted, there appears to be a subscription tier for using that API.</p>

<p>If I, as a developer, want to build an API wrapper around the Medium SDK to retrieve a User’s posts, using this option I would be forced into:</p>

<ul>
  <li>Create a RapidAPI key for <code class="language-plaintext highlighter-rouge">mediumapi.com</code>, or directing users of the library to sign up on <em>another</em> site and acquire a RapidAPI key for requests to <code class="language-plaintext highlighter-rouge">mediumapi.com</code>.</li>
  <li>Rate limiting my library at <strong>150 requests / month</strong>, assuming I want to hardcode and provide users with my RapidAPI key, in order to provide convenience and promote adoption of said library.</li>
</ul>

<p>In short, this is likewise a disaster, albeit of different proportions. There is <strong>no way</strong> I can use a third-party, unofficial API, even though I have personally tested out this API endpoint and confirmed that it returns the correct and desired data.</p>

<p>Thus, the unavoidable road to the <strong><em>DIY project</em></strong>, begins…</p>

<h2 id="the-eureka-moment">The “Eureka!” Moment</h2>

<p>To me, the lightbulb went off, when I was just bored and staring at a random Medium user’s profile, and I had Chrome Developer tools open and was just browsing casually through the <code class="language-plaintext highlighter-rouge">Network</code> tab.</p>

<p>The <code class="language-plaintext highlighter-rouge">graphql</code> endpoints, in particular, stood out to me:</p>

<p><img src="/assets/images/chrome-dev-graphql.png" alt="Chrome Dev GraphQL" height="200%" /></p>

<p>If curious, the URL to that is <code class="language-plaintext highlighter-rouge">https://medium.com/_/graphql</code>, and it’s an HTTP <code class="language-plaintext highlighter-rouge">POST</code> request that gets sent.</p>

<p>Shortly after, I fired up Google search again and this time inputted keywords “<strong>how to use medium api graphql to fetch all user posts</strong>”.</p>

<p>The result was this highly informative post on using Medium’s GraphQL endpoint to retrieve Medium posts for a user:</p>

<blockquote>
  <p><a href="https://medium.com/@mike820324/web-crawler-getting-medium-post-a6e52fd36fd6">https://medium.com/@mike820324/web-crawler-getting-medium-post-a6e52fd36fd6</a></p>
</blockquote>

<p>I decided to implement the same logic in the JavaScript (Node.js + TypeScript) library I was writing, with an intent to publish as an NPM package to <a href="https://npmjs.com/"><code class="language-plaintext highlighter-rouge">npmjs.com</code></a>.</p>

<p>The hardest part for me was how to:</p>

<ul>
  <li>Modify the GraphQL query to only return user post titles</li>
  <li>Paginate through the results, as GraphQL appears to have a limit of <code class="language-plaintext highlighter-rouge">25</code> results (posts) per request.</li>
</ul>

<p>I managed to figure that out, even though it took me a long while.</p>

<p>By the way, I adapted the code from <a href="https://github.com/redco/medium-sdk-nodejs">a fork of the Medium SDK for Node.js</a>.</p>

<p>I made my own changes, and a <strong><em>lot</em></strong> of updates along the way.</p>

<p>Simply said, this was the very first <em>TypeScript NPM package</em> I had ever written. So there was a lot of learning curve.</p>

<h2 id="the-result">The Result</h2>

<p>The work was split into a few main parts:</p>
<ul>
  <li>Porting over existing Medium SDK code from <em>Node.js</em> to <em>TypeScript</em>.</li>
  <li>The hurdle that come with publishing a TypeScript NPM package, such as setting up <code class="language-plaintext highlighter-rouge">tsconfig.json</code> to allow support for <a href="https://www.sensedeep.com/blog/posts/2021/how-to-create-single-source-npm-module.html">both ESM and CommonJS syntax</a> simultaneously.</li>
  <li>Integrating pagination and GraphQL into the codebase.</li>
  <li>Best practices for NPM projects in TypeScript, such as setting up code quality checks and CI/CD automation – such as publishing package to a registry (NPM) automatically.</li>
</ul>

<p>This project is the direct result of my hard work and effort:</p>

<p><a href="https://github.com/rnag/medium-sdk-ts">https://github.com/rnag/medium-sdk-ts</a></p>

<p>Below is the package on the NPM registry:</p>

<p><a href="https://www.npmjs.com/package/medium-sdk-ts">https://www.npmjs.com/package/medium-sdk-ts</a></p>

<h3 id="install">Install</h3>

<p>The package can be installed with <a href="https://www.npmjs.com/">npm</a>, or a package manager of choice:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i medium-sdk-ts
</code></pre></div></div>

<h3 id="import">Import</h3>

<p>The library supports importing via the newer, more modern <em>ES Module (ESM)</em> syntax prevalent in TypeScript code:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span>
  <span class="nx">MediumClient</span><span class="p">,</span>
  <span class="nx">PostContentFormat</span><span class="p">,</span>
  <span class="nx">PostPublishStatus</span><span class="p">,</span>
<span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">medium-sdk-ts</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>

<p>Or, for <em>CommonJS</em> syntax in Node.js, using <code class="language-plaintext highlighter-rouge">require</code>:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span>
    <span class="nx">MediumClient</span><span class="p">,</span>
    <span class="nx">PostContentFormat</span><span class="p">,</span>
    <span class="nx">PostPublishStatus</span><span class="p">,</span>
<span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">medium-sdk-ts</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="usage">Usage</h3>

<p>Now create a client for the <strong>Medium SDK</strong>:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Access Token is optional, can also be set</span>
<span class="c1">// as environment variable `MEDIUM_ACCESS_TOKEN`</span>
<span class="kd">const</span> <span class="nx">medium</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MediumClient</span><span class="p">(</span><span class="dl">'</span><span class="s1">YOUR_ACCESS_TOKEN</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>

<p>Retrieve User Details:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">medium</span><span class="p">.</span><span class="nx">getUser</span><span class="p">();</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`User: </span><span class="p">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">user</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="mi">2</span><span class="p">)}</span><span class="s2">`</span><span class="p">);</span>
</code></pre></div></div>

<p>Publish a new <em>Draft Post</em> under your user:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">post</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">medium</span><span class="p">.</span><span class="nx">createPost</span><span class="p">({</span>
    <span class="c1">// Only `title` and `content` are required to create a post</span>
    <span class="na">title</span><span class="p">:</span> <span class="dl">'</span><span class="s1">A new post</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">content</span><span class="p">:</span> <span class="dl">'</span><span class="s1">&lt;h1&gt;A New Post&lt;/h1&gt;&lt;p&gt;This is my new post.&lt;/p&gt;</span><span class="dl">'</span><span class="p">,</span>
    <span class="c1">// Optional below</span>
    <span class="na">contentFormat</span><span class="p">:</span> <span class="nx">PostContentFormat</span><span class="p">.</span><span class="nx">HTML</span><span class="p">,</span>   <span class="c1">// Defaults to `markdown`</span>
    <span class="na">publishStatus</span><span class="p">:</span> <span class="nx">PostPublishStatus</span><span class="p">.</span><span class="nx">DRAFT</span><span class="p">,</span>  <span class="c1">// Defaults to `draft`</span>
    <span class="c1">// tags: ["my", "tags"],</span>
    <span class="c1">// canonicalUrl: "https://my-url.com",</span>
<span class="p">});</span>

<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`New Post: </span><span class="p">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">post</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="mi">2</span><span class="p">)}</span><span class="s2">`</span><span class="p">);</span>
</code></pre></div></div>

<p>Retrieve a User’s Published Posts (using GraphQL endpoint, which does not require an access token):</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Get User's Published Posts</span>
<span class="kd">const</span> <span class="nx">posts</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">medium</span><span class="p">.</span><span class="nx">getPosts</span><span class="p">(</span><span class="dl">'</span><span class="s1">@username</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`User Post: </span><span class="p">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">posts</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="mi">2</span><span class="p">)}</span><span class="s2">`</span><span class="p">);</span>
</code></pre></div></div>

<p>Or, to simply retrieve the user’s post titles only – note that this results in a slightly more simplified GraphQL query being sent:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Get User's Published Posts (Title Only)</span>
<span class="kd">const</span> <span class="nx">postTitles</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">medium</span><span class="p">.</span><span class="nx">getPostTitles</span><span class="p">(</span><span class="dl">'</span><span class="s1">@username</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`User Post Titles: </span><span class="p">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">postTitles</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="mi">2</span><span class="p">)}</span><span class="s2">`</span><span class="p">);</span>
</code></pre></div></div>

<p>That’s all for now!</p>

<p>I hope this post was informative. Feel free to play around with this NPM package I have published.</p>

<p>Again, here is the source code for it: <a href="https://github.com/rnag/medium-sdk-ts">https://github.com/rnag/medium-sdk-ts</a></p>

<p>If this library has proved useful in your automation efforts, please let me know via the comments down below, and feel free to also <strong>star</strong> the project on GitHub if you’d like.</p>

<p>As mentioned, this is my first-ever TypeScript library package I have published to NPM, so I would appreciate the support or feedback on it – do certainly let me know if there’s anything else I can improve on it.</p>]]></content><author><name>Ritvik Nag</name><email>me@ritviknag.com</email></author><category term="tech-tips" /><category term="medium-api" /><category term="graphql" /><category term="programming" /><category term="javascript" /><category term="npm-package" /><summary type="html"><![CDATA[Leverage the Medium API in JavaScript to publish a post to medium.com, and retrieve a list of all (published) posts for a user.]]></summary></entry><entry><title type="html">How to Mount Current Working Directory To Your Docker Container</title><link href="https://ritviknag.com/tech-tips/how-to-mount-current-working-directory-to-your-docker-container/" rel="alternate" type="text/html" title="How to Mount Current Working Directory To Your Docker Container" /><published>2024-02-09T11:12:54-05:00</published><updated>2024-02-09T11:12:54-05:00</updated><id>https://ritviknag.com/tech-tips/how-to-mount-current-working-directory-to-your-docker-container</id><content type="html" xml:base="https://ritviknag.com/tech-tips/how-to-mount-current-working-directory-to-your-docker-container/"><![CDATA[<p class="notice--warning">This article has been cross-posted on my <a href="https://medium.com/@ritviknag">Medium</a> and <a href="https://dev.to/ritvik-nag">Dev.to</a>.
<br /><br />
If you found this post useful, please consider <em>following me</em> on those platforms as I would be really grateful for the support. Thanks!</p>
<!-- {: .notice--info} -->

<hr />

<p>My team has been building out a new web app in <em>Express.js</em>, and for local deployment and testing we use <code class="language-plaintext highlighter-rouge">nodemon</code>, <code class="language-plaintext highlighter-rouge">babel</code>, and Docker.</p>

<p>One of the burning questions we wanted to find an answer to - apart from the ultimate question of all life, the universe, and everything - is how can we mount our local (current) directory to the running Docker container, so that <code class="language-plaintext highlighter-rouge">nodemon</code> can listen for local file changes when we develop, and automatically reload or restart the (containerized) web server as needed.</p>

<p>Towards that end, I hastily Googled online for “Can Docker mount local drive and listen for file changes”, and even kicked this same prompt to my work-approved AI assistant (*wink* ChatGPT alternative?). Anyway, the responses from the AI prompt are recorded separately below.</p>

<p>By the way, this article is inspired by some other articles on that I found on the web while researching if such a thing is possible - e.g. mounting a local folder to a Docker container - including the following article on Medium, which does a good job of condensing the main points:</p>

<blockquote>
  <p><a href="https://medium.com/@kale.miller96/how-to-mount-your-current-working-directory-to-your-docker-container-in-windows-74e47fa104d7"><strong>How To Mount Your Current Working Directory To Your Docker Container In Windows</strong></a></p>
</blockquote>

<p>As I and my team are developing on an (Apple Silicon-based) Mac, we don’t care too much about the <code class="language-plaintext highlighter-rouge">docker run</code> command being compatible between Mac and Windows overmuch.</p>

<p>However, I certainly agree that Docker is super handy for anyone wanting to write software in a Linux environment while still running a Mac (or Windows) computer.</p>

<h2 id="theory">Theory</h2>

<p>Our <code class="language-plaintext highlighter-rouge">Dockerfile</code> - we use the same one for development and production - looks something like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Start from the Node.js image for AMD64 architecture</span>
FROM <span class="nt">--platform</span><span class="o">=</span>linux/amd64 node:latest

<span class="c"># Set the working directory for your Node.js app</span>
WORKDIR /usr/app

ARG PORT
ARG NODE_ENV

ENV <span class="nv">PORT</span><span class="o">=</span><span class="nv">$PORT</span>
ENV <span class="nv">NODE_ENV</span><span class="o">=</span><span class="nv">$NODE_ENV</span>

<span class="c"># Some installation steps</span>
...

<span class="c"># Copy your Node.js application files and the startup script</span>
COPY <span class="nb">.</span> <span class="nb">.</span>
COPY start.sh <span class="nb">.</span>

<span class="c"># Make the startup script executable</span>
RUN <span class="nb">chmod</span> +x ./start.sh

<span class="c"># Install Node.js dependencies</span>
RUN npm <span class="nb">install</span> <span class="nt">--include</span><span class="o">=</span>dev

<span class="c"># Expose the port your app runs on</span>
EXPOSE <span class="nv">$PORT</span>
EXPOSE 80

<span class="c"># Command to run the startup script</span>
CMD <span class="o">[</span><span class="s2">"./start.sh"</span><span class="o">]</span>
</code></pre></div></div>

<p>We have a custom <code class="language-plaintext highlighter-rouge">start.sh</code> script we use to start our web server. This basically calls <code class="language-plaintext highlighter-rouge">npm run start &amp;</code> within it - e.g. our <strong>npm</strong> script <code class="language-plaintext highlighter-rouge">start</code>.</p>

<p>The above <code class="language-plaintext highlighter-rouge">Dockerfile</code> basically expects arguments to be passed in during the Docker <code class="language-plaintext highlighter-rouge">build</code> step.</p>

<p>This allows us to pass in environment variables (and Bash script values) such as the <code class="language-plaintext highlighter-rouge">PORT</code> for the web server, and <code class="language-plaintext highlighter-rouge">NODE_ENV</code> for the web app - such as <code class="language-plaintext highlighter-rouge">dev</code> or <code class="language-plaintext highlighter-rouge">production</code> .</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker build <span class="se">\</span>
  <span class="nt">--build-arg</span> <span class="nv">PORT</span><span class="o">=</span><span class="k">${</span><span class="nv">PORT</span><span class="k">}</span> <span class="se">\</span>
  <span class="nt">--build-arg</span> <span class="nv">NODE_ENV</span><span class="o">=</span><span class="k">${</span><span class="nv">NODE_ENV</span><span class="k">}</span> <span class="se">\</span>
  <span class="nt">--tag</span> <span class="k">${</span><span class="nv">TAG</span><span class="k">}</span> <span class="se">\</span>
  <span class="nb">.</span>
</code></pre></div></div>

<p>To the question of mounting a local folder to a Docker container, obviously we only want to enable this behavior when running in development mode.</p>

<p>That is, we only want to mount a local directory to the container into the location at <code class="language-plaintext highlighter-rouge">/usr/app</code> - the <code class="language-plaintext highlighter-rouge">WORKDIR</code> from the Docker instructions above - when we are in development mode, and we <em>don’t</em> want to mount anything when we are building a production Docker image, to later ship to <a href="https://www.freecodecamp.org/news/build-and-push-docker-images-to-aws-ecr/">Amazon ECR</a> for example.</p>

<p>This is a great use case for the potential solution, which will be discussed below. Now that we have the theory in place of what we want to achieve:</p>

<ul>
  <li>
    <p>We would rather not update <code class="language-plaintext highlighter-rouge">Dockerfile</code> to mount a volume or a local folder.</p>
  </li>
  <li>
    <p>Not necessary (or desirable) to mount local folder to a volume when running <code class="language-plaintext highlighter-rouge">docker build</code> to create an image from a <code class="language-plaintext highlighter-rouge">Dockerfile</code> . This is because need to create separate <code class="language-plaintext highlighter-rouge">docker build</code> commands for development and non-development, which is harder to management.</p>
  </li>
  <li>
    <p>Ideal solution will be to update only <code class="language-plaintext highlighter-rouge">docker run</code> command. Which we only run locally anyway. We do not need to execute <code class="language-plaintext highlighter-rouge">docker run</code> for production, as generally for production we just push the final image directly to ECR via a shell command.</p>
  </li>
</ul>

<h2 id="goal">Goal</h2>

<p>Now that theory is knocked out of the way, we understand that we prefer to update <code class="language-plaintext highlighter-rouge">docker run</code> command syntax, rather than touch <code class="language-plaintext highlighter-rouge">docker build</code> or <code class="language-plaintext highlighter-rouge">Dockerfile</code> itself.</p>

<p>Also, it is important here to go over the problem statement once more.</p>

<p>The goal that we are trying to solve is <strong>how to obviate the need to rebuild the Docker image every time an HTML, JS, EJS, CSS, or similar file in the current local directory changes.</strong></p>

<p>The host’s current directory is the same that we are copying over to <code class="language-plaintext highlighter-rouge">/usr/app</code> on the Docker container, as specified in the above <code class="language-plaintext highlighter-rouge">Dockerfile</code> .</p>

<p>This can be a simple change because of renaming your project, or changing any of the code for the project itself. This is not ideal.</p>

<p>There is a way to mount a local folder as a volume on a Docker container. This will solve it for us, when we are running <code class="language-plaintext highlighter-rouge">nodemon</code> on the Docker container to listen for file changes on the <code class="language-plaintext highlighter-rouge">/usr/app</code> location on the container side. We can instead trick Docker to listen for local file changes instead - let us see how this is possible.</p>

<h2 id="online-results">Online Results</h2>

<p>When searching online for the prompt “Can Docker mount local drive and listen for file changes” - as this would be incredibly helpful for local development - I found the below articles to be helpful.</p>

<p>Also, note that the first result is from the official Docker documentation itself. This was helpful for me to determine if Docker can mount local drive and listen for file changes.</p>

<ul>
  <li>
    <p><a href="https://docs.docker.com/storage/bind-mounts/">https://docs.docker.com/storage/bind-mounts/</a></p>
  </li>
  <li>
    <p><a href="https://medium.com/@kale.miller96/how-to-mount-your-current-working-directory-to-your-docker-container-in-windows-74e47fa104d7">https://medium.com/@kale.miller96/how-to-mount-your-current-working-directory-to-your-docker-container-in-windows-74e47fa104d7</a></p>
  </li>
  <li>
    <p><a href="https://stackoverflow.com/q/23439126/10237506">https://stackoverflow.com/q/23439126/10237506</a></p>
  </li>
</ul>

<p>In short, this appears to be a question of <code class="language-plaintext highlighter-rouge">--volume</code> vs. <code class="language-plaintext highlighter-rouge">-- mount</code> with a <code class="language-plaintext highlighter-rouge">type:bind</code>. The <a href="https://docs.docker.com/storage/bind-mounts/#differences-between--v-and---mount-behavior">Docker docs</a> (<em>see first link above)</em> mention that using <code class="language-plaintext highlighter-rouge">--volume</code> is the old style, and there is one important difference in behavior. Hence, using <code class="language-plaintext highlighter-rouge">--mount</code> explicitly going forward seems to be the safer (and more future-proof) approach.</p>

<p>However, note that either approach (<code class="language-plaintext highlighter-rouge">--volume</code> or <code class="language-plaintext highlighter-rouge">--mount</code>) should work for our use case.</p>

<h2 id="ask-an-ai-assistant">Ask an AI Assistant</h2>

<p>When I kicked the same prompt (more or less) to an AI assistant, this is what it replied with - rest of its response truncated for abbreviation sake.</p>

<hr />

<p>Yes, Docker mount local drive to listen for file changes should certainly be possible.</p>

<h3 id="mounting-a-host-directory-in-a-docker-container">Mounting a Host Directory in a Docker Container:</h3>

<ul>
  <li>You can use the <code class="language-plaintext highlighter-rouge">-v</code> flag with the <code class="language-plaintext highlighter-rouge">docker run</code> command to mount a local directory into a container. For example:</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-v</span> /path/to/host/directory:/path/in/container my_image
</code></pre></div></div>

<ul>
  <li>
    <p>Replace <code class="language-plaintext highlighter-rouge">/path/to/host/directory</code> with the actual path to your local directory and <code class="language-plaintext highlighter-rouge">/path/in/container</code> with the desired path inside the container.</p>
  </li>
  <li>
    <p>This approach ensures that any updates made in the host directory are reflected within the container.</p>
  </li>
</ul>

<hr />

<p>Wow, that wasn’t too bad. It got us more than halfway there. Honestly, if we want to use the legacy <code class="language-plaintext highlighter-rouge">--volume</code> option, we can just go with the AI suggestion. But to future-proof it, we can make a small tweak and use <code class="language-plaintext highlighter-rouge">--mount</code> instead, as we’ll see below.</p>

<h2 id="the-trick">The Trick</h2>

<p>I’ve found the best, future-proof method is to use the <code class="language-plaintext highlighter-rouge">--mount type=bind</code> flag when running the <code class="language-plaintext highlighter-rouge">docker run</code> command. With this command, you can attach the local directory to your docker container at runtime instead of during the build process.</p>

<p>The <code class="language-plaintext highlighter-rouge">--mount type=bind</code> command takes a single parameter formatted like so:</p>

<blockquote>
  <p><em><code class="language-plaintext highlighter-rouge">src=&lt;host directory&gt;,dst=&lt;target directory&gt;</code></em></p>
</blockquote>

<p>If we want to <em><strong>exclude</strong></em> any sub-folders in our <code class="language-plaintext highlighter-rouge">&lt;host directory&gt;</code> when mounting the local folder to the Docker container, we need to pass <code class="language-plaintext highlighter-rouge">--mount type=volume</code> with the full path to that sub-folder, and exclude the <code class="language-plaintext highlighter-rouge">src</code> argument, as follows:</p>

<blockquote>
  <p><em><code class="language-plaintext highlighter-rouge">dst=&lt;host directory to not mount&gt;</code></em></p>
</blockquote>

<h2 id="example">Example</h2>

<p>It’s easier to understand the trick when put into practice.</p>

<p>Putting it all together, here I’ve created a scenario where I would like to:</p>

<ul>
  <li>
    <p>Mount my current working directory (<code class="language-plaintext highlighter-rouge">/Users/rnag/Git-Projects/Work/my_project</code>) into the <code class="language-plaintext highlighter-rouge">alpine:latest</code> image at the <code class="language-plaintext highlighter-rouge">/usr/app</code> location in the container.</p>
  </li>
  <li>
    <p>Make the <code class="language-plaintext highlighter-rouge">run</code> command compatible for Apple-silicon-based Mac’s, with <code class="language-plaintext highlighter-rouge">--platform linux/amd64</code> .</p>
  </li>
  <li>
    <p>Exclude (skip) copying over the <code class="language-plaintext highlighter-rouge">node_modules</code> sub-folder in my current working directory, when mounting my current working directory <code class="language-plaintext highlighter-rouge">.</code> to the <code class="language-plaintext highlighter-rouge">/usr/app</code> location in the container.</p>
  </li>
</ul>

<p>We would do that as so:</p>

<blockquote>
  <p><em><code class="language-plaintext highlighter-rouge">docker run -it --platform linux/amd64 --mount type=bind,src=.,dst=/usr/app --mount type=volume,dst=/usr/app/node_modules alpine:latest</code></em></p>
</blockquote>

<p>Or, prettified a bit more, for all you neat freaks (like me):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="se">\</span>
  <span class="nt">-it</span> <span class="se">\</span>
  <span class="nt">--platform</span> linux/amd64 <span class="se">\</span>
  <span class="nt">--mount</span> <span class="nb">type</span><span class="o">=</span><span class="nb">bind</span>,src<span class="o">=</span>.,dst<span class="o">=</span>/usr/app <span class="se">\</span>
  <span class="nt">--mount</span> <span class="nb">type</span><span class="o">=</span>volume,dst<span class="o">=</span>/usr/app/node_modules <span class="se">\</span>
  alpine:latest
</code></pre></div></div>

<p>This should start up your container and attach your working directory. Now any changes you make locally (i.e. in your Mac or Windows machine) will be reflected in your Docker container!</p>

<p>In short, this will solve it for us, when we are running <code class="language-plaintext highlighter-rouge">nodemon</code> or similar on the Docker container to listen for file changes on the <code class="language-plaintext highlighter-rouge">/usr/app</code> location on the container side. We have successfully tricked Docker to listen for local file changes instead - what a win!</p>]]></content><author><name>Ritvik Nag</name><email>me@ritviknag.com</email></author><category term="tech-tips" /><category term="docker" /><category term="mac" /><category term="linux" /><category term="expressjs" /><category term="nodejs" /><summary type="html"><![CDATA[Learn how to obviate the need to rebuild a Docker image every time an local file in the host's current directory changes, with the power of Bind mounts!]]></summary></entry><entry><title type="html">Ruby (Versioning) Hell with Jekyll &amp;amp; GitHub Pages</title><link href="https://ritviknag.com/tech-tips/ruby-versioning-hell-with-jekyll-&-github-pages/" rel="alternate" type="text/html" title="Ruby (Versioning) Hell with Jekyll &amp;amp; GitHub Pages" /><published>2024-01-28T22:06:58-05:00</published><updated>2024-01-28T22:06:58-05:00</updated><id>https://ritviknag.com/tech-tips/ruby-versioning-hell-with-jekyll-&amp;-github-pages</id><content type="html" xml:base="https://ritviknag.com/tech-tips/ruby-versioning-hell-with-jekyll-&amp;-github-pages/"><![CDATA[<p class="notice--warning">This article has been cross-posted on my <a href="https://medium.com/@ritviknag">Medium</a> and <a href="https://dev.to/ritvik-nag">Dev.to</a>.
<br /><br />
If you found this post useful, please consider <em>following me</em> on those platforms as I would be really grateful for the support. Thanks!</p>
<!-- {: .notice--info} -->

<hr />

<p>So, huge disclaimer, a lesser-known fact (who am I kidding, I’m not ashamed to admit it) is that I’m a bit of a <code class="language-plaintext highlighter-rouge">ruby</code> newbie.</p>

<p>That is, I’ve never cared to learn <code class="language-plaintext highlighter-rouge">ruby</code> before. Also, perhaps as a direct result of that, I am a little taken aback when a project asks me to install <code class="language-plaintext highlighter-rouge">ruby</code> and use tools that it provides such as <code class="language-plaintext highlighter-rouge">bundler</code> , because the assumption there is that I know what I am doing.</p>

<p>Just to set the record straight, I don’t know what I am doing when it comes to <em>anything</em> <code class="language-plaintext highlighter-rouge">ruby</code> .</p>

<p>Recently, a lot of stuff blew up and hell broke loose, when I inadvertently ran <code class="language-plaintext highlighter-rouge">brew upgrade</code> on my new Mac to update (or upgrade?) packages installed by <strong>homebrew</strong>.</p>

<p>But alas, I did not take <code class="language-plaintext highlighter-rouge">ruby</code> into account. A while back, I had installed <code class="language-plaintext highlighter-rouge">ruby</code> with homebrew:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">brew</span> <span class="n">install</span> <span class="n">ruby</span>
</code></pre></div></div>

<p>Homebrew, without my permission, apparently took it upon itself to upgrade the bundled <code class="language-plaintext highlighter-rouge">ruby</code> version - from 3.2.3 to 3.3.0.</p>

<p>Hell broke lose when I <code class="language-plaintext highlighter-rouge">cd</code>‘d into my project repo that I use for <a href="https://github.com/rnag/rnag.github.io">my website</a>, and ran <code class="language-plaintext highlighter-rouge">bundle</code> to install fresh dependencies - which was needed, since <code class="language-plaintext highlighter-rouge">ruby</code> version was upgraded, so it was a fresh environment with no gems available.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">bundle</span> <span class="n">install</span>
</code></pre></div></div>

<p>The error I received was something like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/&lt;user&gt;/somewhere <span class="nv">$ </span>bundle <span class="nb">exec </span>jekyll serve
jekyll 3.9.3 | Error:  undefined method <span class="sb">`</span><span class="o">[]</span><span class="s1">' for nil
/usr/local/Cellar/ruby/3.3.0/lib/ruby/3.3.0/logger.rb:384:in `level'</span>: undefined method <span class="sb">`</span><span class="o">[]</span><span class="s1">' for nil (NoMethodError)

    @level_override[Fiber.current] || @level
</span></code></pre></div></div>

<p>This indeed was the specific error message I saw:
 <code class="language-plaintext highlighter-rouge">Error: undefined method '[]' for nil</code></p>

<p>Searching online, you can see a whole slew of posts that mention this error, and apparently it only happens with ruby <code class="language-plaintext highlighter-rouge">3.3.0</code> and the <code class="language-plaintext highlighter-rouge">jekyll</code> version <code class="language-plaintext highlighter-rouge">3.9.3</code> that comes bundled with <code class="language-plaintext highlighter-rouge">github-pages</code> .</p>

<ul>
  <li>
    <p><a href="https://stackoverflow.com/questions/77851863/bundle-exec-jekyll-serve-not-working-locally">https://stackoverflow.com/questions/77851863/bundle-exec-jekyll-serve-not-working-locally</a></p>
  </li>
  <li>
    <p><a href="https://talk.jekyllrb.com/t/unable-to-install-jekyll-on-m3-macbook/8836">https://talk.jekyllrb.com/t/unable-to-install-jekyll-on-m3-macbook/8836</a></p>
  </li>
  <li>
    <p><a href="https://talk.jekyllrb.com/t/error-when-executing-bundle-install/8822">https://talk.jekyllrb.com/t/error-when-executing-bundle-install/8822</a></p>
  </li>
</ul>

<p>Hence, while it might not bear repeating, it helps to clarify - upgrading <code class="language-plaintext highlighter-rouge">jeykll</code> version for a project is a <strong>no-go</strong>, especially when the project <code class="language-plaintext highlighter-rouge">Gemfile</code> relies explicitly on <code class="language-plaintext highlighter-rouge">github-pages</code> dependency, which <em>itself</em> relies on <code class="language-plaintext highlighter-rouge">jekyll</code> :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s">"github-pages"</span><span class="p">,</span> <span class="k">group</span><span class="p">:</span> <span class="p">:</span><span class="n">jekyll_plugins</span>
</code></pre></div></div>

<p>And yes, just to double check, the <code class="language-plaintext highlighter-rouge">Gemfile.lock</code> should show the lines that clearly show the dependency relationship between the</p>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nt">github-pages</span> <span class="o">(</span><span class="nt">228</span><span class="o">)</span>
      <span class="nt">github-pages-health-check</span> <span class="o">(=</span> <span class="nt">1</span><span class="nc">.17.9</span><span class="o">)</span>
      <span class="nt">jekyll</span> <span class="o">(=</span> <span class="nt">3</span><span class="nc">.9.3</span><span class="o">)</span>
    <span class="nc">...</span>
</code></pre></div></div>

<p>To recap, if one installs <code class="language-plaintext highlighter-rouge">ruby</code> with <code class="language-plaintext highlighter-rouge">brew</code> and runs <code class="language-plaintext highlighter-rouge">brew upgrade</code> or similar and results in <code class="language-plaintext highlighter-rouge">ruby@3.3.0</code> being inadvertently installed, this can break when attempting to work with projects that rely on a specific version of <code class="language-plaintext highlighter-rouge">jekyll</code>, especially ones that rely on <code class="language-plaintext highlighter-rouge">github-pages</code> gem for deployment.</p>

<p>There does not appear to be any solution involving <code class="language-plaintext highlighter-rouge">brew</code> and <code class="language-plaintext highlighter-rouge">ruby</code> alone.</p>

<p>Upon catching the issue that the mismatch b/w the two gems caused the build error when running <code class="language-plaintext highlighter-rouge">bundle exec jekyll serve</code> , I immediately tried some steps, that first involved installing <code class="language-plaintext highlighter-rouge">csv</code> , as apparently there was a warning printed to terminal since that doesn’t come bundled in ruby 3.3.0</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">bundle</span> <span class="n">install</span> <span class="n">csv</span>
</code></pre></div></div>

<p>Then I tried to upgrade all <code class="language-plaintext highlighter-rouge">gem</code> versions in project by running:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">bundle</span> <span class="n">upgrade</span>
</code></pre></div></div>

<p>That didn’t help, so I tried to specify both gems explicitly:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">bundle</span> <span class="n">update</span> <span class="n">jekyll</span>
<span class="err">$</span> <span class="n">bundle</span> <span class="n">update</span> <span class="n">github</span><span class="o">-</span><span class="n">pages</span>
</code></pre></div></div>

<p>Still no dice, and I was at wit’s end by now. Manually upgrading either of those <code class="language-plaintext highlighter-rouge">gem</code> versions specified in <code class="language-plaintext highlighter-rouge">Gemfile.lock</code> didn’t help either. There was a dependency conflict if I upgraded <code class="language-plaintext highlighter-rouge">jekyll</code>, as <code class="language-plaintext highlighter-rouge">github-pages</code> relied on a specific version.</p>

<p>I also tried deleting <code class="language-plaintext highlighter-rouge">Gemfile.lock</code> file completely and running <code class="language-plaintext highlighter-rouge">bundle</code> again, but still no dice.</p>

<p>In short, this was exactly what they call “dependency hell”. I cannot change either gem versions, and hence this is basically a “SNAFU!” situation. As in, there’s no solution to be had that allows me to keep the up-to-date <code class="language-plaintext highlighter-rouge">ruby</code> version 3.3.0 for use with my project.</p>

<p>I even tried uninstalling and re-installing specific ruby version 2.3.2 via <strong>homebrew</strong>, but if you can believe it there <em>still</em> was no dice:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>brew uninstall ruby
<span class="nv">$ </span>brew <span class="nb">install </span>ruby@3.2
<span class="nv">$ </span><span class="nb">echo</span> <span class="s1">'export PATH="/opt/homebrew/opt/ruby@3.2/bin:$PATH"'</span> <span class="o">&gt;&gt;</span> ~/.zshrc
<span class="nv">$ </span><span class="nb">source</span> ~/.zshrc
<span class="nv">$ </span>ruby <span class="nt">-v</span>
ruby 3.2.3
<span class="nv">$ </span>gem <span class="nb">install </span>bundler
<span class="nv">$ </span>bundle
&lt;error&gt;
</code></pre></div></div>

<p>I’ll be honest, I felt like tearing my hair out at this point. Sweat started metaphorically trickling down my back. Being a <code class="language-plaintext highlighter-rouge">ruby</code> newb, I was about to throw in the towel, and as a last resort maybe open up an issue on the project page for one of the gems, or post about it on a forum asking other users for help.</p>

<p>Now that I think about it, maybe ChatGPT could have helped me. Hello ChatGPT? Can you explain this error please, and suggest me a solution &lt;<em>paste stack trace</em>&gt;.</p>

<p>However, thankfully, that was ultimately not needed. I found a workaround that helps me to maintain <code class="language-plaintext highlighter-rouge">ruby</code> version across different machines, consistently.</p>

<p>After researching online for quite a bit, I found out that someone was using <code class="language-plaintext highlighter-rouge">rbenv</code> and decided to look into what that is. Turns out, it’s something similar like <code class="language-plaintext highlighter-rouge">pyenv</code> for <code class="language-plaintext highlighter-rouge">python</code>, or basically a version management tool for a programming language.</p>

<p>Feeling I was on to something at this point, and shrugging my shoulders after realizing I had nothing to lose, I decided to install it after all:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>brew <span class="nb">install </span>rbenv ruby-build
<span class="nv">$ </span><span class="nb">echo</span> <span class="s1">'eval "$(rbenv init - zsh)"'</span> <span class="o">&gt;&gt;</span> ~/.zshrc
<span class="nv">$ </span><span class="nb">source</span> ~/.zshrc
</code></pre></div></div>

<p>That got me the<code class="language-plaintext highlighter-rouge">rbenv</code> command, but of course I wasn’t done there yet.</p>

<p>I have to install my necessary previous <strong>ruby</strong> version <code class="language-plaintext highlighter-rouge">3.2.3</code> with <code class="language-plaintext highlighter-rouge">rbenv</code> :</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">rbenv</span> <span class="n">install</span> <span class="mf">3.2</span><span class="o">.</span><span class="mi">3</span>
</code></pre></div></div>

<p>After installing a version with <code class="language-plaintext highlighter-rouge">rbenv</code> , you can set it either <strong>globally</strong> (so that <code class="language-plaintext highlighter-rouge">ruby</code> version is changed regardless of your location in terminal) or <strong>locally</strong>, so it will only affect a project after you <code class="language-plaintext highlighter-rouge">cd</code> into its folder.</p>

<p>I decided to do <em>both</em>, because the default <code class="language-plaintext highlighter-rouge">ruby</code> version that comes bundled with my new Mac Air M2 laptop is something like <code class="language-plaintext highlighter-rouge">2.x</code> , which is hella old. And I’m clearly never gonna use the built-in one.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>rbenv global 3.2.3
<span class="nv">$ </span>rbenv <span class="nb">local </span>3.2.3
</code></pre></div></div>

<p>Now we’re cooking! To double-check that we got the new <strong>ruby</strong> version installed:</p>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="nt">ruby</span> <span class="nt">-v</span>
<span class="nt">ruby</span> <span class="nt">3</span><span class="nc">.2.3</span> <span class="o">(</span><span class="nt">2024-01-18</span> <span class="nt">revision</span> <span class="nt">52bb2ac0a6</span><span class="o">)</span> <span class="o">[</span><span class="nt">arm64-darwin23</span><span class="o">]</span>
</code></pre></div></div>

<p>Great! Now that’s out of the way, we want to install or upgrade <code class="language-plaintext highlighter-rouge">bundler</code>, and then we’re good to go.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">gem</span> <span class="n">install</span> <span class="n">bundler</span>
<span class="err">$</span> <span class="n">bundle</span> <span class="o">-</span><span class="n">v</span>
<span class="no">Bundler</span> <span class="n">version</span> <span class="mf">2.5</span><span class="o">.</span><span class="mi">5</span>
</code></pre></div></div>

<p>If it helps, one might need to delete and re-create<code class="language-plaintext highlighter-rouge">Gemfile.lock</code> by running <code class="language-plaintext highlighter-rouge">bundle</code>, especially if it says the gems were installed with another version of <code class="language-plaintext highlighter-rouge">bundler</code> :</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">BUNDLED</span> <span class="k">WITH</span>
   <span class="mi">2</span><span class="p">.</span><span class="mi">5</span><span class="p">.</span><span class="mi">4</span>
</code></pre></div></div>

<p>After that, you can run <code class="language-plaintext highlighter-rouge">bundle exec</code> or however you start up your project that relies on <code class="language-plaintext highlighter-rouge">jekyll</code> and <code class="language-plaintext highlighter-rouge">github-pages</code> , and then you should be all set.</p>

<p>If it helps, I’ve actually created an alias command <code class="language-plaintext highlighter-rouge">jb</code> , which serves that exact purpose in my case:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>which jb
jb: aliased to bundle <span class="nb">exec </span>jekyll serve <span class="nt">-low</span>
</code></pre></div></div>

<p>It’s easy enough to add that alias in to <code class="language-plaintext highlighter-rouge">bash</code> or <code class="language-plaintext highlighter-rouge">zsh</code> shell configuration. To add it to <code class="language-plaintext highlighter-rouge">zsh</code> - my default interpreter - here’s what I did:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"alias jb='bundle exec jekyll serve -low'"</span> <span class="o">&gt;&gt;</span> ~/.zshenv
</code></pre></div></div>

<p>Then <code class="language-plaintext highlighter-rouge">source ~/.zshenv</code> to reload the changes in the current terminal window.</p>

<p>All said, this whole ordeal took me <em><strong>&gt;1 hour</strong></em> of pointless debuggery. It was the exact opposite of fun. And, ideally not how I would have wanted to spend a Sunday morning.</p>

<p>However, the upshot is that the confusing <strong>ruby</strong> versioning hell along with two of the aforementioned gems - <code class="language-plaintext highlighter-rouge">jekyll</code> and <code class="language-plaintext highlighter-rouge">github-pages</code> - is now finally resolved, at long lost.</p>

<blockquote>
  <p>Here then was a lesson learned: manage <code class="language-plaintext highlighter-rouge">ruby</code> versions with <strong>rbenv</strong> instead of <strong>homebrew</strong>.</p>
</blockquote>

<p>Praise be! Massive dependency hell solved, and headache (mostly) avoided. Hope this helps someone else out. Now go out there and get coding!</p>]]></content><author><name>Ritvik Nag</name><email>me@ritviknag.com</email></author><category term="tech-tips" /><category term="ruby" /><category term="github-pages" /><category term="jekyll" /><category term="rbenv" /><category term="homebrew" /><summary type="html"><![CDATA[A how-to (with lessons learned) on Ruby versioning with Jekyll and GitHub Pages.]]></summary></entry></feed>