<?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: Marco Kohns</title>
    <description>The latest articles on DEV Community by Marco Kohns (@marco_kohns_f07d8496a3f28).</description>
    <link>https://dev.to/marco_kohns_f07d8496a3f28</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%2F3734905%2Fada7a283-23db-4e9b-b8a8-9efa19924e0f.jpg</url>
      <title>DEV Community: Marco Kohns</title>
      <link>https://dev.to/marco_kohns_f07d8496a3f28</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marco_kohns_f07d8496a3f28"/>
    <language>en</language>
    <item>
      <title>Building a job training site for the Philippines on free tiers, where every number has a source</title>
      <dc:creator>Marco Kohns</dc:creator>
      <pubDate>Fri, 16 Oct 2026 22:31:11 +0000</pubDate>
      <link>https://dev.to/marco_kohns_f07d8496a3f28/building-a-job-training-site-for-the-philippines-on-free-tiers-where-every-number-has-a-source-20fe</link>
      <guid>https://dev.to/marco_kohns_f07d8496a3f28/building-a-job-training-site-for-the-philippines-on-free-tiers-where-every-number-has-a-source-20fe</guid>
      <description>&lt;p&gt;&lt;a href="https://www.trabahofit.com/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=devto-build" rel="noopener noreferrer"&gt;TrabahoFit&lt;/a&gt; helps Filipino job seekers train for a specific job: what it pays, what employers ask for and what to practice first. It runs for close to zero fixed cost, and every figure on it comes from a source a reader can check. Here is how it is built.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 16 with the App Router, React 19, TypeScript and Tailwind CSS v4&lt;/li&gt;
&lt;li&gt;Supabase (Sydney region, closest to the Philippines) with one table for quiz attempts, written only through RPCs&lt;/li&gt;
&lt;li&gt;Vercel for hosting; production deployments are indexable, preview and local builds are not, controlled in one place&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data first, pages second
&lt;/h2&gt;

&lt;p&gt;Every guide starts as a dataset in the repo: 50 call center job posts from JobStreet coded field by field (experience, education, salary, tests), 40 freelance posts from OnlineJobs.ph, 216 public e-TESDA courses, and the Department of Labor and Employment's in-demand jobs list. Each dataset has a small Python script that prints every figure the page uses, so a number on the site can be reproduced from the repo.&lt;/p&gt;

&lt;p&gt;That's how we found that 62% of the call center posts accepted beginners (n=50) and that none mentioned the Versant test, which most preparation content focuses on.&lt;/p&gt;

&lt;h2&gt;
  
  
  A gate before every push
&lt;/h2&gt;

&lt;p&gt;Pages follow a writing spec, and &lt;code&gt;npm run build &amp;amp;&amp;amp; npm run gate&lt;/code&gt; must pass before a push, so pages that break the content rules don't ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quiz that credits its landing page
&lt;/h2&gt;

&lt;p&gt;The quiz is embedded inline on every guide page and records which page it started on, so we can see which guides lead to quiz completions.&lt;/p&gt;

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

&lt;p&gt;Building the dataset before the page made the writing fast and the claims safe. If you want to see the result, start with the &lt;a href="https://www.trabahofit.com/career/call-center?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=devto-build" rel="noopener noreferrer"&gt;call center agent guide&lt;/a&gt;. Questions about the setup are welcome in the comments.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>supabase</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building a price comparison site where every price comes from our own checkout runs</title>
      <dc:creator>Marco Kohns</dc:creator>
      <pubDate>Fri, 16 Oct 2026 22:11:12 +0000</pubDate>
      <link>https://dev.to/marco_kohns_f07d8496a3f28/building-a-price-comparison-site-where-every-price-comes-from-our-own-checkout-runs-94k</link>
      <guid>https://dev.to/marco_kohns_f07d8496a3f28/building-a-price-comparison-site-where-every-price-comes-from-our-own-checkout-runs-94k</guid>
      <description>&lt;p&gt;&lt;a href="https://schliessfach24.de/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=devto-build" rel="noopener noreferrer"&gt;schliessfach24.de&lt;/a&gt; compares luggage lockers and luggage storage at German stations and airports. Its one rule: no advertised prices. Every number on the site comes from a checkout we ran up to the payment button, with a screenshot and a date. This post is about how that is built and kept honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Next.js with the App Router and Tailwind, statically generated pages per city and place (station, airport, trade fair, sight)&lt;/li&gt;
&lt;li&gt;Supabase behind the scenes, content as typed data files per city in &lt;code&gt;src/data/content/&amp;lt;city&amp;gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Vercel, with &lt;code&gt;main&lt;/code&gt; as production and a &lt;code&gt;preview&lt;/code&gt; branch for every change&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Research before code
&lt;/h2&gt;

&lt;p&gt;Each city starts as a research document: every provider near a place, the checkout run for a fixed scenario (one regular suitcase, same-day drop-off and pick-up, plus an overnight case), the total including service and protection fees, the billing rule (per started 24 hours or per calendar day), opening hours, payment methods and the screenshot file names. Official sources come first: Deutsche Bahn station pages and the airport operator's price list.&lt;/p&gt;

&lt;p&gt;The research is where the interesting findings come from. At Frankfurt Hbf, one suitcase overnight was 6.00 euros in a Deutsche Bahn locker and 11.99 or 13.65 euros through apps, because of fees and calendar-day billing. At Frankfurt Airport the apps' "airport" results were lockers 57 to 141 minutes' walk away.&lt;/p&gt;

&lt;h2&gt;
  
  
  A gate before every page
&lt;/h2&gt;

&lt;p&gt;A content skill turns the research into the typed page data and then runs a blocking gate per city: every page needs at least one screenshot as evidence, at least two data visualisations, a FAQ with three or more questions, a visible "last checked" date and &lt;code&gt;dateModified\&lt;/code&gt; plus an author in the JSON-LD. It fails on &lt;code&gt;noindex\&lt;/code&gt;, on banned words, on missing internal links inside the city cluster, and on any sentence that also appears on another city's page. Before a push, typecheck, build and an SEO audit against a local server have to pass too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured data that matches the page
&lt;/h2&gt;

&lt;p&gt;Each page emits one JSON-LD &lt;code&gt;@graph&lt;/code&gt;: Organization and WebSite with stable &lt;code&gt;@id&lt;/code&gt;s, the article with its image, a map (&lt;code&gt;hasMap&lt;/code&gt;), and the provider list with prices and opening hours, so search engines read the same facts the visitor sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping it fresh
&lt;/h2&gt;

&lt;p&gt;Every page shows when it was last checked, and prices are re-run at least every 90 days. That cadence is the main cost of the product, and the reason it is worth trusting.&lt;/p&gt;

&lt;p&gt;If you want to see the result, start with &lt;a href="https://schliessfach24.de/frankfurt/hbf?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=devto-build" rel="noopener noreferrer"&gt;Frankfurt Hbf&lt;/a&gt; (the site is in German). Happy to answer questions about the pipeline in the comments.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>seo</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building a local course directory as a fully static Next.js site</title>
      <dc:creator>Marco Kohns</dc:creator>
      <pubDate>Fri, 16 Oct 2026 17:09:20 +0000</pubDate>
      <link>https://dev.to/marco_kohns_f07d8496a3f28/ein-lokales-kursverzeichnis-als-statische-nextjs-seite-wie-kursvorort-gebaut-ist-2ef1</link>
      <guid>https://dev.to/marco_kohns_f07d8496a3f28/ein-lokales-kursverzeichnis-als-statische-nextjs-seite-wie-kursvorort-gebaut-ist-2ef1</guid>
      <description>&lt;p&gt;&lt;a href="https://kursvorort.de/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=devto-build" rel="noopener noreferrer"&gt;kursvorort.de&lt;/a&gt; is a directory for in-person courses in Germany, starting with swimming, dance and yoga classes in 15 cities. The gap it fills: course offers are spread across municipal pools, adult-education centres, clubs and private schools, and their prices can't be compared. This post is about how it is built, and why it runs without a database, without logins and without a single form.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 15 with the App Router, React 19, strict TypeScript, Tailwind&lt;/li&gt;
&lt;li&gt;fully static: every page is generated at build time via &lt;code&gt;generateStaticParams&lt;/code&gt;, with &lt;code&gt;dynamicParams = false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;hosted on Vercel, with cookieless Vercel Web Analytics&lt;/li&gt;
&lt;li&gt;no database and no accounts; providers get in touch by email&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sounds unusual for a directory, but it fits the content. Pages only change when the research is redone, not on every request. Static means fast pages, nothing that can fail at night, and no personal data to protect under GDPR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data as typed files
&lt;/h2&gt;

&lt;p&gt;All content lives as typed files in &lt;code&gt;src/data/&lt;/code&gt;: categories, cities, providers, and one content file per page such as &lt;code&gt;src/data/content/tanzkurse-berlin.ts&lt;/code&gt;. Pages never import that data directly; they read it through one layer in &lt;code&gt;src/lib/data.ts&lt;/code&gt;. That keeps the source swappable: once paid listings arrive, providers move into a database and no page has to change.&lt;/p&gt;

&lt;p&gt;The URL structure follows the search terms: &lt;code&gt;/{category}/{city}&lt;/code&gt;, so &lt;code&gt;/schwimmkurse/berlin&lt;/code&gt;, and one level deeper &lt;code&gt;/{category}/{city}/{course-type}&lt;/code&gt; for course types. Title, H1 and meta description come from one central pattern, and tests check their length.&lt;/p&gt;

&lt;h2&gt;
  
  
  Research as a pipeline with a gate
&lt;/h2&gt;

&lt;p&gt;The interesting part is the path from research to page. Every city and category starts as a research document with every offer taken from primary sources: price, sessions, minutes per session, mandatory extras like pool entry or club membership, booking route, source and date. From that, the pipeline computes the all-in price per unit (45 minutes for swimming, 60 minutes per person for dance and yoga) and the median per provider type.&lt;/p&gt;

&lt;p&gt;Before a page is allowed into the build, a blocking gate runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every number in the text needs a source with a date, and every table needs a source line right below it&lt;/li&gt;
&lt;li&gt;external links in editorial text must be &lt;code&gt;nofollow&lt;/code&gt;, and a page needs a minimum number of distinct source domains&lt;/li&gt;
&lt;li&gt;no ratings or stars in the text, and no future dates in the prose (those go into course-start blocks that expire on their own)&lt;/li&gt;
&lt;li&gt;no sentence may appear word for word on another city page, and every city needs sections of its own&lt;/li&gt;
&lt;li&gt;FAQ schema, a visible "last checked" date and required blocks like the price comparison and the calculator, plus typecheck, tests, build, an SEO audit against the local server and a mobile check&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only when everything passes does the page get built.&lt;/p&gt;

&lt;h2&gt;
  
  
  Indexing only with content
&lt;/h2&gt;

&lt;p&gt;A city page is indexable only if it has editorial content or at least three approved providers. Otherwise it gets &lt;code&gt;noindex,follow&lt;/code&gt; and stays out of the sitemap. Internal links only point to indexable pages, and cities without research show as "coming soon" without a link. The sitemap is generated from the same data and currently lists 96 URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  No redirects, ever
&lt;/h2&gt;

&lt;p&gt;kursvorort had an earlier version built with Lovable, with its own URLs. The rebuild kept every existing URL exactly as it was: a test checks the old sitemap for status 200 without a redirect and for a self-referencing canonical. Routes that are no longer needed simply aren't built and return 404.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from it
&lt;/h2&gt;

&lt;p&gt;For a directory whose value is verified data, a static site with data files and a gate is the simplest architecture that holds up. The complexity sits in the research and in the rules the gate enforces, which is where it belongs.&lt;/p&gt;

&lt;p&gt;If you want to see the result, start with &lt;a href="https://kursvorort.de/tanzkurse/berlin?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=devto-build" rel="noopener noreferrer"&gt;dance classes in Berlin&lt;/a&gt; (the site itself is in German). Happy to answer questions about the pipeline in the comments.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>seo</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Product-Channel Fit Is Why Your Lovable App Has No Users</title>
      <dc:creator>Marco Kohns</dc:creator>
      <pubDate>Tue, 27 Jan 2026 10:35:05 +0000</pubDate>
      <link>https://dev.to/marco_kohns_f07d8496a3f28/product-channel-fit-is-why-your-lovable-app-has-no-users-2bn</link>
      <guid>https://dev.to/marco_kohns_f07d8496a3f28/product-channel-fit-is-why-your-lovable-app-has-no-users-2bn</guid>
      <description>&lt;p&gt;Most builders talk about product-market fit. Few talk about product-channel fit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product-channel fit asks a different question:&lt;/strong&gt;&lt;br&gt;
Does this product naturally acquire users through a specific channel without brute force?&lt;/p&gt;

&lt;h2&gt;
  
  
  If the answer is no, your MVP is incomplete.
&lt;/h2&gt;

&lt;p&gt;A Lovable app built without a channel in mind often looks fine but fails in predictable ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No indexable pages for SEO&lt;/li&gt;
&lt;li&gt;No lead capture for outbound&lt;/li&gt;
&lt;li&gt;No onboarding logic for PLG&lt;/li&gt;
&lt;li&gt;No feedback loop to measure activation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not marketing issues. They are product decisions that were never made.&lt;/p&gt;

&lt;p&gt;In our workflow, the channel is selected first. The MVP then inherits the requirements of that channel.&lt;/p&gt;

&lt;p&gt;That single constraint eliminates most early-stage confusion. You are no longer guessing what to build next. The channel tells you.&lt;/p&gt;

&lt;p&gt;We codified this workflow into Lovable Prompts so the output prompt already includes distribution requirements alongside features and tech.&lt;/p&gt;

&lt;p&gt;If your app cannot explain how it gets users, it is not done yet.&lt;/p&gt;

&lt;p&gt;The full breakdown is in the free Distribution-First Lovable Prompting Handbook. You can access it for free here: &lt;a href="https://lovable-prompts.com" rel="noopener noreferrer"&gt;lovable-prompts.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>vibecoding</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
