[{"data":1,"prerenderedAt":1503},["ShallowReactive",2],{"blog:\u002Fblog\u002Fbaseline-grid":3},{"page":4,"surround":809},{"id":5,"title":6,"author":7,"body":8,"brandColor":791,"colorBg":791,"colorBgSecondary":791,"colorBrand":791,"colorFooter":791,"colorFooterText":791,"colorHeaders":791,"colorInvertedText":791,"colorText":791,"coverImage":17,"date":792,"description":793,"draft":794,"enquiry":791,"extension":795,"galleryDark":794,"galleryImage":791,"gallerySize":791,"grid":796,"h1":797,"hoverVideo":791,"label":798,"meta":799,"navigation":796,"order":361,"path":800,"seo":801,"stem":802,"tags":803,"theme":807,"updated":791,"__hash__":808},"blog\u002Fblog\u002Fbaseline-grid.mdc","Setting the Web on a Baseline Grid - Ben Nasedkin","Ben Nasedkin",{"type":9,"value":10,"toc":776},"minimark",[11,18,22,26,29,34,37,40,43,59,62,66,69,72,107,111,114,118,121,207,210,254,265,276,280,283,288,440,443,471,475,481,485,494,521,536,540,547,551,562,641,644,648,655,659,663,678,682,685,747,751,763,772],[12,13],"content-media",{"alt":14,"ratio":15,"sizes":16,"src":17},"The word Baseline set in Graphik on an 8 px grid, a red rule marking its baseline","1200:752","mobile:100vw desktop:66vw wide:66vw","\u002Fblog\u002Fbaseline-grid\u002F01.png",[19,20,21],"p",{},"Set two columns of text side by side, a caption and the body copy next to it. If their lines sit at different heights, the eye notices before you can say what is wrong. In a magazine this does not happen: every line on the spread sits on one shared, invisible ruler. On the web it happens almost every time.",[12,23],{"alt":24,"ratio":15,"sizes":16,"src":25},"Two pairs of columns: on the left the box tops agree and the lines drift apart; on the right the head and body share baselines","\u002Fblog\u002Fbaseline-grid\u002F02.png",[19,27,28],{},"This post covers why, and how this site puts every baseline (headings, captions, tables, buttons) on one 8 px step. No JavaScript and no offsets tuned by eye: one CSS mixin that could not have been written two years ago.",[30,31,33],"h2",{"id":32},"what-a-baseline-grid-is-in-print","What a Baseline Grid Is in Print",[19,35,36],{},"The baseline is the line letters stand on: the bottom of x, n and o. A baseline grid is a set of evenly spaced horizontal lines across the page, and every baseline of text lands on one of them.",[19,38,39],{},"In print this is an old craft. Compositors measured lines in whole body sizes and leads, so text on both sides of a sheet matched by itself and lines did not show through between the lines on the reverse. The Swiss school turned it into a system: in Müller-Brockmann's grids a module's height is a whole number of body lines, and the top of an image meets the cap height in the next column.",[19,41,42],{},"In InDesign it is one checkbox. Set a Baseline Grid (say, 12 pt), turn on Align to Baseline Grid in the paragraph style, and every line snaps to the nearest grid line. The rules are simple:",[44,45,46,50,53,56],"ul",{},[47,48,49],"li",{},"every style's leading is a multiple of the grid step;",[47,51,52],{},"space before and after paragraphs is a multiple too;",[47,54,55],{},"a large heading spans two or three grid lines, but its baseline still sits on the grid;",[47,57,58],{},"images and panels are a whole number of steps tall.",[19,60,61],{},"The result is not visible as a technique; it reads as order. A page stays calm with five type sizes on it.",[30,63,65],{"id":64},"why-the-web-does-not-do-this-by-itself","Why the Web Does Not Do This by Itself",[19,67,68],{},"A browser aligns boxes, not letters. The top of a text block is the top of its first line box, not the top of its capitals and not the baseline.",[19,70,71],{},"Three things make the baseline's position unpredictable:",[73,74,75,87,93],"ol",{},[47,76,77,81,82,86],{},[78,79,80],"strong",{},"Half-leading"," The difference between ",[83,84,85],"code",{},"line-height"," and the font's height is split and added above and below each line. Text at 16\u002F24 gets invisible air above its first line that the design file never had.",[47,88,89,92],{},[78,90,91],{},"Font metrics"," Where the baseline sits inside the line depends on the font file's ascender and descender. Two typefaces at the same size put it in different places.",[47,94,95,98,99,102,103,106],{},[78,96,97],{},"No snapping"," CSS has no Align to Baseline Grid. The CSS Line Grid draft (",[83,100,101],{},"line-grid",", ",[83,104,105],{},"line-snap",") has been a working draft since 2014 and no browser implemented it.",[12,108],{"alt":109,"ratio":15,"sizes":16,"src":110},"On the left a line box with half-leading above and below; on the right the same line trimmed from cap height to baseline","\u002Fblog\u002Fbaseline-grid\u002F03.png",[19,112,113],{},"Hence the most common designer complaint: \"the gap is 24 in Figma but looks bigger in the browser.\" The gap is 24; two half-leadings were added to it, one under the first block and one over the next.",[30,115,117],{"id":116},"from-magic-numbers-to-text-box-trim","From Magic Numbers to text-box-trim",[19,119,120],{},"For years it ran on magic numbers tuned to one font. Now the browser knows where the top and bottom of the letters are.",[122,123,124,140],"table",{},[125,126,127],"thead",{},[128,129,130,134,137],"tr",{},[131,132,133],"th",{},"Approach",[131,135,136],{},"How it works",[131,138,139],{},"Catch",[141,142,143,157,175,194],"tbody",{},[128,144,145,149,154],{},[146,147,148],"td",{},"Rhythm only",[146,150,151,153],{},[83,152,85],{}," and every gap are multiples of the module",[146,155,156],{},"Lines keep an even step, but the baselines themselves sit off the grid",[128,158,159,162,172],{},[146,160,161],{},"Basehold and similar",[146,163,164,167,168,171],{},[83,165,166],{},"position: relative; top: Npx"," plus a reduced ",[83,169,170],{},"margin-bottom",", numbers tuned per size",[146,173,174],{},"A new font or size means a new table of offsets",[128,176,177,180,191],{},[146,178,179],{},"Capsize",[146,181,182,183,186,187,190],{},"Negative margins on ",[83,184,185],{},"::before"," and ",[83,188,189],{},"::after"," computed from font metrics",[146,192,193],{},"Accurate, but the metrics are hard-coded; a fallback font drifts",[128,195,196,201,204],{},[146,197,198],{},[83,199,200],{},"text-box-trim",[146,202,203],{},"The browser trims half-leading to the cap height and the baseline",[146,205,206],{},"Needs a fallback for older browsers",[19,208,209],{},"The new property looks like this:",[211,212,217],"pre",{"className":213,"code":214,"language":215,"meta":216,"style":216},"language-css shiki shiki-themes github-light github-dark","h1 {\n  text-box: trim-both cap alphabetic;\n}\n","css","",[83,218,219,232,248],{"__ignoreMap":216},[220,221,224,228],"span",{"class":222,"line":223},"line",1,[220,225,227],{"class":226},"s9eBZ","h1",[220,229,231],{"class":230},"sVt8B"," {\n",[220,233,235,239,242,245],{"class":222,"line":234},2,[220,236,238],{"class":237},"sj4cs","  text-box",[220,240,241],{"class":230},": trim-both cap ",[220,243,244],{"class":237},"alphabetic",[220,246,247],{"class":230},";\n",[220,249,251],{"class":222,"line":250},3,[220,252,253],{"class":230},"}\n",[19,255,256,257,264],{},"The block now starts at the top of the capitals and ends at the baseline of its last line, like a text frame in InDesign. A 24 px gap between two such blocks is 24 px between letters, not between invisible boxes. Safari has it from 18.2, Chrome from 133, and Firefox from 154 (August 2026), which made it ",[258,259,263],"a",{"href":260,"rel":261},"https:\u002F\u002Fweb.dev\u002Fblog\u002Fweb-platform-08-2026",[262],"nofollow","Baseline",".",[19,266,267,268,271,272,275],{},"Trimming alone does not give you a grid. It removes the excess but does not put the baseline on a step of the module: cap height is rarely a whole number of modules. That is the job of two other recent additions to CSS: the ",[83,269,270],{},"round()"," function and the ",[83,273,274],{},"cap"," unit.",[30,277,279],{"id":278},"how-this-site-does-it","How This Site Does It",[19,281,282],{},"The system rests on three rules: an 8 px module, every vertical size a multiple of it, and every text block starting and ending on a grid line.",[284,285,287],"h3",{"id":286},"one-mixin","One Mixin",[211,289,293],{"className":290,"code":291,"language":292,"meta":216,"style":216},"language-scss shiki shiki-themes github-light github-dark","@mixin on-baseline($top: 0px) {\n  padding-top: $top;\n  @supports (text-box: trim-both cap alphabetic) {\n    text-box: trim-both cap alphabetic;\n    padding-top: calc(#{$top} + round(up, 1cap, 8px) - 1cap);\n  }\n}\n","scss",[83,294,295,324,335,359,375,429,435],{"__ignoreMap":216},[220,296,297,301,305,308,312,315,318,321],{"class":222,"line":223},[220,298,300],{"class":299},"szBVR","@mixin",[220,302,304],{"class":303},"sScJk"," on-baseline",[220,306,307],{"class":230},"(",[220,309,311],{"class":310},"s4XuR","$top",[220,313,314],{"class":230},": ",[220,316,317],{"class":237},"0",[220,319,320],{"class":299},"px",[220,322,323],{"class":230},") {\n",[220,325,326,329,331,333],{"class":222,"line":234},[220,327,328],{"class":237},"  padding-top",[220,330,314],{"class":230},[220,332,311],{"class":310},[220,334,247],{"class":230},[220,336,337,340,343,346,349,352,355,357],{"class":222,"line":250},[220,338,339],{"class":299},"  @supports",[220,341,342],{"class":230}," (",[220,344,345],{"class":237},"text-box",[220,347,348],{"class":230},": trim",[220,350,351],{"class":237},"-",[220,353,354],{"class":230},"both cap ",[220,356,244],{"class":237},[220,358,323],{"class":230},[220,360,362,365,367,369,371,373],{"class":222,"line":361},4,[220,363,364],{"class":237},"    text-box",[220,366,348],{"class":230},[220,368,351],{"class":237},[220,370,354],{"class":230},[220,372,244],{"class":237},[220,374,247],{"class":230},[220,376,378,381,383,386,388,391,394,397,399,402,404,407,409,411,414,416,419,421,424,426],{"class":222,"line":377},5,[220,379,380],{"class":237},"    padding-top",[220,382,314],{"class":230},[220,384,385],{"class":237},"calc",[220,387,307],{"class":230},[220,389,390],{"class":310},"#{$top}",[220,392,393],{"class":299}," +",[220,395,396],{"class":237}," round",[220,398,307],{"class":230},[220,400,401],{"class":237},"up",[220,403,102],{"class":230},[220,405,406],{"class":237},"1",[220,408,274],{"class":310},[220,410,102],{"class":230},[220,412,413],{"class":237},"8",[220,415,320],{"class":299},[220,417,418],{"class":230},") ",[220,420,351],{"class":299},[220,422,423],{"class":237}," 1",[220,425,274],{"class":310},[220,427,428],{"class":230},");\n",[220,430,432],{"class":222,"line":431},6,[220,433,434],{"class":230},"  }\n",[220,436,438],{"class":222,"line":437},7,[220,439,253],{"class":230},[19,441,442],{},"Step by step:",[73,444,445,455,461,468],{},[47,446,447,450,451,454],{},[83,448,449],{},"text-box: trim-both cap alphabetic"," makes the block run from cap height to baseline, so the first baseline sits exactly ",[83,452,453],{},"1cap"," below the top of the block.",[47,456,457,460],{},[83,458,459],{},"round(up, 1cap, 8px)"," is the next multiple of 8 up. For a cap height of 11.4 px that is 16 px.",[47,462,463,464,467],{},"The difference goes into ",[83,465,466],{},"padding-top",": 16 − 11.4 = 4.6 px. The first baseline drops exactly onto a grid line.",[47,469,470],{},"The bottom of the block is trimmed to the baseline, which is on the grid, so the next block starts on the grid too, provided the gap is a multiple of 8.",[12,472],{"alt":473,"ratio":15,"sizes":16,"src":474},"The on-baseline mixin measured on the word Hamburg: block top on grid line 256, cap height 114.4 px rounded up to 120 px, baseline on grid line 376","\u002Fblog\u002Fbaseline-grid\u002F04.png",[19,476,477,478,480],{},"The ",[83,479,274],{}," unit takes the cap height from the font that actually loaded. No metrics table as in Capsize: change the font, or let a fallback kick in, and the formula recalculates itself.",[284,482,484],{"id":483},"leading-in-multiples-of-the-module","Leading in Multiples of the Module",[19,486,487,488,490,491,493],{},"Once the first baseline is on the grid, the rest follow as long as ",[83,489,85],{}," is a multiple of 8. Captions are 16 px, body copy 24 px, row titles 32 px. Fluid type uses the same ",[83,492,270],{},":",[211,495,497],{"className":290,"code":496,"language":292,"meta":216,"style":216},"font-size: clamp(2rem, 4.5vw, 4rem);\nline-height: round(1.08em, 8px);\n",[83,498,499,510],{"__ignoreMap":216},[220,500,501,504,507],{"class":222,"line":223},[220,502,503],{"class":230},"font-size: clamp(2rem, 4",[220,505,506],{"class":303},".5vw",[220,508,509],{"class":230},", 4rem);\n",[220,511,512,515,518],{"class":222,"line":234},[220,513,514],{"class":230},"line-height: round(1",[220,516,517],{"class":303},".08em",[220,519,520],{"class":230},", 8px);\n",[19,522,523,524,527,528,531,532,535],{},"The font size scales freely; the leading steps in eights. Spacing sits on the module too: ",[83,525,526],{},"--space-copy"," 16, ",[83,529,530],{},"--space-block"," 24, ",[83,533,534],{},"--space-group"," 32, header height 64.",[284,537,539],{"id":538},"rules-come-out-of-the-leading","Rules Come out of the Leading",[19,541,542,543,546],{},"A 1 px rule pushes everything below it down a pixel, so the pixel comes out of the neighbouring padding: a table row under its rule is ",[83,544,545],{},"padding: calc(1rem - 1px) 0 1rem",". The rule takes the place of some of the air, and the rhythm holds.",[284,548,550],{"id":549},"images-round-to-8","Images Round to 8",[19,552,553,554,557,558,561],{},"An image with ",[83,555,556],{},"height: auto"," gets an arbitrary height and shifts the text below it. In ",[83,559,560],{},"Media.vue"," the wrapper is a size container, and the height is computed from its width and rounded:",[211,563,565],{"className":290,"code":564,"language":292,"meta":216,"style":216},".media { container-type: inline-size; }\n.media img {\n  height: round(calc(100cqw * var(--media-ratio)), 8px);\n}\n",[83,566,567,586,595,637],{"__ignoreMap":216},[220,568,569,572,575,578,580,583],{"class":222,"line":223},[220,570,571],{"class":303},".media",[220,573,574],{"class":230}," { ",[220,576,577],{"class":237},"container-type",[220,579,314],{"class":230},[220,581,582],{"class":237},"inline-size",[220,584,585],{"class":230},"; }\n",[220,587,588,590,593],{"class":222,"line":234},[220,589,571],{"class":303},[220,591,592],{"class":226}," img",[220,594,231],{"class":230},[220,596,597,600,602,605,607,609,611,614,617,620,623,625,628,631,633,635],{"class":222,"line":250},[220,598,599],{"class":237},"  height",[220,601,314],{"class":230},[220,603,604],{"class":237},"round",[220,606,307],{"class":230},[220,608,385],{"class":237},[220,610,307],{"class":230},[220,612,613],{"class":237},"100",[220,615,616],{"class":299},"cqw",[220,618,619],{"class":299}," *",[220,621,622],{"class":237}," var",[220,624,307],{"class":230},[220,626,627],{"class":310},"--media-ratio",[220,629,630],{"class":230},")), ",[220,632,413],{"class":237},[220,634,320],{"class":299},[220,636,428],{"class":230},[220,638,639],{"class":222,"line":361},[220,640,253],{"class":230},[19,642,643],{},"The proportion drifts by a fraction of a pixel; the text under the image stays on the grid. Logos have fixed heights of 40 and 96 px.",[284,645,647],{"id":646},"rows-share-one-baseline","Rows Share One Baseline",[19,649,650,651,654],{},"A row in the work list holds a number (12 px), a title (32 px) and a description (16 px). A grid row with ",[83,652,653],{},"align-items: baseline"," puts them on one line, as in a magazine table, instead of pinning them to the top edge.",[12,656],{"alt":657,"ratio":15,"sizes":16,"src":658},"Three work list rows: number, 32 px title and 16 px description standing on one shared baseline","\u002Fblog\u002Fbaseline-grid\u002F05.png",[284,660,662],{"id":661},"the-grid-on-a-key","The Grid on a Key",[19,664,665,666,669,670,673,674,677],{},"Checking this by eye is pointless. On this site the ",[83,667,668],{},"g"," key (or ",[83,671,672],{},"?grid"," in the address) turns on an overlay: cyan lines every 8 px and red bands for the 14 columns. It is one ",[83,675,676],{},"repeating-linear-gradient"," that follows the scroll. A baseline is on the grid when the letters stand on a line, visible at a glance without the inspector.",[30,679,681],{"id":680},"where-the-grid-breaks","Where the Grid Breaks",[19,683,684],{},"A perfect grid is not yet possible in a browser. Here is what had to be worked around:",[44,686,687,700,710,720,734],{},[47,688,689,692,693,695,696,699],{},[78,690,691],{},"Flex and grid"," Only block containers trim: ",[83,694,200],{}," does nothing on flex and grid containers. In a link with an arrow, the text ",[83,697,698],{},"\u003Cspan>"," is trimmed and the arrow aligns by its bottom edge.",[47,701,702,705,706,709],{},[78,703,704],{},"Anonymous text"," Text after a ",[83,707,708],{},"\u003Cstrong>"," in Markdown is not its own element, so there is nothing to trim. In those rows the number and title are positioned absolutely, and the row itself is a plain block.",[47,711,712,715,716,719],{},[78,713,714],{},"Form controls"," An ",[83,717,718],{},"\u003Cinput>"," cannot be trimmed, so it pads 7 px above a 24 px line. The CAPTCHA widget gets a 72 px box.",[47,721,722,725,726,729,730,733],{},[78,723,724],{},"Inline strut"," A standalone link with ",[83,727,728],{},"display: inline"," drags its parent's strut along. ",[83,731,732],{},"display: flex; width: fit-content"," fixes it.",[47,735,736,739,740,742,743,746],{},[78,737,738],{},"Older browsers"," Without ",[83,741,200],{}," the branch outside ",[83,744,745],{},"@supports"," applies: baselines are offset, but line boxes still sit on the module and the rhythm holds.",[30,748,750],{"id":749},"conclusion","Conclusion",[19,752,753,754,756,757,759,760,762],{},"A baseline grid on the web no longer needs magic numbers. Three tools are enough: ",[83,755,200],{},", the ",[83,758,274],{}," unit and ",[83,761,270],{},", plus the discipline to keep every vertical size a multiple of the module.",[19,764,765,766,768,769,771],{},"The valuable part is not the technique but the rule from print: distance is measured from letters to letters, not from box to box. Once CSS thinks the same way, a site starts to look typeset rather than laid out. Turn the grid on right here: View in the left margin, the ",[83,767,668],{}," key or ",[83,770,672],{}," in the address.",[773,774,775],"style",{},"html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":216,"searchDepth":234,"depth":234,"links":777},[778,779,780,781,789,790],{"id":32,"depth":234,"text":33},{"id":64,"depth":234,"text":65},{"id":116,"depth":234,"text":117},{"id":278,"depth":234,"text":279,"children":782},[783,784,785,786,787,788],{"id":286,"depth":250,"text":287},{"id":483,"depth":250,"text":484},{"id":538,"depth":250,"text":539},{"id":549,"depth":250,"text":550},{"id":646,"depth":250,"text":647},{"id":661,"depth":250,"text":662},{"id":680,"depth":234,"text":681},{"id":749,"depth":234,"text":750},null,"2026-10-28","How this site puts every line of text on an 8 px baseline grid with text-box-trim, the cap unit and round(), and where the grid still breaks.",false,"mdc",true,"Setting the Web on a Baseline Grid","Baseline Grid",{},"\u002Fblog\u002Fbaseline-grid",{"title":6,"description":793},"blog\u002Fbaseline-grid",[215,804,805,806],"typography","layout","design-systems","swiss_precision","TjrvjcqbcZ17aAFWTNspzd4czFIKgh4zQTb3gBrk-4o",[810,791],{"id":811,"title":812,"author":7,"body":813,"brandColor":791,"colorBg":791,"colorBgSecondary":791,"colorBrand":791,"colorFooter":791,"colorFooterText":791,"colorHeaders":791,"colorInvertedText":791,"colorText":791,"coverImage":791,"date":1488,"description":1489,"draft":794,"enquiry":791,"extension":795,"galleryDark":794,"galleryImage":791,"gallerySize":791,"grid":794,"h1":1490,"hoverVideo":791,"label":1491,"meta":1492,"navigation":796,"order":234,"path":1493,"seo":1494,"stem":1495,"tags":1496,"theme":807,"updated":1501,"__hash__":1502},"blog\u002Fblog\u002Fbun-nuxt4-runtime-guide.mdc","Deploying Nuxt 4 on Bun: Runtime, SQLite, and Image Processing - Ben Nasedkin",{"type":9,"value":814,"toc":1480},[815,818,825,829,845,922,933,950,954,957,1003,1019,1022,1083,1086,1090,1107,1110,1187,1202,1206,1213,1367,1377,1381,1399,1403,1418,1464,1474,1477],[19,816,817],{},"Using Bun as a package manager, running the Nuxt CLI with Bun, building a Bun-targeted server, and starting that server under Bun are four separate decisions. A green build checks only part of the deployment: failures can still appear when the generated server starts or first loads an image adapter.",[19,819,820,821,824],{},"This is the configuration used for this portfolio, not a claim that Nuxt requires Bun. ",[78,822,823],{},"Verified October 4, 2026:"," Bun 1.4.2, Nuxt 4.5.2, Nuxt Content 3.16.1, Nuxt Image 2.1.0, and TypeScript 6.0.3. The isolated-output test below ran on Linux ARM64; production builds run on the deployment host.",[30,826,828],{"id":827},"make-runtime-selection-explicit","Make Runtime Selection Explicit",[19,830,831,834,835,840,841,844],{},[83,832,833],{},"bun install"," determines how packages are installed; it does not guarantee that a CLI with a Node shebang executes under Bun. Bun's ",[258,836,839],{"href":837,"rel":838},"https:\u002F\u002Fbun.com\u002Fguides\u002Fecosystem\u002Fnuxt",[262],"Nuxt guide"," describes the ",[83,842,843],{},"--bun"," override. This project puts it inside each relevant script:",[211,846,850],{"className":847,"code":848,"language":849,"meta":216,"style":216},"language-json shiki shiki-themes github-light github-dark","{\n  \"packageManager\": \"bun@1.4.2\",\n  \"scripts\": {\n    \"dev\": \"bun --bun run nuxt dev\",\n    \"build\": \"bun --bun run nuxt build\",\n    \"typecheck\": \"bun --bun run nuxt typecheck\"\n  }\n}\n","json",[83,851,852,857,871,879,891,903,913,917],{"__ignoreMap":216},[220,853,854],{"class":222,"line":223},[220,855,856],{"class":230},"{\n",[220,858,859,862,864,868],{"class":222,"line":234},[220,860,861],{"class":237},"  \"packageManager\"",[220,863,314],{"class":230},[220,865,867],{"class":866},"sZZnC","\"bun@1.4.2\"",[220,869,870],{"class":230},",\n",[220,872,873,876],{"class":222,"line":250},[220,874,875],{"class":237},"  \"scripts\"",[220,877,878],{"class":230},": {\n",[220,880,881,884,886,889],{"class":222,"line":361},[220,882,883],{"class":237},"    \"dev\"",[220,885,314],{"class":230},[220,887,888],{"class":866},"\"bun --bun run nuxt dev\"",[220,890,870],{"class":230},[220,892,893,896,898,901],{"class":222,"line":377},[220,894,895],{"class":237},"    \"build\"",[220,897,314],{"class":230},[220,899,900],{"class":866},"\"bun --bun run nuxt build\"",[220,902,870],{"class":230},[220,904,905,908,910],{"class":222,"line":431},[220,906,907],{"class":237},"    \"typecheck\"",[220,909,314],{"class":230},[220,911,912],{"class":866},"\"bun --bun run nuxt typecheck\"\n",[220,914,915],{"class":222,"line":437},[220,916,434],{"class":230},[220,918,920],{"class":222,"line":919},8,[220,921,253],{"class":230},[19,923,924,925,928,929,932],{},"Consequently, ",[83,926,927],{},"bun run build"," runs the script that explicitly selects Bun; merely changing the package-manager command in a different project would not have the same effect. The top-level ",[83,930,931],{},"packageManager"," field records a version expectation, not an installation mechanism.",[19,934,935,936,939,940,945,946,949],{},"Some older Bun\u002FNuxt combinations needed ",[83,937,938],{},"--no-fork"," to restore the development HMR connection. The ",[258,941,944],{"href":942,"rel":943},"https:\u002F\u002Fgithub.com\u002Foven-sh\u002Fbun\u002Fissues\u002F18440",[262],"original Bun issue"," was reported against Bun 1.2.5 and closed as completed in July 2026. This repository still documents ",[83,947,948],{},"bun run dev --no-fork"," as a known development invocation; test HMR on your actual versions rather than treating that flag as a universal Nuxt requirement. It has no role in the production server command.",[30,951,953],{"id":952},"match-the-nitro-preset-to-the-server-runtime","Match the Nitro Preset to the Server Runtime",[19,955,956],{},"The deployed server uses Nitro's Bun preset:",[211,958,962],{"className":959,"code":960,"language":961,"meta":216,"style":216},"language-typescript shiki shiki-themes github-light github-dark","export default defineNuxtConfig({\n  nitro: {\n    preset: 'bun',\n  },\n})\n","typescript",[83,963,964,978,983,993,998],{"__ignoreMap":216},[220,965,966,969,972,975],{"class":222,"line":223},[220,967,968],{"class":299},"export",[220,970,971],{"class":299}," default",[220,973,974],{"class":303}," defineNuxtConfig",[220,976,977],{"class":230},"({\n",[220,979,980],{"class":222,"line":234},[220,981,982],{"class":230},"  nitro: {\n",[220,984,985,988,991],{"class":222,"line":250},[220,986,987],{"class":230},"    preset: ",[220,989,990],{"class":866},"'bun'",[220,992,870],{"class":230},[220,994,995],{"class":222,"line":361},[220,996,997],{"class":230},"  },\n",[220,999,1000],{"class":222,"line":377},[220,1001,1002],{"class":230},"})\n",[19,1004,1005,1010,1011,1014,1015,1018],{},[258,1006,1009],{"href":1007,"rel":1008},"https:\u002F\u002Fnitro.build\u002Fdeploy\u002Fruntimes\u002Fbun",[262],"Nitro documents"," the Bun preset and the generated ",[83,1012,1013],{},".output\u002Fserver\u002Findex.mjs"," entry point. The preset changes the generated server; ",[78,1016,1017],{},"the process that executes the entry point determines the runtime",". A Node-targeted output can sometimes run under Bun, but that does not establish that its dependency resolution and lazy-loading paths match the Bun-targeted build.",[19,1020,1021],{},"Run the output, not only the build command:",[211,1023,1027],{"className":1024,"code":1025,"language":1026,"meta":216,"style":216},"language-bash shiki shiki-themes github-light github-dark","bun run test\nbun run typecheck\nbun run build\nHOST=127.0.0.1 PORT=3001 bun .output\u002Fserver\u002Findex.mjs\n","bash",[83,1028,1029,1040,1049,1058],{"__ignoreMap":216},[220,1030,1031,1034,1037],{"class":222,"line":223},[220,1032,1033],{"class":303},"bun",[220,1035,1036],{"class":866}," run",[220,1038,1039],{"class":866}," test\n",[220,1041,1042,1044,1046],{"class":222,"line":234},[220,1043,1033],{"class":303},[220,1045,1036],{"class":866},[220,1047,1048],{"class":866}," typecheck\n",[220,1050,1051,1053,1055],{"class":222,"line":250},[220,1052,1033],{"class":303},[220,1054,1036],{"class":866},[220,1056,1057],{"class":866}," build\n",[220,1059,1060,1063,1066,1069,1072,1074,1077,1080],{"class":222,"line":361},[220,1061,1062],{"class":230},"HOST",[220,1064,1065],{"class":299},"=",[220,1067,1068],{"class":866},"127.0.0.1",[220,1070,1071],{"class":230}," PORT",[220,1073,1065],{"class":299},[220,1075,1076],{"class":866},"3001",[220,1078,1079],{"class":303}," bun",[220,1081,1082],{"class":866}," .output\u002Fserver\u002Findex.mjs\n",[19,1084,1085],{},"Use an available port and terminate the local process after smoke testing. Building under Bun and subsequently launching with Node is a different compatibility test.",[30,1087,1089],{"id":1088},"account-for-nuxt-contents-sqlite-connector","Account for Nuxt Content's SQLite Connector",[19,1091,1092,1093,1098,1099,1102,1103,1106],{},"This site's Nuxt Content 3 database uses SQLite. Its ",[258,1094,1097],{"href":1095,"rel":1096},"https:\u002F\u002Fcontent.nuxt.com\u002Fdocs\u002Fgetting-started\u002Fconfiguration#experimentalsqliteconnector",[262],"connector documentation"," states that building under Bun automatically selects ",[83,1100,1101],{},"bun:sqlite"," for a Bun target. If CI builds under Node for a Bun deployment, explicitly set ",[83,1104,1105],{},"content.experimental.sqliteConnector: 'bun'","; the build can then use a Node-compatible connector while the deployed runtime uses Bun's driver.",[19,1108,1109],{},"This project builds and runs under Bun, so it does not set that experimental option:",[211,1111,1113],{"className":959,"code":1112,"language":961,"meta":216,"style":216},"export default defineNuxtConfig({\n  content: {\n    database: {\n      type: 'sqlite',\n      filename: '.\u002F.data\u002Fcontent\u002Fcontents.sqlite',\n    },\n  },\n  nitro: {\n    preset: 'bun',\n  },\n})\n",[83,1114,1115,1125,1130,1135,1145,1155,1160,1164,1168,1177,1182],{"__ignoreMap":216},[220,1116,1117,1119,1121,1123],{"class":222,"line":223},[220,1118,968],{"class":299},[220,1120,971],{"class":299},[220,1122,974],{"class":303},[220,1124,977],{"class":230},[220,1126,1127],{"class":222,"line":234},[220,1128,1129],{"class":230},"  content: {\n",[220,1131,1132],{"class":222,"line":250},[220,1133,1134],{"class":230},"    database: {\n",[220,1136,1137,1140,1143],{"class":222,"line":361},[220,1138,1139],{"class":230},"      type: ",[220,1141,1142],{"class":866},"'sqlite'",[220,1144,870],{"class":230},[220,1146,1147,1150,1153],{"class":222,"line":377},[220,1148,1149],{"class":230},"      filename: ",[220,1151,1152],{"class":866},"'.\u002F.data\u002Fcontent\u002Fcontents.sqlite'",[220,1154,870],{"class":230},[220,1156,1157],{"class":222,"line":431},[220,1158,1159],{"class":230},"    },\n",[220,1161,1162],{"class":222,"line":437},[220,1163,997],{"class":230},[220,1165,1166],{"class":222,"line":919},[220,1167,982],{"class":230},[220,1169,1171,1173,1175],{"class":222,"line":1170},9,[220,1172,987],{"class":230},[220,1174,990],{"class":866},[220,1176,870],{"class":230},[220,1178,1180],{"class":222,"line":1179},10,[220,1181,997],{"class":230},[220,1183,1185],{"class":222,"line":1184},11,[220,1186,1002],{"class":230},[19,1188,1189,1190,1193,1194,1197,1198,1201],{},"The configured file path is for the content build; it is not a reason to copy the development database into the production artifact. An SQLite native-module error warrants checking the ",[78,1191,1192],{},"build runtime, selected connector, and deployed runtime"," before changing package managers or installing another driver. Test the actual ",[83,1195,1196],{},".output"," in isolation: this site's generated server served the article without the source checkout or its ",[83,1199,1200],{},"node_modules"," directory.",[30,1203,1205],{"id":1204},"exercise-an-uncached-image-transformation","Exercise an Uncached Image Transformation",[19,1207,1208,1209,1212],{},"Startup is not sufficient when dependencies are loaded dynamically. With the installed Nuxt Image\u002FIPX combination, an earlier generated server started successfully but failed on its first image transformation: IPX loaded ",[83,1210,1211],{},"srvx\u002Fnode"," through a path Nitro had not traced. The project resolves that module from Nuxt Image's own dependency tree and adds it to the Nitro output:",[211,1214,1216],{"className":959,"code":1215,"language":961,"meta":216,"style":216},"import { createRequire } from 'node:module'\n\nconst requireFromImage = createRequire(\n  import.meta.resolve('@nuxt\u002Fimage\u002Fpackage.json')\n)\nconst requireFromIpx = createRequire(requireFromImage.resolve('ipx'))\n\nexport default defineNuxtConfig({\n  nitro: {\n    preset: 'bun',\n    externals: {\n      traceInclude: [requireFromIpx.resolve('srvx\u002Fnode')],\n    },\n  },\n})\n",[83,1217,1218,1232,1237,1254,1277,1281,1305,1309,1319,1323,1331,1336,1352,1357,1362],{"__ignoreMap":216},[220,1219,1220,1223,1226,1229],{"class":222,"line":223},[220,1221,1222],{"class":299},"import",[220,1224,1225],{"class":230}," { createRequire } ",[220,1227,1228],{"class":299},"from",[220,1230,1231],{"class":866}," 'node:module'\n",[220,1233,1234],{"class":222,"line":234},[220,1235,1236],{"emptyLinePlaceholder":796},"\n",[220,1238,1239,1242,1245,1248,1251],{"class":222,"line":250},[220,1240,1241],{"class":299},"const",[220,1243,1244],{"class":237}," requireFromImage",[220,1246,1247],{"class":299}," =",[220,1249,1250],{"class":303}," createRequire",[220,1252,1253],{"class":230},"(\n",[220,1255,1256,1259,1261,1264,1266,1269,1271,1274],{"class":222,"line":361},[220,1257,1258],{"class":299},"  import",[220,1260,264],{"class":230},[220,1262,1263],{"class":237},"meta",[220,1265,264],{"class":230},[220,1267,1268],{"class":303},"resolve",[220,1270,307],{"class":230},[220,1272,1273],{"class":866},"'@nuxt\u002Fimage\u002Fpackage.json'",[220,1275,1276],{"class":230},")\n",[220,1278,1279],{"class":222,"line":377},[220,1280,1276],{"class":230},[220,1282,1283,1285,1288,1290,1292,1295,1297,1299,1302],{"class":222,"line":431},[220,1284,1241],{"class":299},[220,1286,1287],{"class":237}," requireFromIpx",[220,1289,1247],{"class":299},[220,1291,1250],{"class":303},[220,1293,1294],{"class":230},"(requireFromImage.",[220,1296,1268],{"class":303},[220,1298,307],{"class":230},[220,1300,1301],{"class":866},"'ipx'",[220,1303,1304],{"class":230},"))\n",[220,1306,1307],{"class":222,"line":437},[220,1308,1236],{"emptyLinePlaceholder":796},[220,1310,1311,1313,1315,1317],{"class":222,"line":919},[220,1312,968],{"class":299},[220,1314,971],{"class":299},[220,1316,974],{"class":303},[220,1318,977],{"class":230},[220,1320,1321],{"class":222,"line":1170},[220,1322,982],{"class":230},[220,1324,1325,1327,1329],{"class":222,"line":1179},[220,1326,987],{"class":230},[220,1328,990],{"class":866},[220,1330,870],{"class":230},[220,1332,1333],{"class":222,"line":1184},[220,1334,1335],{"class":230},"    externals: {\n",[220,1337,1339,1342,1344,1346,1349],{"class":222,"line":1338},12,[220,1340,1341],{"class":230},"      traceInclude: [requireFromIpx.",[220,1343,1268],{"class":303},[220,1345,307],{"class":230},[220,1347,1348],{"class":866},"'srvx\u002Fnode'",[220,1350,1351],{"class":230},")],\n",[220,1353,1355],{"class":222,"line":1354},13,[220,1356,1159],{"class":230},[220,1358,1360],{"class":222,"line":1359},14,[220,1361,997],{"class":230},[220,1363,1365],{"class":222,"line":1364},15,[220,1366,1002],{"class":230},[19,1368,1369,1370,1373,1374,1376],{},"This is a ",[78,1371,1372],{},"dependency-specific packaging fix",", not recommended boilerplate for every Nuxt project. Check whether your resolved IPX release still needs it. Request an image variant not present in prerendered output; an existing cached image cannot exercise the lazy import. In the current Linux ARM64 build, an isolated ",[83,1375,1196],{}," served a new IPX width with HTTP 200. Build on the deployment architecture when bundling Sharp: a locally bundled ARM64 binary is not a portable x86-64 artifact.",[30,1378,1380],{"id":1379},"keep-compiler-versions-in-the-compatibility-set","Keep Compiler Versions in the Compatibility Set",[19,1382,1383,1384,1387,1388,1391,1392,1395,1396,1398],{},"A runtime update is not a reason to assume the newest compiler is supported by the rest of the toolchain. With ",[83,1385,1386],{},"vue-tsc"," 3.3.12, moving this project from TypeScript 6.0.3 to 7.0.2 made type checking fail with ",[83,1389,1390],{},"Cannot find module 'typescript\u002Flib\u002Ftsc'",". Returning to the pinned ",[83,1393,1394],{},"~6.0.3"," passed type checking. Revisit the pin when ",[83,1397,1386],{}," and Nuxt support the newer compiler; do not present TypeScript 7 as a Bun runtime failure.",[30,1400,1402],{"id":1401},"validate-the-deployment-artifact","Validate the Deployment Artifact",[19,1404,1405,1406,1409,1410,1413,1414,1417],{},"Coolify deploys ",[83,1407,1408],{},"main"," with ",[83,1411,1412],{},"nixpacks.toml",". It installs the pinned Bun 1.4.2 into ",[83,1415,1416],{},"\u002Fopt\u002Fbun",", then uses the same executable for the lockfile install, regression tests, type check, build, and server entry point:",[211,1419,1421],{"className":1024,"code":1420,"language":1026,"meta":216,"style":216},"bun install --frozen-lockfile\nbun run test\nbun run typecheck\nbun run build\n\u002Fopt\u002Fbun\u002Fbin\u002Fbun .output\u002Fserver\u002Findex.mjs\n",[83,1422,1423,1433,1441,1449,1457],{"__ignoreMap":216},[220,1424,1425,1427,1430],{"class":222,"line":223},[220,1426,1033],{"class":303},[220,1428,1429],{"class":866}," install",[220,1431,1432],{"class":237}," --frozen-lockfile\n",[220,1434,1435,1437,1439],{"class":222,"line":234},[220,1436,1033],{"class":303},[220,1438,1036],{"class":866},[220,1440,1039],{"class":866},[220,1442,1443,1445,1447],{"class":222,"line":250},[220,1444,1033],{"class":303},[220,1446,1036],{"class":866},[220,1448,1048],{"class":866},[220,1450,1451,1453,1455],{"class":222,"line":361},[220,1452,1033],{"class":303},[220,1454,1036],{"class":866},[220,1456,1057],{"class":866},[220,1458,1459,1462],{"class":222,"line":377},[220,1460,1461],{"class":303},"\u002Fopt\u002Fbun\u002Fbin\u002Fbun",[220,1463,1082],{"class":866},[19,1465,1466,1467,1470,1471,1473],{},"The most useful additional check is to copy ",[78,1468,1469],{},"only"," ",[83,1472,1196],{}," into a separate directory and run the entry point there. Probe a published page, a missing route, and a previously ungenerated IPX variant. For this build, those requests returned 200, 404, and 200 respectively. This tests packaging and basic responses, not the delivery of a valid contact submission or every interactive route. Server-side image support must also be rechecked on the actual deployment CPU architecture; successful compilation on a different machine is not enough.",[19,1475,1476],{},"There is no controlled Bun-versus-Node performance benchmark behind this setup. Measure install time, build time, memory, and application latency on the same workload and hardware before attributing an improvement to the runtime.",[773,1478,1479],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":216,"searchDepth":234,"depth":234,"links":1481},[1482,1483,1484,1485,1486,1487],{"id":827,"depth":234,"text":828},{"id":952,"depth":234,"text":953},{"id":1088,"depth":234,"text":1089},{"id":1204,"depth":234,"text":1205},{"id":1379,"depth":234,"text":1380},{"id":1401,"depth":234,"text":1402},"2026-01-15","A production-tested Bun and Nuxt 4 setup: explicit runtime selection, Content SQLite, Nitro dependency tracing, and checks beyond a successful build.","Deploying Nuxt 4 on Bun","Bun + Nuxt 4",{},"\u002Fblog\u002Fbun-nuxt4-runtime-guide",{"title":812,"description":1489},"blog\u002Fbun-nuxt4-runtime-guide",[1033,1497,1498,1499,1500],"nuxt","runtime","performance","node","2026-10-04","3mHbic9qWBrSkTgo_f5oLSJqT8gYx6yDh5MzO3Vw4NI",1793219229477]