[{"data":1,"prerenderedAt":304},["ShallowReactive",2],{"home-writing":3},[4,227],{"id":5,"title":6,"body":7,"date":214,"description":215,"draft":216,"extension":217,"meta":218,"navigation":219,"path":220,"seo":221,"stem":222,"tags":223,"__hash__":226},"blog\u002Fblog\u002Fhello-world.md","Hello, World — Why I Rebuilt My Portfolio on Nuxt",{"type":8,"value":9,"toc":210},"minimark",[10,28,33,36,65,71,75,82,203,206],[11,12,13,14,18,19,22,23,27],"p",{},"I spent years with a single ",[15,16,17],"code",{},"index.html",", an ",[15,20,21],{},"app.js",", and a big stylesheet. It worked.\nBut every time I wanted to add a project or write something, I was editing arrays buried\nin JavaScript. So I rebuilt it as a ",[24,25,26],"strong",{},"template",": content lives in one config file, posts\nlive in markdown, and the design is the reusable shell.",[29,30,32],"h2",{"id":31},"what-changed","What changed",[11,34,35],{},"Three things now live apart from each other:",[37,38,39,50,59],"ol",{},[40,41,42,45,46,49],"li",{},[24,43,44],{},"Config"," — who I am, my projects, my experience. One file: ",[15,47,48],{},"app\u002Fapp.config.ts",".",[40,51,52,55,56,49],{},[24,53,54],{},"Content"," — these blog posts, as plain markdown in ",[15,57,58],{},"content\u002Fblog\u002F",[40,60,61,64],{},[24,62,63],{},"Design"," — the Vue components and the stylesheet that render it all.",[66,67,68],"blockquote",{},[11,69,70],{},"If you can fork it and make it yours by editing data, it's a template. If you have to\ntouch component code, it's just a website.",[29,72,74],{"id":73},"the-blog","The blog",[11,76,77,78,81],{},"This very post is a markdown file. Nuxt Content turns the ",[15,79,80],{},"content\u002F"," folder into a\nqueryable collection, so the index page just asks for every post, sorted by date:",[83,84,89],"pre",{"className":85,"code":86,"language":87,"meta":88,"style":88},"language-ts shiki shiki-themes github-light github-dark","const { data: posts } = await useAsyncData('blog-list', () =>\n  queryCollection('blog').where('draft', '=', false).order('date', 'DESC').all()\n)\n","ts","",[15,90,91,141,197],{"__ignoreMap":88},[92,93,96,100,104,108,111,115,118,121,124,128,131,135,138],"span",{"class":94,"line":95},"line",1,[92,97,99],{"class":98},"szBVR","const",[92,101,103],{"class":102},"sVt8B"," { ",[92,105,107],{"class":106},"s4XuR","data",[92,109,110],{"class":102},": ",[92,112,114],{"class":113},"sj4cs","posts",[92,116,117],{"class":102}," } ",[92,119,120],{"class":98},"=",[92,122,123],{"class":98}," await",[92,125,127],{"class":126},"sScJk"," useAsyncData",[92,129,130],{"class":102},"(",[92,132,134],{"class":133},"sZZnC","'blog-list'",[92,136,137],{"class":102},", () ",[92,139,140],{"class":98},"=>\n",[92,142,144,147,149,152,155,158,160,163,166,169,171,174,176,179,181,184,186,189,191,194],{"class":94,"line":143},2,[92,145,146],{"class":126},"  queryCollection",[92,148,130],{"class":102},[92,150,151],{"class":133},"'blog'",[92,153,154],{"class":102},").",[92,156,157],{"class":126},"where",[92,159,130],{"class":102},[92,161,162],{"class":133},"'draft'",[92,164,165],{"class":102},", ",[92,167,168],{"class":133},"'='",[92,170,165],{"class":102},[92,172,173],{"class":113},"false",[92,175,154],{"class":102},[92,177,178],{"class":126},"order",[92,180,130],{"class":102},[92,182,183],{"class":133},"'date'",[92,185,165],{"class":102},[92,187,188],{"class":133},"'DESC'",[92,190,154],{"class":102},[92,192,193],{"class":126},"all",[92,195,196],{"class":102},"()\n",[92,198,200],{"class":94,"line":199},3,[92,201,202],{"class":102},")\n",[11,204,205],{},"Add a file, get a post. That's the whole workflow.",[207,208,209],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .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":88,"searchDepth":143,"depth":143,"links":211},[212,213],{"id":31,"depth":143,"text":32},{"id":73,"depth":143,"text":74},"2026-05-31","Moving from a hand-rolled static site to a content-driven Nuxt template, and why the separation of config, content, and design matters.",false,"md",{},true,"\u002Fblog\u002Fhello-world",{"title":6,"description":215},"blog\u002Fhello-world",[224,225],"Nuxt","Meta","vVvXbfWuvSvs9jyJdpGQuQjICk8QA0c5b4c-uRpWggo",{"id":228,"title":229,"body":230,"date":293,"description":294,"draft":216,"extension":217,"meta":295,"navigation":219,"path":296,"seo":297,"stem":298,"tags":299,"__hash__":303},"blog\u002Fblog\u002Fscheduling-without-celery.md","Scheduling Without Celery: APScheduler Inside FastAPI",{"type":8,"value":231,"toc":288},[232,235,239,242,246,253,272,275,279,282],[11,233,234],{},"Kickmates scores football predictions the moment real matches finish. That means three\ntime-sensitive systems running at once: a rate-limited external API, a background scoring\nengine, and a real-time push layer. The obvious tool is Celery. I didn't reach for it.",[29,236,238],{"id":237},"the-constraint","The constraint",[11,240,241],{},"API-Football allows 90 calls per minute. A naive poll-everything loop would burn the quota\nin hours or score predictions minutes late — either one kills the live feel the product is\nbuilt on.",[29,243,245],{"id":244},"the-decision","The decision",[11,247,248,249,252],{},"Celery is correct at scale, but it adds a broker, a worker container, and operational weight\nthe product didn't need yet. I embedded ",[24,250,251],{},"APScheduler"," in FastAPI's lifespan with a tiered\nschedule:",[254,255,256,259,266,269],"ul",{},[40,257,258],{},"Fixture statuses sync once daily at 11 PM UTC",[40,260,261,262,265],{},"Scoring runs every 50 minutes, targeting only ",[15,263,264],{},"is_scored = False"," matches",[40,267,268],{},"Reminders fire hourly",[40,270,271],{},"Gameweek advancement runs nightly at 1 AM",[11,273,274],{},"A sliding-window rate limiter with async locking keeps concurrent jobs from racing to the\nsame quota.",[29,276,278],{"id":277},"the-trade-off","The trade-off",[11,280,281],{},"APScheduler is single-instance. If the server restarts mid-job, the scheduler doesn't survive\nwithout coordination logic. Acceptable here: missed windows recover on the next 50-minute\ncycle with no data loss, and the current scale doesn't justify Celery's overhead.",[11,283,284,285],{},"The lesson isn't \"never use Celery.\" It's: ",[24,286,287],{},"pick the tool that fits the constraint in front\nof you, not the one that fits a scale you don't have yet.",{"title":88,"searchDepth":143,"depth":143,"links":289},[290,291,292],{"id":237,"depth":143,"text":238},{"id":244,"depth":143,"text":245},{"id":277,"depth":143,"text":278},"2026-04-18","How Kickmates scores live football predictions on a tiered schedule without exhausting an external API — and why I skipped Celery.",{},"\u002Fblog\u002Fscheduling-without-celery",{"title":229,"description":294},"blog\u002Fscheduling-without-celery",[300,301,302],"Python","FastAPI","Architecture","hdT9FJcXl2AFPXDIBvH6cKJcgfl5TpJU8wMxgxksbxY",1781109090876]