[{"data":1,"prerenderedAt":2629},["ShallowReactive",2],{"home-projets-en":3,"home-articles":558},[4,74,108,141,199,233,284,316,350,384,417,454,489,526],{"id":5,"title":6,"annee":7,"body":8,"client":57,"description":51,"extension":58,"image":59,"imageAlt":59,"lien":51,"meta":60,"navigation":61,"ordre":62,"path":63,"resume":64,"seo":65,"stack":66,"stem":71,"type":72,"__hash__":73},"projets_en\u002Fen\u002Fprojects\u002Felis-esic.md","eLIS & eSIC national health information systems","2024 – present",{"type":9,"value":10,"toc":50},"minimark",[11,16,29,33,36,39,43],[12,13,15],"h2",{"id":14},"context","Context",[17,18,19,20,24,25,28],"p",{},"As part of the digitalization of the Congolese health system led by the\nMinistry of Health's Directorate of Information and Communication\nTechnologies (DTIC), funded by The Global Fund (C19RM grant) with the\nsupport of Catholic Relief Services, two national systems were developed:\n",[21,22,23],"strong",{},"eLIS",", the laboratory network information system, and ",[21,26,27],{},"eSIC",", the\ncommunity health information system.",[12,30,32],{"id":31},"what-was-delivered","What was delivered",[17,34,35],{},"As an expert IT consultant, I contributed to the design, development and\ndeployment of both platforms: full sample lifecycle coverage for eLIS\n(patient reception, sampling, packaging, transport, analysis, storage) and\ncommunity care packages for eSIC (consultations, HIV and tuberculosis care,\npregnancy follow-up, cash desk). The architecture combines a tablet-based\nmobile module for field data entry, a web back-office and dashboards, with\nDHIS2 interoperability as a target.",[17,37,38],{},"The pilot phase was deployed in late 2024 across the departments of\nBrazzaville, Pointe-Noire and Bouenza: general hospitals, the Brazzaville\nUniversity Hospital (CHU), the national blood transfusion centre (CNTS),\nthe national public health laboratory (LNSP) and integrated health centres,\nwith on-site user training and ongoing support.",[12,40,42],{"id":41},"whats-next","What's next",[17,44,45,46,49],{},"Since July 2026, I have been supporting the Ministry as an independent\nconsultant on ",[21,47,48],{},"phase 2",", in continued collaboration with CRS: functional\nextensions based on pilot feedback, data quality improvements and continued\nnational rollout.",{"title":51,"searchDepth":52,"depth":52,"links":53},"",2,[54,55,56],{"id":14,"depth":52,"text":15},{"id":31,"depth":52,"text":32},{"id":41,"depth":52,"text":42},"Ministry of Health and Population (DTIC), supported by CRS \u002F The Global Fund","md",null,{},true,1,"\u002Fen\u002Fprojects\u002Felis-esic","Design and deployment of the national laboratory (eLIS) and community health (eSIC) information systems for Congo's public health sector.",{"title":6,"description":51},[67,68,69,70],"Mobile app (tablets)","Web back-office","Dashboards","DHIS2 interoperability","en\u002Fprojects\u002Felis-esic","Web & Mobile","cHH2sXPv4TS_s4C8uw-8Z92Nj2PgBLY1hueAOuqyApA",{"id":75,"title":76,"annee":77,"body":78,"client":94,"description":51,"extension":58,"image":95,"imageAlt":96,"lien":51,"meta":97,"navigation":61,"ordre":52,"path":98,"resume":99,"seo":100,"stack":101,"stem":105,"type":106,"__hash__":107},"projets_en\u002Fen\u002Fprojects\u002Fartf.md","ARTF portal redesign","2025 – 2026",{"type":9,"value":79,"toc":90},[80,82,85,87],[12,81,15],{"id":14},[17,83,84],{},"ARTF is the technical body of the Ministry of Finance in charge of\nregulating the money-transfer sector in the Republic of the Congo. Its\nportal carries the agency's institutional voice: missions and history,\nregulatory texts, quarterly sector reports, and information for operators\nand the public.",[12,86,32],{"id":31},[17,88,89],{},"As a software developer within the agency since September 2025, I designed\nand built the complete redesign of the portal: a new information\narchitecture (institution, regulation, publications, news), a structured\ndocument space for sector reports and regulatory texts, and a modernized\ninterface. Production rollout is being finalized.",{"title":51,"searchDepth":52,"depth":52,"links":91},[92,93],{"id":14,"depth":52,"text":15},{"id":31,"depth":52,"text":32},"ARTF - Funds Transfer Regulation Agency","\u002Fimages\u002Fprojets\u002Fartf.jpg","ARTF institutional portal homepage",{},"\u002Fen\u002Fprojects\u002Fartf","Complete redesign of the funds-transfer regulator's portal: institution, regulatory publications and quarterly sector reports.",{"title":76,"description":51},[102,103,104],"Institutional portal","Regulatory publications","Sector data","en\u002Fprojects\u002Fartf","Web","KdxqzAIpXoZNmu2H3dGnPSggENJoIQTA0Y9ExYekRCM",{"id":109,"title":110,"annee":111,"body":112,"client":128,"description":51,"extension":58,"image":129,"imageAlt":130,"lien":131,"meta":132,"navigation":61,"ordre":133,"path":134,"resume":135,"seo":136,"stack":137,"stem":139,"type":106,"__hash__":140},"projets_en\u002Fen\u002Fprojects\u002Farpce.md","ARPCE official portal","2024",{"type":9,"value":113,"toc":124},[114,116,119,121],[12,115,15],{"id":14},[17,117,118],{},"ARPCE (the Posts and Electronic Communications Regulatory Agency) is the\nnational regulator of the postal and telecommunications sector in the\nRepublic of the Congo. Its portal is the institution's official showcase:\nmissions, regulation, publications and sector news.",[12,120,32],{"id":31},[17,122,123],{},"As part of an engagement with Elonga Victorious Communication (Oct.–Dec.\n2024), I delivered the agency's official portal and its related sites:\ninstitutional information architecture, regulatory sections and\npublication spaces.",{"title":51,"searchDepth":52,"depth":52,"links":125},[126,127],{"id":14,"depth":52,"text":15},{"id":31,"depth":52,"text":32},"ARPCE, via Elonga Victorious Communication (EVC)","\u002Fimages\u002Fprojets\u002Farpce.jpg","ARPCE portal homepage","https:\u002F\u002Farpce.cg",{},3,"\u002Fen\u002Fprojects\u002Farpce","Delivery of the official portal of Congo's posts and electronic communications regulator, together with its related sites.",{"title":110,"description":51},[102,138],"Related sites","en\u002Fprojects\u002Farpce","pPICbiyZyZ5ZoP9soJdPHCFUcuZO3MlHpNpEZYBiZTg",{"id":142,"title":143,"annee":144,"body":145,"client":186,"description":51,"extension":58,"image":187,"imageAlt":188,"lien":59,"meta":189,"navigation":61,"ordre":190,"path":191,"resume":192,"seo":193,"stack":194,"stem":197,"type":72,"__hash__":198},"projets_en\u002Fen\u002Fprojects\u002Fpdac.md","PDAC platform and mobile application","2019 – 2024",{"type":9,"value":146,"toc":182},[147,149,152,154,157,179],[12,148,15],{"id":14},[17,150,151],{},"PDAC is a World Bank-funded project supporting commercial agriculture in\nthe Republic of the Congo. The project needed three things: an information\nwebsite for its official communication, a mobile tool usable in the field,\nand a reliable beneficiary registry for monitoring and reporting.",[12,153,32],{"id":31},[17,155,156],{},"All three components were designed and delivered in 2019:",[158,159,160,167,173],"ul",{},[161,162,163,166],"li",{},[21,164,165],{},"The project's official website",": news, calls for proposals,\npresentation of the matching grants and information for agricultural\nentrepreneurs.",[161,168,169,172],{},[21,170,171],{},"The Android mobile application",", built for irregular connectivity\nconditions, featuring the \"Parole aux bénéficiaires\" section: field\ninterviews giving a direct voice to the farmers and entrepreneurs\nsupported by the project.",[161,174,175,178],{},[21,176,177],{},"The centralized beneficiary database",": structuring large volumes of\nfield data into a single registry, used for monitoring support\nactivities and project reporting.",[17,180,181],{},"I then maintained and evolved the whole system as a full-time IT\nconsultant until the end of my engagement in 2024: five years of\ncontinuous operation in support of the project teams.",{"title":51,"searchDepth":52,"depth":52,"links":183},[184,185],{"id":14,"depth":52,"text":15},{"id":31,"depth":52,"text":32},"PDAC - Commercial Agriculture Development Support Project (World Bank)","\u002Fimages\u002Fprojets\u002Fpdac.jpg","Web platform of the PDAC agricultural project",{},4,"\u002Fen\u002Fprojects\u002Fpdac","Official website, field mobile application and centralized beneficiary database for a World Bank-funded agricultural project.",{"title":143,"description":51},[106,195,196],"Android","Beneficiary database","en\u002Fprojects\u002Fpdac","nqqagxdi3CpH5l_1q5VpPGs9z42eDi_ouf1xI3AxMBA",{"id":200,"title":201,"annee":202,"body":203,"client":219,"description":51,"extension":58,"image":220,"imageAlt":221,"lien":222,"meta":223,"navigation":61,"ordre":224,"path":225,"resume":226,"seo":227,"stack":228,"stem":231,"type":106,"__hash__":232},"projets_en\u002Fen\u002Fprojects\u002Fcongo-telecom.md","Congo Télécom website","2020 – 2024",{"type":9,"value":204,"toc":215},[205,207,210,212],[12,206,15],{"id":14},[17,208,209],{},"Congo Télécom is the historical telecommunications operator of the Republic\nof the Congo, fully state-owned, the dominant operator in fixed telephony\nand optical fibre, with over 3,500 km of backbone deployed nationwide. Its\nwebsite serves three distinct audiences: consumers, businesses and\ncorporate accounts.",[12,211,32],{"id":31},[17,213,214],{},"As a consultant for Elonga Victorious Communication (the agency managing\nthe Congo Télécom and ARPCE websites), I designed and built the operator's\nwebsite, then handled its maintenance and evolutions from 2020 to 2024:\nConsumer \u002F Business \u002F Corporate spaces, fibre and fixed-line offers, the\noperator's news, and integration of the SpeedPay payment service.",{"title":51,"searchDepth":52,"depth":52,"links":216},[217,218],{"id":14,"depth":52,"text":15},{"id":31,"depth":52,"text":32},"Congo Télécom, via Elonga Victorious Communication (EVC)","\u002Fimages\u002Fprojets\u002Fcongotelecom.jpg","Congo Télécom website homepage","https:\u002F\u002Fcongotelecom.cg",{},5,"\u002Fen\u002Fprojects\u002Fcongo-telecom","Design of the website of Congo's historical telecommunications operator, followed by four years of ongoing maintenance and evolutions.",{"title":201,"description":51},[229,230],"Multi-audience corporate site","Design & maintenance","en\u002Fprojects\u002Fcongo-telecom","ktv27KNJSkxxwQeHhwhUJ8CAC4fapmEIZMSWkgHQAss",{"id":234,"title":235,"annee":236,"body":237,"client":255,"description":51,"extension":58,"image":270,"imageAlt":271,"lien":272,"meta":273,"navigation":61,"ordre":274,"path":275,"resume":276,"seo":277,"stack":278,"stem":281,"type":282,"__hash__":283},"projets_en\u002Fen\u002Fprojects\u002Fsantef.md","SantéF & SantéF-Doc","2020 – 2022",{"type":9,"value":238,"toc":266},[239,241,244,246,249,263],[12,240,15],{"id":14},[17,242,243],{},"SantéF is a Congolese digital health initiative aiming to improve access to\nhealth services through mobile solutions, for both the general public and\nhealth professionals.",[12,245,32],{"id":31},[17,247,248],{},"Technical lead (April 2020 – November 2022) on the development of two\ncomplementary applications:",[158,250,251,257],{},[161,252,253,256],{},[21,254,255],{},"SantéF",", the public-facing app: an electronic vaccination record and\nhealth advice, including during the COVID-19 period;",[161,258,259,262],{},[21,260,261],{},"SantéF-Doc",", the platform for health professionals: receiving and\nmanaging consultation requests and appointments, within a care pathway\ninterconnecting doctors, pharmacies, laboratories, clinics and patients.",[17,264,265],{},"This project was my first immersion in Congolese digital health: the field\nthat has since become the core of my consulting practice.",{"title":51,"searchDepth":52,"depth":52,"links":267},[268,269],{"id":14,"depth":52,"text":15},{"id":31,"depth":52,"text":32},"\u002Fimages\u002Fprojets\u002Fsantef_app.jpg","SantéF mobile app screen","https:\u002F\u002Fplay.google.com\u002Fstore\u002Fapps\u002Fdetails?id=com.santef",{},6,"\u002Fen\u002Fprojects\u002Fsantef","Technical lead on two digital health applications: a public-facing electronic vaccination record and a care-management platform for health professionals.",{"title":235,"description":51},[195,279,280],"Electronic vaccination record","Care pathway","en\u002Fprojects\u002Fsantef","Mobile","iuSN8-PkUL475RSit5pQoQR3rF594oVkI1SFHy5uuww",{"id":285,"title":286,"annee":287,"body":288,"client":304,"description":51,"extension":58,"image":305,"imageAlt":306,"lien":59,"meta":307,"navigation":61,"ordre":308,"path":309,"resume":310,"seo":311,"stack":312,"stem":314,"type":106,"__hash__":315},"projets_en\u002Fen\u002Fprojects\u002Fambassade-guinee-equatoriale.md","Website of the Embassy of Equatorial Guinea in Congo","2022",{"type":9,"value":289,"toc":300},[290,292,295,297],[12,291,15],{"id":14},[17,293,294],{},"The diplomatic mission of Equatorial Guinea in Brazzaville needed an\nofficial website for its consular services and for promoting the country\nto travellers and investors. Based in Brazzaville, the embassy holds\nregional jurisdiction: it covers Congo-Brazzaville, the Democratic Republic\nof the Congo (Congo-Kinshasa), Rwanda and Burundi.",[12,296,32],{"id":31},[17,298,299],{},"An institutional website presenting the embassy's services and dedicated\nsections (visit, invest, and live in Equatorial Guinea). It also lists the\ncountries under its consular jurisdiction, Congo-Brazzaville, Congo-Kinshasa,\nRwanda and Burundi, so that each national knows which mission to contact.",{"title":51,"searchDepth":52,"depth":52,"links":301},[302,303],{"id":14,"depth":52,"text":15},{"id":31,"depth":52,"text":32},"Embassy of the Republic of Equatorial Guinea","\u002Fimages\u002Fprojets\u002Fambassade-guinee-equatoriale.jpg","Homepage of the Equatorial Guinea Embassy website in Congo",{},7,"\u002Fen\u002Fprojects\u002Fambassade-guinee-equatoriale","Official website of an embassy with regional jurisdiction (Congo, DRC, Rwanda, Burundi): consular services, traveller information and country promotion.",{"title":286,"description":51},[106,313],"Multilingual","en\u002Fprojects\u002Fambassade-guinee-equatoriale","CnpqVOavASXup8zNsd9FGcJfzY79BFgDAz62z6aHhrs",{"id":317,"title":318,"annee":319,"body":320,"client":336,"description":51,"extension":58,"image":337,"imageAlt":338,"lien":339,"meta":340,"navigation":61,"ordre":341,"path":342,"resume":343,"seo":344,"stack":345,"stem":348,"type":106,"__hash__":349},"projets_en\u002Fen\u002Fprojects\u002Ffae-africa.md","African Foundation for Education website","2022 – 2023",{"type":9,"value":321,"toc":332},[322,324,327,329],[12,323,15],{"id":14},[17,325,326],{},"Founded in 2011 by Congolese professionals and recognized as a UNESCO Club\nsince October 2022, the African Foundation for Education works to improve\neducational opportunities in Congo and across Africa: post-secondary\nguidance (CIO), a language centre, the ESMBA programme, FAE Meet workshops,\nand achievements such as the American International School of Brazzaville.",[12,328,32],{"id":31},[17,330,331],{},"Development consultancy (2022–2023) on the foundation's digital presence:\nbuilding the institutional website presenting FAE's missions, programmes,\nachievements and news, with an editorial structure designed for a\nmulti-programme organization.",{"title":51,"searchDepth":52,"depth":52,"links":333},[334,335],{"id":14,"depth":52,"text":15},{"id":31,"depth":52,"text":32},"FAE - Fondation Africaine pour l'Éducation","\u002Fimages\u002Fprojets\u002Ffae.jpg","FAE Africa education organisation website","https:\u002F\u002Ffaeafrica.org",{},8,"\u002Fen\u002Fprojects\u002Ffae-africa","Development of the digital presence of a Congolese educational foundation and UNESCO Club: programmes, achievements and news.",{"title":318,"description":51},[346,347],"Institutional website","Programmes & news","en\u002Fprojects\u002Ffae-africa","zW6eXZSnRznBcOciJkSGzjeSftV7FzIJFXAXadZVxVU",{"id":351,"title":352,"annee":353,"body":354,"client":370,"description":51,"extension":58,"image":371,"imageAlt":372,"lien":373,"meta":374,"navigation":61,"ordre":375,"path":376,"resume":377,"seo":378,"stack":379,"stem":382,"type":106,"__hash__":383},"projets_en\u002Fen\u002Fprojects\u002Ffhic.md","Forum Horizon Initiative et Créativité (FHIC) website","2025",{"type":9,"value":355,"toc":366},[356,358,361,363],[12,357,15],{"id":14},[17,359,360],{},"The Forum Horizon Initiative et Créativité (FHIC) is a Congolese initiative\npromoting entrepreneurship, creativity and entrepreneurial dialogue within\nlocal communities. Founded and chaired by Aline France Etokabeka, it acts as\na job-creation accelerator: panels and conferences, training workshops,\nexhibitions, mentoring and support for project holders. Its editions are\nheld successively in several cities across the country (Brazzaville, Dolisie,\nImpfondo, Pointe-Noire) since 2023, and the forum reports over 250 startups\nsupported and around a thousand direct jobs created.",[12,362,32],{"id":31},[17,364,365],{},"Design and development of the forum's official website, launched in October\n2025: presentation of the initiative and its missions, a spotlight on the current edition, an archive\nof past editions, a photo gallery and an event registration flow. The site\ncarries FHIC's public voice and serves as the entry point for participants,\npartners and project holders.",{"title":51,"searchDepth":52,"depth":52,"links":367},[368,369],{"id":14,"depth":52,"text":15},{"id":31,"depth":52,"text":32},"FHIC - Forum Horizon Initiative et Créativité","\u002Fimages\u002Fprojets\u002Ffhic.jpg","FHIC website homepage","https:\u002F\u002Ffhic.cg",{},9,"\u002Fen\u002Fprojects\u002Ffhic","Website for the Congolese forum devoted to entrepreneurship and job creation: presentation, successive editions and event registration.",{"title":352,"description":51},[346,380,381],"Editions & registration","Gallery","en\u002Fprojects\u002Ffhic","JWBeR5faCWqEwThyL51gwKGcE7cpEgFQcApMsrYJtbA",{"id":385,"title":386,"annee":387,"body":388,"client":404,"description":51,"extension":58,"image":405,"imageAlt":406,"lien":407,"meta":408,"navigation":61,"ordre":409,"path":410,"resume":411,"seo":412,"stack":413,"stem":415,"type":106,"__hash__":416},"projets_en\u002Fen\u002Fprojects\u002Fcabinet-mayabel.md","Cabinet Mayabel website","2023",{"type":9,"value":389,"toc":400},[390,392,395,397],[12,391,15],{"id":14},[17,393,394],{},"A chartered-accounting, audit and advisory firm established in Congo for\nover ten years, with offices in Brazzaville and Pointe-Noire and regional\nCEMAC coverage. Mayabel needed an online presence matching its positioning\nas a \"growth partner\", covering the full range of its services.",[12,396,32],{"id":31},[17,398,399],{},"A clear showcase website presenting the firm's practice areas: accounting\nand statutory audit, internal audit, tax management, management and\nstrategic advisory, payroll, legal support and professional training. The\nsite highlights the firm's experience and locations, with a press kit and a\ncontact journey.",{"title":51,"searchDepth":52,"depth":52,"links":401},[402,403],{"id":14,"depth":52,"text":15},{"id":31,"depth":52,"text":32},"Cabinet Mayabel","\u002Fimages\u002Fprojets\u002Fmayabel.jpg","Cabinet Mayabel website homepage","https:\u002F\u002Fcabinet-mayabel.com",{},10,"\u002Fen\u002Fprojects\u002Fcabinet-mayabel","Showcase website for an accounting, audit and advisory firm in Congo (Brazzaville, Pointe-Noire): presentation of the service offering and contact.",{"title":386,"description":51},[106,414],"Showcase","en\u002Fprojects\u002Fcabinet-mayabel","9V4T0zI7uWUSQO9SSODvHBgQcAaq-J6FTA8m9fBLONU",{"id":418,"title":419,"annee":387,"body":420,"client":442,"description":51,"extension":58,"image":443,"imageAlt":444,"lien":59,"meta":445,"navigation":61,"ordre":446,"path":447,"resume":448,"seo":449,"stack":450,"stem":452,"type":106,"__hash__":453},"projets_en\u002Fen\u002Fprojects\u002Fbic-immo-assurance.md","BIC Immo Assurance portal",{"type":9,"value":421,"toc":438},[422,424,427,429,432],[12,423,15],{"id":14},[17,425,426],{},"BIC Immo Assurance operates in real estate and insurance. The portal needs\nto present properties and services and make it easy to connect with\nclients.",[12,428,32],{"id":31},[17,430,431],{},"A portal presenting the offering (properties, services) and a contact\njourney suited to the business.",[17,433,434],{},[435,436,437],"em",{},"(Case study to be expanded: exact scope, year to be confirmed.)",{"title":51,"searchDepth":52,"depth":52,"links":439},[440,441],{"id":14,"depth":52,"text":15},{"id":31,"depth":52,"text":32},"BIC Immo Assurance","\u002Fimages\u002Fprojets\u002Fbic.jpg","BIC Immo Assurance real-estate and insurance portal",{},11,"\u002Fen\u002Fprojects\u002Fbic-immo-assurance","Real estate and insurance portal: property and service listings, connecting with clients.",{"title":419,"description":51},[106,451],"Real estate","en\u002Fprojects\u002Fbic-immo-assurance","F_g5WL4REz72zXzcoSJbhNvjzO74qLDK880y9r4Nm6Y",{"id":455,"title":456,"annee":457,"body":458,"client":478,"description":51,"extension":58,"image":59,"imageAlt":59,"lien":59,"meta":479,"navigation":61,"ordre":480,"path":481,"resume":482,"seo":483,"stack":484,"stem":487,"type":106,"__hash__":488},"projets_en\u002Fen\u002Fprojects\u002Fkongossa-buzz.md","Kongossa Buzz: online media","2021",{"type":9,"value":459,"toc":474},[460,462,465,467,470],[12,461,15],{"id":14},[17,463,464],{},"Kongossa Buzz is an online media outlet publishing news and content to a\nwide audience. It needed a site able to publish frequently and hold up\nunder traffic peaks.",[12,466,32],{"id":31},[17,468,469],{},"An editorial publishing platform: categories, articles, content highlights\nand reading-focused navigation.",[17,471,472],{},[435,473,437],{},{"title":51,"searchDepth":52,"depth":52,"links":475},[476,477],{"id":14,"depth":52,"text":15},{"id":31,"depth":52,"text":32},"Kongossa Buzz",{},12,"\u002Fen\u002Fprojects\u002Fkongossa-buzz","Online media outlet: news and content publishing, with an editorial setup built for traffic.",{"title":456,"description":51},[106,485,486],"Media","Publishing","en\u002Fprojects\u002Fkongossa-buzz","t4vjLOnr_epz7cJRRFAT3_yG1rtDeOc2guEySDX2Gt0",{"id":490,"title":491,"annee":492,"body":493,"client":513,"description":51,"extension":58,"image":514,"imageAlt":515,"lien":516,"meta":517,"navigation":61,"ordre":518,"path":519,"resume":520,"seo":521,"stack":522,"stem":524,"type":282,"__hash__":525},"projets_en\u002Fen\u002Fprojects\u002Fcine-plus.md","Cine+: movie news on mobile","2019",{"type":9,"value":494,"toc":509},[495,497,500,502],[12,496,15],{"id":14},[17,498,499],{},"Cine+ is a movie-news application published under Ricebean, my personal\nprojects label: keeping up with cinema releases and discovering actors,\nin a deliberately sober interface.",[12,501,32],{"id":31},[17,503,504,505,508],{},"Full design and development of the Android application, published on the\nPlay Store in 2019, where it passed ",[21,506,507],{},"10,000 downloads",". A formative\npersonal project across the whole lifecycle of a published app: design,\ndevelopment, release and iterations.",{"title":51,"searchDepth":52,"depth":52,"links":510},[511,512],{"id":14,"depth":52,"text":15},{"id":31,"depth":52,"text":32},"Ricebean (personal project)","\u002Fimages\u002Fprojets\u002Fcineplus_app.jpg","Cine+ mobile app screen","https:\u002F\u002Fplay.google.com\u002Fstore\u002Fapps\u002Fdetails?id=ricebean.cine_app",{},13,"\u002Fen\u002Fprojects\u002Fcine-plus","Android movie-news application. Over 10,000 downloads on the Play Store.",{"title":491,"description":51},[195,523],"Content API","en\u002Fprojects\u002Fcine-plus","0yUjm9DJXSLfWOJaKf8GYa2nuql4ykgqhPwMww3_LUI",{"id":527,"title":528,"annee":387,"body":529,"client":545,"description":51,"extension":58,"image":546,"imageAlt":547,"lien":548,"meta":549,"navigation":61,"ordre":550,"path":551,"resume":552,"seo":553,"stack":554,"stem":556,"type":282,"__hash__":557},"projets_en\u002Fen\u002Fprojects\u002Fmy-icc.md","myICC: ICC's community app",{"type":9,"value":530,"toc":541},[531,533,536,538],[12,532,15],{"id":14},[17,534,535],{},"Impact Centre Chrétien is an international evangelical church present in\nEurope, Africa, America, the Caribbean and the Indian Ocean. myICC is its\ncommunity mobile application.",[12,537,32],{"id":31},[17,539,540],{},"Design and development of the Android application, published on the Play\nStore in 2023: access to messages and teachings, testimonies, community\nnews, and a locator for local churches and house churches. A project\nrepresentative of the content-and-community apps I deliver for\norganizations.",{"title":51,"searchDepth":52,"depth":52,"links":542},[543,544],{"id":14,"depth":52,"text":15},{"id":31,"depth":52,"text":32},"Impact Centre Chrétien (via Ricebean)","\u002Fimages\u002Fprojets\u002FmyIcc.jpg","myICC mobile app screen","https:\u002F\u002Fplay.google.com\u002Fstore\u002Fapps\u002Fdetails?id=com.ricebean.icc_app",{},14,"\u002Fen\u002Fprojects\u002Fmy-icc","Community mobile app for an international church: messages, testimonies, news and a directory of local congregations.",{"title":528,"description":51},[195,555],"Content & community","en\u002Fprojects\u002Fmy-icc","oLC7EeGamapqSEXO3fsEs43tUJEUmzw5Nzcf8Qgo4bQ",[559,1604,2244],{"id":560,"title":561,"body":562,"brouillon":1592,"date":1593,"description":1594,"extension":58,"image":59,"imageAlt":59,"meta":1595,"navigation":61,"path":1596,"seo":1597,"stem":1598,"tags":1599,"__hash__":1603},"blog\u002Fblog\u002Fapp-fullstack-nuxt-nitro-prisma.md","Une app fullstack avec Nuxt : server routes Nitro et Prisma",{"type":9,"value":563,"toc":1582},[564,567,575,589,616,627,725,753,757,760,807,984,987,1010,1014,1021,1070,1077,1081,1157,1160,1293,1303,1307,1314,1552,1561,1565,1568,1572,1578],[17,565,566],{},"L'un des atouts les plus sous-estimés de Nuxt, c'est qu'il n'a pas besoin\nd'un backend séparé pour être fullstack. Le serveur est déjà là, sous le nom\nde Nitro. Ajoutez Prisma pour parler à une base de données, et vous tenez\nune application complète dans un seul dépôt, en TypeScript du front jusqu'à\nla requête SQL. Voici comment les pièces s'emboîtent.",[12,568,570,571],{"id":569},"le-dossier-server","Le dossier ",[572,573,574],"code",{},"server\u002F",[17,576,577,578,580,581,584,585,588],{},"Tout le code serveur vit dans ",[572,579,574],{},", à la racine du projet (hors de\n",[572,582,583],{},"app\u002F",", car il ne tourne jamais dans le navigateur). La convention la plus\nutile est ",[572,586,587],{},"server\u002Fapi\u002F"," : chaque fichier y devient un point d'accès HTTP.",[158,590,591,600,608],{},[161,592,593,596,597],{},[572,594,595],{},"server\u002Fapi\u002Farticles.get.ts"," répond à ",[572,598,599],{},"GET \u002Fapi\u002Farticles",[161,601,602,596,605],{},[572,603,604],{},"server\u002Fapi\u002Farticles.post.ts",[572,606,607],{},"POST \u002Fapi\u002Farticles",[161,609,610,596,613],{},[572,611,612],{},"server\u002Fapi\u002Farticles\u002F[id].get.ts",[572,614,615],{},"GET \u002Fapi\u002Farticles\u002F42",[17,617,618,619,622,623,626],{},"Le suffixe (",[572,620,621],{},".get",", ",[572,624,625],{},".post",") fixe la méthode HTTP, et l'arborescence fixe\nl'URL. Comme pour les pages : le disque décrit l'API.",[628,629,633],"pre",{"className":630,"code":631,"language":632,"meta":51,"style":51},"language-typescript shiki shiki-themes github-dark","\u002F\u002F server\u002Fapi\u002Fhello.get.ts\nexport default defineEventHandler((event) => {\n  const { nom } = getQuery(event)\n  return { message: `Bonjour ${nom ?? 'le monde'}` }\n})\n","typescript",[572,634,635,643,673,697,720],{"__ignoreMap":51},[636,637,639],"span",{"class":638,"line":62},"line",[636,640,642],{"class":641},"sAwPA","\u002F\u002F server\u002Fapi\u002Fhello.get.ts\n",[636,644,645,649,652,656,660,664,667,670],{"class":638,"line":52},[636,646,648],{"class":647},"snl16","export",[636,650,651],{"class":647}," default",[636,653,655],{"class":654},"svObZ"," defineEventHandler",[636,657,659],{"class":658},"s95oV","((",[636,661,663],{"class":662},"s9osk","event",[636,665,666],{"class":658},") ",[636,668,669],{"class":647},"=>",[636,671,672],{"class":658}," {\n",[636,674,675,678,681,685,688,691,694],{"class":638,"line":133},[636,676,677],{"class":647},"  const",[636,679,680],{"class":658}," { ",[636,682,684],{"class":683},"sDLfK","nom",[636,686,687],{"class":658}," } ",[636,689,690],{"class":647},"=",[636,692,693],{"class":654}," getQuery",[636,695,696],{"class":658},"(event)\n",[636,698,699,702,705,709,711,714,717],{"class":638,"line":190},[636,700,701],{"class":647},"  return",[636,703,704],{"class":658}," { message: ",[636,706,708],{"class":707},"sU2Wk","`Bonjour ${",[636,710,684],{"class":658},[636,712,713],{"class":647}," ??",[636,715,716],{"class":707}," 'le monde'}`",[636,718,719],{"class":658}," }\n",[636,721,722],{"class":638,"line":224},[636,723,724],{"class":658},"})\n",[17,726,727,730,731,733,734,737,738,741,742,622,745,748,749,752],{},[572,728,729],{},"defineEventHandler"," reçoit un ",[572,732,663],{}," (la requête), et ",[21,735,736],{},"ce que vous\nretournez devient la réponse JSON",". Pas de ",[572,739,740],{},"res.json()",", pas de\nsérialisation manuelle. Les utilitaires comme ",[572,743,744],{},"getQuery",[572,746,747],{},"readBody"," ou\n",[572,750,751],{},"getRouterParam"," sont auto-importés.",[12,754,756],{"id":755},"brancher-prisma","Brancher Prisma",[17,758,759],{},"Prisma est un ORM TypeScript : vous décrivez vos tables dans un schéma, il\ngénère un client typé et les migrations. Installation et schéma minimal :",[628,761,765],{"className":762,"code":763,"language":764,"meta":51,"style":51},"language-bash shiki shiki-themes github-dark","npm install -D prisma\nnpm install @prisma\u002Fclient\nnpx prisma init --datasource-provider sqlite\n","bash",[572,766,767,781,790],{"__ignoreMap":51},[636,768,769,772,775,778],{"class":638,"line":62},[636,770,771],{"class":654},"npm",[636,773,774],{"class":707}," install",[636,776,777],{"class":683}," -D",[636,779,780],{"class":707}," prisma\n",[636,782,783,785,787],{"class":638,"line":52},[636,784,771],{"class":654},[636,786,774],{"class":707},[636,788,789],{"class":707}," @prisma\u002Fclient\n",[636,791,792,795,798,801,804],{"class":638,"line":133},[636,793,794],{"class":654},"npx",[636,796,797],{"class":707}," prisma",[636,799,800],{"class":707}," init",[636,802,803],{"class":683}," --datasource-provider",[636,805,806],{"class":707}," sqlite\n",[628,808,812],{"className":809,"code":810,"language":811,"meta":51,"style":51},"language-prisma shiki shiki-themes github-dark","\u002F\u002F prisma\u002Fschema.prisma\ngenerator client {\n  provider = \"prisma-client-js\"\n}\n\ndatasource db {\n  provider = \"sqlite\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Article {\n  id      Int      @id @default(autoincrement())\n  titre   String\n  contenu String\n  publie  Boolean  @default(false)\n  cree_le DateTime @default(now())\n}\n","prisma",[572,813,814,819,829,839,844,849,859,868,887,891,895,905,927,935,942,961,979],{"__ignoreMap":51},[636,815,816],{"class":638,"line":62},[636,817,818],{"class":641},"\u002F\u002F prisma\u002Fschema.prisma\n",[636,820,821,824,827],{"class":638,"line":52},[636,822,823],{"class":647},"generator",[636,825,826],{"class":654}," client",[636,828,672],{"class":658},[636,830,831,834,836],{"class":638,"line":133},[636,832,833],{"class":658},"  provider ",[636,835,690],{"class":647},[636,837,838],{"class":707}," \"prisma-client-js\"\n",[636,840,841],{"class":638,"line":190},[636,842,843],{"class":658},"}\n",[636,845,846],{"class":638,"line":224},[636,847,848],{"emptyLinePlaceholder":61},"\n",[636,850,851,854,857],{"class":638,"line":274},[636,852,853],{"class":647},"datasource",[636,855,856],{"class":654}," db",[636,858,672],{"class":658},[636,860,861,863,865],{"class":638,"line":308},[636,862,833],{"class":658},[636,864,690],{"class":647},[636,866,867],{"class":707}," \"sqlite\"\n",[636,869,870,873,875,878,881,884],{"class":638,"line":341},[636,871,872],{"class":658},"  url      ",[636,874,690],{"class":647},[636,876,877],{"class":683}," env",[636,879,880],{"class":658},"(",[636,882,883],{"class":707},"\"DATABASE_URL\"",[636,885,886],{"class":658},")\n",[636,888,889],{"class":638,"line":375},[636,890,843],{"class":658},[636,892,893],{"class":638,"line":409},[636,894,848],{"emptyLinePlaceholder":61},[636,896,897,900,903],{"class":638,"line":446},[636,898,899],{"class":647},"model",[636,901,902],{"class":654}," Article",[636,904,672],{"class":658},[636,906,907,910,913,916,919,921,924],{"class":638,"line":480},[636,908,909],{"class":658},"  id      ",[636,911,912],{"class":683},"Int",[636,914,915],{"class":654},"      @id",[636,917,918],{"class":654}," @default",[636,920,880],{"class":658},[636,922,923],{"class":683},"autoincrement",[636,925,926],{"class":658},"())\n",[636,928,929,932],{"class":638,"line":518},[636,930,931],{"class":658},"  titre   ",[636,933,934],{"class":683},"String\n",[636,936,937,940],{"class":638,"line":550},[636,938,939],{"class":658},"  contenu ",[636,941,934],{"class":683},[636,943,945,948,951,954,956,959],{"class":638,"line":944},15,[636,946,947],{"class":658},"  publie  ",[636,949,950],{"class":683},"Boolean",[636,952,953],{"class":654},"  @default",[636,955,880],{"class":658},[636,957,958],{"class":683},"false",[636,960,886],{"class":658},[636,962,964,967,970,972,974,977],{"class":638,"line":963},16,[636,965,966],{"class":658},"  cree_le ",[636,968,969],{"class":683},"DateTime",[636,971,918],{"class":654},[636,973,880],{"class":658},[636,975,976],{"class":683},"now",[636,978,926],{"class":658},[636,980,982],{"class":638,"line":981},17,[636,983,843],{"class":658},[17,985,986],{},"On applique le schéma à la base et on génère le client :",[628,988,990],{"className":762,"code":989,"language":764,"meta":51,"style":51},"npx prisma migrate dev --name init\n",[572,991,992],{"__ignoreMap":51},[636,993,994,996,998,1001,1004,1007],{"class":638,"line":62},[636,995,794],{"class":654},[636,997,797],{"class":707},[636,999,1000],{"class":707}," migrate",[636,1002,1003],{"class":707}," dev",[636,1005,1006],{"class":683}," --name",[636,1008,1009],{"class":707}," init\n",[12,1011,1013],{"id":1012},"le-client-prisma-en-singleton","Le client Prisma en singleton",[17,1015,1016,1017,1020],{},"Piège classique : instancier ",[572,1018,1019],{},"new PrismaClient()"," dans chaque route ouvre\nune nouvelle connexion à chaque appel et épuise la base. On l'instancie une\nseule fois, dans un utilitaire serveur réutilisable :",[628,1022,1024],{"className":630,"code":1023,"language":632,"meta":51,"style":51},"\u002F\u002F server\u002Futils\u002Fprisma.ts\nimport { PrismaClient } from '@prisma\u002Fclient'\n\nexport const prisma = new PrismaClient()\n",[572,1025,1026,1031,1045,1049],{"__ignoreMap":51},[636,1027,1028],{"class":638,"line":62},[636,1029,1030],{"class":641},"\u002F\u002F server\u002Futils\u002Fprisma.ts\n",[636,1032,1033,1036,1039,1042],{"class":638,"line":52},[636,1034,1035],{"class":647},"import",[636,1037,1038],{"class":658}," { PrismaClient } ",[636,1040,1041],{"class":647},"from",[636,1043,1044],{"class":707}," '@prisma\u002Fclient'\n",[636,1046,1047],{"class":638,"line":133},[636,1048,848],{"emptyLinePlaceholder":61},[636,1050,1051,1053,1056,1058,1061,1064,1067],{"class":638,"line":190},[636,1052,648],{"class":647},[636,1054,1055],{"class":647}," const",[636,1057,797],{"class":683},[636,1059,1060],{"class":647}," =",[636,1062,1063],{"class":647}," new",[636,1065,1066],{"class":654}," PrismaClient",[636,1068,1069],{"class":658},"()\n",[17,1071,1072,1073,1076],{},"Placé dans ",[572,1074,1075],{},"server\u002Futils\u002F",", il est auto-importé dans toutes les routes\nserveur. Une instance, une réserve de connexions, partagée proprement.",[12,1078,1080],{"id":1079},"une-vraie-route-qui-lit-la-base","Une vraie route qui lit la base",[628,1082,1084],{"className":630,"code":1083,"language":632,"meta":51,"style":51},"\u002F\u002F server\u002Fapi\u002Farticles.get.ts\nexport default defineEventHandler(async () => {\n  return await prisma.article.findMany({\n    where: { publie: true },\n    orderBy: { cree_le: 'desc' }\n  })\n})\n",[572,1085,1086,1091,1111,1127,1138,1148,1153],{"__ignoreMap":51},[636,1087,1088],{"class":638,"line":62},[636,1089,1090],{"class":641},"\u002F\u002F server\u002Fapi\u002Farticles.get.ts\n",[636,1092,1093,1095,1097,1099,1101,1104,1107,1109],{"class":638,"line":52},[636,1094,648],{"class":647},[636,1096,651],{"class":647},[636,1098,655],{"class":654},[636,1100,880],{"class":658},[636,1102,1103],{"class":647},"async",[636,1105,1106],{"class":658}," () ",[636,1108,669],{"class":647},[636,1110,672],{"class":658},[636,1112,1113,1115,1118,1121,1124],{"class":638,"line":133},[636,1114,701],{"class":647},[636,1116,1117],{"class":647}," await",[636,1119,1120],{"class":658}," prisma.article.",[636,1122,1123],{"class":654},"findMany",[636,1125,1126],{"class":658},"({\n",[636,1128,1129,1132,1135],{"class":638,"line":190},[636,1130,1131],{"class":658},"    where: { publie: ",[636,1133,1134],{"class":683},"true",[636,1136,1137],{"class":658}," },\n",[636,1139,1140,1143,1146],{"class":638,"line":224},[636,1141,1142],{"class":658},"    orderBy: { cree_le: ",[636,1144,1145],{"class":707},"'desc'",[636,1147,719],{"class":658},[636,1149,1150],{"class":638,"line":274},[636,1151,1152],{"class":658},"  })\n",[636,1154,1155],{"class":638,"line":308},[636,1156,724],{"class":658},[17,1158,1159],{},"Et une route qui écrit, avec validation de l'entrée :",[628,1161,1163],{"className":630,"code":1162,"language":632,"meta":51,"style":51},"\u002F\u002F server\u002Fapi\u002Farticles.post.ts\nexport default defineEventHandler(async (event) => {\n  const body = await readBody(event)\n\n  if (!body?.titre || !body?.contenu) {\n    throw createError({ statusCode: 400, message: 'Titre et contenu requis' })\n  }\n\n  return await prisma.article.create({\n    data: { titre: body.titre, contenu: body.contenu }\n  })\n})\n",[572,1164,1165,1170,1193,1209,1213,1235,1258,1263,1267,1280,1285,1289],{"__ignoreMap":51},[636,1166,1167],{"class":638,"line":62},[636,1168,1169],{"class":641},"\u002F\u002F server\u002Fapi\u002Farticles.post.ts\n",[636,1171,1172,1174,1176,1178,1180,1182,1185,1187,1189,1191],{"class":638,"line":52},[636,1173,648],{"class":647},[636,1175,651],{"class":647},[636,1177,655],{"class":654},[636,1179,880],{"class":658},[636,1181,1103],{"class":647},[636,1183,1184],{"class":658}," (",[636,1186,663],{"class":662},[636,1188,666],{"class":658},[636,1190,669],{"class":647},[636,1192,672],{"class":658},[636,1194,1195,1197,1200,1202,1204,1207],{"class":638,"line":133},[636,1196,677],{"class":647},[636,1198,1199],{"class":683}," body",[636,1201,1060],{"class":647},[636,1203,1117],{"class":647},[636,1205,1206],{"class":654}," readBody",[636,1208,696],{"class":658},[636,1210,1211],{"class":638,"line":190},[636,1212,848],{"emptyLinePlaceholder":61},[636,1214,1215,1218,1220,1223,1226,1229,1232],{"class":638,"line":224},[636,1216,1217],{"class":647},"  if",[636,1219,1184],{"class":658},[636,1221,1222],{"class":647},"!",[636,1224,1225],{"class":658},"body?.titre ",[636,1227,1228],{"class":647},"||",[636,1230,1231],{"class":647}," !",[636,1233,1234],{"class":658},"body?.contenu) {\n",[636,1236,1237,1240,1243,1246,1249,1252,1255],{"class":638,"line":274},[636,1238,1239],{"class":647},"    throw",[636,1241,1242],{"class":654}," createError",[636,1244,1245],{"class":658},"({ statusCode: ",[636,1247,1248],{"class":683},"400",[636,1250,1251],{"class":658},", message: ",[636,1253,1254],{"class":707},"'Titre et contenu requis'",[636,1256,1257],{"class":658}," })\n",[636,1259,1260],{"class":638,"line":308},[636,1261,1262],{"class":658},"  }\n",[636,1264,1265],{"class":638,"line":341},[636,1266,848],{"emptyLinePlaceholder":61},[636,1268,1269,1271,1273,1275,1278],{"class":638,"line":375},[636,1270,701],{"class":647},[636,1272,1117],{"class":647},[636,1274,1120],{"class":658},[636,1276,1277],{"class":654},"create",[636,1279,1126],{"class":658},[636,1281,1282],{"class":638,"line":409},[636,1283,1284],{"class":658},"    data: { titre: body.titre, contenu: body.contenu }\n",[636,1286,1287],{"class":638,"line":446},[636,1288,1152],{"class":658},[636,1290,1291],{"class":638,"line":480},[636,1292,724],{"class":658},[17,1294,1295,1298,1299,1302],{},[572,1296,1297],{},"createError"," renvoie une vraie erreur HTTP, que le client saura interpréter\nplutôt que de recevoir un ",[572,1300,1301],{},"200"," avec un objet d'erreur déguisé.",[12,1304,1306],{"id":1305},"le-front-consomme-sa-propre-api","Le front consomme sa propre API",[17,1308,1309,1310,1313],{},"Côté page, on ne fait pas d'appel réseau exotique : la même application\nappelle sa route avec ",[572,1311,1312],{},"useFetch",". L'URL est relative, il n'y a pas de\nproblème de CORS, et le rendu serveur pré-remplit déjà les données.",[628,1315,1319],{"className":1316,"code":1317,"language":1318,"meta":51,"style":51},"language-vue shiki shiki-themes github-dark","\u003Cscript setup lang=\"ts\">\nconst { data: articles, refresh } = await useFetch('\u002Fapi\u002Farticles')\n\nasync function publier(nouvel: { titre: string; contenu: string }) {\n  await $fetch('\u002Fapi\u002Farticles', { method: 'POST', body: nouvel })\n  await refresh()\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Carticle v-for=\"a in articles\" :key=\"a.id\">\n    \u003Ch2>{{ a.titre }}\u003C\u002Fh2>\n    \u003Cp>{{ a.contenu }}\u003C\u002Fp>\n  \u003C\u002Farticle>\n\u003C\u002Ftemplate>\n","vue",[572,1320,1321,1344,1381,1385,1426,1447,1456,1460,1469,1473,1482,1508,1522,1535,1544],{"__ignoreMap":51},[636,1322,1323,1326,1330,1333,1336,1338,1341],{"class":638,"line":62},[636,1324,1325],{"class":658},"\u003C",[636,1327,1329],{"class":1328},"s4JwU","script",[636,1331,1332],{"class":654}," setup",[636,1334,1335],{"class":654}," lang",[636,1337,690],{"class":658},[636,1339,1340],{"class":707},"\"ts\"",[636,1342,1343],{"class":658},">\n",[636,1345,1346,1349,1351,1354,1357,1360,1362,1365,1367,1369,1371,1374,1376,1379],{"class":638,"line":52},[636,1347,1348],{"class":647},"const",[636,1350,680],{"class":658},[636,1352,1353],{"class":662},"data",[636,1355,1356],{"class":658},": ",[636,1358,1359],{"class":683},"articles",[636,1361,622],{"class":658},[636,1363,1364],{"class":683},"refresh",[636,1366,687],{"class":658},[636,1368,690],{"class":647},[636,1370,1117],{"class":647},[636,1372,1373],{"class":654}," useFetch",[636,1375,880],{"class":658},[636,1377,1378],{"class":707},"'\u002Fapi\u002Farticles'",[636,1380,886],{"class":658},[636,1382,1383],{"class":638,"line":133},[636,1384,848],{"emptyLinePlaceholder":61},[636,1386,1387,1389,1392,1395,1397,1400,1403,1405,1408,1410,1413,1416,1419,1421,1423],{"class":638,"line":190},[636,1388,1103],{"class":647},[636,1390,1391],{"class":647}," function",[636,1393,1394],{"class":654}," publier",[636,1396,880],{"class":658},[636,1398,1399],{"class":662},"nouvel",[636,1401,1402],{"class":647},":",[636,1404,680],{"class":658},[636,1406,1407],{"class":662},"titre",[636,1409,1402],{"class":647},[636,1411,1412],{"class":683}," string",[636,1414,1415],{"class":658},"; ",[636,1417,1418],{"class":662},"contenu",[636,1420,1402],{"class":647},[636,1422,1412],{"class":683},[636,1424,1425],{"class":658}," }) {\n",[636,1427,1428,1431,1434,1436,1438,1441,1444],{"class":638,"line":224},[636,1429,1430],{"class":647},"  await",[636,1432,1433],{"class":654}," $fetch",[636,1435,880],{"class":658},[636,1437,1378],{"class":707},[636,1439,1440],{"class":658},", { method: ",[636,1442,1443],{"class":707},"'POST'",[636,1445,1446],{"class":658},", body: nouvel })\n",[636,1448,1449,1451,1454],{"class":638,"line":274},[636,1450,1430],{"class":647},[636,1452,1453],{"class":654}," refresh",[636,1455,1069],{"class":658},[636,1457,1458],{"class":638,"line":308},[636,1459,843],{"class":658},[636,1461,1462,1465,1467],{"class":638,"line":341},[636,1463,1464],{"class":658},"\u003C\u002F",[636,1466,1329],{"class":1328},[636,1468,1343],{"class":658},[636,1470,1471],{"class":638,"line":375},[636,1472,848],{"emptyLinePlaceholder":61},[636,1474,1475,1477,1480],{"class":638,"line":409},[636,1476,1325],{"class":658},[636,1478,1479],{"class":1328},"template",[636,1481,1343],{"class":658},[636,1483,1484,1487,1490,1493,1495,1498,1501,1503,1506],{"class":638,"line":446},[636,1485,1486],{"class":658},"  \u003C",[636,1488,1489],{"class":1328},"article",[636,1491,1492],{"class":654}," v-for",[636,1494,690],{"class":658},[636,1496,1497],{"class":707},"\"a in articles\"",[636,1499,1500],{"class":654}," :key",[636,1502,690],{"class":658},[636,1504,1505],{"class":707},"\"a.id\"",[636,1507,1343],{"class":658},[636,1509,1510,1513,1515,1518,1520],{"class":638,"line":480},[636,1511,1512],{"class":658},"    \u003C",[636,1514,12],{"class":1328},[636,1516,1517],{"class":658},">{{ a.titre }}\u003C\u002F",[636,1519,12],{"class":1328},[636,1521,1343],{"class":658},[636,1523,1524,1526,1528,1531,1533],{"class":638,"line":518},[636,1525,1512],{"class":658},[636,1527,17],{"class":1328},[636,1529,1530],{"class":658},">{{ a.contenu }}\u003C\u002F",[636,1532,17],{"class":1328},[636,1534,1343],{"class":658},[636,1536,1537,1540,1542],{"class":638,"line":550},[636,1538,1539],{"class":658},"  \u003C\u002F",[636,1541,1489],{"class":1328},[636,1543,1343],{"class":658},[636,1545,1546,1548,1550],{"class":638,"line":944},[636,1547,1464],{"class":658},[636,1549,1479],{"class":1328},[636,1551,1343],{"class":658},[17,1553,1554,1556,1557,1560],{},[572,1555,1312],{}," pour lire au rendu (avec cache et hydratation), ",[572,1558,1559],{},"$fetch"," pour les\nactions ponctuelles comme un POST. La boucle est complète : la page lit une\nroute Nitro, qui interroge Prisma, qui parle à la base, et tout est typé du\nmême bout à l'autre.",[12,1562,1564],{"id":1563},"ce-que-ça-change-au-déploiement","Ce que ça change au déploiement",[17,1566,1567],{},"Attention : dès qu'il y a des routes serveur et une base, on quitte le monde\npurement statique. Il faut une cible capable d'exécuter du Node (un VPS, une\nplateforme applicative, une fonction serverless), et non plus un simple\nhébergement de fichiers. Nitro sait produire la bonne sortie pour chacune de\nces cibles, mais le choix d'hébergement, lui, n'est plus le même que pour un\nsite statique. C'est le prix, assumé, du fullstack.",[12,1569,1571],{"id":1570},"la-leçon","La leçon",[17,1573,1574,1575,1577],{},"Nuxt fullstack, ce n'est pas « Nuxt plus un backend » : c'est un seul\nprojet, un seul langage, une seule chaîne de types, du composant Vue jusqu'à\nla table SQL. Nitro fournit le serveur, Prisma la base, et ",[572,1576,1312],{}," relie\nles deux sans couture. Pour un produit mené en petite équipe, cette unité\nest un avantage décisif : moins de code de plomberie, moins de contrats\nd'API à maintenir, et le remboursement se fait sur chaque fonctionnalité.",[1579,1580,1581],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#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 pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}",{"title":51,"searchDepth":52,"depth":52,"links":1583},[1584,1586,1587,1588,1589,1590,1591],{"id":569,"depth":52,"text":1585},"Le dossier server\u002F",{"id":755,"depth":52,"text":756},{"id":1012,"depth":52,"text":1013},{"id":1079,"depth":52,"text":1080},{"id":1305,"depth":52,"text":1306},{"id":1563,"depth":52,"text":1564},{"id":1570,"depth":52,"text":1571},false,"2026-07-18","Construire l'API dans le même projet que le front : les routes serveur de Nitro, Prisma pour la base de données, et useFetch côté client. Un seul dépôt, une seule langue.",{},"\u002Fblog\u002Fapp-fullstack-nuxt-nitro-prisma",{"title":561,"description":1594},"blog\u002Fapp-fullstack-nuxt-nitro-prisma",[1600,1601,1602],"Nuxt","Prisma","Fullstack","t2G8Y9IU16PitwCOlzmh_bflZslEo8SfxVkYuzq7oA4",{"id":1605,"title":1606,"body":1607,"brouillon":1592,"date":2235,"description":2236,"extension":58,"image":59,"imageAlt":59,"meta":2237,"navigation":61,"path":2238,"seo":2239,"stem":2240,"tags":2241,"__hash__":2243},"blog\u002Fblog\u002Fgestion-etat-nuxt-vs-flutter.md","Gestion d'état : Nuxt face à Flutter, le parallèle qui éclaire",{"type":9,"value":1608,"toc":2227},[1609,1612,1616,1619,1634,1664,1671,1675,1742,1792,1809,1813,1919,1923,1926,2073,2076,2192,2203,2207,2216,2219,2221,2224],[17,1610,1611],{},"Je travaille des deux côtés : des applications Flutter au long cours et des\nprojets web sous Nuxt. La bonne nouvelle pour qui connaît déjà l'un, c'est\nque les concepts de gestion d'état de l'autre ne sont pas nouveaux : ce sont\nles mêmes idées sous d'autres noms. Voici la table de correspondance que\nj'aurais aimé avoir en tête plus tôt.",[12,1613,1615],{"id":1614},"le-socle-réactivité-contre-notification","Le socle : réactivité contre notification",[17,1617,1618],{},"La différence de fond tient au modèle de réactivité.",[17,1620,1621,1622,1625,1626,1629,1630,1633],{},"En ",[21,1623,1624],{},"Vue\u002FNuxt",", la réactivité est implicite et automatique. Un ",[572,1627,1628],{},"ref"," est\nune valeur observable, et tout ce qui la lit se réabonne tout seul. Vous\nchangez ",[572,1631,1632],{},"compteur.value",", l'interface se met à jour, sans rien déclarer.",[17,1635,1621,1636,1639,1640,1643,1644,1647,1648,1651,1652,1655,1656,1659,1660,1663],{},[21,1637,1638],{},"Flutter",", la réactivité est explicite. Un ",[572,1641,1642],{},"ValueNotifier"," détient une\nvaleur, mais il faut appeler ",[572,1645,1646],{},"notifyListeners()"," (ou assigner ",[572,1649,1650],{},".value",") pour\nprévenir, et un widget doit s'abonner explicitement (via ",[572,1653,1654],{},"ValueListenableBuilder",",\n",[572,1657,1658],{},"ListenableBuilder",", ou le ",[572,1661,1662],{},"watch"," de Riverpod).",[17,1665,1666,1667,1670],{},"Retenez cette phrase : ",[21,1668,1669],{},"Vue observe, Flutter notifie",". Tout le reste en\ndécoule.",[12,1672,1674],{"id":1673},"la-brique-de-base","La brique de base",[628,1676,1678],{"className":630,"code":1677,"language":632,"meta":51,"style":51},"\u002F\u002F Nuxt \u002F Vue\nconst compteur = ref(0)\nconst doubler = computed(() => compteur.value * 2)\ncompteur.value++   \u002F\u002F l'UI qui lit compteur se met à jour\n",[572,1679,1680,1685,1704,1732],{"__ignoreMap":51},[636,1681,1682],{"class":638,"line":62},[636,1683,1684],{"class":641},"\u002F\u002F Nuxt \u002F Vue\n",[636,1686,1687,1689,1692,1694,1697,1699,1702],{"class":638,"line":52},[636,1688,1348],{"class":647},[636,1690,1691],{"class":683}," compteur",[636,1693,1060],{"class":647},[636,1695,1696],{"class":654}," ref",[636,1698,880],{"class":658},[636,1700,1701],{"class":683},"0",[636,1703,886],{"class":658},[636,1705,1706,1708,1711,1713,1716,1719,1721,1724,1727,1730],{"class":638,"line":133},[636,1707,1348],{"class":647},[636,1709,1710],{"class":683}," doubler",[636,1712,1060],{"class":647},[636,1714,1715],{"class":654}," computed",[636,1717,1718],{"class":658},"(() ",[636,1720,669],{"class":647},[636,1722,1723],{"class":658}," compteur.value ",[636,1725,1726],{"class":647},"*",[636,1728,1729],{"class":683}," 2",[636,1731,886],{"class":658},[636,1733,1734,1736,1739],{"class":638,"line":190},[636,1735,1632],{"class":658},[636,1737,1738],{"class":647},"++",[636,1740,1741],{"class":641},"   \u002F\u002F l'UI qui lit compteur se met à jour\n",[628,1743,1747],{"className":1744,"code":1745,"language":1746,"meta":51,"style":51},"language-dart shiki shiki-themes github-dark","\u002F\u002F Flutter\nfinal compteur = ValueNotifier\u003Cint>(0);\ncompteur.value++;  \u002F\u002F les ValueListenableBuilder abonnés se reconstruisent\n","dart",[572,1748,1749,1754,1780],{"__ignoreMap":51},[636,1750,1751],{"class":638,"line":62},[636,1752,1753],{"class":641},"\u002F\u002F Flutter\n",[636,1755,1756,1759,1762,1764,1767,1769,1772,1775,1777],{"class":638,"line":52},[636,1757,1758],{"class":647},"final",[636,1760,1761],{"class":658}," compteur ",[636,1763,690],{"class":647},[636,1765,1766],{"class":683}," ValueNotifier",[636,1768,1325],{"class":658},[636,1770,1771],{"class":683},"int",[636,1773,1774],{"class":658},">(",[636,1776,1701],{"class":683},[636,1778,1779],{"class":658},");\n",[636,1781,1782,1784,1786,1789],{"class":638,"line":133},[636,1783,1632],{"class":658},[636,1785,1738],{"class":647},[636,1787,1788],{"class":658},";  ",[636,1790,1791],{"class":641},"\u002F\u002F les ValueListenableBuilder abonnés se reconstruisent\n",[17,1793,1794,1795,1797,1798,1800,1801,1804,1805,1808],{},"Le ",[572,1796,1628],{}," de Vue correspond au ",[572,1799,1642],{}," de Flutter : une valeur\nobservable. Le ",[572,1802,1803],{},"computed"," de Vue, une valeur dérivée qui se recalcule seule,\ncorrespond à un ",[572,1806,1807],{},"Provider"," dérivé de Riverpod ou à un getter recalculé. Dans\nles deux mondes, on ne recopie jamais une valeur dérivée à la main : on la\ndéclare comme dépendant d'une autre.",[12,1810,1812],{"id":1811},"la-table-de-correspondance","La table de correspondance",[158,1814,1815,1835,1850,1870,1888,1902],{},[161,1816,1817,1825,1826,1834],{},[21,1818,1819,1821,1822],{},[572,1820,1628],{}," \u002F ",[572,1823,1824],{},"reactive"," (Vue) correspond à ",[21,1827,1828,1830,1831],{},[572,1829,1642],{}," \u002F\n",[572,1832,1833],{},"ChangeNotifier"," (Flutter) : la valeur observable de base.",[161,1836,1837,1841,1842,1845,1846,1849],{},[21,1838,1839],{},[572,1840,1803],{}," correspond à un ",[21,1843,1844],{},"provider dérivé"," (Riverpod) ou un\n",[21,1847,1848],{},"getter"," recalculé : l'état dérivé.",[161,1851,1852,1859,1860,1865,1866,1869],{},[21,1853,1854,1821,1856],{},[572,1855,1662],{},[572,1857,1858],{},"watchEffect"," correspond à ",[21,1861,1862],{},[572,1863,1864],{},"ref.listen"," (Riverpod) ou à\nl'écoute d'un ",[572,1867,1868],{},"Listenable"," : réagir à un changement pour déclencher un\neffet.",[161,1871,1872,1877,1878,1883,1884,1887],{},[21,1873,1874],{},[572,1875,1876],{},"useState"," (état partagé Nuxt) correspond à un ",[21,1879,1880,1882],{},[572,1881,1807],{}," global","\n(Riverpod) ou à un ",[572,1885,1886],{},"ChangeNotifierProvider"," (Provider) : un état accessible\ndepuis plusieurs endroits.",[161,1889,1890,1893,1894,1897,1898,1901],{},[21,1891,1892],{},"Pinia"," (store avec state, actions, getters) correspond à ",[21,1895,1896],{},"Riverpod\nstructuré"," ou à un ensemble de ",[21,1899,1900],{},"Cubit\u002FBLoC"," : le domaine métier avec sa\nlogique.",[161,1903,1904,1797,1913,1918],{},[21,1905,1906,1907,1821,1910],{},"L'injection par ",[572,1908,1909],{},"provide",[572,1911,1912],{},"inject",[21,1914,1915,1917],{},[572,1916,1807],{},"\nd'arbre de widgets"," de Flutter : fournir une dépendance à un sous-arbre.",[12,1920,1922],{"id":1921},"un-store-deux-écritures","Un store, deux écritures",[17,1924,1925],{},"Un panier, côté Pinia :",[628,1927,1929],{"className":630,"code":1928,"language":632,"meta":51,"style":51},"\u002F\u002F stores\u002Fpanier.ts\nexport const usePanier = defineStore('panier', () => {\n  const articles = ref\u003CArticle[]>([])\n  const total = computed(() =>\n    articles.value.reduce((s, a) => s + a.prix, 0)\n  )\n  function ajouter(a: Article) { articles.value.push(a) }\n  return { articles, total, ajouter }\n})\n",[572,1930,1931,1936,1962,1981,1997,2032,2037,2062,2069],{"__ignoreMap":51},[636,1932,1933],{"class":638,"line":62},[636,1934,1935],{"class":641},"\u002F\u002F stores\u002Fpanier.ts\n",[636,1937,1938,1940,1942,1945,1947,1950,1952,1955,1958,1960],{"class":638,"line":52},[636,1939,648],{"class":647},[636,1941,1055],{"class":647},[636,1943,1944],{"class":683}," usePanier",[636,1946,1060],{"class":647},[636,1948,1949],{"class":654}," defineStore",[636,1951,880],{"class":658},[636,1953,1954],{"class":707},"'panier'",[636,1956,1957],{"class":658},", () ",[636,1959,669],{"class":647},[636,1961,672],{"class":658},[636,1963,1964,1966,1969,1971,1973,1975,1978],{"class":638,"line":133},[636,1965,677],{"class":647},[636,1967,1968],{"class":683}," articles",[636,1970,1060],{"class":647},[636,1972,1696],{"class":654},[636,1974,1325],{"class":658},[636,1976,1977],{"class":654},"Article",[636,1979,1980],{"class":658},"[]>([])\n",[636,1982,1983,1985,1988,1990,1992,1994],{"class":638,"line":190},[636,1984,677],{"class":647},[636,1986,1987],{"class":683}," total",[636,1989,1060],{"class":647},[636,1991,1715],{"class":654},[636,1993,1718],{"class":658},[636,1995,1996],{"class":647},"=>\n",[636,1998,1999,2002,2005,2007,2010,2012,2015,2017,2019,2022,2025,2028,2030],{"class":638,"line":224},[636,2000,2001],{"class":658},"    articles.value.",[636,2003,2004],{"class":654},"reduce",[636,2006,659],{"class":658},[636,2008,2009],{"class":662},"s",[636,2011,622],{"class":658},[636,2013,2014],{"class":662},"a",[636,2016,666],{"class":658},[636,2018,669],{"class":647},[636,2020,2021],{"class":658}," s ",[636,2023,2024],{"class":647},"+",[636,2026,2027],{"class":658}," a.prix, ",[636,2029,1701],{"class":683},[636,2031,886],{"class":658},[636,2033,2034],{"class":638,"line":274},[636,2035,2036],{"class":658},"  )\n",[636,2038,2039,2042,2045,2047,2049,2051,2053,2056,2059],{"class":638,"line":308},[636,2040,2041],{"class":647},"  function",[636,2043,2044],{"class":654}," ajouter",[636,2046,880],{"class":658},[636,2048,2014],{"class":662},[636,2050,1402],{"class":647},[636,2052,902],{"class":654},[636,2054,2055],{"class":658},") { articles.value.",[636,2057,2058],{"class":654},"push",[636,2060,2061],{"class":658},"(a) }\n",[636,2063,2064,2066],{"class":638,"line":341},[636,2065,701],{"class":647},[636,2067,2068],{"class":658}," { articles, total, ajouter }\n",[636,2070,2071],{"class":638,"line":375},[636,2072,724],{"class":658},[17,2074,2075],{},"Le même panier, côté Cubit (flutter_bloc) :",[628,2077,2079],{"className":1744,"code":2078,"language":1746,"meta":51,"style":51},"class PanierCubit extends Cubit\u003CList\u003CArticle>> {\n  PanierCubit() : super([]);\n\n  int get total => state.fold(0, (s, a) => s + a.prix);\n\n  void ajouter(Article a) => emit([...state, a]);\n}\n",[572,2080,2081,2107,2123,2127,2162,2166,2188],{"__ignoreMap":51},[636,2082,2083,2086,2089,2092,2095,2097,2100,2102,2104],{"class":638,"line":62},[636,2084,2085],{"class":647},"class",[636,2087,2088],{"class":683}," PanierCubit",[636,2090,2091],{"class":647}," extends",[636,2093,2094],{"class":683}," Cubit",[636,2096,1325],{"class":658},[636,2098,2099],{"class":683},"List",[636,2101,1325],{"class":658},[636,2103,1977],{"class":683},[636,2105,2106],{"class":658},">> {\n",[636,2108,2109,2112,2115,2117,2120],{"class":638,"line":52},[636,2110,2111],{"class":683},"  PanierCubit",[636,2113,2114],{"class":658},"() ",[636,2116,1402],{"class":647},[636,2118,2119],{"class":683}," super",[636,2121,2122],{"class":658},"([]);\n",[636,2124,2125],{"class":638,"line":133},[636,2126,848],{"emptyLinePlaceholder":61},[636,2128,2129,2132,2135,2138,2140,2143,2146,2148,2150,2153,2155,2157,2159],{"class":638,"line":190},[636,2130,2131],{"class":683},"  int",[636,2133,2134],{"class":647}," get",[636,2136,2137],{"class":658}," total ",[636,2139,669],{"class":647},[636,2141,2142],{"class":658}," state.",[636,2144,2145],{"class":654},"fold",[636,2147,880],{"class":658},[636,2149,1701],{"class":683},[636,2151,2152],{"class":658},", (s, a) ",[636,2154,669],{"class":647},[636,2156,2021],{"class":658},[636,2158,2024],{"class":647},[636,2160,2161],{"class":658}," a.prix);\n",[636,2163,2164],{"class":638,"line":224},[636,2165,848],{"emptyLinePlaceholder":61},[636,2167,2168,2171,2173,2175,2177,2180,2182,2185],{"class":638,"line":274},[636,2169,2170],{"class":647},"  void",[636,2172,2044],{"class":654},[636,2174,880],{"class":658},[636,2176,1977],{"class":683},[636,2178,2179],{"class":658}," a) ",[636,2181,669],{"class":647},[636,2183,2184],{"class":654}," emit",[636,2186,2187],{"class":658},"([...state, a]);\n",[636,2189,2190],{"class":638,"line":308},[636,2191,843],{"class":658},[17,2193,2194,2195,2198,2199,2202],{},"La forme diffère (Pinia expose un objet réactif, le Cubit émet un nouvel\nétat immuable), mais la structure est identique : un état, une valeur\ndérivée (",[572,2196,2197],{},"total","), une action (",[572,2200,2201],{},"ajouter","). Un développeur Flutter lit le\nstore Pinia sans effort, et réciproquement.",[12,2204,2206],{"id":2205},"la-dimension-que-flutter-na-pas-le-serveur","La dimension que Flutter n'a pas : le serveur",[17,2208,2209,2210,2212,2213,2215],{},"Il y a un endroit où le parallèle s'arrête, et c'est important. Nuxt fait du\nrendu serveur : le même code de gestion d'état s'exécute côté serveur puis\ncôté client. D'où la nécessité de ",[572,2211,1876],{}," plutôt qu'un simple ",[572,2214,1628],{},"\nglobal, pour isoler l'état par requête et éviter qu'un visiteur voie les\ndonnées d'un autre (j'en parle dans l'article sur les bonnes pratiques).",[17,2217,2218],{},"Flutter n'a pas ce problème : une application tourne sur un seul appareil,\npour un seul utilisateur. Un singleton d'état y est parfaitement légitime.\nEn Nuxt, le même singleton est un bug de sécurité. C'est la principale\nadaptation mentale à faire en passant de Flutter au web rendu côté serveur.",[12,2220,1571],{"id":1570},[17,2222,2223],{},"Ne réapprenez pas la gestion d'état, traduisez-la. « Observer une valeur »,\n« en dériver une autre », « réagir à un changement », « partager un\ndomaine » : ces quatre gestes existent des deux côtés, avec des noms\ndifférents. Gardez en tête que Vue observe quand Flutter notifie, et\nn'oubliez jamais la dimension serveur de Nuxt, absente de Flutter. Avec ces\ndeux repères, passer d'un monde à l'autre devient une simple traduction, pas\nun nouvel apprentissage.",[1579,2225,2226],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}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 pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":51,"searchDepth":52,"depth":52,"links":2228},[2229,2230,2231,2232,2233,2234],{"id":1614,"depth":52,"text":1615},{"id":1673,"depth":52,"text":1674},{"id":1811,"depth":52,"text":1812},{"id":1921,"depth":52,"text":1922},{"id":2205,"depth":52,"text":2206},{"id":1570,"depth":52,"text":1571},"2026-07-16","Pour un développeur Flutter qui apprend Nuxt (ou l'inverse) : la correspondance entre ref, computed, useState, Pinia et ValueNotifier, Provider, Riverpod, Bloc.",{},"\u002Fblog\u002Fgestion-etat-nuxt-vs-flutter",{"title":1606,"description":2236},"blog\u002Fgestion-etat-nuxt-vs-flutter",[1600,1638,2242],"Gestion d'état","scuvq3Ifi-uadXy9ziFqGIkbxMqEKu9B-vLJ-MLkUsU",{"id":2245,"title":2246,"body":2247,"brouillon":1592,"date":2621,"description":2622,"extension":58,"image":59,"imageAlt":59,"meta":2623,"navigation":61,"path":2624,"seo":2625,"stem":2626,"tags":2627,"__hash__":2628},"blog\u002Fblog\u002Fgestion-etat-nuxt-bonnes-pratiques.md","Gestion d'état dans Nuxt : useState, bonnes pratiques et pièges",{"type":9,"value":2248,"toc":2610},[2249,2255,2262,2304,2317,2323,2333,2380,2391,2398,2408,2424,2489,2495,2499,2508,2526,2536,2540,2593,2595,2607],[17,2250,2251,2252,2254],{},"En Vue côté client, on partage souvent un état en déclarant un ",[572,2253,1628],{}," dans un\nmodule et en l'important où on veut. Ça marche très bien dans le navigateur,\net ça casse silencieusement dès qu'on passe au rendu serveur. Comprendre\npourquoi, c'est comprendre toute la gestion d'état de Nuxt.",[12,2256,2258,2259,2261],{"id":2257},"le-piège-du-ref-partagé-en-module","Le piège du ",[572,2260,1628],{}," partagé en module",[628,2263,2265],{"className":630,"code":2264,"language":632,"meta":51,"style":51},"\u002F\u002F state.ts  - À NE PAS FAIRE en SSR\nimport { ref } from 'vue'\nexport const utilisateur = ref(null)\n",[572,2266,2267,2272,2284],{"__ignoreMap":51},[636,2268,2269],{"class":638,"line":62},[636,2270,2271],{"class":641},"\u002F\u002F state.ts  - À NE PAS FAIRE en SSR\n",[636,2273,2274,2276,2279,2281],{"class":638,"line":52},[636,2275,1035],{"class":647},[636,2277,2278],{"class":658}," { ref } ",[636,2280,1041],{"class":647},[636,2282,2283],{"class":707}," 'vue'\n",[636,2285,2286,2288,2290,2293,2295,2297,2299,2302],{"class":638,"line":133},[636,2287,648],{"class":647},[636,2289,1055],{"class":647},[636,2291,2292],{"class":683}," utilisateur",[636,2294,1060],{"class":647},[636,2296,1696],{"class":654},[636,2298,880],{"class":658},[636,2300,2301],{"class":683},"null",[636,2303,886],{"class":658},[17,2305,2306,2307,2309,2310,2313,2314,2316],{},"Côté client, ce ",[572,2308,1628],{}," est unique : un seul navigateur, un seul état. Côté\nserveur, le processus Node est ",[21,2311,2312],{},"partagé entre tous les visiteurs",". Ce\n",[572,2315,1628],{}," unique devient alors un état commun à toutes les requêtes\nsimultanées : l'utilisateur A voit les données de l'utilisateur B. C'est une\nfuite de données inter-requêtes, et elle est très difficile à repérer parce\nqu'elle n'apparaît pas en développement local avec un seul onglet.",[12,2318,2320,2322],{"id":2319},"usestate-létat-partagé-fait-pour-le-ssr",[572,2321,1876],{}," : l'état partagé fait pour le SSR",[17,2324,2325,2326,2328,2329,2332],{},"La réponse de Nuxt est ",[572,2327,1876],{},". Il crée un état réactif ",[21,2330,2331],{},"isolé par\nrequête"," côté serveur, et correctement transféré (sérialisé puis hydraté)\nvers le client :",[628,2334,2336],{"className":630,"code":2335,"language":632,"meta":51,"style":51},"\u002F\u002F app\u002Fcomposables\u002FuseUtilisateur.ts\nexport function useUtilisateur() {\n  return useState('utilisateur', () => null)\n}\n",[572,2337,2338,2343,2355,2376],{"__ignoreMap":51},[636,2339,2340],{"class":638,"line":62},[636,2341,2342],{"class":641},"\u002F\u002F app\u002Fcomposables\u002FuseUtilisateur.ts\n",[636,2344,2345,2347,2349,2352],{"class":638,"line":52},[636,2346,648],{"class":647},[636,2348,1391],{"class":647},[636,2350,2351],{"class":654}," useUtilisateur",[636,2353,2354],{"class":658},"() {\n",[636,2356,2357,2359,2362,2364,2367,2369,2371,2374],{"class":638,"line":133},[636,2358,701],{"class":647},[636,2360,2361],{"class":654}," useState",[636,2363,880],{"class":658},[636,2365,2366],{"class":707},"'utilisateur'",[636,2368,1957],{"class":658},[636,2370,669],{"class":647},[636,2372,2373],{"class":683}," null",[636,2375,886],{"class":658},[636,2377,2378],{"class":638,"line":190},[636,2379,843],{"class":658},[17,2381,2382,2383,2386,2387,2390],{},"Deux détails comptent. Le ",[21,2384,2385],{},"premier argument est une clé"," unique : deux\nappels avec la même clé partagent le même état, c'est le mécanisme de\npartage. Le ",[21,2388,2389],{},"second argument est une fonction"," d'initialisation, pas une\nvaleur directe : elle n'est appelée qu'une fois, au bon moment du cycle SSR.",[17,2392,2393,2394,2397],{},"L'envelopper dans un composable ",[572,2395,2396],{},"useUtilisateur()"," évite de répéter la clé\npartout et centralise le point d'accès. Si un jour la clé change, un seul\nfichier bouge.",[12,2399,2401,2403,2404,2407],{"id":2400},"usefetch-et-useasyncdata-létat-cest-aussi-les-données",[572,2402,1312],{}," et ",[572,2405,2406],{},"useAsyncData"," : l'état, c'est aussi les données",[17,2409,2410,2411,2413,2414,2413,2417,2420,2421,2423],{},"La majorité de l'état d'une application, ce sont des données distantes. Ne\nles gérez pas à la main avec un ",[572,2412,1628],{}," plus un ",[572,2415,2416],{},"onMounted",[572,2418,2419],{},"try\u002Fcatch",".\n",[572,2422,1312],{}," fait tout cela, et de façon compatible SSR :",[628,2425,2427],{"className":1316,"code":2426,"language":1318,"meta":51,"style":51},"\u003Cscript setup lang=\"ts\">\nconst { data: articles, status, error } = await useFetch('\u002Fapi\u002Farticles')\n\u003C\u002Fscript>\n",[572,2428,2429,2445,2481],{"__ignoreMap":51},[636,2430,2431,2433,2435,2437,2439,2441,2443],{"class":638,"line":62},[636,2432,1325],{"class":658},[636,2434,1329],{"class":1328},[636,2436,1332],{"class":654},[636,2438,1335],{"class":654},[636,2440,690],{"class":658},[636,2442,1340],{"class":707},[636,2444,1343],{"class":658},[636,2446,2447,2449,2451,2453,2455,2457,2459,2462,2464,2467,2469,2471,2473,2475,2477,2479],{"class":638,"line":52},[636,2448,1348],{"class":647},[636,2450,680],{"class":658},[636,2452,1353],{"class":662},[636,2454,1356],{"class":658},[636,2456,1359],{"class":683},[636,2458,622],{"class":658},[636,2460,2461],{"class":683},"status",[636,2463,622],{"class":658},[636,2465,2466],{"class":683},"error",[636,2468,687],{"class":658},[636,2470,690],{"class":647},[636,2472,1117],{"class":647},[636,2474,1373],{"class":654},[636,2476,880],{"class":658},[636,2478,1378],{"class":707},[636,2480,886],{"class":658},[636,2482,2483,2485,2487],{"class":638,"line":133},[636,2484,1464],{"class":658},[636,2486,1329],{"class":1328},[636,2488,1343],{"class":658},[17,2490,2491,2492,2494],{},"Il récupère les données côté serveur au premier rendu (donc le HTML arrive\ndéjà rempli, bon pour le SEO), les transfère au client sans refetch, et\ngère le cache et la déduplication via une clé. Deux composants qui appellent\n",[572,2493,1312],{}," avec la même URL ne déclenchent qu'une seule requête. Traiter les\ndonnées comme un état géré par le framework, plutôt que comme du code\nimpératif, élimine une classe entière de bugs.",[12,2496,2498],{"id":2497},"quand-sortir-pinia","Quand sortir Pinia",[17,2500,2501,2503,2504,2507],{},[572,2502,1876],{}," couvre l'état simple et partagé. Passez à Pinia (le store\nofficiel de l'écosystème Vue) quand l'état devient un ",[21,2505,2506],{},"domaine"," à part\nentière :",[158,2509,2510,2513,2520,2523],{},[161,2511,2512],{},"de la logique métier autour de l'état (actions, règles, validations) et\npas seulement une valeur ;",[161,2514,2515,2516,2519],{},"des valeurs dérivées nombreuses (des ",[572,2517,2518],{},"getters"," qui se recalculent) ;",[161,2521,2522],{},"un besoin de structurer par modules (panier, session, préférences) avec\nchacun son état, ses actions, ses getters ;",[161,2524,2525],{},"l'envie d'outils de débogage dédiés et d'un point d'entrée testable\nisolément.",[17,2527,2528,2529,2532,2533,2535],{},"Le critère n'est pas la taille de l'application mais la ",[21,2530,2531],{},"nature de\nl'état"," : une poignée de valeurs partagées, ",[572,2534,1876],{}," suffit ; un domaine\navec de la logique, Pinia paie son coût.",[12,2537,2539],{"id":2538},"quelques-pièges-concrets","Quelques pièges concrets",[158,2541,2542,2555,2573],{},[161,2543,2544,2550,2551,2554],{},[21,2545,2546,2547,2549],{},"Ne mettez pas de valeur non sérialisable dans ",[572,2548,1876],{},"."," L'état\ntransite du serveur au client en JSON. Une instance de classe, une\nfonction ou une ",[572,2552,2553],{},"Date"," ne survivent pas au voyage. Stockez des données\nsimples et reconstruisez les objets riches côté client si besoin.",[161,2556,2557,2560,2561,2564,2565,622,2567,1655,2570,2549],{},[21,2558,2559],{},"Attention aux clés qui se chevauchent."," Deux ",[572,2562,2563],{},"useState('data', ...)","\ndans deux contextes différents partagent le même état par accident.\nNommez les clés comme un espace de noms : ",[572,2566,1954],{},[572,2568,2569],{},"'session-user'",[572,2571,2572],{},"'prefs-theme'",[161,2574,2575,2586,2587,2590,2591,2549],{},[21,2576,2577,2578,2581,2582,2585],{},"N'accédez pas à ",[572,2579,2580],{},"window"," ou ",[572,2583,2584],{},"localStorage"," à l'initialisation."," Ce code\ns'exécute aussi côté serveur, où ces objets n'existent pas. Pour un état\npersistant côté navigateur, ",[572,2588,2589],{},"useCookie"," fonctionne des deux côtés ; sinon,\ngardez l'accès au stockage dans ",[572,2592,2416],{},[12,2594,1571],{"id":1570},[17,2596,2597,2598,2600,2601,2603,2604,2606],{},"La gestion d'état en Nuxt se résume à une prise de conscience : votre code\ntourne côté serveur autant que côté client, et un état mal isolé fuit entre\nles requêtes. À partir de là, tout s'ordonne : ",[572,2599,1876],{}," pour l'état\npartagé simple, ",[572,2602,1312],{}," pour les données distantes, Pinia quand l'état\ndevient un domaine. Le ",[572,2605,1628],{}," global de module, lui, reste dans les projets\npurement client. Choisissez l'outil selon la portée de l'état, pas selon\nl'habitude.",[1579,2608,2609],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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 pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":51,"searchDepth":52,"depth":52,"links":2611},[2612,2614,2616,2618,2619,2620],{"id":2257,"depth":52,"text":2613},"Le piège du ref partagé en module",{"id":2319,"depth":52,"text":2615},"useState : l'état partagé fait pour le SSR",{"id":2400,"depth":52,"text":2617},"useFetch et useAsyncData : l'état, c'est aussi les données",{"id":2497,"depth":52,"text":2498},{"id":2538,"depth":52,"text":2539},{"id":1570,"depth":52,"text":1571},"2026-07-14","useState, partage compatible SSR, pollution inter-requêtes, quand sortir Pinia : les règles pour gérer l'état d'une application Nuxt sans se tirer une balle dans le pied.",{},"\u002Fblog\u002Fgestion-etat-nuxt-bonnes-pratiques",{"title":2246,"description":2622},"blog\u002Fgestion-etat-nuxt-bonnes-pratiques",[1600,2242],"ihNvcDqGs6Bw_Po635w67cxCJfCqxO-qiJgBxrfmGII",1784652013779]