<?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: visiohex</title>
    <description>The latest articles on DEV Community by visiohex (@jy_wang_2c4d36f4d66adea80).</description>
    <link>https://dev.to/jy_wang_2c4d36f4d66adea80</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3765913%2F6890e795-c29f-462e-a420-54ad45b80086.png</url>
      <title>DEV Community: visiohex</title>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jy_wang_2c4d36f4d66adea80"/>
    <language>en</language>
    <item>
      <title>I built a browser-only tool to remove visible Gemini watermarks</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Sat, 18 Apr 2026 14:13:22 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-a-browser-only-tool-to-remove-visible-gemini-watermarks-45ke</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-a-browser-only-tool-to-remove-visible-gemini-watermarks-45ke</guid>
      <description>&lt;p&gt;I built a small tool called &lt;a href="https://watermarkzero.org/" rel="noopener noreferrer"&gt;WatermarkZero&lt;/a&gt; to handle one specific case: the visible Gemini watermark that shows up in the bottom-right corner of supported Gemini-generated images.&lt;/p&gt;

&lt;p&gt;Site: &lt;a href="https://watermarkzero.org/" rel="noopener noreferrer"&gt;https://watermarkzero.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/liuyan-wjy/watermarkzero" rel="noopener noreferrer"&gt;https://github.com/liuyan-wjy/watermarkzero&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most tools I found went in one of three directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;they cropped the corner&lt;/li&gt;
&lt;li&gt;they pushed the file through a remote server&lt;/li&gt;
&lt;li&gt;they were vague about what they actually supported&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something narrower and more explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What WatermarkZero does
&lt;/h2&gt;

&lt;p&gt;WatermarkZero runs in the browser and focuses on the visible Gemini mark only. You upload a JPG, PNG, or WebP image, preview the result, and download the cleaned version without sending the file to a backend.&lt;/p&gt;

&lt;p&gt;That constraint shaped the whole project. I was not trying to build a general watermark remover. I wanted a tool with a clear boundary and a workflow that people could understand in a few seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why local processing mattered
&lt;/h2&gt;

&lt;p&gt;Image tools often ask you to upload files before they explain anything. That works for some use cases, but it feels wrong for screenshots, drafts, or personal images that you do not want sitting on a third-party server.&lt;/p&gt;

&lt;p&gt;So I kept the core flow local:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;image selection happens in the browser&lt;/li&gt;
&lt;li&gt;processing happens in the browser&lt;/li&gt;
&lt;li&gt;downloading happens in the browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That also forced me to keep the UX honest. If the restoration is weak on a certain image, you see it right away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope matters more than hype
&lt;/h2&gt;

&lt;p&gt;One thing I tried to avoid was pretending this works on everything.&lt;/p&gt;

&lt;p&gt;WatermarkZero is built for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the visible Gemini watermark&lt;/li&gt;
&lt;li&gt;supported Gemini-generated images&lt;/li&gt;
&lt;li&gt;cases where you want to keep the full frame instead of cropping the corner&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not built for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hidden watermarking systems like SynthID&lt;/li&gt;
&lt;li&gt;every kind of logo or watermark on the internet&lt;/li&gt;
&lt;li&gt;guaranteed success on every textured corner or gradient-heavy image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That narrower scope made the product better. It also made the copy easier to write, because I did not have to blur the edges.&lt;/p&gt;

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

&lt;p&gt;The current version includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local browser processing&lt;/li&gt;
&lt;li&gt;before/after examples&lt;/li&gt;
&lt;li&gt;downloadable output&lt;/li&gt;
&lt;li&gt;support for JPG, PNG, and WebP&lt;/li&gt;
&lt;li&gt;a few support pages that explain how it works and where it fails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also spent time on the boring parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mobile layout&lt;/li&gt;
&lt;li&gt;simple analytics&lt;/li&gt;
&lt;li&gt;Cloudflare deployment&lt;/li&gt;
&lt;li&gt;clear FAQ and privacy pages&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The harder cases
&lt;/h2&gt;

&lt;p&gt;The hardest inputs are the ones you would expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fabric folds&lt;/li&gt;
&lt;li&gt;leaves and plant textures&lt;/li&gt;
&lt;li&gt;soft gradients&lt;/li&gt;
&lt;li&gt;detailed corners where the visible mark overlaps real image detail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those cases are useful because they tell you whether the result looks restored or just blurred over.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to try it
&lt;/h2&gt;

&lt;p&gt;The tool is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://watermarkzero.org/" rel="noopener noreferrer"&gt;https://watermarkzero.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you test it, I would love to hear where it breaks down. Failure cases are more useful than generic praise on a tool this narrow.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a spoiler-safe NYT Connections helper with Astro and Cloudflare</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Tue, 14 Apr 2026 09:14:09 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-a-spoiler-safe-nyt-connections-helper-with-astro-and-cloudflare-24pf</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-a-spoiler-safe-nyt-connections-helper-with-astro-and-cloudflare-24pf</guid>
      <description>&lt;p&gt;I recently built &lt;a href="https://connections-hint.today/" rel="noopener noreferrer"&gt;Connections Hint Today&lt;/a&gt;, a spoiler-safe helper for NYT Connections.&lt;/p&gt;

&lt;p&gt;The original idea was pretty simple: most Connections helper pages solve the wrong problem.&lt;/p&gt;

&lt;p&gt;They are useful if you want the answer right now, but a lot of them jump straight to spoilers. That works for panic-click traffic, but it does not feel great if you still want to solve the board yourself. I wanted something that behaved more like a coach than an answer dump.&lt;/p&gt;

&lt;p&gt;So I built a small site around that idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;light hints first&lt;/li&gt;
&lt;li&gt;stronger hints only if needed&lt;/li&gt;
&lt;li&gt;archive pages for review&lt;/li&gt;
&lt;li&gt;Sports Edition support&lt;/li&gt;
&lt;li&gt;short explanation and trap notes for each group&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site is live here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://connections-hint.today/" rel="noopener noreferrer"&gt;https://connections-hint.today/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the repo is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/liuyan-wjy/connections-coach" rel="noopener noreferrer"&gt;https://github.com/liuyan-wjy/connections-coach&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The product idea
&lt;/h2&gt;

&lt;p&gt;The most important product decision was to treat “help” and “spoiler” as two different things.&lt;/p&gt;

&lt;p&gt;If you play Connections regularly, you usually do not want a giant answer page. You want a nudge. Maybe you are stuck between two groupings. Maybe you know one category is structural but cannot see it yet. Maybe the purple group is clearly some kind of phrase pattern, but you are not sure which one.&lt;/p&gt;

&lt;p&gt;That is a very different user need from “just show me the answer”.&lt;/p&gt;

&lt;p&gt;So the site uses a layered model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Light hint&lt;/li&gt;
&lt;li&gt;Medium hint&lt;/li&gt;
&lt;li&gt;Strong hint&lt;/li&gt;
&lt;li&gt;Reveal answer and explanation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sounds small, but it changes the whole tone of the product. Once I committed to that, the rest of the site became easier to shape: archive pages, strategy pages, and even the result analyzer all needed to support the same idea of helping without flattening the puzzle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Astro + Cloudflare
&lt;/h2&gt;

&lt;p&gt;This project is a pretty good fit for the Astro + Cloudflare stack.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;static-first pages for SEO&lt;/li&gt;
&lt;li&gt;fast page loads&lt;/li&gt;
&lt;li&gt;simple deployment&lt;/li&gt;
&lt;li&gt;low ops overhead&lt;/li&gt;
&lt;li&gt;enough server-side logic to ingest and publish daily puzzle data automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Astro gave me a clean content-heavy frontend without dragging in a lot of complexity. Cloudflare gave me a surprisingly compact deployment model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloudflare Pages for the site&lt;/li&gt;
&lt;li&gt;Workers for the ingest pipeline&lt;/li&gt;
&lt;li&gt;D1 for puzzle storage&lt;/li&gt;
&lt;li&gt;Browser Rendering as a fallback for dynamic Sports Edition capture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination let me keep the public site lightweight while still handling the scheduled automation behind the scenes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ingestion problem
&lt;/h2&gt;

&lt;p&gt;The most interesting part of the project turned out not to be the UI. It was the ingest pipeline.&lt;/p&gt;

&lt;p&gt;The main Connections board and the Sports Edition do not behave the same way. The main edition was fairly straightforward once I found the right live daily endpoint. Sports was messier.&lt;/p&gt;

&lt;p&gt;At first, I tried to capture Sports data through browser automation. That worked some of the time, but it was not stable enough for a daily publishing flow. The browser connection would occasionally time out, which is exactly the kind of failure you do not want in a project that is supposed to update on its own every day.&lt;/p&gt;

&lt;p&gt;The fix was to change the ingestion strategy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use a direct GraphQL request first&lt;/li&gt;
&lt;li&gt;keep browser capture only as fallback&lt;/li&gt;
&lt;li&gt;write both &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;sports&lt;/code&gt; puzzles into D1&lt;/li&gt;
&lt;li&gt;mark them as published only when the payload is valid&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once that was in place, daily publishing became much more reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule-based copy generation
&lt;/h2&gt;

&lt;p&gt;Another interesting challenge was content generation.&lt;/p&gt;

&lt;p&gt;I did not want every puzzle page to say the same thing with a new date stamped on it. At the same time, I also did not want to rely on live AI generation for every daily update. That would increase cost, add latency, and make the output less predictable.&lt;/p&gt;

&lt;p&gt;So I built a rule-based copy layer instead.&lt;/p&gt;

&lt;p&gt;Right now the generation works more like a lightweight editorial engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;detect whether a group is direct, structural, phrase-based, or sports-specific&lt;/li&gt;
&lt;li&gt;generate different hint and explanation styles based on that category shape&lt;/li&gt;
&lt;li&gt;vary summary copy by puzzle type&lt;/li&gt;
&lt;li&gt;keep the tone consistent across the site&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not trying to imitate a human writer perfectly. It is trying to be useful, stable, and a little less repetitive than a pure template system.&lt;/p&gt;

&lt;p&gt;That tradeoff feels right for this stage of the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO and information architecture
&lt;/h2&gt;

&lt;p&gt;Because this is a search-heavy product, structure mattered a lot from the start.&lt;/p&gt;

&lt;p&gt;I did not want the site to be just a single &lt;code&gt;/today&lt;/code&gt; page. That would make it too dependent on one expiring keyword pattern.&lt;/p&gt;

&lt;p&gt;Instead, I split it into a few clear surfaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/today&lt;/code&gt; for fast daily usage&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/puzzle/{id}&lt;/code&gt; as the canonical puzzle pages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/archive&lt;/code&gt; for review&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/sports&lt;/code&gt; for Sports Edition&lt;/li&gt;
&lt;li&gt;strategy pages for evergreen traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also made &lt;code&gt;/today&lt;/code&gt; &lt;code&gt;noindex,follow&lt;/code&gt; and pointed canonical to the actual puzzle detail page. That gave the site a cleaner search shape and reduced duplication risk.&lt;/p&gt;

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

&lt;p&gt;A few things stood out while building this:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The real product is not the answer page
&lt;/h3&gt;

&lt;p&gt;The answer page gets the initial traffic, but the real product is the habit loop around it.&lt;/p&gt;

&lt;p&gt;If someone comes back for archive review, strategy pages, or Sports Edition, that is much more valuable than a one-time click for today’s board.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Small automation details matter more than flashy features
&lt;/h3&gt;

&lt;p&gt;The site became meaningfully better once the daily ingest was reliable. That mattered more than adding another feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Distribution rules matter
&lt;/h3&gt;

&lt;p&gt;Communities that seem like an obvious fit are not always friendly to direct links. Some puzzle communities treat any link to a current-board helper as a spoiler, even if the page is intentionally spoiler-safe.&lt;/p&gt;

&lt;p&gt;That means the product and the distribution strategy have to be designed together.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. AI is optional
&lt;/h3&gt;

&lt;p&gt;A lot of projects like this immediately jump to “just use AI for the content”. I think that is often the wrong first move.&lt;/p&gt;

&lt;p&gt;For this project, deterministic rules got me much farther than I expected. They are cheaper, easier to test, and easier to reason about when something goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is next
&lt;/h2&gt;

&lt;p&gt;The next things I want to improve are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;better puzzle-specific commentary&lt;/li&gt;
&lt;li&gt;more evergreen strategy content&lt;/li&gt;
&lt;li&gt;stronger archive depth&lt;/li&gt;
&lt;li&gt;clearer post-game analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also want to keep refining the copy so it feels less like a generated system and more like a compact field guide for repeat players.&lt;/p&gt;

&lt;p&gt;If you build content-heavy side projects, I would be curious how you think about this tradeoff:&lt;/p&gt;

&lt;p&gt;When do you keep a site deterministic, and when do you add AI into the publishing path?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>astro</category>
      <category>cloudflarechallenge</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>What Is AutoResearch by Andrej Karpathy? A Clearer Guide for First-Time Readers</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Wed, 25 Mar 2026 08:59:55 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/what-is-autoresearch-by-andrej-karpathy-a-clearer-guide-for-first-time-readers-23kf</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/what-is-autoresearch-by-andrej-karpathy-a-clearer-guide-for-first-time-readers-23kf</guid>
      <description>&lt;h1&gt;
  
  
  What Is AutoResearch by Andrej Karpathy? A Clearer Guide for First-Time Readers
&lt;/h1&gt;

&lt;p&gt;Karpathy's &lt;code&gt;autoresearch&lt;/code&gt; repository has been getting a lot of attention recently.&lt;/p&gt;

&lt;p&gt;A lot of people are searching for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;autoresearch github&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;karpathy autoresearch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;autoresearch ai&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But if you're seeing the project for the first time, the raw GitHub repository can still feel a bit dense.&lt;/p&gt;

&lt;p&gt;So I built a small unofficial guide site to make the project easier to understand before diving into the source code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AutoResearch Is
&lt;/h2&gt;

&lt;p&gt;At a high level, AutoResearch is an open-source project that lets AI agents run repeated ML training experiments inside a compact codebase.&lt;/p&gt;

&lt;p&gt;Instead of manually editing everything yourself, the idea is to let an agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;modify training logic&lt;/li&gt;
&lt;li&gt;run short experiments&lt;/li&gt;
&lt;li&gt;compare results&lt;/li&gt;
&lt;li&gt;iterate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes it interesting not just as a repo, but as a preview of a different way to do ML experimentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why People Care About It
&lt;/h2&gt;

&lt;p&gt;A few things make the project stand out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it comes from Andrej Karpathy&lt;/li&gt;
&lt;li&gt;it is small enough to inspect&lt;/li&gt;
&lt;li&gt;it fits into the current wave of interest around coding agents&lt;/li&gt;
&lt;li&gt;it creates a more readable “research loop” than a giant ML stack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For developers and AI builders, it is the kind of repo that becomes popular very quickly because it is both practical and conceptually interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Made a Separate Guide
&lt;/h2&gt;

&lt;p&gt;A GitHub README is great if you already know what you're looking for.&lt;/p&gt;

&lt;p&gt;But many searchers are not actually asking for source code first. They are asking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what this repo does&lt;/li&gt;
&lt;li&gt;where to start&lt;/li&gt;
&lt;li&gt;whether it can run on smaller hardware&lt;/li&gt;
&lt;li&gt;whether cloud GPUs make more sense&lt;/li&gt;
&lt;li&gt;how it differs from similarly named projects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why I turned it into a small explainer site.&lt;/p&gt;

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

&lt;p&gt;I broke the topic into a few pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Homepage / overview: &lt;a href="https://openrepoguide.com" rel="noopener noreferrer"&gt;Open Repo Guide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub explainer: &lt;a href="https://openrepoguide.com/autoresearch-github-guide/" rel="noopener noreferrer"&gt;AutoResearch GitHub Guide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Step-by-step tutorial: &lt;a href="https://openrepoguide.com/autoresearch-tutorial/" rel="noopener noreferrer"&gt;AutoResearch Tutorial&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hardware limitations: &lt;a href="https://openrepoguide.com/run-autoresearch-on-mac-or-small-gpus/" rel="noopener noreferrer"&gt;Run AutoResearch on Mac or Smaller GPUs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Neutral infra guide: &lt;a href="https://openrepoguide.com/best-gpu-cloud-for-autoresearch/" rel="noopener noreferrer"&gt;Best GPU Cloud Options for AutoResearch&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I’m Testing
&lt;/h2&gt;

&lt;p&gt;This is also a small experiment for me.&lt;/p&gt;

&lt;p&gt;I’m exploring whether a simple content site can grow by taking fast-rising GitHub repositories and turning them into clearer, more beginner-friendly explainer pages.&lt;/p&gt;

&lt;p&gt;Not official product sites.&lt;br&gt;
More like independent project guides for people discovering these repos through search.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback Welcome
&lt;/h2&gt;

&lt;p&gt;If you’ve looked at &lt;code&gt;autoresearch&lt;/code&gt;, I’d love to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what confused you most when you first saw it&lt;/li&gt;
&lt;li&gt;whether this kind of explainer page is actually useful&lt;/li&gt;
&lt;li&gt;which GitHub repos would be worth covering next&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Built a Free AI Instagram Bio Generator in 10 Days — Here's What I Learned</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Wed, 18 Mar 2026 02:10:44 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-a-free-ai-instagram-bio-generator-in-10-days-heres-what-i-learned-4bp4</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-a-free-ai-instagram-bio-generator-in-10-days-heres-what-i-learned-4bp4</guid>
      <description>&lt;p&gt;I spent way too long staring at a blank Instagram bio box last month. 150 characters. How hard could it be?&lt;/p&gt;

&lt;p&gt;Turns out, pretty hard. So I built a tool to solve it.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://www.socialbiogen.net" rel="noopener noreferrer"&gt;SocialBioGen&lt;/a&gt;&lt;/strong&gt; — generate 3–5 personalized, emoji-packed Instagram&lt;br&gt;
   bios in under 3 seconds. No signup required.&lt;/p&gt;




&lt;p&gt;## The Problem&lt;/p&gt;

&lt;p&gt;Writing a 150-character bio that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Captures your personality or brand&lt;/li&gt;
&lt;li&gt;Uses emoji tastefully&lt;/li&gt;
&lt;li&gt;Actually makes people want to follow you&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...is surprisingly difficult. Most people end up with either a generic "Coffee lover ☕ | NYC" or just&lt;br&gt;
  leave it blank.&lt;/p&gt;

&lt;p&gt;I searched for tools and found mostly clunky, ad-heavy generators that required email signups just to&lt;br&gt;
  see results. So I built my own.&lt;/p&gt;




&lt;p&gt;## How It Works&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Choose your type&lt;/strong&gt; — Personal, Business, or Creator&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Describe yourself&lt;/strong&gt; in a few words&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a vibe&lt;/strong&gt; — Professional, Funny, Aesthetic, Minimalist, Bold, or Cute&lt;/li&gt;
&lt;li&gt;Get &lt;strong&gt;3–5 ready-to-copy bios&lt;/strong&gt; in under 3 seconds
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Input:  "Travel lover, coffee addict, based in NYC"
  Vibe:   Funny
  Type:   Personal

  Output:
  → ✈️  Fueled by coffee &amp;amp; flight miles | NYC chaos survivor ☕
  → Professional napper between flights 🌍 NYC base, world playground
  → Lost in every city, found in every café ☕✈️  NYC HQ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;## Tech Stack&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 15&lt;/strong&gt; (App Router) — SSR + SSG for SEO&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenRouter API&lt;/strong&gt; — LLM calls (GPT-4o-mini, ~$0.001/request)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neon PostgreSQL&lt;/strong&gt; — rate limiting + user credits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PayPal SDK&lt;/strong&gt; — one-time payments, no subscription complexity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; — zero-ops deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total infra cost: &lt;strong&gt;&amp;lt; $5/month&lt;/strong&gt; at moderate traffic.&lt;/p&gt;




&lt;p&gt;## The SEO Play: Programmatic Pages&lt;/p&gt;

&lt;p&gt;Beyond the main generator, I built 50+ niche landing pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/instagram-bio-for/travel&lt;/code&gt; — Travel Instagram Bio Generator&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/instagram-bio-for/fitness&lt;/code&gt; — Fitness Instagram Bio Generator&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/instagram-bio-for/photographers&lt;/code&gt; — ...you get the idea&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each page has 20+ pre-generated bio examples + the live AI generator. Built with&lt;br&gt;
  &lt;code&gt;generateStaticParams()&lt;/code&gt; in Next.js — the entire content cost &amp;lt; $1 in API calls to generate upfront.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One bug I hit:&lt;/strong&gt; &lt;code&gt;revalidate = 3600&lt;/code&gt; (ISR) conflicted with &lt;code&gt;next-intl&lt;/code&gt;'s &lt;code&gt;setRequestLocale&lt;/code&gt;, throwing&lt;br&gt;
   a &lt;code&gt;DYNAMIC_SERVER_USAGE&lt;/code&gt; 500 error in production. Fixed by switching to &lt;code&gt;dynamic = 'force-static'&lt;/code&gt; —&lt;br&gt;
  same pattern used in the Next.js docs pages.&lt;/p&gt;




&lt;p&gt;## Monetization&lt;/p&gt;

&lt;p&gt;Free tier: 5 generations/day, no signup.&lt;/p&gt;

&lt;p&gt;Paid:&lt;br&gt;
  | Plan | Price | Credits |&lt;br&gt;
  |------|-------|---------|&lt;br&gt;
  | Starter | $4.99 | 100 |&lt;br&gt;
  | Pro | $9.99 | 300 |&lt;br&gt;
  | &lt;strong&gt;Lifetime&lt;/strong&gt; | &lt;strong&gt;$19.99&lt;/strong&gt; | &lt;strong&gt;Unlimited&lt;/strong&gt; |&lt;/p&gt;

&lt;p&gt;Credits never expire. The Lifetime deal is the anchor — once you see $9.99 vs $19.99, unlimited forever&lt;br&gt;
   feels obvious.&lt;/p&gt;




&lt;p&gt;## What I'd Do Differently&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start with the SEO pages, not the generator.&lt;/strong&gt; The pSEO landing pages will drive organic traffic
long-term. I built the generator first and the SEO layer second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship earlier.&lt;/strong&gt; I spent 2 extra days polishing the UI before launch. Nobody cares about perfect
spacing on day 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limit by browser fingerprint, not just IP.&lt;/strong&gt; Shared IPs (offices, universities) can exhaust
limits for unrelated users.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;## Try It&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://www.socialbiogen.net" rel="noopener noreferrer"&gt;https://www.socialbiogen.net&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Free to use, no signup. Would love your feedback — especially on bio quality and edge cases where the&lt;br&gt;
  AI gives weird results.&lt;/p&gt;

&lt;p&gt;What's your Instagram bio right now? Drop it in the comments 👇&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>I Built a DLSS 5 GPU Compatibility Checker with Next.js 14 (and what I learned about AI rendering)</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Wed, 18 Mar 2026 02:09:05 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-a-dlss-5-gpu-compatibility-checker-with-nextjs-14-and-what-i-learned-about-ai-rendering-4cjh</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-a-dlss-5-gpu-compatibility-checker-with-nextjs-14-and-what-i-learned-about-ai-rendering-4cjh</guid>
      <description>&lt;p&gt;NVIDIA announced DLSS 5 at GTC on March 16, 2026 — and the internet immediately exploded with questions: &lt;em&gt;"Does my GPU support it? Is my RTX 4090 going to be left out?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I couldn't find a clean, reliable answer anywhere. So I built one.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://dlss5.net" rel="noopener noreferrer"&gt;dlss5.net&lt;/a&gt;&lt;/strong&gt; — a free GPU compatibility checker for DLSS 5&lt;/p&gt;




&lt;p&gt;## What is DLSS 5, actually?&lt;/p&gt;

&lt;p&gt;This tripped me up while building the site. A lot of sources were confusing DLSS 5 with Multi Frame Generation (MFG) — but they're completely different things:&lt;/p&gt;

&lt;p&gt;| Version | Key Feature | Focus |&lt;br&gt;
  |---------|-------------|-------|&lt;br&gt;
  | DLSS 3 | Frame Generation (1 AI frame) | Performance |&lt;br&gt;
  | DLSS 4 | Multi Frame Generation 4X | Performance |&lt;br&gt;
  | DLSS 4.5 | Dynamic MFG 6X | Performance |&lt;br&gt;
  | &lt;strong&gt;DLSS 5&lt;/strong&gt; | &lt;strong&gt;Neural Rendering&lt;/strong&gt; | &lt;strong&gt;Visual Fidelity&lt;/strong&gt; |&lt;/p&gt;

&lt;p&gt;DLSS 5's core feature is &lt;strong&gt;Real-time Neural Rendering&lt;/strong&gt; — AI analyzes scene semantics (hair, skin, fabric, lighting) and generates photoreal material responses in real time. Jensen Huang called it &lt;em&gt;"the GPT moment for graphics."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It runs &lt;em&gt;on top of&lt;/em&gt; DLSS 4.5, not instead of it.&lt;/p&gt;




&lt;p&gt;## What I built&lt;/p&gt;

&lt;p&gt;The tool lets you type any GPU name and instantly see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DLSS 5 status&lt;/strong&gt;: Confirmed / Possible / Unlikely / Not Supported&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Current DLSS 4/4.5 performance data&lt;/strong&gt; (since DLSS 5 isn't out yet)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DLSS version comparison table&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upgrade recommendations&lt;/strong&gt; if your GPU isn't confirmed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;## Tech stack&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 14&lt;/strong&gt; (App Router, fully static)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; + &lt;strong&gt;Tailwind CSS&lt;/strong&gt; + &lt;strong&gt;shadcn/ui&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuse.js&lt;/strong&gt; for fuzzy GPU name search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recharts&lt;/strong&gt; for FPS benchmark charts&lt;/li&gt;
&lt;li&gt;Deployed on &lt;strong&gt;Vercel&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The GPU data lives in a JSON file — easy to update when NVIDIA releases&lt;br&gt;
  official specs closer to the Fall 2026 launch.&lt;/p&gt;




&lt;p&gt;## Interesting data decisions&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RTX 5060 / 5060 Ti&lt;/strong&gt;: I initially marked these as "Confirmed" since they're&lt;br&gt;
  RTX 50 series. But the GTC demo ran DLSS 5 on &lt;em&gt;two&lt;/em&gt; RTX 5090s — one for the game,&lt;br&gt;
  one dedicated to the AI model. The RTX 5060 has only 8GB VRAM. I changed both&lt;br&gt;
  to &lt;strong&gt;"Unlikely"&lt;/strong&gt; until NVIDIA publishes official minimum specs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Min. GPU requirement&lt;/strong&gt;: NVIDIA's official FAQ literally says &lt;em&gt;"Minimum GPU&lt;br&gt;
  specifications are pending model optimizations and will be provided closer to release."&lt;/em&gt;&lt;br&gt;
  So the table says &lt;strong&gt;TBD&lt;/strong&gt;, not "RTX 50 confirmed."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RTX 20/30 series&lt;/strong&gt;: These support DLSS 3.5 (Super Resolution + Ray Reconstruction),&lt;br&gt;
  not just Super Resolution. Ray Reconstruction shipped with DLSS 3.5 and is&lt;br&gt;
  backwards-compatible.&lt;/p&gt;




&lt;p&gt;## What's next&lt;/p&gt;

&lt;p&gt;DLSS 5 launches Fall 2026. I'll update the site as NVIDIA releases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Official minimum GPU specs&lt;/li&gt;
&lt;li&gt;Game support list updates&lt;/li&gt;
&lt;li&gt;Actual benchmark data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site also has a &lt;strong&gt;Portuguese version&lt;/strong&gt; (&lt;a href="https://dlss5.net/pt" rel="noopener noreferrer"&gt;dlss5.net/pt&lt;/a&gt;)&lt;br&gt;
  — "dlss 5 quais placas" turned out to be a trending search query, so Brazilian&lt;br&gt;
  gamers were clearly looking for this too.&lt;/p&gt;




&lt;p&gt;If you're a gamer wondering about your GPU, check it out: &lt;strong&gt;&lt;a href="https://dlss5.net" rel="noopener noreferrer"&gt;dlss5.net&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Feedback welcome — especially if you spot any data errors. DLSS versioning is&lt;br&gt;
  genuinely confusing and I want to keep this accurate.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Built a Free Resume Bullet Rewriter for ATS (Action Verbs for Resume)</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Wed, 04 Mar 2026 08:28:21 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-a-free-resume-bullet-rewriter-for-ats-action-verbs-for-resume-2j12</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-a-free-resume-bullet-rewriter-for-ats-action-verbs-for-resume-2j12</guid>
      <description>&lt;p&gt;I recently launched &lt;strong&gt;PowerVerb&lt;/strong&gt;: &lt;a href="https://www.resumeactionverbs.com" rel="noopener noreferrer"&gt;resumeactionverbs.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s a &lt;strong&gt;resume bullet rewriter&lt;/strong&gt; focused on turning weak lines into stronger, &lt;strong&gt;ATS-friendly resume bullets&lt;/strong&gt; using better &lt;strong&gt;action verbs for resume&lt;/strong&gt; writing.&lt;/p&gt;

&lt;p&gt;If you’re searching for &lt;strong&gt;resume action verbs&lt;/strong&gt;, &lt;strong&gt;power verbs for resume&lt;/strong&gt;, or a fast way to rewrite “Responsible for…” bullets, this tool is built for that exact use case.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I built a resume bullet rewriter
&lt;/h2&gt;

&lt;p&gt;Most tools either over-generate or produce generic text.&lt;br&gt;&lt;br&gt;
I wanted a practical &lt;strong&gt;resume action verbs tool&lt;/strong&gt; that is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fast&lt;/li&gt;
&lt;li&gt;cost-efficient&lt;/li&gt;
&lt;li&gt;less likely to hallucinate&lt;/li&gt;
&lt;li&gt;useful for real job applications&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Core features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Rewrite one bullet into 3 ATS-friendly versions&lt;/li&gt;
&lt;li&gt;Suggest better &lt;strong&gt;resume action verbs by role&lt;/strong&gt; (engineering, product, marketing, sales)&lt;/li&gt;
&lt;li&gt;Detect weak/repetitive wording&lt;/li&gt;
&lt;li&gt;Save and reuse recent rewrites&lt;/li&gt;
&lt;li&gt;Daily free quota + paid credits/subscription&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: Next.js (App Router)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DB/Auth&lt;/strong&gt;: Supabase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite engine&lt;/strong&gt;: Rule engine + OpenRouter fallback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments&lt;/strong&gt;: PayPal (orders + subscriptions + webhook idempotency)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy&lt;/strong&gt;: Vercel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS/CDN&lt;/strong&gt;: Cloudflare&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why “rule-first + AI-assisted”?
&lt;/h2&gt;

&lt;p&gt;Initially, I used AI for everything. It worked, but costs and latency were unstable, and weak-verb checks were overkill for LLMs.&lt;/p&gt;

&lt;p&gt;So I split the system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic rules&lt;/strong&gt; for repetition checking + weak verb detection
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI&lt;/strong&gt; only for rewrite generation (with strict constraints)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This reduced token usage and made UX feel much faster.&lt;/p&gt;




&lt;h2&gt;
  
  
  Output quality guardrails
&lt;/h2&gt;

&lt;p&gt;The rewrite prompt enforces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;don’t invent numbers&lt;/li&gt;
&lt;li&gt;don’t invent responsibilities&lt;/li&gt;
&lt;li&gt;keep original facts&lt;/li&gt;
&lt;li&gt;return strict JSON schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I post-process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dedupe variations&lt;/li&gt;
&lt;li&gt;enforce exactly 3 outputs&lt;/li&gt;
&lt;li&gt;fallback to local rule rewrite when model output is invalid&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This combo dramatically improved consistency.&lt;/p&gt;




&lt;h2&gt;
  
  
  Credits/quota logic (important lesson)
&lt;/h2&gt;

&lt;p&gt;One subtle bug I fixed: deduction order.&lt;/p&gt;

&lt;p&gt;I originally deducted paid credits first, which felt wrong for users with daily free quota.&lt;br&gt;&lt;br&gt;
Now the order is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;daily free quota
&lt;/li&gt;
&lt;li&gt;subscription credits
&lt;/li&gt;
&lt;li&gt;purchased credits&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That single logic change removed a lot of confusion and support issues.&lt;/p&gt;




&lt;h2&gt;
  
  
  UX fixes that mattered more than expected
&lt;/h2&gt;

&lt;p&gt;Two tiny changes improved trust a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Copy button feedback&lt;/strong&gt; (&lt;code&gt;Copy&lt;/code&gt; -&amp;gt; &lt;code&gt;Copied&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reuse Output&lt;/strong&gt; auto-scrolls back to editor and shows &lt;code&gt;Loaded&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without visible feedback, users think the app is broken—even when logic works.&lt;/p&gt;




&lt;h2&gt;
  
  
  SEO and launch notes
&lt;/h2&gt;

&lt;p&gt;I shipped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;role-based long-tail landing pages&lt;/li&gt;
&lt;li&gt;sitemap + robots&lt;/li&gt;
&lt;li&gt;canonical + OG metadata&lt;/li&gt;
&lt;li&gt;favicon + structured metadata cleanup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then submitted sitemap to Google Search Console and started testing channels like HN Show.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I’d improve next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;JD-aware rewrite mode (align bullet to job description)&lt;/li&gt;
&lt;li&gt;batch rewrite mode&lt;/li&gt;
&lt;li&gt;better analytics mapping (GA4 event taxonomy + funnel reporting)&lt;/li&gt;
&lt;li&gt;export formats (DOCX/Notion/JSON)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;If you’re building an AI writing product, my biggest takeaway is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Put deterministic logic in front of AI whenever possible.&lt;br&gt;&lt;br&gt;
It makes your product cheaper, faster, and easier to trust.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want, I can share a follow-up post with my exact event taxonomy and payment webhook idempotency setup.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>supabase</category>
    </item>
    <item>
      <title>How I Built an AI-Powered APA Citation Generator with Next.js, Supabase, and PayPal</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Thu, 26 Feb 2026 08:12:24 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/how-i-built-an-ai-powered-apa-citation-generator-with-nextjs-supabase-and-paypal-3io7</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/how-i-built-an-ai-powered-apa-citation-generator-with-nextjs-supabase-and-paypal-3io7</guid>
      <description>&lt;p&gt;If you have ever tried to cite random web pages in APA style, you know the pain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missing authors&lt;/li&gt;
&lt;li&gt;missing publication dates&lt;/li&gt;
&lt;li&gt;inconsistent metadata&lt;/li&gt;
&lt;li&gt;lots of manual fixing after "auto generation"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built &lt;strong&gt;APA Citation Generator&lt;/strong&gt; to solve that specific workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accept a URL or DOI&lt;/li&gt;
&lt;li&gt;extract metadata with rule-first parsing&lt;/li&gt;
&lt;li&gt;use AI only for missing fields&lt;/li&gt;
&lt;li&gt;output APA reference + in-text citation&lt;/li&gt;
&lt;li&gt;clearly mark confidence and review warnings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Live demo: &lt;a href="https://apacitationgenerator.online" rel="noopener noreferrer"&gt;https://apacitationgenerator.online&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Most citation tools work great on clean sources, but fail on real-world pages. In practice, users still need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;open the page manually&lt;/li&gt;
&lt;li&gt;find author/date by hand&lt;/li&gt;
&lt;li&gt;patch placeholders like &lt;code&gt;(n.d.)&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My goal was not "generate everything with AI".&lt;br&gt;
My goal was to build a &lt;strong&gt;fast, reviewable citation pipeline&lt;/strong&gt; where users can trust what is extracted and quickly fix what is uncertain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend/App&lt;/strong&gt;: Next.js 16 (App Router)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DB/Auth&lt;/strong&gt;: Supabase (custom &lt;code&gt;apa&lt;/code&gt; schema, Google OAuth)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI&lt;/strong&gt;: OpenRouter (Gemini model fallback chain)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments&lt;/strong&gt;: PayPal (one-time credit packs + subscription)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy&lt;/strong&gt;: Vercel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain/DNS&lt;/strong&gt;: Cloudflare&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Core architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Input classification
&lt;/h3&gt;

&lt;p&gt;User input is classified as either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URL&lt;/li&gt;
&lt;li&gt;DOI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anything else is rejected early with explicit errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Rule-first metadata extraction
&lt;/h3&gt;

&lt;p&gt;For URLs, I first parse metadata from HTML:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;og:title&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;og:site_name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;article:published_time&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;common author/date tags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For DOI, I query Crossref.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) AI completion only for missing fields
&lt;/h3&gt;

&lt;p&gt;If critical fields are missing, I send a trimmed text snapshot to AI and ask for strict JSON output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authors&lt;/li&gt;
&lt;li&gt;publicationDate&lt;/li&gt;
&lt;li&gt;title&lt;/li&gt;
&lt;li&gt;containerTitle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kept latency low and reduced hallucination risk versus full-AI generation.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) APA formatter + confidence model
&lt;/h3&gt;

&lt;p&gt;The formatter creates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;full reference&lt;/li&gt;
&lt;li&gt;in-text citation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then a confidence score is calculated from available fields and inference flags.&lt;br&gt;
Low-confidence outputs are labeled &lt;strong&gt;Needs Review&lt;/strong&gt; with specific warnings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monetization model
&lt;/h2&gt;

&lt;p&gt;I used a mixed model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;free daily quota&lt;/li&gt;
&lt;li&gt;paid credit packs&lt;/li&gt;
&lt;li&gt;monthly Pro subscription&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This works better than a hard paywall for utility tools.&lt;br&gt;
Users can test value first, then upgrade for volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO strategy (early stage)
&lt;/h2&gt;

&lt;p&gt;Primary keyword: &lt;strong&gt;"APA Citation Generator"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Then I added related landing pages and long-tail clusters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no author citation&lt;/li&gt;
&lt;li&gt;no date citation&lt;/li&gt;
&lt;li&gt;DOI to APA&lt;/li&gt;
&lt;li&gt;in-text citation guides&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also shipped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sitemap&lt;/li&gt;
&lt;li&gt;robots&lt;/li&gt;
&lt;li&gt;metadata/canonical&lt;/li&gt;
&lt;li&gt;structured data (FAQ/Breadcrumb/SoftwareApplication)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I had to harden before launch
&lt;/h2&gt;

&lt;p&gt;After initial release, I did a security pass and fixed a few important issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SSRF protections&lt;/strong&gt; on URL fetch&lt;/li&gt;
&lt;li&gt;block localhost/internal hosts/private IP ranges&lt;/li&gt;
&lt;li&gt;restrict ports&lt;/li&gt;
&lt;li&gt;&lt;p&gt;safe redirect handling&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OAuth callback open redirect&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;allow only internal &lt;code&gt;next&lt;/code&gt; paths&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Edit API authorization&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;verify auth + ownership before updating saved citation jobs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quota/credit race conditions&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;switched to compare-and-set style updates for concurrent safety&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CSP + security headers&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;nonce-based CSP&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;frame/content/referrer protections&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lead form anti-bot&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;honeypot + timing checks + per-IP daily limit&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;optional Cloudflare Turnstile support&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rule-first + AI fallback beats AI-only&lt;/strong&gt; for this use case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explain uncertainty&lt;/strong&gt; in UI. Users accept imperfection when they can see why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monetization should follow workflow friction&lt;/strong&gt;, not just pageviews.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security hardening early&lt;/strong&gt; saves painful migrations later.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Current limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;still focused on URL + DOI flows&lt;/li&gt;
&lt;li&gt;some edge cases need manual verification (as expected in citation work)&lt;/li&gt;
&lt;li&gt;content and internal-link SEO still improving&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What’s next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;better source-specific parsing heuristics&lt;/li&gt;
&lt;li&gt;export formats (BibTeX/Word)&lt;/li&gt;
&lt;li&gt;stronger account history and saved references UX&lt;/li&gt;
&lt;li&gt;deeper analytics for conversion and retention&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you build in the education/productivity SEO space, I’d love feedback on two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which citation edge cases break most often in your experience?&lt;/li&gt;
&lt;li&gt;Which growth loop would you prioritize first: browser extension, exports, or team collaboration?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I’m happy to share implementation details if anyone wants to replicate this architecture.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nextjs</category>
      <category>showdev</category>
      <category>supabase</category>
    </item>
    <item>
      <title>I Built an AI Baby Meme Generator with a Two-Stage Prompt Pipeline — Here's How It Works</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Wed, 11 Feb 2026 09:15:07 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-an-ai-baby-meme-generator-with-a-two-stage-prompt-pipeline-heres-how-it-works-lej</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-an-ai-baby-meme-generator-with-a-two-stage-prompt-pipeline-heres-how-it-works-lej</guid>
      <description>&lt;p&gt;Last month I shipped &lt;a href="https://babymeme.art" rel="noopener noreferrer"&gt;babymeme.art&lt;/a&gt; — a free AI baby meme generator with 7 preset styles. You pick a style like "Gangster" or "Cursed," type something like "eating pizza," and get a meme-ready image in under 5 seconds.&lt;/p&gt;

&lt;p&gt;Here's the full technical breakdown of how it works, the two-stage prompt pipeline I designed, and what I learned building it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Input ("eating pizza")
       │
       ▼
┌─────────────────────┐
│  Style Template      │  ← 7 preset prompt templates
│  prefix + input +    │
│  suffix              │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  Stage 1: Gemini     │  ← Prompt enhancement via OpenRouter
│  (prompt engineer)   │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  Stage 2: Flux.1     │  ← Image generation via fal.ai
│  Schnell (4 steps)   │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  Canvas API          │  ← Client-side meme text overlay
│  (Impact font)       │
└─────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tech stack:&lt;/strong&gt; Next.js 14 (App Router), TypeScript, Tailwind CSS, Supabase (auth + credits), PayPal (payments), Vercel (hosting).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Prompt Template System
&lt;/h2&gt;

&lt;p&gt;The core insight: users are lazy. They type "pizza" or "eating pizza" — not a detailed 100-word prompt. So I built 7 style templates that wrap their input into a complete, high-quality prompt.&lt;/p&gt;

&lt;p&gt;Each template has a &lt;code&gt;prefix&lt;/code&gt; and &lt;code&gt;suffix&lt;/code&gt;:&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="c1"&gt;// lib/styles.ts&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gangster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;promptPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hilarious photo of a tough-looking toddler with
    an exaggerated gangster attitude, wearing oversized designer
    sunglasses, heavy gold chains, a tiny leather jacket,
    making a serious tough-guy face while&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;promptSuffix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, fisheye lens shot, 90s hip hop music video
    aesthetic, dramatic flash photography, high contrast,
    absurd humor, viral meme quality, 8k.&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 prompt builder sandwiches user input between them:&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="c1"&gt;// lib/promptBuilder.ts&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;buildPrompt&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;processedInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userInput&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="c1"&gt;// Handle lazy single-word input&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;processedInput&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="s1"&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="nx"&gt;processedInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`doing &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;processedInput&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;prompt&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;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;promptPrefix&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;processedInput&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;promptSuffix&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="na"&gt;negativePrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;negativePrompt&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;So when a user types "eating pizza" with the Gangster style, the actual prompt becomes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Hilarious photo of a tough-looking toddler with an exaggerated gangster attitude, wearing oversized designer sunglasses, heavy gold chains, a tiny leather jacket, making a serious tough-guy face while eating pizza, fisheye lens shot, 90s hip hop music video aesthetic, dramatic flash photography, high contrast, absurd humor, viral meme quality, 8k."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Seven styles, each with a distinct visual personality:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Style&lt;/th&gt;
&lt;th&gt;Vibe&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gangster&lt;/td&gt;
&lt;td&gt;90s hip hop, gold chains, fisheye lens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursed&lt;/td&gt;
&lt;td&gt;Security camera, uncanny valley, VHS grain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Giant Knitted&lt;/td&gt;
&lt;td&gt;Crochet wool kaiju in miniature city&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dramatic Crying&lt;/td&gt;
&lt;td&gt;Oscar-worthy performance, tears flying&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chubby&lt;/td&gt;
&lt;td&gt;Michelin rolls, soft pastel, adorable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cyberpunk&lt;/td&gt;
&lt;td&gt;Neon, holographic, sci-fi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixar&lt;/td&gt;
&lt;td&gt;3D render, Disney character style&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Stage 1: Gemini Prompt Enhancement
&lt;/h2&gt;

&lt;p&gt;The style template gets the job done, but a second pass with an LLM makes it noticeably better. I pipe the assembled prompt through Gemini 2.0 Flash (free tier via OpenRouter) for enhancement:&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="c1"&gt;// lib/image-generation/gemini-enhancer.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://openrouter.ai/api/v1/chat/completions&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;headers&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="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENROUTER_API_KEY&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&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;application/json&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;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google/gemini-2.0-flash-exp:free&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`You are an expert prompt engineer for Flux image
        generation models. Enhance this prompt. Keep it under
        120 words. Return ONLY the enhanced prompt.
        ALWAYS include: "five fingers on each hand,
        anatomically correct hands"
        NEVER use negative phrasing — Flux doesn't support it.`&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;basePrompt&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;Key decisions here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Free model&lt;/strong&gt; — Gemini 2.0 Flash Exp on OpenRouter costs nothing. For a prompt enhancement pass, you don't need GPT-4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;120-word cap&lt;/strong&gt; — Flux Schnell handles shorter prompts better. Longer prompts tend to confuse it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No negative phrasing&lt;/strong&gt; — Flux doesn't support negative prompts the way Stable Diffusion does. Instead of "no extra fingers," you say "five fingers on each hand." This took me a while to figure out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful fallback&lt;/strong&gt; — If Gemini fails, I just use the original template prompt. The system never blocks on enhancement failure.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Stage 2: Flux.1 Schnell on fal.ai
&lt;/h2&gt;

&lt;p&gt;For image generation, I went with Flux.1 Schnell via fal.ai's API:&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="c1"&gt;// lib/image-generation/fal-flux-client.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://fal.run/fal-ai/flux/schnell&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;headers&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="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Key &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FAL_KEY&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&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;application/json&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;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;finalPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;image_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;landscape_4_3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;num_inference_steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;num_images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;enable_safety_checker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="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;Why Flux Schnell?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt; — 4 inference steps, generates in 2-3 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality&lt;/strong&gt; — Surprisingly good for meme-style images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt; — Around $0.003 per image on fal.ai&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No negative prompt needed&lt;/strong&gt; — Simpler API surface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I originally planned to use Replicate, but fal.ai's cold start times were better for my use case. When every second counts in a consumer app, that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client-Side Meme Text with Canvas API
&lt;/h2&gt;

&lt;p&gt;After generation, users can add classic meme text (white Impact font, black stroke). This runs entirely client-side:&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="c1"&gt;// lib/memeCanvas.ts — simplified&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;renderMemeToCanvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLCanvasElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLImageElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;topText&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;bottomText&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&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;fontSize&lt;/span&gt; &lt;span class="o"&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`bold &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px Impact, sans-serif`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textAlign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;white&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strokeStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;black&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lineWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fontSize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Draw with stroke first, then fill (classic meme style)&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;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strokeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fontSize&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fontSize&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&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;One gotcha: cross-origin images. The AI-generated images come from fal.ai's CDN, so you &lt;strong&gt;must&lt;/strong&gt; set &lt;code&gt;crossOrigin = 'anonymous'&lt;/code&gt; when loading them into the canvas — otherwise &lt;code&gt;toDataURL()&lt;/code&gt; throws a security error on download.&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;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crossOrigin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anonymous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// This line saves you 2 hours of debugging&lt;/span&gt;
&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Content Safety Layer
&lt;/h2&gt;

&lt;p&gt;With any AI image generator, content filtering is non-negotiable. I built a three-layer system:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: Keyword blocklist&lt;/strong&gt; — Catches obvious banned terms before they hit the API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: Context-aware replacement&lt;/strong&gt; — For the Gangster style, if someone types "gun," it auto-replaces to "water gun" instead of blocking outright. Fewer frustrated users, same safety outcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3: fal.ai's built-in safety checker&lt;/strong&gt; — &lt;code&gt;enable_safety_checker: true&lt;/code&gt; as the final guardrail.&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;autoReplaceWords&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;styleId&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="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;styleId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gangster&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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;gun&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/gi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;water gun&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;weapon&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/gi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;toy weapon&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;input&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;h2&gt;
  
  
  Credit System with Atomic Operations
&lt;/h2&gt;

&lt;p&gt;Users get 5 free credits on signup, can buy packages ($2.99/50 credits), or subscribe to Pro ($9.99/month for 300 credits). The credit deduction uses Supabase RPC for atomicity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Credits are deducted before generation starts&lt;/span&gt;
&lt;span class="c1"&gt;-- If generation fails, credits are refunded&lt;/span&gt;
&lt;span class="n"&gt;const&lt;/span&gt; &lt;span class="n"&gt;creditResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;useCredit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;creditResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;success&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="n"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;creditResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;402&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Payment&lt;/span&gt; &lt;span class="n"&gt;Required&lt;/span&gt;
  &lt;span class="p"&gt;);&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="n"&gt;generate&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;imageResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Refund&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;failure&lt;/span&gt;
  &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;refundCredit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;creditResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;credit_type&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;Priority order: subscription credits &amp;gt; free credits &amp;gt; purchased credits. This way purchased credits (which never expire) are the last to be consumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO for Programmatic Pages
&lt;/h2&gt;

&lt;p&gt;Each of the 7 styles gets its own page at &lt;code&gt;/styles/[slug]&lt;/code&gt; with unique metadata, JSON-LD structured data, and content:&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="c1"&gt;// app/styles/[slug]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateMetadata&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&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;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getStyleBySlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;seoTitle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;seoDescription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;openGraph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/api/og?style=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="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 OG images are generated dynamically at the edge using Next.js &lt;code&gt;ImageResponse&lt;/code&gt;:&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="c1"&gt;// app/api/og/route.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;edge&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;style&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImageResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="cm"&gt;/* gradient background, emoji, title */&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;styleEmojis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styleNames&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;,
&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;630&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;This gives every style page a unique social share image without storing any static files.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Start with fal.ai from day one.&lt;/strong&gt; I initially set up Replicate, then switched to fal.ai for faster cold starts. Wasted a day on the migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Use an LLM for content filtering instead of keyword lists.&lt;/strong&gt; Keyword blocklists have false positives ("assassin" contains "ass"). An LLM-based classifier would be smarter, but costs more per request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Add image caching earlier.&lt;/strong&gt; Same prompt + same style = same image. Could save a lot on API costs with a simple hash-based cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers After 2 Weeks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Built and deployed in ~12 days&lt;/li&gt;
&lt;li&gt;7 style templates&lt;/li&gt;
&lt;li&gt;~3 second average generation time&lt;/li&gt;
&lt;li&gt;Cost per image: ~$0.003 (fal.ai) + ~$0 (Gemini free tier)&lt;/li&gt;
&lt;li&gt;Deployed on Vercel free tier&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://babymeme.art" rel="noopener noreferrer"&gt;babymeme.art&lt;/a&gt;&lt;/strong&gt; — 5 free credits, no credit card required. Pick a style, type something funny, get a meme.&lt;/p&gt;

&lt;p&gt;If you're building something with AI image generation, the two-stage pipeline (LLM enhancement + fast diffusion model) is worth trying. The quality difference between a raw user prompt and an LLM-enhanced one is significant, and using a free model for enhancement keeps the cost near zero.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have questions about the architecture or prompt engineering? Drop a comment below.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>ai</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
