[{"data":1,"prerenderedAt":611},["ShallowReactive",2],{"blog-post:\u002Fblog\u002Fstorybook-swiss-knife-visual-accessibility-checks":3,"blog-navigation:\u002Fblog\u002Fstorybook-swiss-knife-visual-accessibility-checks":544,"blog-audio:storybook-swiss-knife-visual-accessibility-checks":580,"i-lucide:share-2":585,"related-articles:\u002Fblog\u002Fstorybook-swiss-knife-visual-accessibility-checks":589},{"path":4,"title":5,"description":6,"date":7,"tags":8,"body":13,"image":540,"imageLight":541,"imageAlt":542,"imagePosition":543},"\u002Fblog\u002Fstorybook-swiss-knife-visual-accessibility-checks","Visual and accessibility checks for every Storybook PR","Storybook Swiss Knife screenshots every story, compares it with the base branch and runs axe on every PR. Here's what made those checks trustworthy.","2026-10-14",[9,10,11,12],"testing","accessibility","side-projects","devtools",{"type":14,"value":15,"toc":531},"minimark",[16,20,31,34,43,48,51,54,76,87,90,94,97,100,103,129,135,370,379,382,386,389,395,398,401,404,408,411,456,459,463,466,469,472,476,479,502,508,517,521,524,527],[17,18,19],"p",{},"Three weeks after I shipped Storybook previews for every pull request, I noticed I had stopped opening them.",[17,21,22,23,30],{},"The project was ",[24,25,29],"a",{"href":26,"rel":27},"https:\u002F\u002Fgithub.com\u002FArchetipo95\u002Fstorybook-github-pages",[28],"nofollow","storybook-github-pages",", a GitHub Action that gives every pull request its own Storybook preview link. It worked. But a preview link is like being handed the keys to a house and asked to find what the builders changed. You'd have to walk every room, open every cupboard and notice that one shelf moved 2 centimetres.",[17,32,33],{},"Nobody does that for hundreds of components. I don't either, and I built the thing.",[17,35,36,37,42],{},"What I wanted was a report waiting on the doormat: these rooms changed, this one broke, this door has no handle anymore. That grew into a new project: ",[24,38,41],{"href":39,"rel":40},"https:\u002F\u002Fgithub.com\u002FArchetipo95\u002Fstorybook-github-swiss-knife",[28],"Storybook Swiss Knife",".",[44,45,47],"h2",{"id":46},"what-it-does","What it does",[17,49,50],{},"On every pull request it builds Storybook, takes a screenshot of every story, compares each one with the main branch, and runs axe, an automated accessibility checker, on the same pages. Interaction tests written as play functions run in the same pass, so a broken interaction shows up next to the screenshots.",[17,52,53],{},"You see the results in three places:",[55,56,57,70,73],"ul",{},[58,59,60,61,65,66,69],"li",{},"two required checks on the pull request, ",[62,63,64],"code",{},"swiss-knife \u002F visual"," and ",[62,67,68],{},"swiss-knife \u002F accessibility",";",[58,71,72],{},"one comment with the preview link and both results;",[58,74,75],{},"a Visual panel inside the preview, with the before and after screenshots side by side and the changed pixels highlighted.",[17,77,78,79,82,83,42],{},"If a visual change is on purpose, a reviewer adds the ",[62,80,81],{},"visual-approved"," label and the visual check turns green. Accessibility works differently: known problems are recorded in a baseline, and only new ones block. I wrote about that in ",[24,84,86],{"href":85},"\u002Fblog\u002Faccessibility-baseline-block-new-violations","its own post",[17,88,89],{},"It runs entirely in GitHub Actions and publishes to GitHub Pages, so there's no external service to sign up for. It supports Storybook 8.6, 9 and 10, any framework, and npm, pnpm, yarn or bun.",[44,91,93],{"id":92},"why-trust-a-green-check","Why trust a green check",[17,95,96],{},"Taking screenshots with Playwright is a solved problem. Trusting the green check that comes out of them is harder.",[17,98,99],{},"A pull request is code from someone else. If the job that runs that code is also the job that posts the green check, it's a student grading their own exam. The pull request can change the job, and the job can post whatever it likes.",[17,101,102],{},"So the work is split in two:",[55,104,105,116],{},[58,106,107,111,112,115],{},[108,109,110],"strong",{},"The student sits the exam."," ",[62,113,114],{},"visual.yml"," builds the pull request and takes the screenshots with a read-only token and no secrets. All it can do is hand in its answers: a bundle of results.",[58,117,118,111,121,124,125,128],{},[108,119,120],{},"The teacher marks it.",[62,122,123],{},"visual-gate.yml"," runs on ",[62,126,127],{},"workflow_run",", from the main branch. It never runs the pull request's code. It reads its settings from the main branch, decides, and posts the checks.",[17,130,131,132,134],{},"A ",[62,133,127],{}," trigger can't live inside a reusable workflow, so the teacher needs a small file of its own in your repository:",[136,137,142],"pre",{"className":138,"code":139,"language":140,"meta":141,"style":141},"language-yaml shiki shiki-themes github-light-high-contrast-aaa github-light-high-contrast-aaa material-theme-palenight-aaa","# .github\u002Fworkflows\u002Fvisual-gate.yml\nname: Visual Gate\non:\n  workflow_run:\n    workflows: [\"Visual\"]\n    types: [completed]\npermissions:\n  contents: read\njobs:\n  gate:\n    if: github.event.workflow_run.event == 'pull_request'\n    uses: Archetipo95\u002Fstorybook-github-swiss-knife\u002F.github\u002Fworkflows\u002Fvisual-gate.yml@v0.7.0\n    permissions:\n      actions: read\n      checks: write\n      contents: write\n      issues: write\n      pull-requests: write\n    with:\n      caller_workflow: .github\u002Fworkflows\u002Fvisual.yml\n      protected_paths: .storybook\u002Fswiss-knife.json\n","yaml","",[62,143,144,153,168,178,186,209,224,232,243,251,259,270,281,289,299,310,320,330,340,348,359],{"__ignoreMap":141},[145,146,149],"span",{"class":147,"line":148},"line",1,[145,150,152],{"class":151},"skbv8","# .github\u002Fworkflows\u002Fvisual-gate.yml\n",[145,154,156,160,164],{"class":147,"line":155},2,[145,157,159],{"class":158},"s8_pN","name",[145,161,163],{"class":162},"s8YBD",":",[145,165,167],{"class":166},"szLOH"," Visual Gate\n",[145,169,171,175],{"class":147,"line":170},3,[145,172,174],{"class":173},"smg_G","on",[145,176,177],{"class":162},":\n",[145,179,181,184],{"class":147,"line":180},4,[145,182,183],{"class":158},"  workflow_run",[145,185,177],{"class":162},[145,187,189,192,194,197,201,204,206],{"class":147,"line":188},5,[145,190,191],{"class":158},"    workflows",[145,193,163],{"class":162},[145,195,196],{"class":162}," [",[145,198,200],{"class":199},"sghLD","\"",[145,202,203],{"class":166},"Visual",[145,205,200],{"class":199},[145,207,208],{"class":162},"]\n",[145,210,212,215,217,219,222],{"class":147,"line":211},6,[145,213,214],{"class":158},"    types",[145,216,163],{"class":162},[145,218,196],{"class":162},[145,220,221],{"class":166},"completed",[145,223,208],{"class":162},[145,225,227,230],{"class":147,"line":226},7,[145,228,229],{"class":158},"permissions",[145,231,177],{"class":162},[145,233,235,238,240],{"class":147,"line":234},8,[145,236,237],{"class":158},"  contents",[145,239,163],{"class":162},[145,241,242],{"class":166}," read\n",[145,244,246,249],{"class":147,"line":245},9,[145,247,248],{"class":158},"jobs",[145,250,177],{"class":162},[145,252,254,257],{"class":147,"line":253},10,[145,255,256],{"class":158},"  gate",[145,258,177],{"class":162},[145,260,262,265,267],{"class":147,"line":261},11,[145,263,264],{"class":158},"    if",[145,266,163],{"class":162},[145,268,269],{"class":166}," github.event.workflow_run.event == 'pull_request'\n",[145,271,273,276,278],{"class":147,"line":272},12,[145,274,275],{"class":158},"    uses",[145,277,163],{"class":162},[145,279,280],{"class":166}," Archetipo95\u002Fstorybook-github-swiss-knife\u002F.github\u002Fworkflows\u002Fvisual-gate.yml@v0.7.0\n",[145,282,284,287],{"class":147,"line":283},13,[145,285,286],{"class":158},"    permissions",[145,288,177],{"class":162},[145,290,292,295,297],{"class":147,"line":291},14,[145,293,294],{"class":158},"      actions",[145,296,163],{"class":162},[145,298,242],{"class":166},[145,300,302,305,307],{"class":147,"line":301},15,[145,303,304],{"class":158},"      checks",[145,306,163],{"class":162},[145,308,309],{"class":166}," write\n",[145,311,313,316,318],{"class":147,"line":312},16,[145,314,315],{"class":158},"      contents",[145,317,163],{"class":162},[145,319,309],{"class":166},[145,321,323,326,328],{"class":147,"line":322},17,[145,324,325],{"class":158},"      issues",[145,327,163],{"class":162},[145,329,309],{"class":166},[145,331,333,336,338],{"class":147,"line":332},18,[145,334,335],{"class":158},"      pull-requests",[145,337,163],{"class":162},[145,339,309],{"class":166},[145,341,343,346],{"class":147,"line":342},19,[145,344,345],{"class":158},"    with",[145,347,177],{"class":162},[145,349,351,354,356],{"class":147,"line":350},20,[145,352,353],{"class":158},"      caller_workflow",[145,355,163],{"class":162},[145,357,358],{"class":166}," .github\u002Fworkflows\u002Fvisual.yml\n",[145,360,362,365,367],{"class":147,"line":361},21,[145,363,364],{"class":158},"      protected_paths",[145,366,163],{"class":162},[145,368,369],{"class":166}," .storybook\u002Fswiss-knife.json\n",[17,371,372,65,375,378],{},[62,373,374],{},"caller_workflow",[62,376,377],{},"protected_paths"," stop the student from editing the exam itself. A pull request that changes the visual workflow or the capture settings fails the checks. Those changes go in a pull request of their own, where nobody can miss them.",[17,380,381],{},"The threat model in the docs is explicit that the checks protect against mistakes and unnoticed changes, not against a malicious author. A pull request can still change what its own stories show. That's what code review is for.",[44,383,385],{"id":384},"the-approval-label-that-approved-too-much","The approval label that approved too much",[17,387,388],{},"The bug I'm least proud of was in the label.",[17,390,391,392,394],{},"An approval is a signature on a page. It should only cover the page you read. The first version accepted ",[62,393,81],{}," if it was added after the first run on the latest commit had started. That sounds fine until someone pushes while a reviewer is looking. The reviewer approves the screenshots of commit A. Meanwhile commit B is still being captured. The label is newer than B's run, so it approves B too, and nobody has seen B's screenshots.",[17,396,397],{},"Force-pushing back to an older commit was worse: it brought back an approval given to a different commit.",[17,399,400],{},"The fix in 0.6.0 made the rule strict. The label counts only if it was added after the run that captured these exact screenshots had finished, and after the last force-push. A new push, a force-push or a re-run of the capture withdraws it. You label again once the new results are in.",[17,402,403],{},"It's annoying in the moment, but it's the only version of the rule where a green check means a person looked at the pixels that will be merged.",[44,405,407],{"id":406},"bugs-i-only-found-by-running-it","Bugs I only found by running it",[17,409,410],{},"Some of my favourite bugs from the changelog:",[55,412,413,428,435],{},[58,414,415,416,419,420,423,424,427],{},"A story with ",[62,417,418],{},"closed"," or ",[62,421,422],{},"timeout"," in its name, like ",[62,425,426],{},"accordion--closed",", could never show a reviewable change. Playwright's error message includes the screenshot name, and the code that keeps real timeouts and closed pages blocking was searching that message for the bare word. It's a spam filter that blocks every email containing \"free\", so your colleague Mr Freeman never gets any mail. Every diff on that story was reported as a crash no label could accept.",[58,429,430,431,42],{},"Saved screenshots are cached per GitHub machine version, and during a rollout two versions run at once. A job could land on the other version, find nothing saved and fail. Jobs now fall back to the same commit's screenshots from the other version, with a warning. I wrote the whole story in ",[24,432,434],{"href":433},"\u002Fblog\u002Fvisual-regression-baseline-cache-key","a post about the cache key",[58,436,437,438,441,442,445,446,449,450,453,454,42],{},"With ",[62,439,440],{},"visual.threshold: 0.2",", Playwright's default, a background changing from blue ",[62,443,444],{},"#1d4ed8"," to purple ",[62,447,448],{},"#6d28d9"," counts as unchanged. Playwright compares colours in YIQ, the colour space of old analog TV, where brightness carries most of the weight. Those two colours have almost the same brightness, so to the comparison they look like the same grey on a black-and-white TV. At ",[62,451,452],{},"0.1"," it's caught. For a design system, start at ",[62,455,452],{},[17,457,458],{},"None of these are clever. You only find them by running the tool on real Storybooks, again and again. The end-to-end tests do that for three setups: Storybook 8 with Vue on npm, 9 with React on pnpm, and 10 with Vue on yarn.",[44,460,462],{"id":461},"built-with-an-agent-owned-by-me","Built with an agent, owned by me",[17,464,465],{},"Swiss Knife went from the first commit to v0.7.0 between October 4 and October 12: 112 commits and more than 500 tests. 106 of those commits are co-authored with Claude, and I'm not going to pretend otherwise.",[17,467,468],{},"The agent wrote a lot of the code. Think of a builder who lays bricks faster than anyone you've hired, but has never read the building code. What it couldn't do was decide what \"trustworthy\" means here: which job may post a check, when a label stops counting, what happens to a pull request from a fork (it gets checks, nothing is published). Those rules came first. Every change had to keep them, and there are tests that check job permissions and that every action is pinned to an exact commit, so a \"quick fix\" can't loosen them unnoticed.",[17,470,471],{},"That's how I work with agents at my day job too: they make the change, the checks catch the obvious regressions, and I still read the diff and own the merge.",[44,473,475],{"id":474},"try-it","Try it",[17,477,478],{},"From the root of a repository with Storybook:",[136,480,484],{"className":481,"code":482,"language":483,"meta":141,"style":141},"language-bash shiki shiki-themes github-light-high-contrast-aaa github-light-high-contrast-aaa material-theme-palenight-aaa","npx --yes https:\u002F\u002Fgithub.com\u002FArchetipo95\u002Fstorybook-github-swiss-knife\u002Freleases\u002Fdownload\u002Fv0.7.0\u002Fstorybook-swiss-knife-0.7.0.tgz init\n","bash",[62,485,486],{"__ignoreMap":141},[145,487,488,492,496,499],{"class":147,"line":148},[145,489,491],{"class":490},"sa3fw","npx",[145,493,495],{"class":494},"sd7-E"," --yes",[145,497,498],{"class":166}," https:\u002F\u002Fgithub.com\u002FArchetipo95\u002Fstorybook-github-swiss-knife\u002Freleases\u002Fdownload\u002Fv0.7.0\u002Fstorybook-swiss-knife-0.7.0.tgz",[145,500,501],{"class":166}," init\n",[17,503,504,507],{},[62,505,506],{},"init"," writes the workflows and the config, creates the label, and prints what's left: install the addon, make the two checks required, and push once to the main branch so there are screenshots to compare against. The addon isn't on npm yet, which is why the command points at a release file.",[17,509,510,511,516],{},"It's still 0.x, so expect rough edges. If you try it on your Storybook and something breaks, ",[24,512,515],{"href":513,"rel":514},"https:\u002F\u002Fgithub.com\u002FArchetipo95\u002Fstorybook-github-swiss-knife\u002Fissues",[28],"open an issue on GitHub",". That's exactly the feedback I need.",[44,518,520],{"id":519},"what-i-took-away","What I took away",[17,522,523],{},"Most of the work went into one question: when is a green check allowed to be green? Two rules answer it. The check can't come from the code it's checking, and an approval stops counting as soon as the screenshots it covered change. I only got the second one right in 0.6.0, after I found the label could approve a commit nobody had looked at.",[17,525,526],{},"The same two rules made the agent easier to work with. 106 of the 112 commits are co-authored with Claude, and every one had to keep them. Checking a diff against two sentences is much faster than checking it for everything at once: a change that lets the pull request post its own check, or keeps an old approval alive, is easy to spot.",[528,529,530],"style",{},"html pre.shiki code .skbv8, html code.shiki .skbv8{--shiki-light:#495159;--shiki-light-font-style:inherit;--shiki-default:#495159;--shiki-default-font-style:inherit;--shiki-dark:#969EC4;--shiki-dark-font-style:italic}html pre.shiki code .s8_pN, html code.shiki .s8_pN{--shiki-light:#024C1A;--shiki-default:#024C1A;--shiki-dark:#F1777D}html pre.shiki code .s8YBD, html code.shiki .s8YBD{--shiki-light:#0E1116;--shiki-default:#0E1116;--shiki-dark:#89DDFF}html pre.shiki code .szLOH, html code.shiki .szLOH{--shiki-light:#032563;--shiki-default:#032563;--shiki-dark:#C3E88D}html pre.shiki code .smg_G, html code.shiki .smg_G{--shiki-light:#023B95;--shiki-default:#023B95;--shiki-dark:#FF9CAC}html pre.shiki code .sghLD, html code.shiki .sghLD{--shiki-light:#032563;--shiki-default:#032563;--shiki-dark:#89DDFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sa3fw, html code.shiki .sa3fw{--shiki-light:#702C00;--shiki-default:#702C00;--shiki-dark:#FFCB6B}html pre.shiki code .sd7-E, html code.shiki .sd7-E{--shiki-light:#023B95;--shiki-default:#023B95;--shiki-dark:#C3E88D}",{"title":141,"searchDepth":155,"depth":155,"links":532},[533,534,535,536,537,538,539],{"id":46,"depth":155,"text":47},{"id":92,"depth":155,"text":93},{"id":384,"depth":155,"text":385},{"id":406,"depth":155,"text":407},{"id":461,"depth":155,"text":462},{"id":474,"depth":155,"text":475},{"id":519,"depth":155,"text":520},"\u002Fimages\u002Fblog\u002Fstorybook-swiss-knife-visual-accessibility-checks.png","\u002Fimages\u002Fblog\u002Fstorybook-swiss-knife-visual-accessibility-checks-light.jpg","An editorial cartoon of Martin as a Renaissance surveyor in a neon-lit Piazza dei Miracoli, comparing an old and a new photo of the Leaning Tower through a magnifying glass, with the part that moved outlined in pink and a multi-tool of camera lens, magnifier and accessibility icon in his hand.","13% 60%",[545,549,553,557,558,562,565,568,572,576],{"path":546,"title":547,"date":548},"\u002Fblog\u002Fdebugging-home-wifi-claude-code-dfs-radar","Debugging Home Wi-Fi with Claude Code and the 5 GHz Radar Rules","2026-11-06",{"path":550,"title":551,"date":552},"\u002Fblog\u002Fbuilding-martinmasevski-dev-nuxt-vue","How I built this site with Nuxt and Vue and kept Lighthouse at 100","2026-11-04",{"path":554,"title":555,"date":556},"\u002Fblog\u002Fdesigning-an-accessible-light-theme","Turning a neon dark site into an accessible light theme","2026-11-01",{"path":4,"title":5,"date":7},{"path":559,"title":560,"date":561},"\u002Fblog\u002Fstorybook-pr-previews-github-pages-untrusted-code","Storybook PR previews on GitHub Pages without trusting PR code","2026-09-24",{"path":433,"title":563,"date":564},"A GitHub Actions cache key for visual regression baselines","2026-08-18",{"path":85,"title":566,"date":567},"Blocking new accessibility violations with an axe baseline","2026-07-14",{"path":569,"title":570,"date":571},"\u002Fblog\u002Freviving-a-raspberry-pi-with-ai","Reviving a Raspberry Pi with AI","2026-05-26",{"path":573,"title":574,"date":575},"\u002Fblog\u002Fbuilding-local-first-browser-tts-studio-kokoro","Building a local-first browser TTS studio with Kokoro","2026-03-28",{"path":577,"title":578,"date":579},"\u002Fblog\u002Fdebugging-timezone-chrome-devtools-mcp","How to change the timezone in Chrome DevTools and debug it with MCP","2026-03-14",{"hasAudio":581,"sources":582},false,{"opus":583,"mp3":584},"https:\u002F\u002Fbucket.martinmasevski.dev\u002Fblog\u002Fstorybook-swiss-knife-visual-accessibility-checks\u002Fstorybook-swiss-knife-visual-accessibility-checks.opus","https:\u002F\u002Fbucket.martinmasevski.dev\u002Fblog\u002Fstorybook-swiss-knife-visual-accessibility-checks\u002Fstorybook-swiss-knife-visual-accessibility-checks.mp3",{"left":586,"top":586,"width":587,"height":587,"rotate":586,"vFlip":581,"hFlip":581,"body":588},0,24,"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Ccircle cx=\"18\" cy=\"5\" r=\"3\"\u002F>\u003Ccircle cx=\"6\" cy=\"12\" r=\"3\"\u002F>\u003Ccircle cx=\"18\" cy=\"19\" r=\"3\"\u002F>\u003Cpath d=\"m8.59 13.51l6.83 3.98m-.01-10.98l-6.82 3.98\"\u002F>\u003C\u002Fg>",[590,596,605],{"path":554,"title":555,"description":591,"date":556,"tags":592,"image":593,"imageLight":594,"imageAlt":595},"My site was dark only, all neon pink and gold. Adding a light theme meant new colors that pass AAA, a switch that remembers you, a reveal animation and daytime covers.",[10,9,11],"\u002Fimages\u002Fblog\u002Fdesigning-an-accessible-light-theme.jpg","\u002Fimages\u002Fblog\u002Fdesigning-an-accessible-light-theme-light.jpg","An AI-generated cartoon of Martin as a lamplighter on a wooden ladder in Milan's Galleria Vittorio Emanuele II, lighting a lamp under the glass dome, with the Duomo through the far archway, pink and gold shop windows, an easel of pink and gold paint swatches and a sleeping black cat.",{"path":550,"title":551,"description":597,"date":552,"tags":598,"image":602,"imageLight":603,"imageAlt":604},"The Nuxt and Vue setup behind martinmasevski.dev, the features I had fun building, and how it scores 100 on Lighthouse with animations and images on every page.",[599,600,601,10,11],"nuxt","vue","performance","\u002Fimages\u002Fblog\u002Fbuilding-martinmasevski-dev-nuxt-vue.png","\u002Fimages\u002Fblog\u002Fbuilding-martinmasevski-dev-nuxt-vue-light.jpg","An AI-generated Studio Ghibli style illustration of Martin at a café table by the Vardar river in Skopje, smiling at a laptop showing four green 100 scores. On the left, modern Skopje has the Warrior on a Horse statue and glowing Nuxt and Vue signs. On the right, past the Stone Bridge, old Skopje has the Old Bazaar, a mosque, the Kale Fortress, kilim rugs, strings of red peppers and a sleeping cat.",{"path":559,"title":560,"description":606,"date":561,"tags":607,"image":608,"imageLight":609,"imageAlt":610},"Why pull_request_target is the wrong tool for PR previews, how I split build and publish with workflow_run, and three bugs that still broke previews.",[12,11],"\u002Fimages\u002Fblog\u002Fstorybook-pr-previews-github-pages-untrusted-code.png","\u002Fimages\u002Fblog\u002Fstorybook-pr-previews-github-pages-untrusted-code-light.jpg","An editorial cartoon of Martin as a Venetian plague doctor on a neon-lit quarantine dock, checking a sealed crate's wax seal against a ledger with a brass key kept at his belt, while a ship flying a fork flag waits behind a chain in the lagoon.",1794215545316]