Introduction

When performing Static Site Generation (SSG) with Nuxt 4, you may want to load data from local JSON files to generate static pages. However, it is not as simple as Next.js’s getStaticProps, and there are several pitfalls to be aware of.

This article introduces the correct approach discovered through trial and error.

Problem: Why Simple fs Reading Doesn’t Work

First Approach (Failed)

c}o;ni}crsfoetccccrntT(ooooeCsuhfinnnntltriemssssuinstpttttrerconneadhrfpfdtswoLtsauaJpaeo.tltSsoiscm=hlaOintnaePNds'lta=a=.eertDawtpea.aahfa=swtsiwsrpoaeta=.saorrirewnk=vitpe(asemaadiearpitdat.s)omhFtjyrp.iafsn{torl)eoc(ree;tn'tsSc((f(oyh)fs'ln(;i'pvc`l)ae(/e;t(fdPhpuaa'rltt)olah;cP/:ea$st{sshft.,ircliw'enduPg(ta))ft,-h=8}>''`p)){u;;blic/data',filePath);

This approach has the following problems:

  1. process.cwd() differs in build environments: The working directory differs between local development and build environments like Vercel
  2. Files cannot be found during Nitro pre-rendering: During SSG, Nitro operates in its own context
  3. Without useAsyncData, it also runs on the client side: This defeats the purpose of SSG

Solution: Nitro Storage API + Server API Route + useAsyncData

Architecture

[PSp[DDaeuGaugrbetNrevlnaoieienCrcriJgouu/asSmsAsdtOSpePeaeeNSoAIStdmGnstableyRo/HeoBnnor*Tdautcua.MddiDtgjLeilaees]dndtog]a(ni/Nnnaiept_eirpd/oaelydoSlctooaoanlrd-a.tdgjhaesetoaAnc/Pl[Ii.)e.n.tpastihd]e)

Step 1: Configure serverAssets in nuxt.config.ts

e}x)pn}r};noi,o,urtp}s}uxtrr,e]ttoecfrbd,e.d:rraMvaiRceeaioesru'of{nwlure:l:nadlOnANefueLntsas{ilriEsm:gt:nrpee/p.krut:p{rtd{sobsuese:rl:'brf:idleitc[ainnrf/{tcdeuadaeNela'dru,st,a:xeatt,atCa'rosunefsie}gr,(v{erassetsviaNitroStorageAPI

Step 2: Create a Server API Route (Nitro Storage + fs Fallback)

iie}mmx)pppci}ci}cci}ci}t;soooofofoofofherrrntntnncrncrrrttts(hsS(hUAsCsT(oeFs(oeovt!rtefrsctotrantatentwe{{dpocioecnywsulxsurepawfulwesvsatrlfitrc/rrfatireNstettinbssnraeeathcliPcisorootdaPtdepasuhPretartrtradcasaJaidolPaePytertaarstaktStSt/Fltaraa:haohgfgetat:hyaOelivratt.teeietoanNEoledamehPieSlKrr=;R=c=.rceem)ErnEt'=eeiae(praS}fr=ecrodyegaarfraolyi={rvlrraupvewdeserr-nfnoAeuoatsa=e.asPas(dcregrrndrgaethidoade{a,oEe(rte(e'ShfvatiltF(tmvt{as{tiisrvhidsaeeRyp(AsotlaIsee)lat/x'nos.a'sPtroettc()eta[iptutit.tIoaPSeotpSat.saHtash.argsatmrlr{y)u.ttaetA'taetto(ayon;s.shnrurt)ug(ohrsgccCpS'dPsrr)se'r.ateve(oay;laCaaCaargo.isfdtneroyv{odsgeergassehcrad(edesepae.P:](meprefelAgtfca.}a(:as:itkaPeIswt4tsetansecIKtdh0sfyv4hl4e:yeeef(,4rne0P0dd(ymo),ocn0a0a(/)(r,'mt,r,itr\)sum(,anae/td'te'emmm'p/{oepfsfv'e)en)lgrvu-ssepssu;a,aeb8a'nas?sxcgll'g;ttaate'eoi)e)hgpg.:Kpc;:'eaec/'em/=):t:o)yed`>;hnw;)naF'P'fi;tti{PaIitalarnghe'etav.n,hmat:vn.ls)ifoijiritsodsolienefrnprmPoe(aveauq'tentnu/hrthdi''A):r)s;e}s$d:)e{';tfpsi}al)te;hPPaatrha}m`;});

Benefits of using Nitro Storage API:

  • Stable operation in build environments like Vercel
  • Avoids OS-specific path separator issues
  • Nitro optimizes file access based on the environment

Why fs fallback is needed:

In development mode (npm run dev), serverAssets may not function fully, so direct fs reading is added as a fallback. The Storage API takes priority during builds.

Step 3: Wrap with a Composable

e}x;pc}rcoo;eornt}}tmtsruptyi}}ccrrocfaoensof{recirtntane(elofecsu{bstitsnthorltcmDueCSs(ulnfehpurlet!r(eesuLorn{irrne.nt/sorievrereucuectnaneesarrlhsLa.gwtrspworlLeolmapoaro;oLcDeSisAoni)rcoaatStiPnst(acltaGdIse{`laDa./$ee.rEDlasSf:doeraDt=eSeo=ksrtaarRtFe)poatav:cesaora=sehtnwrn}.yrU(c'aesl;tn)s`htiteosce/tu.a{aderjd=(Spixfnsi>feirieonir/estnng{llctcu(:eeothl)Prclo(l;$aayn`;{tAl/fhP-fsdi:IdrtalaoatestmtaPtai/ar/pc$ti$u{hn{bhf}gfloi`)iisl,lcte=eiPe>Pnaragtr{thoh(}r}S`)`3);),;;Netlify,etc.)

Why client-side fallback is important:

When deploying SSG-generated static sites to S3 or Netlify, Server API routes don’t exist. Therefore, during client-side navigation, JSON needs to be fetched directly from /data/.

Step 4: Use useAsyncData in Page Components

<cc}c<soo)o/t/cnncr;nse<trsAsoeAscmd/eitltntctrpi{dmpwsucilv{ipt{a{treppavlynsottp>asfsdrss>e-oteeaert>fsettwtsessot>ucrausar.pha:luf==tLptle"iloptlcptacwa=;yoolnaigmseglteawpt=DhDwiu}"aaatti}ttutihensasatd"ec(p>}A}fo(osem)s=y=tptncu=sucaht>"sDwLeeaaodp:LticakoatagecleyauDD=lsaa"DettpaAaaots(.say'vt(nja.)csli;Doudane"ta?>ap.(id'/amntyoa-dpea/gpeo[-s]dt)as;t.aj's,ona's)y;nc=>{

Important Points

1. useAsyncData is Required

If you await without useAsyncData, it will also execute on the client side.

ccoonnssttBCado{dartrdeaeaxcta=tampae}lwxeaa=imtpalwfeaeittchuLsoecAaslyDnactDaa(t'ap(o'sktesy.'j,son')=;>fetchLocalData('posts.json'));

2. Keys Must Be Unique

The first argument key of useAsyncData must be unique across pages/components.

c}o)nr;Usestteu{randuaantwiaaqiu}te=fkeeatywcahpiLetorcuapslaeDgAaestyan(c`Dpaotsat(s`/p$o{srto-u$t{er.opuatrea.mpsa.riadm}s..jisdo}n``,);async=>{

3. Access Safely with computed

Since the return value of useAsyncData is a Ref, wrapping it with computed is safe when using it in templates or logic.

cccooonnnssstttW{rtiaitdptealmtwesai:t==hpccacoogommemppDpuuautttteeaeddd((}(())===a>>wappiaatggeeuDDsaaettAaas..yvvnaacllDuuaeet??a..(ti.it.te.lm)es;[]));;

4. Watch Out for Variable Scope

Variables defined inside the useAsyncData callback cannot be used outside the callback.

c}ccc}co)ooo)oncr;nnnr;nsoesssestnttttttBsuCua{triot{ridntryntdterpdeeayameeaamxtpwscstwsaaeataamsi==i=p}te}tl=[x[[e=f'a'=f'[exmaexa't'p'at'wac,l,wc,a'heahi,D''iD'taybtay't''t'uba,]ua,s'(;s(e])e)A;;A;s.s.ytytnyDnycpecpDDefDeaesiastf]nt]ai;ea;(nd('e'kdokeueyiEtyO'nrs'K,sri,iodadrease:sycynctancaylclpllebbsaac=cik=>ks>{n{otdefined

Result

Static sites generated with this method:

  1. Data is embedded in HTML: Fast initial rendering
  2. Data is saved in _payload.json: Used during client-side navigation
  3. No requests to the original JSON files: Reduced network load
[PSp[DDaeuGaugrbetNrevlnaoieienCrcriJgouu/asSmsAsdtOSpePeaeeNSoAIStdmGnstableyRo/HeoBnnor*Tdautcua.MddiDtgjLeilaees]dndtog]a(ni/Nnnaiept_eirpd/oaelydoSlctooaoanlrd-a.tdgjhaesetoaAnc/Pl[Ii.)e.n.tpastihd]e)

Alternative Approaches

For Small JSON Files: Direct Import

If the file size is small (under 50KB) and dynamic paths are not needed, direct import is the simplest approach.

[PSp[DDaeuGaugrbetNrevlnaoieienCrcriJgouu/asSmsAsdtOSpePeaeeNSoAIStdmGnstableyRo/HeoBnnor*Tdautcua.MddiDtgjLeilaees]dndtog]a(ni/Nnnaiept_eirpd/oaelydoSlctooaoanlrd-a.tdgjhaesetoaAnc/Pl[Ii.)e.n.tpastihd]e)

For Content Management: Nuxt Content

If you are building a site that handles markdown or JSON content, the Nuxt Content module is suitable.

[PSp[DDaeuGaugrbetNrevlnaoieienCrcriJgouu/asSmsAsdtOSpePeaeeNSoAIStdmGnstableyRo/HeoBnnor*Tdautcua.MddiDtgjLeilaees]dndtog]a(ni/Nnnaiept_eirpd/oaelydoSlctooaoanlrd-a.tdgjhaesetoaAnc/Pl[Ii.)e.n.tpastihd]e)

Summary

To combine SSG with local JSON in Nuxt 4:

  1. Use the Nitro Storage API to load JSON in Server API routes
  2. Always wrap with useAsyncData
  3. Access data safely with computed
  4. Don’t forget to implement client-side fallback

With this approach, all data is prefetched during build time, preventing additional API calls on the client side.