<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Thedolceway</title>
    <description>The latest articles on DEV Community by Thedolceway (@thedolceway).</description>
    <link>https://dev.to/thedolceway</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3733349%2F379a6b33-09b6-4045-beca-0860eddea1d3.jpg</url>
      <title>DEV Community: Thedolceway</title>
      <link>https://dev.to/thedolceway</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thedolceway"/>
    <language>en</language>
    <item>
      <title>I removed the LLM call and replaced it with 200 lines of template code</title>
      <dc:creator>Thedolceway</dc:creator>
      <pubDate>Tue, 25 Aug 2026 12:11:32 +0000</pubDate>
      <link>https://dev.to/thedolceway/i-removed-the-llm-call-and-replaced-it-with-200-lines-of-template-code-2lh0</link>
      <guid>https://dev.to/thedolceway/i-removed-the-llm-call-and-replaced-it-with-200-lines-of-template-code-2lh0</guid>
      <description>&lt;p&gt;The feature was a letter generator. Somebody fills in a few fields and gets a finished&lt;br&gt;
letter of recommendation, resignation letter or notice letter, in plain text, ready to&lt;br&gt;
paste into an email.&lt;/p&gt;

&lt;p&gt;The obvious build is a prompt and a model call. I wrote the deterministic version instead:&lt;br&gt;
a pure function, about two hundred lines, no network, no key, no tokens. I want to lay out&lt;br&gt;
the reasoning, because "just call a model" is the default now and the default is not always&lt;br&gt;
right.&lt;/p&gt;
&lt;h2&gt;
  
  
  The three reasons, in order of weight
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The output is short and the shape is fixed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A recommendation letter is a date block, a greeting, three or four paragraphs, a sign off&lt;br&gt;
and a name. There is no structural variation to discover. Generation is valuable when the&lt;br&gt;
space of good outputs is large and you cannot enumerate it. Here the space is small enough&lt;br&gt;
to write down, and once you have written it down the model is doing an expensive&lt;br&gt;
approximation of a &lt;code&gt;switch&lt;/code&gt; statement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. It is a legal-adjacent document.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not legal advice, but it goes into an employment record. A resignation letter that invents&lt;br&gt;
a notice period, or a reference that invents a fact about a person, is a real problem for&lt;br&gt;
the person who sent it. Templates cannot hallucinate. Everything specific in the output&lt;br&gt;
either came from a form field or is a sentence I wrote and can be held to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Zero marginal cost changes what the product can be.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the one that actually decided it. A model call costs money per use, and anything&lt;br&gt;
that costs money per use needs an account, a rate limit and eventually a card. A pure&lt;br&gt;
function costs nothing, so the tool can stay open with no signup, forever, without a&lt;br&gt;
business case. That is a product decision expressed as an architecture decision, and it&lt;br&gt;
only works if the code path is free.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the code looks like
&lt;/h2&gt;

&lt;p&gt;The whole engine is one exported function over one input type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LetterKind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resignation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recommendation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LetterTone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;formal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;brief&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateLetter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LetterInput&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tone is not a prompt instruction, it is a dimension of the data. Two tiny functions carry&lt;br&gt;
most of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LetterInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LetterTone&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipientName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tone&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Dear Sir or Madam,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tone&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Hi &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Dear &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;signOff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LetterTone&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tone&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;With thanks,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tone&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;brief&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Regards,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sincerely,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bodies are arrays of paragraphs, assembled conditionally. A recommendation body opens&lt;br&gt;
differently depending on whether the writer told us how they know the subject:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;paras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;rel&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`I am pleased to recommend &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;who&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; for the role of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, which gave me a direct view of how they work.`&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`I am pleased to recommend &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;who&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; for the role of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, based on my direct experience of working with them at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;company&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That ternary is the whole trick, repeated maybe fifteen times. It is not clever. Clever was&lt;br&gt;
never the requirement.&lt;/p&gt;
&lt;h2&gt;
  
  
  What you get for free that a model call does not give you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Determinism, which means testability.&lt;/strong&gt; Same input, same bytes out. A snapshot test over&lt;br&gt;
the full cross product of three kinds and three tones is nine assertions and runs in&lt;br&gt;
milliseconds. Testing a model call means either mocking it, in which case you are testing&lt;br&gt;
your mock, or asserting fuzzy properties of real output and paying for the privilege on&lt;br&gt;
every CI run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offline and instant.&lt;/strong&gt; No spinner, no failure state, no retry logic, no timeout, no&lt;br&gt;
"the service is busy" copy to write and translate. The letter updates as the user types&lt;br&gt;
because rendering it is a function call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A real validator instead of an implicit one.&lt;/strong&gt; With a model you tend to send whatever you&lt;br&gt;
have and hope. With a template you have to decide what is required, which forces the&lt;br&gt;
product question into the open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;missingFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LetterInput&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;senderName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Company&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recommendation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subjectName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Who you are recommending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Their role&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your role&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastDay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Last working day&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the required set differs by kind. A recommendation has no last working day. A&lt;br&gt;
resignation has no subject. A single prompt would have blurred those together and produced&lt;br&gt;
something plausible for a missing field, which is worse than refusing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Localisation is mechanical.&lt;/strong&gt; Nine strings per tone, translated once, correct forever. The&lt;br&gt;
same feature backed by a model needs the prompt tuned per language and the output checked&lt;br&gt;
per language by someone who reads it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the approach genuinely loses
&lt;/h2&gt;

&lt;p&gt;I am not going to pretend this scales to everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It cannot say the specific thing.&lt;/strong&gt; The generated paragraphs are competent and generic,&lt;br&gt;
and generic is exactly the part of a reference letter that carries no weight. A hiring&lt;br&gt;
manager skims "demonstrated consistent judgement" and stops at "she rewrote our billing&lt;br&gt;
reconciliation and the month-end close went from four days to one".&lt;/p&gt;

&lt;p&gt;So the engine has a &lt;code&gt;highlight&lt;/code&gt; field, free text, dropped verbatim into the middle of the&lt;br&gt;
letter. That is not a limitation I worked around, it is the correct division of labour. The&lt;br&gt;
tool writes the scaffolding nobody reads. The human writes the one sentence that does the&lt;br&gt;
work. A model would have written a fluent guess at that sentence, and a fluent guess about&lt;br&gt;
a real person is precisely the thing you do not want in a reference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It cannot rewrite arbitrary prose.&lt;/strong&gt; Paste in three rambling paragraphs and ask for them&lt;br&gt;
tightened, and templates have nothing to offer. That is a genuine generation task and I&lt;br&gt;
would use a model for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding a kind costs a function.&lt;/strong&gt; A new letter type means new code, not a new prompt&lt;br&gt;
string. For three kinds that is fine. For thirty I would be rethinking it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The heuristic I took away
&lt;/h2&gt;

&lt;p&gt;Reach for generation when the output space is large, variable, and you cannot enumerate the&lt;br&gt;
good answers. Reach for templates when the output space is small, the shape is fixed, and&lt;br&gt;
being wrong is expensive.&lt;/p&gt;

&lt;p&gt;Short formal documents sit squarely in the second category, and the industry keeps building&lt;br&gt;
them with the first tool because the first tool is what everyone is holding.&lt;/p&gt;

&lt;p&gt;The engine described here runs the&lt;br&gt;
&lt;a href="https://cvbooster.ai/recommendation-letter-generator" rel="noopener noreferrer"&gt;letter of recommendation template&lt;/a&gt;&lt;br&gt;
tool on the resume builder I maintain. No account, no card, no model call, and the plain&lt;br&gt;
text output is free to copy. It renders in whatever time a string concatenation takes,&lt;br&gt;
which is the entire point.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
    <item>
      <title>I audited 99 resume templates for contrast. Nine rendered the candidate's name invisible.</title>
      <dc:creator>Thedolceway</dc:creator>
      <pubDate>Tue, 25 Aug 2026 11:16:23 +0000</pubDate>
      <link>https://dev.to/thedolceway/i-audited-99-resume-templates-for-contrast-nine-rendered-the-candidates-name-invisible-hom</link>
      <guid>https://dev.to/thedolceway/i-audited-99-resume-templates-for-contrast-nine-rendered-the-candidates-name-invisible-hom</guid>
      <description>&lt;p&gt;I run a resume builder with 99 templates. A user said the designs looked "basic", so I&lt;br&gt;
wrote a script to measure them instead of arguing about taste. The script did not find a&lt;br&gt;
taste problem. It found that nine templates rendered the candidate's name at a contrast&lt;br&gt;
ratio of about 1.0 against its background.&lt;/p&gt;

&lt;p&gt;Contrast 1.0 means the text and the background are the same colour. Two of them were&lt;br&gt;
literally white on white. The name — the single most important string on the document —&lt;br&gt;
was invisible.&lt;/p&gt;

&lt;p&gt;Nobody had reported it, because nobody could see it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why nobody noticed
&lt;/h2&gt;

&lt;p&gt;The templates are generated. A base design defines a palette, and variants override parts&lt;br&gt;
of it. That is a completely normal pattern and it is where the bug lives.&lt;/p&gt;

&lt;p&gt;The override was a shallow merge of a &lt;code&gt;colors&lt;/code&gt; object. So a variant could set a pale&lt;br&gt;
&lt;code&gt;headerBackground&lt;/code&gt; while silently inheriting &lt;code&gt;headerText&lt;/code&gt; from a dark base design. Both&lt;br&gt;
values are individually reasonable. The combination is unreadable, and nothing in the type&lt;br&gt;
system objects, because both are valid colour strings.&lt;/p&gt;

&lt;p&gt;The failure only exists in the &lt;em&gt;pair&lt;/em&gt;, and nothing was checking pairs.&lt;/p&gt;
&lt;h2&gt;
  
  
  Measuring instead of eyeballing
&lt;/h2&gt;

&lt;p&gt;Contrast ratio is defined in WCAG and is fully mechanical. Relative luminance per channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.03928&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;12.92&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.055&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1.055&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;luminance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="mf"&gt;0.2126&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;chan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.7152&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;chan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.0722&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;chan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contrast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;luminance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;luminance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The range is 1 to 21. AA wants 4.5 for body text, 3.0 for large text. Anything reporting&lt;br&gt;
close to 1.0 is not "low contrast", it is &lt;em&gt;the same colour&lt;/em&gt;, and it should be treated as a&lt;br&gt;
crash rather than a style nit.&lt;/p&gt;

&lt;p&gt;Rendering each template with sample data and walking the DOM took an afternoon. The result:&lt;br&gt;
nine invisible names, and — the part I did not expect — &lt;strong&gt;92 failing nodes in sidebars&lt;/strong&gt;,&lt;br&gt;
including 14 templates rendering certification names at exactly 1.00.&lt;/p&gt;

&lt;p&gt;Sidebars were worse than main content because dark-sidebar designs are the ones most likely&lt;br&gt;
to inherit a light-background text colour. The pattern that looks best is the pattern most&lt;br&gt;
exposed to the bug.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two things the audit taught me that I did not expect
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The inherited claims were mostly wrong.&lt;/strong&gt; I started from a previous audit's notes that&lt;br&gt;
said 69 templates had five or more failing nodes and that one template rendered every&lt;br&gt;
heading at 1.00. Measured: the worst had four, and that template's headings came in at&lt;br&gt;
11.83, comfortably passing. The described root cause was a function that no longer existed&lt;br&gt;
in the codebase — only a comment saying it used to.&lt;/p&gt;

&lt;p&gt;If I had trusted the notes I would have "fixed" three things that were not broken and&lt;br&gt;
missed the 92 that were. Re-measure before you refactor, especially when the notes sound&lt;br&gt;
confident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My type checker was checking nothing.&lt;/strong&gt; While wiring the audit into CI I ran&lt;br&gt;
&lt;code&gt;tsc --noEmit&lt;/code&gt; and it passed instantly on a codebase I knew had errors. The root&lt;br&gt;
&lt;code&gt;tsconfig.json&lt;/code&gt; had &lt;code&gt;"files": []&lt;/code&gt;. It was type checking an empty set, and had been for&lt;br&gt;
a long time, exiting 0 the whole way. The real command was&lt;br&gt;
&lt;code&gt;tsc -p tsconfig.app.json --noEmit&lt;/code&gt;, which reported a 65-error backlog nobody knew about.&lt;/p&gt;

&lt;p&gt;A green check on an empty input looks exactly like a green check on a clean codebase.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix that did not break the designs
&lt;/h2&gt;

&lt;p&gt;The tempting fix is to force every text colour to something safe. That flattens every&lt;br&gt;
design at once, and the designs were mostly fine.&lt;/p&gt;

&lt;p&gt;Instead I resolved colours at render time and substituted &lt;strong&gt;only when a pair actually&lt;br&gt;
fails&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallbackDark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallbackLight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;contrast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fg&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;4.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// untouched&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;luminance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;fallbackDark&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fallbackLight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every passing node stays byte-identical. Only the broken pairs change, and they change to&lt;br&gt;
the correct end of the scale for their background. Final audit: &lt;strong&gt;0 failing nodes across&lt;br&gt;
99 templates&lt;/strong&gt;, with no design regressions, because nothing that worked was touched.&lt;/p&gt;

&lt;p&gt;The audit script stayed in the repo. A rule that is not enforced is a rule that comes back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would take to any generated-design system
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shallow-merging a palette is a correctness bug waiting to happen.&lt;/strong&gt; Validate the
resulting pairs, not the individual values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contrast is measurable, so measure it.&lt;/strong&gt; "Looks fine to me" is not a test, and the
people who cannot read it are not in the room.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distrust audit notes, including your own.&lt;/strong&gt; Re-measure. Confident prose about a
function that no longer exists is a real thing that happens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify your verifier.&lt;/strong&gt; Feed it a known-bad input and confirm it fails. An empty
&lt;code&gt;files&lt;/code&gt; array will pass every check you write.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The templates this came from are at &lt;a href="https://cvbooster.ai/templates" rel="noopener noreferrer"&gt;CVBooster&lt;/a&gt; — free to&lt;br&gt;
browse, no account, and now with every one of them actually legible.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>a11y</category>
    </item>
    <item>
      <title>Building a Minimalist Habit Tracker — Why I Stopped Using Streaks and Forest</title>
      <dc:creator>Thedolceway</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:05:56 +0000</pubDate>
      <link>https://dev.to/thedolceway/building-a-minimalist-habit-tracker-why-i-stopped-using-streaks-and-forest-272k</link>
      <guid>https://dev.to/thedolceway/building-a-minimalist-habit-tracker-why-i-stopped-using-streaks-and-forest-272k</guid>
      <description>&lt;p&gt;I have tried, without exaggeration, every popular habit tracker on the App Store. Streaks. Forest. Habitica. Fabulous. Productive. Way of Life. Done. I used each of them for between a week and three months. Then I stopped. Then I tried the next one. Then I built my own.&lt;/p&gt;

&lt;p&gt;This is a post about why all of them eventually annoyed me, what I wanted instead, and the iOS app I ended up writing over about four evenings. It is called SimpleStreaks and it is about as boring as the name suggests, which is the entire point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern I kept hitting
&lt;/h2&gt;

&lt;p&gt;Every habit app I tried started the same way. Clean. Promising. I would set up three habits — meditate, read, stretch — and feel vaguely virtuous about my new system. Then, reliably, one of these things would happen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The app asked me to subscribe.&lt;/strong&gt; Usually on day three, sometimes on day one, occasionally hidden inside the "advanced" features I actually wanted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The gamification got in the way.&lt;/strong&gt; A tree dies. A pet is sad. A little warrior loses XP. I am a grown adult and I do not need a guilt trip from a pixel owl because I skipped yoga on a Tuesday.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The UI got too clever.&lt;/strong&gt; Charts inside charts. Swipe gestures for everything. Settings nested four screens deep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The data model broke my brain.&lt;/strong&gt; Some apps let you check a habit off multiple times per day. Some don't. Some treat skip and fail differently. Some have "rest days". By the time I understood the rules I had already lost the streak.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I do not think these are bad apps. Several of them are great. They are just not what I wanted, which is a thing that shows me seven circles, lets me tap each one once a day, and leaves me alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually wanted
&lt;/h2&gt;

&lt;p&gt;I wrote the requirements on a napkin before I opened Xcode. I still have the napkin. It says:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Home screen shows all my habits at once.&lt;/li&gt;
&lt;li&gt;One tap marks a habit done for today.&lt;/li&gt;
&lt;li&gt;A streak counter next to each habit. That is the only number.&lt;/li&gt;
&lt;li&gt;No accounts. No cloud. No subscription. No ads.&lt;/li&gt;
&lt;li&gt;Can I open it, check three things, and close it in under five seconds? That is the only UX test.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything that did not serve those five lines got cut.&lt;/p&gt;

&lt;h2&gt;
  
  
  The build
&lt;/h2&gt;

&lt;p&gt;SimpleStreaks is a SwiftUI app with a Core Data backend. That is almost the entire architecture. A habit is a row with a name, an emoji, a colour, and an ordered list of completion dates. The streak is computed, not stored — walk backwards from today until you hit a gap.&lt;/p&gt;

&lt;p&gt;The bits that were harder than expected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reordering.&lt;/strong&gt; SwiftUI's &lt;code&gt;List&lt;/code&gt; reorder works, but persisting the order to Core Data in a way that survives undo, redo, and iCloud sync took longer than the rest of the app combined. I dropped iCloud sync for v1 because of this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Today" logic.&lt;/strong&gt; What counts as "today" in a timezone-changing world? I went with device-local midnight. It is not perfect but it is predictable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Haptics.&lt;/strong&gt; The difference between a habit tracker that feels good and one that feels sterile is one line of &lt;code&gt;UIImpactFeedbackGenerator&lt;/code&gt;. Do not skip haptics on the check tap. It is the entire reward loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Widget.&lt;/strong&gt; I wanted the widget to be the app. You should be able to tap habits on your home screen without ever opening SimpleStreaks. AppIntents made this possible in an afternoon, which felt like cheating.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total code: about 1,800 lines of Swift. No external dependencies. No analytics SDK. No SDK anything, actually.&lt;/p&gt;

&lt;h2&gt;
  
  
  The things I deliberately did not build
&lt;/h2&gt;

&lt;p&gt;This list was longer than the feature list.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Streak recovery.&lt;/strong&gt; Every app has this. Pay us and your broken streak comes back. No. If the streak broke, the streak broke. Start again on Monday. It is fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Social feeds.&lt;/strong&gt; The worst thing you can do to a habit tracker is turn it into a feed. The moment I have to perform my habits for strangers, they stop being habits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XP, levels, achievements.&lt;/strong&gt; If the streak number itself is not enough reward, no amount of confetti will save the product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI coaching.&lt;/strong&gt; Several people have asked me for this. I am not ruling it out but I am ruling it out for now. The app is small on purpose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Categories, tags, folders.&lt;/strong&gt; If you have so many habits that you need folders, the problem is not your habit tracker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cutting things is the actual work of a minimalist app. Writing code is easy. Saying no to features that sound reasonable is the hard part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it is for
&lt;/h2&gt;

&lt;p&gt;If you used Streaks and found it a bit over-engineered, you will probably like SimpleStreaks. If you used Forest for studying and wanted something that stuck around for non-study habits, same answer. If you loved Fabulous but fell off when the subscription kicked in, this is the free version of that itch.&lt;/p&gt;

&lt;p&gt;If you are the kind of person who likes charts, analytics, mood journals, and deep customisation — this is honestly not your app. Use Productive. Use Way of Life. Those are good tools for that job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to find it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Landing page with screenshots and context: &lt;a href="https://thedolceway.com/apps/simplestreaks" rel="noopener noreferrer"&gt;https://thedolceway.com/apps/simplestreaks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Free, no ads, no subscription: &lt;a href="https://apps.apple.com/us/app/id6760590721?pt=126940293&amp;amp;ct=devto&amp;amp;mt=8" rel="noopener noreferrer"&gt;https://apps.apple.com/us/app/id6760590721?pt=126940293&amp;amp;ct=devto&amp;amp;mt=8&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would love to hear what you think, especially if you bounce off it, because "why did this not stick" is more useful feedback than "nice work". The contact is on the landing page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for the next small app
&lt;/h2&gt;

&lt;p&gt;A few things I am taking into the next build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write the napkin first.&lt;/strong&gt; Five lines. If a feature does not serve a line on the napkin, it does not go in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship the ugly version.&lt;/strong&gt; I spent a full evening on the settings screen. Nobody has used the settings screen. I should have shipped without one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Haptics and animation are 80% of "feel".&lt;/strong&gt; The actual logic of a habit tracker is trivial. The reason people keep opening it is how the check button feels under your thumb.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimalism is a promise, not an aesthetic.&lt;/strong&gt; It is fine to have lots of colours and big emojis if the app still opens, does one thing, and closes. Minimal means "no wasted steps", not "black and white".&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>showdev</category>
      <category>ios</category>
      <category>productivity</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>I Built a Hydration Reminder App in a Weekend (and What I Learned)</title>
      <dc:creator>Thedolceway</dc:creator>
      <pubDate>Sun, 05 Apr 2026 19:57:11 +0000</pubDate>
      <link>https://dev.to/thedolceway/i-built-a-hydration-reminder-app-in-a-weekend-and-what-i-learned-28m4</link>
      <guid>https://dev.to/thedolceway/i-built-a-hydration-reminder-app-in-a-weekend-and-what-i-learned-28m4</guid>
      <description>&lt;p&gt;I spent a lot of last year telling myself I drank enough water. I did not. I knew this because every afternoon around 3pm my head would start to feel like someone had parked a small van on it, and the only cure was a glass of water and the quiet humiliation of realising I had drunk exactly one coffee and half a Coke Zero since breakfast.&lt;/p&gt;

&lt;p&gt;So one Saturday I decided to build a water tracking app. Not a productivity opus. Not a subscription empire. Just something that would nag me gently, let me tap a button, and get out of the way. I called it WaterDrop and shipped it on the App Store two days later. This post is about how the weekend actually went — the parts that worked, the parts I threw away, and the stuff I wish someone had told me before I opened Xcode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why every water app I tried annoyed me
&lt;/h2&gt;

&lt;p&gt;Before I wrote a single line of code I downloaded the top ten hydration trackers on the App Store. I wanted to see what the category looked like. Here is what I found, in roughly descending order of how much it bothered me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Onboarding flows that asked for my height, weight, activity level, climate zone, and astrological sign before I could log a single glass.&lt;/li&gt;
&lt;li&gt;A paywall on day three. Sometimes day one.&lt;/li&gt;
&lt;li&gt;Gamification so aggressive that logging water felt like being yelled at by a cartoon fish.&lt;/li&gt;
&lt;li&gt;Reminders that fired at 11pm, because apparently the app thought I should chug a litre before bed.&lt;/li&gt;
&lt;li&gt;Widgets that were just ads for the premium tier.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wanted the opposite of all of this. Open app. Tap once. Done. No login. No fish.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture, such as it is
&lt;/h2&gt;

&lt;p&gt;WaterDrop is embarrassingly simple and I am not apologising for it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SwiftUI for everything. No UIKit bridges.&lt;/li&gt;
&lt;li&gt;A single &lt;code&gt;@AppStorage&lt;/code&gt; key for today's count. A small Core Data store for history, because I wanted streaks.&lt;/li&gt;
&lt;li&gt;Local notifications scheduled via &lt;code&gt;UNUserNotificationCenter&lt;/code&gt;, no server.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WidgetKit&lt;/code&gt; for the home screen widget. &lt;code&gt;AppIntents&lt;/code&gt; so you can log a glass from Siri or a shortcut.&lt;/li&gt;
&lt;li&gt;Zero network calls. Zero analytics SDKs. Zero accounts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole thing is maybe 1,400 lines of Swift. The hardest part was not the code. The hardest part was resisting the urge to add things.&lt;/p&gt;

&lt;h2&gt;
  
  
  The features I cut
&lt;/h2&gt;

&lt;p&gt;Here is a partial list of things I designed, half-built, or sketched and then deleted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A "hydration score" that factored in caffeine and alcohol. Fun, but it needed a whole second data model and nobody asked for it.&lt;/li&gt;
&lt;li&gt;Social sharing of streaks. Why would you share this with anyone. Cut.&lt;/li&gt;
&lt;li&gt;Apple Health two-way sync. I still want this but doing it properly takes more than a weekend.&lt;/li&gt;
&lt;li&gt;A plant that grows as you drink. Cute. Not me. Cut.&lt;/li&gt;
&lt;li&gt;Custom drink types (tea, sparkling, etc). I may add this later. For launch it was friction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every time I cut something the app felt lighter. That is the actual lesson of the weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reminder logic is harder than it looks
&lt;/h2&gt;

&lt;p&gt;The part I underestimated was reminders. You would think "ping the user every 90 minutes during waking hours" is a three-line function. It is not. You have to think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Quiet hours. Nobody wants a 2am water ping.&lt;/li&gt;
&lt;li&gt;The user's actual sleep schedule, which you do not know.&lt;/li&gt;
&lt;li&gt;Not reminding them if they just logged a glass (otherwise the app feels robotic).&lt;/li&gt;
&lt;li&gt;Weekends vs weekdays.&lt;/li&gt;
&lt;li&gt;What happens after the device restarts and your pending notifications were reset.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ended up with a simple rule: the user picks a start hour and end hour. Inside that window, reminders fire every N hours, and any reminder within 30 minutes of a logged glass gets cancelled. That was version one. Turns out version one was also version final, because it just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping on a Sunday night
&lt;/h2&gt;

&lt;p&gt;I hit upload in Xcode around 9pm Sunday. Apple approved it in about 18 hours, which is faster than I expected for a brand new developer account doing anything health-adjacent. The only feedback from review was a request to clarify that the app does not replace medical advice, which is fair.&lt;/p&gt;

&lt;p&gt;If you want to see what I ended up with, it is here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Landing page with screenshots: &lt;a href="https://thedolceway.com/apps/waterdrop" rel="noopener noreferrer"&gt;https://thedolceway.com/apps/waterdrop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Free on the App Store: &lt;a href="https://apps.apple.com/us/app/id6760830381?pt=126940293&amp;amp;ct=devto&amp;amp;mt=8" rel="noopener noreferrer"&gt;https://apps.apple.com/us/app/id6760830381?pt=126940293&amp;amp;ct=devto&amp;amp;mt=8&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is free, there is no subscription, and there is no account. If you try it and something feels wrong I would genuinely like to hear about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;p&gt;A few honest regrets.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Screenshots.&lt;/strong&gt; I rushed them. App Store screenshots are the single biggest conversion lever for a free app and I spent maybe 40 minutes on them. I am redoing them this week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyword research.&lt;/strong&gt; I picked "hydration tracker" as my main keyword on vibes. I should have used an actual ASO tool. I am fixing this in the next metadata update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Icon iteration.&lt;/strong&gt; I made one icon and shipped it. It is fine. It is not memorable. Icons deserve their own afternoon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not timing the build.&lt;/strong&gt; I wish I had logged how long each part took. Would have been great data for the next one.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The point of the weekend
&lt;/h2&gt;

&lt;p&gt;I have shipped maybe a dozen things this year, most of which nobody will ever use, and a few I genuinely like. WaterDrop is in the second category because it solved an actual problem for me personally. I stopped getting the afternoon headache about a week after I started using it, which is either the app working or a placebo, and honestly I do not care which.&lt;/p&gt;

&lt;p&gt;If you are sitting on a small idea and telling yourself it is too small to ship — it is not. Two days. One screen. One problem you personally have. That is the whole formula. Go build the thing.&lt;/p&gt;

&lt;p&gt;And drink some water while you are at it.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>indiehackers</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How AI Resume Builders Are Changing Job Applications in 2026</title>
      <dc:creator>Thedolceway</dc:creator>
      <pubDate>Thu, 26 Mar 2026 11:53:37 +0000</pubDate>
      <link>https://dev.to/thedolceway/how-ai-resume-builders-are-changing-job-applications-in-2026-3145</link>
      <guid>https://dev.to/thedolceway/how-ai-resume-builders-are-changing-job-applications-in-2026-3145</guid>
      <description>&lt;p&gt;The job market has always rewarded people who know how to present themselves. In 2026, that presentation is increasingly mediated by AI — on both sides of the hiring table.&lt;/p&gt;

&lt;p&gt;Recruiters are using AI to screen applications. So job seekers are using AI to write them. What started as a niche productivity hack has become table stakes for anyone serious about their search.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers Are Hard to Ignore
&lt;/h2&gt;

&lt;p&gt;Recent hiring data shows that over 75% of resumes submitted to large employers are filtered by applicant tracking systems before a human ever sees them. These systems scan for keywords, formatting compliance, and relevance signals. A beautifully designed resume that fails ATS parsing is invisible.&lt;/p&gt;

&lt;p&gt;AI-powered resume tools have responded to this shift directly. The best ones don't just help you write — they help you write in a way that survives automated screening and reads well to a human afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI Resume Tools Are Actually Good At
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Rewriting weak bullet points.&lt;/strong&gt; This is where AI genuinely earns its keep. "Managed a team" becomes "Led a cross-functional team of 8 engineers to deliver a platform migration 3 weeks ahead of schedule." The AI pulls on pattern recognition trained across millions of successful resumes to elevate vague descriptions into quantified accomplishments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keyword optimization.&lt;/strong&gt; A good &lt;a href="https://cvbooster.ai/ai-resume-builder" rel="noopener noreferrer"&gt;ai resume builder&lt;/a&gt; will analyze a job posting and suggest which terms belong in your resume. This isn't gaming the system — it's speaking the language that both ATS and recruiters are scanning for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reducing blank-page paralysis.&lt;/strong&gt; Many people know what they've done but struggle to articulate it in resume format. AI suggestions act as a starting draft that you refine, rather than a blank document that stares back at you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistent formatting at speed.&lt;/strong&gt; Tailoring a resume to each application used to mean an hour of reformatting per job. AI-powered tools can restructure and reorder sections in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What They're Not Good At
&lt;/h2&gt;

&lt;p&gt;AI resume tools are pattern-matching engines, not career coaches. They don't know your actual impact, the real context of your achievements, or what makes you different. Output always needs a human editorial pass.&lt;/p&gt;

&lt;p&gt;They're also not magic for career changers. If you're pivoting industries, the AI will default to your existing experience framing. You'll need to guide it more deliberately toward how your transferable skills map to the new domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Democratization Angle
&lt;/h2&gt;

&lt;p&gt;Perhaps the most important shift is accessibility. Professional resume writing services charge $200-600 per resume. That's a real barrier for recent graduates, career returners, or anyone in financial transition — precisely the people who most need help.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cvbooster.ai/ai-resume-builder" rel="noopener noreferrer"&gt;AI-powered resume tools&lt;/a&gt; like CVBooster have changed that equation. The same quality of structured, keyword-optimized, professionally formatted resume that used to require either expertise or a budget is now available for free, instantly, without a subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hiring Side Is Adapting Too
&lt;/h2&gt;

&lt;p&gt;It's worth acknowledging the counter-movement: some hiring managers are now flagging AI-generated resumes as a negative signal, particularly for roles requiring strong written communication. The lesson here is balance — use AI to structure and optimize, but make sure your voice and specific context come through in the final version.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The trajectory is clear. Resume screening will become more AI-to-AI in nature — automated systems on both sides, with humans reviewing only shortlisted candidates. The job seekers who understand this dynamic and use AI tools intelligently will have a structural advantage over those who don't.&lt;/p&gt;

&lt;p&gt;The tools are good now. They'll be better in six months. The question isn't whether to use them — it's whether you're using them well.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
    </item>
    <item>
      <title>The Best Free Resume Builders That Don't Charge You After Download</title>
      <dc:creator>Thedolceway</dc:creator>
      <pubDate>Thu, 26 Mar 2026 09:17:45 +0000</pubDate>
      <link>https://dev.to/thedolceway/the-best-free-resume-builders-that-dont-charge-you-after-download-130o</link>
      <guid>https://dev.to/thedolceway/the-best-free-resume-builders-that-dont-charge-you-after-download-130o</guid>
      <description>&lt;p&gt;There's a frustrating pattern in the resume builder industry. You spend 45 minutes filling in your work history, choosing a template, tweaking the layout — and then you hit download. Suddenly a paywall appears. Your resume is being held hostage.&lt;/p&gt;

&lt;p&gt;This is not a coincidence. It's a business model. Get users invested before revealing the price. After 45 minutes of work, many people just pay rather than start over.&lt;/p&gt;

&lt;p&gt;Let's talk about how to avoid this — and which tools are actually free.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Freemium Trap" in Resume Builders
&lt;/h2&gt;

&lt;p&gt;Most resume builders operate on a freemium model where the core feature — exporting your resume — is locked behind a subscription. The free tier exists to create friction and investment, not to deliver value.&lt;/p&gt;

&lt;p&gt;Here's what "free" usually means in this industry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resume.io&lt;/strong&gt;: Free to build, $2.95/week to download in a usable format&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zety&lt;/strong&gt;: Free to build, watermark on PDF unless you pay ~$5.99/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Novoresume&lt;/strong&gt;: Free plan limited to one resume with restricted templates; download requires premium&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VisualCV&lt;/strong&gt;: Free plan creates a web resume, but PDF export is paid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canva&lt;/strong&gt;: Genuinely free for many templates, but resume-specific features are limited and formatting can be inconsistent for ATS systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key test: can you download a clean, watermark-free PDF without entering payment details? Most tools fail this test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools That Are Genuinely Free
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CVBooster&lt;/strong&gt; is the standout here. The &lt;a href="https://cvbooster.ai/free-resume-builder" rel="noopener noreferrer"&gt;free resume builder&lt;/a&gt; gives you access to professional templates, an AI-assisted editor, and clean PDF download — no signup wall, no credit card, no watermark. You can build and export a complete resume in under 20 minutes at zero cost.&lt;/p&gt;

&lt;p&gt;This matters more than it might seem. Job hunting is already expensive — LinkedIn Premium, Indeed sponsored applications, professional wardrobe for interviews. Paying $20/month for a resume builder adds up fast, especially when you might be between jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Docs&lt;/strong&gt; (with a resume template) is another genuinely free option if you know how to format documents. The trade-off is manual labor — no AI suggestions, no ATS optimization hints, just you and a word processor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LibreOffice&lt;/strong&gt; templates fall in the same category: free but manual.&lt;/p&gt;

&lt;p&gt;For most job seekers who want professional output without the manual work, the &lt;a href="https://cvbooster.ai/free-resume-builder" rel="noopener noreferrer"&gt;free resume builder&lt;/a&gt; at CVBooster is the strongest option I've found.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Look For in a Free Resume Builder
&lt;/h2&gt;

&lt;p&gt;When evaluating any tool, check these before investing your time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Download test first&lt;/strong&gt;: Before filling in your details, check whether PDF download requires payment. Many tools advertise this in their pricing page — read it before you start.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ATS compatibility&lt;/strong&gt;: Does the template use single-column, clean formatting? Multi-column layouts often fail ATS parsing, which can disqualify your application before a human reads it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No watermarks&lt;/strong&gt;: A resume with a service watermark looks unprofessional. Confirm the export is clean.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No forced account creation&lt;/strong&gt;: Some tools won't let you access your resume later without creating an account, which becomes a vector for upsell emails and locked features.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Template variety&lt;/strong&gt;: You may want to tweak templates for different industries. A tool with only 2-3 free templates limits your options.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Broader Point
&lt;/h2&gt;

&lt;p&gt;The resume builder market is built on a pain point — people genuinely struggle to present themselves on paper — and many companies exploit that vulnerability with bait-and-switch pricing. Knowing this going in puts you in a much stronger position.&lt;/p&gt;

&lt;p&gt;You don't need to pay to get a great resume. You need the right tool and 20 focused minutes. Start with something genuinely free, invest your energy in the content, and save your money for the parts of your job search that actually require spending it.&lt;/p&gt;

</description>
      <category>career</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I Tested 5 AI Resume Builders — Here's What Actually Works</title>
      <dc:creator>Thedolceway</dc:creator>
      <pubDate>Thu, 26 Mar 2026 09:16:09 +0000</pubDate>
      <link>https://dev.to/thedolceway/i-tested-5-ai-resume-builders-heres-what-actually-works-4okn</link>
      <guid>https://dev.to/thedolceway/i-tested-5-ai-resume-builders-heres-what-actually-works-4okn</guid>
      <description>&lt;p&gt;I spent a week testing every major AI resume builder on the market. Not quick demos — actual resumes, submitted to real jobs, run through ATS scanners. Here is what I found.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With Most AI Resume Builders
&lt;/h2&gt;

&lt;p&gt;Most tools in this space do the same thing: you paste your info, the AI rewrites your bullets to sound fancier, and you download a PDF. The problem is that "fancier" does not mean "optimized." A resume that sounds impressive to a human can still score poorly in an Applicant Tracking System if it lacks the right keywords.&lt;/p&gt;

&lt;p&gt;The best &lt;a href="https://cvbooster.ai/ai-resume-builder" rel="noopener noreferrer"&gt;AI resume builder&lt;/a&gt; tools do more than rewrite — they analyze the job description, match keywords, and ensure formatting survives ATS parsing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Tested
&lt;/h2&gt;

&lt;p&gt;I evaluated five tools on these criteria: ATS compatibility, template quality, keyword matching, and whether the "free" tier is actually usable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Zety&lt;/strong&gt; — Slick interface, good templates. But the free tier is bait — you cannot download without paying. ATS optimization is minimal. The AI suggestions are generic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Resume.io&lt;/strong&gt; — Clean templates, fast editor. Same paywall problem. No keyword matching against job descriptions. The AI just rephrases your existing text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Teal&lt;/strong&gt; — Better approach. Matches your resume against a job description and highlights missing keywords. Free tier is limited to basic features. Good for keyword gaps but the template selection is small.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Rezi&lt;/strong&gt; — Focused on ATS optimization. Decent keyword analysis. Interface feels dated. The AI rewriting is solid but template variety is lacking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. CVBooster&lt;/strong&gt; — This one surprised me. 160+ templates including 17 completely free ones. No account required. The &lt;a href="https://cvbooster.ai/ai-resume-builder" rel="noopener noreferrer"&gt;AI resume builder&lt;/a&gt; matches your resume against job descriptions, suggests missing keywords, rewrites bullets with measurable impact, and generates cover letters. You can download a polished PDF without creating an account or hitting a paywall.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Verdict
&lt;/h2&gt;

&lt;p&gt;Most AI resume builders are fancy text editors with a paywall at the end. The ones that actually help you get hired do three things: keyword matching against the specific job, ATS-friendly formatting, and honest pricing.&lt;/p&gt;

&lt;p&gt;If you want to try the one that scored highest in my testing, check out CVBooster's &lt;a href="https://cvbooster.ai/ai-resume-builder" rel="noopener noreferrer"&gt;AI resume builder&lt;/a&gt; — it is genuinely free to start and does not trap you at the download step.&lt;/p&gt;

&lt;p&gt;The resume builder market is crowded but most of the crowd is selling the same thing. Find the tool that actually optimizes for ATS, not just for looking good on screen.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
    </item>
    <item>
      <title>5 Forest App Alternatives That Actually Help You Focus in 2026</title>
      <dc:creator>Thedolceway</dc:creator>
      <pubDate>Thu, 26 Mar 2026 07:27:56 +0000</pubDate>
      <link>https://dev.to/thedolceway/5-forest-app-alternatives-that-actually-help-you-focus-in-2026-2c62</link>
      <guid>https://dev.to/thedolceway/5-forest-app-alternatives-that-actually-help-you-focus-in-2026-2c62</guid>
      <description>&lt;p&gt;If you've ever tried to cut down on phone distractions, you've probably come across Forest — the app that grows a virtual tree while you stay off your phone. It's charming, and it works for a lot of people. But it's not for everyone.&lt;/p&gt;

&lt;p&gt;The good news is that there are solid forest app alternatives out there. Here are five worth trying in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Flora
&lt;/h2&gt;

&lt;p&gt;Flora is the closest spiritual sibling to Forest. It uses the same tree-growing mechanic but adds a social layer — you can plant trees with friends and keep each other accountable. If you break focus, the tree dies for everyone. Flora also lets you donate to real tree-planting initiatives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; People who want accountability with friends or coworkers.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Freedom
&lt;/h2&gt;

&lt;p&gt;Freedom takes a more aggressive approach. Rather than gamifying focus, it blocks distracting websites and apps across all your devices simultaneously. One session locks you out on your Mac, iPhone, and iPad at the same time. It's less cute than Forest, but far more effective for people who need hard barriers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Remote workers who need serious distraction blocking across multiple devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Be Focused (Pomodoro Timer)
&lt;/h2&gt;

&lt;p&gt;Be Focused is a clean Pomodoro timer for Apple devices. No gamification — just structured 25-minute work blocks with short breaks. It tracks how many Pomodoros you complete each day. Sometimes the most effective tool is the simplest one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Minimalists who prefer the Pomodoro technique.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Focusmate
&lt;/h2&gt;

&lt;p&gt;Focusmate flips the model entirely. Instead of a virtual tree, you get a real human. You book a video session with a stranger, state your goal, work silently, and check in at the end. The social presence effect is one of the strongest focus hacks known to productivity researchers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; People who struggle with isolation while working from home.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Session
&lt;/h2&gt;

&lt;p&gt;Session is a premium focus timer for Mac that integrates with your calendar and task manager. It shows your tasks alongside your timer, lets you block apps, and gives detailed analytics on your work patterns. If you want a forest app alternative that feels polished on macOS, Session is the one to beat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Mac users who want deep workflow integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which One Should You Pick?
&lt;/h2&gt;

&lt;p&gt;The right app depends on what kind of friction you need. Social accountability? Flora or Focusmate. Hard blocks? Freedom. Simplicity? Be Focused or Session.&lt;/p&gt;

&lt;p&gt;For a deeper comparison with pros, cons, and pricing for each forest app alternative, check out the full breakdown at &lt;a href="https://thedolceway.com/blog/forest-app-alternative" rel="noopener noreferrer"&gt;The Dolce Way&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Finding the right alternative is about matching the tool to how your brain actually works — not just downloading whatever has the best App Store rating.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>beginners</category>
    </item>
    <item>
      <title>5 Ways AI is Changing Small Business Operations in 2026</title>
      <dc:creator>Thedolceway</dc:creator>
      <pubDate>Thu, 26 Mar 2026 07:26:35 +0000</pubDate>
      <link>https://dev.to/thedolceway/5-ways-ai-is-changing-small-business-operations-in-2026-k9l</link>
      <guid>https://dev.to/thedolceway/5-ways-ai-is-changing-small-business-operations-in-2026-k9l</guid>
      <description>&lt;p&gt;AI is no longer a luxury reserved for enterprise companies. In 2026, small businesses are using AI tools to automate repetitive tasks, make smarter decisions, and compete with organizations ten times their size.&lt;/p&gt;

&lt;p&gt;Here are five ways this is actually playing out.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Customer Support That Never Sleeps
&lt;/h2&gt;

&lt;p&gt;AI chatbots have moved past the "press 1 for billing" era. Modern AI support agents can understand context, pull from knowledge bases, and resolve issues without human intervention. For a small business owner who cannot afford a 24/7 support team, this is transformative.&lt;/p&gt;

&lt;p&gt;The key shift is that these tools now integrate with existing workflows. They connect to your CRM, read your documentation, and handle the first line of contact — escalating to humans only when necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Automated Bookkeeping and Financial Forecasting
&lt;/h2&gt;

&lt;p&gt;AI-powered accounting tools can categorize expenses, flag anomalies, and generate cash flow projections based on historical patterns. Small business owners spend an average of five hours per week on bookkeeping. AI can cut that to thirty minutes.&lt;/p&gt;

&lt;p&gt;The real value is not in data entry — it is in the forecasting. Knowing next month's likely revenue before it arrives changes how you plan inventory, hiring, and marketing spend.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Content Creation and Marketing Automation
&lt;/h2&gt;

&lt;p&gt;Small businesses need content but rarely have dedicated marketing teams. AI writing assistants can draft social media posts, email campaigns, and blog outlines in seconds. The output still needs a human eye, but the first draft is no longer the bottleneck.&lt;/p&gt;

&lt;p&gt;Marketing automation platforms now use AI to determine optimal send times, segment audiences, and personalize messaging at a level that was previously only possible for companies with data science teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Smart Inventory and Supply Chain Management
&lt;/h2&gt;

&lt;p&gt;For product-based businesses, AI demand forecasting prevents both overstock and stockouts. These systems analyze seasonal trends, competitor pricing, and historical sales data to recommend order quantities.&lt;/p&gt;

&lt;p&gt;The difference from traditional inventory management is precision. Instead of ordering based on gut feeling and last year's numbers, you are ordering based on a model that accounts for dozens of variables simultaneously.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. AI-Powered Business Intelligence
&lt;/h2&gt;

&lt;p&gt;Dashboards that just show numbers are table stakes. AI business intelligence tools explain why the numbers changed and what to do about it. They surface patterns that a human reviewing spreadsheets would miss.&lt;/p&gt;

&lt;p&gt;Platforms like &lt;a href="https://vectaai.com" rel="noopener noreferrer"&gt;Vecta AI&lt;/a&gt; are building exactly this kind of intelligence layer for small businesses — turning raw data into actionable recommendations without requiring a data analyst on staff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;The businesses adopting AI in 2026 are not doing it because it is trendy. They are doing it because the tools are finally good enough, cheap enough, and simple enough to deliver real ROI at small scale.&lt;/p&gt;

&lt;p&gt;The gap between AI-enabled small businesses and those still running on spreadsheets and manual processes is widening every quarter. The time to start is not next year.&lt;/p&gt;

&lt;p&gt;Learn more about AI solutions for business at &lt;a href="https://vectaai.com" rel="noopener noreferrer"&gt;vectaai.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>startup</category>
    </item>
    <item>
      <title>How I Ship One App Per Week as a Solo Developer</title>
      <dc:creator>Thedolceway</dc:creator>
      <pubDate>Thu, 26 Mar 2026 07:24:37 +0000</pubDate>
      <link>https://dev.to/thedolceway/how-i-ship-one-app-per-week-as-a-solo-developer-34ci</link>
      <guid>https://dev.to/thedolceway/how-i-ship-one-app-per-week-as-a-solo-developer-34ci</guid>
      <description>&lt;p&gt;People ask me how I manage to keep shipping while working alone. The honest answer is that I stopped trying to build perfect things and started building complete things instead.&lt;/p&gt;

&lt;p&gt;That mindset shift changed everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trap of the Unfinished Project
&lt;/h2&gt;

&lt;p&gt;Most solo developers I know have a graveyard. A folder somewhere full of half-built apps that were exciting for two weeks and then stalled. The UI is half-done. The auth works but nothing else does.&lt;/p&gt;

&lt;p&gt;I lived in that graveyard for years. The moment I started shipping on a weekly cadence, the graveyard stopped growing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Shipping" Actually Means
&lt;/h2&gt;

&lt;p&gt;Shipping does not mean launching a polished product. It means putting something real in front of real users. A landing page with a waitlist is a ship. A functional MVP with three core features is a ship.&lt;/p&gt;

&lt;p&gt;The bar for "shipped" is: can a stranger use it? If yes, you shipped. Everything after that is iteration.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Weekly Rhythm
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Monday and Tuesday&lt;/strong&gt; are for scoping. I pick one thing to build — usually from a list of annoying problems I have personally experienced. I write out the simplest possible version that would be genuinely useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wednesday through Friday&lt;/strong&gt; are for building. Three days of focused work. No feature creep allowed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Saturday&lt;/strong&gt; is for shipping and writing. I deploy, write about what I built and why, and share it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sunday&lt;/strong&gt; is off. Completely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tools That Make It Possible
&lt;/h2&gt;

&lt;p&gt;My stack stays consistent across projects so I am never learning infrastructure while also building a product. TypeScript, a framework I know deeply, a deployment target I can push to in under a minute. Familiarity is a velocity multiplier.&lt;/p&gt;

&lt;p&gt;I also keep a running doc of reusable patterns — auth flows, payment integrations, API scaffolding — that I can drop into a new project without rebuilding from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Have Learned From Shipping
&lt;/h2&gt;

&lt;p&gt;Most ideas that feel big are actually small. The feature you think will take a week takes a day. The part you thought was easy takes three days. Shipping quickly surfaces these realities before you have sunk months into wrong assumptions.&lt;/p&gt;

&lt;p&gt;Users tell you things you could never have predicted. The feature you thought was core, nobody uses. The thing you built as an afterthought becomes the reason people come back.&lt;/p&gt;

&lt;p&gt;And momentum is real. Shipping something creates the energy to ship the next thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I Write About This
&lt;/h2&gt;

&lt;p&gt;I document the whole process — what I built, what broke, what users said — over at &lt;a href="https://thedolceway.com" rel="noopener noreferrer"&gt;The Dolce Way&lt;/a&gt;. It is part build log, part reflection on the indie developer life. If you are trying to ship more and finish more, there is a lot there that might help.&lt;/p&gt;

&lt;p&gt;The short version: scope small, ship early, write about it. Repeat.&lt;/p&gt;

&lt;p&gt;Read more at &lt;a href="https://thedolceway.com" rel="noopener noreferrer"&gt;thedolceway.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>startup</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why Most Resume Templates Fail ATS Scans (And What Actually Works)</title>
      <dc:creator>Thedolceway</dc:creator>
      <pubDate>Thu, 26 Mar 2026 07:23:19 +0000</pubDate>
      <link>https://dev.to/thedolceway/why-most-resume-templates-fail-ats-scans-and-what-actually-works-2761</link>
      <guid>https://dev.to/thedolceway/why-most-resume-templates-fail-ats-scans-and-what-actually-works-2761</guid>
      <description>&lt;p&gt;You spent hours on your resume. The formatting is clean, the fonts are consistent, and you carefully listed every relevant experience. Then you submitted it to forty companies and heard nothing back.&lt;/p&gt;

&lt;p&gt;Sound familiar? You might be getting filtered out before a human ever reads your name.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ATS Actually Does
&lt;/h2&gt;

&lt;p&gt;Applicant Tracking Systems are not magic. They are text parsers. When your PDF or DOCX lands in the system, the ATS strips all visual formatting and attempts to extract raw text. It then tries to map that text to structured fields: name, contact info, work history, education, skills.&lt;/p&gt;

&lt;p&gt;The problem is that most resume templates are designed to look beautiful in a PDF viewer, not to survive text extraction. And those two goals are frequently in direct conflict.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Formatting Traps That Kill Applications
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tables and columns.&lt;/strong&gt; Multi-column layouts look polished in design tools. In an ATS, a two-column layout often gets read left-to-right across both columns simultaneously, turning your job titles and dates into garbled nonsense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text inside images or graphics.&lt;/strong&gt; Any text embedded inside a logo, banner, or decorative element is completely invisible to a parser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fancy fonts and icons.&lt;/strong&gt; Unicode icons used as bullet points can parse as random characters or get dropped entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Headers and footers.&lt;/strong&gt; Many ATS platforms skip content in the header and footer sections of Word documents entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creative section labels.&lt;/strong&gt; Calling your work history "My Journey" instead of "Work Experience" confuses keyword matching.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Works
&lt;/h2&gt;

&lt;p&gt;A clean, single-column layout with standard section headers is the baseline. Use a simple font like Arial, Calibri, or Georgia. Avoid tables for content layout. Use standard bullet points.&lt;/p&gt;

&lt;p&gt;Keywords matter. The ATS ranks your resume against the job description. Mirror the language of the job posting where it is accurate to do so.&lt;/p&gt;

&lt;p&gt;Quantify everything you can. "Increased sales by 34%" beats "improved sales performance" in both human and machine reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Your Resume Before You Apply
&lt;/h2&gt;

&lt;p&gt;The smartest thing you can do is run your resume through an ATS-friendly builder before submitting. Tools like &lt;a href="https://cvbooster.ai" rel="noopener noreferrer"&gt;CVBooster&lt;/a&gt; let you choose from 160+ ATS-optimized templates and see exactly how your resume will parse. It takes about two minutes and can completely change your application strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;ATS systems are not going away. The gap between a resume optimized for human readers and one optimized for machine parsing is real, and most candidates do not know it exists.&lt;/p&gt;

&lt;p&gt;Getting past the ATS is not about gaming the system. It is about removing friction. Fix the formatting first. Then worry about everything else.&lt;/p&gt;

&lt;p&gt;Build a resume that machines can read at &lt;a href="https://cvbooster.ai" rel="noopener noreferrer"&gt;cvbooster.ai&lt;/a&gt; — free templates, no signup required.&lt;/p&gt;

</description>
      <category>career</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building a Config-Driven Resume Template Engine in React</title>
      <dc:creator>Thedolceway</dc:creator>
      <pubDate>Thu, 26 Mar 2026 07:07:04 +0000</pubDate>
      <link>https://dev.to/thedolceway/building-a-config-driven-resume-template-engine-in-react-30f1</link>
      <guid>https://dev.to/thedolceway/building-a-config-driven-resume-template-engine-in-react-30f1</guid>
      <description>&lt;p&gt;When I started building a resume builder, I did what most developers do: I created one React component per template. &lt;code&gt;TemplateModern.tsx&lt;/code&gt;, &lt;code&gt;TemplateClassic.tsx&lt;/code&gt;, &lt;code&gt;TemplateMinimal.tsx&lt;/code&gt;. Each one was a slightly tweaked copy of the last.&lt;/p&gt;

&lt;p&gt;By template five, the pattern was already painful. A bug fix in the header meant five identical changes. A new section type meant touching every component. Worse, adding a new template required duplicating hundreds of lines of JSX just to change some colors and a font.&lt;/p&gt;

&lt;p&gt;There had to be a better way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Insight: Templates Are Data, Not Code
&lt;/h2&gt;

&lt;p&gt;Most resume templates are not structurally different. They share the same sections — header, experience, education, skills. What varies is &lt;em&gt;how those sections are rendered&lt;/em&gt;: column layout, font choices, color palette, the way section headers look, how skills are displayed.&lt;/p&gt;

&lt;p&gt;That variation is configuration, not logic. So I stopped writing components and started writing config objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The TemplateConfig Interface
&lt;/h2&gt;

&lt;p&gt;Every template in the system is defined by a single TypeScript object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TemplateConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;single-column&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;two-column-left-sidebar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;two-column-right-sidebar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;headerStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;centered-text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sidebar-header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;full-bleed-color&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sectionHeaderStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;border-bottom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;left-border&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pill-header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;skillsStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pills&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;progress-bar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tags-code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;comma-list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ColorPalette&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;typography&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TypographyConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SpacingConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No JSX. No component. Just a plain object describing how the resume should look.&lt;/p&gt;

&lt;h2&gt;
  
  
  From 10 Templates to 160+
&lt;/h2&gt;

&lt;p&gt;With config-driven templates, 160 templates is 160 plain objects — roughly 6,000 lines of clean data. Adding a new template takes about two minutes: pick a layout variant, choose fonts, set a color palette, done. The rendering logic is tested once; the configs just exercise it.&lt;/p&gt;

&lt;p&gt;The enum-style union types on layout, headerStyle, and skillsStyle are intentional constraints. Each value maps to exactly one tested rendering path. If a designer asks for a new variant, you add it to the union and implement it once — every template can use it immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google Fonts: Dynamic Loading
&lt;/h2&gt;

&lt;p&gt;With 160+ templates using different font combinations, bundling all fonts statically is not an option. Fonts are loaded dynamically when a template is selected — keeping the initial bundle lean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Print-Ready A4 Output
&lt;/h2&gt;

&lt;p&gt;The renderer wraps everything in a fixed A4 container (210mm x 297mm) and uses &lt;code&gt;@media print&lt;/code&gt; rules for pixel-perfect PDF exports via &lt;code&gt;window.print()&lt;/code&gt; — no headless browser required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with the config interface, not the components.&lt;/strong&gt; Defining your variation axes upfront forces you to think about what actually differs between templates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep enum variants small and orthogonal.&lt;/strong&gt; Every combination of layout + headerStyle + sectionHeaderStyle needs to look good together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSS custom properties are your best friend.&lt;/strong&gt; Set them as CSS variables at the root and let CSS do the cascading.&lt;/p&gt;




&lt;p&gt;I built this for &lt;a href="https://cvbooster.ai" rel="noopener noreferrer"&gt;CVBooster&lt;/a&gt;, a free resume builder with 160+ templates. You can see the config-driven rendering in action — no signup required. Check out the live result at &lt;a href="https://cvbooster.ai" rel="noopener noreferrer"&gt;cvbooster.ai&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
