<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
  <title>jemai</title>
  <link>https://jemai.cc/en/</link>
  <description>A technical blog about Workers, inference, and small-team engineering. Posts are written in one language and published in Chinese, English, Japanese, Spanish, and Arabic.</description>
  <language>en</language>
  <lastBuildDate>Sat, 17 Oct 2026 16:34:37 GMT</lastBuildDate>
  <atom:link href="https://jemai.cc/en/rss.xml" rel="self" type="application/rss+xml"/>
  <item>
    <title>Zero Tech Skills, Zero Cost: Build Your Own Top-Level-Domain Blog in 30 Minutes (A Step-by-Step CF CMS Guide)</title>
    <link>https://jemai.cc/en/posts/zero-cost-blog-cf-cms</link>
    <guid isPermaLink="true">https://jemai.cc/en/posts/zero-cost-blog-cf-cms</guid>
    <pubDate>Sat, 17 Oct 2026 11:55:13 GMT</pubDate>
    <description>Can't code and don't want to rent a server? With open-source CF CMS and Cloudflare's free tier, you pay nothing but the domain. One command deploys a multilingual blog on your own domain that an AI agent can run. Full steps and an error cheat sheet included.</description>
    <content:encoded><![CDATA[<p>Ever had one of those moments?</p>
<p>You&#39;ve been posting on a platform for two years, and then <strong>one throttling, one account ban, and your content and followers are just gone</strong>.</p>
<p>So you decide to build a blog of your own. You search for a tutorial: buy a server, set up the environment, configure Nginx, add HTTPS, fend off attacks every day… and you give up before writing a single word.</p>
<p>This post is for you.</p>
<p>I&#39;ll walk you through building an <strong>independent blog on your own top-level domain</strong> on Cloudflare, using the open-source project <strong>CF CMS</strong>. The site you&#39;re reading right now was built exactly this way.</p>
<p>Let&#39;s be upfront:</p>
<ul>
<li><strong>Zero tech skills</strong>: no coding required. The whole thing is <strong>copy, paste, Enter</strong>. Too lazy even for that? Hand the commands to an AI agent and let it run them.</li>
<li><strong>Zero cost</strong>: <strong>apart from the domain itself</strong> (cheap extensions run a few dollars a year), the site, database, image storage, and automatic translation all run on Cloudflare&#39;s free tier. <strong>No server to rent.</strong></li>
</ul>
<p>Ready? Let&#39;s go.</p>
<h2 id="the-bottom-line-what-you-get">The bottom line: what you get</h2>
<ul>
<li>✅ A top-level domain that&#39;s yours, like <code>yourdomain.com</code></li>
<li>✅ Automatic HTTPS + a global CDN, as fast as the big sites</li>
<li>✅ <strong>Five languages: Chinese, English, Japanese, Spanish, and Arabic.</strong> Write once, translate on save</li>
<li>✅ 8 prefab templates + one-click logo, favicon, and theme color</li>
<li>✅ An admin hidden behind a random secret path; plain <code>/admin</code> returns 404</li>
<li>✅ A built-in MCP endpoint, so <strong>an AI agent can publish posts and change settings for you</strong></li>
</ul>
<p>You only need three things:</p>
<ul>
<li><input disabled="" type="checkbox"> A domain (I&#39;ll show you how to get one)</li>
<li><input disabled="" type="checkbox"> A free Cloudflare account</li>
<li><input disabled="" type="checkbox"> A computer with a terminal (macOS / Linux; on Windows, WSL is recommended)</li>
</ul>
<h2 id="one-diagram-where-does-it-actually-run">One diagram: where does it actually run?</h2>
<p><img src="/media/images/2026/10/a6f004bc-9cee-4ad5-ad9e-e1ba0ae6e689.png" alt="CF CMS architecture: visitors reach yourdomain.com through Cloudflare DNS and CDN, then Pages Functions, backed by the D1 database, R2 image storage, and Workers AI translation" loading="lazy" decoding="async"></p>
<p>In short:</p>
<ul>
<li><strong>Pages</strong>: the site itself, rendering every page</li>
<li><strong>D1</strong>: the database for posts, tags, and settings</li>
<li><strong>R2</strong>: image storage</li>
<li><strong>Workers AI</strong>: translates your post into the other four languages when you save</li>
</ul>
<p><strong>There&#39;s no server for you to maintain.</strong> Patches, scaling, DDoS protection: that&#39;s Cloudflare&#39;s job.</p>
<h2 id="the-whole-process-in-one-picture-8-steps">The whole process in one picture: 8 steps</h2>
<p><img src="/media/images/2026/10/8c5ccb23-d13f-4d5f-8ccf-822fa287c2ff.png" alt="The 8 steps from zero to live: buy a domain, connect Cloudflare, create an API token, enable R2, get the code, deploy with one command, open the hidden admin, write and operate" loading="lazy" decoding="async"></p>
<p>If all goes well, <strong>30–60 minutes</strong>. The only step that actually takes brainpower is step 1: <strong>picking a good name</strong>.</p>
<h2 id="step-1-buy-a-domain">Step 1: Buy a domain</h2>
<p>A domain is your street address on the internet, and it&#39;s <strong>the only thing in this setup that costs money</strong>.</p>
<p>Any reputable registrar will do, for example Spaceship, Cloudflare Registrar, or Namecheap. <strong>Pick one with transparent pricing, fair renewals, and WHOIS privacy.</strong></p>
<p>Three quick tips for choosing a domain:</p>
<ol>
<li><strong>Short, memorable, easy to spell.</strong> If you can say it out loud to a friend, it&#39;s a good domain.</li>
<li><strong>Check the renewal price, not just the first year.</strong> Some extensions are dirt cheap in year one and cost several times more in year two.</li>
<li>Don&#39;t obsess over <code>.com</code>: <code>.cc</code>, <code>.dev</code>, and <code>.me</code> all work great for a personal blog.</li>
</ol>
<h2 id="step-2-connect-your-domain-to-cloudflare">Step 2: Connect your domain to Cloudflare</h2>
<ol>
<li>Go to the Cloudflare website and sign up for a <strong>free account</strong>.</li>
<li>Click <strong>Add a site</strong>, enter your domain, and choose the <strong>Free</strong> plan.</li>
<li>Cloudflare gives you <strong>two nameserver</strong> addresses.</li>
<li>Go back to your registrar&#39;s dashboard and replace the nameservers with those two.</li>
<li>Wait a few minutes to a few hours. Once Cloudflare shows <strong>Active</strong>, you&#39;re done.</li>
</ol>
<p><strong>Key point: the domain must be in the same Cloudflare account you&#39;ll deploy with.</strong> The setup script uses your API token to find it.</p>
<h2 id="step-3-create-an-api-token-just-copy-this">Step 3: Create an API token (just copy this)</h2>
<p>Path: profile icon in the top-right of Cloudflare → <strong>My Profile → API Tokens → Create Token → Create Custom Token</strong>.</p>
<p>Tick each permission shown below:</p>
<p><img src="/media/images/2026/10/0a5a224c-c80d-4c9c-a7bc-34c19abdc3f9.png" alt="Cloudflare API token permission checklist: account-level Cloudflare Pages Edit, D1 Edit, Workers R2 Storage Edit, Account Settings Read; zone-level Zone Read, DNS Edit" loading="lazy" decoding="async"></p>
<ul>
<li>Account · Cloudflare Pages · <strong>Edit</strong></li>
<li>Account · D1 · <strong>Edit</strong></li>
<li>Account · Workers R2 Storage · <strong>Edit</strong></li>
<li>Account · Account Settings · <strong>Read</strong></li>
<li>Zone · Zone · <strong>Read</strong> (when you use your own domain)</li>
<li>Zone · DNS · <strong>Edit</strong> (when you use your own domain)</li>
</ul>
<p>Two reminders:</p>
<ul>
<li><strong>Workers AI needs no extra permission.</strong> At runtime the site uses the Pages AI binding.</li>
<li><strong>This token is for the setup script only. Don&#39;t give it to your writing agent.</strong> It&#39;s shown only once, so save it right away.</li>
</ul>
<h2 id="step-4-enable-r2">Step 4: Enable R2</h2>
<p>R2 is where your images live, and it <strong>must be enabled by hand in the dashboard first</strong>. The script can&#39;t click that button for you.</p>
<p>Open <strong>R2</strong> in the left sidebar of the Cloudflare dashboard and enable it.</p>
<p>⚠️ Honest note: <strong>some accounts must add a card or PayPal before enabling R2</strong>. Don&#39;t panic. Adding one doesn&#39;t mean you&#39;ll be charged: <strong>usage within the free tier (10 GB of storage) is free</strong>. A personal blog&#39;s images will hardly ever use it up.</p>
<p>This is exactly where my first deployment got stuck: the script stopped halfway with <code>10042</code> because R2 wasn&#39;t enabled. I enabled it, reran, and it went straight through.</p>
<h2 id="step-5-get-the-code">Step 5: Get the code</h2>
<p>CF CMS is open source, and the code is on GitHub: <code>github.com/jemkuo/cf-cms</code></p>
<p>Pick one:</p>
<ul>
<li><strong>Recommended</strong>: click <strong>Fork</strong> on GitHub to copy it into your own account, which makes future changes easy.</li>
<li>Or clone it directly:</li>
</ul>
<pre class="code"><code class="language-bash">git clone https://github.com/jemkuo/cf-cms.git
cd cf-cms</code></pre>
<p>You&#39;ll need <strong>Node.js 20 or newer</strong> installed (download the installer from the official site and click through).</p>
<h2 id="step-6-deploy-with-one-command">Step 6: Deploy with one command ⭐</h2>
<p>This is the heart of the whole post. First, put the token from step 3 into an environment variable (<strong>it lives only in your terminal, and the script never prints it</strong>):</p>
<pre class="code"><code class="language-bash">export CLOUDFLARE_API_TOKEN=paste-your-token</code></pre>
<p>Then, <strong>one command</strong>:</p>
<pre class="code"><code class="language-bash">npm install &amp;&amp; ./scripts/setup.sh --project myblog --domain yourdomain.com</code></pre>
<p>Replace <code>myblog</code> with the project name you want and <code>yourdomain.com</code> with your domain.</p>
<p>Nervous? Add <code>--dry-run</code> for a rehearsal that <strong>only checks and creates nothing</strong>:</p>
<pre class="code"><code class="language-bash">./scripts/setup.sh --project myblog --domain yourdomain.com --dry-run</code></pre>
<h3 id="it-gives-your-setup-a-quot-health-check-quot-first">It gives your setup a &quot;health check&quot; first</h3>
<p>Before touching anything, the script prints a <strong>preflight checklist</strong>: token, account, Pages, D1, R2, and domain, each ticked off one by one.</p>
<p><img src="/media/images/2026/10/9ca9725b-50b7-4b8b-b3e4-ed7e2a71ed4c.png" alt="Terminal mock of the setup.sh preflight checklist: the first run fails with error 10042 because R2 is not enabled; after enabling R2 the rerun passes and flags existing DNS records on the domain" loading="lazy" decoding="async"></p>
<ul>
<li><strong>All ✓</strong>: it creates the D1 database, R2 bucket, Pages project, and secrets, then publishes the site.</li>
<li><strong>Any ✗</strong>: <strong>it creates nothing at all.</strong> It tells you which step failed, where to fix it in the dashboard, and which command to rerun, in both Chinese and English.</li>
<li><strong>No hunting for your account id</strong>: when the token can see only one account, the script fills it in for you.</li>
</ul>
<h3 id="failed-halfway-just-rerun">Failed halfway? Just rerun</h3>
<p>Progress is stored locally in <code>.cf-cms-setup.json</code>. <strong>A rerun picks up where it left off.</strong> The D1, R2, and Pages resources already created are reused, never duplicated, and your posts are never wiped.</p>
<h3 id="old-records-on-your-domain-it-asks-first">Old records on your domain? It asks first</h3>
<p>If your domain used to point at another server, the script <strong>lists the old A / AAAA / CNAME records first and leaves them alone by default</strong>.</p>
<p>Once you&#39;re sure they can go, rerun with <code>--replace-dns</code> (or answer <code>y</code> at the prompt), and it swaps them for a CNAME pointing at Pages:</p>
<p><img src="/media/images/2026/10/311e3ccb-67f2-44e3-bdcf-b47ddc7c22fb.png" alt="DNS records before and after: two A records pointing at an old server are replaced with an orange-cloud CNAME to yourproject.pages.dev, while the TXT verification record is kept" loading="lazy" decoding="async"></p>
<p><strong>Heads-up</strong>: after the swap, whatever the old server hosted on this domain is no longer reachable. TXT and other records are kept.</p>
<h3 id="when-it-finishes">When it finishes</h3>
<p>The last thing the terminal prints is the <strong>Admin URL</strong>, like this:</p>
<pre class="code"><code class="language-text">Admin:      https://yourdomain.com/&lt;ADMIN_SAFEPATH&gt;/admin/</code></pre>
<p>The admin password is <strong>randomly generated</strong> and written to <code>.dev.vars</code> in the project folder. <strong>It&#39;s never shown on screen.</strong></p>
<p>👉 <strong>Keep the Admin URL and <code>.dev.vars</code> somewhere safe.</strong> Together they&#39;re the keys to your blog.</p>
<p>If the domain status shows <code>pending</code>, relax. Certificate issuance and DNS propagation usually take a few minutes.</p>
<h2 id="step-7-open-the-hidden-admin-and-make-it-yours">Step 7: Open the hidden admin and make it yours</h2>
<p>Open that Admin URL and enter the password:</p>
<p><img src="/media/images/2026/10/27918977-8d29-4389-831d-ba5b8c5c96c2.png" alt="Real screenshot of the hidden CF CMS admin login page at yourdomain.com/&lt;ADMIN_SAFEPATH&gt;/admin/" loading="lazy" decoding="async"></p>
<p><strong>Why hide it?</strong> Most scanners hammer <code>/admin</code> and <code>/wp-login</code>. The CF CMS admin sits behind a random path, so plain <code>/admin</code> returns 404 and there isn&#39;t even a login form to see.</p>
<p>Once inside, do two things first:</p>
<ol>
<li><strong>Choose a template</strong>: personal tech blog (Dispatch, Signal), lifestyle (Atelier, Hearth), business homepage (Practice, Civic), tech company (Launch, Orbit), plus the originals Paper, Ink, and Terminal.</li>
<li><strong>Generate a visual kit in one click</strong>: hit <strong>Generate visual kit</strong> to create a <strong>logo, favicon, and theme color</strong> that match your template. Don&#39;t like it? Generate again, or swap in your own artwork.</li>
</ol>
<h2 id="step-8-write-and-get-five-languages-automatically">Step 8: Write, and get five languages automatically</h2>
<p>A freshly deployed site comes with a few sample posts so you can see how it looks:</p>
<p><img src="/media/images/2026/10/11177172-d757-4c54-b968-62443f0e7e0a.png" alt="Real screenshot of the blog homepage after deploying CF CMS, default Paper template, with a switcher for Chinese, English, Japanese, Spanish, and Arabic at the top" loading="lazy" decoding="async"></p>
<p>Delete them once you&#39;ve had a look, and write your own:</p>
<ul>
<li>Write in <strong>Markdown</strong>: headings, lists, code blocks, and images all work</li>
<li>Upload images directly; they&#39;re stored in R2</li>
<li><strong>Tick translate when you save</strong>, and Workers AI generates English, Japanese, Spanish, and Arabic versions (Arabic is laid out right-to-left automatically)</li>
<li>Set SEO per post: keywords, canonical URL, and Open Graph share image</li>
</ul>
<p><strong>Write once, and readers in five languages can find you.</strong> Plenty of big platforms can&#39;t offer that.</p>
<h2 id="day-to-day-let-an-ai-agent-be-your-editor">Day-to-day: let an AI agent be your editor</h2>
<p>Going live is just the beginning. The real time-saver is that <strong>day-to-day operations can almost all be handed to AI</strong>.</p>
<h3 id="1-connect-an-ai-agent-with-one-sentence">1. Connect an AI agent with one sentence</h3>
<p>CF CMS has a built-in MCP endpoint (<code>/mcp</code> on your domain). In Cursor, Claude, or any MCP-capable client, <strong>paste the ready-made &quot;one-sentence description&quot; from the project README</strong>, add the <code>ADMIN_API_TOKEN</code> from <code>.dev.vars</code>, and your agent can:</p>
<ul>
<li>List, create, edit, publish, and delete posts</li>
<li>Manage tags, channels, templates, and the logo</li>
<li>Upload images</li>
</ul>
<p>Once connected, you can simply say:</p>
<blockquote>
<p>&quot;Create a Chinese draft with the slug edge-notes in the notes channel, and translate it into the other languages.&quot;</p>
</blockquote>
<p><strong>Security by design</strong>: <code>ADMIN_API_TOKEN</code> can only manage content. <strong>It can&#39;t deploy, and it can&#39;t touch your Cloudflare account.</strong> That&#39;s also why the Cloudflare token from step 3 should never go to the agent.</p>
<h3 id="2-rotate-secrets-regularly">2. Rotate secrets regularly</h3>
<p>Worried a password or token has leaked? Swap it with one command:</p>
<pre class="code"><code class="language-bash">npx wrangler pages secret put ADMIN_PASSWORD --project-name myblog</code></pre>
<p>Same for <code>ADMIN_API_TOKEN</code> and <code>SESSION_SECRET</code>. Once deployment is done, you can also delete or rotate the Cloudflare API token from step 3 and create a new one next time you deploy.</p>
<h3 id="3-know-your-free-tier-limits">3. Know your free-tier limits</h3>
<table>
<thead>
<tr>
<th>Resource</th>
<th>What it does for the blog</th>
<th>Typical free allotment</th>
</tr>
</thead>
<tbody><tr>
<td>Pages</td>
<td>Site and Functions</td>
<td>500 builds/month, 100,000 Function requests/day</td>
</tr>
<tr>
<td>D1</td>
<td>Posts, settings</td>
<td>5 GB, daily row read/write limits</td>
</tr>
<tr>
<td>R2</td>
<td>Images</td>
<td>10 GB storage, limited read/write operations</td>
</tr>
<tr>
<td>Workers AI</td>
<td>Translation on save</td>
<td>10,000 Neurons/day</td>
</tr>
</tbody></table>
<p>Check the Cloudflare dashboard for current numbers. <strong>A personal blog&#39;s traffic will basically never hit the ceiling.</strong> Translation runs only when you save, not on every visit, and the Workers AI quota resets daily.</p>
<h3 id="4-common-errors-at-a-glance">4. Common errors at a glance</h3>
<table>
<thead>
<tr>
<th>What you see</th>
<th>What to do</th>
</tr>
</thead>
<tbody><tr>
<td><code>✗ R2 is not enabled</code>, error <strong>10042</strong></td>
<td>Enable R2 in the dashboard (you may need to add a payment method first), then rerun</td>
</tr>
<tr>
<td>Token rejected (<strong>10000</strong>) or missing permission (<strong>9109</strong>, HTTP 403)</td>
<td>Add the permissions from the step 3 checklist, <code>export</code> the token again, and rerun</td>
</tr>
<tr>
<td>Domain stuck on <strong>pending</strong></td>
<td>Review the old A / AAAA / CNAME records the script lists, then rerun with <code>--replace-dns</code> once you&#39;re sure; the certificate still takes a few minutes after DNS is right</td>
</tr>
<tr>
<td>Pages project name taken</td>
<td>Pick another <code>--project</code> name</td>
</tr>
<tr>
<td>D1, R2, or Pages already exists</td>
<td>Just rerun; resources with the same name are reused</td>
</tr>
<tr>
<td>Network error</td>
<td>Check your connection and rerun; finished steps are skipped</td>
</tr>
</tbody></table>
<p><strong>Remember one rule: when something fails, read the message, fix it, rerun.</strong> The script tells you where to fix it and what to run.</p>
<h2 id="about-quot-zero-tech-skills-quot-the-honest-version">About &quot;zero tech skills&quot;: the honest version</h2>
<p>You will open a terminal and type a few commands in this process.</p>
<p>But you <strong>don&#39;t need to understand</strong> what they do. All you have to do is:</p>
<ol>
<li>Copy the command</li>
<li>Paste it into the terminal</li>
<li>Press Enter and look for any ✗</li>
</ol>
<p>The even lazier option: <strong>give this post and the README to your AI agent and let it run everything.</strong> When it gets stuck, it either needs you to click something in the dashboard (like enabling R2) or confirm something (like replacing DNS). Just follow the prompts.</p>
<h2 id="let-39-s-do-the-math">Let&#39;s do the math</h2>
<table>
<thead>
<tr>
<th>Item</th>
<th>Cost</th>
</tr>
</thead>
<tbody><tr>
<td>Domain</td>
<td><strong>From a few dollars a year</strong> (depends on the extension)</td>
</tr>
<tr>
<td>Server</td>
<td>0 (Cloudflare Pages)</td>
</tr>
<tr>
<td>Database</td>
<td>0 (D1 free tier)</td>
</tr>
<tr>
<td>Image storage</td>
<td>0 (R2 free tier)</td>
</tr>
<tr>
<td>Five-language translation</td>
<td>0 (Workers AI free tier)</td>
</tr>
<tr>
<td>HTTPS certificate + CDN</td>
<td>0</td>
</tr>
<tr>
<td>CMS software</td>
<td>0 (open source)</td>
</tr>
</tbody></table>
<p><strong>Apart from the domain, you don&#39;t spend a cent.</strong></p>
<h2 id="final-words">Final words</h2>
<p>Platforms give you reach, but the turf always belongs to the platform.</p>
<p><strong>An independent blog is a digital asset that&#39;s truly yours</strong>: your domain, your content, and readers who can find you directly.</p>
<p>Take the first step today: <strong>pick a domain you love and follow this guide from start to finish.</strong></p>
<p>Hit a snag while building? Leave a comment. If you find the project useful, a ⭐ for CF CMS on GitHub is always appreciated.</p>
<p>Next up: <strong>how to let an AI agent run this blog for you every day</strong>.</p>
]]></content:encoded>
    <category>CF CMS</category>
    <category>Cloudflare</category>
    <category>Indie blog</category>
    <category>Site building</category>
  </item>
</channel>
</rss>
