<?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: Ahmad Adil</title>
    <description>The latest articles on DEV Community by Ahmad Adil (@ahmad_adil_d9e7ddfbdb26ad).</description>
    <link>https://dev.to/ahmad_adil_d9e7ddfbdb26ad</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%2F4245119%2F3c0d3f92-17d1-4fed-aaa2-e3087439338f.png</url>
      <title>DEV Community: Ahmad Adil</title>
      <link>https://dev.to/ahmad_adil_d9e7ddfbdb26ad</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ahmad_adil_d9e7ddfbdb26ad"/>
    <language>en</language>
    <item>
      <title>How I scrape hackathon platforms every day with GitHub Actions and Supabase (and the bug that froze my site for six weeks)</title>
      <dc:creator>Ahmad Adil</dc:creator>
      <pubDate>Sat, 14 Nov 2026 21:48:07 +0000</pubDate>
      <link>https://dev.to/ahmad_adil_d9e7ddfbdb26ad/how-i-scrape-hackathon-platforms-every-day-with-github-actions-and-supabase-and-the-bug-that-froze-420p</link>
      <guid>https://dev.to/ahmad_adil_d9e7ddfbdb26ad/how-i-scrape-hackathon-platforms-every-day-with-github-actions-and-supabase-and-the-bug-that-froze-420p</guid>
      <description>&lt;p&gt;Hackathons are published all over the place. Devpost, Devfolio, Unstop, MLH, CTFtime, DoraHacks, Google Developer Groups, plus a lot of local events that only exist as an Instagram post. I'm a student in Pakistan and I kept finding out about events after the deadline, so I built &lt;a href="https://www.devsafar.app" rel="noopener noreferrer"&gt;DevSafar&lt;/a&gt;, a free site that collects them into one list.&lt;/p&gt;

&lt;p&gt;This post is about how it works and, more usefully, about what went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scrapers&lt;/strong&gt; run daily on GitHub Actions. Some platforms have JSON endpoints; the JavaScript-heavy ones need Puppeteer. Each scraper writes rows into Supabase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two tables&lt;/strong&gt;: one for community-submitted events, one for scraped ones. Both are public-readable for approved rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleanup scripts&lt;/strong&gt; deduplicate (the same event often appears on two platforms), normalise locations and categories, and mark ended events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The app&lt;/strong&gt; is Expo and React Native for web and Android, from one codebase, hosted on Vercel.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bug: a frozen site and 15 indexed pages
&lt;/h2&gt;

&lt;p&gt;I assumed the site updated itself. It didn't.&lt;/p&gt;

&lt;p&gt;The scraper wrote to the database every night, and the app read the database, so everything looked fine when I used it. But the pages that search engines see are generated when Vercel &lt;em&gt;builds&lt;/em&gt;, and nothing triggered a build after the scrape. A secret for the deploy hook was missing and the workflow step skipped silently when it wasn't set.&lt;/p&gt;

&lt;p&gt;For six weeks, the live sitemap stayed frozen at 3,588 URLs while the database had grown to 5,191 listings. Every new hackathon was served as a blank page with an empty title.&lt;/p&gt;

&lt;p&gt;Worse, even the pages that were built were nearly identical. The details loaded with JavaScript after the page opened, so the HTML that Googlebot and Bingbot read had the same 219 characters of body text on all ~5,000 hackathon pages. Google's verdict in Search Console: 15 pages indexed, over 3,600 "crawled, currently not indexed".&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild after every scrape.&lt;/strong&gt; The workflow now calls a Vercel deploy hook, and fails loudly (a red run and an email) if the secret is missing, instead of skipping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real content in the HTML.&lt;/strong&gt; Expo Router has build-time data loaders. Each page's &lt;code&gt;loader&lt;/code&gt; runs once during &lt;code&gt;expo export&lt;/code&gt; and the data is written into the page, so a crawler sees the title, dates, prize and links without running any JavaScript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't pre-render everything.&lt;/strong&gt; Pre-rendering all 5,191 pages ran the build out of memory, and events that ended two months ago don't deserve a place in the index anyway. They're now &lt;code&gt;noindex&lt;/code&gt; and left out of the sitemap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pages that match what people search for&lt;/strong&gt;, generated from the data: live now, online, free, by country, by city, by month. A page only exists if it has enough listings, and none is generated if it would be nearly identical to its parent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A smaller JavaScript bundle.&lt;/strong&gt; three.js (used only for a login-screen background) was in the bundle every page downloaded. Moving it behind a dynamic &lt;code&gt;import()&lt;/code&gt; cut the shared bundle from 3.1 MB to 1.6 MB.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I'd tell myself earlier
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A scheduled job that writes data and a static site that reads it need an explicit link between them. Check the &lt;em&gt;deployed&lt;/em&gt; output, not the app.&lt;/li&gt;
&lt;li&gt;View source on a few pages. If the content isn't in the HTML, search engines mostly won't see it.&lt;/li&gt;
&lt;li&gt;Make "this step is not configured" a failure, not a skip.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site is free and I'd like feedback: &lt;a href="https://www.devsafar.app" rel="noopener noreferrer"&gt;www.devsafar.app&lt;/a&gt;. Which platforms are missing?&lt;/p&gt;

</description>
      <category>hackathon</category>
      <category>showdev</category>
      <category>webdev</category>
      <category>supabase</category>
    </item>
  </channel>
</rss>
