[{"data":1,"prerenderedAt":1192},["ShallowReactive",2],{"writing-claude-code-content-plan":3,"writing":543,"hidden-projects-engineering-design":1191},{"id":4,"title":5,"body":6,"cover":92,"date":527,"description":528,"draft":529,"extension":530,"lang":531,"meta":532,"navigation":533,"path":534,"seo":535,"stem":536,"tags":537,"__hash__":542},"writing\u002Fwriting\u002Fclaude-code-content-plan.md","Turning a 30-day content plan into draft articles with Claude Code",{"type":7,"value":8,"toc":515},"minimark",[9,20,23,28,31,55,62,66,74,86,93,98,110,183,186,207,211,225,307,310,314,321,329,336,339,345,349,352,355,383,387,390,393,404,415,435,438,441,467,471,491,495,498,504,511],[10,11,12,19],"p",{},[13,14,18],"a",{"href":15,"rel":16},"https:\u002F\u002Fkatili.dev",[17],"nofollow","katili.dev"," — my hosting and web-development studio — runs a bilingual blog. Each month I plan about 30 articles in a spreadsheet (title, description, category, language, reference links, and a ready-made prompt), import it into the Laravel admin panel, and work through it day by day.",[10,21,22],{},"Writing the articles was the slow part, and not because of the writing.",[24,25,27],"h2",{"id":26},"the-copy-paste-loop","The copy-paste loop",[10,29,30],{},"The routine for every single article looked like this:",[32,33,34,38,46,49,52],"ol",{},[35,36,37],"li",{},"Open a Claude project whose instructions hold my writing guide (structure, tone, SEO fields, and the exact JSON shape my importer expects).",[35,39,40,41,45],{},"Go back to the admin panel, find the day, press ",[42,43,44],"strong",{},"Copy Prompt",".",[35,47,48],{},"Switch tabs, open a new chat in the project, paste, wait.",[35,50,51],{},"Copy the JSON answer, go to the import page, paste it, import.",[35,53,54],{},"Go back to the plan and mark the day as done, then link it to the new post.",[10,56,57,58,61],{},"Five context switches per article, thirty articles a month. None of it was hard; all of it was friction. I wanted to type one thing and come back to drafts — ",[42,59,60],{},"without"," adding an AI API key to the server and paying per token on top of the subscription I already use every day.",[24,63,65],{"id":64},"the-idea-let-claude-code-drive-keep-the-server-dumb","The idea: let Claude Code drive, keep the server dumb",[10,67,68,69,73],{},"Claude Code already runs in my terminal, inside the repository. It can run shell commands, fetch web pages, write files, and start subagents with a chosen model. So the server doesn't need to ",[70,71,72],"em",{},"generate"," anything. It only needs to:",[75,76,77,80,83],"ul",{},[35,78,79],{},"tell Claude what is in the plan,",[35,81,82],{},"hand over one item's prompt,",[35,84,85],{},"accept the finished JSON and import it.",[10,87,88],{},[89,90],"img",{"alt":91,"src":92},"The flow: Claude Code calls a small API through a bash script; one subagent per article","\u002Fimages\u002Fwriting\u002Fclaude-code-content-plan\u002Fflow.svg",[94,95,97],"h3",{"id":96},"_1-a-small-boring-api","1. A small, boring API",[10,99,100,101,105,106,109],{},"Five routes in ",[102,103,104],"code",{},"routes\u002Fapi.php",", behind one middleware that compares a Bearer token with an environment variable using ",[102,107,108],{},"hash_equals",". No Sanctum, no users — it is a tool for one person.",[111,112,113,126],"table",{},[114,115,116],"thead",{},[117,118,119,123],"tr",{},[120,121,122],"th",{},"Endpoint",[120,124,125],{},"Purpose",[127,128,129,140,153,163,173],"tbody",{},[117,130,131,137],{},[132,133,134],"td",{},[102,135,136],{},"GET \u002Fapi\u002Fcontent-plans",[132,138,139],{},"plans with done \u002F in progress \u002F to-do counts",[117,141,142,147],{},[132,143,144],{},[102,145,146],{},"GET \u002Fapi\u002Fcontent-plans\u002F{plan}\u002Fitems",[132,148,149,150,152],{},"items ",[42,151,60],{}," their prompts",[117,154,155,160],{},[132,156,157],{},[102,158,159],{},"GET \u002Fapi\u002Fcontent-plan-items\u002F{item}",[132,161,162],{},"one item with its full prompt and references",[117,164,165,170],{},[132,166,167],{},[102,168,169],{},"PATCH \u002Fapi\u002Fcontent-plan-items\u002F{item}",[132,171,172],{},"change status",[117,174,175,180],{},[132,176,177],{},[102,178,179],{},"POST \u002Fapi\u002Fcontent-plan-items\u002F{item}\u002Fimport",[132,181,182],{},"import the generated articles",[10,184,185],{},"Two decisions mattered more than the routes themselves:",[75,187,188,194],{},[35,189,190,193],{},[42,191,192],{},"An empty token switches the API off"," (403) instead of leaving it open. Forgetting to set an env var should never publish an import endpoint to the world.",[35,195,196,203,204,45],{},[42,197,198,199,202],{},"The import reuses the existing ",[102,200,201],{},"blog:import-json"," command"," — the same code path the admin page uses. The endpoint stores the JSON on the same disk, calls the command, then reads back the post IDs it wrote. There is still only one way an article enters the database, and every new post lands as a ",[42,205,206],{},"draft",[94,208,210],{"id":209},"_2-a-bash-client-instead-of-a-smart-one","2. A bash client instead of a smart one",[10,212,213,216,217,220,221,224],{},[102,214,215],{},"bin\u002Fcontent-plan"," is plain bash and ",[102,218,219],{},"curl"," — no ",[102,222,223],{},"jq",", nothing to install:",[226,227,232],"pre",{"className":228,"code":229,"language":230,"meta":231,"style":231},"language-bash shiki shiki-themes github-light github-dark","bin\u002Fcontent-plan plans\nbin\u002Fcontent-plan items 1          # what is still to do\nbin\u002Fcontent-plan show 42          # the full prompt\nbin\u002Fcontent-plan check 42 a.json  # validate only (dry run)\nbin\u002Fcontent-plan import 42 a.json # draft posts, item marked done\n","bash","",[102,233,234,246,262,276,292],{"__ignoreMap":231},[235,236,239,242],"span",{"class":237,"line":238},"line",1,[235,240,215],{"class":241},"sScJk",[235,243,245],{"class":244},"sZZnC"," plans\n",[235,247,249,251,254,258],{"class":237,"line":248},2,[235,250,215],{"class":241},[235,252,253],{"class":244}," items",[235,255,257],{"class":256},"sj4cs"," 1",[235,259,261],{"class":260},"sJ8bj","          # what is still to do\n",[235,263,265,267,270,273],{"class":237,"line":264},3,[235,266,215],{"class":241},[235,268,269],{"class":244}," show",[235,271,272],{"class":256}," 42",[235,274,275],{"class":260},"          # the full prompt\n",[235,277,279,281,284,286,289],{"class":237,"line":278},4,[235,280,215],{"class":241},[235,282,283],{"class":244}," check",[235,285,272],{"class":256},[235,287,288],{"class":244}," a.json",[235,290,291],{"class":260},"  # validate only (dry run)\n",[235,293,295,297,300,302,304],{"class":237,"line":294},5,[235,296,215],{"class":241},[235,298,299],{"class":244}," import",[235,301,272],{"class":256},[235,303,288],{"class":244},[235,305,306],{"class":260}," # draft posts, item marked done\n",[10,308,309],{},"It is just as useful by hand as it is to Claude, which is a good sign the interface is right.",[94,311,313],{"id":312},"_3-a-skill-and-one-subagent-per-article","3. A skill, and one subagent per article",[10,315,316,317,320],{},"A project skill (",[102,318,319],{},".claude\u002Fskills\u002Fgenerate-blog\u002FSKILL.md",") turns the whole loop into:",[226,322,327],{"className":323,"code":325,"language":326,"meta":231},[324],"language-text","\u002Fgenerate-blog 1 3,5,7-9 sonnet\n","text",[102,328,325],{"__ignoreMap":231},[10,330,331,332,335],{},"Plan 1, items 3, 5, 7, 8 and 9, written by Sonnet (the default is Opus). For each item the main session starts ",[42,333,334],{},"one subagent"," with the chosen model. The subagent reads the writing guide straight from the repository, asks the API for that item's prompt, fetches the reference links, writes the bilingual JSON, validates it with a dry run, fixes what the server complains about, and imports it.",[10,337,338],{},"Then it answers with a single line:",[226,340,343],{"className":341,"code":342,"language":326,"meta":231},[324],"OK 42 | Apa Itu Harness dalam AI Agent? | #101 (id), #102 (en) · draft\n",[102,344,342],{"__ignoreMap":231},[24,346,348],{"id":347},"designing-for-tokens-not-just-for-features","Designing for tokens, not just for features",[10,350,351],{},"The interesting constraint wasn't functionality — it was context size. A naïve version would pull the 7.6 KB guide, thirty prompts, a dozen fetched web pages, and every finished article into one conversation. By article five the session would be slow and expensive; by article ten it would be forgetting the guide.",[10,353,354],{},"Three choices keep it lean:",[75,356,357,371,377],{},[35,358,359,362,363,366,367,370],{},[42,360,361],{},"Text, not JSON, for anything a model reads."," Every ",[102,364,365],{},"GET"," accepts ",[102,368,369],{},"?format=text"," and returns a compact pipe-separated table. JSON repeats every key on every row; a model pays for each repetition.",[35,372,373,376],{},[42,374,375],{},"Prompts only on demand."," The item list never includes prompts. Only the item being written fetches its prompt.",[35,378,379,382],{},[42,380,381],{},"Subagents as a context firewall."," The guide, the references and the article itself live only inside the subagent's context and disappear when it finishes. The main session sees a table going in and one line per article coming out — so the tenth article costs about as much as the first.",[24,384,386],{"id":385},"making-it-work-from-the-browser-too","Making it work from the browser too",[10,388,389],{},"I wanted the same command to work from Claude Code on the web, so I could start a batch from my phone. A cloud session has three differences from my laptop: the repository is a fresh clone (my local config file isn't there), outbound traffic goes through a network policy, and nobody is around to approve each command.",[10,391,392],{},"Configuration was easy — the script now reads environment variables first and the local file second, so the token can live in the cloud environment's settings.",[10,394,395,396,399,400,403],{},"The network part was more interesting. When I tried it, the session's proxy refused the connection to my domain with a ",[42,397,398],{},"403",". My API ",[70,401,402],{},"also"," answers 403 when it is switched off, and 401 for a wrong token. From the script's point of view, \"your token is wrong\" and \"you are not allowed to leave this sandbox\" looked the same, and the fixes are completely different.",[10,405,406,407,410,411,414],{},"The fix is a single response header. The middleware stamps ",[102,408,409],{},"X-Content-Plan-Api: 1"," on ",[42,412,413],{},"every"," response, including its own 401 and 403. The script checks for it:",[75,416,417,424],{},[35,418,419,420,423],{},"header present → the API answered; exit code ",[42,421,422],{},"1"," with the API's own message;",[35,425,426,427,430,431,434],{},"header missing → something else answered (a proxy, the wrong host, a server that hasn't deployed the route yet); exit code ",[42,428,429],{},"2"," with a ",[102,432,433],{},"NETWORK:"," prefix and a hint to allow the domain.",[10,436,437],{},"The skill reads those exit codes and tells me exactly which setting to change — and never asks me to paste a token into the chat.",[10,439,440],{},"Two smaller guards came from the same \"what if two of me run this?\" question:",[75,442,443,457],{},[35,444,445,448,449,452,453,456],{},[42,446,447],{},"A per-item lock on import."," A laptop session and a web session (or two parallel subagents) could both pass the \"does this item already have a post?\" check before either finishes. The import now takes a cache lock per item, re-reads the item inside it, and answers ",[42,450,451],{},"423"," to the loser. An item that already has a post gets ",[42,454,455],{},"409"," unless I explicitly force a re-import.",[35,458,459,462,463,466],{},[42,460,461],{},"A non-interactive mode."," ",[102,464,465],{},"\u002Fgenerate-blog next 2"," takes the next two unfinished items without asking anything, so it can run on a schedule. The output is still drafts; publishing stays a human decision.",[24,468,470],{"id":469},"what-i-deliberately-left-out","What I deliberately left out",[75,472,473,479,485],{},[35,474,475,478],{},[42,476,477],{},"No AI calls on the server."," The server stores and imports; the writing happens in a tool I already pay for.",[35,480,481,484],{},[42,482,483],{},"No auto-publishing."," Generated articles are drafts. I still read, edit and schedule each one.",[35,486,487,490],{},[42,488,489],{},"No permission changes committed for me."," The allow-list that lets a cloud session run unattended is documented, not committed — that is a decision for the repository owner, not for the agent that wrote the feature.",[24,492,494],{"id":493},"result","Result",[10,496,497],{},"The routine went from five tab switches per article to one line for a whole batch:",[226,499,502],{"className":500,"code":501,"language":326,"meta":231},[324],"\u002Fgenerate-blog next 3\n",[102,503,501],{"__ignoreMap":231},[10,505,506,507,510],{},"A few minutes later there are three bilingual drafts in the admin panel, linked to their days in the content-plan calendar, with the source JSON kept next to every other import. The whole feature is roughly 550 lines of PHP and bash plus one Markdown skill, covered by 14 feature tests — and most of the thinking went into what the model should ",[70,508,509],{},"not"," have to read.",[512,513,514],"style",{},"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}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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);}",{"title":231,"searchDepth":248,"depth":248,"links":516},[517,518,523,524,525,526],{"id":26,"depth":248,"text":27},{"id":64,"depth":248,"text":65,"children":519},[520,521,522],{"id":96,"depth":264,"text":97},{"id":209,"depth":264,"text":210},{"id":312,"depth":264,"text":313},{"id":347,"depth":248,"text":348},{"id":385,"depth":248,"text":386},{"id":469,"depth":248,"text":470},{"id":493,"depth":248,"text":494},"2026-10-03","How I replaced a copy-paste loop between browser tabs with one slash command — a tiny token-protected API, a bash client, and one subagent per article to keep the context small.",false,"md","en",{},true,"\u002Fwriting\u002Fclaude-code-content-plan",{"title":5,"description":528},"writing\u002Fclaude-code-content-plan",[538,539,540,541],"Engineering","AI","Laravel","Claude Code","611ySB-04ID2X-PlkUY8MvlQGA-28X7X5_iZRSFbhkI",[544,880,1161],{"id":4,"title":5,"body":545,"cover":92,"date":527,"description":528,"draft":529,"extension":530,"lang":531,"meta":877,"navigation":533,"path":534,"seo":878,"stem":536,"tags":879,"__hash__":542},{"type":7,"value":546,"toc":865},[547,552,554,556,558,572,576,578,582,590,594,596,602,656,658,672,674,682,736,738,740,744,749,753,755,760,762,764,766,784,786,788,790,796,802,814,816,818,834,836,850,852,854,859,863],[10,548,549,19],{},[13,550,18],{"href":15,"rel":551},[17],[10,553,22],{},[24,555,27],{"id":26},[10,557,30],{},[32,559,560,562,566,568,570],{},[35,561,37],{},[35,563,40,564,45],{},[42,565,44],{},[35,567,48],{},[35,569,51],{},[35,571,54],{},[10,573,57,574,61],{},[42,575,60],{},[24,577,65],{"id":64},[10,579,68,580,73],{},[70,581,72],{},[75,583,584,586,588],{},[35,585,79],{},[35,587,82],{},[35,589,85],{},[10,591,592],{},[89,593],{"alt":91,"src":92},[94,595,97],{"id":96},[10,597,100,598,105,600,109],{},[102,599,104],{},[102,601,108],{},[111,603,604,612],{},[114,605,606],{},[117,607,608,610],{},[120,609,122],{},[120,611,125],{},[127,613,614,622,632,640,648],{},[117,615,616,620],{},[132,617,618],{},[102,619,136],{},[132,621,139],{},[117,623,624,628],{},[132,625,626],{},[102,627,146],{},[132,629,149,630,152],{},[42,631,60],{},[117,633,634,638],{},[132,635,636],{},[102,637,159],{},[132,639,162],{},[117,641,642,646],{},[132,643,644],{},[102,645,169],{},[132,647,172],{},[117,649,650,654],{},[132,651,652],{},[102,653,179],{},[132,655,182],{},[10,657,185],{},[75,659,660,664],{},[35,661,662,193],{},[42,663,192],{},[35,665,666,203,670,45],{},[42,667,198,668,202],{},[102,669,201],{},[42,671,206],{},[94,673,210],{"id":209},[10,675,676,216,678,220,680,224],{},[102,677,215],{},[102,679,219],{},[102,681,223],{},[226,683,684],{"className":228,"code":229,"language":230,"meta":231,"style":231},[102,685,686,692,702,712,724],{"__ignoreMap":231},[235,687,688,690],{"class":237,"line":238},[235,689,215],{"class":241},[235,691,245],{"class":244},[235,693,694,696,698,700],{"class":237,"line":248},[235,695,215],{"class":241},[235,697,253],{"class":244},[235,699,257],{"class":256},[235,701,261],{"class":260},[235,703,704,706,708,710],{"class":237,"line":264},[235,705,215],{"class":241},[235,707,269],{"class":244},[235,709,272],{"class":256},[235,711,275],{"class":260},[235,713,714,716,718,720,722],{"class":237,"line":278},[235,715,215],{"class":241},[235,717,283],{"class":244},[235,719,272],{"class":256},[235,721,288],{"class":244},[235,723,291],{"class":260},[235,725,726,728,730,732,734],{"class":237,"line":294},[235,727,215],{"class":241},[235,729,299],{"class":244},[235,731,272],{"class":256},[235,733,288],{"class":244},[235,735,306],{"class":260},[10,737,309],{},[94,739,313],{"id":312},[10,741,316,742,320],{},[102,743,319],{},[226,745,747],{"className":746,"code":325,"language":326,"meta":231},[324],[102,748,325],{"__ignoreMap":231},[10,750,331,751,335],{},[42,752,334],{},[10,754,338],{},[226,756,758],{"className":757,"code":342,"language":326,"meta":231},[324],[102,759,342],{"__ignoreMap":231},[24,761,348],{"id":347},[10,763,351],{},[10,765,354],{},[75,767,768,776,780],{},[35,769,770,362,772,366,774,370],{},[42,771,361],{},[102,773,365],{},[102,775,369],{},[35,777,778,376],{},[42,779,375],{},[35,781,782,382],{},[42,783,381],{},[24,785,386],{"id":385},[10,787,389],{},[10,789,392],{},[10,791,395,792,399,794,403],{},[42,793,398],{},[70,795,402],{},[10,797,406,798,410,800,414],{},[102,799,409],{},[42,801,413],{},[75,803,804,808],{},[35,805,419,806,423],{},[42,807,422],{},[35,809,426,810,430,812,434],{},[42,811,429],{},[102,813,433],{},[10,815,437],{},[10,817,440],{},[75,819,820,828],{},[35,821,822,448,824,452,826,456],{},[42,823,447],{},[42,825,451],{},[42,827,455],{},[35,829,830,462,832,466],{},[42,831,461],{},[102,833,465],{},[24,835,470],{"id":469},[75,837,838,842,846],{},[35,839,840,478],{},[42,841,477],{},[35,843,844,484],{},[42,845,483],{},[35,847,848,490],{},[42,849,489],{},[24,851,494],{"id":493},[10,853,497],{},[226,855,857],{"className":856,"code":501,"language":326,"meta":231},[324],[102,858,501],{"__ignoreMap":231},[10,860,506,861,510],{},[70,862,509],{},[512,864,514],{},{"title":231,"searchDepth":248,"depth":248,"links":866},[867,868,873,874,875,876],{"id":26,"depth":248,"text":27},{"id":64,"depth":248,"text":65,"children":869},[870,871,872],{"id":96,"depth":264,"text":97},{"id":209,"depth":264,"text":210},{"id":312,"depth":264,"text":313},{"id":347,"depth":248,"text":348},{"id":385,"depth":248,"text":386},{"id":469,"depth":248,"text":470},{"id":493,"depth":248,"text":494},{},{"title":5,"description":528},[538,539,540,541],{"id":881,"title":882,"body":883,"cover":1150,"date":1151,"description":1152,"draft":529,"extension":530,"lang":531,"meta":1153,"navigation":533,"path":1154,"seo":1155,"stem":1156,"tags":1157,"__hash__":1160},"writing\u002Fwriting\u002Fbuilding-this-portfolio.md","Rebuilding my portfolio with Nuxt 4 and Nuxt Content",{"type":7,"value":884,"toc":1144},[885,892,899,903,906,1075,1086,1090,1093,1123,1127,1134,1138,1141],[10,886,887,888,891],{},"My old site was a Hugo theme with Bootstrap, a few inline scripts and a lot of ",[102,889,890],{},"!important"," for dark mode. It worked, but adding a project meant copying Markdown tables of images and hoping the layout held.",[10,893,894,895,898],{},"The goal of the rebuild: ",[42,896,897],{},"content separated from presentation",", and a site that is itself a portfolio piece.",[24,900,902],{"id":901},"content-is-the-source-of-truth","Content is the source of truth",[10,904,905],{},"Every project is one Markdown file. Front-matter is validated with a schema, so a missing field fails the build instead of breaking a page.",[226,907,911],{"className":908,"code":909,"language":910,"meta":231,"style":231},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F content.config.ts\nprojects: defineCollection({\n  type: 'page',\n  source: 'projects\u002F*.md',\n  schema: z.object({\n    category: z.string(),\n    year: z.number(),\n    role: z.string(),\n    featured: z.boolean().default(false),\n    metrics: z.array(z.object({ value: z.string(), label: z.string() })).default([]),\n    cover: z.string(),\n  }),\n}),\n","ts",[102,912,913,918,933,944,954,964,976,987,997,1021,1053,1063,1069],{"__ignoreMap":231},[235,914,915],{"class":237,"line":238},[235,916,917],{"class":260},"\u002F\u002F content.config.ts\n",[235,919,920,923,927,930],{"class":237,"line":248},[235,921,922],{"class":241},"projects",[235,924,926],{"class":925},"sVt8B",": ",[235,928,929],{"class":241},"defineCollection",[235,931,932],{"class":925},"({\n",[235,934,935,938,941],{"class":237,"line":264},[235,936,937],{"class":925},"  type: ",[235,939,940],{"class":244},"'page'",[235,942,943],{"class":925},",\n",[235,945,946,949,952],{"class":237,"line":278},[235,947,948],{"class":925},"  source: ",[235,950,951],{"class":244},"'projects\u002F*.md'",[235,953,943],{"class":925},[235,955,956,959,962],{"class":237,"line":294},[235,957,958],{"class":925},"  schema: z.",[235,960,961],{"class":241},"object",[235,963,932],{"class":925},[235,965,967,970,973],{"class":237,"line":966},6,[235,968,969],{"class":925},"    category: z.",[235,971,972],{"class":241},"string",[235,974,975],{"class":925},"(),\n",[235,977,979,982,985],{"class":237,"line":978},7,[235,980,981],{"class":925},"    year: z.",[235,983,984],{"class":241},"number",[235,986,975],{"class":925},[235,988,990,993,995],{"class":237,"line":989},8,[235,991,992],{"class":925},"    role: z.",[235,994,972],{"class":241},[235,996,975],{"class":925},[235,998,1000,1003,1006,1009,1012,1015,1018],{"class":237,"line":999},9,[235,1001,1002],{"class":925},"    featured: z.",[235,1004,1005],{"class":241},"boolean",[235,1007,1008],{"class":925},"().",[235,1010,1011],{"class":241},"default",[235,1013,1014],{"class":925},"(",[235,1016,1017],{"class":256},"false",[235,1019,1020],{"class":925},"),\n",[235,1022,1024,1027,1030,1033,1035,1038,1040,1043,1045,1048,1050],{"class":237,"line":1023},10,[235,1025,1026],{"class":925},"    metrics: z.",[235,1028,1029],{"class":241},"array",[235,1031,1032],{"class":925},"(z.",[235,1034,961],{"class":241},[235,1036,1037],{"class":925},"({ value: z.",[235,1039,972],{"class":241},[235,1041,1042],{"class":925},"(), label: z.",[235,1044,972],{"class":241},[235,1046,1047],{"class":925},"() })).",[235,1049,1011],{"class":241},[235,1051,1052],{"class":925},"([]),\n",[235,1054,1056,1059,1061],{"class":237,"line":1055},11,[235,1057,1058],{"class":925},"    cover: z.",[235,1060,972],{"class":241},[235,1062,975],{"class":925},[235,1064,1066],{"class":237,"line":1065},12,[235,1067,1068],{"class":925},"  }),\n",[235,1070,1072],{"class":237,"line":1071},13,[235,1073,1074],{"class":925},"}),\n",[10,1076,1077,1078,1081,1082,1085],{},"Adding ",[102,1079,1080],{},"content\u002Fprojects\u002Fmy-project.md"," creates ",[102,1083,1084],{},"\u002Fwork\u002Fmy-project"," — no component changes.",[24,1087,1089],{"id":1088},"components-inside-markdown","Components inside Markdown",[10,1091,1092],{},"Case studies use a few components directly from Markdown, for example an architecture explorer and animated metrics:",[226,1094,1097],{"className":1095,"code":1096,"language":530,"meta":231,"style":231},"language-md shiki shiki-themes github-light github-dark","::architecture-explorer{:nodes=\"architecture\"}\n::\n\n::metric-grid{:metrics=\"metrics\"}\n::\n",[102,1098,1099,1104,1109,1114,1119],{"__ignoreMap":231},[235,1100,1101],{"class":237,"line":238},[235,1102,1103],{},"::architecture-explorer{:nodes=\"architecture\"}\n",[235,1105,1106],{"class":237,"line":248},[235,1107,1108],{},"::\n",[235,1110,1111],{"class":237,"line":264},[235,1112,1113],{"emptyLinePlaceholder":533},"\n",[235,1115,1116],{"class":237,"line":278},[235,1117,1118],{},"::metric-grid{:metrics=\"metrics\"}\n",[235,1120,1121],{"class":237,"line":294},[235,1122,1108],{},[24,1124,1126],{"id":1125},"motion-but-only-when-it-helps","Motion, but only when it helps",[10,1128,1129,1130,1133],{},"One animation library (GSAP), loaded on the client only. Scroll reveals and counters respect ",[102,1131,1132],{},"prefers-reduced-motion","; when it is set, everything is simply visible.",[24,1135,1137],{"id":1136},"static-by-default","Static by default",[10,1139,1140],{},"Every page is prerendered, images are optimised at build time, and the whole site deploys to GitHub Pages as static files.",[512,1142,1143],{},"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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":231,"searchDepth":248,"depth":248,"links":1145},[1146,1147,1148,1149],{"id":901,"depth":248,"text":902},{"id":1088,"depth":248,"text":1089},{"id":1125,"depth":248,"text":1126},{"id":1136,"depth":248,"text":1137},null,"2026-09-29","Why I moved from a Hugo theme to Nuxt, and how content, images and motion are organised so adding a project is one Markdown file.",{},"\u002Fwriting\u002Fbuilding-this-portfolio",{"title":882,"description":1152},"writing\u002Fbuilding-this-portfolio",[538,1158,1159],"Nuxt","Design","iF-MqYU0u9Y4CZ01rpLE7AyBLEnEw2QGgPwmO1s8dRs",{"id":1162,"title":1163,"body":1164,"cover":1171,"date":1180,"description":1181,"draft":529,"extension":530,"lang":1182,"meta":1183,"navigation":533,"path":1184,"seo":1185,"stem":1186,"tags":1187,"__hash__":1190},"writing\u002Fwriting\u002Fanak-anak-masjid.md","Anak-anak di Masjid Umar Bin Khattab Bypass",{"type":7,"value":1165,"toc":1178},[1166,1172,1175],[10,1167,1168],{},[89,1169],{"alt":1170,"src":1171},"Anak-anak di masjid","\u002Fimages\u002Fwriting\u002Fanak-anak-masjid\u002Fcover.png",[10,1173,1174],{},"Kami baru saja selesai melaksanakan sholat jum'at di masjid umar bin khattab bypass. Kami duduk sejenak untuk bersantai, menunggu awan melintas dilangit untuk melindungi kami dari terik matahari gorontalo yang saking panasnya, konon katanya disini mataharinya dua wkwk.",[10,1176,1177],{},"Kami lalu dikagetkan dengan aktivitas anak kecil disekeliling kami. Awalnya saya pikir mereka sedang bermain, namun ternyata mereka sedang membantu merapikan karpet masjid selesai dipakai jamaah sholat. Saya melihat senyum berseri dari wajah kecil mereka, ketika diberikan uang 2000 rupiah oleh bapak takmirul. Kebahagiaan telah ditebar dari senyum polos mereka. Sebuah pemandangan yang jarang terlihat, jadi saya memutuskan untuk mengambil moment tersebut.",{"title":231,"searchDepth":248,"depth":248,"links":1179},[],"2022-05-24","Catatan kecil selepas sholat Jum'at — tentang senyum anak-anak yang merapikan karpet masjid.","id",{},"\u002Fwriting\u002Fanak-anak-masjid",{"title":1163,"description":1181},"writing\u002Fanak-anak-masjid",[1188,1189],"Personal","Cerita","qJfUpO0XCmqTLjOy7NkcHbCyrx7GxXQa6A13pfZnPRE",[],1791066235750]