[{"data":1,"prerenderedAt":1678},["ShallowReactive",2],{"astro-vs-nuxt-en":3},{"id":4,"title":5,"body":6,"description":1664,"extension":1665,"meta":1666,"navigation":245,"path":1669,"rawbody":1670,"seo":1671,"stem":1672,"__hash__":1673,"createdAt":1674,"updatedAt":1677},"content_en/blog/astro-vs-nuxt/en.md","Astro vs. Nuxt: Which one should you use for your blog?",{"type":7,"value":8,"toc":1652},"minimark",[9,21,42,45,50,53,56,67,71,74,84,87,92,95,105,108,119,122,126,137,270,277,283,543,546,553,557,560,563,570,629,640,795,805,1033,1036,1054,1058,1061,1067,1237,1243,1512,1515,1521,1527,1530,1534,1557,1560,1573,1580,1583,1587,1590,1606,1620,1632,1635,1639,1642,1645,1648],[10,11,12,13,20],"p",{},"Hello! Over the past few days, I took a look at ",[14,15,19],"a",{"href":16,"rel":17},"https://astro.build/",[18],"nofollow","Astro",", a website framework whose main focus is content; that is, it was built with articles, documentation, and blogs in mind.",[10,22,23,24,29,30,35,36,41],{},"Its biggest benefit is achieved through minimal Javascript. This really caught my attention. I mentioned ",[14,25,28],{"href":26,"rel":27},"https://blog.marciosobel.dev/my-first-blog-post/",[18],"in my first post"," that I had thought a lot about using Astro or ",[14,31,34],{"href":32,"rel":33},"https://gohugo.io/",[18],"Hugo"," for my blog, but I ended up choosing ",[14,37,40],{"href":38,"rel":39},"https://nuxt.com/",[18],"Nuxt"," because it is a framework made for building websites from scratch, which would give me more flexibility to shape the site to fit my style.",[10,43,44],{},"However, I decided to test Astro and see what it would be like for me. Who knows, maybe I'll use it instead of Nuxt on this blog!",[46,47,49],"h2",{"id":48},"why-astro","Why Astro?",[10,51,52],{},"Right off the bat, what caught my attention the most about Astro was its focus on static content. This means it wasn't made to have a lot of interactivity: buttons, requests, forms... It was made for things that change very little, like documentation, articles, landing pages, and blogs.",[10,54,55],{},"It has first-level support for Markdown, the language I use to write my posts. Also, it avoids sending Javascript code to the user as much as possible; this means the sites are always super lightweight.",[10,57,58,59,62,63,66],{},"Well, I decided to test Astro because I had a nagging doubt regarding ",[60,61,40],"code",{},": it carries ",[60,64,65],{},"Vue","'s Javascript runtime along with it. Even though it's not that big of a deal, I wanted my blog to be as simple and lightweight as possible. And so my journey began!",[46,68,70],{"id":69},"recreating-my-blog-with-astro","Recreating my blog with Astro",[10,72,73],{},"Let's go, first let's see what I want to achieve here.",[75,76,77,81],"ul",{},[78,79,80],"li",{},"Write my posts in Markdown and not worry about how they end up on the site; I just want to write, upload to the cloud, and Astro's build should update the listing and generate the links accordingly;",[78,82,83],{},"Have the site in two or more languages.",[10,85,86],{},"Yeah, the blog is pretty simple; but, I believe that with these requirements, it will be enough to get my hands dirty with the framework.",[88,89,91],"h3",{"id":90},"starting-the-project","Starting the project",[10,93,94],{},"This is the easiest and most straightforward part of this whole journey. I only needed to run:",[96,97,102],"pre",{"className":98,"code":100,"language":101},[99],"language-text","pnpm create astro@latest\n","text",[60,103,100],{"__ignoreMap":104},"",[10,106,107],{},"And follow the step-by-step instructions from the cute little robot.",[10,109,110,111,114,115,118],{},"The architecture is very simple, file-based routing... Everything is super intuitive. I didn't use any template, so for me, there was only an ",[60,112,113],{},"index.astro",", with a completely blank screen, just with an ",[60,116,117],{},"h1"," reading \"Astro\".",[10,120,121],{},"Well, let's start checking some items off the list.",[88,123,125],{"id":124},"listing-all-posts","Listing all posts",[10,127,128,129,132,133,136],{},"I started by creating a ",[60,130,131],{},"posts/"," folder and put the blog posts in it. Since Astro was already made with handling posts in mind, it was very easy. You just need to create a ",[60,134,135],{},"content.config.ts"," file with the following code:",[96,138,142],{"className":139,"code":140,"language":141,"meta":104,"style":104},"language-typescript shiki shiki-themes gruvbox-light-soft gruvbox-dark-soft","const blog = defineCollection({\n  loader: glob({ base: \"posts\", pattern: \"**/*.{md,mdx}\" }),\n});\n\nexport const collections = { blog };\n","typescript",[60,143,144,173,229,240,247],{"__ignoreMap":104},[145,146,149,153,157,161,165,169],"span",{"class":147,"line":148},"line",1,[145,150,152],{"class":151},"s5BFC","const",[145,154,156],{"class":155},"selZM"," blog",[145,158,160],{"class":159},"sJcFL"," =",[145,162,164],{"class":163},"s9LfA"," defineCollection",[145,166,168],{"class":167},"s7kJP","(",[145,170,172],{"class":171},"sLjwx","{\n",[145,174,176,179,182,185,187,190,193,195,198,202,205,208,211,213,215,218,220,223,226],{"class":147,"line":175},2,[145,177,178],{"class":167},"  loader",[145,180,181],{"class":171},":",[145,183,184],{"class":163}," glob",[145,186,168],{"class":167},[145,188,189],{"class":171},"{",[145,191,192],{"class":167}," base",[145,194,181],{"class":171},[145,196,197],{"class":171}," \"",[145,199,201],{"class":200},"sg8ZQ","posts",[145,203,204],{"class":171},"\"",[145,206,207],{"class":171},",",[145,209,210],{"class":167}," pattern",[145,212,181],{"class":171},[145,214,197],{"class":171},[145,216,217],{"class":200},"**/*.{md,mdx}",[145,219,204],{"class":171},[145,221,222],{"class":171}," }",[145,224,225],{"class":167},")",[145,227,228],{"class":171},",\n",[145,230,232,235,237],{"class":147,"line":231},3,[145,233,234],{"class":171},"}",[145,236,225],{"class":167},[145,238,239],{"class":171},";\n",[145,241,243],{"class":147,"line":242},4,[145,244,246],{"emptyLinePlaceholder":245},true,"\n",[145,248,250,254,257,260,262,265,267],{"class":147,"line":249},5,[145,251,253],{"class":252},"sn-iv","export",[145,255,256],{"class":151}," const",[145,258,259],{"class":155}," collections",[145,261,160],{"class":159},[145,263,264],{"class":171}," {",[145,266,156],{"class":155},[145,268,269],{"class":171}," };\n",[10,271,272,273,276],{},"That was very easy. I used ",[60,274,275],{},"zod"," to create a schema for the frontmatter, the metadata, of the posts.",[10,278,279,280,282],{},"Then, in the ",[60,281,113],{}," file, the website's entrypoint, I placed the following:",[96,284,288],{"className":285,"code":286,"language":287,"meta":104,"style":104},"language-astro shiki shiki-themes gruvbox-light-soft gruvbox-dark-soft","---\nimport { getCollection } from \"astro:content\";\nconst posts = await getCollection(\"blog\");\nif (!posts) return Astro.redirect(\"/404\");\n---\n\n\u003Ch1>Blog\u003C/h1>\n\u003Cul>\n  {posts.map((post) => (\n    \u003Cli>\n      \u003Ca href={post.id}>{post.data.title}\u003C/a>\n    \u003C/li>\n  ))}\n\u003C/ul>\n","astro",[60,289,290,296,320,347,388,392,397,419,428,456,466,515,525,534],{"__ignoreMap":104},[145,291,292],{"class":147,"line":148},[145,293,295],{"class":294},"sFEbH","---\n",[145,297,298,301,303,306,308,311,313,316,318],{"class":147,"line":175},[145,299,300],{"class":252},"import",[145,302,264],{"class":171},[145,304,305],{"class":155}," getCollection",[145,307,222],{"class":171},[145,309,310],{"class":252}," from",[145,312,197],{"class":171},[145,314,315],{"class":200},"astro:content",[145,317,204],{"class":171},[145,319,239],{"class":171},[145,321,322,324,327,329,332,334,336,338,341,343,345],{"class":147,"line":231},[145,323,152],{"class":151},[145,325,326],{"class":155}," posts",[145,328,160],{"class":159},[145,330,331],{"class":252}," await",[145,333,305],{"class":163},[145,335,168],{"class":167},[145,337,204],{"class":171},[145,339,340],{"class":200},"blog",[145,342,204],{"class":171},[145,344,225],{"class":167},[145,346,239],{"class":171},[145,348,349,352,355,358,360,363,366,369,372,375,377,379,382,384,386],{"class":147,"line":242},[145,350,351],{"class":252},"if",[145,353,354],{"class":167}," (",[145,356,357],{"class":252},"!",[145,359,201],{"class":155},[145,361,362],{"class":167},") ",[145,364,365],{"class":252},"return",[145,367,368],{"class":155}," Astro",[145,370,371],{"class":171},".",[145,373,374],{"class":163},"redirect",[145,376,168],{"class":167},[145,378,204],{"class":171},[145,380,381],{"class":200},"/404",[145,383,204],{"class":171},[145,385,225],{"class":167},[145,387,239],{"class":171},[145,389,390],{"class":147,"line":249},[145,391,295],{"class":294},[145,393,395],{"class":147,"line":394},6,[145,396,246],{"emptyLinePlaceholder":245},[145,398,400,403,405,408,411,414,416],{"class":147,"line":399},7,[145,401,402],{"class":155},"\u003C",[145,404,117],{"class":159},[145,406,407],{"class":155},">",[145,409,410],{"class":167},"Blog",[145,412,413],{"class":155},"\u003C/",[145,415,117],{"class":159},[145,417,418],{"class":155},">\n",[145,420,422,424,426],{"class":147,"line":421},8,[145,423,402],{"class":155},[145,425,75],{"class":159},[145,427,418],{"class":155},[145,429,431,434,436,438,441,443,445,448,450,453],{"class":147,"line":430},9,[145,432,433],{"class":171},"  {",[145,435,201],{"class":155},[145,437,371],{"class":171},[145,439,440],{"class":163},"map",[145,442,168],{"class":167},[145,444,168],{"class":171},[145,446,447],{"class":155},"post",[145,449,225],{"class":171},[145,451,452],{"class":151}," =>",[145,454,455],{"class":167}," (\n",[145,457,459,462,464],{"class":147,"line":458},10,[145,460,461],{"class":155},"    \u003C",[145,463,78],{"class":159},[145,465,418],{"class":155},[145,467,469,472,474,477,480,482,484,486,489,491,493,495,497,499,502,504,507,509,511,513],{"class":147,"line":468},11,[145,470,471],{"class":155},"      \u003C",[145,473,14],{"class":159},[145,475,476],{"class":163}," href",[145,478,479],{"class":159},"=",[145,481,189],{"class":171},[145,483,447],{"class":155},[145,485,371],{"class":171},[145,487,488],{"class":155},"id",[145,490,234],{"class":171},[145,492,407],{"class":155},[145,494,189],{"class":171},[145,496,447],{"class":155},[145,498,371],{"class":171},[145,500,501],{"class":155},"data",[145,503,371],{"class":171},[145,505,506],{"class":155},"title",[145,508,234],{"class":171},[145,510,413],{"class":155},[145,512,14],{"class":159},[145,514,418],{"class":155},[145,516,518,521,523],{"class":147,"line":517},12,[145,519,520],{"class":155},"    \u003C/",[145,522,78],{"class":159},[145,524,418],{"class":155},[145,526,528,531],{"class":147,"line":527},13,[145,529,530],{"class":167},"  ))",[145,532,533],{"class":171},"}\n",[145,535,537,539,541],{"class":147,"line":536},14,[145,538,413],{"class":155},[145,540,75],{"class":159},[145,542,418],{"class":155},[10,544,545],{},"Andddd that worked? That's too easy!",[10,547,548,549,552],{},"In Astro, any code executed inside the \"fences\" (",[60,550,551],{},"---",") runs only at build time. This means our application so far has a grand total of zero Javascript!",[88,554,556],{"id":555},"generating-pages-dynamically","Generating pages dynamically",[10,558,559],{},"After creating the post listing page, I wanted to create a page for each post. For this, I needed a dynamic route, meaning I would need Astro to generate the pages for me.",[10,561,562],{},"I would like to take this moment to say that Astro's documentation is very good! It would definitely be a lot more work to do this without it.",[10,564,565,566,569],{},"Anyway, I created a ",[60,567,568],{},"[slug].astro"," file. This file accepts any value as input, and I can use this value via code.",[96,571,573],{"className":285,"code":572,"language":287,"meta":104,"style":104},"---\nconst { slug } = Astro.params;\n---\n\u003Cp>Slug: {slug}\u003C/p>\n",[60,574,575,579,601,605],{"__ignoreMap":104},[145,576,577],{"class":147,"line":148},[145,578,295],{"class":294},[145,580,581,583,585,588,590,592,594,596,599],{"class":147,"line":175},[145,582,152],{"class":151},[145,584,264],{"class":171},[145,586,587],{"class":155}," slug",[145,589,222],{"class":171},[145,591,160],{"class":159},[145,593,368],{"class":155},[145,595,371],{"class":171},[145,597,598],{"class":155},"params",[145,600,239],{"class":171},[145,602,603],{"class":147,"line":231},[145,604,295],{"class":294},[145,606,607,609,611,613,616,618,621,623,625,627],{"class":147,"line":242},[145,608,402],{"class":155},[145,610,10],{"class":159},[145,612,407],{"class":155},[145,614,615],{"class":167},"Slug: ",[145,617,189],{"class":171},[145,619,620],{"class":155},"slug",[145,622,234],{"class":171},[145,624,413],{"class":155},[145,626,10],{"class":159},[145,628,418],{"class":155},[10,630,631,632,635,636,639],{},"Then I used this to get a post's ID. Fortunately, Astro already has a specific function to get an item from a collection: ",[60,633,634],{},"getEntry",". It receives the collection and the ID to search for. After that, I needed to know how to show the Markdown content in HTML. Then I stumbled upon the ",[60,637,638],{},"render"," function, how useful!",[96,641,643],{"className":285,"code":642,"language":287,"meta":104,"style":104},"---\nimport { getEntry, render } from \"astro:content\";\nconst { slug } = Astro.params;\nconst post = await getEntry(\"blog\", slug);\nif (!post) return Astro.redirect(\"/404\");\nconst { Content } = await render(post);\n---\n\u003CContent />\n",[60,644,645,649,675,695,724,756,781,785],{"__ignoreMap":104},[145,646,647],{"class":147,"line":148},[145,648,295],{"class":294},[145,650,651,653,655,658,660,663,665,667,669,671,673],{"class":147,"line":175},[145,652,300],{"class":252},[145,654,264],{"class":171},[145,656,657],{"class":155}," getEntry",[145,659,207],{"class":171},[145,661,662],{"class":155}," render",[145,664,222],{"class":171},[145,666,310],{"class":252},[145,668,197],{"class":171},[145,670,315],{"class":200},[145,672,204],{"class":171},[145,674,239],{"class":171},[145,676,677,679,681,683,685,687,689,691,693],{"class":147,"line":231},[145,678,152],{"class":151},[145,680,264],{"class":171},[145,682,587],{"class":155},[145,684,222],{"class":171},[145,686,160],{"class":159},[145,688,368],{"class":155},[145,690,371],{"class":171},[145,692,598],{"class":155},[145,694,239],{"class":171},[145,696,697,699,702,704,706,708,710,712,714,716,718,720,722],{"class":147,"line":242},[145,698,152],{"class":151},[145,700,701],{"class":155}," post",[145,703,160],{"class":159},[145,705,331],{"class":252},[145,707,657],{"class":163},[145,709,168],{"class":167},[145,711,204],{"class":171},[145,713,340],{"class":200},[145,715,204],{"class":171},[145,717,207],{"class":171},[145,719,587],{"class":155},[145,721,225],{"class":167},[145,723,239],{"class":171},[145,725,726,728,730,732,734,736,738,740,742,744,746,748,750,752,754],{"class":147,"line":249},[145,727,351],{"class":252},[145,729,354],{"class":167},[145,731,357],{"class":252},[145,733,447],{"class":155},[145,735,362],{"class":167},[145,737,365],{"class":252},[145,739,368],{"class":155},[145,741,371],{"class":171},[145,743,374],{"class":163},[145,745,168],{"class":167},[145,747,204],{"class":171},[145,749,381],{"class":200},[145,751,204],{"class":171},[145,753,225],{"class":167},[145,755,239],{"class":171},[145,757,758,760,762,765,767,769,771,773,775,777,779],{"class":147,"line":394},[145,759,152],{"class":151},[145,761,264],{"class":171},[145,763,764],{"class":155}," Content",[145,766,222],{"class":171},[145,768,160],{"class":159},[145,770,331],{"class":252},[145,772,662],{"class":163},[145,774,168],{"class":167},[145,776,447],{"class":155},[145,778,225],{"class":167},[145,780,239],{"class":171},[145,782,783],{"class":147,"line":399},[145,784,295],{"class":294},[145,786,787,789,792],{"class":147,"line":421},[145,788,402],{"class":155},[145,790,791],{"class":167},"Content ",[145,793,794],{"class":155},"/>\n",[10,796,797,798,800,801,804],{},"And in theory, it was supposed to work super well... But Astro cannot know all the possible routes for ",[60,799,568],{},", which prevented it from keeping the site clean of Javascript. To circumvent this, we have to tell Astro all the possible routes. We do this by exporting a ",[60,802,803],{},"getStaticPaths"," function, which fortunately can be async, where we return a list with all the completed variables. We can also pass props to each page, which means we can pass all the posts directly!",[96,806,808],{"className":285,"code":807,"language":287,"meta":104,"style":104},"---\nimport { getCollection, render } from \"astro:content\"; // we now fetch the whole collection!\n\nexport async function getStaticPaths() {\n  const posts = await getCollection(\"blog\");\n  return posts.map((post) => ({\n    params: { slug: post.id },\n    props: { post }, // we can pass the post directly to the page!\n  }));\n}\n\nconst { post } = Astro.props; // and the post is guaranteed to exist!\nconst { Content } = await render(post);\n---\n\u003CContent />\n",[60,809,810,814,842,846,865,890,915,937,954,964,968,972,996,1020,1024],{"__ignoreMap":104},[145,811,812],{"class":147,"line":148},[145,813,295],{"class":294},[145,815,816,818,820,822,824,826,828,830,832,834,836,839],{"class":147,"line":175},[145,817,300],{"class":252},[145,819,264],{"class":171},[145,821,305],{"class":155},[145,823,207],{"class":171},[145,825,662],{"class":155},[145,827,222],{"class":171},[145,829,310],{"class":252},[145,831,197],{"class":171},[145,833,315],{"class":200},[145,835,204],{"class":171},[145,837,838],{"class":171},";",[145,840,841],{"class":294}," // we now fetch the whole collection!\n",[145,843,844],{"class":147,"line":231},[145,845,246],{"emptyLinePlaceholder":245},[145,847,848,850,853,856,859,862],{"class":147,"line":242},[145,849,253],{"class":252},[145,851,852],{"class":151}," async",[145,854,855],{"class":151}," function",[145,857,858],{"class":163}," getStaticPaths",[145,860,861],{"class":171},"()",[145,863,864],{"class":171}," {\n",[145,866,867,870,872,874,876,878,880,882,884,886,888],{"class":147,"line":249},[145,868,869],{"class":151},"  const",[145,871,326],{"class":155},[145,873,160],{"class":159},[145,875,331],{"class":252},[145,877,305],{"class":163},[145,879,168],{"class":167},[145,881,204],{"class":171},[145,883,340],{"class":200},[145,885,204],{"class":171},[145,887,225],{"class":167},[145,889,239],{"class":171},[145,891,892,895,897,899,901,903,905,907,909,911,913],{"class":147,"line":394},[145,893,894],{"class":252},"  return",[145,896,326],{"class":155},[145,898,371],{"class":171},[145,900,440],{"class":163},[145,902,168],{"class":167},[145,904,168],{"class":171},[145,906,447],{"class":155},[145,908,225],{"class":171},[145,910,452],{"class":151},[145,912,354],{"class":167},[145,914,172],{"class":171},[145,916,917,920,922,924,926,928,930,932,934],{"class":147,"line":399},[145,918,919],{"class":167},"    params",[145,921,181],{"class":171},[145,923,264],{"class":171},[145,925,587],{"class":167},[145,927,181],{"class":171},[145,929,701],{"class":155},[145,931,371],{"class":171},[145,933,488],{"class":155},[145,935,936],{"class":171}," },\n",[145,938,939,942,944,946,948,951],{"class":147,"line":421},[145,940,941],{"class":167},"    props",[145,943,181],{"class":171},[145,945,264],{"class":171},[145,947,701],{"class":155},[145,949,950],{"class":171}," },",[145,952,953],{"class":294}," // we can pass the post directly to the page!\n",[145,955,956,959,962],{"class":147,"line":430},[145,957,958],{"class":171},"  }",[145,960,961],{"class":167},"))",[145,963,239],{"class":171},[145,965,966],{"class":147,"line":458},[145,967,533],{"class":171},[145,969,970],{"class":147,"line":468},[145,971,246],{"emptyLinePlaceholder":245},[145,973,974,976,978,980,982,984,986,988,991,993],{"class":147,"line":517},[145,975,152],{"class":151},[145,977,264],{"class":171},[145,979,701],{"class":155},[145,981,222],{"class":171},[145,983,160],{"class":159},[145,985,368],{"class":155},[145,987,371],{"class":171},[145,989,990],{"class":155},"props",[145,992,838],{"class":171},[145,994,995],{"class":294}," // and the post is guaranteed to exist!\n",[145,997,998,1000,1002,1004,1006,1008,1010,1012,1014,1016,1018],{"class":147,"line":527},[145,999,152],{"class":151},[145,1001,264],{"class":171},[145,1003,764],{"class":155},[145,1005,222],{"class":171},[145,1007,160],{"class":159},[145,1009,331],{"class":252},[145,1011,662],{"class":163},[145,1013,168],{"class":167},[145,1015,447],{"class":155},[145,1017,225],{"class":167},[145,1019,239],{"class":171},[145,1021,1022],{"class":147,"line":536},[145,1023,295],{"class":294},[145,1025,1027,1029,1031],{"class":147,"line":1026},15,[145,1028,402],{"class":155},[145,1030,791],{"class":167},[145,1032,794],{"class":155},[10,1034,1035],{},"Okay! That was pretty easy.",[10,1037,1038,1039,1043,1044,1047,1048,1053],{},"Testing the different posts, I noticed that one of them was rendering accents incorrectly. For example, instead of the word ",[1040,1041,1042],"em",{},"água"," (which means \"water\"), the rendered HTML returned ",[1040,1045,1046],{},"Ã¡gua",". I was confused about the reason, and apparently Astro no longer defines the document's charset as UTF-8 by default. I was quite confused because ",[14,1049,1052],{"href":1050,"rel":1051},"https://docs.astro.build/en/guides/markdown-content/#frontmatter-layout-property",[18],"I only saw this mentioned in one place in the docs","? But that's fine, I lost a bit of time on this part.",[88,1055,1057],{"id":1056},"dealing-with-different-languages","Dealing with different languages",[10,1059,1060],{},"In case you've never noticed, this blog is available in both Brazilian Portuguese and English. As of the writing of this post, all posts are available in both languages. I don't know if at some point they will diverge and have different content.",[10,1062,1063,1064,1066],{},"Anyway, having multiple languages is necessary. Like the good programmer that I am™, I wanted to keep it dynamic enough to have the least amount of headache possible if I want to write in some other language. First, I changed the ",[60,1065,135],{}," file to have the posts separated:",[96,1068,1070],{"className":139,"code":1069,"language":141,"meta":104,"style":104},"function createCollection(suffix: string) {\n  return defineCollection({\n    loader: glob({ base: `posts/${suffix}`, pattern: \"**/*.{md,mdx}\" });\n  });\n}\n\nexport const collections = {\n  \"blog-en\": createCollection(\"en\"),\n  \"blog-pt\": createCollection(\"pt\"),\n};\n",[60,1071,1072,1094,1104,1151,1159,1163,1167,1179,1206,1232],{"__ignoreMap":104},[145,1073,1074,1077,1080,1082,1085,1087,1090,1092],{"class":147,"line":148},[145,1075,1076],{"class":151},"function",[145,1078,1079],{"class":163}," createCollection",[145,1081,168],{"class":171},[145,1083,1084],{"class":155},"suffix",[145,1086,181],{"class":159},[145,1088,1089],{"class":163}," string",[145,1091,225],{"class":171},[145,1093,864],{"class":171},[145,1095,1096,1098,1100,1102],{"class":147,"line":175},[145,1097,894],{"class":252},[145,1099,164],{"class":163},[145,1101,168],{"class":167},[145,1103,172],{"class":171},[145,1105,1106,1109,1111,1113,1115,1117,1119,1121,1124,1126,1129,1131,1134,1136,1138,1140,1142,1144,1146,1148],{"class":147,"line":231},[145,1107,1108],{"class":167},"    loader",[145,1110,181],{"class":171},[145,1112,184],{"class":163},[145,1114,168],{"class":167},[145,1116,189],{"class":171},[145,1118,192],{"class":167},[145,1120,181],{"class":171},[145,1122,1123],{"class":171}," `",[145,1125,131],{"class":200},[145,1127,1128],{"class":171},"${",[145,1130,1084],{"class":155},[145,1132,1133],{"class":171},"}`",[145,1135,207],{"class":171},[145,1137,210],{"class":167},[145,1139,181],{"class":171},[145,1141,197],{"class":171},[145,1143,217],{"class":200},[145,1145,204],{"class":171},[145,1147,222],{"class":171},[145,1149,1150],{"class":167},");\n",[145,1152,1153,1155,1157],{"class":147,"line":242},[145,1154,958],{"class":171},[145,1156,225],{"class":167},[145,1158,239],{"class":171},[145,1160,1161],{"class":147,"line":249},[145,1162,533],{"class":171},[145,1164,1165],{"class":147,"line":394},[145,1166,246],{"emptyLinePlaceholder":245},[145,1168,1169,1171,1173,1175,1177],{"class":147,"line":399},[145,1170,253],{"class":252},[145,1172,256],{"class":151},[145,1174,259],{"class":155},[145,1176,160],{"class":159},[145,1178,864],{"class":171},[145,1180,1181,1184,1187,1189,1191,1193,1195,1197,1200,1202,1204],{"class":147,"line":421},[145,1182,1183],{"class":171},"  \"",[145,1185,1186],{"class":200},"blog-en",[145,1188,204],{"class":171},[145,1190,181],{"class":171},[145,1192,1079],{"class":163},[145,1194,168],{"class":167},[145,1196,204],{"class":171},[145,1198,1199],{"class":200},"en",[145,1201,204],{"class":171},[145,1203,225],{"class":167},[145,1205,228],{"class":171},[145,1207,1208,1210,1213,1215,1217,1219,1221,1223,1226,1228,1230],{"class":147,"line":430},[145,1209,1183],{"class":171},[145,1211,1212],{"class":200},"blog-pt",[145,1214,204],{"class":171},[145,1216,181],{"class":171},[145,1218,1079],{"class":163},[145,1220,168],{"class":167},[145,1222,204],{"class":171},[145,1224,1225],{"class":200},"pt",[145,1227,204],{"class":171},[145,1229,225],{"class":167},[145,1231,228],{"class":171},[145,1233,1234],{"class":147,"line":458},[145,1235,1236],{"class":171},"};\n",[10,1238,1239,1240,136],{},"And then I created an ",[60,1241,1242],{},"i18n.ts",[96,1244,1246],{"className":139,"code":1245,"language":141,"meta":104,"style":104},"const entries = {\n  en: {\n    \"all-posts\": \"All posts\",\n  },\n  pt: {\n    \"all-posts\": \"Todos os posts\",\n  },\n};\n\nexport type Locales = keyof typeof entries;\nexport const locales = Object.keys(entries) as Locale[];\nexport const defaultLocale: Locale = \"en\" as const;\n\nimport { getCollection as getContentCollection } from \"astro:content\";\n\nexport function getCollection(locale: Locale = defaultLocale) {\n    return getContentCollection(`blog-${locale}`);\n}\n",[60,1247,1248,1259,1268,1289,1294,1303,1322,1326,1330,1334,1356,1393,1422,1426,1451,1455,1481,1507],{"__ignoreMap":104},[145,1249,1250,1252,1255,1257],{"class":147,"line":148},[145,1251,152],{"class":151},[145,1253,1254],{"class":155}," entries",[145,1256,160],{"class":159},[145,1258,864],{"class":171},[145,1260,1261,1264,1266],{"class":147,"line":175},[145,1262,1263],{"class":167},"  en",[145,1265,181],{"class":171},[145,1267,864],{"class":171},[145,1269,1270,1273,1276,1278,1280,1282,1285,1287],{"class":147,"line":231},[145,1271,1272],{"class":171},"    \"",[145,1274,1275],{"class":200},"all-posts",[145,1277,204],{"class":171},[145,1279,181],{"class":171},[145,1281,197],{"class":171},[145,1283,1284],{"class":200},"All posts",[145,1286,204],{"class":171},[145,1288,228],{"class":171},[145,1290,1291],{"class":147,"line":242},[145,1292,1293],{"class":171},"  },\n",[145,1295,1296,1299,1301],{"class":147,"line":249},[145,1297,1298],{"class":167},"  pt",[145,1300,181],{"class":171},[145,1302,864],{"class":171},[145,1304,1305,1307,1309,1311,1313,1315,1318,1320],{"class":147,"line":394},[145,1306,1272],{"class":171},[145,1308,1275],{"class":200},[145,1310,204],{"class":171},[145,1312,181],{"class":171},[145,1314,197],{"class":171},[145,1316,1317],{"class":200},"Todos os posts",[145,1319,204],{"class":171},[145,1321,228],{"class":171},[145,1323,1324],{"class":147,"line":399},[145,1325,1293],{"class":171},[145,1327,1328],{"class":147,"line":421},[145,1329,1236],{"class":171},[145,1331,1332],{"class":147,"line":430},[145,1333,246],{"emptyLinePlaceholder":245},[145,1335,1336,1338,1341,1344,1346,1349,1352,1354],{"class":147,"line":458},[145,1337,253],{"class":252},[145,1339,1340],{"class":151}," type",[145,1342,1343],{"class":163}," Locales",[145,1345,160],{"class":159},[145,1347,1348],{"class":159}," keyof",[145,1350,1351],{"class":159}," typeof",[145,1353,1254],{"class":155},[145,1355,239],{"class":171},[145,1357,1358,1360,1362,1365,1367,1370,1372,1375,1377,1380,1382,1385,1388,1391],{"class":147,"line":468},[145,1359,253],{"class":252},[145,1361,256],{"class":151},[145,1363,1364],{"class":155}," locales",[145,1366,160],{"class":159},[145,1368,1369],{"class":155}," Object",[145,1371,371],{"class":171},[145,1373,1374],{"class":163},"keys",[145,1376,168],{"class":167},[145,1378,1379],{"class":155},"entries",[145,1381,362],{"class":167},[145,1383,1384],{"class":252},"as",[145,1386,1387],{"class":163}," Locale",[145,1389,1390],{"class":167},"[]",[145,1392,239],{"class":171},[145,1394,1395,1397,1399,1402,1404,1407,1409,1411,1413,1415,1418,1420],{"class":147,"line":517},[145,1396,253],{"class":252},[145,1398,256],{"class":151},[145,1400,1401],{"class":155}," defaultLocale",[145,1403,181],{"class":159},[145,1405,1406],{"class":163}," Locale ",[145,1408,479],{"class":159},[145,1410,197],{"class":171},[145,1412,1199],{"class":200},[145,1414,204],{"class":171},[145,1416,1417],{"class":252}," as",[145,1419,256],{"class":151},[145,1421,239],{"class":171},[145,1423,1424],{"class":147,"line":527},[145,1425,246],{"emptyLinePlaceholder":245},[145,1427,1428,1430,1432,1434,1436,1439,1441,1443,1445,1447,1449],{"class":147,"line":536},[145,1429,300],{"class":252},[145,1431,264],{"class":171},[145,1433,305],{"class":155},[145,1435,1417],{"class":252},[145,1437,1438],{"class":155}," getContentCollection",[145,1440,222],{"class":171},[145,1442,310],{"class":252},[145,1444,197],{"class":171},[145,1446,315],{"class":200},[145,1448,204],{"class":171},[145,1450,239],{"class":171},[145,1452,1453],{"class":147,"line":1026},[145,1454,246],{"emptyLinePlaceholder":245},[145,1456,1458,1460,1462,1464,1466,1469,1471,1473,1475,1477,1479],{"class":147,"line":1457},16,[145,1459,253],{"class":252},[145,1461,855],{"class":151},[145,1463,305],{"class":163},[145,1465,168],{"class":171},[145,1467,1468],{"class":155},"locale",[145,1470,181],{"class":159},[145,1472,1406],{"class":163},[145,1474,479],{"class":159},[145,1476,1401],{"class":155},[145,1478,225],{"class":171},[145,1480,864],{"class":171},[145,1482,1484,1487,1489,1491,1494,1497,1499,1501,1503,1505],{"class":147,"line":1483},17,[145,1485,1486],{"class":252},"    return",[145,1488,1438],{"class":163},[145,1490,168],{"class":167},[145,1492,1493],{"class":171},"`",[145,1495,1496],{"class":200},"blog-",[145,1498,1128],{"class":171},[145,1500,1468],{"class":155},[145,1502,1133],{"class":171},[145,1504,225],{"class":167},[145,1506,239],{"class":171},[145,1508,1510],{"class":147,"line":1509},18,[145,1511,533],{"class":171},[10,1513,1514],{},"The page architecture looked like this:",[96,1516,1519],{"className":1517,"code":1518,"language":101},[99],"src/\n├── content.config.ts\n├── i18n.ts\n├── pages/\n│   ├── [lang]/\n│   │   ├── [slug].astro\n│   │   └── index.astro\n│   ├── [slug].astro\n└── └── index.astro\n",[60,1520,1518],{"__ignoreMap":104},[10,1522,1523,1524,371],{},"And everywhere I needed the selected language, I just put ",[60,1525,1526],{},"const { lang = defaultLocale } = Astro.params;",[10,1528,1529],{},"From then on, everything worked perfectly fine. I created a component that adds the language suffix to the URL, and that way I could switch languages. Everything worked perfectly and, so far, zero JavaScript!",[46,1531,1533],{"id":1532},"where-things-started-to-go-downhill","Where things started to go downhill...",[10,1535,1536,1537,1540,1541,1544,1545,1548,1549,1552,1553,1556],{},"In Astro, Markdown is rendered into HTML. The rendering is very basic: links become ",[60,1538,1539],{},"\u003Ca>",", headings become ",[60,1542,1543],{},"\u003Ch1>",", ",[60,1546,1547],{},"\u003Ch2>","... Everything normal. It turns out that I wanted a small customization: if you want to test it in this post, every title is clickable. I left it like this because, in case someone wants to share just a specific topic of the post, it is possible. To do this, I created a ",[60,1550,1551],{},"Prose"," component in Nuxt, which allows me to ",[1040,1554,1555],{},"override"," the tags generated by Markdown.",[10,1558,1559],{},"In Astro, however, this is not possible. Not in a simple way. I had two choices:",[1561,1562,1563,1570],"ol",{},[78,1564,1565,1566,1569],{},"Use ",[60,1567,1568],{},"MDX"," to write the posts instead of markdown and call the components as needed, or;",[78,1571,1572],{},"Use a rehype plugin to alter the tags at rendering time.",[10,1574,1575,1576,1579],{},"Since I'm not really a big fan of ",[60,1577,1578],{},"mdx",", I went with the second option. It turns out that this generated very ugly code, barely functional (it bugged a lot), and that I wouldn't understand a few days later. But, that's fine, I did it anyway.",[10,1581,1582],{},"And voilà, after adding the css, I had an exact replica of my blog, 100% functional.",[46,1584,1586],{"id":1585},"verdict-and-comparison","Verdict and comparison",[10,1588,1589],{},"Well, the entire blog didn't remain immune to JavaScript. It's possible to search for posts and there is a way to toggle between light and dark themes. I built the site and wanted to see how heavy it was.",[1591,1592,1593],"blockquote",{},[10,1594,1595,1596,1599,1600,1605],{},"The metrics are in the format ",[60,1597,1598],{},"\u003Ccompressed_size>/\u003Cuncompressed_size>",". The site will always be downloaded compressed and is uncompressed locally. You can access the reference post ",[14,1601,1604],{"href":1602,"rel":1603},"https://blog.marciosobel.dev/switching-to-nixos/",[18],"here",". All caches were disabled in the measurements.",[10,1607,1608,1609,1611,1612,1616,1617,371],{},"My blog, with ",[60,1610,40],{},", the listing of all posts uses ",[1613,1614,1615],"strong",{},"228 kB / 569 kB",". Accessing a post uses ",[1613,1618,1619],{},"351 kB / 657 kB",[10,1621,1622,1623,1625,1626,1616,1629,371],{},"The replica, with ",[60,1624,19],{},", on the listing screen, uses ",[1613,1627,1628],{},"164 kB / 166 kB",[1613,1630,1631],{},"257 kB / 260 kB",[10,1633,1634],{},"It is noticeable that Astro uses 28% less network than Nuxt in the listing, and 27% less when viewing a post, with its biggest difference being in the uncompressed data, possessing a difference of 71% in the listing and 60% when viewing a post!",[46,1636,1638],{"id":1637},"conclusion-which-framework-am-i-staying-with","Conclusion: which framework am I staying with?",[10,1640,1641],{},"The difference in the final bundle is certainly notable. Astro delivers a much lighter site, containing only the content you choose to deliver. But, despite this, I decided to stick with Nuxt. Here is why: I like to tweak things back and forth. I found Astro for this use case of mine to be not very flexible, especially in the part of modifying the markdown output. And, although it is much lighter, it is only in the uncompressed content; on the network side, the difference isn't that shocking. Today's phones and computers barely break a sweat to compensate for this difference. So I accept making this sacrifice in exchange for a better DX.",[10,1643,1644],{},"Nonetheless, Astro definitely won me over with its charm and simplicity. I will certainly use it to generate documentation for possible libraries of mine in the future. I didn't get to explore its full potential and, if I were starting from scratch, I would probably pick a template and build from there.",[10,1646,1647],{},"That was my review of Astro, I hope you had a good read. Until next time!",[1649,1650,1651],"style",{},"html pre.shiki code .s5BFC, html code.shiki .s5BFC{--shiki-default:#AF3A03;--shiki-dark:#FE8019}html pre.shiki code .selZM, html code.shiki .selZM{--shiki-default:#076678;--shiki-dark:#83A598}html pre.shiki code .sJcFL, html code.shiki .sJcFL{--shiki-default:#427B58;--shiki-dark:#8EC07C}html pre.shiki code .s9LfA, html code.shiki .s9LfA{--shiki-default:#B57614;--shiki-dark:#FABD2F}html pre.shiki code .s7kJP, html code.shiki .s7kJP{--shiki-default:#3C3836;--shiki-dark:#EBDBB2}html pre.shiki code .sLjwx, html code.shiki .sLjwx{--shiki-default:#7C6F64;--shiki-dark:#A89984}html pre.shiki code .sg8ZQ, html code.shiki .sg8ZQ{--shiki-default:#79740E;--shiki-dark:#B8BB26}html pre.shiki code .sn-iv, html code.shiki .sn-iv{--shiki-default:#9D0006;--shiki-dark:#FB4934}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 .sFEbH, html code.shiki .sFEbH{--shiki-default:#928374;--shiki-default-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic}",{"title":104,"searchDepth":175,"depth":175,"links":1653},[1654,1655,1661,1662,1663],{"id":48,"depth":175,"text":49},{"id":69,"depth":175,"text":70,"children":1656},[1657,1658,1659,1660],{"id":90,"depth":231,"text":91},{"id":124,"depth":231,"text":125},{"id":555,"depth":231,"text":556},{"id":1056,"depth":231,"text":1057},{"id":1532,"depth":175,"text":1533},{"id":1585,"depth":175,"text":1586},{"id":1637,"depth":175,"text":1638},"If you're looking to start a blog, you will probably come across these two very popular options. In this post, I will discuss the pros and cons of each.","md",{"date":1667,"updated":1668},"2026-06-17",null,"/blog/astro-vs-nuxt/en","---\ntitle: \"Astro vs. Nuxt: Which one should you use for your blog?\"\ndescription: If you're looking to start a blog, you will probably come across these two very popular options. In this post, I will discuss the pros and cons of each.\ndate: 2026-06-17\nupdated:\n---\nHello! Over the past few days, I took a look at [Astro](https://astro.build/), a website framework whose main focus is content; that is, it was built with articles, documentation, and blogs in mind.\n\nIts biggest benefit is achieved through minimal Javascript. This really caught my attention. I mentioned [in my first post](https://blog.marciosobel.dev/my-first-blog-post/) that I had thought a lot about using Astro or [Hugo](https://gohugo.io/) for my blog, but I ended up choosing [Nuxt](https://nuxt.com/) because it is a framework made for building websites from scratch, which would give me more flexibility to shape the site to fit my style.\n\nHowever, I decided to test Astro and see what it would be like for me. Who knows, maybe I'll use it instead of Nuxt on this blog!\n## Why Astro?\nRight off the bat, what caught my attention the most about Astro was its focus on static content. This means it wasn't made to have a lot of interactivity: buttons, requests, forms... It was made for things that change very little, like documentation, articles, landing pages, and blogs.\n\nIt has first-level support for Markdown, the language I use to write my posts. Also, it avoids sending Javascript code to the user as much as possible; this means the sites are always super lightweight.\n\nWell, I decided to test Astro because I had a nagging doubt regarding `Nuxt`: it carries `Vue`'s Javascript runtime along with it. Even though it's not that big of a deal, I wanted my blog to be as simple and lightweight as possible. And so my journey began!\n## Recreating my blog with Astro\nLet's go, first let's see what I want to achieve here.\n- Write my posts in Markdown and not worry about how they end up on the site; I just want to write, upload to the cloud, and Astro's build should update the listing and generate the links accordingly;\n- Have the site in two or more languages.\n\nYeah, the blog is pretty simple; but, I believe that with these requirements, it will be enough to get my hands dirty with the framework.\n### Starting the project\nThis is the easiest and most straightforward part of this whole journey. I only needed to run:\n```\npnpm create astro@latest\n```\nAnd follow the step-by-step instructions from the cute little robot.\n\nThe architecture is very simple, file-based routing... Everything is super intuitive. I didn't use any template, so for me, there was only an `index.astro`, with a completely blank screen, just with an `h1` reading \"Astro\".\n\nWell, let's start checking some items off the list.\n### Listing all posts\nI started by creating a `posts/` folder and put the blog posts in it. Since Astro was already made with handling posts in mind, it was very easy. You just need to create a `content.config.ts` file with the following code:\n```typescript\nconst blog = defineCollection({\n  loader: glob({ base: \"posts\", pattern: \"**/*.{md,mdx}\" }),\n});\n\nexport const collections = { blog };\n```\nThat was very easy. I used `zod` to create a schema for the frontmatter, the metadata, of the posts.\n\nThen, in the `index.astro` file, the website's entrypoint, I placed the following:\n```astro\n---\nimport { getCollection } from \"astro:content\";\nconst posts = await getCollection(\"blog\");\nif (!posts) return Astro.redirect(\"/404\");\n---\n\n\u003Ch1>Blog\u003C/h1>\n\u003Cul>\n  {posts.map((post) => (\n    \u003Cli>\n      \u003Ca href={post.id}>{post.data.title}\u003C/a>\n    \u003C/li>\n  ))}\n\u003C/ul>\n```\nAndddd that worked? That's too easy!\n\nIn Astro, any code executed inside the \"fences\" (`---`) runs only at build time. This means our application so far has a grand total of zero Javascript!\n### Generating pages dynamically\nAfter creating the post listing page, I wanted to create a page for each post. For this, I needed a dynamic route, meaning I would need Astro to generate the pages for me.\n\nI would like to take this moment to say that Astro's documentation is very good! It would definitely be a lot more work to do this without it.\n\nAnyway, I created a `[slug].astro` file. This file accepts any value as input, and I can use this value via code.\n```astro\n---\nconst { slug } = Astro.params;\n---\n\u003Cp>Slug: {slug}\u003C/p>\n```\nThen I used this to get a post's ID. Fortunately, Astro already has a specific function to get an item from a collection: `getEntry`. It receives the collection and the ID to search for. After that, I needed to know how to show the Markdown content in HTML. Then I stumbled upon the `render` function, how useful!\n```astro\n---\nimport { getEntry, render } from \"astro:content\";\nconst { slug } = Astro.params;\nconst post = await getEntry(\"blog\", slug);\nif (!post) return Astro.redirect(\"/404\");\nconst { Content } = await render(post);\n---\n\u003CContent />\n```\nAnd in theory, it was supposed to work super well... But Astro cannot know all the possible routes for `[slug].astro`, which prevented it from keeping the site clean of Javascript. To circumvent this, we have to tell Astro all the possible routes. We do this by exporting a `getStaticPaths` function, which fortunately can be async, where we return a list with all the completed variables. We can also pass props to each page, which means we can pass all the posts directly!\n```astro\n---\nimport { getCollection, render } from \"astro:content\"; // we now fetch the whole collection!\n\nexport async function getStaticPaths() {\n  const posts = await getCollection(\"blog\");\n  return posts.map((post) => ({\n    params: { slug: post.id },\n    props: { post }, // we can pass the post directly to the page!\n  }));\n}\n\nconst { post } = Astro.props; // and the post is guaranteed to exist!\nconst { Content } = await render(post);\n---\n\u003CContent />\n```\nOkay! That was pretty easy.\n\nTesting the different posts, I noticed that one of them was rendering accents incorrectly. For example, instead of the word *água* (which means \"water\"), the rendered HTML returned _Ã¡gua_. I was confused about the reason, and apparently Astro no longer defines the document's charset as UTF-8 by default. I was quite confused because [I only saw this mentioned in one place in the docs](https://docs.astro.build/en/guides/markdown-content/#frontmatter-layout-property)? But that's fine, I lost a bit of time on this part.\n### Dealing with different languages\nIn case you've never noticed, this blog is available in both Brazilian Portuguese and English. As of the writing of this post, all posts are available in both languages. I don't know if at some point they will diverge and have different content.\n\nAnyway, having multiple languages is necessary. Like the good programmer that I am™, I wanted to keep it dynamic enough to have the least amount of headache possible if I want to write in some other language. First, I changed the `content.config.ts` file to have the posts separated:\n```typescript\nfunction createCollection(suffix: string) {\n  return defineCollection({\n    loader: glob({ base: `posts/${suffix}`, pattern: \"**/*.{md,mdx}\" });\n  });\n}\n\nexport const collections = {\n  \"blog-en\": createCollection(\"en\"),\n  \"blog-pt\": createCollection(\"pt\"),\n};\n```\nAnd then I created an `i18n.ts` file with the following code:\n```typescript\nconst entries = {\n  en: {\n    \"all-posts\": \"All posts\",\n  },\n  pt: {\n    \"all-posts\": \"Todos os posts\",\n  },\n};\n\nexport type Locales = keyof typeof entries;\nexport const locales = Object.keys(entries) as Locale[];\nexport const defaultLocale: Locale = \"en\" as const;\n\nimport { getCollection as getContentCollection } from \"astro:content\";\n\nexport function getCollection(locale: Locale = defaultLocale) {\n\treturn getContentCollection(`blog-${locale}`);\n}\n```\nThe page architecture looked like this:\n```\nsrc/\n├── content.config.ts\n├── i18n.ts\n├── pages/\n│   ├── [lang]/\n│   │   ├── [slug].astro\n│   │   └── index.astro\n│   ├── [slug].astro\n└── └── index.astro\n```\nAnd everywhere I needed the selected language, I just put `const { lang = defaultLocale } = Astro.params;`.\n\nFrom then on, everything worked perfectly fine. I created a component that adds the language suffix to the URL, and that way I could switch languages. Everything worked perfectly and, so far, zero JavaScript!\n## Where things started to go downhill...\nIn Astro, Markdown is rendered into HTML. The rendering is very basic: links become `\u003Ca>`, headings become `\u003Ch1>`, `\u003Ch2>`... Everything normal. It turns out that I wanted a small customization: if you want to test it in this post, every title is clickable. I left it like this because, in case someone wants to share just a specific topic of the post, it is possible. To do this, I created a `Prose` component in Nuxt, which allows me to *override* the tags generated by Markdown.\n\nIn Astro, however, this is not possible. Not in a simple way. I had two choices:\n1. Use `MDX` to write the posts instead of markdown and call the components as needed, or;\n2. Use a rehype plugin to alter the tags at rendering time.\n\nSince I'm not really a big fan of `mdx`, I went with the second option. It turns out that this generated very ugly code, barely functional (it bugged a lot), and that I wouldn't understand a few days later. But, that's fine, I did it anyway.\n\nAnd voilà, after adding the css, I had an exact replica of my blog, 100% functional.\n## Verdict and comparison\nWell, the entire blog didn't remain immune to JavaScript. It's possible to search for posts and there is a way to toggle between light and dark themes. I built the site and wanted to see how heavy it was.\n\n> The metrics are in the format `\u003Ccompressed_size>/\u003Cuncompressed_size>`. The site will always be downloaded compressed and is uncompressed locally. You can access the reference post [here](https://blog.marciosobel.dev/switching-to-nixos/). All caches were disabled in the measurements.\n\nMy blog, with `Nuxt`, the listing of all posts uses **228 kB / 569 kB**. Accessing a post uses **351 kB / 657 kB**.\n\nThe replica, with `Astro`, on the listing screen, uses **164 kB / 166 kB**. Accessing a post uses **257 kB / 260 kB**.\n\nIt is noticeable that Astro uses 28% less network than Nuxt in the listing, and 27% less when viewing a post, with its biggest difference being in the uncompressed data, possessing a difference of 71% in the listing and 60% when viewing a post!\n## Conclusion: which framework am I staying with?\nThe difference in the final bundle is certainly notable. Astro delivers a much lighter site, containing only the content you choose to deliver. But, despite this, I decided to stick with Nuxt. Here is why: I like to tweak things back and forth. I found Astro for this use case of mine to be not very flexible, especially in the part of modifying the markdown output. And, although it is much lighter, it is only in the uncompressed content; on the network side, the difference isn't that shocking. Today's phones and computers barely break a sweat to compensate for this difference. So I accept making this sacrifice in exchange for a better DX.\n\nNonetheless, Astro definitely won me over with its charm and simplicity. I will certainly use it to generate documentation for possible libraries of mine in the future. I didn't get to explore its full potential and, if I were starting from scratch, I would probably pick a template and build from there.\n\nThat was my review of Astro, I hope you had a good read. Until next time!",{"title":5,"description":1664},"blog/astro-vs-nuxt/en","6byxV0Dq2ZQh_9Fm4hY3qmzu7J7lbvP1yOshr1JMGHg",{"iso":1675,"formatted":1676},"2026-06-17T00:00:00.000Z","June 17th, 2026",{"iso":-1,"formatted":-1},1781699323807]