<?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>Forem: wyatt fruit</title>
    <description>The latest articles on Forem by wyatt fruit (@fruitwyatt).</description>
    <link>https://forem.com/fruitwyatt</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%2F3871292%2Fa794e262-6261-419a-9136-10ff98bb75a5.png</url>
      <title>Forem: wyatt fruit</title>
      <link>https://forem.com/fruitwyatt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/fruitwyatt"/>
    <language>en</language>
    <item>
      <title>How I Built a Claude Code Skill with URL Deep Linking</title>
      <dc:creator>wyatt fruit</dc:creator>
      <pubDate>Sat, 11 Apr 2026 09:59:27 +0000</pubDate>
      <link>https://forem.com/fruitwyatt/how-i-built-a-claude-code-skill-with-url-deep-linking-52g4</link>
      <guid>https://forem.com/fruitwyatt/how-i-built-a-claude-code-skill-with-url-deep-linking-52g4</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I run a free puzzle generator site (&lt;a href="https://jigsawmake.com" rel="noopener noreferrer"&gt;JigsawMake.com&lt;/a&gt;) with 30+ tools — word search, crossword, bingo, sudoku, jigsaw. Teachers and event organizers kept asking the same question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Which puzzles should I use for my classroom / party / team event, and how do I set them up?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I realized I could build a Claude Code skill that answers this automatically — not just with recommendations, but with &lt;strong&gt;pre-configured links&lt;/strong&gt; that open the generator with everything already filled in.&lt;/p&gt;

&lt;p&gt;The key insight: &lt;strong&gt;URL parameters turn a recommendation into a one-click solution.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;A skill called &lt;code&gt;puzzle-activity-planner&lt;/code&gt; that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Takes an event description (e.g., "45-minute science class about ocean animals, 25 students")&lt;/li&gt;
&lt;li&gt;Generates a structured activity plan with timing and materials&lt;/li&gt;
&lt;li&gt;Outputs generator links with URL parameters pre-filled — the user clicks and gets a ready-to-print puzzle&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's what the output looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Word Search: https://jigsawmake.com/word-search-maker?title=Ocean%20Animals&amp;amp;words=DOLPHIN,OCTOPUS,SEAHORSE,WHALE&amp;amp;gridSize=12
Crossword:   https://jigsawmake.com/crossword-puzzle-maker?title=Ocean%20Animals&amp;amp;clues=DOLPHIN:Smart%20marine%20mammal|OCTOPUS:Has%20eight%20arms
Bingo:       https://jigsawmake.com/bingo-card-generator?title=Ocean%20Bingo&amp;amp;items=Dolphin,Octopus,Seahorse&amp;amp;cardCount=25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI generates the content (words, clues, items) and bakes it right into the URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Add URL Parameter Support to Your Web App
&lt;/h2&gt;

&lt;p&gt;Before building the skill, your web app needs to accept URL parameters. Here's the pattern I used in React (Next.js):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In your generator component's mount useEffect&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&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="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&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;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&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;urlTitle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&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;title&lt;/span&gt;&lt;span class="dl"&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;urlWords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&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;words&lt;/span&gt;&lt;span class="dl"&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;urlGridSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&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;gridSize&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;urlTitle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlTitle&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;urlGridSize&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;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlGridSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&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;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setGridSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlWords&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;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlWords&lt;/span&gt;&lt;span class="p"&gt;)&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="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="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;w&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="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;w&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;0&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;words&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;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setWordList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setWordListText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="c1"&gt;// Trigger puzzle generation&lt;/span&gt;
      &lt;span class="nx"&gt;pendingGenerateRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&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;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;&lt;strong&gt;Key points:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always validate and bound numeric values (grid size, counts)&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;decodeURIComponent()&lt;/code&gt; for text values&lt;/li&gt;
&lt;li&gt;Only accept known enum values for string options (e.g., difficulty must be "easy", "medium", or "hard")&lt;/li&gt;
&lt;li&gt;Invalid parameters should be silently ignored, not crash the page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For crossword puzzles, I used a pipe-separated format for word:clue pairs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;?&lt;span class="n"&gt;clues&lt;/span&gt;=&lt;span class="n"&gt;DOLPHIN&lt;/span&gt;:&lt;span class="n"&gt;Smart&lt;/span&gt;%&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="n"&gt;marine&lt;/span&gt;%&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="n"&gt;mammal&lt;/span&gt;|&lt;span class="n"&gt;OCTOPUS&lt;/span&gt;:&lt;span class="n"&gt;Has&lt;/span&gt;%&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="n"&gt;eight&lt;/span&gt;%&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="n"&gt;arms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the URL readable while supporting structured data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create the SKILL.md
&lt;/h2&gt;

&lt;p&gt;A Claude Code skill is just a markdown file with YAML frontmatter. Create &lt;code&gt;SKILL.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;puzzle-activity-planner&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Plan&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;puzzle-based&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;activities&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;classrooms,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;parties,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;events."&lt;/span&gt;
  &lt;span class="s"&gt;Use when someone says "plan a puzzle activity" or "classroom puzzle session".&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;description&lt;/code&gt; field is critical — Claude reads it to decide when to activate the skill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Document URL Parameters in the Skill
&lt;/h2&gt;

&lt;p&gt;This is where the magic happens. Instead of just linking to your tools, &lt;strong&gt;teach the AI your URL parameter schema&lt;/strong&gt; so it can construct pre-filled links:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## URL Parameters&lt;/span&gt;

&lt;span class="gu"&gt;### Word Search&lt;/span&gt;
| Param | Values | Default |
|-------|--------|---------|
| &lt;span class="sb"&gt;`title`&lt;/span&gt; | URL-encoded text | "Word Search Puzzle" |
| &lt;span class="sb"&gt;`words`&lt;/span&gt; | Comma-separated | default list |
| &lt;span class="sb"&gt;`gridSize`&lt;/span&gt; | 5-30 | 15 |
| &lt;span class="sb"&gt;`diagonal`&lt;/span&gt; | true/false | true |
| &lt;span class="sb"&gt;`backward`&lt;/span&gt; | true/false | false |

&lt;span class="gu"&gt;### Crossword&lt;/span&gt;
| Param | Values | Default |
|-------|--------|---------|
| &lt;span class="sb"&gt;`title`&lt;/span&gt; | URL-encoded text | "Crossword Puzzle" |
| &lt;span class="sb"&gt;`difficulty`&lt;/span&gt; | easy, medium, hard | medium |
| &lt;span class="sb"&gt;`clues`&lt;/span&gt; | WORD:clue pairs separated by &lt;span class="se"&gt;\|&lt;/span&gt; | default clues |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add a behavior rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Rules&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Always use URL parameters in generator links with pre-filled content
&lt;span class="p"&gt;-&lt;/span&gt; Generate word-clue pairs yourself and embed them in the URL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Claude: "You are the AI — generate the content and put it in the URL. Don't make the user do it."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Test It
&lt;/h2&gt;

&lt;p&gt;Install the skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.claude/skills/puzzle-activity-planner
&lt;span class="nb"&gt;cp &lt;/span&gt;SKILL.md ~/.claude/skills/puzzle-activity-planner/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/puzzle-activity-planner Plan a 30-minute puzzle activity &lt;span class="k"&gt;for &lt;/span&gt;a 4th grade 
science class about ocean animals, 25 students
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude generates a complete plan with pre-configured links. The teacher clicks, gets a ready-to-print puzzle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The URL Deep Linking Pattern
&lt;/h2&gt;

&lt;p&gt;This pattern works for any web tool, not just puzzles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add URL parameter support&lt;/strong&gt; to your web app's form/generator&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document the parameter schema&lt;/strong&gt; in your SKILL.md&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tell the AI to generate content&lt;/strong&gt; and embed it in the URL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User gets one-click links&lt;/strong&gt; instead of manual configuration&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key is that the AI generates domain-specific content (vocabulary words, trivia clues, bingo items) and encodes it directly into the URL. The web app just reads the parameters and renders.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;URL parameters are an underused interface.&lt;/strong&gt; Most web tools accept them but don't document or promote them. By teaching an AI your URL schema, you turn a static link into a personalized, ready-to-use tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skills are surprisingly simple.&lt;/strong&gt; A SKILL.md file is just instructions + a parameter reference table. No API, no auth, no backend. The skill I built is ~200 lines of markdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI is the content engine.&lt;/strong&gt; For crosswords, the AI generates word-clue pairs. For word searches, it picks theme-appropriate vocabulary. For bingo, it creates themed items. All embedded in the URL. The web app doesn't need its own AI — the skill handles content generation.&lt;/p&gt;

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

&lt;p&gt;The skill is open source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install with npx skills&lt;/span&gt;
npx skills add fruitwyatt/puzzle-activity-planner

&lt;span class="c"&gt;# Or clone directly&lt;/span&gt;
git clone https://github.com/fruitwyatt/puzzle-activity-planner &lt;span class="se"&gt;\&lt;/span&gt;
  ~/.claude/skills/puzzle-activity-planner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/fruitwyatt/puzzle-activity-planner" rel="noopener noreferrer"&gt;fruitwyatt/puzzle-activity-planner&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The generators are all free at &lt;a href="https://jigsawmake.com" rel="noopener noreferrer"&gt;JigsawMake.com&lt;/a&gt; — no signup, no watermarks, runs entirely in the browser.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a Free Puzzle Generator That Supports 8 Languages — Here's What I Learned</title>
      <dc:creator>wyatt fruit</dc:creator>
      <pubDate>Fri, 10 Apr 2026 09:19:35 +0000</pubDate>
      <link>https://forem.com/fruitwyatt/i-built-a-free-puzzle-generator-that-supports-8-languages-heres-what-i-learned-1fnj</link>
      <guid>https://forem.com/fruitwyatt/i-built-a-free-puzzle-generator-that-supports-8-languages-heres-what-i-learned-1fnj</guid>
      <description>&lt;p&gt;Last year I started building &lt;a href="https://puzzlegenio.com" rel="noopener noreferrer"&gt;PuzzleGenio&lt;/a&gt; — a free online puzzle maker for crosswords, word searches, sudoku, jigsaw puzzles, and more. What began as&lt;br&gt;
   a simple weekend project turned into a platform with 20+ tools supporting 8 languages.&lt;/p&gt;

&lt;p&gt;Here's what I learned along the way.&lt;/p&gt;

&lt;p&gt;## Why Puzzles?&lt;/p&gt;

&lt;p&gt;Teachers, parents, and event planners constantly search for "free crossword maker" or "printable word search generator." The existing tools are either paywalled, riddled with&lt;br&gt;
  ads, or stuck in 2005 UI. I saw a gap.&lt;/p&gt;

&lt;p&gt;The core idea: &lt;strong&gt;one URL = one tool = one keyword.&lt;/strong&gt; Each puzzle maker is a standalone page that works as both a functional tool AND an SEO landing page. Users generate their&lt;br&gt;
  puzzle, customize it, and download a print-ready PDF — all on the same page, no signup required.&lt;/p&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) with TypeScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;next-intl&lt;/strong&gt; for i18n (8 locales: en, zh, de, es, fr, pt, it, id)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client-side generation&lt;/strong&gt; — puzzles are generated in the browser, no server load&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF export&lt;/strong&gt; — vector-based PDFs using jsPDF, so prints look crisp at any size&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; — responsive design that works on mobile and desktop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;## The i18n Challenge&lt;/p&gt;

&lt;p&gt;Supporting 8 languages isn't just about translating strings. Here's what actually matters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Localized keyword research, not literal translation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Word Search" in German isn't "Wort Suche" — German users search for "Buchstabenrätsel" or "Suchsel." I researched Google Autocomplete and competitor sites for each language&lt;br&gt;
  to find what real users actually type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Native language puzzle content&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A Spanish user generating a crossword expects Spanish words in the puzzle, not English ones. So I built a locale-aware word list system that generates puzzles with native&lt;br&gt;
  vocabulary for each supported language.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. SEO metadata per locale&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every page has localized meta titles and descriptions, hreflang tags, and JSON-LD structured data (FAQPage, HowTo, WebApplication) — all using translated strings, never&lt;br&gt;
  hardcoded English.&lt;/p&gt;

&lt;p&gt;## What Worked for SEO&lt;/p&gt;

&lt;p&gt;A few things that moved the needle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One keyword = one page.&lt;/strong&gt; Instead of one generic "puzzle maker" page, I have dedicated pages for &lt;code&gt;crossword-puzzle-maker&lt;/code&gt;, &lt;code&gt;printable-crossword-puzzles&lt;/code&gt;,
&lt;code&gt;word-search-for-kids&lt;/code&gt;, &lt;code&gt;large-print-word-search&lt;/code&gt;, etc. Each targets a specific long-tail keyword.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subdirectory i18n&lt;/strong&gt; (&lt;code&gt;/es/crossword-puzzle-maker&lt;/code&gt;, &lt;code&gt;/de/sudoku-puzzle-maker&lt;/code&gt;) instead of query params or cookies. Google treats each as a separate indexable page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema markup on every tool page&lt;/strong&gt; — FAQPage schema gets you those expandable FAQ snippets in search results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast load times&lt;/strong&gt; — puzzle generation happens client-side, so the server just delivers static HTML. No spinners, no "generating..." wait screens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;## Lessons Learned&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with fewer languages, do them well.&lt;/strong&gt; I initially translated everything with AI and shipped it. The German and Spanish translations had embarrassing errors (wrong&lt;br&gt;
  grammatical cases, invented words). Now I research each language individually and verify with native speakers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PDF export quality matters more than you think.&lt;/strong&gt; My first version used canvas-based screenshots for PDFs. They looked blurry when printed. Switching to vector-based PDF&lt;br&gt;
  generation (drawing shapes and text directly) made the output print-perfect and reduced file sizes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't over-engineer early.&lt;/strong&gt; I spent too long on a fancy puzzle-sharing system before realizing 90% of users just want to download a PDF and print it. Build for the dominant&lt;br&gt;
   use case first.&lt;/p&gt;

&lt;p&gt;## Numbers So Far&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/es/&lt;/code&gt; and &lt;code&gt;/de/&lt;/code&gt; pages are now getting real organic traffic. Some long-tail keywords are ranking on page 1 within weeks — that's the power of low-competition keywords +&lt;br&gt;
  quality localized content.&lt;/p&gt;

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

&lt;p&gt;If you need puzzles for your classroom, party, or just for fun:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🧩 &lt;a href="https://puzzlegenio.com/crossword-puzzle-maker" rel="noopener noreferrer"&gt;Crossword Puzzle Maker&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🔍 &lt;a href="https://puzzlegenio.com/word-search-maker" rel="noopener noreferrer"&gt;Word Search Maker&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🔢 &lt;a href="https://puzzlegenio.com/sudoku-puzzle-maker" rel="noopener noreferrer"&gt;Sudoku Puzzle Maker&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🧩 &lt;a href="https://puzzlegenio.com/jigsaw-puzzle-maker" rel="noopener noreferrer"&gt;Jigsaw Puzzle Maker&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is free, no signup required. Available in English, Chinese, German, Spanish, French, Portuguese, Italian, and Indonesian.&lt;/p&gt;

&lt;p&gt;I'd love to hear your feedback — especially if you're a teacher or parent who uses puzzle tools regularly. What features would make your life easier?&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>seo</category>
    </item>
    <item>
      <title>Why Your App's Photos Look Weird: A Developer's Guide to Moiré Patterns</title>
      <dc:creator>wyatt fruit</dc:creator>
      <pubDate>Fri, 10 Apr 2026 09:14:42 +0000</pubDate>
      <link>https://forem.com/fruitwyatt/why-your-apps-photos-look-weird-a-developers-guide-to-moire-patterns-1h5a</link>
      <guid>https://forem.com/fruitwyatt/why-your-apps-photos-look-weird-a-developers-guide-to-moire-patterns-1h5a</guid>
      <description>&lt;p&gt;You've probably seen it before — strange rainbow-colored waves rippling across a photo of a computer screen, or weird grid-like artifacts in a scanned document. That's called a &lt;strong&gt;moiré pattern&lt;/strong&gt;, and if you're building any application that handles images, it's something you'll inevitably run into.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Exactly Is a Moiré Pattern?
&lt;/h2&gt;

&lt;p&gt;Moiré patterns occur when two repetitive patterns overlap at slightly different angles or scales. Think of it 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;Pattern A:  | | | | | | | | | |
Pattern B:   | | | | | | | | | |
Result:     |||  |  |||  |  |||    ← interference pattern
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the physical world, this happens constantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Screen photography&lt;/strong&gt;: Your phone camera's pixel grid interferes with the monitor's pixel grid → rainbow waves&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scanning printed material&lt;/strong&gt;: The scanner's sampling grid clashes with the halftone dot pattern → wavy artifacts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fabric photography&lt;/strong&gt;: The camera sensor grid interacts with the weave pattern → visual noise&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video recording&lt;/strong&gt;: Shooting someone wearing a striped shirt on camera → shimmering patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Developers Should Care
&lt;/h2&gt;

&lt;p&gt;If you're building any of these, moiré will bite you:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Image Upload Platforms
&lt;/h3&gt;

&lt;p&gt;Users upload photos of screens, scanned documents, and product images all the time. Moiré degrades image quality and makes OCR unreliable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Your OCR pipeline might fail on moiré-affected scans&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tesseract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recognize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scannedImage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// result.confidence: 45% 😬 — moiré confused the character recognition&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. E-commerce Product Photos
&lt;/h3&gt;

&lt;p&gt;Photographing textured fabrics, mesh materials, or screens? Moiré makes products look defective. This directly impacts conversion rates.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Screen Capture &amp;amp; Recording Tools
&lt;/h3&gt;

&lt;p&gt;Building a screen recording app? If users capture one screen with another device, moiré is guaranteed. Even screenshot tools can produce moiré when downscaling.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Document Scanning Apps
&lt;/h3&gt;

&lt;p&gt;Any app that digitizes printed materials needs to handle the halftone-to-pixel conversion problem. Without descreening, your scanned PDFs look amateur.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Math Behind Moiré
&lt;/h2&gt;

&lt;p&gt;For the curious, moiré is an &lt;strong&gt;aliasing artifact&lt;/strong&gt; — a fundamental concept in signal processing.&lt;/p&gt;

&lt;p&gt;When you sample a signal (an image) at a rate lower than twice its highest frequency, you get aliasing. This is the &lt;strong&gt;Nyquist-Shannon sampling theorem&lt;/strong&gt; in action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tex"&gt;&lt;code&gt;f&lt;span class="p"&gt;_&lt;/span&gt;moiré = |f₁ - f₂|

Where:
  f₁ = frequency of pattern 1 (e.g., screen pixel pitch)
  f₂ = frequency of pattern 2 (e.g., camera sensor pitch)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;f₁&lt;/code&gt; and &lt;code&gt;f₂&lt;/code&gt; are close but not identical, you get a low-frequency interference pattern — that's your moiré.&lt;/p&gt;

&lt;p&gt;This is the same principle behind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Audio aliasing in digital music&lt;/li&gt;
&lt;li&gt;The "wagon wheel effect" in video&lt;/li&gt;
&lt;li&gt;Temporal aliasing in animation frame rates&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Fix Moiré: The Technical Approaches
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Approach 1: Gaussian Blur (The Brute Force Way)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;remove_moire_blur&lt;/span&gt;&lt;span class="p"&gt;(&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;kernel_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Simple but destructive — removes moiré by 
    low-pass filtering, but also kills detail.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GaussianBlur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kernel_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kernel_size&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Simple, fast&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Cons&lt;/strong&gt;: Destroys image detail. It's like fixing a headache with a sledgehammer.&lt;/p&gt;
&lt;h3&gt;
  
  
  Approach 2: Frequency Domain Filtering
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;remove_moire_frequency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Smarter approach: find moiré peaks in frequency 
    domain and notch them out.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Convert to frequency domain
&lt;/span&gt;    &lt;span class="n"&gt;f_transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fft2&lt;/span&gt;&lt;span class="p"&gt;(&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;f_shift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fftshift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f_transform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Create notch filter to remove moiré frequencies
&lt;/span&gt;    &lt;span class="c1"&gt;# (frequencies identified by spectral analysis)
&lt;/span&gt;    &lt;span class="n"&gt;magnitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f_shift&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Find and suppress anomalous frequency peaks
&lt;/span&gt;    &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;magnitude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;std&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;magnitude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;magnitude&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;

    &lt;span class="c1"&gt;# Apply filter and reconstruct
&lt;/span&gt;    &lt;span class="n"&gt;filtered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f_shift&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ifft2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ifftshift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Preserves more detail&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Cons&lt;/strong&gt;: Requires manual tuning per image, doesn't generalize well.&lt;/p&gt;
&lt;h3&gt;
  
  
  Approach 3: AI/Deep Learning (The Modern Way)
&lt;/h3&gt;

&lt;p&gt;Modern neural networks can learn to separate moiré patterns from actual image content. This is where the field has moved — models trained on paired moiré/clean image datasets can selectively remove the interference while preserving detail.&lt;/p&gt;

&lt;p&gt;The key architectures used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;U-Net variants&lt;/strong&gt; — encoder-decoder with skip connections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-scale approaches&lt;/strong&gt; — process at different resolutions to catch moiré at various frequencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GAN-based methods&lt;/strong&gt; — adversarial training for more realistic restoration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most developers, implementing this from scratch isn't practical. Tools like &lt;a href="https://moireremoval.com/" rel="noopener noreferrer"&gt;Moire Removal&lt;/a&gt; use AI models specifically trained for this, so you can integrate moiré removal into your workflow without building the ML pipeline yourself.&lt;/p&gt;
&lt;h2&gt;
  
  
  Practical Tips for Your Application
&lt;/h2&gt;

&lt;p&gt;If you're dealing with moiré in your product, here's a decision tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is moiré in your input images?
├── Yes, from screen photos
│   └── Consider: slightly defocus, angle the camera, 
│       or use AI post-processing
├── Yes, from scanned documents  
│   └── Use descreening (most scanner software has this)
│       or try specialized tools like descreening APIs
├── Yes, from fabric/product photos
│   └── Adjust camera distance/angle at capture time
│       or use AI removal in post-processing
└── Yes, from downscaling in your app
    └── Use proper anti-aliasing:
        CSS: image-rendering: auto; (not crisp-edges)
        Canvas: ctx.imageSmoothingEnabled = true;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Quick Win: Prevent Moiré in Canvas Downscaling
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;downscaleWithAntiAlias&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;targetWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetHeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Step-down approach prevents moiré from aggressive downscaling&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;steps&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;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log2&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="nx"&gt;targetWidth&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;currentCanvas&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="k"&gt;for &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;i&lt;/span&gt; &lt;span class="o"&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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;stepCanvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;stepCanvas&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="nx"&gt;currentCanvas&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;stepCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&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="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;stepCanvas&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="nx"&gt;imageSmoothingEnabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;imageSmoothingQuality&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&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;currentCanvas&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;stepCanvas&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;stepCanvas&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="nx"&gt;currentCanvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stepCanvas&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Final resize to exact target&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalCanvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;finalCanvas&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="nx"&gt;targetWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;finalCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetHeight&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;finalCanvas&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="nx"&gt;imageSmoothingEnabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;currentCanvas&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;targetWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetHeight&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;finalCanvas&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;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Moiré is physics, not a bug&lt;/strong&gt; — it's aliasing from overlapping patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevention &amp;gt; Cure&lt;/strong&gt; — adjust capture conditions when possible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI removal is now practical&lt;/strong&gt; — you don't need to implement FFT notch filters from scratch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Think about it in your image pipeline&lt;/strong&gt; — especially if you handle user-uploaded photos, scans, or screen captures&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://moireremoval.com/blog/understanding-moire-patterns" rel="noopener noreferrer"&gt;Understanding Moiré Patterns — visual explainer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.imageprocessingplace.com/" rel="noopener noreferrer"&gt;Digital Image Processing — Gonzalez &amp;amp; Woods&lt;/a&gt; (Chapter on frequency domain filtering)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Nyquist%E2%80%93Shannon_sampling_theorem" rel="noopener noreferrer"&gt;Nyquist-Shannon Sampling Theorem — Wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Have you dealt with moiré in your projects? I'd love to hear your approach in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>imageprocessing</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
