<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
  <title>Yangworks</title>
  <link>https://yangworks.dev/</link>
  <atom:link href="https://yangworks.dev/feed.xml" rel="self" type="application/rss+xml"/>
  <description>Hands-on deployment guides, field notes, reviews, and pricing comparisons for freelance developers, indie makers, and small technical teams who run their own infrastructure.</description>
  <language>en</language>
  <lastBuildDate>Wed, 12 Aug 2026 00:00:00 GMT</lastBuildDate>
  <item>
    <title>Azure Free Account VMs (2026): 750 Hours Each of Three Sizes, and a Twelve-Month Cliff</title>
    <link>https://yangworks.dev/guides/azure-free-account-vm</link>
    <guid isPermaLink="true">https://yangworks.dev/guides/azure-free-account-vm</guid>
    <pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate>
    <description>The allowance is per size, not pooled: three free VMs for a year, which almost nobody uses. Then month thirteen arrives and nothing visibly changes except the bill.</description>
    <content:encoded><![CDATA[<p>Azure's free account is the odd one out among the big-three free compute offers, and the difference is not a detail. <strong>Azure has no always-free virtual machine.</strong> Oracle and Google both include a VM that runs at no cost indefinitely. Azure's free VM hours are a twelve-month introductory benefit, and on month thirteen the same machine starts billing at pay-as-you-go rates.</p>

<p>That single fact should determine whether you read further. If you want a box that is still free in two years, this is the wrong article — read the Oracle or Google guides instead. If you want a generous first year, Azure's offer is the most generous of the three for that year, and the rest of this article is about using it deliberately.</p>

<p><strong>What this article is:</strong> a reading of Microsoft's published free-account terms and VM size documentation, checked on 12 August 2026, with the credit-model arithmetic worked out. <strong>What it is not:</strong> a review or benchmark. Yangworks does not run workloads on Azure and measured nothing here. Every specification is quoted from Microsoft Learn; everything derived from those numbers is shown as arithmetic; everything else is labelled judgment.</p>

<h2>What you actually get</h2>

<p>Microsoft's free account page lists the virtual machine benefit as <q>750 hours each of B1s, B2pts v2 (Arm-based), and B2ats v2 (AMD-based) burstable VMs</q>, under the product heading <q>Azure Virtual Machines for Linux or Windows</q>, in the twelve-month category rather than the always-free one. Alongside it: a <q>$200 credit</q> to spend <q>on Azure services within 30 days</q>.</p>

<p>Read <q>750 hours each</q> carefully, because it is the most valuable and least understood phrase in the offer. A calendar month has at most 744 hours. So 750 hours funds one instance of that size running continuously, with margin — and the allowance is per size, not pooled. Three sizes, 750 hours each:</p>

<pre><code>B1s        750 h/month   x86    ) all three, concurrently,
B2pts_v2   750 h/month   Arm    ) for twelve months,
B2ats_v2   750 h/month   AMD    ) at no VM-hour cost</code></pre>

<p><em>Author judgment:</em> this is the strongest thing about the offer and almost nobody uses it. Most people spin up one B1s and leave the other two allowances untouched. If you want a year of running a small three-node setup — a service, a database host, and a build or staging box — or you want to test the same workload on x86, AMD, and Arm side by side, the allowance already covers it.</p>

<h2>The sizes, precisely</h2>

<p>These are the documented figures, and the memory column is the one that matters.</p>

<pre><code>Size         vCPU   Memory   Base CPU   Processor
B2pts_v2       2     1 GiB      20%     Ampere Altra (Arm64, 3.0 GHz)
B2ats_v2       2     1 GiB      20%     AMD EPYC 7763v (Milan)
B1s            1     1 GiB      10%     Intel (previous-generation Bv1)</code></pre>

<p>Note what the "2 vCPU" sizes are not. <code>B2pts_v2</code> and <code>B2ats_v2</code> have two vCPUs and <strong>one gibibyte</strong> of memory — they are the "t" (tiny-memory) members of their series. If you were planning around "2 core, 2 GB," Azure's free tier does not contain that machine. The next size up, <code>B2pls_v2</code> at 2 vCPU and 4 GiB, is not in the free offer.</p>

<p>And note what B1s is. Microsoft's documentation places it in the Bv1 series, which carries a standing notice: <q>These virtual machine sizes are a previous generation series. While older VM sizes are supported until further notice, we recommended using newer generations for improved performance and security.</q> Its documented base CPU performance is 10%, against 20% for the v2 sizes, and it banks 6 credits an hour to a 144-credit ceiling, against 24 an hour to 576.</p>

<p><em>Author judgment:</em> B1s is the size everyone picks, because it is the one they have heard of, and it is the weakest of the three. Same memory, half the baseline CPU, a quarter of the credit ceiling, one vCPU instead of two, and a previous-generation platform. Unless you specifically need an Intel x86 host or Generation 1 VM support, start with <code>B2ats_v2</code> or <code>B2pts_v2</code> and treat B1s as the third box rather than the first.</p>

<h2>The burst credit model, and what 20% really means</h2>

<p>Every size in the free offer is burstable, which means the CPU figure in the table is a ceiling you visit, not a level you occupy. Microsoft describes the mechanism plainly:</p>

<blockquote>
<p>The virtual machine accumulates CPU credits when a workload is operating below the base CPU performance threshold and, uses credits when running above the base CPU performance threshold until all of its credits are consumed. Upon consuming all the CPU credits, a Bsv2-series virtual machine is throttled back to its base CPU performance until it accumulates the credits to CPU burst again.</p>
</blockquote>

<p>The documented figures for the 2-vCPU free sizes: base CPU performance 20%, initial credits 60, credits banked per hour 24, maximum banked credits 576.</p>

<p>Those numbers are internally consistent and worth converting into time, because the vendor never does. A credit is one vCPU-minute at full speed. Base performance of 20% on a 2-vCPU VM is 0.4 vCPU continuously, which is 0.4 × 60 = 24 vCPU-minutes banked per idle hour — exactly the documented banking rate. The cap of 576 credits is therefore 576 ÷ 24 = 24 hours of accumulation.</p>

<pre><code>Sustained, credits exhausted   0.4 vCPU-equivalent
Full bank (576 credits)        576 vCPU-minutes
  spent on both vCPUs at 100%  288 minutes ≈ 4.8 hours
Time to refill from empty      24 hours of idling</code></pre>

<p><em>Author judgment:</em> this is a good fit for spiky work and a bad fit for steady work. A build server, a CI runner, or a service with a daily peak banks credits overnight and spends them when needed. Anything that holds CPU above 20% around the clock — a busy database, a transcoder, a chatty gateway under constant load — drains the bank within hours and then runs permanently at 0.4 vCPU, which is slower than the spec sheet suggests and confusing if you did not expect it.</p>

<h2>The eligibility rules that disqualify people</h2>

<p>Microsoft's footnote is restrictive in three specific ways. The twelve-month benefit is <q>available only to new customers who have not previously had an Azure account or received 12 months of free services</q> — a prior account, including a long-abandoned one, disqualifies you permanently. It is not offered to customers signing up directly for pay-as-you-go <q>in China and India</q>. And there is a step that catches people out: you must move to pay-as-you-go <q>within 30 days to continue receiving 12 months free services</q>.</p>

<p>That last one deserves emphasis. The twelve months of free services are not automatic for the full year just because you signed up. If you let the initial 30-day credit window lapse without converting to pay-as-you-go, you lose the remaining free-services entitlement. Converting is what keeps the benefit alive; it does not by itself start charging you for things inside the free allowances.</p>

<h2>What happens at month thirteen</h2>

<p>Nothing dramatic, which is the problem. Microsoft's FAQ states that after the free-for-12-months window ends, resources keep running but <q>you'll be billed for them at pay-as-you-go rates.</q></p>

<p>There is no interruption, no email at the moment of transition that you will necessarily act on, and no change in the console. A VM you forgot about simply becomes a line item.</p>

<pre><code>Do this on day one, not day 360:

1. Set a budget alert at a low threshold (e.g. $1) with
   an email action. This is the safety net.
2. Put the account creation date and "+12 months" in
   your calendar, with the resource group name in the note.
3. Tag every resource you create under the free benefit,
   so a year later you can tell which ones were free.</code></pre>

<h2>Storage is billed separately — verify your allowance</h2>

<p>Microsoft's own size documentation says it directly, for every B-series page: <q>Disk storage is billed separately from virtual machines.</q> The 750 VM hours cover compute, not the managed disk the VM boots from.</p>

<p>The free account does include a managed-disk allowance among its twelve-month services, but I did not verify the current figure to a standard I am willing to publish, and it has changed over time. <strong>Check the current free-services list for the managed disk line before you provision</strong>, and size your OS disk to fit it. This is the most likely source of a small unexpected charge in month one, exactly as the disk-type default is on Google Cloud.</p>

<h2>What you can realistically run</h2>

<p>Reasoning from documented specifications, not measurement. I have not run these workloads on these instances.</p>

<p>One gibibyte is the binding constraint on all three free sizes, and it constrains what you co-locate far more than what you run. <a href="https://github.com/QuantumNous/new-api">New API</a>, a commonly self-hosted LLM gateway, documents its deployment requirements as SQLite with a mounted <code>/data</code> directory or a remote <q>MySQL ≥ 5.7.8 or PostgreSQL ≥ 9.6</q>, under Docker or Docker Compose, and <q>64-bit only (amd64 / arm64); 32-bit systems are not supported</q>. It publishes no minimum CPU or RAM figure.</p>

<p>Both Arm and AMD free sizes satisfy the architecture requirement, which is worth noting — the Arm allowance is not a second-class option for containerised Go services, and <code>arm64</code> images are explicitly supported.</p>

<pre><code>Fits well      one compiled service + SQLite, per box
               reverse proxy, tunnel endpoint, bastion
               CI/build runner (bursty: ideal credit fit)
               staging environment for a year

Use the        service on one box, MySQL or PostgreSQL on
three boxes    another, build/staging on the third — rather
               than stacking everything onto one 1 GiB VM

Poor fit       steady CPU above 20% (credit exhaustion)
               anything wanting 2 GB+ RAM on one box
               a permanent home for something you care about
               (it stops being free in twelve months)</code></pre>

<p>A caveat on one popular use case: routing a subscription-based AI CLI through a self-hosted proxy may conflict with that provider's terms of service. That is a licensing question, not a capacity question, and free infrastructure does not change the answer.</p>

<h2>How to apply</h2>

<pre><code>1. Sign up with an account that has never had Azure before.
   A card is required for verification.
2. Note the 30-day credit window; convert to pay-as-you-go
   inside it to keep the 12-month benefits.
3. Create VMs using B1s, B2pts_v2, or B2ats_v2 by exact
   size name. Neighbouring sizes are not free.
4. Size the OS disk to the free managed-disk allowance.
5. Set a budget alert immediately.
6. Calendar the 12-month expiry with resource names.</code></pre>

<h2>Who should not use this</h2>

<p>Anyone who has held an Azure account before, since the benefit is one per customer, ever. Anyone in China or India signing up directly for pay-as-you-go. Anyone who needs the machine to still be free in year two — the twelve-month cliff makes this a poor foundation for a long-lived personal service, and a genuinely bad one for client work you will not be actively managing when the year is up. And anyone whose workload holds CPU steadily: the burst credit model is a real constraint, and 0.4 vCPU sustained is what you get after the bank empties.</p>

<p><em>Author judgment:</em> the honest use of Azure's free account is as a <strong>time-boxed</strong> resource. A year of three free VMs is a lot of runway for learning, for staging, for a build farm, or for a project you intend to migrate or shut down. Treating it as permanent infrastructure is the mistake the offer invites and does not warn you about.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Free VPS Compared (2026): Oracle Cloud vs Google Cloud vs Azure Free Tiers</title>
    <link>https://yangworks.dev/compare/free-tier-vps-google-oracle-azure</link>
    <guid isPermaLink="true">https://yangworks.dev/compare/free-tier-vps-google-oracle-azure</guid>
    <pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate>
    <description>Two are free forever, one expires at twelve months, and one of them bills you about $3.65 a month for the IP address nobody mentions.</description>
    <content:encoded><![CDATA[<p>Three of the largest cloud providers will give you a virtual machine for nothing. They are not comparable offers, and the axis that separates them is not performance — it is whether the machine is still free next year, and how many bytes it is allowed to send.</p>

<p><strong>What this article is:</strong> a side-by-side reading of the published free-tier terms for Oracle Cloud, Google Cloud, and Microsoft Azure, checked on 12 August 2026, with the trade-offs made explicit. <strong>What it is not:</strong> a benchmark or a review. Yangworks runs on Cloudflare and operates none of these accounts. No throughput, latency, uptime, or price-performance measurement was made, and none is claimed. Where a figure could not be verified against vendor documentation directly, it is marked as such rather than filled in.</p>

<h2>The short answer</h2>

<pre><code>Want a free box that stays free           -> Oracle Cloud
Want free compute for jobs, not serving   -> Google Cloud
Want a generous first year, then migrate  -> Azure

Want a free box to serve public traffic   -> Oracle, or none
Want free Arm with real memory            -> Oracle (2 OCPU / 12 GB)
Want the bill to be exactly zero          -> NOT Google (see below)</code></pre>

<h2>The comparison</h2>

<pre><code>                    ORACLE              GOOGLE            AZURE
                    Always Free         Always Free       Free account

Permanent?          yes, life of        yes               NO - 12 months,
                    the account                           then billed

x86 machine         2 x E2.1.Micro      1 x e2-micro      B1s   1 vCPU 1 GiB
                    1/8 OCPU, 1 GB      2 shared vCPU     B2ats_v2
                                        (25%), 1 GB       2 vCPU 1 GiB

Arm machine         A1.Flex pool:       none in the       B2pts_v2
                    2 OCPU, 12 GB       free tier         2 vCPU, 1 GiB

Storage             200 GB block        30 GB-months      billed separately
                    (boot + block)      pd-standard       - verify allowance

Egress              10 TB / month       1 GB / month      not verified here

Region              home region only,   us-west1          broad
                    chosen once,        us-central1
                    permanent           us-east1 only

External IPv4       included            $0.005/hr after   included in the
                                        1 free hr/month   free VM hours
                                        = ~$3.65/month

Signature           idle instances      the IP charge,    the 12-month
gotcha              may be reclaimed    and the wrong     cliff; burst
                                        disk type or      credit exhaustion
                                        region</code></pre>

<h2>Permanence is the decisive axis</h2>

<p>Oracle's free resources are <q>free of charge in the home region of the tenancy, for the life of the account</q>. Google's free tier likewise continues after the trial credit is spent. Azure's VM hours are listed under the twelve-month category, and Microsoft's FAQ states that afterwards resources keep running but <q>you'll be billed for them at pay-as-you-go rates.</q></p>

<p>This is not a minor asymmetry. It changes what each offer is for.</p>

<p><em>Author judgment:</em> a machine that stops being free in twelve months is not infrastructure, it is a trial with a long fuse. That makes Azure excellent for things with a natural end — learning, staging, a build farm, a project you plan to migrate — and a poor foundation for a personal service you intend to forget about. The failure mode is not a large bill; it is a small one arriving on month thirteen for something you stopped thinking about on month two.</p>

<h2>Egress decides more than CPU does</h2>

<p>The specification people compare is cores. The number that actually determines feasibility is outbound transfer, and the spread here is four orders of magnitude.</p>

<pre><code>Oracle   10 TB / month     10,000 GB
Google    1 GB / month          1 GB
                              ------
                              10,000x</code></pre>

<p>Google's allowance works out to roughly 33 MB a day. At a 200 KB page that is about 5,000 views a month; at 2 MB with images, about 500. The exact wording is <q>1 GB of outbound data transfer from North America to all region destinations (excluding China and Australia) per month</q> — measured leaving North America, with China and Australia traffic not drawing on it at all.</p>

<p><em>Author judgment:</em> this single line reclassifies the Google offer. The free e2-micro is a <strong>compute</strong> box — a scheduled job, a scraper, a webhook receiver, a bot, a monitoring agent, a build runner — not a <strong>serving</strong> box. Anyone planning to host a website on it will hit the egress ceiling long before the quarter-core CPU becomes the complaint. Oracle's 10 TB, by contrast, is more egress than most small projects will ever use, and it is the reason Oracle is the only one of the three I would consider for public serving.</p>

<h2>Memory, and the "2 core" illusion</h2>

<p>Every free x86 option here has 1 GB of memory. The core counts are more generous than the memory, and in every case they are less than they look.</p>

<p>Google's e2-micro presents two vCPUs, each entitled to 12.5% of CPU time — 25% of a core in total, with bursting. Oracle's E2.1.Micro is <q>1/8th of an OCPU with the ability to use additional CPU resources</q>. Azure's free 2-vCPU sizes have a documented base CPU performance of 20% and run on a credit model: they bank credits while below baseline and are throttled back to baseline once the bank empties.</p>

<p>Note especially that Azure's two-core free sizes, <code>B2pts_v2</code> and <code>B2ats_v2</code>, carry <strong>1 GiB</strong> of memory, not 2 GiB. They are the tiny-memory members of their series. If you have been planning around a "2 core, 2 GB" free machine, no such thing exists in any of these three offers.</p>

<p>The exception, and it is a large one: Oracle's Arm allowance. Oracle documents it as a monthly pool — <q>the first 1,500 OCPU hours and 9,000 GB hours per month</q>, which it states is <q>equivalent to 2 OCPUs and 12 GB of memory</q> for Always Free tenancies. Twelve gigabytes is not in the same category as one, and it is the only free machine among the three on which co-locating a real database is comfortable.</p>

<p>One correction worth making explicitly, because the outdated figure is everywhere: Oracle's Arm allowance is documented today as 2 OCPUs and 12 GB, not the 4 OCPUs and 24 GB that most tutorials still quote. This is the one figure in this comparison I could check against real hardware — an A1.Flex instance provisioned at the Always Free allowance reports <code>ocpus 2.0</code>, <code>memoryInGBs 12.0</code>, and an aarch64 Neoverse-N1 processor. Check the current page before designing around the larger number.</p>

<h2>The gotcha each one has</h2>

<p><strong>Oracle: idle reclamation.</strong> Oracle documents that idle Always Free instances may be reclaimed, with all of the following true across a 7-day window: 95th-percentile CPU below 20%, network utilisation below 20%, and (on A1 shapes) memory utilisation below 20%. A box doing real work is not the target; a box provisioned and forgotten is. Also: home region is permanent, outbound port 25 is blocked by default, and <q>out of host capacity</q> errors at provisioning time are a documented and common experience.</p>

<p><strong>Google: the free VM is not actually free.</strong> This is the finding that most changed my view while writing this. Google's free tier page does not mention external IP addresses; the network pricing page does, and it says <q>Both static and ephemeral IP addresses assigned to standard VM instances are offered with a free tier. This free usage is limited to one hour per month per account.</q> After that hour the rate is $0.005 per hour on a standard VM, which over a 730-hour month is $3.65 — roughly $43.80 a year for the address on a machine advertised as free. Stopping the VM does not help: <q>Google Cloud considers a static external IP address as in use if it is associated with a VM instance whether the instance is running or stopped.</q></p>

<p>On top of that, the 30 GB allowance covers <em>standard</em> persistent disk specifically, while the default boot disk type on many current images is balanced, which bills from the first gigabyte. And free-tier eligibility is limited to three US regions while e2-micro is creatable in nearly all of them.</p>

<p><em>Author judgment:</em> Oracle and Azure both include the address in the free offer. Google does not, and it is the only one of the three where following the console defaults produces a bill. That does not make the offer bad — $3.65 a month for a permanent VM is a good price — but it does mean the honest description is "very cheap," not "free," and the many articles listing all three as free VPS options are wrong about this one. You can avoid the charge by running without an external IPv4 address and reaching the box over IAP, a tunnel, or IPv6.</p>

<p><strong>Azure: the cliff and the credits.</strong> Beyond the twelve-month expiry, the burst model has teeth. On the free 2-vCPU sizes the documented figures are 20% base performance, 24 credits banked per idle hour, and a 576-credit cap. Converting: 576 credits is 24 hours of accumulation, and spending it across both vCPUs at full speed lasts about 4.8 hours. Bursty workloads fit this beautifully; anything holding CPU above 20% around the clock runs permanently at roughly 0.4 vCPU-equivalent.</p>

<h2>Eligibility, which disqualifies more people than specs do</h2>

<p>Azure's is the most restrictive: the twelve-month benefit is <q>available only to new customers who have not previously had an Azure account or received 12 months of free services</q>, is not offered to those signing up directly for pay-as-you-go <q>in China and India</q>, and requires moving to pay-as-you-go <q>within 30 days to continue receiving 12 months free services</q>. A dormant account from years ago disqualifies you permanently.</p>

<p>All three require a payment card for identity verification even when you intend to use only free resources. Oracle additionally requires choosing a home region at signup that cannot be changed later.</p>

<h2>Choosing</h2>

<p>Reasoning from documented specifications, not measurement.</p>

<pre><code>A service you want running in three years
  -> Oracle A1.Flex (2 OCPU / 12 GB). Nothing else
     is both permanent and roomy enough.

A cron job, scraper, bot, or webhook receiver
  -> Google e2-micro. Permanent, tiny, and the 1 GB
     egress ceiling is irrelevant when you consume
     bytes rather than emit them.

Anything serving public traffic for free
  -> Oracle, on the 10 TB egress allowance. Or put
     the site on a static host and skip the VM.

A year of three machines for learning or staging
  -> Azure. 750 hours each of three sizes, run
     concurrently, is the most underused part of
     any of these offers.

Free Arm with real memory
  -> Oracle. Google has no free Arm; Azure's Arm
     size has 1 GiB.</code></pre>

<p><em>Author judgment:</em> the strongest combination is not one provider. Oracle's A1 instance as the permanent home for anything that matters, plus Google's e2-micro as a free always-on job runner in a different failure domain, costs nothing and gives you two independent providers. Azure joins that picture only if you have never had an account and want a bounded year of extra capacity — and only with the expiry in your calendar on the day you sign up.</p>

<p>Whichever you choose, set a billing alert at a low threshold before you create anything. All three offers have at least one documented way to start charging you quietly, and a $1 budget alert converts every one of them from a surprise into an email.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Google Cloud Free Tier e2-micro (2026): The Free VM That Costs $3.65 a Month</title>
    <link>https://yangworks.dev/guides/google-cloud-free-tier-e2-micro</link>
    <guid isPermaLink="true">https://yangworks.dev/guides/google-cloud-free-tier-e2-micro</guid>
    <pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate>
    <description>The instance really is free forever. The IP address on it is free for one hour a month, which is how a $0 VM ends up costing about $43 a year.</description>
    <content:encoded><![CDATA[<p>Google Cloud's Always Free tier includes a genuinely permanent virtual machine — one e2-micro instance, running continuously, at no charge. It is the smallest of the three big-cloud free VM offers, and the one whose limits are easiest to breach by accident, because the binding constraint is not the machine. It is the one gigabyte of monthly egress.</p>

<p>There is also a charge most people do not expect, on the default configuration, which the free tier page does not mention at all. It is covered below.</p>

<p><strong>What this article is:</strong> a reading of published free-tier limits with the practical consequences worked out, checked against Google's own pages on 12 August 2026. <strong>What it is not:</strong> a review or benchmark. Yangworks runs on Cloudflare, not on Compute Engine, and has provisioned none of these instances. No throughput, latency, or uptime figure is measured or claimed.</p>

<h2>What the allowance actually is</h2>

<p>Quoting the Free Tier page directly:</p>

<pre><code>Instance    "1 non-preemptible e2-micro VM instance per month
             in one of the following US regions"
Regions     "Oregon: us-west1"
            "Iowa: us-central1"
            "South Carolina: us-east1"
Disk        "30 GB-months standard persistent disk"
Egress      "1 GB of outbound data transfer from North America
             to all region destinations (excluding China and
             Australia) per month"
Excluded    "GPUs and TPUs are not included in the Free Tier
             offer. You are always charged for GPUs and TPUs
             that you add to VM instances."</code></pre>

<p>Two things about that list are easy to misread.</p>

<p><strong>The instance allowance is metered in hours, not instances.</strong> Google spells this out: <q>Your Free Tier e2-micro instance limit is by time, not by instance. Each month, eligible use of all of your e2-micro instances is free until you have used a number of hours equal to the total hours in the current month. Usage calculations are combined across the supported regions.</q> So it funds one machine running continuously — or, equivalently, two machines each running half the month. Run two e2-micro instances concurrently for a full month and roughly half that usage is billable.</p>

<p><strong>Only three US regions qualify.</strong> E2 machine types are available in essentially every Google Cloud region; free-tier eligibility is not. Launch your e2-micro in Frankfurt or Singapore and it is an ordinary billable VM that looks identical in the console. This is the single most common way people are surprised by a Compute Engine bill.</p>

<h2>The machine: two vCPUs that are really a quarter of one</h2>

<p>e2-micro is a shared-core machine type with 1 GB of memory. It presents two vCPUs, each entitled to 12.5% of CPU time — 25% of a core in total — with the ability to burst above that for short periods. Bursting does not cost extra; it is part of the shared-core design rather than a credit you buy.</p>

<p><em>Author judgment:</em> treat the "2 vCPU" in the spec sheet as a scheduling detail, not as capacity. A quarter of a modern core is enough to run a small compiled service comfortably and enough to make a package install or a container build feel slow. If your mental model is "two cores," you will be disappointed; if it is "a quarter core that can briefly sprint," you will size things correctly.</p>

<h2>The disk trap: your default boot disk is probably the wrong type</h2>

<p>The 30 GB-month allowance is specifically for <strong>standard</strong> persistent disk — <code>pd-standard</code>. Balanced and SSD persistent disks bill at ordinary rates from the first gigabyte.</p>

<p>This matters because the default boot disk type for many current images is balanced, not standard. Accept the defaults in the console and you get a free VM attached to a billable disk — a small charge, but a recurring one that arrives with no warning and is genuinely confusing to diagnose, because the instance line item really is zero.</p>

<pre><code>gcloud compute instances create free-box \
  --machine-type=e2-micro \
  --zone=us-west1-b \
  --boot-disk-type=pd-standard \
  --boot-disk-size=30GB</code></pre>

<p>The <code>--boot-disk-type=pd-standard</code> flag is the one that is easy to omit and expensive to forget.</p>

<h2>One gigabyte of egress, and what it rules out</h2>

<p>This is the limit that decides whether the free tier fits your project at all. Google's wording is <q>1 GB of outbound data transfer from North America to all region destinations (excluding China and Australia) per month</q> — so the allowance is measured leaving North America, and traffic to China or Australia does not draw on it at all.</p>

<p>A gigabyte a month is roughly 33 MB a day. Some arithmetic to make that concrete:</p>

<pre><code>A 200 KB page view            ~5,000 views/month
A 2 MB page with images         ~500 views/month
Streaming API responses       depends entirely on volume
Serving any media             not viable</code></pre>

<p>Inbound transfer is free — the network pricing page states <q>No charge for inbound data transfer</q>, with the caveat that a resource processing that data may itself be billed. Traffic to a different Google Cloud service in the same region using an external or internal IP is also listed as no charge, with named exceptions including Memorystore for Redis, Filestore, GKE, and Cloud SQL.</p>

<p><em>Author judgment:</em> the 1 GB ceiling means the free e2-micro is a <strong>compute</strong> box, not a <strong>serving</strong> box. It is well suited to work that consumes bytes and emits few: a scheduled job, a scraper writing to a database, a webhook receiver, a bot, a monitoring agent, a build runner, a personal VPN endpoint used lightly. It is unsuited to hosting a website you hope people read. If you want the same machine class for public serving, look at a provider whose free egress is measured in terabytes rather than gigabytes.</p>

<h2>The part that is not free: the external IP address</h2>

<p>This is the most important thing in this article, and the free tier page does not mention it. An external IPv4 address attached to a running VM is billed, and the free allowance for it is an hour a month.</p>

<p>From Google's VPC network pricing page:</p>

<blockquote>
<p>Free Tier: Both static and ephemeral IP addresses assigned to standard VM instances are offered with a free tier. This free usage is limited to one hour per month per account.</p>
</blockquote>

<p>The rates that apply after that hour:</p>

<pre><code>Static/ephemeral IP, in use on a standard VM   $0.005 / hour
Static/ephemeral IP, in use on Spot/preemptible $0.0025 / hour
Static IP, assigned but unused                 $0.01  / hour
IP attached to a forwarding rule               no charge
External IPv6 assigned to a VM                 no charge</code></pre>

<p>Arithmetic, since the consequence is what matters:</p>

<pre><code>$0.005/hr x 730 hr        = $3.65 / month
                          ~ $43.80 / year
minus the 1 free hour     = $3.645 / month</code></pre>

<p>Note also Google's definition of "in use": <q>Google Cloud considers a static external IP address as in use if it is associated with a VM instance whether the instance is running or stopped.</q> Stopping the VM does not stop the address charge.</p>

<p><em>Author judgment:</em> this is the single most misleading thing about the offer, and it is not a trap in the sense of a mistake you make — it is the <em>default</em>. Create an e2-micro the ordinary way, in the right region, with the right disk type, and you still have a machine that costs roughly $3.65 a month. That is cheap, and it is not free, and every "free VPS forever" article that omits it is wrong. The instance line item genuinely reads zero, so the charge appears under networking where nobody thinks to look.</p>

<p>Two ways out. Run the instance with <strong>no external IPv4 address</strong> and reach it over Identity-Aware Proxy or a tunnel, giving it outbound access via Cloud NAT — but Cloud NAT is itself billed at $0.005 per hour for the addresses it uses, so this only helps if you were already going to run one, or if the instance needs no outbound internet at all. Or use <strong>IPv6</strong>, which Google states is not charged for external addresses assigned to VM instances — viable only if everything you talk to is reachable over IPv6.</p>

<p>Either way: read the networking section of your first monthly bill, not just the Compute Engine section.</p>

<h2>Free tier versus the trial credit</h2>

<p>New accounts get a free trial credit, and the Free Tier is separate from it. Free Tier usage is not charged against the trial credit; during the trial you have access to both. When the trial ends and you upgrade to a paid account, remaining credit carries over and Free Tier access continues — you are billed only for usage beyond the free limits.</p>

<p>The practical consequence: the interesting question is not "what happens when my $300 runs out," it is "what am I running that sits outside the free limits." A free-tier e2-micro in us-west1 with a pd-standard boot disk and light egress should survive the end of the trial unchanged. A balanced disk, a second instance, or a wrong region will not.</p>

<h2>What you can realistically run</h2>

<p>Reasoning from documented specifications, not measurement. I have not run these workloads on this instance type.</p>

<p>One gigabyte of memory with a quarter core is a real constraint, but a narrower one than it looks for compiled services. <a href="https://github.com/QuantumNous/new-api">New API</a>, a frequently self-hosted LLM gateway, documents its requirements as SQLite with a mounted <code>/data</code> directory or a remote <q>MySQL ≥ 5.7.8 or PostgreSQL ≥ 9.6</q>, under Docker or Docker Compose, <q>64-bit only (amd64 / arm64)</q>. It states no minimum CPU or RAM at all.</p>

<p>That silence is the point: a Go service proxying HTTP is not what exhausts 1 GB. Co-locating a MySQL server with it is. On e2-micro the realistic configuration is the service plus SQLite, with swap configured, and the database elsewhere if you need a real one.</p>

<pre><code>Comfortable    one small compiled service + SQLite
               reverse proxy / tunnel endpoint
               cron jobs, scrapers, webhook receivers
               monitoring or log-shipping agent

Tight          Docker builds (do them elsewhere, pull the image)
               anything JVM-based
               co-located MySQL or PostgreSQL

Wrong tool     serving public traffic at any volume
               (the 1 GB egress cap, not the CPU)</code></pre>

<p>One caveat worth stating on a popular use case: routing a subscription-based AI CLI through a self-hosted proxy may conflict with that provider's terms of service. That is a licensing question rather than a capacity one, and running it on free infrastructure does not change the answer.</p>

<h2>How to apply</h2>

<pre><code>1. Create a Google Cloud account; a billing account with a
   card is required even to use only free-tier resources.
2. Create the instance in us-west1, us-central1, or us-east1.
   Nowhere else qualifies.
3. Machine type e2-micro. Not e2-small, which is not free.
4. Explicitly set --boot-disk-type=pd-standard, 30 GB or less.
5. Decide the external IP question deliberately. Keeping one
   costs about $3.65/month; dropping it means reaching the box
   over IAP, a tunnel, or IPv6.
6. Set a billing budget alert at a low threshold, e.g. $1.
   This is the step that turns a silent mistake into an email.
7. After the first full month, read the billing report line by
   line — the networking section, not just Compute Engine.</code></pre>

<p>Step 6 is the one I would not skip. The free tier's failure mode is not a big bill; it is a small recurring one you do not notice for a year.</p>

<h2>Who should not use this</h2>

<p>Anyone who needs the machine outside the three eligible US regions. Anyone serving more than a trickle of public traffic — the egress cap will decide this for you. Anyone who wants a free box for a client project, given that the configuration required to keep it free is precise enough that an unrelated console change can quietly start billing. And anyone who wants Arm: the free e2-micro is x86, and Google's Arm machine types are not in the free tier.</p>

<p>Above all, anyone who needs the total to be <em>zero</em>. Between the external IP address and the boot disk default, this offer is better described as a very cheap VM than a free one. If zero is the requirement, the offer to look at is one whose free machine keeps its address.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Oracle Cloud Always Free VPS (2026): What the Documentation Actually Says</title>
    <link>https://yangworks.dev/guides/oracle-cloud-always-free-vps</link>
    <guid isPermaLink="true">https://yangworks.dev/guides/oracle-cloud-always-free-vps</guid>
    <pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate>
    <description>The figure most articles repeat — four Arm cores and 24 GB free forever — is not what Oracle documents today. The pool arithmetic gives 2 OCPUs and 12 GB.</description>
    <content:encoded><![CDATA[<p>Oracle Cloud's Always Free tier is the most generous permanent free compute offer among the large clouds, and it is also the one most often described inaccurately. The figure repeated in most blog posts and forum replies — four Arm cores and 24 GB of memory, free forever — is not what Oracle's documentation says today.</p>

<p><strong>What this article is:</strong> a reading of Oracle's official Always Free resources documentation, checked on 12 August 2026, with the limits quoted directly and the practical consequences worked out. <strong>What it is not:</strong> a review or a benchmark. Yangworks does not run an Oracle Cloud tenancy, has provisioned none of these instances, and measured no throughput, latency, or uptime. Every allowance below is quoted from Oracle's documentation; everything I infer from those numbers is labelled as judgment.</p>

<h2>The number most articles get wrong</h2>

<p>Oracle's Always Free documentation describes the Arm allowance as a monthly pool of hours, not as a fixed machine:</p>

<blockquote>
<p>All tenancies get the first 1,500 OCPU hours and 9,000 GB hours per month for free for VM instances using the VM.Standard.A1.Flex shape, which has an Arm processor. For Always Free tenancies, this is equivalent to 2 OCPUs and 12 GB of memory.</p>
</blockquote>

<p>Do the division. 1,500 OCPU hours spread across a 30-day month is 1,500 ÷ 720 ≈ 2.08 OCPUs running continuously; 9,000 GB hours is 9,000 ÷ 720 = 12.5 GB. That is where Oracle's own <q>2 OCPUs and 12 GB of memory</q> comes from, and it is half the four-core, 24 GB figure that circulates widely.</p>

<p>The documentation also states how you may divide it: <q>You can use all of the Always Free OCPUs and memory to create a single instance, or create up to two instances of 1 OCPU each (2 OCPUs total) that each use a portion of the resources.</q></p>

<p><em>Author judgment:</em> if you are planning capacity from a tutorial written a while ago, re-read the current page before you design around 24 GB. A single 2 OCPU / 12 GB Arm box is still an excellent free machine — it is simply a different machine from the one the internet remembers.</p>

<h2>The two shapes, as documented</h2>

<p>Always Free compute comes in two flavours, and you can hold both at once.</p>

<pre><code>VM.Standard.E2.1.Micro   (AMD, x86_64)
  count      up to two per tenancy
  processor  1/8th of an OCPU, "with the ability to use additional CPU resources"
  memory     1 GB each
  network    1 VNIC, 1 public IP, up to 50 Mbps to the internet

VM.Standard.A1.Flex      (Arm, Ampere)
  pool       1,500 OCPU hours + 9,000 GB hours per month
  effective  2 OCPUs and 12 GB total for Always Free tenancies
  split      one instance, or up to two of 1 OCPU each</code></pre>

<p>Oracle's own worked example of a legal combination: <q>using the default boot volume size of 47 GB, you could provision two instances using the VM.Standard.E2.1.Micro shape, and one OCI Ampere A1 Compute instance with 2 OCPUs</q>.</p>

<p>Always Free-eligible images are limited: Oracle Linux, Ubuntu, and CentOS for the AMD micro shape; Oracle Linux and Ubuntu for A1. Oracle Linux Cloud Developer is listed too, but <q>requires at least 8 GB of memory</q>, which rules it out on the 1 GB micro shape. Anything outside that list needs a paid account: <q>To provision a compute instance with an image that is not Always Free-eligible, you must have a paid account or a Free Trial account with available credits.</q></p>

<h2>Checked against a running instance</h2>

<p>Unusually for this article's subject matter, I have access to an A1.Flex instance provisioned at exactly the Always Free allowance, so the central claim above is not purely a reading of documentation. On 12 August 2026 that machine reported:</p>

<pre><code>$ curl -H 'Authorization: Bearer Oracle' \
    http://169.254.169.254/opc/v2/instance/
  shape        VM.Standard.A1.Flex
  ocpus        2.0
  memoryInGBs  12.0
  networkingBandwidthInGbps  2.0
  maxVnicAttachments         2
  region       phx

$ uname -m                  aarch64
$ nproc                     2
$ lscpu | grep 'Model name' Neoverse-N1
$ free -m  (total)          11927
$ lsblk    (sda)            200G</code></pre>

<p>Two OCPUs, 12 GB — the documented Always Free figure, not the 4 and 24 that circulate. The processor identifies as Neoverse-N1, the core design used in Ampere Altra. The <code>free -m</code> total of 11,927 MB is what 12 GB looks like after firmware reservations.</p>

<p>Two things the metadata adds that the Always Free page does not spell out. The boot volume is the full 200 GB, which confirms the entire Block Volume allowance can sit on a single instance rather than being spread across several. And networking bandwidth is 2 Gbps, scaling at 1 Gbps per OCPU — against the <q>up to 50 Mbps</q> documented for the E2.1.Micro shape. That is a factor of forty, and it is a larger practical difference between the two free shapes than the core count is.</p>

<p>What this does <em>not</em> establish: anything about billing status, provisioning success rates, capacity availability in other regions, or the reclamation policy in practice. It is one machine, in one region, at one moment. It confirms the shape figures and nothing else.</p>

<h2>Storage: 200 GB total, and the boot volume eats it</h2>

<p>The documentation states: <q>All tenancies receive a total of 200 GB of Block Volume storage, and five volume backups included in the Always Free resources. These amounts apply to both boot volumes and block volumes combined.</q></p>

<p>That total is shared, and every instance takes a bite before you store anything. Worth noting: the page gives two different minimum boot volume figures in two different sections — <q>The minimum boot volume size for each instance is 47 GB, regardless of shape</q> under Compute, and under Block Volume, <q>because the minimum boot volume size allowed for compute instances is 50 GB, launching four instances will use all your Always Free Block Volume resources.</q> Both appear as written; plan against 50 GB and you will not be caught out either way.</p>

<p>Four instances is therefore the practical ceiling regardless of CPU: 4 × 50 GB = 200 GB, and you have no storage left. Backups are capped at <q>a maximum of five Always Free volume backups at any time</q>, and they must live at home — <q>To create an Always Free block volume, the volume must be created in the home region of the tenancy. Volumes created outside of the home region incur regular block volume costs.</q></p>

<h2>Egress: 10 TB a month, which is the real headline</h2>

<p>Oracle states plainly: <q>As part of your Always Free resources, you get 10 TB per month of outbound data.</q></p>

<p><em>Author judgment:</em> this is the allowance that most distinguishes Oracle from the other two free tiers, and it is rarely the thing people compare. For anything that serves bytes — a media proxy, a file drop, a self-hosted gateway that streams responses — the egress ceiling decides feasibility long before the CPU does. Google's free tier meters egress in gigabytes per month. Oracle meters it in terabytes.</p>

<h2>The gotchas that actually bite</h2>

<p><strong>Your home region is chosen once.</strong> <q>All Oracle Cloud Infrastructure accounts (whether free or paid) have a set of resources that are free of charge in the home region of the tenancy, for the life of the account</q>, and <q>You must create the Always Free compute instances in your home region.</q> Pick the region nearest your users at signup; you do not get to move it later.</p>

<p><strong>Capacity is genuinely unavailable sometimes.</strong> Oracle documents the failure mode rather than hiding it: <q>If you receive an 'out of host capacity' error when trying to create a Compute instance, this indicates a temporary lack of Always Free shapes in your home region. Try creating the instance in a different availability domain, or wait a while, then try to create the instance again.</q> A1 instances can be created in any availability domain <q>except South Korea North (Chuncheon)</q>, while <q>Instances using the VM.Standard.E2.1.Micro shape can only be created in one availability domain.</q></p>

<p><strong>Idle instances can be taken back.</strong> This is the rule that surprises people who spin up a box "just in case":</p>

<blockquote>
<p>Idle Always Free compute instances may be reclaimed by Oracle. Oracle will deem virtual machine and bare metal compute instances as idle if, during a 7-day period, the following are true: CPU utilization for the 95th percentile is less than 20%; Network utilization is less than 20%; Memory utilization is less than 20% (applies to A1 shapes only).</p>
</blockquote>

<p>Read the conjunction carefully: all of the listed conditions must hold across the seven-day window. A box that is genuinely serving something — even lightly — is not the target; a box provisioned and forgotten is.</p>

<p><strong>Port 25 is closed.</strong> <q>Tenancies are by default not allowed to send e-mail through outbound TCP port 25 to the internet.</q> Do not plan to self-host outbound mail here. Use an API-based sending service.</p>

<p><strong>Two VCNs.</strong> Free Tier tenancies <q>can have up to 2 virtual cloud networks (VCNs)</q>, which is ample for one project and constraining if you wanted isolated networks per client.</p>

<h2>What you can realistically run</h2>

<p>This section is reasoning from documented specifications, not measurement. I have not run these workloads on these instances, and neither the projects nor Oracle publish figures that would let me state memory footprints as fact. Treat it as a starting hypothesis to verify on your own box.</p>

<p>The relevant documented constraint from the application side is narrower than people expect. <a href="https://github.com/QuantumNous/new-api">New API</a>, a commonly self-hosted LLM gateway, documents its deployment requirements as SQLite with a mounted <code>/data</code> directory, or a remote <q>MySQL ≥ 5.7.8 or PostgreSQL ≥ 9.6</q>, running under Docker or Docker Compose, and <q>64-bit only (amd64 / arm64); 32-bit systems are not supported</q>. It publishes no minimum CPU or RAM figure at all.</p>

<p>That absence is the important part. The sizing question is not "what does the gateway need" — a Go binary proxying HTTP is not memory-hungry — it is "what else are you putting on the same 1 GB." A Go service plus SQLite is a very different footprint from the same service plus a MySQL server plus Redis.</p>

<pre><code>E2.1.Micro  1/8 OCPU, 1 GB     one small Go/Rust service + SQLite;
                                a reverse proxy; a cron/worker box;
                                a WireGuard or Tailscale exit node
                                --> avoid: co-locating MySQL, running
                                    Docker builds, anything JVM

A1.Flex     2 OCPU, 12 GB      the same gateway plus MySQL plus Redis,
                                with headroom; multiple containers;
                                a small database you actually care about;
                                Arm-native builds</code></pre>

<p><em>Author judgment:</em> the 12 GB Arm instance is the one worth having, and the 1 GB micro instances are best treated as auxiliary — a proxy, a tunnel endpoint, a monitoring agent, a place to park a cron job — rather than as a home for your main service. Add swap on the micro shape before you need it. And if a project's own documentation declines to state a memory minimum, that is a signal to measure rather than to trust a number you read somewhere.</p>

<p>One caveat on a popular use case: proxying a subscription-based AI CLI through a self-hosted gateway may conflict with that provider's terms of service, independently of whether the server is free. That is a licensing question, not a capacity question, and the free tier does not change the answer.</p>

<h2>How to apply</h2>

<p>Signup requires a credit or debit card for identity verification even though Always Free resources do not charge it, and it requires choosing a home region you cannot later change. After signup you land in a Free Trial with credits; when those expire or are consumed, the account continues with Always Free resources only, and anything outside the Always Free list stops.</p>

<p>The practical sequence that avoids the two common dead ends:</p>

<pre><code>1. Choose the home region nearest your users. Permanent.
2. Provision the A1.Flex instance first, while you have
   the full 200 GB of storage to allocate.
   If you get "out of host capacity", try another
   availability domain, then retry later.
3. Keep the boot volume small (50 GB) unless you have a
   reason; storage is the binding constraint at four
   instances, not CPU.
4. Give the box real work, or expect the reclamation
   policy to apply.
5. Do not plan on outbound port 25.</code></pre>

<h2>Who should not use this</h2>

<p>Anyone who needs a specific region outside Oracle's footprint, anyone who needs more than two isolated networks, anyone whose workload is bursty enough that an "out of host capacity" error at provisioning time is unacceptable, and anyone hosting something for a client where an idle-reclamation policy is an unacceptable operational risk. Free compute with a documented reclamation clause is excellent for your own projects and a poor promise to make on someone else's behalf.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Cloudflare Pages vs Workers Static Assets (2026): What Actually Changes When You Migrate</title>
    <link>https://yangworks.dev/compare/cloudflare-pages-vs-workers-static-assets</link>
    <guid isPermaLink="true">https://yangworks.dev/compare/cloudflare-pages-vs-workers-static-assets</guid>
    <pubDate>Fri, 07 Aug 2026 00:00:00 GMT</pubDate>
    <description>For a static site the platforms are close enough that the decision is about defaults and direction, not capability — but the gap list runs both ways.</description>
    <content:encoded><![CDATA[<p>Yangworks ran on Cloudflare Pages and a direct-upload project, and now runs on Workers Static Assets. That migration is the evidence behind this comparison, and it also fixes its limits: this was a static site with no Pages Functions, no bindings, and no server-side rendering. Everything below is scoped to that case, and the sections that go beyond it are labelled.</p>

<p>The short version: for a purely static site the two platforms are close enough that the decision is not about capability. It is about which set of defaults you want to be responsible for, and where Cloudflare is putting its future work.</p>

<h2>Cloudflare's own position is not neutral</h2>

<p>This is the one part of the comparison that does not require judgment, because Cloudflare stated it directly. In the post announcing full-stack support on Workers, Cloudflare wrote that <q>you should <strong>start with Workers</strong></q>, that <q>Cloudflare Pages will continue to be supported</q>, and that <q>going forward, all of our investment, optimizations, and feature work will be dedicated to improving Workers.</q></p>

<p>Read that carefully, because it says two different things. Pages is supported — it is not deprecated and it is not scheduled for removal. But Pages is also not where new capability is going to appear. <em>Author judgment:</em> that makes the choice for a new project easy and the choice for a working Pages project genuinely open, which is the opposite of how migration advice is usually written.</p>

<h2>The configuration model is the real difference</h2>

<p>Pages kept most of its behaviour in the dashboard and inferred the rest from your build output. Workers keeps it in a checked-in Wrangler file. Cloudflare's migration guide maps the keys:</p>

<pre><code>Pages                        Workers
---------------------------  --------------------------------
pages_build_output_dir  -->  assets.directory
(compatibility date optional) compatibility_date  (required)
ASSETS binding automatic -->  assets.binding      (declared)
Functions run first      -->  assets.run_worker_first</code></pre>

<p>For Yangworks the entire Workers configuration is four lines of asset settings and no Worker script:</p>

<pre><code>{
  "name": "yangworks",
  "compatibility_date": "2025-01-01",
  "assets": {
    "directory": "./public",
    "not_found_handling": "404-page"
  }
}</code></pre>

<p>The Cloudflare docs note that if there is no Worker script you should drop the <code>"binding": "ASSETS"</code> entry, since it is only meaningful alongside <code>main</code>.</p>

<h3>Inference becomes declaration</h3>

<p>Pages inspected the output directory for <code>404.html</code> and <code>index.html</code> and guessed whether you wanted single-page-application fallback or a custom not-found page. Workers requires <code>not_found_handling</code> to be set to either <code>"single-page-application"</code> or <code>"404-page"</code>. The documentation explains why in one sentence: <q>to prevent accidental misconfiguration, this behavior is explicit and must be set up manually.</q></p>

<p>The same pattern repeats elsewhere. Pages automatically excluded files such as <code>node_modules</code>, <code>.DS_Store</code>, and <code>.git</code> from upload; on Workers you reproduce that with an <code>.assetsignore</code> file inside the asset directory.</p>

<p><em>Author judgment:</em> for a generated <code>public/</code> directory that is rebuilt from scratch on every run, this trade is clearly favourable — the ignore list is empty and the 404 behaviour is now visible in a pull request instead of a settings screen. For a project whose output directory is also its working directory, it is extra bookkeeping.</p>

<h3>Routing precedence flips</h3>

<p>This is the change most likely to break a real application, and it did not affect Yangworks at all because there is no Worker code here. Cloudflare documents that Pages ran Functions ahead of static assets, tunable with <code>_routes.json</code> and middleware, while Workers <q>will default to serving static assets ahead of your Worker script</q>. If you relied on middleware for authentication checks or request logging, you need <code>run_worker_first</code> to restore the old order. The docs also note that Pages middleware moved to Workers <q>is charged as a normal Worker invocation.</q></p>

<h2>Clean URLs and 404s behave the same, but you should verify them</h2>

<p>Workers Static Assets normalizes URLs through <code>html_handling</code>, which defaults to <code>auto-trailing-slash</code>: individual files are served without a trailing slash, folder index files with one. Every redirect documented for that normalization is a <strong>307</strong>, not a 301.</p>

<p>On the live Yangworks deployment, checked 7 August 2026, that matches: <code>/handbook/niche.html</code> returns 307 to <code>/handbook/niche</code>, and a route that does not exist returns a 404 status with the generated error page. Those two requests are worth making after any migration, because a friendly error page served as <code>200 OK</code> is still a routing bug.</p>

<p>One compatibility detail that saves work: <code>_headers</code> and <code>_redirects</code> are, per Cloudflare's documentation, <q>supported natively in Workers with static assets</q> — you keep the files where they were.</p>

<h2>Git deployment and previews are close, not identical</h2>

<p>Both platforms build from a connected repository. On Workers the flow is Workers Builds running your build command and then <code>npx wrangler deploy</code>; Yangworks uses <code>npm run build</code> and the production branch <code>main</code>.</p>

<p>Preview parity takes two settings rather than being automatic: Cloudflare's guide says to keep <code>preview_urls</code> enabled (it is on by default) and to turn on builds for non-production branches. Preview URLs can optionally be placed behind Cloudflare Access.</p>

<p>The migration guide is also explicit about a failure mode that Git history will not catch: after connecting Workers Builds, <strong>disable automatic deployments on the Pages project</strong>. Otherwise one repository quietly feeds two deployment systems while you believe the move is finished. Delete the Pages project only after traffic and routes are confirmed.</p>

<h2>What Pages still does that Workers does not</h2>

<p>All of these are from Cloudflare's own migration guide, which is unusually candid about the gaps:</p>

<ul>
  <li><strong>Custom domains on off-Cloudflare nameservers.</strong> Supported on Pages, unsupported on Workers. For anyone whose DNS is not on Cloudflare, this is a blocker rather than an inconvenience.</li>
  <li><strong>Per-environment bindings.</strong> Workers cannot define different bindings for production and non-production builds; the workaround is Wrangler Environments plus build configuration. Cloudflare says this is <q>something we are actively exploring.</q></li>
  <li><strong>Custom Branch Aliases</strong> are marked coming soon, and <strong>Branch Deploy Controls</strong> are only partially matched.</li>
  <li><strong>Early Hints</strong> is a workaround (zone setting plus <code>Link</code> headers) rather than a feature.</li>
  <li><strong>File-based routing and Pages Plugins</strong> require either framework support or compiling the <code>functions/</code> directory yourself.</li>
</ul>

<h2>What Workers does that Pages does not</h2>

<p>The same guide lists the other direction: the Vite plugin, gradual deployments, remote development, Workers Logs, Logpush, Tail Workers, source maps, Cron Triggers, Queue Consumers, Rate Limiting, Email Workers, Image Resizing, non-root routes, and serving assets on a path. Durable Objects are usable directly rather than through a separate Worker.</p>

<p><em>Author judgment:</em> none of that list matters to a static publication today. It matters a great deal to the version of the project that eventually needs a login, a scheduled job, or an API route — and that is the argument for moving before you need them, not after.</p>

<h2>Cost and limits</h2>

<p>Cloudflare's pricing page states that <q>Requests to static assets are free and unlimited</q>, and that storing assets carries no additional cost. The Workers Free plan allows 100,000 requests per day; Workers Paid starts at $5 per month with 10 million requests included. Platform limits for static assets are 20,000 files per Worker version on Free and 100,000 on Paid, with a 25 MiB maximum for an individual file.</p>

<p>One free-tier caveat is worth knowing before enabling <code>run_worker_first</code>: matching requests always invoke the Worker, and once free-tier request limits are exceeded those requests receive a 429 rather than falling back to static asset serving.</p>

<p><strong>This article measures no cost difference between the two platforms.</strong> Yangworks did not run both in parallel, did not record bills, and does not claim savings.</p>

<h2>The migration order that worked here</h2>

<p>The sequence that made this readable was to change one thing at a time. Source consolidation first: one repository, one <code>npm run build</code>, one <code>public/</code> artifact. Then the deployment: add <code>wrangler.jsonc</code>, connect Workers Builds, verify, disable and remove the old projects. Language expansion came later and changed only the contents of <code>public/</code>, not the number of deployments. The commit-level account is in <a href="https://yangworks.dev/field-notes/moving-yangworks-to-cloudflare-workers">the migration field note</a>, and the resulting build and deployment contract is described in <a href="https://yangworks.dev/guides/deploy-static-site-cloudflare-workers">the deploy guide</a>.</p>

<p>The step that is easiest to skip and most expensive to skip is writing down the expected status code and final URL for every route before cutover — <code>.html</code> forms, trailing slashes, language roots, and any legacy host — then checking them against the deployed responses rather than against a browser's address bar.</p>

<h2>Which one to choose</h2>

<p><em>The following is author judgment, not Cloudflare guidance.</em></p>

<p><strong>Start on Workers</strong> if the project is new, if you want the deployment contract in version control, or if you can foresee needing scheduled jobs, queues, logs, or an API path alongside the static files.</p>

<p><strong>Stay on Pages</strong> if it is working, if your nameservers are not on Cloudflare, if you depend on per-environment bindings or Pages Plugins, or if your team's review process assumes branch-alias behaviour that Workers has not matched yet. "Supported" is a real commitment, and a migration you do not need is still a migration you have to verify.</p>

<h2>What this comparison does not establish</h2>

<p>One site, one migration, no Functions, no bindings, no SSR. No performance benchmark was run, no build times were compared, no bills were compared, and no availability difference was observed or claimed. A team migrating a full-stack Pages application with middleware and per-environment bindings is facing a materially harder job than the one documented here, and should treat the gap list above as the starting point rather than the footnote.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Cloudflare Workers Static Assets Review (2026): Running a Real Production Site on It</title>
    <link>https://yangworks.dev/reviews/cloudflare-workers-static-assets-review</link>
    <guid isPermaLink="true">https://yangworks.dev/reviews/cloudflare-workers-static-assets-review</guid>
    <pubDate>Fri, 07 Aug 2026 00:00:00 GMT</pubDate>
    <description>Four lines of configuration, correct 404s, documented 307s, and a caching default you will want to override. Two months is enough to review ergonomics, not reliability.</description>
    <content:encoded><![CDATA[<p>Yangworks has run on Cloudflare Workers Static Assets since 9 June 2026 — roughly two months at the time of writing. It is one site: a trilingual handbook, a publication with four sections, a landing page, and a set of legal pages. 75 files, 1.1 MB, rebuilt from scratch on every deploy.</p>

<p>That is the whole basis of this review, and it is a narrow one. There is no Worker script here, no server-side rendering, no bindings, no database, no authentication, and no traffic worth calling traffic. Everything below is either something this site actually does, or a documented fact from Cloudflare that is labelled as such.</p>

<h2>Getting it running took four lines of configuration</h2>

<p>The entire platform-specific surface of this project is one file:</p>

<pre><code>{
  "name": "yangworks",
  "compatibility_date": "2025-01-01",
  "assets": {
    "directory": "./public",
    "not_found_handling": "404-page"
  }
}</code></pre>

<p>There is no Worker script and no <code>main</code> entry. <code>assets.directory</code> points at whatever the build produced; <code>not_found_handling</code> decides what happens on a miss. Cloudflare requires that second setting to be explicit — the documentation says the behaviour is manual <q>to prevent accidental misconfiguration.</q></p>

<p>The build side stayed equally plain. <code>package.json</code> declares zero dependencies and zero devDependencies; <code>npm run build</code> is a Node script that empties <code>public/</code> and regenerates it, and it completes in about 0.2 seconds locally. Workers Static Assets did not push any tooling into the project, which is the main thing I wanted from it.</p>

<h2>The deployment chain is a push</h2>

<p>The GitHub repository is connected to Workers Builds with <code>main</code> as the production branch, <code>npm run build</code> as the build command, and <code>npx wrangler deploy</code> as the deploy command. A merge to <code>main</code> rebuilds and publishes.</p>

<p><em>Author judgment:</em> this is genuinely convenient and genuinely sharp. There is no staging gate between a merge and production, so branch protection and local verification stop being hygiene and start being the release process. On a solo project that is a fair trade; I would not describe it as safe by default.</p>

<h2>Routing behaved as documented, including the parts that surprise people</h2>

<p>Every route family below was requested against the live site on 7 August 2026:</p>

<pre><code>/                              200
/handbook/                     200
/en/handbook/                  200
/ja/handbook/niche             200
/about                         200
/en/handbook/niche.html        307  ->  /en/handbook/niche
/en                            307  ->  /en/
/no-such-page                  404  (custom page)
/en/handbook/does-not-exist    404  (custom page)</code></pre>

<p>Two things are worth pulling out. First, the <code>.html</code> redirect is a <strong>307</strong>, not a 301 — that is the documented behaviour of the default <code>html_handling</code> mode, <code>auto-trailing-slash</code>, in which files are served without a trailing slash and folder indexes with one. If you are putting URLs into canonicals, sitemaps, or hreflang clusters, use the form that returns 200 rather than the form that redirects to it.</p>

<p>Second, the 404 is a real 404. Both an unknown root path and an unknown path inside a language subtree returned status 404 with the generated error page, rather than a styled page served as 200. That distinction is invisible in a browser and is exactly the kind of thing a static host can get wrong.</p>

<p>The three language subtrees needed no routing configuration at all. <code>/handbook/</code>, <code>/en/handbook/</code>, and <code>/ja/handbook/</code> are just directories in the artifact.</p>

<h2>The default response headers are opinionated</h2>

<p>Without a <code>_headers</code> file, this is what the platform sends:</p>

<pre><code>cache-control: public, max-age=0, must-revalidate
etag: W/"f91fb91a8cf2447d94f9b1c62ec62354"
content-encoding: br
alt-svc: h3=":443"; ma=86400</code></pre>

<p>ETags and Brotli negotiation are automatic; HTTP/2 and HTTP/3 are on. But <code>max-age=0, must-revalidate</code> means browsers revalidate every asset on every navigation, including fingerprint-free files like <code>styles.css</code>. Cloudflare's edge cache absorbs most of that — <code>cf-cache-status</code> showed <code>HIT</code> on repeat requests — but the browser round-trip still happens.</p>

<p><em>Author judgment:</em> this is a sensible default for a platform that cannot know whether your filenames are content-hashed, and a bad default for a site whose assets are stable. Cloudflare supports <code>_headers</code> natively in Workers Static Assets, so fixing it is a file, not a migration. I have not fixed it here yet, which is a fair criticism of this deployment rather than of the platform.</p>

<h2>Pricing and limits</h2>

<p>All of this is from Cloudflare's own documentation, checked 7 August 2026, not from a bill:</p>

<ul>
  <li>Cloudflare states that <q>Requests to static assets are free and unlimited</q>, and that storing assets carries <q>no additional cost</q>.</li>
  <li>The Workers Free plan allows 100,000 requests per day. Workers Paid starts at $5 per month with 10 million requests included.</li>
  <li>Static asset limits are 20,000 files per Worker version on Free and 100,000 on Paid, with a maximum individual file size of 25 MiB.</li>
  <li>One trap: if you enable <code>run_worker_first</code>, matching requests always invoke the Worker, and once free-tier limits are exceeded those requests receive a 429 rather than falling back to serving the asset.</li>
</ul>

<p>At 75 files this project is three orders of magnitude below the file-count ceiling, so the limits are theoretical here. <strong>This review reports no cost figure</strong>, because a site with no meaningful traffic produces no meaningful bill.</p>

<h2>What's good</h2>

<ul>
  <li><strong>The configuration is small and it is in the repository.</strong> Four asset settings replace a dashboard's worth of inferred behaviour, and a reviewer can see the 404 policy in a diff.</li>
  <li><strong>It imposes nothing on the build.</strong> Any process that produces a directory works. Zero dependencies stayed zero.</li>
  <li><strong>Static assets are free and unlimited per Cloudflare's pricing page</strong>, with no egress charge — the cost model for a content site is close to trivial.</li>
  <li><strong>Sensible transport defaults.</strong> Brotli, ETags, HTTP/2 and HTTP/3 required no configuration.</li>
  <li><strong>Correct status codes.</strong> Real 404s, documented 307 normalization, no surprises between the documentation and the wire.</li>
  <li><strong>Room to grow without moving.</strong> Adding a Worker script later is a config key, not a replatform — which is the difference from a pure static host.</li>
</ul>

<h2>What's not</h2>

<ul>
  <li><strong>The caching default needs correcting for most sites.</strong> <code>max-age=0, must-revalidate</code> on every asset is conservative to a fault.</li>
  <li><strong>307, not 301.</strong> Legacy <code>.html</code> URLs are normalized with a temporary redirect. It works, but if you want a permanent signal you have to arrange it yourself.</li>
  <li><strong>Push-to-production has no built-in gate.</strong> Preview URLs and non-production branch builds exist, but they are settings you must turn on, not a default workflow.</li>
  <li><strong>Documented feature gaps versus Pages are real.</strong> Cloudflare's own migration guide lists domains on off-Cloudflare nameservers as unsupported, per-environment bindings as unavailable, Custom Branch Aliases as coming soon, and Early Hints as a workaround.</li>
  <li><strong>Nothing warns you about what you forgot.</strong> <code>www.yangworks.dev</code> currently has no DNS record at all, because nothing in the platform prompts you to add a second hostname. That is my omission, but it illustrates the point: the deployment is checked into Git, the edge around it is not.</li>
</ul>

<h2>Who it suits</h2>

<p>Developers and small teams shipping a static or mostly-static site from Git who want the hosting contract in version control rather than in a settings panel — documentation, publications, marketing sites, docs-plus-app combinations. It suits you especially well if you expect the project to eventually need a scheduled job, a queue, an API route, or an authenticated path, because you can add those without changing platform. If you are already inside Cloudflare for DNS, it is the path of least resistance.</p>

<h2>Who it doesn't</h2>

<p>Anyone whose nameservers are not on Cloudflare, since custom domains in that configuration are documented as unsupported. Teams that depend on per-environment bindings, Pages Plugins, file-based routing for <code>functions/</code>, or Custom Branch Aliases will find those either missing or a workaround. Organizations that need a mandatory review-and-promote pipeline should expect to build it around Workers Builds rather than receive it. And if you have a Cloudflare Pages project that works, none of this is a reason to move — Cloudflare has said Pages <q>will continue to be supported</q>, even while stating that new investment goes to Workers.</p>

<h2>Alternatives, one sentence each</h2>

<p><em>Author judgment, from documentation rather than side-by-side testing:</em> <strong>Cloudflare Pages</strong> is the closest option and the honest default if you are already on it — the differences are set out in <a href="https://yangworks.dev/compare/cloudflare-pages-vs-workers-static-assets">the Pages versus Workers comparison</a>. <strong>Netlify</strong> and <strong>Vercel</strong> lead on build-pipeline ergonomics and preview workflow, at the cost of a metered build-minutes and bandwidth model. <strong>GitHub Pages</strong> remains the simplest thing that works when a project needs nothing but files. <strong>An object store behind a CDN</strong> is cheaper at scale and hands you the routing, redirect, and 404 behaviour as your own problem.</p>

<h2>What this review does not cover</h2>

<p>No Worker script, no SSR, no bindings, no Durable Objects, no Cron Triggers, no queues. No load testing and no traffic at a scale that would reveal anything. No preview-branch workflow, no rollbacks, no Cloudflare Access, and no multi-developer or multi-project team usage. No measured latency, uptime, or cost, and no controlled comparison against another host. Two months on one small static site is enough to review setup, routing, defaults, and deployment ergonomics — and it is not enough to review reliability.</p>

<p>The build and deployment contract behind this setup is documented in <a href="https://yangworks.dev/guides/deploy-static-site-cloudflare-workers">the deploy guide</a>, and how the site got here is in <a href="https://yangworks.dev/field-notes/moving-yangworks-to-cloudflare-workers">the migration field note</a>.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Deploy a Multilingual Static Site with Cloudflare Workers Static Assets</title>
    <link>https://yangworks.dev/guides/deploy-static-site-cloudflare-workers</link>
    <guid isPermaLink="true">https://yangworks.dev/guides/deploy-static-site-cloudflare-workers</guid>
    <pubDate>Fri, 07 Aug 2026 00:00:00 GMT</pubDate>
    <description>The deploy command is simple; the real work is making one reproducible artifact and one explicit routing contract.</description>
    <content:encoded><![CDATA[<p>The deploy commands are simple:</p>

<pre><code>npm run build
npx wrangler deploy</code></pre>

<p>The work is deciding what those two commands mean. For Yangworks, the answer is deliberately plain: the first command must produce the entire site in <code>public/</code>; the second must publish that directory without adding a runtime application. The landing page, legal pages, handbook language trees, publication pages, sitemap, and 404 page all cross the same build boundary.</p>

<p>Cloudflare now recommends Workers for new projects while continuing to support Pages. That product direction is useful context, but it is not the reason this architecture works. The useful part is having one reproducible artifact and one checked-in deployment contract. The repository history behind that decision is covered in <a href="https://yangworks.dev/field-notes/moving-yangworks-to-cloudflare-workers">Moving Yangworks to Cloudflare Workers Static Assets</a>.</p>

<h2>The shape of the deployment</h2>

<pre><code>GitHub repository
  |
  | npm run build
  v
root build.js
  |-- copy src/site/   --------&gt; public/
  |-- copy src/pages/  --------&gt; public/
  |-- copy src/static/ --------&gt; public/
  |-- run handbook builder
  |     |-- Chinese ----------&gt; public/handbook/
  |     |-- English ----------&gt; public/en/handbook/
  |     `-- Japanese ---------&gt; public/ja/handbook/
  |-- run publication builder
  |     `-- articles/indexes -&gt; public/{guides,field-notes,reviews,compare}/
  |-- write /en/ and /ja/ entry pages
  |-- write 404.html
  `-- write sitemap.xml

npx wrangler deploy
  `-- upload public/ as Workers Static Assets</code></pre>

<p>Cloudflare sees generated files. It does not need to understand the source folders, translation registry, publication registry, or sitemap logic. Those remain repository concerns, which means they can be built and inspected before a deployment exists.</p>

<p>The normal build emits only publication entries marked <code>published</code>. A draft build adds final-route previews with <code>noindex,nofollow</code>, but draft URLs never enter the sitemap. Keeping that rule in the root build prevents local editorial previews from becoming production content.</p>

<h2>Make <code>public/</code> the contract</h2>

<p>The package scripts expose one build entry point and one local-only variant of it:</p>

<pre><code>{
  "scripts": {
    "build": "node build.js",
    "build:drafts": "node build.js --drafts",
    "test": "node --test"
  }
}</code></pre>

<p>Only <code>build</code> is wired to the deployment. <code>build:drafts</code> exists so unpublished pages can be reviewed at their final routes without any path to production.</p>

<p>The root builder deletes <code>public/</code>, recreates it, copies the ordinary site files, invokes the handbook builder, and then writes the generated support files. Cleaning the directory is important. If a source page is removed, its old output must disappear too; a deployment should not preserve stale HTML because a previous build happened to leave it behind.</p>

<p>It also means <code>public/</code> is disposable. Editing a generated file by hand produces a change that vanishes at the next build. Every public file needs an owner under <code>src/</code> or in a generator.</p>

<h3>Let the nested builder own language paths</h3>

<p>The root builder passes the site output root to the handbook builder:</p>

<pre><code>const OUT = path.join(ROOT, 'public');
const hb = require(path.join(SRC, 'handbook', 'build.js'));

hb.build(OUT);</code></pre>

<p>Passing <code>public/handbook/</code> would look reasonable and be wrong. The nested builder already decides that Chinese belongs in <code>public/handbook/</code>, English in <code>public/en/handbook/</code>, and Japanese in <code>public/ja/handbook/</code>. It needs the site root in order to make that decision once.</p>

<p>The same builder treats a translation as published only when its source fragment exists. A missing language file therefore produces no HTML page, navigation item, language-switch link, or sitemap alternate. The build does not create a thin placeholder merely to keep the directory tree symmetrical.</p>

<h2>Keep Wrangler explicit</h2>

<p>The current configuration contains no Worker script:</p>

<pre><code>{
  "name": "yangworks",
  "compatibility_date": "2025-01-01",
  "assets": {
    "directory": "./public",
    "not_found_handling": "404-page"
  }
}</code></pre>

<p><code>assets.directory</code> connects the deployment to the build artifact. <code>not_found_handling</code> connects missing routes to the generated <code>404.html</code>. Wrangler uploads the files as Workers Static Assets; this is not a Cloudflare Pages deployment with a different label.</p>

<p>The explicit 404 setting is worth calling out because Pages and Workers do not make the same guesses. Cloudflare’s Pages-to-Workers migration guide says Pages inferred SPA or custom-404 behavior from the output, while Workers requires <code>not_found_handling</code> to be chosen. Yangworks uses <code>"404-page"</code> because unknown content routes should remain misses. Use <code>"single-page-application"</code> only when a client router must receive otherwise-unmatched paths.</p>

<p>If a Worker script is added later, routing order becomes another explicit choice. When Worker code must inspect requests that also match static assets, configure <code>run_worker_first</code>; otherwise matching assets bypass Worker code by default. Yangworks does not need that setting today because it has no runtime routes.</p>

<p>Keep the Wrangler file in version control, but keep credentials out of it. Authentication belongs to the deployment environment or Cloudflare integration.</p>

<h2>Treat a push to <code>main</code> as a release</h2>

<p>The Workers project is connected to GitHub with <code>main</code> as the production branch:</p>

<pre><code>Build command:  npm run build
Deploy command: npx wrangler deploy</code></pre>

<p>A push to <code>main</code> rebuilds the repository and publishes the resulting assets. The stable facts are the branch, project name, commands, and checked-in Wrangler file—not the current arrangement of controls in Cloudflare’s dashboard.</p>

<p>If this is a migration from an active Pages project, connecting Workers Builds is only half the cutover. Cloudflare’s migration guide explicitly tells you to disable automatic deployments on the Pages project after the Workers build is connected. Otherwise one repository can continue feeding two deployment systems while you think the move is complete. Validate the Worker first; remove the old project only after traffic and routes are correct.</p>

<p>For Yangworks, local verification matters because production deploys are automatic. A build that exits successfully can still publish a missing page, a wrong canonical, or a broken language link.</p>

<h2>Clean URLs are a hosting behavior, not a filename convention</h2>

<p>The generator writes files such as <code>public/handbook/niche.html</code>. On the deployed site, the <code>.html</code> URL has been observed to return a 307 redirect to <code>/handbook/niche</code>. Canonicals, sitemap entries, and language-switch links therefore use the extensionless form.</p>

<p>Cloudflare exposes this behavior through <code>assets.html_handling</code>; the default is <code>auto-trailing-slash</code>. Do not assume a local file server reproduces it. Test the deployed response chain before deciding which URL belongs in metadata.</p>

<p>Some handbook links still point to relative paths such as <code>./niche.html</code>. They resolve, but they take the redirect hop. Removing it is a separate cleanup: generated navigation, body links, output filenames, canonicals, and the sitemap need to agree. Changing one layer alone makes generated links, metadata, and deployed routes disagree.</p>

<h2>The 404 page and language roots need status checks</h2>

<p>The root build writes a shared <code>404.html</code> with <code>noindex</code> and links to the available language sections. With <code>not_found_handling: "404-page"</code>, Workers serves the nearest applicable 404 document with a not-found status. Verify both pieces. A friendly error page returned as <code>200 OK</code> is still a routing bug.</p>

<p>The build also writes <code>public/en/index.html</code> and <code>public/ja/index.html</code>. Workers serves them at <code>/en/</code> and <code>/ja/</code>; the no-slash forms may first receive the provider’s trailing-slash redirect. The generated pages then use a zero-second meta refresh to reach the handbook roots.</p>

<p>Those pages are not HTTP 301 redirects. They have a canonical target, a visible link, and <code>noindex</code>, but a header check will still show an HTML response. If a permanent server-side redirect is required, implement one at the edge rather than describing the current stubs as something they are not.</p>

<h2>Generate discovery metadata from the page model</h2>

<p>The sitemap is written after the handbook build, when the root script knows which language files were actually generated. Root pages receive ordinary URL entries. Each handbook page receives one entry per available language, and every language cluster is derived from the same alternate list.</p>

<p>That list contains the page itself, the other existing translations, and <code>x-default</code> pointing to the Chinese route. A missing translation never reaches the sitemap because the builder and sitemap share the same availability check. The details are covered in <a href="https://yangworks.dev/guides/multilingual-static-site-hreflang-sitemap">Multilingual SEO for a static site without a framework</a>.</p>

<h2>Verify the artifact, then the edge behavior</h2>

<p>Run the production build from a clean checkout and inspect the artifact rather than trusting the exit code. Confirm the root page, handbook roots, localized directories, <code>404.html</code>, and <code>sitemap.xml</code> exist. If the publication registry contains published entries, spot-check a generated article and section index. Draft article files must be absent from a production build, and no draft URL should appear in the sitemap. Then check that every language page canonicals to itself and that the sitemap contains no URL for a missing file.</p>

<p>After deployment, a few requests expose most routing mistakes:</p>

<pre><code>curl -I https://yangworks.dev/
curl -I https://yangworks.dev/handbook/
curl -I https://yangworks.dev/en/handbook/
curl -I https://yangworks.dev/handbook/niche.html
curl -I https://yangworks.dev/a-route-that-does-not-exist
curl https://yangworks.dev/sitemap.xml</code></pre>

<p>Read the status and <code>Location</code> headers instead of relying on what a browser eventually displays. Then open representative pages to catch CSS, JavaScript, relative-link, and mobile-navigation problems. Finally, confirm that the Cloudflare deployment points to the commit you intended to release.</p>

<h2>What this setup does—and does not—buy</h2>

<p>The result is easy to reason about: one dependency-light Node build, one static output directory, one deployment configuration, and no application server. The static artifact can move to another host, but its redirects, HTML handling, 404 behavior, and Git deployment need to be recreated there.</p>

<p>The custom generator is still infrastructure. Its URL rules, page registry, translation availability, sitemap generation, and tests need maintenance. A full rebuild is sensible at this size and may be wasteful for a much larger publication. Automatic Git deployment also turns branch protection and review quality into production controls.</p>

<p>This case does not establish lower cost, better latency, faster builds, higher rankings, or zero-downtime migration. Cloudflare’s own position is narrower: start new projects on Workers; Pages remains supported; future product investment is centered on Workers. For this site, the decision was about a clear artifact and a deployment model that matched it—not a benchmark victory.</p>

<p>This pattern assumes public routes can be materialized at build time and editors can work through Git. Authentication, request-time personalization, database reads, and application APIs require Worker code or another runtime.</p>
]]></content:encoded>
  </item>
  <item>
    <title>The Actual Tool Stack Behind a One-Person Developer Publication (2026)</title>
    <link>https://yangworks.dev/guides/freelance-developer-tool-stack</link>
    <guid isPermaLink="true">https://yangworks.dev/guides/freelance-developer-tool-stack</guid>
    <pubDate>Fri, 07 Aug 2026 00:00:00 GMT</pubDate>
    <description>One rule: if it is not currently running this site, it is not on the list. That leaves ten tools, four honest weaknesses, and one entry that was missing because a dashboard toggle never shows up in a diff.</description>
    <content:encoded><![CDATA[<p>Most "developer tool stack" posts are shopping lists. This one has a stricter rule: <strong>if it is not currently running this site, it is not on the list.</strong> Everything below can be verified from the repository, the DNS records, or the HTTP responses of yangworks.dev.</p>

<p>That rule cuts out a lot. There is no CMS, no framework, no bundler, no design tool, no issue tracker, and no newsletter platform — not because those are bad, but because this project does not run them. The last section of this article says what "absent" actually means.</p>

<h2>Hosting, DNS, and email: Cloudflare, three products</h2>

<p><strong>Workers Static Assets</strong> serves the site. The configuration is four asset settings and no Worker script. <em>Why it:</em> the deployment contract lives in <code>wrangler.jsonc</code> in the repository rather than in a dashboard, and adding server-side code later is a config key rather than a replatform. <em>What I did not choose:</em> Cloudflare Pages, which still works and which this site migrated off — the differences are in <a href="https://yangworks.dev/compare/cloudflare-pages-vs-workers-static-assets">the Pages versus Workers comparison</a>, and the two-month verdict is in <a href="https://yangworks.dev/reviews/cloudflare-workers-static-assets-review">the review</a>. <em>Cost:</em> free tier; Cloudflare's pricing page states requests to static assets are free and unlimited.</p>

<p><strong>Cloudflare DNS</strong> holds the zone. <em>Why it:</em> the custom domain, the Workers deployment, and email routing are all one account, and Workers documents custom domains on off-Cloudflare nameservers as unsupported — so keeping DNS here is not really optional given the hosting choice. <em>Cost:</em> free.</p>

<p><strong>Cloudflare Email Routing</strong> handles <code>hello@yangworks.dev</code>. The zone's MX records point at <code>route1</code>, <code>route2</code>, and <code>route3.mx.cloudflare.net</code> with an SPF record including <code>_spf.mx.cloudflare.net</code>. <em>Why it:</em> a publication needs a contact address that is not a personal Gmail, and forwarding into an existing inbox is less to maintain than a mailbox. <em>The limitation:</em> routing is not a mailbox. Replies do not go out from the domain — Cloudflare's Email Sending is a separate beta on the Workers Paid plan. <em>What I did not choose:</em> Google Workspace or Fastmail, either of which would give a real sendable mailbox for a monthly fee. <em>Cost:</em> Email Routing is available on Cloudflare's Free plan.</p>

<h2>Source and CI: GitHub, with the pipeline on Cloudflare's side</h2>

<p>The repository is on GitHub with <code>main</code> as the production branch. There is no <code>.github/workflows</code> directory. Cloudflare Workers Builds is connected to the repository and runs <code>npm run build</code> followed by <code>npx wrangler deploy</code> on a push to <code>main</code>.</p>

<p><em>Why it:</em> one pipeline instead of two. A GitHub Actions workflow that builds and then calls <code>wrangler deploy</code> would duplicate what Workers Builds already does. <em>What I gave up:</em> tests do not currently run in CI — <code>npm test</code> runs locally, and a push to <code>main</code> deploys whether or not it passed. That is the weakest link in this stack and I am naming it rather than describing it as simplicity. <em>Cost:</em> free — the repository is private, and GitHub's Free plan covers <q>unlimited private repositories with a limited feature set.</q></p>

<h2>Build: Node.js and nothing else</h2>

<p><code>package.json</code> declares zero dependencies and zero devDependencies. The build is a Node script that deletes <code>public/</code>, copies three source directories into it, runs the handbook and publication generators, and writes the sitemap, language-root stubs, and 404 page. It finishes in roughly 0.2 seconds and produces 75 files.</p>

<p>Tests use <code>node --test</code>, the runtime's built-in test runner.</p>

<p><em>Why it:</em> a site this size does not need a build system, and every dependency is something to update. <em>What I did not choose:</em> Astro, Eleventy, and Hugo would all handle this comfortably and would have saved me writing an i18n availability model, a sitemap generator, and a publication registry by hand. <em>The honest trade:</em> that custom code is now infrastructure I maintain. A framework would have moved the work, not removed it — but it would have moved it onto someone else's maintenance schedule, which is worth something. <em>Cost:</em> free.</p>

<p>One inconsistency worth admitting: <code>.nvmrc</code> pins Node 20 and the machine that builds locally is on 22.14.0. Nothing has broken, because the build only uses long-stable built-ins, but the pin does not currently describe reality.</p>

<h2>Deploy: Wrangler</h2>

<p>Wrangler 4.63.0, invoked as <code>npx wrangler deploy</code>. <em>Why it:</em> it is the supported way to deploy a Workers project and it reads the same checked-in config that Workers Builds uses, so a local deploy and a CI deploy do the same thing. <em>Cost:</em> free.</p>

<h2>Writing: HTML fragments and a metadata registry</h2>

<p>Articles are plain HTML fragments in <code>src/publication/content/</code>. Their metadata — title, dates, test scope, evidence, sources with the date each was checked, affiliate status, related links — lives in a single JavaScript registry that the build validates. Publishing is a status field.</p>

<p><em>Why it:</em> the validator is the point. It refuses to build a published article missing a required field, with an unresolvable internal link, or with an unfinished marker left in the body, and it keeps draft URLs out of the sitemap. That is editorial policy expressed as a build failure rather than as a promise. <em>What I did not choose:</em> Notion, Ghost, and WordPress all offer a better writing experience and none of them would let me express "an article cannot be published without a checked source date" as a hard error. <em>Cost:</em> free.</p>

<p><strong>Claude Code</strong> is used for drafting and for repository work; the repository contains a <code>CLAUDE.md</code> with the project's rules. Every published sentence is reviewed by a human before it ships, and the <a href="https://yangworks.dev/editorial-policy">editorial policy</a> states this in full. <em>Cost:</em> a paid subscription — the one line item here that is neither free nor strictly required.</p>

<h2>Typography: Google Fonts</h2>

<p>Inter, loaded from <code>fonts.googleapis.com</code> and <code>fonts.gstatic.com</code>. <em>Why it:</em> it was the fastest way to get consistent typography across three writing systems. <em>The cost is not money.</em> It sends every visitor's request to a third party, which is why the <a href="https://yangworks.dev/privacy">privacy policy</a> discloses it. Self-hosting the font files would remove that dependency, and it is on the list of things to fix.</p>

<h2>Analytics: Cloudflare Web Analytics, and a lesson in checking your own site</h2>

<p><strong>Cloudflare Web Analytics</strong> is running, and it is the one entry on this list that is invisible in the repository. It is enabled at the zone level with Cloudflare's <em>automatic setup</em> option, which means Cloudflare injects <code>beacon.min.js</code> from <code>static.cloudflareinsights.com</code> into HTML responses at the edge. Nothing is added at build time: the build scripts contain no analytics code, and the generated <code>public/</code> directory contains no beacon script tag — the site's own test suite asserts this, so that a hand-added snippet can never quietly double-count alongside the edge injection.</p>

<p>This entry started as a section titled "Analytics: none," which was wrong. The rule at the top of this article — if it is not currently running this site, it is not on the list — has an obvious inverse that is easier to violate: <strong>if it <em>is</em> running this site, it belongs on the list, whether or not you put it there in code.</strong> A dashboard toggle flipped once, months before, does not show up in any diff.</p>

<p>The verification detail that hid it is worth repeating, because it will bite anyone auditing their own headers: <strong>edge injection is gated on the <code>Accept</code> request header, not on the User-Agent.</strong> A plain <code>curl https://yangworks.dev/</code> sends <code>Accept: */*</code> and gets back HTML with no beacon in it, which reads exactly like proof that no analytics exists. Ask for HTML explicitly and the beacon is there — with curl's own User-Agent, unchanged:</p>

<pre><code>curl -s https://yangworks.dev/ | grep -c beacon.min.js
# 0    &lt;- Accept: */*  (curl's default) — this is the trap

curl -s -H 'Accept: text/html' https://yangworks.dev/ | grep -c beacon.min.js
# 1    &lt;- same UA, same URL, one header different</code></pre>

<p>The pair that isolates the cause is the crossed one: a Chrome User-Agent with <code>Accept: */*</code> returns <em>no</em> beacon, while curl's default User-Agent with <code>Accept: text/html</code> returns it. A Googlebot User-Agent asking for <code>text/html</code> gets it too. Checked against this site on 12 August 2026 and repeatable.</p>

<p><em>Correction:</em> an earlier version of this section attributed the difference to the User-Agent and called it a browser check. The observation was right — bare <code>curl</code> really does show no beacon — but the explanation was wrong, because the browser-like command it contrasted against set both headers at once and never varied them independently. Changing two variables and crediting one of them is the ordinary way to get a confident wrong answer, and it is worth saying so in a section about auditing your own site properly.</p>

<p><em>Why it:</em> it is free, it is cookieless, and enabling it required no code. <em>What it collects:</em> Cloudflare's product page describes aggregate page views, referrers, browsers, and countries without cookies or cross-site fingerprinting; the <a href="https://yangworks.dev/privacy">privacy policy</a> now names it explicitly. <em>What I did not choose:</em> Plausible and Fathom, both of which are still absent. <em>What I will not do with it:</em> quote its numbers. This site does not publish traffic, ranking, or revenue figures, and having a dashboard does not change that.</p>

<h2>What this stack is bad at</h2>

<ul>
  <li><strong>Collaboration.</strong> Everything here assumes one person who can read a diff. Handing this to a non-technical editor would require building a layer that does not exist.</li>
  <li><strong>Release safety.</strong> Push to <code>main</code> is production, and tests are not enforced in CI.</li>
  <li><strong>Scale of content.</strong> A full rebuild on every change is right at 75 files and wrong at 10,000.</li>
  <li><strong>Anything dynamic.</strong> No auth, no database, no forms. Those need a Worker script, which the platform supports and this project does not yet use.</li>
</ul>

<h2>Costs, honestly</h2>

<p>Every hosting, DNS, email, source-control, and build tool in this list is on a free tier and none of them is free because of a trial. The domain registration is the only recurring infrastructure cost, and the Claude Code subscription is the only recurring tooling cost. There is no affiliate relationship with any product named in this article, and no commission is earned from any of them.</p>

<h2>What "not on the list" means</h2>

<p>Vercel, Netlify, Astro, Tailwind, Plausible, Fathom, Ghost, Linear, Figma, and every other good tool that is not mentioned above are absent for exactly one reason: <strong>this project does not run them.</strong> That is not a verdict. A tool I have not operated is a tool I cannot review, and the fastest way to make a stack article useless is to pad it with things the author read about.</p>

<p>Yangworks is also building <strong>Upwork Toolkit</strong>, a browser extension currently in development. It is listed here for disclosure, not as part of the stack — it is a product this project makes, not a tool this project uses.</p>

<p>If you want the reasoning behind the hosting choice in more depth, start with <a href="https://yangworks.dev/guides/deploy-static-site-cloudflare-workers">the deploy guide</a>.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Moving Yangworks to Cloudflare Workers Static Assets</title>
    <link>https://yangworks.dev/field-notes/moving-yangworks-to-cloudflare-workers</link>
    <guid isPermaLink="true">https://yangworks.dev/field-notes/moving-yangworks-to-cloudflare-workers</guid>
    <pubDate>Fri, 07 Aug 2026 00:00:00 GMT</pubDate>
    <description>The decisive commit unified the build before the Cloudflare deployment changed.</description>
    <content:encoded><![CDATA[<p>The most important migration commit did not add Cloudflare configuration. It moved the landing page and handbook into the same source tree and taught one command to build both.</p>

<p>That order is the main lesson from the Yangworks move. Hosting changed later. First, the repository had to answer a simpler question: which files make up <code>yangworks.dev</code>?</p>

<p>The history supports an account of source ownership, URL decisions, and deployment cleanup. It does not contain a before-and-after benchmark, a traffic graph, or a downtime record. This is not evidence that Workers made the site faster or cheaper. It is evidence that one repository became responsible for one public URL space.</p>

<h2>Before: one site, several operational units</h2>

<p>Commit <code>2d4e14f</code> contained a root landing page and an architecture note. The note made a useful distinction: editorial content should live under the main domain, beginning with <code>/handbook</code>; product sites should remain separate, on their own subdomains or domains.</p>

<p>The deployed shape did not yet match that plan. The handbook source lived outside this repository. An older <code>dev-money-handbook</code> project was associated with <code>blog.yangworks.dev</code>, while the root site had its own direct-upload deployment. The landing page and the site’s substantial content were both static, but they did not share a build or release artifact.</p>

<p>Serving those files was not the hard part. Ownership was. The intended site map put the landing page at <code>/</code>, the handbook at <code>/handbook/</code>, and pages such as <code>/about</code> and <code>/privacy</code> alongside them. Keeping the handbook on another host preserved an old deployment boundary that no longer matched the public information architecture.</p>

<h2>The URLs were the constraint</h2>

<p>Chinese handbook pages already used short slugs such as <code>why</code>, <code>niche</code>, and <code>growth</code>. More descriptive slugs were tempting, but the source fragments contained hard-coded links such as <code>./niche.html</code>. Renaming a generated file without rewriting and checking every cross-link would trade a theoretical keyword improvement for real broken navigation.</p>

<p>So the migration moved the handbook under <code>/handbook</code> and kept its leaf slugs. That decision is not glamorous, but it prevented the hosting move from becoming a content-wide URL rewrite.</p>

<p>The site also stayed static. The landing page was plain HTML and CSS; the handbook already used HTML fragments and a small Node builder. There was no need to introduce Astro, a CMS, or a client router simply because two static outputs needed to become one.</p>

<p>Product sites remained outside the repository. Consolidation applied to the personal site and editorial content, not to every property carrying the Yangworks name.</p>

<h2>The build came before the host</h2>

<p>Commit <code>56a256b</code> did the structural work. It moved landing assets into <code>src/site</code>, brought the handbook into <code>src/handbook</code>, added <code>src/pages</code> for About and Privacy, and added static files such as <code>robots.txt</code> and the <code>ads.txt</code> placeholder. The landing page’s handbook link changed to <code>/handbook/</code>.</p>

<p>The root <code>build.js</code> then established the output contract. It recreated <code>public/</code>, copied the ordinary site directories into it, and invoked the handbook builder. <code>package.json</code> exposed the whole operation as <code>npm run build</code>.</p>

<p>At that point the repository had a platform-neutral answer to “what should production serve?” That made the eventual Cloudflare configuration small. It also created one place to inspect path ownership across the landing page, legal pages, static files, and handbook. Collision detection was not yet implemented, but there was finally one build in which it could be added.</p>

<p>Deleting <code>public/</code> on every build is useful and unforgiving. Stale pages cannot linger after their sources disappear. On the other hand, a missing copy step or content file removes the output immediately; there is no previous artifact quietly filling the gap. That is the right behavior for a deterministic build, provided the generated tree and links are tested.</p>

<h2>Workers configuration was the small commit</h2>

<p>Commit <code>4095194</code> added <code>wrangler.jsonc</code>. The current static-assets portion is short:</p>

<pre><code>{
  "name": "yangworks",
  "assets": {
    "directory": "./public",
    "not_found_handling": "404-page"
  }
}</code></pre>

<p>There is no Worker script. <code>assets.directory</code> points at the artifact the root build already knew how to make, and <code>not_found_handling</code> tells Workers to use the generated 404 page for misses.</p>

<p>That explicit setting is one practical difference between Pages and Workers Static Assets. Pages inferred SPA or custom-404 behavior from the output; Workers asks the project to choose with <code>not_found_handling</code>. For Yangworks, <code>"404-page"</code> is the relevant choice: there is no Worker code or SPA shell competing with the static routes.</p>

<p>Commit <code>6dc71e1</code> recorded the deployed arrangement: the GitHub repository connected to the <code>main</code> production branch, <code>npm run build</code> as the build command, and <code>npx wrangler deploy</code> as the deploy command. The same documentation recorded removal of the older handbook project and direct-upload root project.</p>

<p>Cloudflare recommends Workers for new projects and says Pages will remain supported, with new feature work focused on Workers. That made Workers a sensible destination while Yangworks was already changing its build and deployment. It does not make every stable Pages site an urgent migration.</p>

<h2>The cutover was not captured in Git</h2>

<p>The repository shows the end state, not the handoff. Cloudflare’s guide says to connect Workers Builds, disable Pages automatic deployments, validate the Worker, move production traffic, and only then delete the old project. Next time I would put those steps beside the custom-domain and route checks so two deployment systems cannot keep publishing by accident.</p>

<p><code>wrangler.jsonc</code> also cannot prove which public endpoints remain enabled. Preview URLs and the default <code>workers.dev</code> address are Cloudflare account settings, so they need separate dashboard or HTTP checks.</p>

<h2>Multilingual output expanded the artifact, not the deployment count</h2>

<p>Commit <code>0971851</code> expanded the same artifact to three language trees without moving the original Chinese pages from <code>/handbook/</code>. The builder emits only translations backed by source files; the canonical and <code>hreflang</code> mechanics are covered in <a href="https://yangworks.dev/guides/multilingual-static-site-hreflang-sitemap">the multilingual static-site guide</a>. The migration point is that languages expanded <code>public/</code>, not the number of deployments.</p>

<p>Commit <code>bd2dcb7</code> supplied a more visible maintenance payoff: navigation and design-token changes across the landing page, legal pages, handbook, and 404 page shipped in one release while each page family kept its own layout.</p>

<h2>The loose ends are more useful than a victory lap</h2>

<ul>
  <li><strong>Internal links still take an extra hop.</strong> Some handbook links use relative <code>.html</code> paths while canonicals and sitemap entries use extensionless URLs. Workers currently redirects the file form, but the source and preferred public form are not yet fully aligned.</li>
  <li><strong>The language roots are HTML stubs.</strong> <code>/en/</code> and <code>/ja/</code> use meta refresh, not HTTP 301 redirects. A browser reaching the intended page does not change the response semantics.</li>
  <li><strong>Git cannot describe the whole edge.</strong> The repository proves the build and Wrangler asset settings. It does not independently prove dashboard state, DNS, preview settings, or whether a <code>workers.dev</code> URL remains public.</li>
  <li><strong>A successful build is still not a complete test.</strong> A later test suite added assertions for the publication layer — canonical uniqueness, draft isolation from the sitemap, unresolvable body links, and sitemap URL counts. The handbook side has no equivalent coverage yet: required routes, language clusters, and real 404 statuses are still verified by hand.</li>
</ul>

<h2>What I would do first next time</h2>

<p>I would start with a route sheet: current host, target path, generating source, required redirect, and whether each fact lives in Git or provider configuration. I would build and test the replacement artifact while the old deployment still served traffic, adding required-route and internal-link checks with the root build rather than as later hardening.</p>

<p>Before cutover I would write down the expected status and final URL for <code>.html</code> links, language roots, and legacy hosts. I would also keep source consolidation, deployment replacement, and language expansion in separate changes. That commit order made this migration readable, and it is the part worth repeating.</p>

<p>The durable result is narrower than a hosting victory lap: one build owns the main site’s static output, and one Workers Static Assets project deploys it.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Multilingual SEO for a Static Site Without a Framework</title>
    <link>https://yangworks.dev/guides/multilingual-static-site-hreflang-sitemap</link>
    <guid isPermaLink="true">https://yangworks.dev/guides/multilingual-static-site-hreflang-sitemap</guid>
    <pubDate>Fri, 07 Aug 2026 00:00:00 GMT</pubDate>
    <description>How one availability model keeps localized pages, canonicals, language links, and sitemap alternates in sync.</description>
    <content:encoded><![CDATA[<p>The difficult part of <code>hreflang</code> is not writing an XML tag. It is deciding which translations are real, keeping their URLs stable, and making every output agree about that decision.</p>

<p>Yangworks has a Chinese, English, and Japanese handbook built with small Node.js scripts. There is no runtime locale detector and no framework plugin. Each language receives its own URL; a source file decides whether that version is published; the same availability data drives the HTML, language switcher, canonicals, and sitemap.</p>

<p>That shared model matters more than the choice of XML versus head tags. Google treats HTML, HTTP-header, and sitemap <code>hreflang</code> as equivalent methods and says there is no Search benefit to maintaining several at once. Yangworks uses the sitemap because it is the easiest place for this build to generate complete reciprocal clusters.</p>

<h2>Give every language a URL you can request directly</h2>

<pre><code>Chinese:  /handbook/              /handbook/niche
English:  /en/handbook/           /en/handbook/niche
Japanese: /ja/handbook/           /ja/handbook/niche</code></pre>

<p>These are separate HTML documents. The server does not return different languages from one URL based on a cookie, IP address, or <code>Accept-Language</code> header. A reader can copy an English URL and know that it will still be English when someone else opens it.</p>

<p>Google’s current guidance recommends separate URLs for language versions and lists country domains, subdomains, and subdirectories as workable structures. The choice of <code>/en/</code> here is operational, not a claim that subdirectories receive a ranking bonus. One host and one build are simpler for this site. A project with different regional ownership or infrastructure might choose subdomains or country domains for good reasons.</p>

<h2>Do not move the default language just to make the tree pretty</h2>

<p>Chinese already lived at <code>/handbook/...</code> when English and Japanese were added. Moving it to <code>/zh/handbook/...</code> would have created a second migration: redirects, rewritten links, changed canonicals, and a period in which old and new URLs competed for attention.</p>

<p>The build therefore keeps an asymmetric but stable mapping:</p>

<pre><code>LANGS = ['zh', 'en', 'ja']
DEFAULT_LANG = 'zh'

zh -&gt; /handbook/...
en -&gt; /en/handbook/...
ja -&gt; /ja/handbook/...</code></pre>

<p>The prefixes do not need to be symmetrical. What matters here is keeping one preferred URL for each Chinese page instead of publishing both <code>/handbook/niche</code> and <code>/zh/handbook/niche</code>.</p>

<h2>Publication starts with the file, not the language registry</h2>

<p>The page registry knows the shared slug, source filename, order, and translated navigation labels. It does not assume every translation is ready. Availability comes from the filesystem:</p>

<pre><code>const contentPath = (lang, file) =&gt;
  path.join(ROOT, 'content', lang, file);

const hasLang = (page, lang) =&gt;
  fs.existsSync(contentPath(lang, page.file));

const availableLangs = (page) =&gt;
  LANGS.filter((lang) =&gt; hasLang(page, lang));</code></pre>

<p>If <code>content/ja/04-validation.html</code> is missing, the build creates no Japanese validation page. The Japanese navigation omits it. The language selector on the Chinese and English versions does not offer Japanese. The sitemap does not include the URL or advertise it as an alternate.</p>

<p>The rule prevents the build from emitting <code>hreflang</code> for a translation it did not render. It says nothing about quality: a copied, unfinished, or awkward file still counts as published, so editorial review must happen before the file enters the language directory.</p>

<h2>Canonicalize each translation to itself</h2>

<p>The three versions of the same handbook chapter use three canonicals:</p>

<pre><code>https://yangworks.dev/handbook/niche
https://yangworks.dev/en/handbook/niche
https://yangworks.dev/ja/handbook/niche</code></pre>

<p>Each page uses its own language URL as canonical. Pointing the English and Japanese pages at Chinese would introduce a conflicting consolidation signal where the build intends three independently indexable translations.</p>

<p>Google recommends a self-referencing canonical on the canonical page and, when canonical and <code>hreflang</code> are combined, a canonical in the same language where possible. The Open Graph URL uses the same clean URL for consistency.</p>

<p>Canonical and <code>hreflang</code> are related but not interchangeable. Canonical chooses the preferred URL for a document. <code>hreflang</code> describes equivalent pages intended for different language or regional audiences.</p>

<h2>Build one reciprocal cluster, then reuse it</h2>

<p>For each handbook page, the root build finds the languages that exist and constructs one alternate list. When all three translations are present, the Yangworks list contains Chinese, English, Japanese, plus its chosen <code>x-default</code> fallback. <code>x-default</code> is useful here but is not required to form a valid <code>hreflang</code> cluster. The same list is written into the sitemap entry for every available version:</p>

<pre><code>&lt;url&gt;
  &lt;loc&gt;https://yangworks.dev/en/handbook/niche&lt;/loc&gt;
  &lt;xhtml:link rel="alternate" hreflang="zh-Hans"
    href="https://yangworks.dev/handbook/niche"/&gt;
  &lt;xhtml:link rel="alternate" hreflang="en"
    href="https://yangworks.dev/en/handbook/niche"/&gt;
  &lt;xhtml:link rel="alternate" hreflang="ja"
    href="https://yangworks.dev/ja/handbook/niche"/&gt;
  &lt;xhtml:link rel="alternate" hreflang="x-default"
    href="https://yangworks.dev/handbook/niche"/&gt;
&lt;/url&gt;</code></pre>

<p>Every sitemap entry repeats the same alternate set and includes its own URL, so the relationships are reciprocal by construction. Google may ignore a one-way relationship; one generated list is less error-prone than three hand-maintained copies.</p>

<p>The language codes are <code>zh-Hans</code>, <code>en</code>, and <code>ja</code>. The sitemap declares the XHTML namespace used by <code>xhtml:link</code>, uses absolute HTTPS URLs, and is advertised in <code>robots.txt</code>.</p>

<h2><code>x-default</code> is a fallback, not a boost</h2>

<p>Google describes <code>x-default</code> as the URL for users whose language or region does not match another entry. It can point to a language selector, a neutral page, or a chosen default version. Google has also said it may help URL discovery. None of that makes it a ranking shortcut.</p>

<p>Yangworks points <code>x-default</code> to the established Chinese version of each page. That does not make Chinese canonical for the English and Japanese pages, and it does not redirect a visitor.</p>

<p>There is one implementation debt here. The generator assumes the Chinese source exists whenever another language exists. That is true today, but it is not enforced. An English-only future page could produce an <code>x-default</code> URL that was never built. The build should eventually fail that state or choose an available fallback explicitly.</p>

<h2>Keep the language switcher even when the sitemap is correct</h2>

<p>Every page with more than one published version shows a visible language selector. The current language is text with <code>aria-current="true"</code>; the others are ordinary anchors to the clean localized URLs.</p>

<p>These links are not redundant. Google recommends visible links that let users change language, and the ordinary anchors give crawlers a path between versions. They also work without JavaScript.</p>

<p>The site does not redirect by IP address or <code>Accept-Language</code>. Google warns that inferred-language routing can hide versions from users and crawlers. Googlebot usually originates from US IP addresses and sends requests without an <code>Accept-Language</code> header, so a single adaptive URL is a poor discovery mechanism.</p>

<p>The document <code>lang</code> attribute is still set to <code>zh-Hans</code>, <code>en</code>, or <code>ja</code> for semantics and assistive technology. It is not a substitute for clearly translated visible content; Google says visible content is how it determines page language.</p>

<h2>Language roots and missing routes should tell the truth</h2>

<p>The content roots are <code>/en/handbook/</code> and <code>/ja/handbook/</code>. The build also writes <code>public/en/index.html</code> and <code>public/ja/index.html</code>. Workers serves those at <code>/en/</code> and <code>/ja/</code>, while the no-slash forms may first receive the default trailing-slash redirect.</p>

<p>Each generated language-root page contains a canonical to the handbook root, a zero-second meta refresh, a visible link, and <code>noindex</code>. It is an HTML forwarding page, not an HTTP 301. That distinction is visible in the response headers and should stay visible in the documentation.</p>

<p>Unknown handbook paths use Workers Static Assets with <code>not_found_handling: "404-page"</code>. The shared error page is marked <code>noindex</code> and links to the three handbook roots. A browser showing the right design is not enough; the response must still carry a 404 status.</p>

<h2>The current internal links are not fully clean</h2>

<p>Canonicals, sitemap locations, alternates, and the language selector use extensionless leaf URLs such as <code>/en/handbook/niche</code>. Handbook index URLs keep their trailing slash.</p>

<p>Some generated navigation and hand-authored cross-links still use <code>./niche.html</code>. Workers redirects them to the clean public form, so they work at the cost of an extra hop. Replacing those links is worthwhile, but it needs to be done across the generators and all three language trees rather than as a search-and-replace in one template.</p>

<p>The file-exists rule also cannot validate links inside an HTML fragment. A translated article can still link to a sibling translation that is absent. Complete current coverage hides that problem; future partial launches need an internal-link check in the build.</p>

<h2>Verify relationships, not just counts</h2>

<p>The handbook currently has 13 pages in three languages, so the build emits 39 handbook URLs into the sitemap and 39 self-referencing canonicals in the handbook output. The sitemap total is larger, because root-level and publication pages are appended to the same file. Counts like these are useful smoke tests, not proof that the clusters are correct.</p>

<p>The stronger check walks the generated output and asks:</p>

<ul>
  <li>Does every sitemap location resolve to a generated page?</li>
  <li>Does every localized entry list itself and every available alternate?</li>
  <li>Are the return links identical across the cluster?</li>
  <li>Does each page canonicalize inside its own language tree?</li>
  <li>Do language links omit versions that were not built?</li>
  <li>Does <code>x-default</code> resolve to a real fallback?</li>
  <li>Do missing routes return a real 404 and a <code>noindex</code> page?</li>
  <li>Do the <code>.html</code> links reach the advertised clean URL without loops?</li>
</ul>

<p>A framework would not remove these questions; it would only move the code that answers them. The durable part of the Yangworks implementation is that page availability, URL generation, canonicals, language links, and sitemap alternates all start from the same small set of facts.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Netlify Pricing for Freelance Developers (2026): Free Tier Limits, Overage Triggers, and When to Switch</title>
    <link>https://yangworks.dev/guides/netlify-pricing-for-freelance-developers</link>
    <guid isPermaLink="true">https://yangworks.dev/guides/netlify-pricing-for-freelance-developers</guid>
    <pubDate>Fri, 07 Aug 2026 00:00:00 GMT</pubDate>
    <description>On the credit model, twelve deploys cost more than three gigabytes of traffic — and at zero credits every project on the account is paused, not just the one that overran.</description>
    <content:encoded><![CDATA[<p>Netlify changed how it charges, and the change matters more to small projects than to large ones. If you are a freelance developer hosting client sites, a side project, or a publication, the number you need is not the monthly price. It is the point at which the free tier stops serving your site.</p>

<p><strong>What this article is:</strong> a reading of Netlify's official pricing and billing documentation, checked on 7 August 2026, with the arithmetic worked out. <strong>What it is not:</strong> a review. Yangworks has never run a production site on Netlify, has never received a Netlify bill, and measures nothing here. Every figure below is quoted from Netlify's documentation; every calculation is stated so you can redo it; every opinion is labelled.</p>

<h2>First: find out which pricing system you are on</h2>

<p>There are two, and they share no units.</p>

<p>Netlify's legacy pricing page states: <q>Starting on September 4, 2025, all new Netlify accounts will use the new credit-based pricing plans</q>, and <q>If you've created your Netlify account with a Free, Starter, or Pro plan before September 4, 2025, then your pricing plan is now considered a legacy pricing plan.</q></p>

<p>Legacy accounts are not being forced to move — the changelog says <q>You can stay on your legacy plan with no action required.</q> But the move is one-way: <q>You cannot revert to a Legacy plan from a Credit-based plan. This change is permanent.</q></p>

<p>So before you use any number from any pricing article, including this one, check <strong>Usage &amp; billing → Billing details</strong> in your Netlify dashboard.</p>

<h2>The credit model, at the documented rates</h2>

<p>Credit-based plans meter five things. Netlify's rates:</p>

<pre><code>Production deploy        15 credits each
Bandwidth                20 credits per GB
Web requests              2 credits per 10,000
Compute                  10 credits per GB-hour
AI inference            180 credits per $1 of model usage

Deploy previews           free
Branch deploys            free
Failed deploys            free
Rollbacks                 free
Form submissions          free and unlimited</code></pre>

<p>The plans: Free is <q>$0/month</q> with <q>300 credits/month with credit hard limit</q>; Personal is <q>$9/month</q> with 1,000 credits and auto recharge at 500 credits for $5; Pro <q>Starts at $20/month</q> with 3,000 credits, scaling to 20,000 credits at $126/month, with auto recharge at 1,500 credits for $10.</p>

<p>Two details freelancers should notice immediately. Free and Personal have no team seats — <q>Only 1 Team Owner</q> — so the first client who needs dashboard access puts you on Pro. And rollover credits only exist <q>on Pro plans with 5,000+ monthly credits</q>; on Free, Personal, and base Pro, unused credits expire.</p>

<h2>What 300 free credits actually buys</h2>

<p>This is just division, but it is the part that changes decisions.</p>

<p>Taken one line at a time, 300 credits is <strong>20 production deploys</strong>, or <strong>15 GB of bandwidth</strong>, or <strong>1.5 million web requests</strong>. You do not get all three; you get 300 credits total across all of them, for every project on the team.</p>

<p>A realistic small-site month:</p>

<pre><code>12 production deploys      12 x 15  = 180 credits
3 GB bandwidth              3 x 20  =  60 credits
400,000 web requests       40 x  2  =  80 credits
                                     -----------
                                       320 credits   over the 300 limit</code></pre>

<p><em>Author judgment:</em> the surprising term in that sum is the first one. On a content site with modest traffic, <strong>deploying is more expensive than being read</strong>. Twelve pushes to main cost more than three gigabytes of traffic. A freelancer who commits small and often — the normal way to work — burns the free allotment on the act of shipping, not on an audience.</p>

<p>The documented mitigations are real, though: deploy previews and branch deploys cost nothing, and failed deploys cost nothing. Batching work on a branch and merging once is a genuine cost lever rather than a trick.</p>

<h2>What happens at zero</h2>

<p>This is the most important paragraph in Netlify's billing documentation, and it is worth quoting in full:</p>

<blockquote>
<p>Once your monthly credit allotment is used up for all web projects on your team, all of your web projects (sites/apps) are paused and visitors to your web projects will find a <code>Site not available</code> page at each of your web project's URLs.</p>
</blockquote>

<p>The docs continue: while paused, projects <q>will not receive new web requests, web traffic, or form submissions and you cannot trigger new production deploys.</q> And the blast radius is the account, not the project: <q>if one site/web project exceeds its limits, all sites/projects on your account will be paused.</q></p>

<p>Warnings arrive at <q>50%, 75%, 90%, and 100% of your monthly credit allotment</q> by email and in-app. On Free there are exactly two ways out — <q>wait till the start of the next billing cycle or upgrade to a higher plan</q> — because credit packs cannot be bought on Free.</p>

<p><em>Author judgment:</em> this is a defensible design and a bad fit for client work. The Free tier's hard limit is a genuine feature if the thing you fear is a surprise invoice; Netlify says as much — <q>By using the Free plan, you can ensure you won't incur charges.</q> But "one project overruns, every client site on the account goes dark" is not a risk profile you can accept on someone else's behalf. If you host client work, either put each client on their own team or do not use the free tier for production.</p>

<h2>If you are on a legacy plan, the numbers are different</h2>

<p>Legacy Free and Starter: <q>100 GB</q> bandwidth per month and <q>300 minutes</q> of build time, <q>1</q> concurrent build, and <q>Limited to 1 free member, with free &amp; unlimited Git Contributors</q>. On legacy Free those are hard limits; on Starter, exceeding them costs <q>$55 per 100GB</q> of bandwidth and <q>$7 per 500</q> build minutes. Legacy Pro raises the allowances to 1 TB and 25,000 minutes at the same overage rates, with seats at <q>$19 /month per member</q> and extra concurrent builds at <q>$40 each</q>.</p>

<p>Legacy free-tier exhaustion behaves similarly to the credit model: <q>If the build limit is reached, your sites will still be served to visitors, but new builds will be disabled for all sites on your account</q>, while for other metered features <q>new builds will be disabled and all sites on your account will be paused.</q> One footnote with teeth: <q>deleting the site with overages will not restore the account.</q></p>

<h2>The factual contrast with Cloudflare</h2>

<p>Yangworks runs on Cloudflare Workers Static Assets, so treat this section as an interested party quoting the other side's documentation. Both columns are official statements, checked 7 August 2026 — not benchmarks.</p>

<pre><code>                       Netlify Free (credit)      Cloudflare Workers Free
Price                  $0                         $0
Static asset requests  2 credits / 10k requests   "free and unlimited"
Bandwidth              20 credits per GB          no separate asset charge
Production deploys     15 credits each            not metered
Build minutes          (counted as compute)       3,000 per month
Concurrent builds      1                          1
Team seats             1 Team Owner               n/a for static assets
At the limit           all projects paused        documented per-feature</code></pre>

<p>Cloudflare's pricing page states that <q>Requests to static assets are free and unlimited</q> and that storing them carries <q>no additional cost</q>; the Workers Free plan allows 100,000 Worker requests per day, and Workers Builds includes <q>3,000 per month</q> build minutes on Free against <q>6,000 per month (then, +$0.005 per minute)</q> on Paid.</p>

<p><em>Author judgment, and the honest asymmetry:</em> for a purely static site with no server-side code, Cloudflare's free tier has no deploy meter and no request meter, which removes exactly the two costs that dominate the Netlify arithmetic above. That is a real structural difference, not a marketing one. It is also a narrow comparison — it says nothing about build ergonomics, preview workflow, or the framework integrations Netlify is genuinely good at, and I have not run a Netlify project to weigh those.</p>

<h2>When Netlify's pricing is the right call</h2>

<p><em>Everything in this section is judgment from the documentation, not from operating experience.</em></p>

<p><strong>The free tier fits</strong> a personal site or a demo you deploy rarely, where a hard stop is preferable to a bill and the site going dark for a few days is survivable.</p>

<p><strong>Personal at $9</strong> is the tier for a solo developer shipping regularly: 1,000 credits is roughly 66 production deploys before anything else, and auto recharge exists as a backstop — though note it is off by default and only Team Owners can enable it.</p>

<p><strong>Pro at $20</strong> is the real freelancer tier, and not because of the credits. It is the first plan with <q>Unlimited</q> seats, audit logs, and 30-day analytics retention. Compare that with legacy Pro's <q>$19 /month per member</q>: for a two-or-three-person collaboration the credit-based Pro plan is straightforwardly cheaper.</p>

<p><strong>Switch away</strong> when your workload is mostly static delivery with frequent deploys, since that is the shape the credit model prices least favourably. <strong>Stay</strong> when you are using the parts Netlify meters generously — free deploy previews, free branch deploys, unlimited forms — or when your framework's Netlify adapter is the reason the project ships at all.</p>

<h2>What this article does not establish</h2>

<p>No Netlify site was operated, deployed, or billed in the writing of this guide. There is no measured build time, no measured bandwidth, no observed invoice, no support-quality assessment, and no reliability claim. Enterprise pricing is excluded because it is not published. Every plan figure comes from Netlify's public documentation on the date shown and pricing changes — the credit model itself is less than a year old and was revised in April 2026 — so re-check the source pages before making a decision. No affiliate relationship exists with Netlify or Cloudflare, and nothing here earns a commission.</p>

<p>For how the Cloudflare side of that comparison actually works in practice, see <a href="https://yangworks.dev/reviews/cloudflare-workers-static-assets-review">the Workers Static Assets review</a> and <a href="https://yangworks.dev/guides/deploy-static-site-cloudflare-workers">the deploy guide</a>.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Printify Review for Developers and Indie Makers (2026): Marketplace POD, Pricing, and When Printful Wins</title>
    <link>https://yangworks.dev/guides/printify-for-developers-indie-makers</link>
    <guid isPermaLink="true">https://yangworks.dev/guides/printify-for-developers-indie-makers</guid>
    <pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate>
    <description>Printify is a provider marketplace: strong on cost and catalog, uneven on quality unless you sample. Free first; Premium only when the discount math works.</description>
    <content:encoded><![CDATA[<p><strong>Review verdict:</strong> Printify is best understood as a <em>print-provider marketplace</em>, not a single factory. That model is why base costs and catalog depth are competitive—and why quality, shipping rates, and lead times vary by provider. For developers and indie makers testing merch with zero inventory, start on Free, treat provider selection as part of the product, and only pay for Premium after order volume makes the discount math work. Prefer a more consistent in-house network (commonly Printful) if brand packaging control and uniform quality matter more than unit cost.</p>

<aside class="publication-callout" role="note">
  <p><strong>Affiliate disclosure.</strong> This page includes a Printify referral link (<code>try.printify.com/iosrsfk051cj</code>). If you sign up through it, Yangworks may earn a commission at no extra cost to you. Claims are labeled as <em>official</em>, <em>third-party</em>, or <em>author judgment</em>. Yangworks did <strong>not</strong> operate a live Printify store or measure sales for this article.</p>
</aside>

<h2>How this review was researched</h2>

<p>Checked on <time datetime="2026-07-27">27 July 2026</time>:</p>

<ul>
  <li><strong>Official:</strong> Printify how-it-works, pricing, integrations, API, merchant protection pages.</li>
  <li><strong>Platform context:</strong> Shopify’s Printful vs Printify guide; Printful’s public merger announcement (Nov 2024).</li>
  <li><strong>Independent / seller-facing reviews:</strong> Merch Titans Printify Review 2026, Print on Demand Business comparison, Style Factory comparison, CostBench hidden-cost notes, Trustpilot aggregate score pages.</li>
  <li><strong>Community signal:</strong> Public Reddit threads under r/Printify and r/printondemand (quality/shipping variance is a recurring theme; individual posts are not treated as statistics).</li>
  <li><strong>Not available for this pass:</strong> Live X/Twitter search via Grok returned upstream 403 errors on 27 July 2026, so recent posts on X were not cited.</li>
</ul>

<p><strong>Out of scope:</strong> Yangworks revenue, conversion, ad ROAS, or multi-provider lab samples. Where third parties report sample results or break-even math, that is their claim—not ours.</p>

<h2>Who this is for</h2>

<ul>
  <li><strong>Indie makers</strong> adding merch to a software brand, newsletter, or community without holding stock.</li>
  <li><strong>Freelance developers</strong> shipping Shopify/Woo/Etsy stores for clients who need POD fulfillment.</li>
  <li><strong>Technical founders</strong> who may later use the Printify API for bulk publish or custom storefronts.</li>
</ul>

<p>Skip marketplace POD if you need proprietary materials, contracted factory QC, or sub-commodity unit costs at high volume.</p>

<h2>What Printify actually is</h2>

<p>Printify routes designs to a network of independent Print Providers that print, pack, and ship after a customer buys. You do not buy inventory up front. Official pages (checked 27 July 2026) emphasize free signup, a large catalog (public copy ranges from “1,300+” on some surfaces to “2,000+” on how-it-works/pricing), 90+ providers, and 140+ facilities.</p>

<p>Third-party reviews repeatedly use the same framing: Printify is closer to a marketplace (or “Uber for printing”) than to a single manufacturer. That distinction is the whole review:</p>

<ul>
  <li><strong>Strength:</strong> provider competition → lower base prices and wider SKU choice.</li>
  <li><strong>Weakness:</strong> quality and shipping are provider-dependent; a sample from one shop does not certify the whole network.</li>
</ul>

<p>Shopify’s comparison guide states the same quality point explicitly: different partners may use different machines or inks, so a sample reflects one facility—not “Printify quality” as a single standard.</p>

<h2>Company context: Printful merger (still two products)</h2>

<p>In November 2024, Printful and Printify announced a merger as equal partners. The public announcement says both brands continue for the foreseeable future, with sellers expected to see new offerings on both platforms. Wikipedia also notes the 2024 merger. Shopify’s 2026 guide still treats them as separate products with different operating models.</p>

<p><strong>Author judgment:</strong> for buying decisions in 2026, compare workflows and unit economics as two products—not as “already the same app.” Re-check branding and feature convergence before you assume shared inventory or shared QC.</p>

<h2>Workflow (official four steps)</h2>

<ol>
  <li><strong>Select</strong> a blank from the catalog; compare base cost, location, and provider ratings.</li>
  <li><strong>Create</strong> art in the free Product Creator (or upload print-ready files). Shutterstock is marketed as pay-as-you-sell.</li>
  <li><strong>Publish</strong> to a channel (Etsy, Shopify, WooCommerce, eBay, Wix, TikTok Shop in supported regions, Amazon, etc.) or a Pop-Up Store.</li>
  <li><strong>Fulfill</strong> after checkout: provider produces and ships; you keep retail minus costs and channel fees.</li>
</ol>

<pre><code>Storefront checkout (Shopify / Etsy / Woo / custom / Pop-Up)
        |
        v
Printify order routing
        |
        v
Chosen Print Provider (print → pack → ship)
        |
        v
Buyer receives item; your margin = retail − base − shipping − fees − ads</code></pre>

<h2>Pricing: Free, Premium, Enterprise</h2>

<p>From <a href="https://printify.com/pricing/" rel="noreferrer">printify.com/pricing</a> on 27 July 2026:</p>

<table>
  <thead>
    <tr>
      <th>Plan</th>
      <th>Official list price</th>
      <th>Stores</th>
      <th>Official highlights</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Free</td>
      <td>$0/month</td>
      <td>5</td>
      <td>Unlimited designs; core creator + integrations</td>
    </tr>
    <tr>
      <td>Premium</td>
      <td>From $39/month, or from $24.99/month billed yearly</td>
      <td>10</td>
      <td>Up to 33% discount on new products / branding features as marketed; Printify Connect; mentorship packaging as marketed</td>
    </tr>
    <tr>
      <td>Enterprise</td>
      <td>Custom</td>
      <td>Unlimited</td>
      <td>Additional discounts, dedicated manager, custom API packaging as marketed</td>
    </tr>
  </tbody>
</table>

<p>Official FAQ notes: platform use can be free; Printify earns on fulfillment when you sell; Premium is non-refundable if you miss cancellation before renewal. Official profit example: tee base <strong>$8.77</strong> sold at <strong>$19.99</strong> leaves “over $11” <em>before</em> shipping, taxes, and storefront fees.</p>

<p><strong>Third-party break-even framing (not Yangworks math):</strong> several 2026 seller guides argue Premium’s product discount often lands around roughly the mid-teens to low-twenties of orders per month before the subscription pays for itself—exact SKUs, shipping, and ad costs change that number. CostBench and similar pages also warn that free-plan base costs can be less competitive than Premium-discounted peers, and that multi-provider carts can stack shipping fees.</p>

<p><strong>Author rule:</strong> stay Free until you have repeat orders and a spreadsheet that includes shipping + channel fees. Upgrade only when measured discounts beat the fee on <em>your</em> SKUs—the same advice Printify’s pricing page implies (“upgrade only when the discounts outearn the fee”).</p>

<h2>What third-party reviews and sellers consistently report</h2>

<h3>Pros (recurring across sources)</h3>

<ul>
  <li><strong>Lower base costs / stronger margins</strong> vs many in-house POD options, especially with Premium (Shopify, Print on Demand Business, Merch Titans, Style Factory comparisons).</li>
  <li><strong>Very large catalog</strong> for niche testing (apparel, home, accessories, AOP, etc.).</li>
  <li><strong>Multi-channel integrations</strong> and a free Pop-Up Store path for early validation.</li>
  <li><strong>Provider redundancy:</strong> if one shop is slow or OOS, another may cover the blank.</li>
  <li><strong>Public reputation aggregate:</strong> Trustpilot pages for printify.com show a high average (commonly cited around the mid-4s / “Excellent” band in 2026 snapshots; UK page snippets referenced thousands of reviews). Aggregates mix happy path and support tickets—read recent 1–2★ threads, not only the headline score.</li>
</ul>

<h3>Cons / failure modes (recurring)</h3>

<ul>
  <li><strong>Quality variance by provider</strong> is the #1 structural risk (Shopify, Merch Titans, Reddit r/Printify / r/printondemand themes: mixed “great print” vs “worse than Printful / off-center / slow ship”).</li>
  <li><strong>Shipping math is hard on multi-item orders</strong> when items route to different providers (flat-rate Etsy profiles can undercharge; CostBench and POD comparison writers flag stacked shipping).</li>
  <li><strong>Branding depth is thinner</strong> than Printful’s in-house packaging story on many third-party comparisons (neck labels/inserts only with some providers).</li>
  <li><strong>Returns are typically defect-oriented</strong> via Merchant Protection / support—not casual size exchanges (seller guides and Printify protection copy).</li>
  <li><strong>Operational incidents happen:</strong> independent seller blogs document carrier changes, integration outages, and product-removal scares. Treat these as risk scenarios to monitor—not as universal permanent state. Keep mockups and design masters offline; do not treat the dashboard as your only backup.</li>
</ul>

<h3>Sample evidence others published</h3>

<p>TrakSource’s 2026 write-up describes ordering an 11oz accent mug sample via a rated provider, receiving it in about five days, and reporting sharp print at roughly <strong>~$6.50</strong> including shipping—as <em>their</em> test, not ours. Merch Titans recommends ordering the same design from multiple providers before locking a hero SKU. That “sample rule” is the highest-leverage practice in this category.</p>

<h2>Printify vs Printful (decision table)</h2>

<p>Both remain relevant after the 2024 merger announcement. Synthesis of official models + Shopify / POD comparison coverage:</p>

<table>
  <thead>
    <tr>
      <th>Dimension</th>
      <th>Printify</th>
      <th>Printful (typical contrast)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Operating model</td>
      <td>Network of third-party providers</td>
      <td>In-house facilities / tighter process control</td>
    </tr>
    <tr>
      <td>Base cost</td>
      <td>Usually lower / more competitive</td>
      <td>Usually higher, simpler pricing</td>
    </tr>
    <tr>
      <td>Quality</td>
      <td>Variable by provider; sample ≠ network</td>
      <td>More consistent across sites (as reported)</td>
    </tr>
    <tr>
      <td>Catalog breadth</td>
      <td>Very large</td>
      <td>Smaller, more curated</td>
    </tr>
    <tr>
      <td>Shipping predictability</td>
      <td>Provider-dependent; multi-SKU risk</td>
      <td>More standardized rate tools</td>
    </tr>
    <tr>
      <td>Branding / packaging</td>
      <td>Limited / provider-specific</td>
      <td>Stronger custom packaging story</td>
    </tr>
    <tr>
      <td>Best default for</td>
      <td>Margin testing, niche SKUs, multi-channel experiments</td>
      <td>Brand-sensitive stores, consistent QC, simpler ops</td>
    </tr>
  </tbody>
</table>

<p><strong>Author judgment for Yangworks readers:</strong> use Printify to validate demand and margin on a Free account; graduate hero products only after samples. If client brand risk is high (agency merch, premium apparel), default to the more consistent ops model—or run both with different SKUs.</p>

<h2>Integrations and developer notes</h2>

<p>Official integrations cover major marketplaces and builders (Etsy, Shopify, WooCommerce, eBay, Wix, Squarespace, BigCommerce, PrestaShop, Amazon, TikTok Shop where supported). API docs live at <a href="https://developers.printify.com/#overview" rel="noreferrer">developers.printify.com</a>; tokens are generated in-account. Printify does not build your custom integration for you.</p>

<p>Practical pairing:</p>

<ul>
  <li><strong>Validate:</strong> Pop-Up Store or Etsy + few designs.</li>
  <li><strong>Own brand:</strong> Shopify or Woo when you need domain, email, and SEO.</li>
  <li><strong>Automate later:</strong> API bulk publish / multi-store ops only after you understand manual order failure modes.</li>
</ul>

<h2>Launch checklist (developer-friendly)</h2>

<ol>
  <li>Open Free; connect <strong>one</strong> channel only.</li>
  <li>Pick one niche and ≤3 products.</li>
  <li>Filter providers by rating + location near buyers.</li>
  <li>Order samples of the same design from 2–3 providers before ads.</li>
  <li>Price with a full landed-cost sheet: base + shipping + channel fee + payment fee + expected ad spend.</li>
  <li>Write shipping copy that includes production time, not only carrier transit.</li>
  <li>Keep master artwork and mockups outside Printify.</li>
  <li>Revisit Premium only after 2–4 weeks of real order data.</li>
  <li>Watch provider score changes; re-sample after major provider or carrier shifts.</li>
</ol>

<h2>Who should use Printify</h2>

<ul>
  <li>You optimize for catalog breadth and base cost.</li>
  <li>You will invest time in provider selection and samples.</li>
  <li>You already have (or can build) demand—POD does not create traffic.</li>
  <li>You may want API automation later.</li>
</ul>

<h2>Who should skip or wait</h2>

<ul>
  <li>You need uniform QC and branded unboxing on every order without managing providers.</li>
  <li>Your unit economics only work with contracted wholesale manufacturing.</li>
  <li>You expected passive income without creative, ads, or support work.</li>
  <li>You cannot tolerate variable shipping on multi-item carts.</li>
</ul>

<h2>How to start</h2>

<p>If the checklist fits, start Free, sample hard, and keep Premium optional until the discount is earned.</p>

<p>
  <a class="publication-cta" href="https://try.printify.com/iosrsfk051cj" rel="sponsored nofollow">Start with Printify (free account)</a>
  <span class="publication-cta-note">Referral link · Official Free plan available · Re-check pricing and provider details on launch day</span>
</p>

<h2>Bottom line</h2>

<p>Printify is a strong <strong>low-inventory commerce layer</strong> for makers who treat providers as suppliers to qualify—not as a black-box “print button.” Public docs, Trustpilot-scale feedback, and 2026 comparison coverage agree on the trade: better price/variety potential in exchange for more operational judgment. After the Printful merger announcement, both brands still matter as different products. Yangworks’ position: Free + samples first; Premium only with math; walk to in-house POD or a factory when brand risk outweighs margin.</p>
]]></content:encoded>
  </item>
</channel>
</rss>
