[{"data":1,"prerenderedAt":1542},["ShallowReactive",2],{"doc":3,"all-blog-posts-nav":179},{"id":4,"title":5,"body":6,"created_at":161,"description":162,"extension":163,"image":164,"imageAlt":165,"imagePrompt":166,"meta":167,"navigation":168,"path":169,"published":168,"seo":170,"stem":171,"tags":172,"updated_at":177,"__hash__":178},"blog/blog/my-current-claude-code-setup.md","My current Claude Code setup and how I use it",{"type":7,"value":8,"toc":152},"minimark",[9,13,17,20,25,28,47,50,54,61,64,70,73,102,105,109,116,119,122,125,129,149],[10,11,5],"h1",{"id":12},"my-current-claude-code-setup-and-how-i-use-it",[14,15,16],"p",{},"Claude Code is my daily driver. Making use of the Max plan, I enjoy writing code and am doing it faster than ever before.",[14,18,19],{},"While everybody can use Claude Code and produce code with it, I have noticed that the quality of your input and the way you use the tool dictate whether the output is actually scalable. Smaller projects can be vibe-coded with ease by anyone. But once you work on something larger, a skilled and structural approach is what makes LLMs truly useful.",[21,22,24],"h2",{"id":23},"planning-matters-but-not-blindly","Planning matters, but not blindly",[14,26,27],{},"Planning is important. That said, I have found that the built-in planning mode of Claude makes a lot of mistakes when you blindly accept its suggestions. It tends to spit out a plan early, and if you just go with it, you often end up course-correcting halfway through.",[14,29,30,31,35,36,43,44,46],{},"Recently I came across the ",[32,33,34],"code",{},"/grill-me"," skill by ",[37,38,42],"a",{"href":39,"rel":40},"https://github.com/mattpocock/skills",[41],"nofollow","Matt Pocock",". This one changed how I approach planning entirely. Instead of letting Claude draft a plan and nodding along, ",[32,45,34],{}," flips the dynamic. It interviews you relentlessly about every aspect of your plan, walking through each branch of the design tree and resolving dependencies between decisions one by one. For each question, it provides a recommended answer, and if something can be answered by exploring the codebase, it does that instead of asking you.",[14,48,49],{},"I fired it off yesterday and went 33 questions deep to get a thorough co-understanding of the feature I had to build. By the end of it, both Claude and I were fully aligned on what needed to happen, with no ambiguity left. I encourage you to try it.",[21,51,53],{"id":52},"test-driven-development-that-actually-works","Test-driven development that actually works",[14,55,56,57,60],{},"Matt has more skills published, and ",[32,58,59],{},"/tdd"," is another one I reach for regularly.",[14,62,63],{},"LLMs often mess up writing tests. The typical failure mode is that they look at the existing code and write tests that satisfy the current implementation rather than testing what the code should actually do. The tests pass, but they are brittle and coupled to internals. Refactor something and they break, even when the behavior has not changed.",[14,65,66,67,69],{},"The ",[32,68,59],{}," skill tackles this by enforcing a vertical slice approach. Instead of writing all tests first and then all implementation (horizontal slicing), it works in small cycles: write one test, write just enough code to make it pass, then move on to the next behavior. Each cycle builds on what was learned from the previous one.",[14,71,72],{},"The flow looks like this:",[74,75,76,84,90,96],"ol",{},[77,78,79,83],"li",{},[80,81,82],"strong",{},"Plan"," the behaviors to test and confirm the interface",[77,85,86,89],{},[80,87,88],{},"Tracer bullet"," -- write a single test for the first behavior (red), then minimal code to pass it (green)",[77,91,92,95],{},[80,93,94],{},"Incremental loop"," -- repeat for each remaining behavior, one test at a time",[77,97,98,101],{},[80,99,100],{},"Refactor"," -- only after all tests pass, extract duplication and clean up. Never refactor while red.",[14,103,104],{},"Each test should describe behavior, use the public interface only, and survive internal refactoring. This forces both you and the LLM to think about what the code should do rather than how it currently does it.",[21,106,108],{"id":107},"exploring-architectural-improvements","Exploring architectural improvements",[14,110,111,112,115],{},"I have also been using the ",[32,113,114],{},"/improve-codebase-architecture"," skill. This one takes a different angle. Instead of working on a specific feature, it explores your codebase looking for architectural improvement opportunities.",[14,117,118],{},"The core idea is borrowed from John Ousterhout's \"A Philosophy of Software Design.\" It looks for shallow modules -- components where the interface is nearly as complex as the implementation -- and identifies opportunities to deepen them. Deep modules have simple interfaces that hide complex implementations, which makes them more testable and easier to work with.",[14,120,121],{},"The skill walks through a multi-step process. It starts by organically exploring your codebase, looking for friction points: concepts scattered across many files, tightly coupled modules, functions extracted purely for testability, or components that are hard to test. It then presents a list of candidates, and once you pick one, it spins up multiple sub-agents to generate radically different interface designs. You pick the one that fits best, and it files a refactor RFC as a GitHub issue.",[14,123,124],{},"What I like about this approach is that it does not just point out problems. It gives you concrete options and lets you decide what to act on.",[21,126,128],{"id":127},"what-else-is-out-there","What else is out there?",[14,130,131,132,136,137,140,141,144,145,148],{},"Matt's ",[37,133,135],{"href":39,"rel":134},[41],"skill repository"," has even more options. There is ",[32,138,139],{},"/write-a-prd"," for creating product requirement documents through an interactive interview, ",[32,142,143],{},"/prd-to-plan"," for converting those into phased implementation strategies, ",[32,146,147],{},"/request-refactor-plan"," for creating detailed refactor plans with small commits, and several others.",[14,150,151],{},"I am curious to explore more of these. If you come across any interesting skills or have built your own, let me know.",{"title":153,"searchDepth":154,"depth":155,"links":156},"",2,3,[157,158,159,160],{"id":23,"depth":154,"text":24},{"id":52,"depth":154,"text":53},{"id":107,"depth":154,"text":108},{"id":127,"depth":154,"text":128},"2026-04-03T00:00:00.000Z","How I get the most out of Claude Code with the Max plan, and why skills like grill-me, tdd, and improve-codebase-architecture changed the way I work with LLMs.","md","/images/blog/my-current-claude-code-setup.png","Hero image for \"My current Claude Code setup and how I use it\": Abstract workspace with layered translucent panels showing code fragments and question marks morphin","Abstract workspace with layered translucent panels showing code fragments and question marks morphing into structured blueprints, flowing teal and indigo gradients, geometric minimalist aesthetic",{},true,"/blog/my-current-claude-code-setup",{"title":5,"description":162},"blog/my-current-claude-code-setup",[173,174,175,176],"Claude Code","AI","Development Tools","Productivity",null,"LH7ksaC8y7cRT_apXpPmPIg7yG3D7Ih-TjjCE4rDobs",[180,218,262,421,489,1448],{"id":181,"title":182,"body":183,"created_at":206,"description":207,"extension":163,"image":208,"imageAlt":209,"imagePrompt":210,"meta":211,"navigation":168,"path":212,"published":168,"seo":213,"stem":214,"tags":215,"updated_at":177,"__hash__":217},"blog/blog/hello_world.md","Hello, World!",{"type":7,"value":184,"toc":204},[185,188,191,201],[10,186,182],{"id":187},"hello-world",[14,189,190],{},"Starting a blog. Not entirely sure what it will become, but here we are.",[14,192,193,194,200],{},"I built this site as a ",[37,195,199],{":target":196,"href":197,"rel":198},"_blank","https://www.nuxt.com",[41],"Nuxt"," application. The process of designing and building it was fun, and I plan to write about that and other personal projects going forward.",[14,202,203],{},"No grand plan yet. I will figure out the direction as I go. For now, this is the obligatory first post. Hello, world.",{"title":153,"searchDepth":154,"depth":155,"links":205},[],"2023-04-05T00:00:00.000Z","First post: Hello, World!","/images/blog/hello_world.png","Hero image for \"hello_world\": Luminescent portal opening to infinite possibilities, soft geometric patterns emerging from center, warm purple and gold accents, ethereal dawn atmosphere","Luminescent portal opening to infinite possibilities, soft geometric patterns emerging from center, warm purple and gold accents, ethereal dawn atmosphere",{},"/blog/hello_world",{"title":182,"description":207},"blog/hello_world",[216],"Hello world","4MPrnD_T5MZIskYWdwgIL2vhqo7NPGbUaXosulAz_zs",{"id":219,"title":220,"body":221,"created_at":252,"description":253,"extension":163,"image":254,"imageAlt":255,"imagePrompt":256,"meta":257,"navigation":168,"path":258,"published":168,"seo":259,"stem":260,"tags":177,"updated_at":177,"__hash__":261},"blog/blog/plans.md","Plans for this site",{"type":7,"value":222,"toc":250},[223,226,229,235,241,247],[10,224,220],{"id":225},"plans-for-this-site",[14,227,228],{},"Writing things down helps me think, and making those plans public keeps me honest. So here is what I have in mind for this site.",[14,230,231,234],{},[80,232,233],{},"More technical writing."," I want to write about the tools and patterns I use at work and in personal projects. Not tutorials aimed at beginners, but the kind of posts I would want to read myself when figuring something out.",[14,236,237,240],{},[80,238,239],{},"A newsletter."," At some point I want to add an email option for people who prefer that over checking a website. No pressure, no schedule, just a notification when something new goes up.",[14,242,243,246],{},[80,244,245],{},"Better discoverability."," The site exists, but nobody knows about it. Some basic SEO work and maybe sharing posts in the right places would help.",[14,248,249],{},"That is roughly it for now. I will update this as things change.",{"title":153,"searchDepth":154,"depth":155,"links":251},[],"2023-04-08T00:00:00.000Z","What I want to do with this site and where I see it going.","/images/blog/plans.png","Hero image for \"plans\": Abstract interconnected pathways of light forming constellation map, nodes glowing with potential, deep indigo with teal accents, futuristic navigation","Abstract interconnected pathways of light forming constellation map, nodes glowing with potential, deep indigo with teal accents, futuristic navigation",{},"/blog/plans",{"title":220,"description":253},"blog/plans","XFKplItQf4sT2T5mrv3DSko-TsA7g7TpMYg3wU6VsBs",{"id":263,"title":264,"body":265,"created_at":408,"description":409,"extension":163,"image":410,"imageAlt":411,"imagePrompt":412,"meta":413,"navigation":168,"path":414,"published":168,"seo":415,"stem":416,"tags":417,"updated_at":177,"__hash__":420},"blog/blog/github_copilot_experience.md","First time using Github Copilot",{"type":7,"value":266,"toc":404},[267,270,273,277,284,300,303,384,387,391,394,397,400],[10,268,264],{"id":269},"first-time-using-github-copilot",[14,271,272],{},"I had Copilot enabled for a while but never really paid attention to its suggestions. Then I needed to add a dynamic copyright year to the footer of this site, and figured it was a good excuse to actually try it properly.",[21,274,276],{"id":275},"the-test","The test",[14,278,279,280,283],{},"I opened ",[32,281,282],{},"components/Footer.vue"," and typed a comment:",[285,286,290],"pre",{"className":287,"code":288,"language":289,"meta":153,"style":153},"language-html shiki shiki-themes github-light github-dark","\u003C!-- Generate dynamic copyright year -->\n","html",[32,291,292],{"__ignoreMap":153},[293,294,297],"span",{"class":295,"line":296},"line",1,[293,298,288],{"class":299},"sJ8bj",[14,301,302],{},"Copilot suggested this:",[285,304,306],{"className":287,"code":305,"language":289,"meta":153,"style":153},"\u003Csmall class=\"mb-2 flex space-x-2 text-sm text-slate-500 dark:text-slate-400\">\n  \u003Cdiv>Martijn Bos\u003C/div>\n  \u003Cdiv>•\u003C/div>\n  \u003Cdiv>© {{ new Date().getFullYear() }}\u003C/div>\n\u003C/small>\n",[32,307,308,332,347,360,374],{"__ignoreMap":153},[293,309,310,314,318,322,325,329],{"class":295,"line":296},[293,311,313],{"class":312},"sVt8B","\u003C",[293,315,317],{"class":316},"s9eBZ","small",[293,319,321],{"class":320},"sScJk"," class",[293,323,324],{"class":312},"=",[293,326,328],{"class":327},"sZZnC","\"mb-2 flex space-x-2 text-sm text-slate-500 dark:text-slate-400\"",[293,330,331],{"class":312},">\n",[293,333,334,337,340,343,345],{"class":295,"line":154},[293,335,336],{"class":312},"  \u003C",[293,338,339],{"class":316},"div",[293,341,342],{"class":312},">Martijn Bos\u003C/",[293,344,339],{"class":316},[293,346,331],{"class":312},[293,348,349,351,353,356,358],{"class":295,"line":155},[293,350,336],{"class":312},[293,352,339],{"class":316},[293,354,355],{"class":312},">•\u003C/",[293,357,339],{"class":316},[293,359,331],{"class":312},[293,361,363,365,367,370,372],{"class":295,"line":362},4,[293,364,336],{"class":312},[293,366,339],{"class":316},[293,368,369],{"class":312},">© {{ new Date().getFullYear() }}\u003C/",[293,371,339],{"class":316},[293,373,331],{"class":312},[293,375,377,380,382],{"class":295,"line":376},5,[293,378,379],{"class":312},"\u003C/",[293,381,317],{"class":316},[293,383,331],{"class":312},[14,385,386],{},"It picked up on the existing Tailwind classes, the dark mode pattern, and even my name from elsewhere in the project. The suggestion was exactly what I would have written myself. I accepted it and moved on.",[21,388,390],{"id":389},"thoughts","Thoughts",[14,392,393],{},"This is a tiny example, obviously. A dynamic copyright year is not a hard problem. But the interesting part was that Copilot did not just generate the JavaScript expression. It produced a complete template block that matched the styling of the rest of the component.",[14,395,396],{},"For small, repetitive tasks like this, it saves a bit of time. Not because the code is hard to write, but because you skip the step of looking up class names and matching the existing patterns.",[14,398,399],{},"I have since used it more, and the results vary. It works well for boilerplate and common patterns. It struggles when the logic gets more specific to your project. The autocomplete style of working, where you accept or reject line by line, also has its limits. But for a first impression, it did exactly what I needed.",[401,402,403],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":153,"searchDepth":154,"depth":155,"links":405},[406,407],{"id":275,"depth":154,"text":276},{"id":389,"depth":154,"text":390},"2023-06-01T00:00:00.000Z","Trying Github Copilot for the first time on a small task and seeing what comment-driven development looks like in practice.","/images/blog/github_copilot_experience.png","Hero image for \"First time using Github Copilot\": Abstract geometric shapes forming collaborative dance, soft glowing particles connecting them, teal","Abstract geometric shapes forming collaborative dance, soft glowing particles connecting them, teal and blue color scheme, modern minimalist aesthetic",{},"/blog/github_copilot_experience",{"title":264,"description":409},"blog/github_copilot_experience",[418,419],"Github Copilot","Code Generation","---4_KOt440XIC1P2GzVZJ6hj-9ryjv9RYffq3F0FrQ",{"id":422,"title":423,"body":424,"created_at":478,"description":479,"extension":163,"image":480,"imageAlt":481,"imagePrompt":482,"meta":483,"navigation":168,"path":484,"published":168,"seo":485,"stem":486,"tags":487,"updated_at":177,"__hash__":488},"blog/blog/claude-code-research-preview.md","Trying Claude Code on the web",{"type":7,"value":425,"toc":472},[426,429,432,436,439,442,446,449,452,456,459,462,466,469],[10,427,423],{"id":428},"trying-claude-code-on-the-web",[14,430,431],{},"Anthropic released a research preview of Claude Code that runs in the browser. I had been using AI coding tools in my editor for a while, so I wanted to see how a web-based approach would feel.",[21,433,435],{"id":434},"the-difference-from-editor-integrations","The difference from editor integrations",[14,437,438],{},"The main thing that sets this apart from something like Copilot is that it works at the project level rather than the file level. It reads your directory structure, understands how files relate to each other, and can make changes across multiple files in one go.",[14,440,441],{},"With Copilot I was always working within a single file, accepting or rejecting line-by-line suggestions. Claude Code operates more like a colleague who has access to your whole repo. You describe what you want, and it figures out which files to touch.",[21,443,445],{"id":444},"what-worked","What worked",[14,447,448],{},"I pointed it at this blog and asked it to make some changes. It picked up on the Nuxt Content structure, the frontmatter format, and the component patterns without me having to explain any of it. That part was genuinely useful.",[14,450,451],{},"The web interface also means there is nothing to install. You connect your repo and start working. For quick tasks on a different machine, that is convenient.",[21,453,455],{"id":454},"what-did-not","What did not",[14,457,458],{},"The context window has limits. On larger codebases, it can lose track of things or miss files that are relevant. You also have to be specific about what you want. Vague instructions lead to vague results, which is true for any AI tool but feels more noticeable when it has access to your entire project.",[14,460,461],{},"Also, I let it write this blog post as an experiment, and the original version was full of generic AI praise. The irony of using an AI tool to write about an AI tool is that it tends to be overly positive about itself. I had to come back and rewrite it later.",[21,463,465],{"id":464},"where-it-fits","Where it fits",[14,467,468],{},"For me, Claude Code on the web works best for quick, scoped tasks: fixing a bug, adding a small feature, or exploring an unfamiliar codebase. For longer development sessions I still prefer working locally with the CLI, where I have more control over the workflow.",[14,470,471],{},"It is a different kind of tool than autocomplete. Whether that is better depends on what you are doing.",{"title":153,"searchDepth":154,"depth":155,"links":473},[474,475,476,477],{"id":434,"depth":154,"text":435},{"id":444,"depth":154,"text":445},{"id":454,"depth":154,"text":455},{"id":464,"depth":154,"text":465},"2025-10-21T00:00:00.000Z","First impressions of the Claude Code web research preview and how it compares to working with AI in a local editor.","/images/blog/claude-code-research-preview.png","Hero image for \"Trying Claude Code on the web\": Translucent browser window floating in cosmic digital space, streams of code and light flowing through","Translucent browser window floating in cosmic digital space, streams of code and light flowing through, deep blue and purple gradient, ethereal glow, minimalist 3D render",{},"/blog/claude-code-research-preview",{"title":423,"description":479},"blog/claude-code-research-preview",[173,174,175],"3VZoMK3GvpdrpCLCIkxa2xgSw6_NCxC-2x82ignv3OY",{"id":490,"title":491,"body":492,"created_at":1435,"description":1436,"extension":163,"image":1437,"imageAlt":1438,"imagePrompt":1439,"meta":1440,"navigation":168,"path":1441,"published":168,"seo":1442,"stem":1443,"tags":1444,"updated_at":177,"__hash__":1447},"blog/blog/ai-powered-blog-images.md","Building an AI-powered image pipeline for my blog with Claude Code",{"type":7,"value":493,"toc":1422},[494,498,501,505,508,512,515,525,531,548,552,557,560,672,678,682,685,851,857,861,868,1015,1019,1022,1037,1040,1044,1047,1291,1294,1310,1320,1324,1385,1388,1392,1398,1404,1410,1419],[10,495,497],{"id":496},"how-this-blog-generates-its-own-images","How this blog generates its own images",[14,499,500],{},"Every post on this blog has a hero image. None of them were made by hand. A GitHub Action generates them automatically whenever a new post is pushed, using AI for both the prompt and the image itself. Here is how it works.",[21,502,504],{"id":503},"the-problem","The problem",[14,506,507],{},"A blog without images looks flat. But manually creating featured images for every post is tedious, and stock photos feel generic. I wanted something that actually reflects the content of each post, without any manual effort.",[21,509,511],{"id":510},"two-layers","Two layers",[14,513,514],{},"The system has two layers, each serving a different purpose.",[14,516,517,520,521,524],{},[80,518,519],{},"Layer 1: Programmatic OG Images."," Using ",[32,522,523],{},"nuxt-og-image"," with Satori, every post gets a text-based social card automatically. These are the fallback. If anything goes wrong with the AI images, social sharing still looks good.",[14,526,527,530],{},[80,528,529],{},"Layer 2: AI-Generated Hero Images."," A TypeScript script generates a unique hero image for each post through two steps: first a text model writes an image prompt based on the post content, then an image model turns that prompt into an actual image.",[14,532,533,534,539,540,543,544,547],{},"Both steps run through ",[37,535,538],{"href":536,"rel":537},"https://openrouter.ai",[41],"OpenRouter",", which provides access to many models through a single API. The current setup uses ",[80,541,542],{},"Gemini 2.5 Flash"," for prompt generation and ",[80,545,546],{},"Gemini 3.1 Flash Image"," for the actual image. Cost is roughly $0.07 per image.",[21,549,551],{"id":550},"how-it-works","How it works",[553,554,556],"h3",{"id":555},"the-content-schema","The content schema",[14,558,559],{},"The blog uses Nuxt Content with three image-related fields in the frontmatter:",[285,561,565],{"className":562,"code":563,"language":564,"meta":153,"style":153},"language-typescript shiki shiki-themes github-light github-dark","schema: z.object({\n  published: z.boolean(),\n  created_at: z.date(),\n  tags: z.array(z.string()).optional(),\n  image: z.string().optional(),\n  imageAlt: z.string().optional(),\n  imagePrompt: z.string().optional(),\n})\n","typescript",[32,566,567,581,592,602,624,638,652,666],{"__ignoreMap":153},[293,568,569,572,575,578],{"class":295,"line":296},[293,570,571],{"class":320},"schema",[293,573,574],{"class":312},": z.",[293,576,577],{"class":320},"object",[293,579,580],{"class":312},"({\n",[293,582,583,586,589],{"class":295,"line":154},[293,584,585],{"class":312},"  published: z.",[293,587,588],{"class":320},"boolean",[293,590,591],{"class":312},"(),\n",[293,593,594,597,600],{"class":295,"line":155},[293,595,596],{"class":312},"  created_at: z.",[293,598,599],{"class":320},"date",[293,601,591],{"class":312},[293,603,604,607,610,613,616,619,622],{"class":295,"line":362},[293,605,606],{"class":312},"  tags: z.",[293,608,609],{"class":320},"array",[293,611,612],{"class":312},"(z.",[293,614,615],{"class":320},"string",[293,617,618],{"class":312},"()).",[293,620,621],{"class":320},"optional",[293,623,591],{"class":312},[293,625,626,629,631,634,636],{"class":295,"line":376},[293,627,628],{"class":312},"  image: z.",[293,630,615],{"class":320},[293,632,633],{"class":312},"().",[293,635,621],{"class":320},[293,637,591],{"class":312},[293,639,641,644,646,648,650],{"class":295,"line":640},6,[293,642,643],{"class":312},"  imageAlt: z.",[293,645,615],{"class":320},[293,647,633],{"class":312},[293,649,621],{"class":320},[293,651,591],{"class":312},[293,653,655,658,660,662,664],{"class":295,"line":654},7,[293,656,657],{"class":312},"  imagePrompt: z.",[293,659,615],{"class":320},[293,661,633],{"class":312},[293,663,621],{"class":320},[293,665,591],{"class":312},[293,667,669],{"class":295,"line":668},8,[293,670,671],{"class":312},"})\n",[14,673,66,674,677],{},[32,675,676],{},"imagePrompt"," field stores the prompt that was used to generate the image. This makes regeneration straightforward and keeps things transparent.",[553,679,681],{"id":680},"prompt-generation","Prompt generation",[14,683,684],{},"A system prompt defines the visual style for the blog: abstract, modern, deep blues and purples, no literal depictions of laptops or code screens. The text model receives the blog post content along with this system prompt and returns a short image prompt (under 200 characters).",[285,686,688],{"className":562,"code":687,"language":564,"meta":153,"style":153},"const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${OPENROUTER_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    model: 'google/gemini-2.5-flash',\n    messages: [\n      { role: 'system', content: systemPrompt },\n      { role: 'user', content: userPrompt },\n    ],\n    max_tokens: 200,\n  }),\n})\n",[32,689,690,718,729,734,750,763,768,784,794,800,812,823,829,840,846],{"__ignoreMap":153},[293,691,692,696,700,703,706,709,712,715],{"class":295,"line":296},[293,693,695],{"class":694},"szBVR","const",[293,697,699],{"class":698},"sj4cs"," response",[293,701,702],{"class":694}," =",[293,704,705],{"class":694}," await",[293,707,708],{"class":320}," fetch",[293,710,711],{"class":312},"(",[293,713,714],{"class":327},"'https://openrouter.ai/api/v1/chat/completions'",[293,716,717],{"class":312},", {\n",[293,719,720,723,726],{"class":295,"line":154},[293,721,722],{"class":312},"  method: ",[293,724,725],{"class":327},"'POST'",[293,727,728],{"class":312},",\n",[293,730,731],{"class":295,"line":155},[293,732,733],{"class":312},"  headers: {\n",[293,735,736,739,742,745,748],{"class":295,"line":362},[293,737,738],{"class":312},"    Authorization: ",[293,740,741],{"class":327},"`Bearer ${",[293,743,744],{"class":698},"OPENROUTER_API_KEY",[293,746,747],{"class":327},"}`",[293,749,728],{"class":312},[293,751,752,755,758,761],{"class":295,"line":376},[293,753,754],{"class":327},"    'Content-Type'",[293,756,757],{"class":312},": ",[293,759,760],{"class":327},"'application/json'",[293,762,728],{"class":312},[293,764,765],{"class":295,"line":640},[293,766,767],{"class":312},"  },\n",[293,769,770,773,776,779,782],{"class":295,"line":654},[293,771,772],{"class":312},"  body: ",[293,774,775],{"class":698},"JSON",[293,777,778],{"class":312},".",[293,780,781],{"class":320},"stringify",[293,783,580],{"class":312},[293,785,786,789,792],{"class":295,"line":668},[293,787,788],{"class":312},"    model: ",[293,790,791],{"class":327},"'google/gemini-2.5-flash'",[293,793,728],{"class":312},[293,795,797],{"class":295,"line":796},9,[293,798,799],{"class":312},"    messages: [\n",[293,801,803,806,809],{"class":295,"line":802},10,[293,804,805],{"class":312},"      { role: ",[293,807,808],{"class":327},"'system'",[293,810,811],{"class":312},", content: systemPrompt },\n",[293,813,815,817,820],{"class":295,"line":814},11,[293,816,805],{"class":312},[293,818,819],{"class":327},"'user'",[293,821,822],{"class":312},", content: userPrompt },\n",[293,824,826],{"class":295,"line":825},12,[293,827,828],{"class":312},"    ],\n",[293,830,832,835,838],{"class":295,"line":831},13,[293,833,834],{"class":312},"    max_tokens: ",[293,836,837],{"class":698},"200",[293,839,728],{"class":312},[293,841,843],{"class":295,"line":842},14,[293,844,845],{"class":312},"  }),\n",[293,847,849],{"class":295,"line":848},15,[293,850,671],{"class":312},[14,852,853,854,856],{},"If a post already has an ",[32,855,676],{}," in its frontmatter, the script skips this step and reuses the existing prompt.",[553,858,860],{"id":859},"image-generation","Image generation",[14,862,863,864,867],{},"The image model receives the prompt and returns a base64-encoded image, which gets saved to ",[32,865,866],{},"public/images/blog/",". The frontmatter is then updated with the image path, alt text, and the prompt that was used.",[285,869,871],{"className":562,"code":870,"language":564,"meta":153,"style":153},"const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${OPENROUTER_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    model: 'google/gemini-3.1-flash-image-preview',\n    modalities: ['image', 'text'],\n    messages: [{\n      role: 'user',\n      content: [{ type: 'text', text: `Generate a 1200x630 blog hero image: ${prompt}` }],\n    }],\n  }),\n})\n",[32,872,873,891,899,903,915,925,929,941,950,967,972,981,1002,1007,1011],{"__ignoreMap":153},[293,874,875,877,879,881,883,885,887,889],{"class":295,"line":296},[293,876,695],{"class":694},[293,878,699],{"class":698},[293,880,702],{"class":694},[293,882,705],{"class":694},[293,884,708],{"class":320},[293,886,711],{"class":312},[293,888,714],{"class":327},[293,890,717],{"class":312},[293,892,893,895,897],{"class":295,"line":154},[293,894,722],{"class":312},[293,896,725],{"class":327},[293,898,728],{"class":312},[293,900,901],{"class":295,"line":155},[293,902,733],{"class":312},[293,904,905,907,909,911,913],{"class":295,"line":362},[293,906,738],{"class":312},[293,908,741],{"class":327},[293,910,744],{"class":698},[293,912,747],{"class":327},[293,914,728],{"class":312},[293,916,917,919,921,923],{"class":295,"line":376},[293,918,754],{"class":327},[293,920,757],{"class":312},[293,922,760],{"class":327},[293,924,728],{"class":312},[293,926,927],{"class":295,"line":640},[293,928,767],{"class":312},[293,930,931,933,935,937,939],{"class":295,"line":654},[293,932,772],{"class":312},[293,934,775],{"class":698},[293,936,778],{"class":312},[293,938,781],{"class":320},[293,940,580],{"class":312},[293,942,943,945,948],{"class":295,"line":668},[293,944,788],{"class":312},[293,946,947],{"class":327},"'google/gemini-3.1-flash-image-preview'",[293,949,728],{"class":312},[293,951,952,955,958,961,964],{"class":295,"line":796},[293,953,954],{"class":312},"    modalities: [",[293,956,957],{"class":327},"'image'",[293,959,960],{"class":312},", ",[293,962,963],{"class":327},"'text'",[293,965,966],{"class":312},"],\n",[293,968,969],{"class":295,"line":802},[293,970,971],{"class":312},"    messages: [{\n",[293,973,974,977,979],{"class":295,"line":814},[293,975,976],{"class":312},"      role: ",[293,978,819],{"class":327},[293,980,728],{"class":312},[293,982,983,986,988,991,994,997,999],{"class":295,"line":825},[293,984,985],{"class":312},"      content: [{ type: ",[293,987,963],{"class":327},[293,989,990],{"class":312},", text: ",[293,992,993],{"class":327},"`Generate a 1200x630 blog hero image: ${",[293,995,996],{"class":312},"prompt",[293,998,747],{"class":327},[293,1000,1001],{"class":312}," }],\n",[293,1003,1004],{"class":295,"line":831},[293,1005,1006],{"class":312},"    }],\n",[293,1008,1009],{"class":295,"line":842},[293,1010,845],{"class":312},[293,1012,1013],{"class":295,"line":848},[293,1014,671],{"class":312},[553,1016,1018],{"id":1017},"the-style-guide","The style guide",[14,1020,1021],{},"The quality of the images depends on the system prompt. Here is the gist of what works well:",[1023,1024,1025,1028,1031,1034],"ul",{},[77,1026,1027],{},"Abstract geometric patterns, flowing light trails, data streams",[77,1029,1030],{},"Deep blues, purples, and teals with accent colors",[77,1032,1033],{},"Enough negative space for text overlay",[77,1035,1036],{},"No literal depictions, no stock photo cliches, no faces",[14,1038,1039],{},"Each image ends up feeling like it belongs to the same blog while still being distinct.",[21,1041,1043],{"id":1042},"the-pipeline","The pipeline",[14,1045,1046],{},"Everything runs in a GitHub Action. No local tooling required.",[285,1048,1052],{"className":1049,"code":1050,"language":1051,"meta":153,"style":153},"language-yaml shiki shiki-themes github-light github-dark","name: Generate Blog Images\n\non:\n  push:\n    branches: [main]\n    paths:\n      - 'content/blog/**'\n  workflow_dispatch: {}\n\njobs:\n  generate-images:\n    runs-on: ubuntu-latest\n    if: github.actor != 'github-actions[bot]'\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v4\n      - run: pnpm install --frozen-lockfile\n      - name: Generate missing blog images\n        env:\n          OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}\n        run: npx tsx scripts/generate-blog-images.ts\n      - name: Commit generated images\n        run: |\n          git add public/images/blog/ content/blog/\n          git diff --cached --quiet || git commit -m \"Generate blog hero images\" && git push\n","yaml",[32,1053,1054,1064,1069,1077,1084,1098,1105,1113,1121,1125,1132,1139,1149,1159,1166,1178,1190,1202,1215,1227,1235,1246,1257,1269,1279,1285],{"__ignoreMap":153},[293,1055,1056,1059,1061],{"class":295,"line":296},[293,1057,1058],{"class":316},"name",[293,1060,757],{"class":312},[293,1062,1063],{"class":327},"Generate Blog Images\n",[293,1065,1066],{"class":295,"line":154},[293,1067,1068],{"emptyLinePlaceholder":168},"\n",[293,1070,1071,1074],{"class":295,"line":155},[293,1072,1073],{"class":698},"on",[293,1075,1076],{"class":312},":\n",[293,1078,1079,1082],{"class":295,"line":362},[293,1080,1081],{"class":316},"  push",[293,1083,1076],{"class":312},[293,1085,1086,1089,1092,1095],{"class":295,"line":376},[293,1087,1088],{"class":316},"    branches",[293,1090,1091],{"class":312},": [",[293,1093,1094],{"class":327},"main",[293,1096,1097],{"class":312},"]\n",[293,1099,1100,1103],{"class":295,"line":640},[293,1101,1102],{"class":316},"    paths",[293,1104,1076],{"class":312},[293,1106,1107,1110],{"class":295,"line":654},[293,1108,1109],{"class":312},"      - ",[293,1111,1112],{"class":327},"'content/blog/**'\n",[293,1114,1115,1118],{"class":295,"line":668},[293,1116,1117],{"class":316},"  workflow_dispatch",[293,1119,1120],{"class":312},": {}\n",[293,1122,1123],{"class":295,"line":796},[293,1124,1068],{"emptyLinePlaceholder":168},[293,1126,1127,1130],{"class":295,"line":802},[293,1128,1129],{"class":316},"jobs",[293,1131,1076],{"class":312},[293,1133,1134,1137],{"class":295,"line":814},[293,1135,1136],{"class":316},"  generate-images",[293,1138,1076],{"class":312},[293,1140,1141,1144,1146],{"class":295,"line":825},[293,1142,1143],{"class":316},"    runs-on",[293,1145,757],{"class":312},[293,1147,1148],{"class":327},"ubuntu-latest\n",[293,1150,1151,1154,1156],{"class":295,"line":831},[293,1152,1153],{"class":316},"    if",[293,1155,757],{"class":312},[293,1157,1158],{"class":327},"github.actor != 'github-actions[bot]'\n",[293,1160,1161,1164],{"class":295,"line":842},[293,1162,1163],{"class":316},"    steps",[293,1165,1076],{"class":312},[293,1167,1168,1170,1173,1175],{"class":295,"line":848},[293,1169,1109],{"class":312},[293,1171,1172],{"class":316},"uses",[293,1174,757],{"class":312},[293,1176,1177],{"class":327},"actions/checkout@v4\n",[293,1179,1181,1183,1185,1187],{"class":295,"line":1180},16,[293,1182,1109],{"class":312},[293,1184,1172],{"class":316},[293,1186,757],{"class":312},[293,1188,1189],{"class":327},"pnpm/action-setup@v4\n",[293,1191,1193,1195,1197,1199],{"class":295,"line":1192},17,[293,1194,1109],{"class":312},[293,1196,1172],{"class":316},[293,1198,757],{"class":312},[293,1200,1201],{"class":327},"actions/setup-node@v4\n",[293,1203,1205,1207,1210,1212],{"class":295,"line":1204},18,[293,1206,1109],{"class":312},[293,1208,1209],{"class":316},"run",[293,1211,757],{"class":312},[293,1213,1214],{"class":327},"pnpm install --frozen-lockfile\n",[293,1216,1218,1220,1222,1224],{"class":295,"line":1217},19,[293,1219,1109],{"class":312},[293,1221,1058],{"class":316},[293,1223,757],{"class":312},[293,1225,1226],{"class":327},"Generate missing blog images\n",[293,1228,1230,1233],{"class":295,"line":1229},20,[293,1231,1232],{"class":316},"        env",[293,1234,1076],{"class":312},[293,1236,1238,1241,1243],{"class":295,"line":1237},21,[293,1239,1240],{"class":316},"          OPENROUTER_API_KEY",[293,1242,757],{"class":312},[293,1244,1245],{"class":327},"${{ secrets.OPENROUTER_API_KEY }}\n",[293,1247,1249,1252,1254],{"class":295,"line":1248},22,[293,1250,1251],{"class":316},"        run",[293,1253,757],{"class":312},[293,1255,1256],{"class":327},"npx tsx scripts/generate-blog-images.ts\n",[293,1258,1260,1262,1264,1266],{"class":295,"line":1259},23,[293,1261,1109],{"class":312},[293,1263,1058],{"class":316},[293,1265,757],{"class":312},[293,1267,1268],{"class":327},"Commit generated images\n",[293,1270,1272,1274,1276],{"class":295,"line":1271},24,[293,1273,1251],{"class":316},[293,1275,757],{"class":312},[293,1277,1278],{"class":694},"|\n",[293,1280,1282],{"class":295,"line":1281},25,[293,1283,1284],{"class":327},"          git add public/images/blog/ content/blog/\n",[293,1286,1288],{"class":295,"line":1287},26,[293,1289,1290],{"class":327},"          git diff --cached --quiet || git commit -m \"Generate blog hero images\" && git push\n",[14,1292,1293],{},"The flow:",[74,1295,1296,1301,1304,1307],{},[77,1297,1298,1299],{},"Push a new blog post to ",[32,1300,1094],{},[77,1302,1303],{},"The action detects the change, generates the prompt and image",[77,1305,1306],{},"It commits the image and updated frontmatter back to the repo",[77,1308,1309],{},"Cloudflare Pages picks up the new commit and deploys",[14,1311,66,1312,1315,1316,1319],{},[32,1313,1314],{},"github.actor"," check prevents the action from running on its own commits, avoiding an infinite loop. Posts that already have an ",[32,1317,1318],{},"image"," field are skipped, so each image is only generated once.",[21,1321,1323],{"id":1322},"the-result","The result",[1325,1326,1327,1340],"table",{},[1328,1329,1330],"thead",{},[1331,1332,1333,1337],"tr",{},[1334,1335,1336],"th",{},"Scenario",[1334,1338,1339],{},"What Happens",[1341,1342,1343,1354,1364,1374],"tbody",{},[1331,1344,1345,1351],{},[1346,1347,1348,1349],"td",{},"New post, no ",[32,1350,676],{},[1346,1352,1353],{},"Text model generates a prompt, image model generates the image",[1331,1355,1356,1361],{},[1346,1357,1358,1359],{},"New post, has ",[32,1360,676],{},[1346,1362,1363],{},"Skips prompt generation, generates the image from the existing prompt",[1331,1365,1366,1371],{},[1346,1367,1368,1369],{},"Existing post with ",[32,1370,1318],{},[1346,1372,1373],{},"Skipped entirely",[1331,1375,1376,1379],{},[1346,1377,1378],{},"Need to regenerate",[1346,1380,1381,1382,1384],{},"Remove the ",[32,1383,1318],{}," field from frontmatter, push",[14,1386,1387],{},"The whole pipeline runs in about 30 seconds per image. For a blog that publishes occasionally, the cost is negligible.",[21,1389,1391],{"id":1390},"takeaways","Takeaways",[14,1393,1394,1397],{},[80,1395,1396],{},"Store your prompts."," Keeping the prompt in frontmatter means you can always see what generated an image, tweak it, and regenerate. It also means the image model step can run independently from the prompt generation step.",[14,1399,1400,1403],{},[80,1401,1402],{},"Separate prompt generation from image generation."," Text models are good at understanding content and writing prompts. Image models are good at generating visuals. Letting each do what it is best at produces better results than trying to do everything in one step.",[14,1405,1406,1409],{},[80,1407,1408],{},"Always have a fallback."," The site works fine without hero images. The programmatic OG images ensure social sharing always looks professional, even if the AI pipeline has a bad day.",[14,1411,1412,1415,1416,1418],{},[80,1413,1414],{},"Keep it boring in CI."," The script checks for an existing ",[32,1417,1318],{}," field before doing anything. No unnecessary API calls, no surprise costs. Write a post, push, and forget about it.",[401,1420,1421],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":153,"searchDepth":154,"depth":155,"links":1423},[1424,1425,1426,1432,1433,1434],{"id":503,"depth":154,"text":504},{"id":510,"depth":154,"text":511},{"id":550,"depth":154,"text":551,"children":1427},[1428,1429,1430,1431],{"id":555,"depth":155,"text":556},{"id":680,"depth":155,"text":681},{"id":859,"depth":155,"text":860},{"id":1017,"depth":155,"text":1018},{"id":1042,"depth":154,"text":1043},{"id":1322,"depth":154,"text":1323},{"id":1390,"depth":154,"text":1391},"2026-01-07T00:00:00.000Z","How I used Claude Code on the web to design and implement automatic AI-generated featured images for my Nuxt blog, complete with Cloudflare Pages integration.","/images/blog/ai-powered-blog-images.png","Hero image for \"Building an AI-powered image pipeline for my blog with Claude Code\": Abstract digital assembly line with glowing nodes transforming text into vibrant images, streams of","Abstract digital assembly line with glowing nodes transforming text into vibrant images, streams of data flowing through crystalline pipelines, deep blue and magenta gradient, futuristic minimal aesthetic",{},"/blog/ai-powered-blog-images",{"title":491,"description":1436},"blog/ai-powered-blog-images",[173,174,199,1445,1446],"Automation","Cloudflare","WUDCPMv0Atwn3jzHpQkx-AZFEEOxOs97KPR__09fUdU",{"id":4,"title":5,"body":1449,"created_at":161,"description":162,"extension":163,"image":164,"imageAlt":165,"imagePrompt":166,"meta":1539,"navigation":168,"path":169,"published":168,"seo":1540,"stem":171,"tags":1541,"updated_at":177,"__hash__":178},{"type":7,"value":1450,"toc":1533},[1451,1453,1455,1457,1459,1461,1470,1472,1474,1478,1480,1484,1486,1504,1506,1508,1512,1514,1516,1518,1520,1531],[10,1452,5],{"id":12},[14,1454,16],{},[14,1456,19],{},[21,1458,24],{"id":23},[14,1460,27],{},[14,1462,30,1463,35,1465,43,1468,46],{},[32,1464,34],{},[37,1466,42],{"href":39,"rel":1467},[41],[32,1469,34],{},[14,1471,49],{},[21,1473,53],{"id":52},[14,1475,56,1476,60],{},[32,1477,59],{},[14,1479,63],{},[14,1481,66,1482,69],{},[32,1483,59],{},[14,1485,72],{},[74,1487,1488,1492,1496,1500],{},[77,1489,1490,83],{},[80,1491,82],{},[77,1493,1494,89],{},[80,1495,88],{},[77,1497,1498,95],{},[80,1499,94],{},[77,1501,1502,101],{},[80,1503,100],{},[14,1505,104],{},[21,1507,108],{"id":107},[14,1509,111,1510,115],{},[32,1511,114],{},[14,1513,118],{},[14,1515,121],{},[14,1517,124],{},[21,1519,128],{"id":127},[14,1521,131,1522,136,1525,140,1527,144,1529,148],{},[37,1523,135],{"href":39,"rel":1524},[41],[32,1526,139],{},[32,1528,143],{},[32,1530,147],{},[14,1532,151],{},{"title":153,"searchDepth":154,"depth":155,"links":1534},[1535,1536,1537,1538],{"id":23,"depth":154,"text":24},{"id":52,"depth":154,"text":53},{"id":107,"depth":154,"text":108},{"id":127,"depth":154,"text":128},{},{"title":5,"description":162},[173,174,175,176],1779349360344]