<?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: Chris</title>
    <description>The latest articles on DEV Community by Chris (@christherohrer).</description>
    <link>https://dev.to/christherohrer</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%2F4164095%2Fa4fa5e7d-cf35-4b4e-b888-eb8c7892f2ed.png</url>
      <title>DEV Community: Chris</title>
      <link>https://dev.to/christherohrer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/christherohrer"/>
    <language>en</language>
    <item>
      <title>89 German bylaws, three ways to bill rain: building a fee dataset people can cite</title>
      <dc:creator>Chris</dc:creator>
      <pubDate>Mon, 05 Oct 2026 14:14:31 +0000</pubDate>
      <link>https://dev.to/christherohrer/89-german-bylaws-three-ways-to-bill-rain-building-a-fee-dataset-people-can-cite-2i4c</link>
      <guid>https://dev.to/christherohrer/89-german-bylaws-three-ways-to-bill-rain-building-a-fee-dataset-people-can-cite-2i4c</guid>
      <description>&lt;p&gt;People who call a drain-cleaning company ask a surprising number of questions that have nothing to do with drains. One of the most common: &lt;em&gt;why is my neighbour's sewage bill so much lower than mine?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I build and maintain the website of &lt;a href="https://fachservice-rohrreinigung.de/" rel="noopener noreferrer"&gt;Fachservice Rohrreinigung&lt;/a&gt;, a German drain and sewer cleaning business. We wanted a proper answer to that question: one comparable yearly number per city, where every number can be traced back to the city's own legal document. It ended up covering 89 cities. Most of the work was not collecting numbers. It was building the checks that stop a wrong number from going live.&lt;/p&gt;

&lt;p&gt;This post covers the data model, the three billing systems that refused to fit it, and the failures that turned into validation rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  One household, one formula
&lt;/h2&gt;

&lt;p&gt;German municipalities set their wastewater fees in a bylaw (&lt;em&gt;Satzung&lt;/em&gt;) or an official price sheet. To compare them you need a fixed reference household. Ours is four people in a detached house with 200 m³ of fresh water and 130 m² of sealed roof and paving per year:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MODELL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;schmutzwasserM3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// m³ fresh water = billed wastewater&lt;/span&gt;
  &lt;span class="na"&gt;versiegelteFlaecheM2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;130&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// m² sealed area draining into the sewer&lt;/span&gt;
  &lt;span class="na"&gt;formel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;200 × wastewater rate + 130 × rainwater rate + base fee, if any&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each city is one record with its rates, the year they took effect and a source:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ludwigsburg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;stadt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ludwigsburg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;gebuehrenmodell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gesplittet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// split fee: wastewater + rainwater&lt;/span&gt;
  &lt;span class="nx"&gt;schmutzwasser_eur_m3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.57&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;niederschlagswasser_eur_m2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;gueltig_ab&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2025&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;quelle_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.ludwigsburg.de/…/Abwassersatzung_ab_2025.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;jahreskostenMusterhaushalt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;350.4&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;&lt;code&gt;jahreskostenMusterhaushalt&lt;/code&gt; (yearly cost for the model household) is stored, but never trusted. The validator recomputes it from the rates on every run. More on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to bill the rain
&lt;/h2&gt;

&lt;p&gt;Most cities fit the model: a price per m³ of wastewater and a price per m² of sealed area. Two well-known cities show why "most" matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leipzig weights the area by surface type.&lt;/strong&gt; According to the &lt;a href="https://www.l.de/wasserwerke/preise/niederschlagswasser/" rel="noopener noreferrer"&gt;Leipziger Wasserwerke rainwater price page&lt;/a&gt;, densely sealed surfaces count 100%, partially sealed ones 50% and unsealed ones 0%. Our model treats all 130 m² as fully sealed, so Leipzig fits, as long as you remember the assumption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Magdeburg bills rain by volume.&lt;/strong&gt; The &lt;a href="https://www.agm-magdeburg.de/preise-und-entgelte" rel="noopener noreferrer"&gt;AGM terms&lt;/a&gt; convert the area into an estimated runoff volume and charge per cubic metre:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;V = Ψ × r × A
Ψ  runoff coefficient: 0.95 pitched roof, 0.60 paving without sealed joints, 0.30 green roof
r  0.494 m³ per m² per year
A  connected area in m²
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 1.59 €/m³, 100 m² of pitched roof comes to 0.95 × 0.494 × 100 × 1.59 = 74.62 € a year.&lt;/p&gt;

&lt;p&gt;We could have converted Magdeburg into a per-m² figure by picking a surface type. We didn't. A comparison table that quietly assumes a roof type for one row and not the others is wrong in a way no reader can spot. Instead, the record carries a flag, and the page reports two averages: one over all cities and one over the fully comparable ones.&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="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;magdeburg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schmutzwasser_eur_m3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;4.07&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nichtVergleichbar&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="cm"&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 general rule: &lt;strong&gt;when a data point does not fit the model, mark it. Don't bend it until it does.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources: official or nothing
&lt;/h2&gt;

&lt;p&gt;Every rate must come from the city's bylaw, its official gazette or the price page of its utility. Comparison portals are not accepted as evidence, however convenient. The validator enforces this with a crude but effective hostname blocklist:&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;VERBOTENE_QUELLEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/statista|hausjournal|nebenkosten|vergleich&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;|check24|verivox|wikipedia|immobilienscout/i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every rate was then checked a second time, independently, at the same source. Anything that could not be found again was dropped. That is also why there are 89 rows and not more.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke, and the rule each failure left behind
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Prose in a year field
&lt;/h3&gt;

&lt;p&gt;Part of the research was done with AI research agents that fill in records from source documents. One of them put a whole explanatory sentence into &lt;code&gt;gueltig_ab&lt;/code&gt; instead of a bare year. Twenty-nine source titles were over 200 characters long, the longest 371. Both would have broken narrow table columns. Neither was visible locally, because at the time the static build hung on my machine without any output.&lt;/p&gt;

&lt;p&gt;Now it is a check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d{4}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gueltig_ab&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;fehler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stadt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: gueltig_ab is "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gueltig_ab&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;", expected a bare year`&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;If you let a model fill structured fields, validate the &lt;em&gt;shape&lt;/em&gt; of every field and not just its presence. Models are very good at writing helpful explanations into fields that should hold a number.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The prompt that was too big to finish
&lt;/h3&gt;

&lt;p&gt;The agent that wrote the explanatory page text first received all 89 records, including each record's source quotes and notes: 149 KB of JSON. It stalled six times in a row. Projecting the data down to the fields the text actually uses brought it to 18 KB, and it ran on the first try. Prompt size is a failure mode, and the fix was on the data side, not the prompt side.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Numbers are rendered, never written
&lt;/h3&gt;

&lt;p&gt;Several of the site's city pages show the local fee, for example the pages on &lt;a href="https://fachservice-rohrreinigung.de/schorndorf/" rel="noopener noreferrer"&gt;drain cleaning in Schorndorf&lt;/a&gt; and &lt;a href="https://fachservice-rohrreinigung.de/gladbeck/" rel="noopener noreferrer"&gt;Rohrreinigung for Gladbeck in the Ruhr area&lt;/a&gt;. Those numbers are not typed into the page text. The city page renders them from the same record the comparison table uses.&lt;/p&gt;

&lt;p&gt;The reason is simple: bylaws change. If a rate appears in hand-written prose on a city page and in the data on the comparison page, the next update changes one of them, and the site then contradicts itself. A separate check fails the build if a fee figure shows up in a page's prose instead of coming from the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The validator, briefly
&lt;/h2&gt;

&lt;p&gt;The fee check runs before every push and fails on any of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The formula.&lt;/strong&gt; &lt;code&gt;jahreskostenMusterhaushalt&lt;/code&gt; must equal 200 × wastewater rate + 130 × rainwater rate + base fee, within one cent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plausibility bands.&lt;/strong&gt; 1–7 €/m³ for wastewater and 0.2–3 €/m² for rainwater. This is not a hard truth, just a brake on comma and unit mistakes (2,85 vs 28.5).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split fee without a rain rate.&lt;/strong&gt; A record marked as split-fee without a rainwater rate must carry the &lt;code&gt;nichtVergleichbar&lt;/code&gt; flag. Otherwise its yearly total is incomplete and the page would not say so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headline figures match the table.&lt;/strong&gt; The cheapest city, the most expensive city, the spread and the averages shown at the top must equal what the table computes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sitemap date.&lt;/strong&gt; The page's "last checked" date and its sitemap &lt;code&gt;lastmod&lt;/code&gt; must agree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The text names the current extremes.&lt;/strong&gt; If the cheapest city changes and the page text still names the old one, the check warns.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Running a page's frontmatter without rendering it
&lt;/h3&gt;

&lt;p&gt;Because the full build was unreliable on my machine, the check also executes the page's real Astro frontmatter, without the renderer. It strips the &lt;code&gt;.astro&lt;/code&gt; component imports, rewrites the data imports to absolute paths, transpiles the result with esbuild and imports it from a &lt;code&gt;data:&lt;/code&gt; URL:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;transform&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;esbuild&lt;/span&gt;&lt;span class="dl"&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;js&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frontmatter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;esm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es2022&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tsconfigRaw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;compilerOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;verbatimModuleSyntax&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="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nx"&gt;code&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;mod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data:text/javascript;base64,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&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;One flag is essential: &lt;strong&gt;&lt;code&gt;verbatimModuleSyntax&lt;/code&gt;&lt;/strong&gt;. Without it, esbuild's TypeScript loader drops every import that the frontmatter itself does not use. Several imports are only used in the markup below the frontmatter, so the probe reported "x is not defined" for variables that exist perfectly well in the real page. The &lt;a href="https://esbuild.github.io/content-types/#typescript-caveats" rel="noopener noreferrer"&gt;TypeScript caveats in the esbuild documentation&lt;/a&gt; explain why the loader behaves this way.&lt;/p&gt;

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

&lt;p&gt;For the same household, the yearly bill across the 89 cities ranges from 350.40 € in Ludwigsburg to 1,412.90 € in Potsdam. Potsdam's rates are high, but it lands at the top partly because of a 120 € annual base fee. On the price per cubic metre alone, Herford (5.84 €/m³) is more expensive than Potsdam (5.60 €/m³), so a comparison of headline rates would have ranked them the other way round.&lt;/p&gt;

&lt;p&gt;That last point is the lesson I'd keep from the whole project. &lt;strong&gt;A comparison is only as honest as its least visible component.&lt;/strong&gt; Base fees, provision charges and surface weightings are easy to miss because they don't sit next to the headline rate. So it's worth reading each tariff sheet once more, specifically for the charges that don't scale with volume, before trusting any total.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I build and run the site linked above. The rates quoted are from the official sources linked in this post as of October 2026. If you spot a city whose numbers have changed, I'd like to hear about it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>datavalidation</category>
      <category>javascript</category>
      <category>astro</category>
      <category>opendata</category>
    </item>
  </channel>
</rss>
