<?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: Jean Carlo - Dev</title>
    <description>The latest articles on DEV Community by Jean Carlo - Dev (@jean_carloschmitz).</description>
    <link>https://dev.to/jean_carloschmitz</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%2F2143549%2Ffaeb914e-0a6b-4d84-bdc5-5bc722f2b7c4.png</url>
      <title>DEV Community: Jean Carlo - Dev</title>
      <link>https://dev.to/jean_carloschmitz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jean_carloschmitz"/>
    <language>en</language>
    <item>
      <title>How I schedule blog posts on a 100% static site for $0, no server, no CMS, no runtime database</title>
      <dc:creator>Jean Carlo - Dev</dc:creator>
      <pubDate>Tue, 16 Jun 2026 16:13:12 +0000</pubDate>
      <link>https://dev.to/jean_carloschmitz/how-i-schedule-blog-posts-on-a-100-static-site-for-0-no-server-no-cms-no-runtime-database-12j8</link>
      <guid>https://dev.to/jean_carloschmitz/how-i-schedule-blog-posts-on-a-100-static-site-for-0-no-server-no-cms-no-runtime-database-12j8</guid>
      <description>&lt;p&gt;For a while I believed the two things I wanted were mutually exclusive:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;fully static site&lt;/strong&gt;. Cloudflare Pages, &lt;code&gt;adapter-static&lt;/code&gt;, everything pre-rendered, zero servers to babysit, free tier forever.&lt;/li&gt;
&lt;li&gt;A blog where I could write a post today and have it &lt;strong&gt;go live next Tuesday at 7am&lt;/strong&gt;, without me being awake to push a button.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Static means the HTML is frozen at build time. Scheduling means content appears based on the clock. Those feel like opposites. The usual answer is "just add a CMS" or "render the blog server-side." I didn't want either. So I went looking for a third option, and it turned out to be simpler than I expected and it costs &lt;strong&gt;$0/month&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's the architecture I landed on for &lt;a href="https://quickeasy.tools/" rel="noopener noreferrer"&gt;Quick Tools&lt;/a&gt;, and why each piece is the way it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: the database is a build-time input, not a runtime dependency
&lt;/h2&gt;

&lt;p&gt;The trick is to stop thinking of the database as something the &lt;em&gt;site&lt;/em&gt; talks to, and start thinking of it as something the &lt;em&gt;build&lt;/em&gt; talks to.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The blog content lives in &lt;strong&gt;Neon Postgres&lt;/strong&gt; (one row per &lt;code&gt;(slug, lang)&lt;/code&gt; pair).&lt;/li&gt;
&lt;li&gt;A Node script reads that database &lt;strong&gt;once, during the build&lt;/strong&gt;, and writes the posts to disk as plain &lt;code&gt;meta.json&lt;/code&gt; + &lt;code&gt;{lang}.md&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;The rest of the build (SvelteKit's SSG, sitemap, RSS, OG images) reads those files like they were always there.&lt;/li&gt;
&lt;li&gt;The shipped site contains &lt;strong&gt;zero&lt;/strong&gt; database code. There's no connection string in the client bundle, no API route, nothing. The DB might as well not exist once the build finishes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the site stays 100% static. The database is just where I keep my drafts and schedule a fancy spreadsheet that the build pipeline happens to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Materialization: turning rows into files
&lt;/h2&gt;

&lt;p&gt;The whole publishing rule lives in one SQL &lt;code&gt;WHERE&lt;/code&gt; clause:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`
  SELECT slug, lang, title, excerpt, body, cat, published_at, /* ... */
  FROM blog_posts
  WHERE status IN ('scheduled', 'published')
    AND published_at &amp;lt;= now()
  ORDER BY slug, lang
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire scheduling engine. A post is visible &lt;strong&gt;only&lt;/strong&gt; when its status is &lt;code&gt;scheduled&lt;/code&gt; or &lt;code&gt;published&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; its &lt;code&gt;published_at&lt;/code&gt; is in the past. A post dated next Tuesday simply doesn't come back from this query today — so it never gets written to disk, so it never ends up in the build.&lt;/p&gt;

&lt;p&gt;Then we just write the rows out as files:&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;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;grouped&lt;/span&gt;&lt;span class="p"&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;dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BLOG_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contents&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rowsToFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;meta.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;stringifyMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&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;lang&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.md`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&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;This script runs &lt;strong&gt;first&lt;/strong&gt; in the build pipeline, before sitemap/RSS/OG generation all of which read the materialized files. Nothing downstream knows or cares that the content came from Postgres. It just sees a folder full of markdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  The clock: who triggers the build when nobody pushed?
&lt;/h2&gt;

&lt;p&gt;A static site only changes when it rebuilds. A push to &lt;code&gt;main&lt;/code&gt; rebuilds it  but I'm not going to push a commit at 7am every day just to flip a post live. So the build needs to fire on a schedule too.&lt;/p&gt;

&lt;p&gt;GitHub Actions cron, three times a day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Rebuild to publish due scheduled posts. ~90 builds/month within the&lt;/span&gt;
    &lt;span class="c1"&gt;# Cloudflare Pages free tier (500/month). Times are UTC.&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt; &lt;span class="c1"&gt;# 07h BRT&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;15&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt; &lt;span class="c1"&gt;# 12h BRT&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;22&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt; &lt;span class="c1"&gt;# 19h BRT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every cron run is a full rebuild. The build re-runs that SQL query against &lt;code&gt;now()&lt;/code&gt;, and any post whose &lt;code&gt;published_at&lt;/code&gt; has passed since the last build gets materialized and shipped. No post-go-live commit, no manual deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  The honest trade-off
&lt;/h3&gt;

&lt;p&gt;Go-live is &lt;strong&gt;the next cron run after &lt;code&gt;published_at&lt;/code&gt;, not the exact minute&lt;/strong&gt;. If I schedule a post for 07:30 BRT, it goes live at the 12h build, not at 07:30 on the dot. For a blog, "live within a few hours of the scheduled time" is completely fine and that imprecision is the price of staying static. I decided up front this was an acceptable trade, and three builds a day keeps the worst-case lag to a handful of hours.&lt;/p&gt;

&lt;p&gt;The build budget math also matters: 3 builds/day ≈ 90 builds/month, comfortably inside Cloudflare Pages' free tier of 500/month. I get scheduling &lt;strong&gt;and&lt;/strong&gt; I never see a bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually bit me: don't let an empty database wipe your blog
&lt;/h2&gt;

&lt;p&gt;Here's the failure mode that's easy to miss. The build reads the DB and overwrites the &lt;code&gt;blog/&lt;/code&gt; directory. So what happens the day Neon has a hiccup, or a connection times out, or a bad query returns zero rows?&lt;/p&gt;

&lt;p&gt;The naive version of this script clears &lt;code&gt;blog/&lt;/code&gt;, gets nothing back from the database, writes nothing and cheerfully deploys an &lt;strong&gt;empty blog&lt;/strong&gt;. A transient database blip becomes a production content wipe. That's the kind of thing you discover at the worst possible moment.&lt;/p&gt;

&lt;p&gt;The fix is to treat the committed files as a last-known-good snapshot and refuse to destroy them on a bad read:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchRows&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&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;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listPostDirs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BLOG_DIR&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;listPostDirs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;NEWS_DIR&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`⚠  Could not reach Neon (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;). Keeping committed snapshot of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; posts.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// proceed with last-known-good content&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Neon unreachable and no committed snapshot exists`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`⚠  Query returned 0 posts. Keeping committed snapshot instead of wiping blog/.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;writePosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;groupBySlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&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;Two guards, both pointing the same way: &lt;strong&gt;a bad or empty read never wipes good content.&lt;/strong&gt; If Neon is unreachable, we keep whatever's committed in git and log a warning. If the query genuinely returns zero rows (suspicious I always have published posts), same thing. The blog only gets rewritten when the database hands back real data.&lt;/p&gt;

&lt;p&gt;This is why the &lt;code&gt;blog/&lt;/code&gt; files are committed to the repo at all. They're not the source of truth Neon is but they're a durable fallback that makes every deploy safe even when the source of truth is temporarily gone. The materialization is idempotent: a clean build against unchanged data produces a byte-identical diff, so committing the snapshot stays noise-free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authoring: writing and scheduling without touching SQL
&lt;/h2&gt;

&lt;p&gt;The day-to-day flow never involves writing SQL by hand. There's an upsert script that reads a post directory (the same &lt;code&gt;meta.json&lt;/code&gt; + &lt;code&gt;{lang}.md&lt;/code&gt; format) and writes it into Neon with a status and a timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# schedule a post to go live at a specific time (status = scheduled)&lt;/span&gt;
pnpm blog:upsert .claude/articles/my-post &lt;span class="nt"&gt;--at&lt;/span&gt; 2026-06-20T10:00:00Z

&lt;span class="c"&gt;# publish immediately&lt;/span&gt;
pnpm blog:upsert .claude/articles/my-post &lt;span class="nt"&gt;--status&lt;/span&gt; published
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I write markdown locally, run one command to push it into the schedule, and forget about it. The next cron build after that timestamp puts it live. The files-on-disk format and the database rows are two views of the same thing, and a pure mapping module converts between them with no database code in it at all which keeps the migration, the build, and the authoring script all sharing one source of truth for the shape of a post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this beats the obvious alternatives
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;vs. a real CMS / headless backend:&lt;/strong&gt; No server to run, patch, or pay for. No runtime coupling if my database vendor disappeared tomorrow, the currently-deployed site keeps working forever, because the content is baked in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vs. server-side rendering the blog:&lt;/strong&gt; SSR would give me exact-minute scheduling, but at the cost of a running server on every request, cold starts, and a database in the hot path of page loads. For a content blog, that's a lot of moving parts to buy precision I don't need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vs. pure git-based (markdown in the repo, no DB):&lt;/strong&gt; This is the closest alternative, and it's great until you want &lt;em&gt;scheduling&lt;/em&gt;. Git has no concept of "publish this Tuesday." You'd be back to writing a commit at go-live time exactly what I was trying to avoid. The database gives me a &lt;code&gt;published_at&lt;/code&gt; column and a cron job does the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this breaks down and the escape hatch
&lt;/h2&gt;

&lt;p&gt;No architecture is free of limits, and the honest thing is to name where this one stops working before you hit it.&lt;/p&gt;

&lt;p&gt;The ceiling isn't disk and it isn't build time it's the &lt;strong&gt;file count per deployment&lt;/strong&gt;. Cloudflare Pages caps a deploy at 20,000 files, and every prerendered page ships as one or two files (the &lt;code&gt;.html&lt;/code&gt;, plus a separate data file if SvelteKit doesn't inline it). So &lt;code&gt;files ≈ (routes × languages) + JS/CSS chunks + images&lt;/code&gt;. At a steady publishing pace this grows linearly, and somewhere in the thousands of posts you'd start eyeing that 20k wall.&lt;/p&gt;

&lt;p&gt;The instinct is "then I'll have to switch to SSR." You don't at least not all of it. The key realization: &lt;strong&gt;a prerendered page is a file; an on-demand (SSR) page is not.&lt;/strong&gt; A page rendered at the edge on request generates zero static files, so it simply doesn't count toward the limit.&lt;/p&gt;

&lt;p&gt;That turns "static vs. SSR" from a binary into a dial. The migration path, when the day comes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;First, just trim.&lt;/strong&gt; Stop prerendering the truly disposable long-tail (for me, that's dated news posts in every language). No architecture change, buys years.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then go hybrid.&lt;/strong&gt; Swap &lt;code&gt;adapter-static&lt;/code&gt; for the Cloudflare adapter and decide &lt;em&gt;per route&lt;/em&gt;: keep the pages that matter tools, the blog index, recent and popular posts prerendered as static files; let the long-tail render &lt;strong&gt;on-demand at the edge&lt;/strong&gt;. Those edge-rendered pages produce no files, so the file budget stops growing for them. Crucially, a crawler hitting an edge-rendered page gets the same fully-formed HTML it would from a static file &lt;strong&gt;SSR at the edge costs you a few milliseconds of compute, not your SEO.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only at extreme scale&lt;/strong&gt; would you cache SSR responses at the edge with revalidation and that's a problem most projects never have.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the answer to "will I eventually need a server?" is: only for the slice that actually needs it, and even then it's edge compute on a free tier, not a box you babysit. The trigger to watch is concrete total files in the build output approaching ~80% of the limit. You act on a number, not on a panic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The reframe that unlocked it: &lt;strong&gt;"static" is a property of what you ship, not of where your content lives.&lt;/strong&gt; A database can absolutely be part of a static site's life as long as it only ever speaks to the build, never to the browser.&lt;/p&gt;

&lt;p&gt;Once you accept that builds are cheap and can be triggered by a clock, "scheduled publishing on a static site" stops being a contradiction and becomes a cron job plus one &lt;code&gt;WHERE&lt;/code&gt; clause. The only real engineering left is making sure a bad read can't take your content down with it. And the best part: the whole stack Cloudflare Pages, GitHub Actions, Neon runs on the free tier. Monthly cost: $0.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Quick Tools is a platform of online utilities. Check it out at &lt;a href="https://quickeasy.tools/" rel="noopener noreferrer"&gt;quickeasy.tools&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>sveltekit</category>
      <category>jamstack</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>Generate Realistic Mock JSON Data for API Testing (No Backend, No Setup)</title>
      <dc:creator>Jean Carlo - Dev</dc:creator>
      <pubDate>Thu, 16 Apr 2026 13:48:33 +0000</pubDate>
      <link>https://dev.to/jean_carloschmitz/generate-realistic-mock-json-data-for-api-testing-no-backend-no-setup-4le1</link>
      <guid>https://dev.to/jean_carloschmitz/generate-realistic-mock-json-data-for-api-testing-no-backend-no-setup-4le1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcpajh0lfo3sdmetikt3r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcpajh0lfo3sdmetikt3r.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you're building or testing an API, you hit the same wall every&lt;br&gt;
time:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I just need realistic JSON data... fast."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You don't want to: - create seed scripts - install faker libraries -&lt;br&gt;
spin up a backend - or manually type fake users, products, or orders&lt;/p&gt;

&lt;p&gt;You just want JSON.&lt;/p&gt;

&lt;p&gt;ߑ &lt;a href="https://quickeasy.tools/en/tools/json-mock-generator" rel="noopener noreferrer"&gt;https://quickeasy.tools/en/tools/json-mock-generator&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The problem with most mock data approaches
&lt;/h2&gt;

&lt;p&gt;Typical flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;faker
create script
run script
adjust fields
run again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or worse... copy/paste from old projects.&lt;/p&gt;

&lt;p&gt;This breaks your flow when you're: - prototyping a frontend - testing&lt;br&gt;
API contracts - mocking responses - creating Postman collections -&lt;br&gt;
writing automated tests&lt;/p&gt;

&lt;p&gt;You don't need a project.\&lt;br&gt;
You need data.&lt;/p&gt;


&lt;h2&gt;
  
  
  What this JSON Mock Generator does
&lt;/h2&gt;

&lt;p&gt;You define fields like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  id (UUID)&lt;/li&gt;
&lt;li&gt;  full_name&lt;/li&gt;
&lt;li&gt;  username&lt;/li&gt;
&lt;li&gt;  email&lt;/li&gt;
&lt;li&gt;  phone&lt;/li&gt;
&lt;li&gt;  address&lt;/li&gt;
&lt;li&gt;  company&lt;/li&gt;
&lt;li&gt;  job_title&lt;/li&gt;
&lt;li&gt;  active (boolean)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And instantly get:&lt;br&gt;
&lt;/p&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"e24e0d8f-a5e-4c13-8ec5-0af008884f"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"full_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;"Kevin Wilson"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kevin.wilson"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kevin.wilson@email.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"phone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+1 (555) 709-795"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"address"&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;"street"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"497 Maple Dr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dallas"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"zip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"52278"&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;"company"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lorem ipsum dolor sit amet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"job_title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DevOps"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;No setup. No login. No libraries.&lt;/p&gt;




&lt;h2&gt;
  
  
  Perfect for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  Frontend developers mocking API responses&lt;/li&gt;
&lt;li&gt;  Backend developers testing serializers&lt;/li&gt;
&lt;li&gt;  QA creating test payloads&lt;/li&gt;
&lt;li&gt;  Postman / Insomnia testing&lt;/li&gt;
&lt;li&gt;  Writing unit tests&lt;/li&gt;
&lt;li&gt;  Prototyping dashboards&lt;/li&gt;
&lt;li&gt;  Generating seed-like data instantly&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Custom schemas (this is the killer feature)
&lt;/h2&gt;

&lt;p&gt;You're not stuck with "users".&lt;/p&gt;

&lt;p&gt;You can build &lt;strong&gt;any structure&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  products&lt;/li&gt;
&lt;li&gt;  orders&lt;/li&gt;
&lt;li&gt;  invoices&lt;/li&gt;
&lt;li&gt;  customers&lt;/li&gt;
&lt;li&gt;  logs&lt;/li&gt;
&lt;li&gt;  nested objects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add fields. Choose types. Generate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why browser-based is better
&lt;/h2&gt;

&lt;p&gt;Because you don't leave your flow.&lt;/p&gt;

&lt;p&gt;No: - npm - docker - scripts - dependencies&lt;/p&gt;

&lt;p&gt;Just open → configure → copy JSON.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example use cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mock API response for frontend
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste generated JSON into your mock server.&lt;/p&gt;




&lt;h3&gt;
  
  
  Unit tests
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fakeUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./mock.json&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;Done.&lt;/p&gt;




&lt;h3&gt;
  
  
  Postman collection
&lt;/h3&gt;

&lt;p&gt;Paste into response body simulation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Types supported
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  UUID&lt;/li&gt;
&lt;li&gt;  Full name&lt;/li&gt;
&lt;li&gt;  Username&lt;/li&gt;
&lt;li&gt;  Email&lt;/li&gt;
&lt;li&gt;  Phone&lt;/li&gt;
&lt;li&gt;  Address&lt;/li&gt;
&lt;li&gt;  Company&lt;/li&gt;
&lt;li&gt;  Words / Lorem&lt;/li&gt;
&lt;li&gt;  Enum&lt;/li&gt;
&lt;li&gt;  Boolean&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can mix everything into your own schema.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it here
&lt;/h2&gt;

&lt;p&gt;ߑ &lt;a href="https://quickeasy.tools/en/tools/json-mock-generator" rel="noopener noreferrer"&gt;https://quickeasy.tools/en/tools/json-mock-generator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Takes 5 seconds to generate your first dataset.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I made this
&lt;/h2&gt;

&lt;p&gt;I was tired of wasting time generating fake data every time I needed to&lt;br&gt;
test something.&lt;/p&gt;

&lt;p&gt;So this became my internal tool.\&lt;br&gt;
Now it's public.&lt;/p&gt;

&lt;p&gt;If you build APIs, frontends, or tests --- this saves real time.&lt;/p&gt;




&lt;p&gt;If you have suggestions for new field types, I'm all ears.&lt;/p&gt;

</description>
      <category>api</category>
      <category>productivity</category>
      <category>testing</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Why I started building my own “simple tools” website</title>
      <dc:creator>Jean Carlo - Dev</dc:creator>
      <pubDate>Tue, 07 Apr 2026 18:20:46 +0000</pubDate>
      <link>https://dev.to/jean_carloschmitz/why-i-started-building-my-own-simple-tools-website-5b96</link>
      <guid>https://dev.to/jean_carloschmitz/why-i-started-building-my-own-simple-tools-website-5b96</guid>
      <description>&lt;p&gt;As developers, we often need very small utilities: format JSON, decode base64, generate hashes, convert timestamps…&lt;/p&gt;

&lt;p&gt;But most websites offering these tools are overloaded with &lt;strong&gt;ads&lt;/strong&gt;, &lt;strong&gt;trackers&lt;/strong&gt; and &lt;strong&gt;login walls&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I wanted something:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;clean&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;fast&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;distraction-free&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I started building &lt;a href="https://quickeasy.tools" rel="noopener noreferrer"&gt;https://quickeasy.tools&lt;/a&gt;&lt;br&gt;
 — a collection of minimal online tools that just work.&lt;/p&gt;

&lt;p&gt;It’s a small side project, but I plan to grow it weekly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feedback&lt;/strong&gt; is very welcome.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>tooling</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
