[{"data":1,"prerenderedAt":977},["ShallowReactive",2],{"profile":3,"projects:page:\u002Fprojects\u002Fforevermore\u002Frendering-pipeline":22,"projects:docs:\u002Fprojects\u002Fforevermore":936,"structured-data:current-stack":963},{"id":4,"alias":5,"current":6,"email":10,"extension":11,"headline":12,"links":13,"location":17,"meta":18,"name":19,"stem":20,"__hash__":21},"profile\u002Fprofile.yml","readonly",{"role":7,"company":8,"companyUrl":9},"Fullstack Engineer — Tech Lead","DataCose","https:\u002F\u002Fdatacose.com","redonemini18@gmail.com","yml","Fullstack Engineer",{"github":14,"linkedin":15,"twitch":16},"https:\u002F\u002Fgithub.com\u002FRedoni18","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fredon-emini\u002F","https:\u002F\u002Fwww.twitch.tv\u002Fr3adonly","Pristina, Kosovo",{},"Redon Emini","profile","Fbb1IAp4nuUOOtgXdwpzcqSmG0CUKtZ4D0ifPqdK5QU",{"id":23,"title":24,"body":25,"description":921,"extension":922,"featured":923,"kind":924,"links":925,"meta":926,"navigation":927,"order":929,"path":930,"period":931,"preview":931,"seo":932,"stack":933,"stem":934,"year":931,"__hash__":935},"projects\u002Fprojects\u002Fforevermore\u002F2.rendering-pipeline.md","One payload, many worlds",{"type":26,"value":27,"toc":909},"minimark",[28,32,35,40,51,333,336,339,360,363,371,377,384,392,396,399,406,409,542,549,556,560,571,574,585,588,591,595,598,819,822,826,829,873,883,886,890,893,905],[29,30,31],"p",{},"Forevermore has dozens of worlds, and visually they don't have much in common. Some are full Three.js scenes with lighting and cameras. Others are 2D, built from SVG, CSS and GSAP animations. There's a fairground at night, a paper airplane flying past planets, and a fishing trip that warms from morning into golden hour, one catch at a time.",[29,33,34],{},"What they do share is where their content comes from. This post is about the decisions that let one person build and maintain that many worlds without every new feature turning into forty separate changes.",[36,37,39],"h2",{"id":38},"every-world-reads-one-object","Every world reads one object",[29,41,42,43,50],{},"Every world, 2D or 3D, receives exactly one object when it starts: the payload. It's defined once, with ",[44,45,49],"a",{"href":46,"rel":47},"https:\u002F\u002Fzod.dev",[48],"nofollow","zod",", in the shared package:",[52,53,59],"pre",{"className":54,"code":55,"filename":56,"language":57,"meta":58,"style":58},"language-ts shiki shiki-themes vitesse-light vitesse-dark","export const experiencePayloadSchema = z.object({\n  giftId: z.string(),\n  templateSlug: z.string().nullable(),\n  occasion: z.string().nullable(),\n  recipientName: z.string().nullable(),\n  dedication: z.string().nullable(),\n  fields: z.record(z.string(), z.unknown()),   \u002F\u002F world-specific content\n  photos: z.array(experiencePhotoSchema),\n  music: experienceMusicSchema.nullable(),\n  mode: z.enum(['preview', 'published']),\n  \u002F\u002F ...\n})\n","packages\u002Ftemplates\u002Fsrc\u002Fpayload.ts","ts","",[60,61,62,96,117,139,159,179,199,240,263,280,321,327],"code",{"__ignoreMap":58},[63,64,67,71,75,79,83,86,89,93],"span",{"class":65,"line":66},"line",1,[63,68,70],{"class":69},"sTPum","export",[63,72,74],{"class":73},"s5TCs"," const ",[63,76,78],{"class":77},"s9nN2","experiencePayloadSchema",[63,80,82],{"class":81},"si6no"," =",[63,84,85],{"class":77}," z",[63,87,88],{"class":81},".",[63,90,92],{"class":91},"s_xSY","object",[63,94,95],{"class":81},"({\n",[63,97,99,103,106,109,111,114],{"class":65,"line":98},2,[63,100,102],{"class":101},"sHLBJ","  giftId",[63,104,105],{"class":81},": ",[63,107,108],{"class":77},"z",[63,110,88],{"class":81},[63,112,113],{"class":91},"string",[63,115,116],{"class":81},"(),\n",[63,118,120,123,125,127,129,131,134,137],{"class":65,"line":119},3,[63,121,122],{"class":101},"  templateSlug",[63,124,105],{"class":81},[63,126,108],{"class":77},[63,128,88],{"class":81},[63,130,113],{"class":91},[63,132,133],{"class":81},"().",[63,135,136],{"class":91},"nullable",[63,138,116],{"class":81},[63,140,142,145,147,149,151,153,155,157],{"class":65,"line":141},4,[63,143,144],{"class":101},"  occasion",[63,146,105],{"class":81},[63,148,108],{"class":77},[63,150,88],{"class":81},[63,152,113],{"class":91},[63,154,133],{"class":81},[63,156,136],{"class":91},[63,158,116],{"class":81},[63,160,162,165,167,169,171,173,175,177],{"class":65,"line":161},5,[63,163,164],{"class":101},"  recipientName",[63,166,105],{"class":81},[63,168,108],{"class":77},[63,170,88],{"class":81},[63,172,113],{"class":91},[63,174,133],{"class":81},[63,176,136],{"class":91},[63,178,116],{"class":81},[63,180,182,185,187,189,191,193,195,197],{"class":65,"line":181},6,[63,183,184],{"class":101},"  dedication",[63,186,105],{"class":81},[63,188,108],{"class":77},[63,190,88],{"class":81},[63,192,113],{"class":91},[63,194,133],{"class":81},[63,196,136],{"class":91},[63,198,116],{"class":81},[63,200,202,205,207,209,211,214,217,219,221,223,226,228,230,233,236],{"class":65,"line":201},7,[63,203,204],{"class":101},"  fields",[63,206,105],{"class":81},[63,208,108],{"class":77},[63,210,88],{"class":81},[63,212,213],{"class":91},"record",[63,215,216],{"class":81},"(",[63,218,108],{"class":77},[63,220,88],{"class":81},[63,222,113],{"class":91},[63,224,225],{"class":81},"(), ",[63,227,108],{"class":77},[63,229,88],{"class":81},[63,231,232],{"class":91},"unknown",[63,234,235],{"class":81},"()),   ",[63,237,239],{"class":238},"snYqZ","\u002F\u002F world-specific content\n",[63,241,243,246,248,250,252,255,257,260],{"class":65,"line":242},8,[63,244,245],{"class":101},"  photos",[63,247,105],{"class":81},[63,249,108],{"class":77},[63,251,88],{"class":81},[63,253,254],{"class":91},"array",[63,256,216],{"class":81},[63,258,259],{"class":77},"experiencePhotoSchema",[63,261,262],{"class":81},"),\n",[63,264,266,269,271,274,276,278],{"class":65,"line":265},9,[63,267,268],{"class":101},"  music",[63,270,105],{"class":81},[63,272,273],{"class":77},"experienceMusicSchema",[63,275,88],{"class":81},[63,277,136],{"class":91},[63,279,116],{"class":81},[63,281,283,286,288,290,292,295,298,302,306,308,311,313,316,318],{"class":65,"line":282},10,[63,284,285],{"class":101},"  mode",[63,287,105],{"class":81},[63,289,108],{"class":77},[63,291,88],{"class":81},[63,293,294],{"class":91},"enum",[63,296,297],{"class":81},"([",[63,299,301],{"class":300},"scnC2","'",[63,303,305],{"class":304},"spP0B","preview",[63,307,301],{"class":300},[63,309,310],{"class":81},", ",[63,312,301],{"class":300},[63,314,315],{"class":304},"published",[63,317,301],{"class":300},[63,319,320],{"class":81},"]),\n",[63,322,324],{"class":65,"line":323},11,[63,325,326],{"class":238},"  \u002F\u002F ...\n",[63,328,330],{"class":65,"line":329},12,[63,331,332],{"class":81},"})\n",[29,334,335],{},"If you haven't used zod: it lets you describe a shape once and get both a runtime validator and a TypeScript type from it. That's especially handy when the same data crosses a network boundary, because you can check it on arrival instead of trusting it.",[29,337,338],{},"That payload gets built in two different places:",[340,341,342,350],"ul",{},[343,344,345,349],"li",{},[346,347,348],"strong",{},"On the recipient's page",", the server builds it from the published gift in the database.",[343,351,352,355,356,359],{},[346,353,354],{},"In the editor",", the browser builds it from the form the sender is filling in, and sends it into a preview ",[60,357,358],{},"\u003Ciframe>",", so every edit shows up instantly without anything being saved.",[29,361,362],{},"The world can't tell the difference, and it doesn't need to. Two producers, one shape, and every world treats them the same way. The payoff comes each time the editor gets a new kind of content: it's one change to the shared schema and to the code that builds the payload, and every world receives it.",[364,365,367,368],"h3",{"id":366},"a-live-preview-over-postmessage","A live preview over ",[60,369,370],{},"postMessage",[29,372,373,374,376],{},"The preview is worth a closer look, because it's a neat use of a browser API that a lot of developers never touch. The iframe in the editor loads the real viewer app, the same one recipients see. The editor then sends it the payload with ",[60,375,370],{},", the browser's built-in way for two windows (including a page and an iframe inside it) to send each other data.",[29,378,379,380,383],{},"Two details make it reliable. First, the iframe announces when it's ready, and the editor waits for that before sending anything, because a message posted to a page that hasn't finished loading is simply lost. Second, every message is sent to one specific origin rather than ",[60,381,382],{},"'*'",", so the payload can only ever be delivered to the viewer app and never to some other page that ended up in that frame.",[29,385,386,387,391],{},"The result is a preview that isn't an imitation of the gift. It ",[388,389,390],"em",{},"is"," the gift, running the same code, fed from unsaved form state.",[36,393,395],{"id":394},"a-video-is-just-a-photo","A video is just a photo",[29,397,398],{},"This is my favorite trick in the codebase.",[29,400,401,402,405],{},"Supporting video sounds like it means teaching every world how to show one: a video texture in the 3D scenes, a ",[60,403,404],{},"\u003Cvideo>"," element in the 2D ones, play buttons, pausing the scene, and dozens of worlds each with their own layout. Instead, a video isn't a new kind of content at all.",[29,407,408],{},"When someone uploads a video, the browser grabs a frame from partway through, paints a soft play icon onto it, and saves that image as the memory's photo. The photo schema only gained two optional fields:",[52,410,412],{"className":54,"code":411,"filename":56,"language":57,"meta":58,"style":58},"export const experiencePhotoSchema = z.object({\n  url: z.string(),          \u002F\u002F always an image; for a video, its poster frame\n  caption: z.string().nullable(),\n  isCover: z.boolean(),\n  videoUrl: z.string().nullable().optional(),   \u002F\u002F set only for video memories\n  videoDurationSec: z.number().nullable().optional(),\n})\n",[60,413,414,432,451,470,486,514,538],{"__ignoreMap":58},[63,415,416,418,420,422,424,426,428,430],{"class":65,"line":66},[63,417,70],{"class":69},[63,419,74],{"class":73},[63,421,259],{"class":77},[63,423,82],{"class":81},[63,425,85],{"class":77},[63,427,88],{"class":81},[63,429,92],{"class":91},[63,431,95],{"class":81},[63,433,434,437,439,441,443,445,448],{"class":65,"line":98},[63,435,436],{"class":101},"  url",[63,438,105],{"class":81},[63,440,108],{"class":77},[63,442,88],{"class":81},[63,444,113],{"class":91},[63,446,447],{"class":81},"(),          ",[63,449,450],{"class":238},"\u002F\u002F always an image; for a video, its poster frame\n",[63,452,453,456,458,460,462,464,466,468],{"class":65,"line":119},[63,454,455],{"class":101},"  caption",[63,457,105],{"class":81},[63,459,108],{"class":77},[63,461,88],{"class":81},[63,463,113],{"class":91},[63,465,133],{"class":81},[63,467,136],{"class":91},[63,469,116],{"class":81},[63,471,472,475,477,479,481,484],{"class":65,"line":141},[63,473,474],{"class":101},"  isCover",[63,476,105],{"class":81},[63,478,108],{"class":77},[63,480,88],{"class":81},[63,482,483],{"class":91},"boolean",[63,485,116],{"class":81},[63,487,488,491,493,495,497,499,501,503,505,508,511],{"class":65,"line":161},[63,489,490],{"class":101},"  videoUrl",[63,492,105],{"class":81},[63,494,108],{"class":77},[63,496,88],{"class":81},[63,498,113],{"class":91},[63,500,133],{"class":81},[63,502,136],{"class":91},[63,504,133],{"class":81},[63,506,507],{"class":91},"optional",[63,509,510],{"class":81},"(),   ",[63,512,513],{"class":238},"\u002F\u002F set only for video memories\n",[63,515,516,519,521,523,525,528,530,532,534,536],{"class":65,"line":181},[63,517,518],{"class":101},"  videoDurationSec",[63,520,105],{"class":81},[63,522,108],{"class":77},[63,524,88],{"class":81},[63,526,527],{"class":91},"number",[63,529,133],{"class":81},[63,531,136],{"class":91},[63,533,133],{"class":81},[63,535,507],{"class":91},[63,537,116],{"class":81},[63,539,540],{"class":65,"line":201},[63,541,332],{"class":81},[29,543,544,545,548],{},"Every world already knows how to hang a photo in a frame, paste it on a billboard or tuck it into a locket, so a video rides along in the same slot with no changes to any world. When the recipient taps it, the lightbox that every photo already uses notices ",[60,546,547],{},"videoUrl"," and plays the film instead. And because every world already pauses itself while the lightbox is open, \"the scene stops so you can watch\" worked everywhere at once, for free.",[29,550,551,552,555],{},"The new fields are optional ",[388,553,554],{},"and"," nullable on purpose, so gifts saved before video existed still pass validation. Whenever you extend a contract that already has data behind it, the new fields need to be ones the old data can leave out.",[36,557,559],{"id":558},"assets-written-as-code","Assets written as code",[29,561,562,563,566,567,570],{},"The shared asset library ships almost no 3D files: no packs of ",[60,564,565],{},".glb"," models, no texture bundles. A reusable asset here is a small TypeScript module that ",[388,568,569],{},"builds"," a mesh or an SVG from a palette and a few shape parameters. A lantern is a function. A tree is a function that takes a trunk color, a canopy color and a height.",[29,572,573],{},"That one choice pays off in several places:",[340,575,576,579,582],{},[343,577,578],{},"Nothing licensed ever ships to a browser, because there's nothing to license.",[343,580,581],{},"There are no large binaries slowing down page loads or edge deploys.",[343,583,584],{},"Re-theming a world is a palette change, not a trip back into a 3D tool to re-export everything.",[29,586,587],{},"The library has close to a hundred of these kits. A few were written up front as infrastructure (palette helpers, particle systems, cut-paper frame shapes), but most were pulled out of a world after it was finished. When the next world needs a similar prop, it starts from something that already works.",[29,589,590],{},"There's one deliberate exception. For Deep Reef Dive, I wanted creatures and coral with hand-shaped silhouettes that are hard to get out of code, so that world's models are made in Blender and loaded as glTF files. It's a trade I made on purpose for one scene, not a change of default. It's also a useful reminder that a rule like \"assets are code\" exists to serve the product, not the other way around.",[364,592,594],{"id":593},"one-place-for-every-color","One place for every color",[29,596,597],{},"Each world keeps its whole look in one theme module, built from layered overrides. A base palette holds the fully art-directed default. The theme the sender picks overrides part of it. The occasion (a birthday, a retirement, a memorial) adjusts accents on top, like the pacing, a line of text, or whether certain effects play at all. Here's the real resolver from the fishing world:",[52,599,602],{"className":54,"code":600,"filename":601,"language":57,"meta":58,"style":58},"export function resolveFishingLook(occasion: string | null, tripTheme: string): FishingLook {\n  const theme = THEME_LOOKS[tripTheme] ?? THEME_LOOKS['summer-lake']!\n  const palette: FishingPalette = { ...BASE_PALETTE, ...(theme.palette ?? {}) }\n  const accents: FishingAccents = {\n    ...DEFAULT_ACCENTS,\n    ...((occasion && OCCASION_ACCENTS[occasion]) || {}),\n  }\n  return { palette, dayArc: theme.dayArc, accents }\n}\n","apps\u002Fexperience\u002Fapp\u002Ftemplates\u002Fgone-fishing\u002Ftheme-palette.ts",[60,603,604,649,691,726,742,753,779,784,814],{"__ignoreMap":58},[63,605,606,608,611,614,616,619,621,624,627,630,633,636,638,640,643,646],{"class":65,"line":66},[63,607,70],{"class":69},[63,609,610],{"class":73}," function",[63,612,613],{"class":91}," resolveFishingLook",[63,615,216],{"class":81},[63,617,618],{"class":77},"occasion",[63,620,105],{"class":81},[63,622,113],{"class":623},"s_NWU",[63,625,626],{"class":81}," | ",[63,628,629],{"class":73},"null",[63,631,632],{"class":81},",",[63,634,635],{"class":77}," tripTheme",[63,637,105],{"class":81},[63,639,113],{"class":623},[63,641,642],{"class":81},"):",[63,644,645],{"class":623}," FishingLook",[63,647,648],{"class":81}," {\n",[63,650,651,654,657,659,662,665,668,671,674,677,679,681,684,686,688],{"class":65,"line":98},[63,652,653],{"class":73},"  const ",[63,655,656],{"class":77},"theme",[63,658,82],{"class":81},[63,660,661],{"class":77}," THEME_LOOKS",[63,663,664],{"class":81},"[",[63,666,667],{"class":77},"tripTheme",[63,669,670],{"class":81},"]",[63,672,673],{"class":73}," ?? ",[63,675,676],{"class":77},"THEME_LOOKS",[63,678,664],{"class":81},[63,680,301],{"class":300},[63,682,683],{"class":304},"summer-lake",[63,685,301],{"class":300},[63,687,670],{"class":81},[63,689,690],{"class":73},"!\n",[63,692,693,695,698,700,703,705,708,711,714,716,718,720,723],{"class":65,"line":119},[63,694,653],{"class":73},[63,696,697],{"class":77},"palette",[63,699,105],{"class":81},[63,701,702],{"class":623},"FishingPalette",[63,704,82],{"class":81},[63,706,707],{"class":81}," { ...",[63,709,710],{"class":77},"BASE_PALETTE",[63,712,713],{"class":81},", ...(",[63,715,656],{"class":77},[63,717,88],{"class":81},[63,719,697],{"class":77},[63,721,722],{"class":73}," ??",[63,724,725],{"class":81}," {}) }\n",[63,727,728,730,733,735,738,740],{"class":65,"line":141},[63,729,653],{"class":73},[63,731,732],{"class":77},"accents",[63,734,105],{"class":81},[63,736,737],{"class":623},"FishingAccents",[63,739,82],{"class":81},[63,741,648],{"class":81},[63,743,744,747,750],{"class":65,"line":161},[63,745,746],{"class":81},"    ...",[63,748,749],{"class":77},"DEFAULT_ACCENTS",[63,751,752],{"class":81},",\n",[63,754,755,758,760,763,766,768,770,773,776],{"class":65,"line":181},[63,756,757],{"class":81},"    ...((",[63,759,618],{"class":77},[63,761,762],{"class":73}," &&",[63,764,765],{"class":77}," OCCASION_ACCENTS",[63,767,664],{"class":81},[63,769,618],{"class":77},[63,771,772],{"class":81},"]) ",[63,774,775],{"class":73},"||",[63,777,778],{"class":81}," {}),\n",[63,780,781],{"class":65,"line":201},[63,782,783],{"class":81},"  }\n",[63,785,786,789,792,794,796,799,801,803,805,807,809,811],{"class":65,"line":242},[63,787,788],{"class":69},"  return",[63,790,791],{"class":81}," { ",[63,793,697],{"class":77},[63,795,310],{"class":81},[63,797,798],{"class":101},"dayArc",[63,800,105],{"class":81},[63,802,656],{"class":77},[63,804,88],{"class":81},[63,806,798],{"class":77},[63,808,310],{"class":81},[63,810,732],{"class":77},[63,812,813],{"class":81}," }\n",[63,815,816],{"class":65,"line":265},[63,817,818],{"class":81},"}\n",[29,820,821],{},"Every other file in the world reads its colors from the object this returns, and none of them define a color inline. It's an easy rule to break with one quick hardcoded hex value, but keeping it is what makes a new theme a config change instead of a hunt through every file.",[36,823,825],{"id":824},"a-lifecycle-that-stays-boring-on-purpose","A lifecycle that stays boring on purpose",[29,827,828],{},"A gift only ever has three states:",[830,831,832],"mermaid",{},[52,833,836],{"className":834,"code":835,"language":830,"meta":58,"style":58},"language-mermaid shiki shiki-themes vitesse-light vitesse-dark","stateDiagram-v2\n  [*] --> editing\n  editing --> published: payment confirmed (server-side)\n  published --> editing: refund\n  published --> archived: sender archives\n  editing --> archived: sender archives, or retention sweep (unpaid, idle)\n  archived --> editing: restore\n",[60,837,838,843,848,853,858,863,868],{"__ignoreMap":58},[63,839,840],{"class":65,"line":66},[63,841,842],{},"stateDiagram-v2\n",[63,844,845],{"class":65,"line":98},[63,846,847],{},"  [*] --> editing\n",[63,849,850],{"class":65,"line":119},[63,851,852],{},"  editing --> published: payment confirmed (server-side)\n",[63,854,855],{"class":65,"line":141},[63,856,857],{},"  published --> editing: refund\n",[63,859,860],{"class":65,"line":161},[63,861,862],{},"  published --> archived: sender archives\n",[63,864,865],{"class":65,"line":181},[63,866,867],{},"  editing --> archived: sender archives, or retention sweep (unpaid, idle)\n",[63,869,870],{"class":65,"line":201},[63,871,872],{},"  archived --> editing: restore\n",[29,874,875,876,879,880,882],{},"The transitions follow what the product promises. A paid gift never expires, and only two things take it off the recipient's link: a refund, or the sender choosing to archive it. Restoring an archived gift always lands in ",[60,877,878],{},"editing",", never straight back in ",[60,881,315],{},", so nothing becomes public again without the sender seeing it first.",[29,884,885],{},"The editor also shows a checklist of what a gift is still missing (enough photos, a required field, a cover image) as a progress ribbon. That checklist is guidance only. It never disables the publish button, and the code that computes it is kept apart from anything that decides whether checkout is allowed. The reason is subtle: if a well-meaning future change added \"require X before publishing\" to the checklist, it could lock someone out of a gift they had already paid for. Keeping helpful hints and hard rules in different places makes that mistake hard to make.",[36,887,889],{"id":888},"no-trackers-on-a-gift","No trackers on a gift",[29,891,892],{},"The recipient's page has no tracking cookies and no third-party scripts. View counts are recorded on the server, as a side effect of requests that were happening anyway, instead of by an analytics script running in the recipient's browser. A gift is something personal that someone made for you, and it shouldn't behave like an ad.",[894,895,898],"callout",{"title":896,"type":897},"The takeaway","tip",[29,899,900,901,904],{},"A lot of the leverage here came from deciding what ",[388,902,903],{},"not"," to build: no new content type for video, no asset files, no extra gift states, no analytics scripts. Each of those decisions removed a whole category of work from every world that came after.",[906,907,908],"style",{},"html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .sHLBJ, html code.shiki .sHLBJ{--shiki-default:#998418;--shiki-dark:#B8A965}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}html pre.shiki code .scnC2, html code.shiki .scnC2{--shiki-default:#B5695977;--shiki-dark:#C98A7D77}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}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 .s_NWU, html code.shiki .s_NWU{--shiki-default:#2E8F82;--shiki-dark:#5DA994}",{"title":58,"searchDepth":98,"depth":119,"links":910},[911,915,916,919,920],{"id":38,"depth":98,"text":39,"children":912},[913],{"id":366,"depth":119,"text":914},"A live preview over postMessage",{"id":394,"depth":98,"text":395},{"id":558,"depth":98,"text":559,"children":917},[918],{"id":593,"depth":119,"text":594},{"id":824,"depth":98,"text":825},{"id":888,"depth":98,"text":889},"How dozens of very different worlds read one data contract, why a video is secretly a photo, and why most of the 3D assets are code instead of files.","md",false,"personal",{},{},{"title":928},"Rendering pipeline",999,"\u002Fprojects\u002Fforevermore\u002Frendering-pipeline",null,{"title":24,"description":921},[],"projects\u002Fforevermore\u002F2.rendering-pipeline","lijykwdtkYlU_5HEVXDx9oqSBNWZFyL0b32FMhFoXNw",[937,947,954,956],{"path":938,"title":939,"stem":940,"navigation":941,"description":942,"preview":943},"\u002Fprojects\u002Fforevermore","Forevermore","projects\u002Fforevermore\u002Findex",true,"A platform for personalized animated gift worlds. Pick a scene, fill it with photos and a song, and send it like a message.",{"src":944,"alt":945,"og":946},"\u002Fprojects\u002Fforevermore\u002Fpreview.webp","Forevermore: 'Create a world for someone you love.' beside three animated gift worlds fanned out like postcards","\u002Fprojects\u002Fforevermore\u002Fog.png",{"path":948,"title":949,"stem":950,"navigation":951,"description":953,"preview":931},"\u002Fprojects\u002Fforevermore\u002Farchitecture","Subdomains and permissions","projects\u002Fforevermore\u002F1.architecture",{"title":952},"Architecture","How every gift gets its own address on a host that doesn't support wildcard domains, and how Postgres grants keep payment state out of the browser's reach.",{"path":930,"title":24,"stem":934,"navigation":955,"description":921,"preview":931},{"title":928},{"path":957,"title":958,"stem":959,"navigation":960,"description":962,"preview":931},"\u002Fprojects\u002Fforevermore\u002Fautopilot","A marketing pipeline built as a state machine","projects\u002Fforevermore\u002F3.autopilot",{"title":961},"Autopilot","Autopilot plans, writes, renders and checks a week of social posts on its own. How a state machine, compare-and-swap updates and injected dependencies make it safe to run unattended.",[964,965,966,967,968,969,970,971,972,973,974,975,976],"TypeScript","Vue 3","Nuxt","Pinia","Tailwind CSS","Python","FastAPI","SQLAlchemy","Pydantic","PostgreSQL","Supabase","GCP","Firebase",1790471332101]