<?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: MyTaghazout</title>
    <description>The latest articles on DEV Community by MyTaghazout (@mytaghazout).</description>
    <link>https://dev.to/mytaghazout</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%2F4080604%2F6b7c5dfc-1e90-4b8e-8f2c-74529ea9c1f3.png</url>
      <title>DEV Community: MyTaghazout</title>
      <link>https://dev.to/mytaghazout</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mytaghazout"/>
    <language>en</language>
    <item>
      <title>I run a surf forecast for 20 breaks in Morocco on EUR 0/month. Here's the stack.</title>
      <dc:creator>MyTaghazout</dc:creator>
      <pubDate>Mon, 17 Aug 2026 00:04:53 +0000</pubDate>
      <link>https://dev.to/mytaghazout/i-run-a-surf-forecast-for-20-breaks-in-morocco-on-eur-0month-heres-the-stack-1ppf</link>
      <guid>https://dev.to/mytaghazout/i-run-a-surf-forecast-for-20-breaks-in-morocco-on-eur-0month-heres-the-stack-1ppf</guid>
      <description>&lt;p&gt;I live on the Taghazout coast in Morocco - a strip of Atlantic between Agadir and Imsouane that's basically one long right-hand point break after another. Two years ago the only way to know if tomorrow was worth it was to check three different global forecast sites, none of which knew the difference between Anchor Point and the beach break 400m south of it.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://taghazout.io" rel="noopener noreferrer"&gt;taghazout.io&lt;/a&gt;. It now covers 20 named breaks, runs in 10 languages, and costs me nothing per month. Here's how it's actually put together - including the parts I'd do differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack is deliberately boring
&lt;/h2&gt;

&lt;p&gt;Hand-rolled PHP. No framework, no build step, no node_modules. About 4,800 files, server-rendered, no hydration.&lt;/p&gt;

&lt;p&gt;That sounds like a confession, but it was the right call for one reason: &lt;strong&gt;my readers are on phones, on cafe Wi-Fi, often on 3G.&lt;/strong&gt; A server-rendered page that ships HTML and a little CSS beats anything I could have built with a client-side framework in that environment. Time-to-content is the only metric that matters when someone is standing on the beach deciding whether to paddle out.&lt;/p&gt;

&lt;p&gt;The hosting is a cheap shared plan. The forecast data is free and open. The whole thing runs at &lt;strong&gt;EUR 0/month recurring&lt;/strong&gt;, which was a hard constraint from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting part: two ocean models that disagree
&lt;/h2&gt;

&lt;p&gt;The forecast blends two sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open-Meteo&lt;/strong&gt; (CC BY 4.0) - the primary, with a marine endpoint that covers our coastal cells.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NOAA WaveWatch III&lt;/strong&gt; via PacIOOS - the second opinion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the thing nobody tells you: &lt;strong&gt;they disagree, a lot.&lt;/strong&gt; On the same hour at the same break I've seen WaveWatch read ~55% higher than Open-Meteo (1.36m vs 0.88m). Offshore models resolve coastal bathymetry badly, and our points are exactly the kind of close-in, shallow-reef setups where that bias shows up.&lt;/p&gt;

&lt;p&gt;The wrong fix is to pick one and pretend. What I did instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run both, cache both.&lt;/li&gt;
&lt;li&gt;Compute agreement over a 72-hour window - a Pearson correlation on the swell rhythm plus a circular difference on direction (you can't average 350 and 10 degrees naively).&lt;/li&gt;
&lt;li&gt;Show the user a confidence flag: &lt;strong&gt;agree / mixed / diverge&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A recent live read for Anchor Point: r=0.88, 8 degrees direction difference over 73 hours, so "agree". When it says &lt;em&gt;diverge&lt;/em&gt;, that's genuinely useful information - it means treat the numbers with suspicion, not that the site is broken.&lt;/p&gt;

&lt;p&gt;Some gotchas from the WaveWatch integration, since these cost me a full evening:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The ERDDAP query brackets &lt;strong&gt;must&lt;/strong&gt; be percent-encoded as &lt;code&gt;%5B&lt;/code&gt; / &lt;code&gt;%5D&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The time stop must be the literal &lt;code&gt;(last)&lt;/code&gt;, not a future timestamp you computed.&lt;/li&gt;
&lt;li&gt;The coastal grid cell you'd expect (lon 350.5) is &lt;strong&gt;dry land&lt;/strong&gt;. You have to fall back half a degree offshore to get swell at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Honest data beats impressive data
&lt;/h2&gt;

&lt;p&gt;The tide readout says, in the payload itself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"modelled sea level, relative to this window's low"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because that's what it is. It isn't a harbour gauge, and pretending otherwise would get someone hurt on a reef. Every derived number on the site carries its caveat, and the public feed carries the same note in a &lt;code&gt;source_note&lt;/code&gt; field. I'd rather be trusted than look authoritative.&lt;/p&gt;

&lt;h2&gt;
  
  
  I opened up the feed
&lt;/h2&gt;

&lt;p&gt;The same data the site's own &lt;a href="https://taghazout.io/weather/" rel="noopener noreferrer"&gt;forecast pages&lt;/a&gt; run on is public, keyless JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://taghazout.io/weather-data/_feed.php?loc=anchor-point"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Anchor Point"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"area"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"North Taghazout"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"swell_m"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.66&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"period_s"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;8.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"wind_kmh"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;11.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"water_temp_c"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;21.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"surf_potential_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tide"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rising tide"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"trend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rising"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"height_m"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.53&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a small MIT-licensed client for it in Python and JS: &lt;a href="https://github.com/MyTaghazout/taghazout-surf" rel="noopener noreferrer"&gt;github.com/MyTaghazout/taghazout-surf&lt;/a&gt;, a &lt;a href="https://taghazout.gitbook.io/taghazout-docs/introduction" rel="noopener noreferrer"&gt;full API reference&lt;/a&gt;, and the spot registry is published as a &lt;a href="https://huggingface.co/datasets/MyTaghazout/taghazout-surf-spots" rel="noopener noreferrer"&gt;dataset on Hugging Face&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you build something with it, carry the attribution: weather by Open-Meteo (CC BY 4.0), wave model NOAA WaveWatch III via PacIOOS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ten languages was too many, too early.&lt;/strong&gt; Nine of them started as machine output and I have spent months undoing chimera sentences - English word order with individual words swapped. If I started again: two languages done properly, and add the rest only when each one has real readers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I measured nothing for six months.&lt;/strong&gt; The analytics tag was gated behind cookie consent in a way that made most visitors invisible, so I was optimising blind. Fix your measurement before you optimise anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy to answer anything about the model-blending or the caching layer in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Weather data by &lt;a href="https://open-meteo.com/" rel="noopener noreferrer"&gt;Open-Meteo.com&lt;/a&gt; (CC BY 4.0). Wave model NOAA WaveWatch III via PacIOOS.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>php</category>
      <category>showdev</category>
      <category>api</category>
    </item>
  </channel>
</rss>
