<?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: Warren</title>
    <description>The latest articles on DEV Community by Warren (@warrenshi).</description>
    <link>https://dev.to/warrenshi</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%2F3922196%2F08d1e44b-ced3-418d-8627-8d790b70dadb.jpg</url>
      <title>DEV Community: Warren</title>
      <link>https://dev.to/warrenshi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/warrenshi"/>
    <language>en</language>
    <item>
      <title>I Almost Used AI to Classify User Input. Simple Rules Worked Better</title>
      <dc:creator>Warren</dc:creator>
      <pubDate>Sat, 15 Aug 2026 01:56:06 +0000</pubDate>
      <link>https://dev.to/warrenshi/i-almost-used-ai-to-classify-user-input-simple-rules-worked-better-40c1</link>
      <guid>https://dev.to/warrenshi/i-almost-used-ai-to-classify-user-input-simple-rules-worked-better-40c1</guid>
      <description>&lt;p&gt;I’ve been rebuilding a small name tattoo tool recently, and I ran into a problem that looked like a good use case for AI.&lt;/p&gt;

&lt;p&gt;The user enters some text.&lt;/p&gt;

&lt;p&gt;At first, I treated every input basically the same:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;text → show a set of lettering styles&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That works for simple names.&lt;/p&gt;

&lt;p&gt;But these are all very different inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Emma&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;A.M.&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Jack + Mia&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Amelia · 1998-11-14&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Anna-Marie&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Forever Dad&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Showing the exact same recommendations for all of them started to feel wrong.&lt;/p&gt;

&lt;p&gt;My first thought was: classify the input with an LLM.&lt;/p&gt;

&lt;p&gt;I already use AI elsewhere in the product, so sending the text through another model call would have been easy enough.&lt;/p&gt;

&lt;p&gt;But after listing the cases I actually cared about, I realized the classification problem was tiny.&lt;/p&gt;

&lt;h2&gt;
  
  
  The input types were predictable
&lt;/h2&gt;

&lt;p&gt;I didn’t need to understand arbitrary human language.&lt;/p&gt;

&lt;p&gt;I mostly needed to distinguish things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;initials&lt;/li&gt;
&lt;li&gt;initials + date&lt;/li&gt;
&lt;li&gt;name + date&lt;/li&gt;
&lt;li&gt;two names&lt;/li&gt;
&lt;li&gt;hyphenated names&lt;/li&gt;
&lt;li&gt;multi-word text&lt;/li&gt;
&lt;li&gt;short single names&lt;/li&gt;
&lt;li&gt;medium single names&lt;/li&gt;
&lt;li&gt;long single names&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of adding another model call, I used deterministic rules.&lt;/p&gt;

&lt;p&gt;A simplified version looks roughly like this:&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;getNameStructure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&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="nf"&gt;looksLikeInitialsAndDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;initials-date&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="nf"&gt;looksLikeNameAndDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name-date&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="nf"&gt;looksLikeInitials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;initials&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="nf"&gt;looksLikeNamePair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name-pair&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;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hyphenated&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;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;multi-word&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;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;short&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;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;9&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&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="s2"&gt;long&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real implementation has a few more checks, especially around dates and separators, but the idea is the same.&lt;/p&gt;

&lt;p&gt;Nothing clever.&lt;/p&gt;

&lt;p&gt;And that turned out to be a good thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classification only mattered because it changed the UI
&lt;/h2&gt;

&lt;p&gt;I didn’t want classification for its own sake.&lt;/p&gt;

&lt;p&gt;I wanted the first few lettering options to make more sense for the input.&lt;/p&gt;

&lt;p&gt;For example, compact initials can tolerate directions that might feel too heavy for a long name.&lt;/p&gt;

&lt;p&gt;A name pair needs enough spacing to keep both names readable.&lt;/p&gt;

&lt;p&gt;A name + date has a second piece of information competing for attention.&lt;/p&gt;

&lt;p&gt;A long name usually needs a little more restraint than a short one.&lt;/p&gt;

&lt;p&gt;So the structure changes the recommendation order.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recommendations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;initials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bold&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="s2"&gt;minimal&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="s2"&gt;gothic&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name-pair&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;minimal&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="s2"&gt;script&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="s2"&gt;serif&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name-date&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serif&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="s2"&gt;minimal&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="s2"&gt;script&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="na"&gt;short&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;script&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="s2"&gt;signature&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="s2"&gt;bold&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact presets aren’t important here.&lt;/p&gt;

&lt;p&gt;What mattered was that recommendations became predictable.&lt;/p&gt;

&lt;p&gt;And I still keep the other styles visible.&lt;/p&gt;

&lt;p&gt;The classifier doesn’t decide:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is the correct style for your tattoo.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It only decides:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;These are probably useful directions to show first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction helped keep the rules small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I ended up preferring rules over AI here
&lt;/h2&gt;

&lt;p&gt;The more I worked on it, the less attractive an AI classifier became.&lt;/p&gt;

&lt;p&gt;With deterministic rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;there’s no extra inference cost&lt;/li&gt;
&lt;li&gt;there’s no network latency&lt;/li&gt;
&lt;li&gt;the same input always gets the same classification&lt;/li&gt;
&lt;li&gt;edge cases are easy to reproduce&lt;/li&gt;
&lt;li&gt;I can see exactly why a recommendation appeared&lt;/li&gt;
&lt;li&gt;changing product behavior means changing a rule, not changing a prompt and hoping the model interprets it the same way&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly, the problem itself wasn’t fuzzy enough to justify a model.&lt;/p&gt;

&lt;p&gt;The product still uses AI when the user wants an actual custom lettering composition with flowers, symbols, flourishes, or other supporting details.&lt;/p&gt;

&lt;p&gt;But deciding whether &lt;code&gt;A.M.&lt;/code&gt; looks like initials?&lt;/p&gt;

&lt;p&gt;That doesn’t need intelligence.&lt;/p&gt;

&lt;p&gt;It needs a regex.&lt;/p&gt;

&lt;p&gt;I ended up using this approach in the &lt;a href="https://aimaketattoo.com/name-tattoo-generator" rel="noopener noreferrer"&gt;Name Tattoo Generator&lt;/a&gt;, where the lightweight preview stays deterministic and the AI step only comes later when the user actually wants custom composition.&lt;/p&gt;

&lt;h2&gt;
  
  
  I think I was using the wrong default
&lt;/h2&gt;

&lt;p&gt;For a while my default question was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Could AI handle this?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now I’m trying to ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is there enough uncertainty here that AI is actually useful?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the input space is small, the rules are understandable, and predictability matters, a boring classifier can be a better product decision.&lt;/p&gt;

&lt;p&gt;AI is much more valuable later in this workflow, where the user asks for something genuinely open-ended.&lt;/p&gt;

&lt;p&gt;The funny part is that making the AI product better in this case meant using less AI.&lt;/p&gt;

&lt;p&gt;I’m curious how other people draw this line.&lt;/p&gt;

&lt;p&gt;What’s something you originally planned to solve with an LLM, then replaced with normal code?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>typescript</category>
    </item>
    <item>
      <title>I Thought I Was Building an AI Image Generator. I Was Actually Building an Iteration System.</title>
      <dc:creator>Warren</dc:creator>
      <pubDate>Fri, 07 Aug 2026 09:29:27 +0000</pubDate>
      <link>https://dev.to/warrenshi/i-thought-i-was-building-an-ai-image-generator-i-was-actually-building-an-iteration-system-10pn</link>
      <guid>https://dev.to/warrenshi/i-thought-i-was-building-an-ai-image-generator-i-was-actually-building-an-iteration-system-10pn</guid>
      <description>&lt;p&gt;I started with a pretty simple mental model for my AI image product:&lt;/p&gt;

&lt;p&gt;A user enters an idea, chooses a few options, generates an image, and either likes it or tries again.&lt;/p&gt;

&lt;p&gt;That model turned out to be too shallow.&lt;/p&gt;

&lt;p&gt;Once people started using the product repeatedly, the behavior looked less like “generate another image” and more like “keep narrowing the decision.”&lt;/p&gt;

&lt;p&gt;They would keep the core idea and change one variable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a different style&lt;/li&gt;
&lt;li&gt;a different placement&lt;/li&gt;
&lt;li&gt;a narrower or wider proportion&lt;/li&gt;
&lt;li&gt;a different supporting element&lt;/li&gt;
&lt;li&gt;less detail&lt;/li&gt;
&lt;li&gt;a different lettering direction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That changed how I think about the product.&lt;/p&gt;

&lt;p&gt;The generated image is not always the final unit of value.&lt;/p&gt;

&lt;p&gt;Sometimes the real value is the sequence of comparisons that helps the user decide what they actually want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generation is only one step
&lt;/h2&gt;

&lt;p&gt;The obvious architecture for an AI image product looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input
  ↓
generation
  ↓
result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the real user workflow often looks more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idea
  ↓
generate
  ↓
compare
  ↓
change one variable
  ↓
generate again
  ↓
compare again
  ↓
narrow the direction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a different product.&lt;/p&gt;

&lt;p&gt;If the user is iterating toward a decision, several things become more important than simply adding more model choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Controls become part of the reasoning interface
&lt;/h2&gt;

&lt;p&gt;A prompt box gives users freedom, but it also asks them to describe everything again.&lt;/p&gt;

&lt;p&gt;Structured controls let them preserve most of the current idea while intentionally changing one dimension.&lt;/p&gt;

&lt;p&gt;In my case, things like style, placement, proportion, and detail level became more useful than I initially expected.&lt;/p&gt;

&lt;p&gt;They are not just generation parameters. They are comparison variables.&lt;/p&gt;

&lt;p&gt;A user can effectively ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;same idea
+ different placement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;same subject
+ narrower composition
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without rewriting the whole concept.&lt;/p&gt;

&lt;p&gt;That makes the UI part of the reasoning loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. “Regenerate” is a very blunt tool
&lt;/h2&gt;

&lt;p&gt;Most generative interfaces have some version of a regenerate button.&lt;/p&gt;

&lt;p&gt;But full regeneration throws away a lot of information.&lt;/p&gt;

&lt;p&gt;A user may like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the composition&lt;/li&gt;
&lt;li&gt;the main subject&lt;/li&gt;
&lt;li&gt;the balance&lt;/li&gt;
&lt;li&gt;the general style&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and dislike only one thing.&lt;/p&gt;

&lt;p&gt;If every retry regenerates the whole image, the user has to trade one improvement for several accidental changes.&lt;/p&gt;

&lt;p&gt;What they really want may be closer to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;keep:
- composition
- subject

change:
- color
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;keep:
- style
- main element

remove:
- supporting symbol
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much closer to editing than random regeneration.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. History is not enough
&lt;/h2&gt;

&lt;p&gt;I recently added a lightweight generation history feature.&lt;/p&gt;

&lt;p&gt;It solves retrieval well. A user can find an earlier result, open it again, download it, or compare it with something newer.&lt;/p&gt;

&lt;p&gt;But history by itself does not create iteration.&lt;/p&gt;

&lt;p&gt;There is an important difference between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;find an old result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;continue from this result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second one requires more state.&lt;/p&gt;

&lt;p&gt;Potentially:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;generation state
- original input
- selected controls
- tool type
- model/provider
- relevant assets
- previous result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And even that only gets you to continuation.&lt;/p&gt;

&lt;p&gt;Controlled variation is another layer again.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Credits are not always paying for outputs
&lt;/h2&gt;

&lt;p&gt;This was another assumption I had to change.&lt;/p&gt;

&lt;p&gt;The simple interpretation of generation credits is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 credit = 1 more output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But in an iterative workflow, credits can represent something closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;more room to explore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user may not need ten finished images.&lt;/p&gt;

&lt;p&gt;They may need ten attempts to answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which placement works?&lt;/li&gt;
&lt;li&gt;Which composition feels right?&lt;/li&gt;
&lt;li&gt;Is this too detailed?&lt;/li&gt;
&lt;li&gt;Do I prefer this style or the previous one?&lt;/li&gt;
&lt;li&gt;What should I bring to the next step?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That changes how I think about both pricing and UX.&lt;/p&gt;

&lt;p&gt;If the user is paying for iteration room, the product should make each iteration easier to compare and more intentional.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Better models do not automatically fix this
&lt;/h2&gt;

&lt;p&gt;It is tempting to solve every problem in an AI product by improving the model.&lt;/p&gt;

&lt;p&gt;Better image quality helps. More reliable instruction following helps.&lt;/p&gt;

&lt;p&gt;But neither solves the workflow problem.&lt;/p&gt;

&lt;p&gt;A stronger model still does not know which part of the previous result the user wants to preserve unless the product gives them a way to express that.&lt;/p&gt;

&lt;p&gt;This is why I am becoming more interested in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preserving generation state&lt;/li&gt;
&lt;li&gt;comparing related outputs&lt;/li&gt;
&lt;li&gt;restoring prior controls&lt;/li&gt;
&lt;li&gt;creating branches&lt;/li&gt;
&lt;li&gt;controlled variation&lt;/li&gt;
&lt;li&gt;keeping selected parts while changing others&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;than simply adding more styles or another model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture starts changing
&lt;/h2&gt;

&lt;p&gt;Once you treat the product as an iteration system, the data model starts looking different too.&lt;/p&gt;

&lt;p&gt;A generation is no longer just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id
image_url
created_at
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It starts looking more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;generation
- user
- tool
- input state
- selected controls
- output
- parent generation
- model metadata
- created time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may not need all of this on day one.&lt;/p&gt;

&lt;p&gt;I definitely do not.&lt;/p&gt;

&lt;p&gt;But the important part is recognizing that a result may eventually need context about where it came from and what it is related to.&lt;/p&gt;

&lt;p&gt;Otherwise, history becomes a pile of disconnected images.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shift for me
&lt;/h2&gt;

&lt;p&gt;I used to think the core loop was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prompt → image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I think it is closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idea → generation → comparison → refinement → decision
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds like a small wording difference, but it changes what I prioritize.&lt;/p&gt;

&lt;p&gt;The product is not only responsible for producing an image.&lt;/p&gt;

&lt;p&gt;It is also responsible for helping the user move between attempts without losing the reasoning that got them there.&lt;/p&gt;

&lt;p&gt;I ran into this while building the &lt;a href="https://aimaketattoo.com/ai-tattoo-generator" rel="noopener noreferrer"&gt;AI Tattoo Generator&lt;/a&gt; in AIMakeTattoo.&lt;/p&gt;

&lt;p&gt;I am still keeping the system lightweight, but I am much less interested now in “how many outputs can this generate?” and more interested in “how easily can someone move from one useful attempt to the next?”&lt;/p&gt;

&lt;p&gt;For people building generative products: what ended up mattering more in your product — better generation quality, or better iteration controls?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Added Image Upload to an AI Product. The Hard Part Was Everything Around It.</title>
      <dc:creator>Warren</dc:creator>
      <pubDate>Fri, 31 Jul 2026 02:40:12 +0000</pubDate>
      <link>https://dev.to/warrenshi/i-added-image-upload-to-an-ai-product-the-hard-part-was-everything-around-it-46pn</link>
      <guid>https://dev.to/warrenshi/i-added-image-upload-to-an-ai-product-the-hard-part-was-everything-around-it-46pn</guid>
      <description>&lt;p&gt;I recently added an &lt;a href="https://aimaketattoo.com/image-to-tattoo" rel="noopener noreferrer"&gt;Image to Tattoo workflow&lt;/a&gt; to AIMakeTattoo.&lt;/p&gt;

&lt;p&gt;From the user’s perspective, the feature looks straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upload a photo or sketch.&lt;/li&gt;
&lt;li&gt;Choose a tattoo style.&lt;/li&gt;
&lt;li&gt;Adjust proportion and complexity.&lt;/li&gt;
&lt;li&gt;Generate a tattoo design.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From the application’s perspective, image upload introduced a much larger set of concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multipart request parsing&lt;/li&gt;
&lt;li&gt;file validation&lt;/li&gt;
&lt;li&gt;image moderation&lt;/li&gt;
&lt;li&gt;anonymous and signed-in quotas&lt;/li&gt;
&lt;li&gt;paid credits&lt;/li&gt;
&lt;li&gt;concurrent-job protection&lt;/li&gt;
&lt;li&gt;provider uploads&lt;/li&gt;
&lt;li&gt;OAuth state loss&lt;/li&gt;
&lt;li&gt;privacy decisions&lt;/li&gt;
&lt;li&gt;cleanup after partial failure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The image generation request itself was one of the easier parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Image upload changed the request boundary
&lt;/h2&gt;

&lt;p&gt;The existing text-to-image flow accepted structured fields.&lt;/p&gt;

&lt;p&gt;The server could validate the prompt and selected controls, check whether the user was eligible to generate, submit a job, and return a job ID.&lt;/p&gt;

&lt;p&gt;Adding a file changed that boundary.&lt;/p&gt;

&lt;p&gt;The route now had to answer several new questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which file types should be accepted?&lt;/li&gt;
&lt;li&gt;How large can an individual image be?&lt;/li&gt;
&lt;li&gt;Should the complete multipart request have its own size limit?&lt;/li&gt;
&lt;li&gt;When should moderation run?&lt;/li&gt;
&lt;li&gt;Should an ineligible request reach moderation at all?&lt;/li&gt;
&lt;li&gt;Should the application store the original image?&lt;/li&gt;
&lt;li&gt;What happens when the user signs in after choosing a file?&lt;/li&gt;
&lt;li&gt;Which failures require releasing a generation lock?&lt;/li&gt;
&lt;li&gt;At what point should a paid credit be consumed?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these questions directly improved the generated image.&lt;/p&gt;

&lt;p&gt;They determined whether the feature behaved like a reliable part of the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first request flow worked, but it did unnecessary work
&lt;/h2&gt;

&lt;p&gt;My initial flow was roughly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse the multipart request.&lt;/li&gt;
&lt;li&gt;Resolve the user or anonymous visitor.&lt;/li&gt;
&lt;li&gt;Acquire an active-generation lock.&lt;/li&gt;
&lt;li&gt;Moderate the uploaded image.&lt;/li&gt;
&lt;li&gt;Check free quota and paid credits.&lt;/li&gt;
&lt;li&gt;Upload the image to the generation provider.&lt;/li&gt;
&lt;li&gt;Submit the generation job.&lt;/li&gt;
&lt;li&gt;Store the job metadata.&lt;/li&gt;
&lt;li&gt;Consume the correct allowance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The happy path worked.&lt;/p&gt;

&lt;p&gt;But a user who had clearly exhausted every available allowance could still reach image moderation before the request was rejected.&lt;/p&gt;

&lt;p&gt;That meant the application was doing work for a generation that could never proceed.&lt;/p&gt;

&lt;h2&gt;
  
  
  I added an early eligibility check
&lt;/h2&gt;

&lt;p&gt;I moved a read-only eligibility check closer to the beginning of the request.&lt;/p&gt;

&lt;p&gt;The revised flow became:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse and validate the request.&lt;/li&gt;
&lt;li&gt;Resolve the user or anonymous visitor.&lt;/li&gt;
&lt;li&gt;Read the current allowance and credit state.&lt;/li&gt;
&lt;li&gt;Reject requests that are obviously ineligible.&lt;/li&gt;
&lt;li&gt;Acquire the active-generation lock.&lt;/li&gt;
&lt;li&gt;Moderate the image.&lt;/li&gt;
&lt;li&gt;Run the final authoritative eligibility checks.&lt;/li&gt;
&lt;li&gt;Upload the image and submit the generation job.&lt;/li&gt;
&lt;li&gt;Persist the job metadata.&lt;/li&gt;
&lt;li&gt;Consume the correct allowance or credit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The early check can reject obvious cases, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an anonymous visitor who has exhausted the daily allowance&lt;/li&gt;
&lt;li&gt;a signed-in user who has reached the relevant limit&lt;/li&gt;
&lt;li&gt;a user with no free generations and no paid credits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But this early check is deliberately advisory.&lt;/p&gt;

&lt;p&gt;It does not consume anything, and it does not authorize the final generation.&lt;/p&gt;

&lt;p&gt;It only answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this request clearly unable to continue?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why the early check cannot be authoritative
&lt;/h2&gt;

&lt;p&gt;A quota read can become stale immediately.&lt;/p&gt;

&lt;p&gt;Suppose a user has one free generation remaining and sends two requests at nearly the same time.&lt;/p&gt;

&lt;p&gt;Both requests may see the same initial state.&lt;/p&gt;

&lt;p&gt;A paid-credit balance can also change between the early check and the point where the provider job is submitted.&lt;/p&gt;

&lt;p&gt;The final check therefore answers a different question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this request allowed to consume a generation right now?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The two checks may look repetitive, but they have different responsibilities.&lt;/p&gt;

&lt;p&gt;The early check avoids unnecessary work.&lt;/p&gt;

&lt;p&gt;The final check protects the state change.&lt;/p&gt;

&lt;p&gt;This also means the advisory check should fail open when it cannot make a confident decision. A temporary problem in an optimization step should not become a new reason to reject legitimate users.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lock created the most dangerous failure mode
&lt;/h2&gt;

&lt;p&gt;AIMakeTattoo allows only one active generation per user.&lt;/p&gt;

&lt;p&gt;That reduces accidental double submissions and prevents concurrent requests from trying to use the same allowance.&lt;/p&gt;

&lt;p&gt;The route acquires an active-job lock before the expensive part of the workflow.&lt;/p&gt;

&lt;p&gt;The obvious rejection paths released that lock correctly.&lt;/p&gt;

&lt;p&gt;The more dangerous issue was an unexpected exception after the lock had already been acquired.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user starts a generation.&lt;/li&gt;
&lt;li&gt;The application acquires the lock.&lt;/li&gt;
&lt;li&gt;The moderation service throws unexpectedly.&lt;/li&gt;
&lt;li&gt;The request fails.&lt;/li&gt;
&lt;li&gt;The lock remains.&lt;/li&gt;
&lt;li&gt;The next attempt is rejected because the application still thinks a job is active.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The original failure may have been temporary.&lt;/p&gt;

&lt;p&gt;The leaked lock turns it into a persistent product problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every post-lock operation needs a cleanup path
&lt;/h2&gt;

&lt;p&gt;The lesson was broader than image moderation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Once a lock has been acquired, every later operation must live inside a cleanup boundary.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That includes failures during:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;moderation&lt;/li&gt;
&lt;li&gt;final quota validation&lt;/li&gt;
&lt;li&gt;paid-balance validation&lt;/li&gt;
&lt;li&gt;provider upload&lt;/li&gt;
&lt;li&gt;provider job submission&lt;/li&gt;
&lt;li&gt;local job persistence&lt;/li&gt;
&lt;li&gt;allowance or credit consumption&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The simplified structure looks like this:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
ts
const lock = await acquireActiveJobLock(userKey);

try {
  await moderateImage(file);
  await runAuthoritativeEligibilityChecks(user);
  await uploadAndSubmitGeneration(file, controls);
  await persistJobMetadata();
} catch (error) {
  await releaseActiveJobLock(lock);
  throw error;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Built a Roman Numeral Tattoo Generator, Then Realized Conversion Was the Easy Part</title>
      <dc:creator>Warren</dc:creator>
      <pubDate>Wed, 22 Jul 2026 07:25:15 +0000</pubDate>
      <link>https://dev.to/warrenshi/i-built-a-roman-numeral-tattoo-generator-then-realized-conversion-was-the-easy-part-5ac</link>
      <guid>https://dev.to/warrenshi/i-built-a-roman-numeral-tattoo-generator-then-realized-conversion-was-the-easy-part-5ac</guid>
      <description>&lt;p&gt;I thought a Roman numeral tattoo generator would be a very small feature.&lt;/p&gt;

&lt;p&gt;The original workflow looked obvious:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enter a date&lt;/li&gt;
&lt;li&gt;Convert it into Roman numerals&lt;/li&gt;
&lt;li&gt;Show the result&lt;/li&gt;
&lt;li&gt;Let the user copy or download it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The conversion logic itself was not difficult.&lt;/p&gt;

&lt;p&gt;What surprised me was that users were not really asking only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What is this date in Roman numerals?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They were also asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Will this still work as a tattoo?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That second question changed the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first problem was not Roman numerals
&lt;/h2&gt;

&lt;p&gt;It was date ambiguity.&lt;/p&gt;

&lt;p&gt;A date like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;04/11/1992
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;April 11, 1992&lt;/li&gt;
&lt;li&gt;November 4, 1992&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both interpretations are valid depending on the user’s country.&lt;/p&gt;

&lt;p&gt;A converter can produce a mathematically correct result for the wrong intended date.&lt;/p&gt;

&lt;p&gt;The safer workflow begins with the date written in words:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;April 11, 1992
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then each section can be converted separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4 → IV
11 → XI
1992 → MCMXCII
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final month-day-year version becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IV · XI · MCMXCII
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The conversion is correct only after the date order is clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roman numerals do not preserve modern digits
&lt;/h2&gt;

&lt;p&gt;Another source of confusion is zero.&lt;/p&gt;

&lt;p&gt;Traditional Roman numerals do not have a standard symbol for zero.&lt;/p&gt;

&lt;p&gt;That means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;09 → IX
2004 → MMIV
2020 → MMXX
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Users sometimes expect every modern digit to remain visible in the converted result.&lt;/p&gt;

&lt;p&gt;Roman numeral conversion works with the value of the complete number, not each individual digit.&lt;/p&gt;

&lt;p&gt;This is a small historical detail, but it matters when the result may become permanent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correct conversion was still not enough
&lt;/h2&gt;

&lt;p&gt;Once the date was correct, the next questions appeared:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should the date use dots, slashes, or spaces?&lt;/li&gt;
&lt;li&gt;Should it be month-first or day-first?&lt;/li&gt;
&lt;li&gt;Should the full date be used, or only the year?&lt;/li&gt;
&lt;li&gt;Will the chosen font remain readable at tattoo size?&lt;/li&gt;
&lt;li&gt;Is the date too long for the wrist?&lt;/li&gt;
&lt;li&gt;Should the layout be horizontal or stacked?&lt;/li&gt;
&lt;li&gt;Will repeated &lt;code&gt;I&lt;/code&gt; characters merge together?&lt;/li&gt;
&lt;li&gt;Is the preview a design idea or a final stencil?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, the tool was no longer just a converter.&lt;/p&gt;

&lt;p&gt;It had become a small planning workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separators change the design more than expected
&lt;/h2&gt;

&lt;p&gt;The same date can look very different depending on the separator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IV · XI · MCMXCII
IV / XI / MCMXCII
IV – XI – MCMXCII
IV | XI | MCMXCII
IV XI MCMXCII
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dots feel compact and classical.&lt;/p&gt;

&lt;p&gt;Slashes resemble a modern written date.&lt;/p&gt;

&lt;p&gt;Spaces create a minimal look, but the date sections may become less obvious.&lt;/p&gt;

&lt;p&gt;Vertical lines create stronger separation, but they can also resemble the Roman numeral &lt;code&gt;I&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A tiny separator that looks clear on a large screen may nearly disappear when the design is reduced to tattoo size.&lt;/p&gt;

&lt;p&gt;Punctuation is not only decorative. It affects readability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Font previews can be misleading
&lt;/h2&gt;

&lt;p&gt;A Roman numeral date often contains repeated straight characters.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VIII · III · MMXXIII
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can look clear in a large digital preview.&lt;/p&gt;

&lt;p&gt;At a smaller physical size, the repeated &lt;code&gt;I&lt;/code&gt; characters may become difficult to count.&lt;/p&gt;

&lt;p&gt;Decorative fonts create additional risks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Thin serifs may disappear&lt;/li&gt;
&lt;li&gt;Gothic details may merge&lt;/li&gt;
&lt;li&gt;Script flourishes may cross the numerals&lt;/li&gt;
&lt;li&gt;Narrow spacing may make separate date sections look connected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A visually impressive font is not automatically a practical tattoo direction.&lt;/p&gt;

&lt;p&gt;The user still needs to evaluate the design at approximately the intended physical size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Placement changes the format
&lt;/h2&gt;

&lt;p&gt;A short year such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MMXXIV
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can fit into many small placements.&lt;/p&gt;

&lt;p&gt;A full date such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;XII · XXVIII · MMXXIV
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;needs significantly more width.&lt;/p&gt;

&lt;p&gt;Trying to force both into the same small wrist layout usually means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Smaller characters&lt;/li&gt;
&lt;li&gt;Tighter spacing&lt;/li&gt;
&lt;li&gt;Thinner lines&lt;/li&gt;
&lt;li&gt;Less readable separators&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A stacked layout may work better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;XII
XXVIII
MMXXIV
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user may also decide that the year alone carries enough meaning.&lt;/p&gt;

&lt;p&gt;The best format depends on the body area, not only the number.&lt;/p&gt;

&lt;h2&gt;
  
  
  A generated image is not a final tattoo stencil
&lt;/h2&gt;

&lt;p&gt;This became an important product boundary.&lt;/p&gt;

&lt;p&gt;A digital preview can communicate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The date&lt;/li&gt;
&lt;li&gt;The general font direction&lt;/li&gt;
&lt;li&gt;Spacing preferences&lt;/li&gt;
&lt;li&gt;Separator style&lt;/li&gt;
&lt;li&gt;Horizontal or vertical layout&lt;/li&gt;
&lt;li&gt;Supporting visual details&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it does not fully account for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Body curvature&lt;/li&gt;
&lt;li&gt;Physical line thickness&lt;/li&gt;
&lt;li&gt;Skin movement&lt;/li&gt;
&lt;li&gt;Healing&lt;/li&gt;
&lt;li&gt;Long-term readability&lt;/li&gt;
&lt;li&gt;The tattoo artist’s technique&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I stopped describing the output as a final tattoo design.&lt;/p&gt;

&lt;p&gt;The more accurate role is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A visual reference to discuss with a tattoo artist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The artist may still redraw the lettering, increase the spacing, simplify details, or change the size.&lt;/p&gt;

&lt;p&gt;That is not a failure of the generator. It is part of adapting a digital concept to skin.&lt;/p&gt;

&lt;h2&gt;
  
  
  I separated it from the general tattoo generator
&lt;/h2&gt;

&lt;p&gt;Originally, Roman numerals could have remained one option inside a larger AI tattoo tool.&lt;/p&gt;

&lt;p&gt;But the user intent was different.&lt;/p&gt;

&lt;p&gt;Someone searching for a Roman numeral tattoo usually already knows the core subject.&lt;/p&gt;

&lt;p&gt;They are not asking the system to invent a dragon, flower, or abstract concept.&lt;/p&gt;

&lt;p&gt;They need help with a narrower sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;meaningful date
→ correct conversion
→ visual format
→ readable reference
→ artist discussion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That justified a dedicated page and workflow.&lt;/p&gt;

&lt;p&gt;It also made the interface simpler.&lt;/p&gt;

&lt;p&gt;The tool did not need every control from the general tattoo generator. It needed controls and explanations specifically related to dates and lettering.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SEO lesson was similar
&lt;/h2&gt;

&lt;p&gt;At first, I treated several search terms as variations of the same keyword:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Roman numeral converter&lt;/li&gt;
&lt;li&gt;Roman numeral tattoo generator&lt;/li&gt;
&lt;li&gt;Roman numeral tattoo font&lt;/li&gt;
&lt;li&gt;Roman numeral date tattoo&lt;/li&gt;
&lt;li&gt;Roman numeral tattoo creator&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the intent behind them is not identical.&lt;/p&gt;

&lt;p&gt;A generic converter answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What number does this become?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A tattoo-focused tool must also address:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How should this date be presented?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Format&lt;/li&gt;
&lt;li&gt;Style&lt;/li&gt;
&lt;li&gt;Spacing&lt;/li&gt;
&lt;li&gt;Readability&lt;/li&gt;
&lt;li&gt;Placement&lt;/li&gt;
&lt;li&gt;Verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most useful landing page is not the one that repeats “Roman numeral tattoo generator” the most.&lt;/p&gt;

&lt;p&gt;It is the one that resolves the decisions hidden inside that search.&lt;/p&gt;

&lt;h2&gt;
  
  
  The product improved when it made fewer promises
&lt;/h2&gt;

&lt;p&gt;The earliest framing was close to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Generate a Roman numeral tattoo design.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sounds stronger, but it is also less accurate.&lt;/p&gt;

&lt;p&gt;The current framing is closer to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Convert a meaningful date, compare visual directions, and prepare a clearer reference for your tattoo artist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is less dramatic, but it reflects the real workflow better.&lt;/p&gt;

&lt;p&gt;It also avoids implying that a digital preview should be tattooed directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would build first now
&lt;/h2&gt;

&lt;p&gt;If I were rebuilding the feature from the beginning, I would prioritize:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An unambiguous date input&lt;/li&gt;
&lt;li&gt;Clear month-day-year and day-month-year handling&lt;/li&gt;
&lt;li&gt;Separate conversion of the month, day, and year&lt;/li&gt;
&lt;li&gt;Full-date and year-only comparisons&lt;/li&gt;
&lt;li&gt;Several simple separator formats&lt;/li&gt;
&lt;li&gt;Readability guidance&lt;/li&gt;
&lt;li&gt;A clear “visual reference, not final stencil” boundary&lt;/li&gt;
&lt;li&gt;A compact summary the user can discuss with an artist&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I would not begin with dozens of decorative styles.&lt;/p&gt;

&lt;p&gt;Correctness and clarity are more valuable than visual variety when the output represents a permanent date.&lt;/p&gt;

&lt;h2&gt;
  
  
  The broader lesson
&lt;/h2&gt;

&lt;p&gt;The code that converts a date into Roman numerals was the easiest part.&lt;/p&gt;

&lt;p&gt;The real product work was identifying all the decisions surrounding that conversion.&lt;/p&gt;

&lt;p&gt;This pattern appears in many small tools.&lt;/p&gt;

&lt;p&gt;A user may arrive asking for a simple output, but the useful product often needs to support the next decision as well.&lt;/p&gt;

&lt;p&gt;For this tool, the real workflow was not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;date → Roman numerals
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;date
→ verified meaning
→ Roman numeral format
→ readable visual reference
→ tattoo artist adaptation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I eventually turned that workflow into a small &lt;a href="https://aimaketattoo.com/roman-numeral-tattoo-generator" rel="noopener noreferrer"&gt;Roman numeral tattoo generator&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The conversion still takes seconds.&lt;/p&gt;

&lt;p&gt;Understanding what the user actually needs around it took much longer.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>buildinpublic</category>
      <category>seo</category>
      <category>product</category>
    </item>
    <item>
      <title>I Split One Tattoo Text Tool Into Three Different Workflows</title>
      <dc:creator>Warren</dc:creator>
      <pubDate>Fri, 17 Jul 2026 03:43:37 +0000</pubDate>
      <link>https://dev.to/warrenshi/i-split-one-tattoo-text-tool-into-three-different-workflows-2n25</link>
      <guid>https://dev.to/warrenshi/i-split-one-tattoo-text-tool-into-three-different-workflows-2n25</guid>
      <description>&lt;p&gt;At first, I assumed every tattoo text tool could share one simple workflow:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Enter text, choose a style, and preview the result.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That assumption worked for an early version.&lt;/p&gt;

&lt;p&gt;It also created a product that was too broad.&lt;/p&gt;

&lt;p&gt;A person comparing fonts, a person planning a name tattoo, and a person asking AI to create custom lettering may all type words into a box, but they are not trying to complete the same task.&lt;/p&gt;

&lt;p&gt;Over time, I split the original idea into three workflows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tattoo font preview&lt;/li&gt;
&lt;li&gt;Name tattoo planning&lt;/li&gt;
&lt;li&gt;AI tattoo lettering composition&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The interfaces still overlap, but the expected outputs are very different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow 1: Previewing tattoo fonts
&lt;/h2&gt;

&lt;p&gt;The first user already knows what text they want.&lt;/p&gt;

&lt;p&gt;Their main questions are usually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What does this word look like in script?&lt;/li&gt;
&lt;li&gt;Is Old English too heavy?&lt;/li&gt;
&lt;li&gt;Will this date remain readable?&lt;/li&gt;
&lt;li&gt;Should I use serif, sans serif, or handwritten lettering?&lt;/li&gt;
&lt;li&gt;Can I download a simple preview?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This does not require AI generation.&lt;/p&gt;

&lt;p&gt;It needs a fast font workbench:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enter the text&lt;/li&gt;
&lt;li&gt;Browse font categories&lt;/li&gt;
&lt;li&gt;Adjust the size&lt;/li&gt;
&lt;li&gt;Compare readable options&lt;/li&gt;
&lt;li&gt;Download a simple preview&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The output is intentionally limited.&lt;/p&gt;

&lt;p&gt;It is a font reference, not a custom tattoo composition.&lt;/p&gt;

&lt;p&gt;That distinction matters because adding flowers, shadows, frames, symbols, and decorative flourishes would make the tool slower and less predictable for users who only want to compare letterforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow 2: Planning a name tattoo
&lt;/h2&gt;

&lt;p&gt;A name tattoo sounds like a font problem, but it often becomes a structure problem.&lt;/p&gt;

&lt;p&gt;Different inputs behave differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A short first name&lt;/li&gt;
&lt;li&gt;Initials&lt;/li&gt;
&lt;li&gt;Two names joined by an ampersand&lt;/li&gt;
&lt;li&gt;A hyphenated name&lt;/li&gt;
&lt;li&gt;A name with a date&lt;/li&gt;
&lt;li&gt;A longer multi-word phrase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, &lt;code&gt;Mia&lt;/code&gt; can support expressive script lettering.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;A.B. 2014&lt;/code&gt; usually needs a more compact and structured direction.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Mia &amp;amp; Noah&lt;/code&gt; introduces balance between two names.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Anne-Marie&lt;/code&gt; should not be treated the same way as two separate words.&lt;/p&gt;

&lt;p&gt;The useful part of a dedicated name workflow is not showing every available font. It is helping the user decide what kind of lettering direction fits the structure of the name.&lt;/p&gt;

&lt;p&gt;That led me to build a focused &lt;a href="https://aimaketattoo.com/name-tattoo-generator" rel="noopener noreferrer"&gt;Name Tattoo Generator&lt;/a&gt; that classifies the input and recommends a smaller set of directions.&lt;/p&gt;

&lt;p&gt;It does not try to replace the full font browser.&lt;/p&gt;

&lt;p&gt;It acts as a planning entry point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow 3: Generating custom tattoo lettering
&lt;/h2&gt;

&lt;p&gt;The third user wants something beyond a font preview.&lt;/p&gt;

&lt;p&gt;They may ask for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A name with flowers&lt;/li&gt;
&lt;li&gt;A date with ornamental framing&lt;/li&gt;
&lt;li&gt;Chicano lettering with soft shading&lt;/li&gt;
&lt;li&gt;A phrase with controlled flourishes&lt;/li&gt;
&lt;li&gt;Initials combined with symbols&lt;/li&gt;
&lt;li&gt;A memorial composition&lt;/li&gt;
&lt;li&gt;A custom arrangement rather than a plain text sample&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where AI generation becomes useful.&lt;/p&gt;

&lt;p&gt;It also introduces a difficult problem: the model must distinguish between text that must appear and visual instructions that must not appear as extra words.&lt;/p&gt;

&lt;p&gt;A user might enter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exact Text:
Amelia
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Supporting Details:
Soft shading, subtle drips, wider spacing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without a clear boundary, the model may try to render parts of the supporting description as text.&lt;/p&gt;

&lt;p&gt;The interface therefore separates two fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exact Text
&lt;/h3&gt;

&lt;p&gt;The characters that must appear exactly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supporting Details
&lt;/h3&gt;

&lt;p&gt;Instructions about decoration, spacing, mood, shading, symbols, and composition.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://aimaketattoo.com/ai-tattoo-lettering-generator" rel="noopener noreferrer"&gt;AI Tattoo Lettering Generator&lt;/a&gt; then builds a visual reference while trying to preserve the exact text character by character.&lt;/p&gt;

&lt;p&gt;This is a different product job from choosing a font.&lt;/p&gt;

&lt;p&gt;The output is no longer a simple preview. It is a custom visual direction that can support a later discussion with a tattoo artist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search data exposed the overlap
&lt;/h2&gt;

&lt;p&gt;One reason I revisited the product structure was that search engines continued associating name, font, lettering, and text-related queries with the same broad font page.&lt;/p&gt;

&lt;p&gt;Queries such as these can look similar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tattoo font generator
name tattoo generator
tattoo lettering generator
tattoo text generator
tattoo name creator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the interfaces implied by those searches are not identical.&lt;/p&gt;

&lt;p&gt;A font generator should optimize for comparison.&lt;/p&gt;

&lt;p&gt;A name generator should optimize for guidance.&lt;/p&gt;

&lt;p&gt;An AI lettering generator should optimize for custom composition.&lt;/p&gt;

&lt;p&gt;Trying to make one page serve every variation can produce a page that mentions everything but performs no single task particularly well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting pages is not always the answer
&lt;/h2&gt;

&lt;p&gt;This does not mean every keyword deserves its own route.&lt;/p&gt;

&lt;p&gt;Creating separate pages for small wording differences would produce thin and repetitive content.&lt;/p&gt;

&lt;p&gt;The split only made sense because the three workflows had different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User intent&lt;/li&gt;
&lt;li&gt;Interface requirements&lt;/li&gt;
&lt;li&gt;Output formats&lt;/li&gt;
&lt;li&gt;Product boundaries&lt;/li&gt;
&lt;li&gt;Next actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test I now use is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Would this user need a meaningfully different workflow, or only different wording?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When the workflow is the same, it should probably remain one page.&lt;/p&gt;

&lt;p&gt;When the controls and expected result are genuinely different, a separate tool may be justified.&lt;/p&gt;

&lt;h2&gt;
  
  
  The broader lesson
&lt;/h2&gt;

&lt;p&gt;Many products begin with a broad input box because it is the fastest way to ship.&lt;/p&gt;

&lt;p&gt;That is useful for testing.&lt;/p&gt;

&lt;p&gt;But one input box can hide several distinct jobs.&lt;/p&gt;

&lt;p&gt;The next stage is not always adding more options to the same screen. Sometimes it means removing options and creating clearer paths.&lt;/p&gt;

&lt;p&gt;In this case, the final structure became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Font preview
→ Compare letterforms

Name planning
→ Choose a suitable direction

AI lettering
→ Build a custom visual composition
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tools are related, but they no longer pretend to be the same product.&lt;/p&gt;

&lt;p&gt;That separation has made the interfaces easier to explain, easier to maintain, and more honest about what each output can actually do.&lt;/p&gt;

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

&lt;p&gt;I would still start with one broad workflow.&lt;/p&gt;

&lt;p&gt;It is often the fastest way to learn whether users care about the problem at all.&lt;/p&gt;

&lt;p&gt;But I would pay closer attention to where users begin asking for different outcomes.&lt;/p&gt;

&lt;p&gt;Some users want speed and predictability.&lt;/p&gt;

&lt;p&gt;Some want guidance.&lt;/p&gt;

&lt;p&gt;Some want a custom generated composition.&lt;/p&gt;

&lt;p&gt;Those differences should eventually appear in the interface.&lt;/p&gt;

&lt;p&gt;A broad MVP is useful for discovery.&lt;/p&gt;

&lt;p&gt;A clearer set of focused workflows is useful for growth.&lt;/p&gt;

&lt;p&gt;For AIMakeTattoo, the separation now looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the font workflow to compare letterforms&lt;/li&gt;
&lt;li&gt;Use the name workflow to narrow down a direction&lt;/li&gt;
&lt;li&gt;Use the AI lettering workflow to create a more customized visual reference&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next challenge is not adding more options.&lt;/p&gt;

&lt;p&gt;It is making each path easier to discover and making the handoff between them feel natural.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>webdev</category>
      <category>product</category>
      <category>ai</category>
    </item>
    <item>
      <title>Google Indexed My Pages. Nobody Found Them.</title>
      <dc:creator>Warren</dc:creator>
      <pubDate>Tue, 09 Jun 2026 09:16:12 +0000</pubDate>
      <link>https://dev.to/warrenshi/google-indexed-my-pages-nobody-found-them-2h95</link>
      <guid>https://dev.to/warrenshi/google-indexed-my-pages-nobody-found-them-2h95</guid>
      <description>&lt;p&gt;A few weeks ago I thought I had a technical SEO problem.&lt;/p&gt;

&lt;p&gt;Pages weren't showing up.&lt;/p&gt;

&lt;p&gt;Search Console looked empty.&lt;/p&gt;

&lt;p&gt;So I spent a lot of time worrying about indexing.&lt;/p&gt;

&lt;p&gt;Then the pages finally got indexed.&lt;/p&gt;

&lt;p&gt;Nothing happened.&lt;/p&gt;

&lt;p&gt;That was the moment I realized indexing and understanding are completely different problems.&lt;/p&gt;

&lt;p&gt;I'm building a small browser-side utility.&lt;/p&gt;

&lt;p&gt;The product removes metadata from photos and documents.&lt;/p&gt;

&lt;p&gt;From a technical perspective the site was fine.&lt;/p&gt;

&lt;p&gt;Pages were crawlable.&lt;br&gt;
Sitemap was submitted.&lt;br&gt;
Search Console showed indexing progress.&lt;/p&gt;

&lt;p&gt;But search traffic still barely moved.&lt;/p&gt;

&lt;p&gt;At first I thought Google needed more time.&lt;/p&gt;

&lt;p&gt;Then I started looking at the queries that actually appeared.&lt;/p&gt;

&lt;p&gt;Something interesting showed up.&lt;/p&gt;

&lt;p&gt;Users searched for things like:&lt;/p&gt;

&lt;p&gt;"remove location from photo"&lt;/p&gt;

&lt;p&gt;"remove gps from image"&lt;/p&gt;

&lt;p&gt;"remove author from pdf"&lt;/p&gt;

&lt;p&gt;Almost nobody searched for:&lt;/p&gt;

&lt;p&gt;"metadata processing"&lt;/p&gt;

&lt;p&gt;"metadata extraction"&lt;/p&gt;

&lt;p&gt;"browser-side metadata cleanup"&lt;/p&gt;

&lt;p&gt;Those were the phrases I had been using when describing the product.&lt;/p&gt;

&lt;p&gt;The product and the user were talking about the same thing.&lt;/p&gt;

&lt;p&gt;Just in completely different languages.&lt;/p&gt;

&lt;p&gt;That changed how I think about SEO.&lt;/p&gt;

&lt;p&gt;I used to assume:&lt;/p&gt;

&lt;p&gt;Indexing&lt;br&gt;
→ Ranking&lt;br&gt;
→ Traffic&lt;/p&gt;

&lt;p&gt;Now it feels more like:&lt;/p&gt;

&lt;p&gt;Indexing&lt;br&gt;
→ Understanding&lt;br&gt;
→ Ranking&lt;br&gt;
→ Traffic&lt;/p&gt;

&lt;p&gt;And the understanding step is surprisingly slow.&lt;/p&gt;

&lt;p&gt;Especially for small sites.&lt;/p&gt;

&lt;p&gt;The weird part is that AI makes this easier to miss.&lt;/p&gt;

&lt;p&gt;Building is cheaper than ever.&lt;/p&gt;

&lt;p&gt;Adding another page takes minutes.&lt;/p&gt;

&lt;p&gt;Adding another feature takes hours.&lt;/p&gt;

&lt;p&gt;Adding another tool feels almost free.&lt;/p&gt;

&lt;p&gt;So it's easy to create more things.&lt;/p&gt;

&lt;p&gt;But search engines still need to understand what those things are.&lt;/p&gt;

&lt;p&gt;Lately I've been spending less time building pages and more time trying to understand how users describe the problem in their own words.&lt;/p&gt;

&lt;p&gt;That has probably taught me more than any SEO guide I've read so far.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>buildinpublic</category>
      <category>indiehacker</category>
    </item>
    <item>
      <title>AI makes building faster, but semantic distribution is now the hard part</title>
      <dc:creator>Warren</dc:creator>
      <pubDate>Thu, 21 May 2026 04:49:33 +0000</pubDate>
      <link>https://dev.to/warrenshi/ai-makes-building-faster-but-semantic-distribution-is-now-the-hard-part-3jc1</link>
      <guid>https://dev.to/warrenshi/ai-makes-building-faster-but-semantic-distribution-is-now-the-hard-part-3jc1</guid>
      <description>&lt;p&gt;Shipping products feels dramatically easier now.&lt;/p&gt;

&lt;p&gt;Between AI coding tools, templates, and browser-side tooling, I can build and deploy things faster than ever.&lt;/p&gt;

&lt;p&gt;But distribution feels harder.&lt;/p&gt;

&lt;p&gt;Not technically harder.&lt;/p&gt;

&lt;p&gt;Semantically harder.&lt;/p&gt;

&lt;p&gt;A lot of the work now is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;understanding how users actually search&lt;/li&gt;
&lt;li&gt;matching workflows instead of technical terms&lt;/li&gt;
&lt;li&gt;figuring out where intent really exists&lt;/li&gt;
&lt;li&gt;building trust before distribution channels suppress you&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building became compressed.&lt;/p&gt;

&lt;p&gt;Attention became fragmented.&lt;/p&gt;

&lt;p&gt;Honestly feels like distribution is becoming the real product skill now.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>ai</category>
      <category>webdev</category>
      <category>seo</category>
    </item>
    <item>
      <title>Most people search “remove location from photo”, not “EXIF”</title>
      <dc:creator>Warren</dc:creator>
      <pubDate>Fri, 15 May 2026 08:49:20 +0000</pubDate>
      <link>https://dev.to/warrenshi/most-people-search-remove-location-from-photo-not-exif-5186</link>
      <guid>https://dev.to/warrenshi/most-people-search-remove-location-from-photo-not-exif-5186</guid>
      <description>&lt;p&gt;Most people search “remove location from photo”, not “EXIF”&lt;/p&gt;

&lt;p&gt;While building a metadata removal workflow, I realized something interesting:&lt;/p&gt;

&lt;p&gt;Most people never search for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EXIF&lt;/li&gt;
&lt;li&gt;IPTC&lt;/li&gt;
&lt;li&gt;document properties&lt;/li&gt;
&lt;li&gt;metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They search for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“remove location from photo”&lt;/li&gt;
&lt;li&gt;“hidden info in PDF”&lt;/li&gt;
&lt;li&gt;“remove GPS from image”&lt;/li&gt;
&lt;li&gt;“does this file still contain private data?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The technical layer and the user intent layer are often completely different.&lt;/p&gt;

&lt;p&gt;Developers think in systems.&lt;/p&gt;

&lt;p&gt;Users think in outcomes.&lt;/p&gt;

&lt;p&gt;That changed how I started thinking about both SEO and product design.&lt;/p&gt;

&lt;p&gt;A lot of users are not trying to “inspect metadata”.&lt;/p&gt;

&lt;p&gt;They’re usually just at the moment before sharing something publicly and wondering:&lt;/p&gt;

&lt;p&gt;“wait… is there still something hidden in this file?”&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>webdev</category>
      <category>metadata</category>
      <category>seo</category>
    </item>
  </channel>
</rss>
