<?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: kathi beitz</title>
    <description>The latest articles on DEV Community by kathi beitz (@kathi_beitz).</description>
    <link>https://dev.to/kathi_beitz</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%2F4133638%2F08751059-7f7b-4e7d-8f3d-05c0de6bc26f.png</url>
      <title>DEV Community: kathi beitz</title>
      <link>https://dev.to/kathi_beitz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kathi_beitz"/>
    <language>en</language>
    <item>
      <title>How I keep a game companion site from lying to its readers</title>
      <dc:creator>kathi beitz</dc:creator>
      <pubDate>Sun, 20 Sep 2026 03:44:53 +0000</pubDate>
      <link>https://dev.to/kathi_beitz/how-i-keep-a-game-companion-site-from-lying-to-its-readers-2jfl</link>
      <guid>https://dev.to/kathi_beitz/how-i-keep-a-game-companion-site-from-lying-to-its-readers-2jfl</guid>
      <description>&lt;p&gt;Game companion sites have a trust problem. A build guide can be correct on Tuesday, broken by Friday's patch, and still look authoritative three weeks later. If the data lives directly in React components, nobody notices until a reader points out the wrong unlock requirement or an expired code.&lt;/p&gt;

&lt;p&gt;While building &lt;a href="https://lootrtools.com" rel="noopener noreferrer"&gt;Lootr Tools&lt;/a&gt;, an unofficial Dungeon Lootr companion, I treated the content pipeline like a small production data system. The goal was simple: bad references, impossible dates, and unsupported drop-rate claims should fail before they reach the site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the facts behind a data contract
&lt;/h2&gt;

&lt;p&gt;Game facts do not belong inside JSX. Every class, aspect, item, build, code, and source lives in typed data modules. A small helper runs each object through a Zod schema at import time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;gameClassSchema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/schemas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GameClass&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/types&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;defineClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GameClass&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;GameClass&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;gameClassSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&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;That gives editors normal TypeScript completion while still enforcing rules TypeScript alone cannot protect at runtime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;slugs must be lowercase kebab-case;&lt;/li&gt;
&lt;li&gt;verification dates must use &lt;code&gt;YYYY-MM-DD&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;drop rates must be decimals from 0 to 1;&lt;/li&gt;
&lt;li&gt;a drop rate cannot appear without a provenance status;&lt;/li&gt;
&lt;li&gt;query-filterable entities can be explicitly marked &lt;code&gt;noindex&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last rule matters for fan sites. If an entity is known only by name, or two sources disagree about whether it is even a class, the page can remain useful without being sent to search engines as authoritative content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make source references part of the model
&lt;/h2&gt;

&lt;p&gt;A source is not a footer decoration. It is a relationship in the dataset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SourceReference&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sourceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;official&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;community&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;editorial&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;accessedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;Entities reference those source IDs. The UI can then render a real distinction between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;official;&lt;/li&gt;
&lt;li&gt;community verified;&lt;/li&gt;
&lt;li&gt;conflicting sources;&lt;/li&gt;
&lt;li&gt;unverified.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More importantly, it can avoid claiming certainty the source does not have. A community drop estimate is useful, but it should not be presented with the same confidence as a first-party number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate relationships, not just objects
&lt;/h2&gt;

&lt;p&gt;A class schema can be valid while still pointing to an aspect that does not exist. So the data index performs a cross-reference check before the app uses the dataset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;assertValidDataset&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;aspects&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;builds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;codes&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;The validator checks for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicate entity IDs and slugs;&lt;/li&gt;
&lt;li&gt;builds referencing missing classes;&lt;/li&gt;
&lt;li&gt;classes recommending missing aspects;&lt;/li&gt;
&lt;li&gt;aspects recommending missing classes;&lt;/li&gt;
&lt;li&gt;entities citing missing sources;&lt;/li&gt;
&lt;li&gt;item rates outside the 0–1 range;&lt;/li&gt;
&lt;li&gt;item rates without a rate status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This turns a common content mistake into a local failure with a precise message. A build pointing at a nonexistent class never becomes a production page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep freshness static and honest
&lt;/h2&gt;

&lt;p&gt;Dynamic pages should not manufacture freshness from &lt;code&gt;new Date()&lt;/code&gt;. That would make every stale page look current.&lt;/p&gt;

&lt;p&gt;Instead, each entity carries a &lt;code&gt;verifiedAt&lt;/code&gt; date from the data file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cursed-king&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cursed King&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;verifiedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-09-16&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;sourceIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pro-game-guides&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gamepur-tier&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;verificationStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page renders that stored date. If nobody has rechecked the data, the page says so. The content may still be wrong, but it no longer pretends to be fresh.&lt;/p&gt;

&lt;p&gt;This is especially useful for Roblox game codes. Two dated sources can disagree on the same day, so the data model allows a status of &lt;code&gt;unknown&lt;/code&gt; rather than forcing a false active/expired choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate decisions from prose
&lt;/h2&gt;

&lt;p&gt;Reference articles answer, "What is this?" Tools answer, "What should I do?"&lt;/p&gt;

&lt;p&gt;For decision pages, the recommendation logic is deterministic and testable. The Build Finder scores candidate builds by class, goal, source confidence, current-update compatibility, and aspect match. If no exact build exists, it can fall back to another goal and lower the confidence. If no build exists at all, it derives a conservative plan from class basics instead of inventing gear advice.&lt;/p&gt;

&lt;p&gt;Probability tools are also pure functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;chanceAtLeastOneDrop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;runsForTarget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;planFarm&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;dropRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requiredSuccesses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;averageRunMinutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;confidence&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;Keeping the math outside React makes edge cases easier to test: zero percent drops, hundred percent drops, tiny rates across huge run counts, and unreachable confidence targets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;This approach does not solve game research—it constrains the damage when research is incomplete. The production site still needs regular rechecks, but common content failures now happen earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invalid game data:
- Build "example-build" references unknown class "does-not-exist"
- Item "example-item" sets a dropRate without dropRateStatus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much better than publishing a polished page with a broken relationship.&lt;/p&gt;

&lt;p&gt;I'm using this model on &lt;a href="https://lootrtools.com" rel="noopener noreferrer"&gt;Lootr Tools&lt;/a&gt;, where it supports class comparisons, aspect references, item drop sources, update notes, code status, a Build Finder, and farming calculators. It is an unofficial fan project and is not affiliated with Roblox or ClickBytes, but the content model still tries to hold itself to a production standard.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>nextjs</category>
    </item>
  </channel>
</rss>
