<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ZHIWEI</title>
    <description>The latest articles on DEV Community by ZHIWEI (@zhiwei_224e5e15600342dc0b).</description>
    <link>https://dev.to/zhiwei_224e5e15600342dc0b</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4181129%2F67c44224-3270-4202-b374-fc7b047cf507.png</url>
      <title>DEV Community: ZHIWEI</title>
      <link>https://dev.to/zhiwei_224e5e15600342dc0b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zhiwei_224e5e15600342dc0b"/>
    <language>en</language>
    <item>
      <title>How I Keep a Fan Guide in Sync With a Viral Browser Game Using a GitHub Actions Cron</title>
      <dc:creator>ZHIWEI</dc:creator>
      <pubDate>Tue, 13 Oct 2026 05:54:04 +0000</pubDate>
      <link>https://dev.to/zhiwei_224e5e15600342dc0b/how-i-keep-a-fan-guide-in-sync-with-a-viral-browser-game-using-a-github-actions-cron-4ggd</link>
      <guid>https://dev.to/zhiwei_224e5e15600342dc0b/how-i-keep-a-fan-guide-in-sync-with-a-viral-browser-game-using-a-github-actions-cron-4ggd</guid>
      <description>&lt;p&gt;A Nigerian browser game called &lt;strong&gt;Lagos Life&lt;/strong&gt; launched at the start of October 2026 and went viral fast. I run an independent fan guide for it, and within a few days I hit the problem every guide writer for a live game hits: &lt;strong&gt;the game changes faster than I can edit pages.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Players elect a governor who sets a law that changes the economy. There's a new in-game headline every day, a daily gem hunt with a fresh clue, and the developers tweak job and business tables without notice. A hand-maintained page is stale within days.&lt;/p&gt;

&lt;p&gt;So I stopped editing by hand. This post walks through the small pipeline that now does it for me: a GitHub Actions cron that reads what the official site already serves to every visitor, writes a few JSON files, and rebuilds a static site only when something actually changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The guide is a &lt;strong&gt;prerendered static site&lt;/strong&gt; (TanStack Start) on &lt;strong&gt;Cloudflare Pages&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Pages read their data from JSON files committed in the repo (&lt;code&gt;live-log.json&lt;/code&gt;, &lt;code&gt;jobs.json&lt;/code&gt;, …) at build time.&lt;/li&gt;
&lt;li&gt;One workflow runs &lt;strong&gt;every 3 hours&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch scripts ──▶ anything changed? ──no──▶ stop
                        │ yes
                        ▼
                 build ▶ deploy ▶ ping IndexNow ▶ commit the JSON
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;17&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*/3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;   &lt;span class="c1"&gt;# minute 17, not 0: avoid the top-of-hour rush&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;live-log&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;  &lt;span class="c1"&gt;# never kill a run halfway through a deploy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Source 1: the public JSON the site already serves
&lt;/h2&gt;

&lt;p&gt;The official site calls a few JSON endpoints for logged-out visitors: the current governor and law, the daily headline, and the gem hunt. Reading those is the easy part — a plain &lt;code&gt;fetch&lt;/code&gt; with a timeout, a couple of retries, and a User-Agent that says who I am and where the data ends up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-guide live log (+https://example.com/updates/)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User-Agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two decisions mattered more than the code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Store only what the page shows.&lt;/strong&gt; The governor endpoint also returns election candidates and player-written announcements. I never write those to disk. The log keeps the law id, when it was set, the daily date/weather/headline/tip, and the hunt's clue — nothing else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Append, don't overwrite.&lt;/strong&gt; A new law creates a new entry with a &lt;code&gt;firstSeen&lt;/code&gt; timestamp, so the page becomes a dated history instead of a snapshot. If only the term-end time moves, the last entry is updated in place.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The optional endpoint (the gem hunt) is wrapped in its own &lt;code&gt;catch&lt;/code&gt;, so if it breaks, the law and headline log still runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source 2: when there is no API — read the bundle
&lt;/h2&gt;

&lt;p&gt;Job tables and pay rules aren't exposed as JSON; they live in the game's client JavaScript. The game is a Next.js app, so the homepage HTML references chunks under &lt;code&gt;/_next/static/chunks/&lt;/code&gt;, and chunks reference more chunks.&lt;/p&gt;

&lt;p&gt;The script does a small breadth-first crawl: start from the chunk names in the homepage, fetch six at a time, queue any new chunk names found inside, and stop as soon as every value it needs has been found (hard cap: 400 chunks).&lt;/p&gt;

&lt;p&gt;Each value has its own &lt;strong&gt;finder&lt;/strong&gt;, a function that returns &lt;code&gt;null&lt;/code&gt; until it sees its pattern in some chunk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FINDERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;careers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"CAREERS",0,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;starBonus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/"STAR_BONUS",0,&lt;/span&gt;&lt;span class="se"&gt;([\d&lt;/span&gt;&lt;span class="sr"&gt;.&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...one finder per rule constant&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Minified variable names change on every build, so the patterns anchor on things that survive minification — &lt;strong&gt;string literals and object shapes&lt;/strong&gt; like &lt;code&gt;{id:"…",name:"…",levels:[{title:"…",pay:…}]}&lt;/code&gt; — never on short identifiers like &lt;code&gt;e&lt;/code&gt; or &lt;code&gt;a&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guard rails: fail loudly, keep the last good data
&lt;/h2&gt;

&lt;p&gt;A regex scraper against minified code &lt;em&gt;will&lt;/em&gt; break one day. The goal is that when it breaks, the site stays correct and I get told.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Validate before writing.&lt;/strong&gt; If fewer than 30 careers parse, or any career doesn't have five paid levels, or a workplace has no name, the script writes nothing and exits 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolate failures.&lt;/strong&gt; Each fetch script is its own workflow step with &lt;code&gt;continue-on-error: true&lt;/code&gt;, so a broken job-table parser never blocks the law log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two dates, not one.&lt;/strong&gt; &lt;code&gt;checkedAt&lt;/code&gt; moves on every good read; &lt;code&gt;changedAt&lt;/code&gt; moves only when the table actually differs. The page shows both, so readers can tell "verified today" from "changed today".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be polite.&lt;/strong&gt; The bundle crawl runs at most once per Lagos day (WAT, UTC+1). If today's check already happened, it makes zero requests.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WAT_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;WAT_MS&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;checkedAt&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// already checked today&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Commit only after a successful deploy
&lt;/h2&gt;

&lt;p&gt;This one took me a while to get right. If the workflow committed the new JSON &lt;em&gt;before&lt;/em&gt; deploying and the deploy then failed, the next run would see "no change" and the live site would stay stale forever.&lt;/p&gt;

&lt;p&gt;So the order is: &lt;strong&gt;fetch → build → deploy → commit&lt;/strong&gt;. The data file only lands in git once the site that shows it is live. A failed run leaves git untouched, and the next run three hours later simply tries again.&lt;/p&gt;

&lt;p&gt;The build step also greps the generated &lt;code&gt;sitemap.xml&lt;/code&gt; for the expected URLs before deploying — a cheap check that catches a build that "succeeded" but produced the wrong site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tell search engines only about pages that changed
&lt;/h2&gt;

&lt;p&gt;After a deploy, the workflow pings &lt;strong&gt;IndexNow&lt;/strong&gt; — but only for the pages whose content changed. A new check date alone rebuilds the jobs page without a ping; a new law pings only the updates page. Pinging unchanged URLs every three hours would just be noise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;paths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LIVE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nv"&gt;paths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/updates/"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JOBS_TABLE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; changed &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nv"&gt;paths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;paths&lt;/span&gt;:+&lt;span class="nv"&gt;$paths&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/jobs/"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it produces
&lt;/h2&gt;

&lt;p&gt;Since 8 October the log has been filling itself: each law with the date it was first seen, every daily headline, every gem-hunt clue (clues only — no answers). That's the updates page of the guide:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://lagoslifegames.com/updates/" rel="noopener noreferrer"&gt;Lagos Life Updates: This Week's Law, Governor &amp;amp; Gem Hunt&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The game itself is at &lt;a href="https://lagoslife.app/" rel="noopener noreferrer"&gt;lagoslife.app&lt;/a&gt;; my site is an independent fan guide, not affiliated with the developers, and everything it reads is what the official site already serves to any visitor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For a fast-changing live game, a &lt;strong&gt;3-hourly cron + static rebuild&lt;/strong&gt; is plenty — no server, no database.&lt;/li&gt;
&lt;li&gt;Anchor bundle-scraping patterns on &lt;strong&gt;string literals and shapes&lt;/strong&gt;, and validate hard before writing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit after deploy&lt;/strong&gt;, so a failed run is automatically retried.&lt;/li&gt;
&lt;li&gt;Store the &lt;strong&gt;minimum&lt;/strong&gt; — especially anything player-written or personal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've built something similar for another live game, I'd love to hear how you handle the parser breaking.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
