{paragraph}
+ ))} +From 5a7a7c73caf18e51fa9aa98fead5236f9e0f6398 Mon Sep 17 00:00:00 2001
From: Hasnae
- Edit Get started
src/App.tsx and save to test HMR
+ Edit src/routes/index.tsx and save to test{' '}
+ HMR
Join the Vite community
+Join the TanStack community
mI|13!gBV?X`Ozp7x>?6jr`>Qz=^4ea35!$*f}) zS$i+x_k+@P2q1RFUH^ZTTk7=n?cjfR>hTq3l3SY~#w+I8SSutXGyhw;Ws~=zMQ%Vc z>$On~47Ut?P*_!TOQ&PFmLAyJieB2X4_Fd_!WxI-AY`q1Lc-oK?+qcOTzlQ?@~x@OT}*9jTVNfl@3rGvZpWI=eKg>T zZb@6YWz)J=IhP7CF|c?G62vMEG%#U}?#86$0jR4sG~i(jRd#jmn`7b(O#?N;3a;1t zhXLssmUwGhp79luw#(*V8W L0|8+E z6=YZ_O@er~$LrD_PYGc(kJgB=;yw#+Z3X6LDUZ(NcwN=B-hjdiHm!JFar%m{(5bEW z@@_VEtG$5;`EJZ|OkJ@l&G9n((w@uNFwmU%bG|s#TbcJJos!{e +bjCjrCq_}LcN!UFgKtgg7siV*7# z!}1whTRRi*-avJPu->C}Z8EiuK$#886+H_#_!btv+rsiBbv2jAJvJ+O0{#}y(%L3H zfjU-kq_-L@2XrL*ae{{qYJkD{@dw%*bkh2P&YS-0!Xt!PRz7KHV0+~j(t9W8lAVWR zt@B*DgURgEz4>WuN>o?_iKc w$?k{||Pg7{Q2o4|VmJ)mg?{VQJA<}zEr^YAAS zgGm5RT4T3p)U;yz-tfBO^kw8?IoG!IVmc+Z3m#}AOQ?5MRa>)OcU!$N^_+yK6ayn? zK>~WK0!#ysuj^oNLakm)Zvu+J)OSubX^kv!c*xgdIvs;kln!rgG4*uZ;w0mQQO4XD zO9P{GNdv!=cQ(CAL{S(% KtuV^zC& Q{%g)PoXnp^gn^>c*`E>$hLYg2HjnbVGtWLa{7zHdG1jT@B{|Dm16 z7K2(jsfG+m*Zxof)iXxu+!H5M o-0$pkyV3VV4B@Qms46M zuBxGRV@HxU 7Wwx-6CB zaU*HO <_qn$5GH>&@?nRy1{z zkik!sLfWQ)r#75)vVwCBU*r_)Q6mp?!j85{#Xqse)ApRdE$V0%I0*~e(_{)5H)`Mk z#rExC>yjhZxuL@|+#v4#< Axw$+VpV zuT;!2Vww$je$DpAW`$FX_Ab|Ip%$;&T$-lW8jS~B$>G}rd>eQG+$h9lQx4Mx0w={m zx9?T6VU`>sR}XC lkAhHEShOUe8awiq zmizhL+}5UKs3}6~It7vBTig9dfQ2Q8coo+Miiaw7n~>4ybv2Ptt0^^=VqX(t*Yya9 zr`FxxFX8(v*H=+uJ#JJWIB2A(==HDYx~^zZ2nu?2`}|Wsa*f3h3ixc+U|FDtAG$Y! z*lc_7se5Oso-Cgqe0){{!8H4g$3<8!R<6JOurD;((({c$1(pwb>(#TT!sge@4>r2@ zVL7>U`0`nsWAYErezk4(Z!gMI2?UTo{J3Ajo(u4)KYIRd>BRcG4BoS3G0EXyEp@tw z%P7__?A^a>Q&AKL@ayDO9D*Qkc!NHnO9l}kpp_6hXbMppYL(X1L?njdFT|-h2<_$; zAtDZ!1Rf%|yb!qbWKd}%0b`LzBeyNy43|QO(&h2mxQLUL)|0%agVOW)6TV!&Ip^Ls z`PG2cygM8)IecQx=Fc+nqYRo4hS^^-nM_&-y8?EJXUczP=DIw(GkTJdpEdh<_STs{ z|A)4n1GKdE=Wu!!nYoZHcUQ4S&R;oDOKX2lrkdF(mK>hz<$Pp>igjOcvoRIjlN=W8 zu8Gx5(roqn8$>gEE5vy{GiGeW8Tq{vnf3hS-V=$tZkQuftUVuU8o6k&dn= Yg3)6MOIH>nlK^-2+C6BZITr~1@So?NvG#TwL)|~=1YXGMTLpS<)ziK_CSOabe z=cB#5)yz|@0i9dSo?*CX)}UP=s6)B+F@~Em(u@Q(I9J9i_V{LmMu8BfXYMh~*oPP+ z!3~xTv|(>|=n6ZOtT~C@V!z!w%18*8T2t6}U2S##rC)mekBql&VsBX;$~ByGE$oA9 z`0Wzq8p?R{4)$l*on;!cLa}Dh^Xe?owiQZt9nH1fxxh$pN9K%CtOw?u3>85L7rr!d zXs)l{TZ{xXP&U8exz?9cv~dNNibOmt*K4I$?RxqIBZ0(?Mg-9FS{*9Bc49Qc1`=sIF-rye`aNT1G@4NwXcnyc@+bw_mTsR>5< zF<2;X0QesG_pw|TonqVBhRtfqI>ty(SIu&VOXd0CrLlfp+;WH7HYjhqnu^oAY!9cB z=B6#R?Rfz9BP`dJ=@v_?70s3HxQPk+{6Y+lM85f2NF^00*^OcM0~?JOZfR9ZPYF+# zYSs}(_BUYV8{n@2a1hD^SV41bwmi2uztR;PeBgF1F-`9>` zoNss-@3LaF2sjl~>OaaVmp7PNp+UT`6@}gR%uzqHDVeEZ14{Yt?n%JeQm+t(1_u zSc}oj^{b;+rlS|ME%+LjzSI&xu0Bblxo$MJ-J$kJ?Qu_XUXh}*@*-x@ny|}wVM%Lg z3tNB`yvr*}N?ClGL;H2cglcvErIccU3(eP7>@~4nOIcI~-`P8tSQnx=jI&{9)!1}l z;gQ%_h>ZlPSV@o@Azq1 R$C6ja5!^ZGh;YRhhxs58qJWo9@Bc eac&yy(pET1hnn`~7@}2L0&dfPKYs$ih7m2}R!25!(hxqA(!UIw; zK4+~Jowy3=R NC6n E=ncU{LH5?*9@W24lacJlvCZXB$CYtE@>c+~H zkV=(5I&gb{xn2!~f&fs2NQgAL6`p|kyt6kpWk}iVlqIp(H;ig`{_U9yxs1jzu^ETM z7~)Rg8C-Nue qTYP&U8l{DY=Y47cR zOR@U%$KQV{mkRF|4)z9Y^t3K`@p>duY&QLUFeh6VoV`a`$U@)(z!-N*5Cj<1 1$EZW&hJLX83TO{lJYP74rlDZQPkm@t<=U^I)x@|UnHHkdQlh?!ltZwl92rE;;^ zZuIappj4dhld1}kttYYV-j|KF1Kus zWBnzttD^00%LFK(wrwNragFub6xiV8QE2rm<`&fcR4SLFcdtLxVuN!Aal-g6dE4%k zARZ}|xeo;K{0yf7@9aua%2j5o)CPcIOc6uLHFJOcgtB5owlcNAwyAHc0QB0Dts?c@ zUemG~j_E&W7R%+x-IO4FJl8e&*2Blmp1S#RA|)geVrxvP)NHdYuxi~g&Etn?QdN K8ZDKZ?QFLU? zh30 G|t9G>a_X4zk}Ygw<^$7K!GIn(Io$>(d4ODJQ2XSd%jpK zm7>ptl$a3GyB}5-%p4>Q*p#VL^B{yQMuFCM^#l#+N!Ne z5_PrJWB=@Iy+t)H`g1lX`{bm($KE5I?0c(JEYm#t{F}j!xtsb ob0{xu@0TB_*>G7w0ICn zr#V oBktqHZ~XxhiKD*lcG|b;H *|Ny3P^8ceV`sfBRfrhwZ!T+MFZ!F1Bt{q$8d9i6o?~ zODj^POr}&ivSa^R^YFIq7o0giLBKCycH_aU`F6)O6JX%nPTwh~Q`eq6*0iE#Srj2^ z*_hN3%*b83zfafy60@Cp3{J({RlSaEn &E?mrxRNC9GQ7#+f=s! z0KBf-9Ny_v2VbE%aB|Di)5kNJ^t&C`4D(>t7zYUWUFtbxt+Oq=!@O7BU)}>d*R72o zFF)3jQD_lLe4is&xzyJYC1-c{8TX$RU>&>P$%)ufpez0XSAukmh!xcekg`s$c<>-q zI#zn^JU0zzF}V60)o$_gY}PQH>b2M9&8fR Za#OauglPb zeQ@pMm&=!vNgos4CluQjLMV!pfkmxK+35bi^k&=k>9h02?l+u+m0agG ;(h2|Jslc-llvtEwn~*w3bx7qnvZACG<8}AGeaDVvcHbKd2>3G^ zSFPULUn-?Pmo^-_`mLZr??uNH`2=I&yajlrF{DtUxMy#Nu}z=3y7qbUA;5`)hibMR zhXL@@uKyV0-2&A@t@!xyrBnMJl&^o@Gx$&5_q6?D=ji5grd-~=?dlg;ur(_V0wjh! zA=JV^C1m+DDkOsgr<%O9ZQFg!0}pD(#PSz4Dr_EyS5$`)VIAv);4n-SFP~YtC7sH= z7&*MfpH;gd*FHbkmD#)hVxb6xjc9~`t?_{=JS+@ip_cTicXxG<=7m9 & zPX+Z8IC*GSAXuGCrZDHgR$r%jyk-fctis2Kx4HvZ|B~8uC@o)m^>Hy-O!&TKA?$&n zkP2Xc54w~!=z2?^NafyL*L0V9cbYrugHBBUj`xVyZmGFR&kvk#>1J*Z~i zNTz}?IAdJ$gkqd2!Gw(%LzE!O5s4C7q4%T~e_P{+z=DNDKrG**p=U`d5yg^vp`;Zn zsU=8gd0a9s4s0FPJePWR9eH5=+O^Kks&kC-iblNqTh2&Pw*^(4384f+D8N|fewZu_ zg2ejQ)ov;ztz;NQl7yj;A`(!H!XQu_$sqY9h_IrH*}_%1{L&_YLDvO?%R5Z-t+ClW z_qERbL?HKUZ!nt+!E9S`uoh^5A|DaIHe*_gf1`E_Vq+}{&T@t$EGhMnRjJ4z2w_W8 zp+qjs7as22^&S3wY1?+}^j-I=RcCE>#|39)g( lU7v_8;?=qK(9D8-*pPdiy)P3lIblG`+?%ea| zYoD3dopYt!tKgFicfNmNi(EWE=E4hC6(r|PYtanqJlmt57YOVrr2^tfrG(eG9C##X zu&1t@%L$RIvpj!w UA z8i>Pqot#_+Cnp6L2XPcZy1ar|9MnY+7eNvK1E)@Tr#2KsXq1*>)uUCozT7L##ok?o zhA6ofP4E|b*9tAfG?u f$#}>TIR&1A!yslP8}i7w-EzW(x#9VEvx18k%Tn=-$VV zkOtUr0b2!w3t>h?#8AZl^Az*(6KCGlD;4j~yx};`#2gN1_gv=%7KVzecIRakN{f*4 zeaI>yH;-o4OGhvGTU)(quWI )-q?V*(sVesSMv|wMUQ3hLEt=lBB$KZ9TyHr> )f7o%) zPYeU<3P)*P10*7vE)nA5#{c=6-E-_>r_u4e3i!I2+UksELwDqwMeBZ9FSP$;^Ajro z_@M#_Ss$?ejoB@!wN|kbGKs(0zLo%0QpQXW#t;oC$B0MZYZ&Ej?8~fNhcCVvPo3vo zFn0WWZaPliF^8_}yzb`*f@yg0uWv6HgNI)xa=pO%Ck(C<=-60l#uD3(wXP~c7!NoX z0&^6=N`zcc90F#qt@=Rn@r!3(*1v(Tl{B!m?Mc7yIA+nEHpY{YWr$=)F7rhR1P}(v zt{YhY#;jsW6G>#xhP*B`OCk|Pf+NN;ju1rxa*HAgoGq*rvqw&xe~;t1JA31$s?GBb z*g7&@cbKo4n<`>)!UlIAgR6q&))B0KYU8r66GbFj?8Guw4E%&}Qi_lT003LtoIZei zwD~=XZmeo+yZ2Pq3KYCF-R&11^p = z@H%s+=G`}wrbJ{()Mh71#2SP3Zy3m>l1n?0N-N1Q;z6?oSxr-G(H5m4EO>~&;}VKi zfY}3w+9z> vp#d)hVuu`)vG_aaH%3b=WKMnSu&c31;<3O;bz2iD=w+o4#oBb36 z5ZCF*Gu?zjZIR0S>_%pHY2$k8D^n7Sz_K8tCDeXM+dO<#LSg%h6`~dnVG1N@T7v&e z%wEd1!k{^zfz_1BTW{!$!B%g)J^2b87!9Y>>100X1SgT7s0z$o>^lAA=G p_cC1(h=*5Tmf8z&LGJJ>$|K^~s`z9*OWz5MFUr?>Bi?_PGBB)#psD5?>n+q{o_ zz7~ez&;t#h8l$jwGPCC&xq2YetXYQT+0F3j(`xmNGf8dj#an|p#I*pvI*kwW4iuB> z+q3_7xB8y;pLzHG-S%+UHQ A zvqp;$kmGJY>lL sN4C~&TcvAS1SErTcwcw0r@wngk zShAUA1M9b#g}^pL-zH7Q#z^&j#r9F8BTVfkR&qF<=e35goTu7c|GN)0mokj4m0%~0 zXJ8j4Hc_l;HJ&uU*Iw`8d_EscJ``s0tk9mkKo^TYXm-EoAzTQObxa@^u~g2t#T) zJz|rE!I_?i4dCJC=B8(_pZ{YR>|V?0iCcnU;E@$239^x?SYCfNaMHN;CtHIS_zHN9 zTkQc1v@O35okiFtq5_u+5FkY55ap@pi)O?}x0D1c*qB0KpY R}>Ul+B0Vmr}Z@+%mJ|As}sis_=ROPbov@*2thpE&?!V#Qgu$snYvCZ zrkhmkMU+fSf-s8(L37 fPr&M*jRs{{THb!aXQu|P9l_-vJhHvLzMGH zE?1U0H_+Pm NABp9`|KzkGfrrZ%XvdGo6*<{d5m9~L7 z_ ^`M;X6xDo=m6LY6RfvJEvsTK1!u8d2HPx| $S}p;sRy!I zWL55Yxu~_B`OP@~(q6&W3#)~I&+MGL%GWR$#udC151^wsswhqlii;rP9jJpiI7o&Z zAb})=HY7?4HA|re3ns`%$)FuvKCFWjhb~?IE)F6dF2K5}poj-NK6Gf;hw$t3=1txY zoxQxZWrQU6K!%|~!m?~Bnw-6Rr!F3BZ{u5!LqnZTDON}Coj9^@&le)V!NYrVwS~B% zEL+>Sr@}qGwGvu|HrOo|gSt__ezN^&%~{*)a=rf7y1HujUcr`zZB<4#l@T#eN)si} z)lZA<{=tKx8E%c9>A(##6 }_p+~EZpKsl5a4pj`E*;_-6`ysiv zffA!7=MT1vCz}-m4~tjVey1b2KSR4OEtLd-(_DdUqYZ74LaDkhH?KFh?%WAOP2WbX zp@zT+Dx|5_f%JQiAGvVw!oh +g3e50u!aPfMxdC=E)XB{F5IcEZhePIM- zph6Y`$Oy?JBL<8Ex(SqEhLeQ@XcrdA>a?rx+_~HLA;l14)WmmpH}_w?Pg#HBZs0eS zwypwAW?M-x+3AU-(GGWSJ=ngxUEcEZ5OsX(Qlt !MQ zn^(`S{GHkAv(8@D`EAfSYig%Cxv?z!{=w^F#y)5_d7FuKZH7qlR-#5B0bt806%D0I zT7VdVP_?q*%Rq8UR;JkD4i^RXowt+E%#V2U>TfDqzZSDZ+dR!a#T3I>-z_$q9@k|m zy5~A*m~&JWP@E7a=pc}4kVHTc4h&R;Li7d@f`|hKMLkbb^uhOakNr3&FLj lm~i5NBM< zFaYI{;cpiHCNRdE0dg*>qIm(_t?#$h=(SCw?h3rJV2*ER8{O4^3#=dO)KwklZkoqU zS8i5c%YL*y*4;FY#D=XmkQnYj%LH)?02~gSJH`Qp1XY64g>%c_K$xseI&|e)7vRoL zAqRba$G@%fSGA7X7hQk%_3NVOYVS+$leU_!&6*5uN)8#5ZBz_6ASCA;azYS-Rt@ki zg2NWz(=;t}SC(~Ibl63$5C8FPmhXqb^)5#jaJ~I{Ex3xZ!+2h8$}}h_g@Be>HZ;72 z6#y#>AY3^skuVKF#0WxFBQ()5d5_nWb?c6c>EeMM|Mh+*&wEpPyxHCq{R-Gdr-`hN zF=1sxl&mBoK+#qRLl9#CEN|Fg8>nbmsTg3a1;#M9enQ$RgWk}kp#-5wh=EF&1tl%mJln2V^8o%Qv(*=zEuO7y z=m*8?xpUn-*@h5Cl_3BK3joiGkyaScK+>|MWdMRWm@RT!Q1piAlv5hL@B6>3&GI8) zP!xBc6}ZNIpJLL%2a8Y!+(<=f%WX>_uWVxlga9!D*oYt$l0cxRDMvqfU;Kq_mLK5k z)dvqYcgLa_Lz?3HyeF)@$%$&6lI?r4I>6W#M*<)vq{?& Oqrx``d`mhpVPr> z#q078F6gw_X<=?KR>8%^t%@wbITvNMu!hKiTSkCTJkw>1!e*Y{%31#_yMf=LW7{RJ zYoC^w$6%3cBtVG5)x#{Hg6IVTh9XEcM{gQwXk!R^y95^f-hZ`d{aVa+xW1EO4wDV4 zB?JgD7*?qkvc|$nIykTvNl2x0j3Q!MXoLL^)~}d7jcYf(H8D~c+?$pKL(px>Z3`eb z04RzS6_AgFT6Pn#iZAg$Sl_j8#;6ShF%&(Fag#E2asU@@LaN;=b=Wf7sgPKhfzhBM zC@eFL8^MrnA*9&Khe*Ab@CC9*uyJGXyi(;y2>lQLJZt;ShtJi?3Yf_t `F+$hY!+Q2Ndsx=U+bjTiAy7djLji>7k%k`$9&--f<*BNA3Hy&ZrHH|4 zG5H&9cB?O#zI1_OOf0Ce%mDfQxdtp3vU%(iY6yji3iISS61XLv#z|!zI_sZqza@B+ zyu9st5-h+`H7QUKx9}3w@oU@EO}&cEzG?fu!!bLO->%zkcg;i9^j`S~=WKMnDi1f= P00000NkvXXu0mjft=yBf diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/data/posts.ts b/src/data/posts.ts new file mode 100644 index 0000000..66b6a4f --- /dev/null +++ b/src/data/posts.ts @@ -0,0 +1,55 @@ +export type Post = { + slug: string + title: string + excerpt: string + /** ISO date, e.g. "2026-07-20" */ + date: string + readingMinutes: number + tags: string[] + content: string[] +} + +export const posts: Post[] = [ + { + slug: 'hello-tanstack-start', + title: 'Hello, TanStack Start', + excerpt: + 'Why we rebuilt the website template on TanStack Start — server-side rendering, type-safe routing, and server functions out of the box.', + date: '2026-07-24', + readingMinutes: 4, + tags: ['tanstack', 'react'], + content: [ + 'TanStack Start pairs the file-based, fully type-safe routing of TanStack Router with a Vite-powered full-stack runtime. That means server-side rendering, streaming, and server functions without leaving your React app.', + 'This template ships a minimal setup: a document shell in the root route, a home page, and a small blog stream backed by server functions — a solid starting point you can grow into.', + 'Everything is typed end to end. Route params, loader data, and server function inputs all flow through TypeScript, so refactors stay honest.', + ], + }, + { + slug: 'file-based-routing', + title: 'File-based routing, explained', + excerpt: + 'Each file under src/routes becomes a route. Here is how the root route, index route, and dynamic segments fit together.', + date: '2026-07-18', + readingMinutes: 6, + tags: ['routing', 'tanstack'], + content: [ + 'The __root.tsx route owns the HTML document and renders an Outlet for its children. Every other file under src/routes maps to a URL by its name.', + 'Dynamic segments use a $ prefix: blog.$slug.tsx matches /blog/anything. The matched value is available as a typed route param.', + 'The route tree is generated for you into routeTree.gen.ts whenever you run the dev server or a build, so you never hand-maintain it.', + ], + }, + { + slug: 'server-functions', + title: 'Loading data with server functions', + excerpt: + 'Server functions run only on the server, yet you call them like any async function. This blog stream is powered by one.', + date: '2026-07-11', + readingMinutes: 5, + tags: ['data', 'ssr'], + content: [ + 'createServerFn defines a function that always executes on the server. You can read from a database, hit an API, or — as we do here — return static content.', + 'Route loaders call these functions before a route renders, so the data is ready during SSR and hydration. No loading spinners on first paint.', + 'Because the boundary is explicit, secrets and heavy dependencies stay on the server and never ship to the browser.', + ], + }, +] diff --git a/src/lib/blog.ts b/src/lib/blog.ts new file mode 100644 index 0000000..a834ee5 --- /dev/null +++ b/src/lib/blog.ts @@ -0,0 +1,21 @@ +import { createServerFn } from '@tanstack/react-start' +import { posts, type Post } from '@/data/posts' + +/** Lightweight shape for the blog stream — omits the full post body. */ +export type PostSummary = Omit + +const toSummary = ({ content: _content, ...summary }: Post): PostSummary => + summary + +const byNewest = (a: Post, b: Post) => b.date.localeCompare(a.date) + +export const getPosts = createServerFn({ method: 'GET' }).handler( + async (): Promise => + [...posts].sort(byNewest).map(toSummary), +) + +export const getPost = createServerFn({ method: 'GET' }) + .validator((slug: string) => slug) + .handler(async ({ data: slug }): Promise => { + return posts.find((post) => post.slug === slug) ?? null + }) diff --git a/src/lib/format.ts b/src/lib/format.ts new file mode 100644 index 0000000..1cf8b7f --- /dev/null +++ b/src/lib/format.ts @@ -0,0 +1,11 @@ +const dateFormatter = new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', +}) + +/** Format an ISO date string (YYYY-MM-DD) as e.g. "Jul 24, 2026". */ +export function formatDate(iso: string): string { + // Parse as UTC so the day doesn't shift across timezones. + return dateFormatter.format(new Date(`${iso}T00:00:00Z`)) +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index dceedff..f3e4d5f 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -10,33 +10,43 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as IndexRouteImport } from './routes/index' +import { Route as BlogSlugRouteImport } from './routes/blog.$slug' const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', getParentRoute: () => rootRouteImport, } as any) +const BlogSlugRoute = BlogSlugRouteImport.update({ + id: '/blog/$slug', + path: '/blog/$slug', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/blog/$slug': typeof BlogSlugRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/blog/$slug': typeof BlogSlugRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/blog/$slug': typeof BlogSlugRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' + fullPaths: '/' | '/blog/$slug' fileRoutesByTo: FileRoutesByTo - to: '/' - id: '__root__' | '/' + to: '/' | '/blog/$slug' + id: '__root__' | '/' | '/blog/$slug' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + BlogSlugRoute: typeof BlogSlugRoute } declare module '@tanstack/react-router' { @@ -48,11 +58,19 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/blog/$slug': { + id: '/blog/$slug' + path: '/blog/$slug' + fullPath: '/blog/$slug' + preLoaderRoute: typeof BlogSlugRouteImport + parentRoute: typeof rootRouteImport + } } } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + BlogSlugRoute: BlogSlugRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/src/routes/blog.$slug.tsx b/src/routes/blog.$slug.tsx new file mode 100644 index 0000000..adc3bd6 --- /dev/null +++ b/src/routes/blog.$slug.tsx @@ -0,0 +1,67 @@ +import { Link, createFileRoute, notFound } from '@tanstack/react-router' +import { ArrowLeft } from 'lucide-react' +import { getPost } from '@/lib/blog' +import { formatDate } from '@/lib/format' + +export const Route = createFileRoute('/blog/$slug')({ + loader: async ({ params }) => { + const post = await getPost({ data: params.slug }) + if (!post) throw notFound() + return post + }, + head: ({ loaderData }) => + loaderData + ? { meta: [{ title: `${loaderData.title} — website-template` }] } + : {}, + component: BlogPost, + notFoundComponent: PostNotFound, +}) + +function BlogPost() { + const post = Route.useLoaderData() + + return ( + + + ) +} + +function PostNotFound() { + return ( ++ + ++ + · + {post.readingMinutes} min read +++ {post.title} +
++ {post.content.map((paragraph, i) => ( + +{paragraph}
+ ))} ++ + ) +} + +function BackLink() { + return ( + ++ Post not found
++ The post you are looking for does not exist or may have been moved. +
++ Back home + + ) +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 24d6f28..6b22a4b 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,125 +1,103 @@ -import { useState } from 'react' -import { createFileRoute } from '@tanstack/react-router' -import reactLogo from '../assets/react.svg' -import viteLogo from '../assets/vite.svg' -import heroImg from '../assets/hero.png' -import '../App.css' +import { Link, createFileRoute } from '@tanstack/react-router' +import { ArrowRight } from 'lucide-react' +import { getPosts, type PostSummary } from '@/lib/blog' +import { formatDate } from '@/lib/format' +import { Button } from '@/components/ui/button' export const Route = createFileRoute('/')({ + loader: () => getPosts(), component: Home, }) function Home() { - const [count, setCount] = useState(0) + const posts = Route.useLoaderData() return ( - <> - - +---
-
-
-- -Get started
-- Edit
-src/routes/index.tsxand save to test{' '} -HMR-+ + ) +} - +function Hero() { + return ( ++ + + + website-template + + + ) +} -+ Build fast, ship faster. +
++ A minimal starter built with TanStack Start, React, Tailwind, and + shadcn — server-rendered, type-safe, and ready to grow. +
++ }> + Read the blog +++ + + } + > + TanStack Start + + - +function BlogStream({ posts }: { posts: PostSummary[] }) { + return ( +- --Documentation
-Your questions, answered
--
-- - - Explore TanStack Start - -
-- - -
-- Learn more - -
+ + ) +} - - - > +function PostCard({ post }: { post: PostSummary }) { + return ( + +++Latest posts
+ + {posts.length} {posts.length === 1 ? 'post' : 'posts'} + ++ {posts.map((post) => ( +
+- +
+ ))} ++ + + · + {post.readingMinutes} min read +++ {post.title} +
+{post.excerpt}
++ {post.tags.map((tag) => ( + + #{tag} + + ))} ++ ) } From 1f41a2d6026e43cb9a77c63f1a5c4b3f035d4993 Mon Sep 17 00:00:00 2001 From: HasnaeDate: Sat, 25 Jul 2026 10:11:52 +1000 Subject: [PATCH 06/11] adopt site app structure and best practices Restructure the template to mirror labset/startup's web/apps/site and fold in the practices from the "building this site" post. Structure: - content/ owns the MDX loader (posts.ts) + authors.ts, with meta validation - providers/theme/ split into context, hook, provider, barrel - dedicated page components (landing-page, blog-index, blog-post-page, not-found) with thin routes pointing at them - root-document + site-layout + site-header/footer shells - components/mdx/ for the element map and a sample Callout - routes/blog/ dir (index + $slug) and a prerendered 404 route Best practices: - MDX content layer via import.meta.glob (no Shiki, per choice) - static prerendering (crawlLinks) + explicit /404 page - full SEO: socialMeta() OG/Twitter tags + self-referencing canonical - dark mode via useSyncExternalStore + pre-paint inline theme script Tooling: - gitignore the generated src/routeTree.gen.ts; build is "vite build && tsc -b" so the tree is generated before typecheck - eslint only relaxes react-refresh for ui/**; tsconfig keeps baseUrl with ignoreDeprecations "6.0" Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 5 + eslint.config.js | 14 +- package.json | 3 +- pnpm-lock.yaml | 1273 ++++++++++++++++- src/components/blog-index.tsx | 26 + src/components/blog-post-page.tsx | 28 + src/components/blog-post.tsx | 56 + src/components/hero-section.tsx | 28 + src/components/landing-page.tsx | 12 + src/components/latest-posts-section.tsx | 39 + src/components/mdx/callout.tsx | 17 + src/components/mdx/mdx-components.tsx | 50 + src/components/not-found.tsx | 44 + src/components/post-card.tsx | 69 + src/components/root-document.tsx | 21 + src/components/site-footer.tsx | 107 ++ src/components/site-header.tsx | 118 ++ src/components/site-layout.tsx | 23 + src/components/theme-toggle.tsx | 20 + src/components/ui/badge.tsx | 37 + src/components/ui/card.tsx | 75 + src/components/ui/separator.tsx | 23 + src/components/ui/sheet.tsx | 77 + src/content/authors.ts | 28 + src/content/posts.ts | 74 + .../posts/2026/07/file-based-routing.mdx | 24 + .../posts/2026/07/hello-tanstack-start.mdx | 35 + .../posts/2026/07/the-content-layer.mdx | 25 + src/data/posts.ts | 55 - src/lib/blog.ts | 21 - src/lib/format.ts | 11 - src/lib/site.ts | 44 + src/mdx-env.d.ts | 10 + src/providers/theme/index.ts | 3 + src/providers/theme/theme-context.ts | 14 + src/providers/theme/theme-provider.tsx | 68 + src/providers/theme/use-theme.ts | 11 + src/routeTree.gen.ts | 86 -- src/router.tsx | 16 +- src/routes/404.tsx | 16 + src/routes/__root.tsx | 46 +- src/routes/blog.$slug.tsx | 67 - src/routes/blog/$slug.tsx | 38 + src/routes/blog/index.tsx | 20 + src/routes/index.tsx | 108 +- tsconfig.app.json | 1 + tsconfig.json | 1 + vite.config.ts | 19 +- 48 files changed, 2608 insertions(+), 398 deletions(-) create mode 100644 src/components/blog-index.tsx create mode 100644 src/components/blog-post-page.tsx create mode 100644 src/components/blog-post.tsx create mode 100644 src/components/hero-section.tsx create mode 100644 src/components/landing-page.tsx create mode 100644 src/components/latest-posts-section.tsx create mode 100644 src/components/mdx/callout.tsx create mode 100644 src/components/mdx/mdx-components.tsx create mode 100644 src/components/not-found.tsx create mode 100644 src/components/post-card.tsx create mode 100644 src/components/root-document.tsx create mode 100644 src/components/site-footer.tsx create mode 100644 src/components/site-header.tsx create mode 100644 src/components/site-layout.tsx create mode 100644 src/components/theme-toggle.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/separator.tsx create mode 100644 src/components/ui/sheet.tsx create mode 100644 src/content/authors.ts create mode 100644 src/content/posts.ts create mode 100644 src/content/posts/2026/07/file-based-routing.mdx create mode 100644 src/content/posts/2026/07/hello-tanstack-start.mdx create mode 100644 src/content/posts/2026/07/the-content-layer.mdx delete mode 100644 src/data/posts.ts delete mode 100644 src/lib/blog.ts delete mode 100644 src/lib/format.ts create mode 100644 src/lib/site.ts create mode 100644 src/mdx-env.d.ts create mode 100644 src/providers/theme/index.ts create mode 100644 src/providers/theme/theme-context.ts create mode 100644 src/providers/theme/theme-provider.tsx create mode 100644 src/providers/theme/use-theme.ts delete mode 100644 src/routeTree.gen.ts create mode 100644 src/routes/404.tsx delete mode 100644 src/routes/blog.$slug.tsx create mode 100644 src/routes/blog/$slug.tsx create mode 100644 src/routes/blog/index.tsx diff --git a/.gitignore b/.gitignore index c2be2bf..564eb6b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,13 @@ dist dist-ssr *.local +# TanStack Router (generated) +src/routeTree.gen.ts + # TanStack Start .tanstack +.nitro +.output # Editor directories and files .vscode/* diff --git a/eslint.config.js b/eslint.config.js index fba6709..b31ac88 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint' import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig([ - globalIgnores(['dist', 'src/routeTree.gen.ts']), + globalIgnores(['dist']), { files: ['**/*.{ts,tsx}'], extends: [ @@ -18,18 +18,10 @@ export default defineConfig([ languageOptions: { globals: globals.browser, }, - rules: { - '@typescript-eslint/no-unused-vars': [ - 'error', - { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, - ], - }, }, { - // Route files export a `Route`, and UI primitives export variants - // alongside their component — both intentionally colocate non-component - // exports. Fast Refresh still works via the Vite plugins in these dirs. - files: ['src/routes/**/*.tsx', 'src/components/ui/**/*.tsx'], + // shadcn primitives co-locate their cva variants with the component. + files: ['src/components/ui/**/*.tsx'], rules: { 'react-refresh/only-export-components': 'off', }, diff --git a/package.json b/package.json index 5205e8d..31235c1 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite dev", - "build": "vite build", + "build": "vite build && tsc -b", "lint": "eslint .", "preview": "vite preview" }, @@ -27,6 +27,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@mdx-js/rollup": "^3.1.1", "@types/node": "^24.13.2", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5aa8941..df64654 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: version: 1.170.18(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@tanstack/react-start': specifier: ^1.168.32 - version: 1.168.32(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) + version: 1.168.32(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.1.5)(rollup@4.62.2)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -54,6 +54,9 @@ importers: '@eslint/js': specifier: ^10.0.1 version: 10.0.1(eslint@10.7.0(jiti@2.7.0)) + '@mdx-js/rollup': + specifier: ^3.1.1 + version: 3.1.1(rollup@4.62.2) '@types/node': specifier: ^24.13.2 version: 24.13.3 @@ -369,6 +372,14 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mdx-js/mdx@3.1.1': + resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} + + '@mdx-js/rollup@3.1.1': + resolution: {integrity: sha512-v8satFmBB+DqDzYohnm1u2JOvxx6Hl3pUvqzJvfs2Zk/ngZ1aRUhsWpXvwPkNeGN9c2NCm/38H29ZqXQUjf8dw==} + peerDependencies: + rollup: '>=2' + '@modelcontextprotocol/sdk@1.29.0': resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} @@ -514,6 +525,153 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -754,15 +912,33 @@ packages: '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdx@2.0.14': + resolution: {integrity: sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@24.13.3': resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} @@ -774,6 +950,12 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/validate-npm-package-name@4.0.2': resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} @@ -836,6 +1018,9 @@ packages: resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + '@vitejs/plugin-react@6.0.4': resolution: {integrity: sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -908,6 +1093,10 @@ packages: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + atomically@1.7.0: resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} engines: {node: '>=10.12.0'} @@ -915,6 +1104,9 @@ packages: babel-dead-code-elimination@1.0.12: resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -964,10 +1156,25 @@ packages: caniuse-lite@1.0.30001806: resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} @@ -990,6 +1197,12 @@ packages: code-block-writer@13.0.3: resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} @@ -1066,6 +1279,9 @@ packages: supports-color: optional: true + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + dedent@1.7.2: resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} peerDependencies: @@ -1101,10 +1317,17 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff@8.0.4: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} @@ -1161,6 +1384,12 @@ packages: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} + esast-util-from-estree@2.0.0: + resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} + + esast-util-from-js@2.0.1: + resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1226,6 +1455,30 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-scope@1.0.0: + resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} + + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -1263,6 +1516,9 @@ packages: exsolve@1.1.0: resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1413,6 +1669,15 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + hast-util-to-estree@3.1.3: + resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} @@ -1458,6 +1723,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + ip-address@10.2.0: resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} engines: {node: '>= 12'} @@ -1466,9 +1734,18 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -1487,6 +1764,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-in-ssh@1.0.0: resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} engines: {node: '>=20'} @@ -1784,6 +2064,9 @@ packages: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1795,10 +2078,41 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -1814,6 +2128,90 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-mdx-expression@3.0.1: + resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} + + micromark-extension-mdx-jsx@3.0.2: + resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==} + + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-mdx-expression@2.0.3: + resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-events-to-acorn@2.0.3: + resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -1939,6 +2337,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -2024,6 +2425,9 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -2064,6 +2468,32 @@ packages: resolution: {integrity: sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==} engines: {node: '>= 4'} + recma-build-jsx@1.0.0: + resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} + + recma-jsx@1.0.1: + resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + recma-parse@1.0.0: + resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} + + recma-stringify@1.0.0: + resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + + rehype-recma@1.0.0: + resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + + remark-mdx@3.1.1: + resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2088,6 +2518,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + rou3@0.8.1: resolution: {integrity: sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==} @@ -2189,6 +2624,9 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + srvx@0.11.22: resolution: {integrity: sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ==} engines: {node: '>=20.16.0'} @@ -2206,6 +2644,9 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + stringify-object@5.0.0: resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} engines: {node: '>=14.16'} @@ -2230,6 +2671,12 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + systeminformation@5.33.0: resolution: {integrity: sha512-0LYSL01CCbjVeJG7iXI8fUCFU76zMjzbHd/EU3or4QpSFYCLMgslR11prwHuA3siz5jmOkqoLhjgOyDRmXBKmA==} engines: {node: '>=10.0.0'} @@ -2261,6 +2708,12 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -2314,6 +2767,27 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -2380,6 +2854,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@8.1.5: resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2491,6 +2971,9 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@babel/code-frame@7.27.1': @@ -2839,6 +3322,46 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mdx-js/mdx@3.1.1': + dependencies: + '@types/estree': 1.0.9 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdx': 2.0.14 + acorn: 8.17.0 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.0 + estree-walker: 3.0.3 + hast-util-to-jsx-runtime: 2.3.6 + markdown-extensions: 2.0.0 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.1(acorn@8.17.0) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + source-map: 0.7.6 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@mdx-js/rollup@3.1.1(rollup@4.62.2)': + dependencies: + '@mdx-js/mdx': 3.1.1 + '@rollup/pluginutils': 5.4.0(rollup@4.62.2) + rollup: 4.62.2 + source-map: 0.7.6 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.31) @@ -2950,6 +3473,89 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@rollup/pluginutils@5.4.0(rollup@4.62.2)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.5 + optionalDependencies: + rollup: 4.62.2 + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + '@sec-ant/readable-stream@0.4.1': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -3041,14 +3647,14 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - '@tanstack/react-start-rsc@0.1.31(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0))': + '@tanstack/react-start-rsc@0.1.31(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.1.5)(rollup@4.62.2)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0))': dependencies: '@tanstack/react-router': 1.170.18(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@tanstack/router-core': 1.171.15 '@tanstack/router-utils': 1.162.2 '@tanstack/start-client-core': 1.170.14 '@tanstack/start-fn-stubs': 1.162.0 - '@tanstack/start-plugin-core': 1.171.24(@tanstack/react-router@1.170.18(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) + '@tanstack/start-plugin-core': 1.171.24(@tanstack/react-router@1.170.18(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(rolldown@1.1.5)(rollup@4.62.2)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) '@tanstack/start-server-core': 1.169.17 '@tanstack/start-storage-context': 1.167.17 pathe: 2.0.3 @@ -3078,15 +3684,15 @@ snapshots: transitivePeerDependencies: - crossws - '@tanstack/react-start@1.168.32(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0))': + '@tanstack/react-start@1.168.32(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.1.5)(rollup@4.62.2)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0))': dependencies: '@tanstack/react-router': 1.170.18(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@tanstack/react-start-client': 1.168.16(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@tanstack/react-start-rsc': 0.1.31(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) + '@tanstack/react-start-rsc': 0.1.31(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.1.5)(rollup@4.62.2)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) '@tanstack/react-start-server': 1.167.22(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@tanstack/router-utils': 1.162.2 '@tanstack/start-client-core': 1.170.14 - '@tanstack/start-plugin-core': 1.171.24(@tanstack/react-router@1.170.18(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) + '@tanstack/start-plugin-core': 1.171.24(@tanstack/react-router@1.170.18(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(rolldown@1.1.5)(rollup@4.62.2)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) '@tanstack/start-server-core': 1.169.17 pathe: 2.0.3 react: 19.2.8 @@ -3134,7 +3740,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.168.23(@tanstack/react-router@1.170.18(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0))': + '@tanstack/router-plugin@1.168.23(@tanstack/react-router@1.170.18(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(rolldown@1.1.5)(rollup@4.62.2)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0))': dependencies: '@babel/core': 7.29.7 '@babel/template': 7.29.7 @@ -3143,7 +3749,7 @@ snapshots: '@tanstack/router-generator': 1.167.21 '@tanstack/router-utils': 1.162.2 chokidar: 5.0.0 - unplugin: 3.3.0(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) + unplugin: 3.3.0(rolldown@1.1.5)(rollup@4.62.2)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) zod: 4.4.3 optionalDependencies: '@tanstack/react-router': 1.170.18(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -3180,14 +3786,14 @@ snapshots: '@tanstack/start-fn-stubs@1.162.0': {} - '@tanstack/start-plugin-core@1.171.24(@tanstack/react-router@1.170.18(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0))': + '@tanstack/start-plugin-core@1.171.24(@tanstack/react-router@1.170.18(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(rolldown@1.1.5)(rollup@4.62.2)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0))': dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.29.7 '@babel/types': 7.29.7 '@tanstack/router-core': 1.171.15 '@tanstack/router-generator': 1.167.21 - '@tanstack/router-plugin': 1.168.23(@tanstack/react-router@1.170.18(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) + '@tanstack/router-plugin': 1.168.23(@tanstack/react-router@1.170.18(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(rolldown@1.1.5)(rollup@4.62.2)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)) '@tanstack/router-utils': 1.162.2 '@tanstack/start-server-core': 1.169.17 exsolve: 1.1.0 @@ -3249,12 +3855,32 @@ snapshots: tslib: 2.8.1 optional: true + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + '@types/esrecurse@4.3.1': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + '@types/estree@1.0.9': {} + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdx@2.0.14': {} + + '@types/ms@2.1.0': {} + '@types/node@24.13.3': dependencies: undici-types: 7.18.2 @@ -3267,6 +3893,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/validate-npm-package-name@4.0.2': {} '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)': @@ -3360,6 +3990,8 @@ snapshots: '@typescript-eslint/types': 8.65.0 eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.3': {} + '@vitejs/plugin-react@6.0.4(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0))': dependencies: '@rolldown/pluginutils': 1.0.1 @@ -3412,6 +4044,8 @@ snapshots: dependencies: tslib: 2.8.1 + astring@1.9.0: {} + atomically@1.7.0: {} babel-dead-code-elimination@1.0.12: @@ -3423,6 +4057,8 @@ snapshots: transitivePeerDependencies: - supports-color + bail@2.0.2: {} + balanced-match@4.0.4: {} baseline-browser-mapping@2.11.1: {} @@ -3477,8 +4113,18 @@ snapshots: caniuse-lite@1.0.30001806: {} + ccount@2.0.1: {} + chalk@5.6.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chokidar@5.0.0: dependencies: readdirp: 5.0.0 @@ -3497,6 +4143,10 @@ snapshots: code-block-writer@13.0.3: {} + collapse-white-space@2.1.0: {} + + comma-separated-tokens@2.0.3: {} + commander@11.1.0: {} commander@14.0.3: {} @@ -3560,6 +4210,10 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + dedent@1.7.2: {} deep-is@0.1.4: {} @@ -3579,8 +4233,14 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + detect-libc@2.1.2: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + diff@8.0.4: {} dot-prop@6.0.1: @@ -3627,6 +4287,20 @@ snapshots: dependencies: es-errors: 1.3.0 + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 + + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.17.0 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.3 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -3714,6 +4388,41 @@ snapshots: estraverse@5.3.0: {} + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.9 + + estree-util-build-jsx@3.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.3 + + estree-util-is-identifier-name@3.0.0: {} + + estree-util-scope@1.0.0: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + + estree-util-to-js@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.9.0 + source-map: 0.7.6 + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 3.0.3 + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + esutils@2.0.3: {} etag@1.8.1: {} @@ -3794,6 +4503,8 @@ snapshots: exsolve@1.1.0: {} + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -3932,6 +4643,51 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-estree@3.1.3: + dependencies: + '@types/estree': 1.0.9 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + hermes-estree@0.25.1: {} hermes-parser@0.25.1: @@ -3969,12 +4725,23 @@ snapshots: inherits@2.0.4: {} + inline-style-parser@0.2.7: {} + ip-address@10.2.0: {} ipaddr.js@1.9.1: {} + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-arrayish@0.2.1: {} + is-decimal@2.0.1: {} + is-docker@2.2.1: {} is-docker@3.0.0: {} @@ -3985,6 +4752,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-in-ssh@1.0.0: {} is-inside-container@1.0.0: @@ -4188,6 +4957,8 @@ snapshots: chalk: 5.6.2 is-unicode-supported: 1.3.0 + longest-streak@3.1.0: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -4200,8 +4971,109 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + markdown-extensions@2.0.0: {} + math-intrinsics@1.1.0: {} + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.3 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + media-typer@1.1.0: {} merge-descriptors@2.0.0: {} @@ -4210,6 +5082,212 @@ snapshots: merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-expression@3.0.1: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-jsx@3.0.2: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-extension-mdx-md@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-mdxjs-esm@3.0.0: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-extension-mdxjs@3.0.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + micromark-extension-mdx-expression: 3.0.1 + micromark-extension-mdx-jsx: 3.0.2 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-mdx-expression@2.0.3: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-events-to-acorn@2.0.3: + dependencies: + '@types/estree': 1.0.9 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -4332,6 +5410,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.29.7 @@ -4395,6 +5483,8 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + property-information@7.2.0: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -4435,6 +5525,67 @@ snapshots: tiny-invariant: 1.3.3 tslib: 2.8.1 + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.9 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 + + recma-jsx@1.0.1(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.9 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.9 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 + + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.5 + hast-util-to-estree: 3.1.3 + transitivePeerDependencies: + - supports-color + + remark-mdx@3.1.1: + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + require-from-string@2.0.2: {} reselect@5.2.0: {} @@ -4469,6 +5620,37 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.1.5 '@rolldown/binding-win32-x64-msvc': 1.1.5 + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + rou3@0.8.1: {} router@2.2.0: @@ -4614,6 +5796,8 @@ snapshots: source-map@0.7.6: {} + space-separated-tokens@2.0.2: {} + srvx@0.11.22: {} statuses@2.0.2: {} @@ -4626,6 +5810,11 @@ snapshots: get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + stringify-object@5.0.0: dependencies: get-own-enumerable-keys: 1.0.0 @@ -4646,6 +5835,14 @@ snapshots: strip-final-newline@4.0.0: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + systeminformation@5.33.0: {} tailwind-merge@3.6.0: {} @@ -4667,6 +5864,10 @@ snapshots: toidentifier@1.0.1: {} + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: typescript: 6.0.3 @@ -4717,17 +5918,55 @@ snapshots: unicorn-magic@0.3.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universalify@2.0.1: {} unpipe@1.0.0: {} - unplugin@3.3.0(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)): + unplugin@3.3.0(rolldown@1.1.5)(rollup@4.62.2)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)): dependencies: '@jridgewell/remapping': 2.3.5 picomatch: 4.0.5 webpack-virtual-modules: 0.6.2 optionalDependencies: rolldown: 1.1.5 + rollup: 4.62.2 vite: 8.1.5(@types/node@24.13.3)(jiti@2.7.0) update-browserslist-db@1.2.3(browserslist@4.28.7): @@ -4750,6 +5989,16 @@ snapshots: vary@1.1.2: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0): dependencies: lightningcss: 1.33.0 @@ -4813,3 +6062,5 @@ snapshots: zod@3.25.76: {} zod@4.4.3: {} + + zwitch@2.0.4: {} diff --git a/src/components/blog-index.tsx b/src/components/blog-index.tsx new file mode 100644 index 0000000..3c2cc6b --- /dev/null +++ b/src/components/blog-index.tsx @@ -0,0 +1,26 @@ +import { SiteLayout } from '@/components/site-layout' +import { PostCard } from '@/components/post-card' +import { posts } from '@/content/posts' + +export function BlogIndex() { + return ( + + + ) +} diff --git a/src/components/blog-post-page.tsx b/src/components/blog-post-page.tsx new file mode 100644 index 0000000..3e66bfd --- /dev/null +++ b/src/components/blog-post-page.tsx @@ -0,0 +1,28 @@ +import { Link, getRouteApi } from '@tanstack/react-router' + +import { BlogPost } from '@/components/blog-post' +import { NotFoundLayout } from '@/components/not-found' +import { Button } from '@/components/ui/button' +import { getPost } from '@/content/posts' + +const route = getRouteApi('/blog/$slug') + +export function BlogPostPage() { + const { slug } = route.useParams() + const post = getPost(slug) + + if (!post) return++++ ++ Blog +
++ Notes, updates, and the thinking behind this template. +
++ {posts.map((post) => ( +++ ))} + + + return +} + +export function BlogNotFound() { + return ( + + }>All posts + + ) +} diff --git a/src/components/blog-post.tsx b/src/components/blog-post.tsx new file mode 100644 index 0000000..4ec0450 --- /dev/null +++ b/src/components/blog-post.tsx @@ -0,0 +1,56 @@ +import { Link } from '@tanstack/react-router' +import { ArrowLeft } from 'lucide-react' + +import { SiteLayout } from '@/components/site-layout' +import { Badge } from '@/components/ui/badge' +import { mdxComponents } from '@/components/mdx/mdx-components' +import { getAuthor } from '@/content/authors' +import { formatPostDate, type Post } from '@/content/posts' + +export function BlogPost({ post }: { post: Post }) { + const { Content, meta } = post + const author = getAuthor(meta.author) + + return ( ++ + ) +} diff --git a/src/components/hero-section.tsx b/src/components/hero-section.tsx new file mode 100644 index 0000000..64a2026 --- /dev/null +++ b/src/components/hero-section.tsx @@ -0,0 +1,28 @@ +import { ArrowRight } from 'lucide-react' +import { Link } from '@tanstack/react-router' + +import { SITE_DESCRIPTION } from '@/lib/site' +import { Button } from '@/components/ui/button' + +export function HeroSection() { + return ( ++ + ++ All posts + + + + + ++ {meta.title} +
+ ++ + {author.name} + {author.role ? `, ${author.role}` : ''} + + + + + {meta.readingTime} ++ ++ {meta.tags.map((tag) => ( +++ {tag} + + ))} ++++ + + ) +} diff --git a/src/components/landing-page.tsx b/src/components/landing-page.tsx new file mode 100644 index 0000000..2a72b09 --- /dev/null +++ b/src/components/landing-page.tsx @@ -0,0 +1,12 @@ +import { SiteLayout } from '@/components/site-layout' +import { HeroSection } from '@/components/hero-section' +import { LatestPostsSection } from '@/components/latest-posts-section' + +export function LandingPage() { + return ( ++++ Build fast, ship faster. +
+ ++ {SITE_DESCRIPTION} +
+ ++ }> + Read the blog +++ + + + ) +} diff --git a/src/components/latest-posts-section.tsx b/src/components/latest-posts-section.tsx new file mode 100644 index 0000000..fd615bb --- /dev/null +++ b/src/components/latest-posts-section.tsx @@ -0,0 +1,39 @@ +import { Link } from '@tanstack/react-router' +import { ArrowRight } from 'lucide-react' + +import { posts } from '@/content/posts' +import { PostCard } from '@/components/post-card' + +// The blog stream on the landing page: the few most recent posts, with a link +// through to the full index. +export function LatestPostsSection() { + const latest = posts.slice(0, 4) + + return ( ++ + + + ) +} diff --git a/src/components/mdx/callout.tsx b/src/components/mdx/callout.tsx new file mode 100644 index 0000000..800452d --- /dev/null +++ b/src/components/mdx/callout.tsx @@ -0,0 +1,17 @@ +import type { ReactNode } from 'react' +import { Info } from 'lucide-react' + +// A highlighted aside for blog posts. Import it directly inside an .mdx file, or +// use it via the `Callout` key in mdx-components. +export function Callout({ children }: { children: ReactNode }) { + return ( +++ +++ + All posts ++ Latest posts +
++ Notes and updates, freshest first. +
++ + + {latest.map((post) => ( +++ ))} + + ++ ) +} diff --git a/src/components/mdx/mdx-components.tsx b/src/components/mdx/mdx-components.tsx new file mode 100644 index 0000000..1b915a1 --- /dev/null +++ b/src/components/mdx/mdx-components.tsx @@ -0,0 +1,50 @@ +import type { ComponentProps } from 'react' + +import { cn } from '@/lib/utils' +import { Callout } from '@/components/mdx/callout' + +// Tailwind styling for the HTML that MDX compiles to, plus custom components +// posts can use directly (e.g.+ + + {children} ++). Passed to a post's content component +// via its `components` prop, so posts stay plain Markdown by default. +export const mdxComponents = { + Callout, + h2: ({ className, ...props }: ComponentProps<'h2'>) => ( + + ), + h3: ({ className, ...props }: ComponentProps<'h3'>) => ( + + ), + p: ({ className, ...props }: ComponentProps<'p'>) => ( + + ), + ul: ({ className, ...props }: ComponentProps<'ul'>) => ( + + ), + ol: ({ className, ...props }: ComponentProps<'ol'>) => ( +
+ ), + a: ({ className, ...props }: ComponentProps<'a'>) => ( + + ), + code: ({ className, ...props }: ComponentProps<'code'>) => ( +
+ ), + strong: ({ className, ...props }: ComponentProps<'strong'>) => ( + + ), +} diff --git a/src/components/not-found.tsx b/src/components/not-found.tsx new file mode 100644 index 0000000..41eec77 --- /dev/null +++ b/src/components/not-found.tsx @@ -0,0 +1,44 @@ +import type { ReactNode } from 'react' +import { Link } from '@tanstack/react-router' + +import { SiteLayout } from '@/components/site-layout' +import { Button } from '@/components/ui/button' + +// Shared presentation for not-found pages: centred within the site shell, with a +// code eyebrow, heading, message, and a caller-supplied action. +export function NotFoundLayout({ + code = '404', + title, + description, + children, +}: { + code?: string + title: string + description: string + children: ReactNode +}) { + return ( ++ + ) +} + +// Site-wide fallback for any unmatched route. +export function NotFound() { + return ( +++{code}
++ {title} +
+{description}
+{children}++ }>Back to home + + ) +} diff --git a/src/components/post-card.tsx b/src/components/post-card.tsx new file mode 100644 index 0000000..bb8b9b2 --- /dev/null +++ b/src/components/post-card.tsx @@ -0,0 +1,69 @@ +import { Link } from '@tanstack/react-router' +import { ArrowUpRight } from 'lucide-react' + +import { Badge } from '@/components/ui/badge' +import { getAuthor } from '@/content/authors' +import { formatPostDate, type Post } from '@/content/posts' + +// A single entry in the blog stream / index. Shared between the landing page's +// latest-posts section and the full blog index. +export function PostCard({ post }: { post: Post }) { + const { meta } = post + + return ( + ++ {meta.cover ? ( ++ ++ ) : ( + + )} + + {meta.tags[0] ? ( +
+ {meta.tags[0]} + + ) : null} +++ + ) +} diff --git a/src/components/root-document.tsx b/src/components/root-document.tsx new file mode 100644 index 0000000..ee1556d --- /dev/null +++ b/src/components/root-document.tsx @@ -0,0 +1,21 @@ +import { HeadContent, Outlet, Scripts } from '@tanstack/react-router' + +import { ThemeProvider } from '@/providers/theme' + +export function RootDocument() { + return ( + // suppressHydrationWarning: the inline theme script sets the `dark` class on + // before React hydrates. + + ++ + {getAuthor(meta.author).name} + + + + + {meta.readingTime} ++ ++ {meta.title} +
+ ++ {meta.description} +
+ + + Read ++ + + + + + ++ + + + ) +} diff --git a/src/components/site-footer.tsx b/src/components/site-footer.tsx new file mode 100644 index 0000000..9f864e9 --- /dev/null +++ b/src/components/site-footer.tsx @@ -0,0 +1,107 @@ +import { Link } from '@tanstack/react-router' + +import { SITE_NAME } from '@/lib/site' +import { Separator } from '@/components/ui/separator' + +type FooterLink = { + label: string + // Set `to` for in-app links (routed via TanStack), or `href` for external + // ones. Omit both for destinations that don't exist yet; they render disabled. + to?: string + hash?: string + href?: string +} + +const FOOTER_SECTIONS: { title: string; links: FooterLink[] }[] = [ + { + title: 'Explore', + links: [ + { label: 'Blog', to: '/blog' }, + { label: 'Latest', to: '/', hash: 'latest' }, + ], + }, + { + title: 'Built with', + links: [ + { label: 'TanStack Start', href: 'https://tanstack.com/start' }, + { label: 'React', href: 'https://react.dev' }, + { label: 'Tailwind CSS', href: 'https://tailwindcss.com' }, + ], + }, + { + title: 'More', + links: [{ label: 'GitHub', href: 'https://github.com/labset' }], + }, +] + +function FooterItem({ link }: { link: FooterLink }) { + const className = + 'text-sm text-muted-foreground transition-colors hover:text-foreground' + + if (link.to) { + return ( + + {link.label} + + ) + } + if (link.href) { + return ( + + {link.label} + + ) + } + return ( + + {link.label} + + ) +} + +export function SiteFooter() { + return ( + + ) +} diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx new file mode 100644 index 0000000..02a0717 --- /dev/null +++ b/src/components/site-header.tsx @@ -0,0 +1,118 @@ +import { Link } from '@tanstack/react-router' +import { Menu } from 'lucide-react' + +import { SITE_NAME } from '@/lib/site' +import { Button } from '@/components/ui/button' +import { + Sheet, + SheetClose, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from '@/components/ui/sheet' +import { ThemeToggle } from '@/components/theme-toggle' + +type NavLink = { + label: string + to: string + // Optional hash for links that target a section on the home page. + hash?: string +} + +const NAV_LINKS: NavLink[] = [ + { label: 'Blog', to: '/blog' }, + { label: 'Latest', to: '/', hash: 'latest' }, +] + +const GITHUB_URL = 'https://github.com/labset' + +export function SiteHeader() { + return ( + + + ) +} diff --git a/src/components/site-layout.tsx b/src/components/site-layout.tsx new file mode 100644 index 0000000..a97bc9f --- /dev/null +++ b/src/components/site-layout.tsx @@ -0,0 +1,23 @@ +import type { ReactNode } from 'react' + +import { SiteHeader } from '@/components/site-header' +import { SiteFooter } from '@/components/site-footer' +import { cn } from '@/lib/utils' + +// The shared page shell: sticky header, flexible main, footer. Pages pass their +// content as children and can extend the+ + {SITE_NAME} + + + + +++++ + } + > + GitHub + + + + ++ + + } + /> + + ++ + +{SITE_NAME} +classes (e.g. positioning). +export function SiteLayout({ + children, + mainClassName, +}: { + children: ReactNode + mainClassName?: string +}) { + return ( + ++ ) +} diff --git a/src/components/theme-toggle.tsx b/src/components/theme-toggle.tsx new file mode 100644 index 0000000..12b479e --- /dev/null +++ b/src/components/theme-toggle.tsx @@ -0,0 +1,20 @@ +import { Moon, Sun } from 'lucide-react' + +import { Button } from '@/components/ui/button' +import { useTheme } from '@/providers/theme' + +export function ThemeToggle() { + const { resolvedTheme, setTheme } = useTheme() + const isDark = resolvedTheme === 'dark' + + return ( + + ) +} diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..3ad4586 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,37 @@ +import type { ComponentProps } from 'react' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + 'inline-flex items-center gap-1 rounded-4xl border px-2 py-0.5 text-xs font-medium whitespace-nowrap', + { + variants: { + variant: { + default: 'border-transparent bg-primary text-primary-foreground', + secondary: 'border-transparent bg-secondary text-secondary-foreground', + muted: 'border-transparent bg-muted text-muted-foreground', + outline: 'border-border text-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Badge({ + className, + variant = 'default', + ...props +}: ComponentProps<'span'> & VariantProps+ {children} ++ ) { + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..919d889 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,75 @@ +import type { ComponentProps } from 'react' + +import { cn } from '@/lib/utils' + +function Card({ className, ...props }: ComponentProps<'div'>) { + return ( + + ) +} + +function CardHeader({ className, ...props }: ComponentProps<'div'>) { + return ( + + ) +} + +function CardTitle({ className, ...props }: ComponentProps<'div'>) { + return ( + + ) +} + +function CardDescription({ className, ...props }: ComponentProps<'div'>) { + return ( + + ) +} + +function CardContent({ className, ...props }: ComponentProps<'div'>) { + return ( + + ) +} + +function CardFooter({ className, ...props }: ComponentProps<'div'>) { + return ( + + ) +} + +export { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, +} diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..4f65961 --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,23 @@ +import { Separator as SeparatorPrimitive } from "@base-ui/react/separator" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + ...props +}: SeparatorPrimitive.Props) { + return ( + + ) +} + +export { Separator } diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx new file mode 100644 index 0000000..850a230 --- /dev/null +++ b/src/components/ui/sheet.tsx @@ -0,0 +1,77 @@ +import type { ComponentProps } from 'react' +import { Dialog } from '@base-ui/react/dialog' +import { X } from 'lucide-react' + +import { cn } from '@/lib/utils' + +const Sheet = Dialog.Root +const SheetTrigger = Dialog.Trigger +const SheetClose = Dialog.Close + +const sideClasses = { + right: + 'inset-y-0 right-0 border-l data-[starting-style]:translate-x-full data-[ending-style]:translate-x-full', + left: 'inset-y-0 left-0 border-r data-[starting-style]:-translate-x-full data-[ending-style]:-translate-x-full', +} as const + +function SheetContent({ + className, + children, + side = 'right', + ...props +}: ComponentProps & { + side?: keyof typeof sideClasses +}) { + return ( + + + ) +} + +function SheetHeader({ className, ...props }: ComponentProps<'div'>) { + return ( + + ) +} + +function SheetTitle({ + className, + ...props +}: ComponentProps+ + ++ + {children} ++ ) { + return ( + + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetTitle, +} diff --git a/src/content/authors.ts b/src/content/authors.ts new file mode 100644 index 0000000..573816a --- /dev/null +++ b/src/content/authors.ts @@ -0,0 +1,28 @@ +// Blog authors. Add an entry per person; posts reference an author by key +// through their `author` meta field. Optional fields (role, avatar, url) can be +// filled in as we grow the team. +export type Author = { + name: string + // Optional short role or title shown alongside the name. + role?: string + // Optional avatar path under /public. + avatar?: string + // Optional link to a profile or personal site. + url?: string +} + +export const authors = { + hasnae: { + name: 'Hasnae', + }, +} satisfies Record + +export type AuthorId = keyof typeof authors + +export function getAuthor(id: AuthorId): Author { + const author = authors[id] + if (!author) { + throw new Error(`Unknown author "${id}". Add it to content/authors.ts.`) + } + return author +} diff --git a/src/content/posts.ts b/src/content/posts.ts new file mode 100644 index 0000000..06d0c9a --- /dev/null +++ b/src/content/posts.ts @@ -0,0 +1,74 @@ +import type { ComponentType } from 'react' + +import { authors, type AuthorId } from '@/content/authors' + +export type PostMeta = { + title: string + description: string + // Author key, resolved against the registry in content/authors.ts. + author: AuthorId + // ISO date, YYYY-MM-DD. Used for sorting and display. + date: string + readingTime: string + tags: string[] + // Optional cover image (a path under public/). When absent, the listing draws + // a generated gradient instead. + cover?: string +} + +// The compiled MDX body. It accepts an optional `components` map so posts can be +// rendered with our styled elements (see components/mdx/mdx-components.tsx). +export type PostContent = ComponentType<{ components?: Record }> + +export type Post = { + slug: string + meta: PostMeta + Content: PostContent +} + +// Every .mdx file under posts/ is a published entry, grouped into year/month +// folders. Slug is the filename, so the folders never touch the URL. +const modules = import.meta.glob<{ meta: PostMeta; default: PostContent }>( + './posts/**/*.mdx', + { eager: true }, +) + +// .mdx meta literals are not type-checked by tsc, so validate them here. This +// runs at import (build/prerender) and fails loudly on a malformed post rather +// than crashing later when a component dereferences the missing field. +function toPost(path: string, mod: { meta: PostMeta; default: PostContent }): Post { + const slug = path.split('/').pop()!.replace(/\.mdx$/, '') + const meta = mod.meta + + if (!meta) throw new Error(`Blog post "${slug}" is missing its \`meta\` export.`) + for (const field of ['title', 'description', 'author', 'date', 'readingTime'] as const) { + if (!meta[field]) throw new Error(`Blog post "${slug}" is missing \`meta.${field}\`.`) + } + if (!Array.isArray(meta.tags)) throw new Error(`Blog post "${slug}" is missing \`meta.tags\`.`) + if (!authors[meta.author]) { + throw new Error( + `Blog post "${slug}" has unknown author "${meta.author}". Add it to content/authors.ts.`, + ) + } + + return { slug, meta, Content: mod.default } +} + +export const posts: Post[] = Object.entries(modules) + .map(([path, mod]) => toPost(path, mod)) + // Newest first. + .sort((a, b) => b.meta.date.localeCompare(a.meta.date)) + +export function getPost(slug: string): Post | undefined { + return posts.find((post) => post.slug === slug) +} + +export function formatPostDate(date: string): string { + // Parse as UTC so the displayed day never shifts with the viewer's timezone. + return new Date(`${date}T00:00:00Z`).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }) +} diff --git a/src/content/posts/2026/07/file-based-routing.mdx b/src/content/posts/2026/07/file-based-routing.mdx new file mode 100644 index 0000000..bb2e0d3 --- /dev/null +++ b/src/content/posts/2026/07/file-based-routing.mdx @@ -0,0 +1,24 @@ +export const meta = { + title: 'File-based routing, explained', + description: + 'Each file under src/routes becomes a route. Here is how the root route, index route, and dynamic segments fit together.', + author: 'hasnae', + date: '2026-07-18', + readingTime: '6 min read', + tags: ['routing', 'tanstack'], +} + +The `__root.tsx` route owns the HTML document and renders an `Outlet` for its +children. Every other file under `src/routes` maps to a URL by its name. + +## Dynamic segments + +Dynamic segments use a `$` prefix: `blog/$slug.tsx` matches `/blog/anything`. +The matched value arrives as a typed route param, so you never reach for a +loosely-typed `params` bag. + +## The generated route tree + +The route tree is generated for you into `routeTree.gen.ts` whenever you run the +dev server or a build — you never hand-maintain it. Because it is generated, +navigation, params, and loader data are all checked at compile time. diff --git a/src/content/posts/2026/07/hello-tanstack-start.mdx b/src/content/posts/2026/07/hello-tanstack-start.mdx new file mode 100644 index 0000000..bfcc7eb --- /dev/null +++ b/src/content/posts/2026/07/hello-tanstack-start.mdx @@ -0,0 +1,35 @@ +export const meta = { + title: 'Hello, TanStack Start', + description: + 'Why we rebuilt the website template on TanStack Start — server rendering, type-safe routing, and static prerendering out of the box.', + author: 'hasnae', + date: '2026-07-24', + readingTime: '4 min read', + tags: ['tanstack', 'react'], +} + +TanStack Start pairs the file-based, fully type-safe routing of TanStack Router +with a Vite-powered full-stack runtime. That means server-side rendering, +streaming, and static prerendering without leaving your React app. + +## What ships in this template + +This starter keeps things minimal: a document shell in the root route, a home +page, and a small blog stream backed by an MDX content layer — a solid starting +point you can grow into. + +- **Type-safe routing** — route params and loader data flow through TypeScript. +- **Static prerendering** — every page is crawled from the homepage and written + to HTML at build time. +- **Author-controlled styling** — Tailwind tokens in OKLCH, headless Base UI + primitives wrapped with shadcn patterns. + + + Posts are MDX, so you can drop React components right into the prose when plain + Markdown isn't enough. + + +## Why it matters + +Refactors stay honest because the types follow you everywhere, and readers get +real HTML on first paint instead of a loading spinner. diff --git a/src/content/posts/2026/07/the-content-layer.mdx b/src/content/posts/2026/07/the-content-layer.mdx new file mode 100644 index 0000000..5c00648 --- /dev/null +++ b/src/content/posts/2026/07/the-content-layer.mdx @@ -0,0 +1,25 @@ +export const meta = { + title: 'An MDX content layer', + description: + 'Posts are MDX files discovered at build time with import.meta.glob. This blog stream is powered by exactly that.', + author: 'hasnae', + date: '2026-07-11', + readingTime: '5 min read', + tags: ['content', 'mdx'], +} + +Posts live under `src/content/posts/YYYY/MM/` as MDX — Markdown with the option +to drop in React components when you need them. Each file exports a `meta` +object and its body compiles to a component. + +## One glob to rule them all + +`import.meta.glob('./posts/**/*.mdx', { eager: true })` loads every post module +up front. We validate each `meta`, map it to a slug and content component, then +sort by date. Routes consume that single, centralized array. + +## Slug from filename + +The slug comes from the filename, so the `YYYY/MM` folders keep things tidy +without affecting URLs. Add a file, and it shows up in the stream — no registry +to update. diff --git a/src/data/posts.ts b/src/data/posts.ts deleted file mode 100644 index 66b6a4f..0000000 --- a/src/data/posts.ts +++ /dev/null @@ -1,55 +0,0 @@ -export type Post = { - slug: string - title: string - excerpt: string - /** ISO date, e.g. "2026-07-20" */ - date: string - readingMinutes: number - tags: string[] - content: string[] -} - -export const posts: Post[] = [ - { - slug: 'hello-tanstack-start', - title: 'Hello, TanStack Start', - excerpt: - 'Why we rebuilt the website template on TanStack Start — server-side rendering, type-safe routing, and server functions out of the box.', - date: '2026-07-24', - readingMinutes: 4, - tags: ['tanstack', 'react'], - content: [ - 'TanStack Start pairs the file-based, fully type-safe routing of TanStack Router with a Vite-powered full-stack runtime. That means server-side rendering, streaming, and server functions without leaving your React app.', - 'This template ships a minimal setup: a document shell in the root route, a home page, and a small blog stream backed by server functions — a solid starting point you can grow into.', - 'Everything is typed end to end. Route params, loader data, and server function inputs all flow through TypeScript, so refactors stay honest.', - ], - }, - { - slug: 'file-based-routing', - title: 'File-based routing, explained', - excerpt: - 'Each file under src/routes becomes a route. Here is how the root route, index route, and dynamic segments fit together.', - date: '2026-07-18', - readingMinutes: 6, - tags: ['routing', 'tanstack'], - content: [ - 'The __root.tsx route owns the HTML document and renders an Outlet for its children. Every other file under src/routes maps to a URL by its name.', - 'Dynamic segments use a $ prefix: blog.$slug.tsx matches /blog/anything. The matched value is available as a typed route param.', - 'The route tree is generated for you into routeTree.gen.ts whenever you run the dev server or a build, so you never hand-maintain it.', - ], - }, - { - slug: 'server-functions', - title: 'Loading data with server functions', - excerpt: - 'Server functions run only on the server, yet you call them like any async function. This blog stream is powered by one.', - date: '2026-07-11', - readingMinutes: 5, - tags: ['data', 'ssr'], - content: [ - 'createServerFn defines a function that always executes on the server. You can read from a database, hit an API, or — as we do here — return static content.', - 'Route loaders call these functions before a route renders, so the data is ready during SSR and hydration. No loading spinners on first paint.', - 'Because the boundary is explicit, secrets and heavy dependencies stay on the server and never ship to the browser.', - ], - }, -] diff --git a/src/lib/blog.ts b/src/lib/blog.ts deleted file mode 100644 index a834ee5..0000000 --- a/src/lib/blog.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createServerFn } from '@tanstack/react-start' -import { posts, type Post } from '@/data/posts' - -/** Lightweight shape for the blog stream — omits the full post body. */ -export type PostSummary = Omit- -const toSummary = ({ content: _content, ...summary }: Post): PostSummary => - summary - -const byNewest = (a: Post, b: Post) => b.date.localeCompare(a.date) - -export const getPosts = createServerFn({ method: 'GET' }).handler( - async (): Promise => - [...posts].sort(byNewest).map(toSummary), -) - -export const getPost = createServerFn({ method: 'GET' }) - .validator((slug: string) => slug) - .handler(async ({ data: slug }): Promise => { - return posts.find((post) => post.slug === slug) ?? null - }) diff --git a/src/lib/format.ts b/src/lib/format.ts deleted file mode 100644 index 1cf8b7f..0000000 --- a/src/lib/format.ts +++ /dev/null @@ -1,11 +0,0 @@ -const dateFormatter = new Intl.DateTimeFormat('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', -}) - -/** Format an ISO date string (YYYY-MM-DD) as e.g. "Jul 24, 2026". */ -export function formatDate(iso: string): string { - // Parse as UTC so the day doesn't shift across timezones. - return dateFormatter.format(new Date(`${iso}T00:00:00Z`)) -} diff --git a/src/lib/site.ts b/src/lib/site.ts new file mode 100644 index 0000000..65e7749 --- /dev/null +++ b/src/lib/site.ts @@ -0,0 +1,44 @@ +// Shared site-wide constants used for canonical URLs and social cards. +// Change SITE_URL to your deployed origin. No trailing slash; build paths as +// `${SITE_URL}/blog/...`. +export const SITE_URL = 'https://example.com' +export const SITE_NAME = 'website-template' +export const SITE_DESCRIPTION = + 'A minimal starter built with TanStack Start, React, Tailwind, and shadcn — server-rendered, type-safe, and ready to grow.' + +type SocialMetaOptions = { + title: string + description: string + url: string + image?: string + type?: 'website' | 'article' +} + +// The Open Graph + Twitter card tags shared by every route's `head()`. Callers +// still own the page , meta description, canonical link, and any +// page-specific tags (robots, article:published_time). Pass `image` once you +// have social cards to point at. +export function socialMeta({ + title, + description, + url, + image, + type = 'website', +}: SocialMetaOptions) { + return [ + { property: 'og:type', content: type }, + { property: 'og:site_name', content: SITE_NAME }, + { property: 'og:title', content: title }, + { property: 'og:description', content: description }, + { property: 'og:url', content: url }, + { name: 'twitter:card', content: image ? 'summary_large_image' : 'summary' }, + { name: 'twitter:title', content: title }, + { name: 'twitter:description', content: description }, + ...(image + ? [ + { property: 'og:image', content: image }, + { name: 'twitter:image', content: image }, + ] + : []), + ] +} diff --git a/src/mdx-env.d.ts b/src/mdx-env.d.ts new file mode 100644 index 0000000..a0e2999 --- /dev/null +++ b/src/mdx-env.d.ts @@ -0,0 +1,10 @@ +// Type declarations for MDX blog posts. Each post default-exports its rendered +// content component and named-exports a `meta` object describing the entry. +declare module '*.mdx' { + import type { ComponentType } from 'react' + import type { PostMeta } from '@/content/posts' + + export const meta: PostMeta + const MDXContent: ComponentType<{ components?: Record }> + export default MDXContent +} diff --git a/src/providers/theme/index.ts b/src/providers/theme/index.ts new file mode 100644 index 0000000..b5b50fa --- /dev/null +++ b/src/providers/theme/index.ts @@ -0,0 +1,3 @@ +export { ThemeProvider } from './theme-provider' +export { useTheme } from './use-theme' +export type { Theme, ResolvedTheme } from './theme-context' diff --git a/src/providers/theme/theme-context.ts b/src/providers/theme/theme-context.ts new file mode 100644 index 0000000..3de6fad --- /dev/null +++ b/src/providers/theme/theme-context.ts @@ -0,0 +1,14 @@ +import { createContext } from 'react' + +export type Theme = 'light' | 'dark' | 'system' +export type ResolvedTheme = 'light' | 'dark' + +export type ThemeContextValue = { + theme: Theme + resolvedTheme: ResolvedTheme + setTheme: (theme: Theme) => void +} + +export const THEME_STORAGE_KEY = 'theme' + +export const ThemeContext = createContext (null) diff --git a/src/providers/theme/theme-provider.tsx b/src/providers/theme/theme-provider.tsx new file mode 100644 index 0000000..52e7152 --- /dev/null +++ b/src/providers/theme/theme-provider.tsx @@ -0,0 +1,68 @@ +import { useEffect, useSyncExternalStore, type ReactNode } from 'react' + +import { THEME_STORAGE_KEY, ThemeContext, type ResolvedTheme, type Theme } from './theme-context' + +// External store: theme lives in localStorage + the OS colour-scheme media +// query. useSyncExternalStore reads it SSR-safely (getServerSnapshot) with no +// hydration mismatch and no setState-in-effect. +const listeners = new Set<() => void>() + +function subscribe(onStoreChange: () => void) { + listeners.add(onStoreChange) + const media = window.matchMedia('(prefers-color-scheme: dark)') + media.addEventListener('change', onStoreChange) + window.addEventListener('storage', onStoreChange) + return () => { + listeners.delete(onStoreChange) + media.removeEventListener('change', onStoreChange) + window.removeEventListener('storage', onStoreChange) + } +} + +function prefersDark() { + if (typeof window === 'undefined') return false + return window.matchMedia('(prefers-color-scheme: dark)').matches +} + +function readStoredTheme(): Theme { + if (typeof localStorage === 'undefined') return 'system' + try { + const stored = localStorage.getItem(THEME_STORAGE_KEY) + return stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system' + } catch { + return 'system' + } +} + +function resolveTheme(theme: Theme): ResolvedTheme { + if (theme === 'system') return prefersDark() ? 'dark' : 'light' + return theme +} + +export function ThemeProvider({ children }: { children: ReactNode }) { + const theme = useSyncExternalStore(subscribe, readStoredTheme, () => 'system' as Theme) + const resolvedTheme = useSyncExternalStore( + subscribe, + () => resolveTheme(readStoredTheme()), + () => 'light' as ResolvedTheme, + ) + + useEffect(() => { + document.documentElement.classList.toggle('dark', resolvedTheme === 'dark') + }, [resolvedTheme]) + + const setTheme = (next: Theme) => { + try { + localStorage.setItem(THEME_STORAGE_KEY, next) + } catch { + // Ignore storage failures (private mode, disabled storage). + } + listeners.forEach((listener) => listener()) + } + + return ( + + {children} + + ) +} diff --git a/src/providers/theme/use-theme.ts b/src/providers/theme/use-theme.ts new file mode 100644 index 0000000..d4a6251 --- /dev/null +++ b/src/providers/theme/use-theme.ts @@ -0,0 +1,11 @@ +import { useContext } from 'react' + +import { ThemeContext } from './theme-context' + +export function useTheme() { + const context = useContext(ThemeContext) + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider') + } + return context +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts deleted file mode 100644 index f3e4d5f..0000000 --- a/src/routeTree.gen.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* eslint-disable */ - -// @ts-nocheck - -// noinspection JSUnusedGlobalSymbols - -// This file was automatically generated by TanStack Router. -// You should NOT make any changes in this file as it will be overwritten. -// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. - -import { Route as rootRouteImport } from './routes/__root' -import { Route as IndexRouteImport } from './routes/index' -import { Route as BlogSlugRouteImport } from './routes/blog.$slug' - -const IndexRoute = IndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => rootRouteImport, -} as any) -const BlogSlugRoute = BlogSlugRouteImport.update({ - id: '/blog/$slug', - path: '/blog/$slug', - getParentRoute: () => rootRouteImport, -} as any) - -export interface FileRoutesByFullPath { - '/': typeof IndexRoute - '/blog/$slug': typeof BlogSlugRoute -} -export interface FileRoutesByTo { - '/': typeof IndexRoute - '/blog/$slug': typeof BlogSlugRoute -} -export interface FileRoutesById { - __root__: typeof rootRouteImport - '/': typeof IndexRoute - '/blog/$slug': typeof BlogSlugRoute -} -export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/blog/$slug' - fileRoutesByTo: FileRoutesByTo - to: '/' | '/blog/$slug' - id: '__root__' | '/' | '/blog/$slug' - fileRoutesById: FileRoutesById -} -export interface RootRouteChildren { - IndexRoute: typeof IndexRoute - BlogSlugRoute: typeof BlogSlugRoute -} - -declare module '@tanstack/react-router' { - interface FileRoutesByPath { - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexRouteImport - parentRoute: typeof rootRouteImport - } - '/blog/$slug': { - id: '/blog/$slug' - path: '/blog/$slug' - fullPath: '/blog/$slug' - preLoaderRoute: typeof BlogSlugRouteImport - parentRoute: typeof rootRouteImport - } - } -} - -const rootRouteChildren: RootRouteChildren = { - IndexRoute: IndexRoute, - BlogSlugRoute: BlogSlugRoute, -} -export const routeTree = rootRouteImport - ._addFileChildren(rootRouteChildren) - ._addFileTypes() - -import type { getRouter } from './router.tsx' -import type { createStart } from '@tanstack/react-start' -declare module '@tanstack/react-start' { - interface Register { - ssr: true - router: Awaited > - } -} diff --git a/src/router.tsx b/src/router.tsx index de065cc..6f4925c 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,15 +1,15 @@ -import { createRouter as createTanStackRouter } from '@tanstack/react-router' +import { createRouter } from '@tanstack/react-router' + +import { NotFound } from '@/components/not-found' import { routeTree } from './routeTree.gen' export function getRouter() { - return createTanStackRouter({ + return createRouter({ routeTree, + defaultPreload: 'intent', scrollRestoration: true, + // Site-wide fallback for any unmatched route (the blog $slug route + // overrides this with its own notFoundComponent). + defaultNotFoundComponent: NotFound, }) } - -declare module '@tanstack/react-router' { - interface Register { - router: ReturnType - } -} diff --git a/src/routes/404.tsx b/src/routes/404.tsx new file mode 100644 index 0000000..07268ea --- /dev/null +++ b/src/routes/404.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' + +import { NotFound } from '@/components/not-found' + +// Prerendered to dist/client/404.html (see vite.config.ts `pages`) so a static +// host can serve our branded not-found page for any unmatched URL. In-app +// navigation to a missing route uses the router's defaultNotFoundComponent. +export const Route = createFileRoute('/404')({ + head: () => ({ + meta: [ + { title: 'Page not found · website-template' }, + { name: 'robots', content: 'noindex' }, + ], + }), + component: NotFound, +}) diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index ce32ea8..e3de360 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,36 +1,34 @@ -import { - HeadContent, - Outlet, - Scripts, - createRootRoute, -} from '@tanstack/react-router' -import appCss from '../index.css?url' +import { createRootRoute } from '@tanstack/react-router' + +import { RootDocument } from '@/components/root-document' +import { SITE_DESCRIPTION, SITE_NAME, SITE_URL, socialMeta } from '@/lib/site' +import appCss from '@/index.css?url' + +const TITLE = `${SITE_NAME} — TanStack Start starter` +const HOME_URL = `${SITE_URL}/` + +// Apply the saved theme before paint to avoid a flash of the wrong colour +// scheme. Keep the 'theme' key in sync with THEME_STORAGE_KEY in +// src/providers/theme/theme-context.ts. +const themeScript = `(function(){try{var t=localStorage.getItem('theme')||'system';var d=t==='dark'||(t==='system'&&window.matchMedia('(prefers-color-scheme: dark)').matches);if(d)document.documentElement.classList.add('dark');}catch(e){}})();` export const Route = createRootRoute({ head: () => ({ meta: [ { charSet: 'utf-8' }, - { name: 'viewport', content: 'width=device-width, initial-scale=1.0' }, - { title: 'website-template' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { title: TITLE }, + { name: 'description', content: SITE_DESCRIPTION }, + { name: 'robots', content: 'index, follow' }, + ...socialMeta({ title: TITLE, description: SITE_DESCRIPTION, url: HOME_URL }), ], + // Canonical is page-specific and lives on each leaf route so it isn't + // duplicated; TanStack dedupes but not . See routes/index.tsx. links: [ { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }, { rel: 'stylesheet', href: appCss }, ], + scripts: [{ children: themeScript }], }), - component: RootComponent, + component: RootDocument, }) - -function RootComponent() { - return ( - - - - - - - - - - ) -} diff --git a/src/routes/blog.$slug.tsx b/src/routes/blog.$slug.tsx deleted file mode 100644 index adc3bd6..0000000 --- a/src/routes/blog.$slug.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Link, createFileRoute, notFound } from '@tanstack/react-router' -import { ArrowLeft } from 'lucide-react' -import { getPost } from '@/lib/blog' -import { formatDate } from '@/lib/format' - -export const Route = createFileRoute('/blog/$slug')({ - loader: async ({ params }) => { - const post = await getPost({ data: params.slug }) - if (!post) throw notFound() - return post - }, - head: ({ loaderData }) => - loaderData - ? { meta: [{ title: `${loaderData.title} — website-template` }] } - : {}, - component: BlogPost, - notFoundComponent: PostNotFound, -}) - -function BlogPost() { - const post = Route.useLoaderData() - - return ( - - - ) -} - -function PostNotFound() { - return ( -- - -- - · - {post.readingMinutes} min read --- {post.title} -
-- {post.content.map((paragraph, i) => ( - -{paragraph}
- ))} -- - ) -} - -function BackLink() { - return ( - -- Post not found
-- The post you are looking for does not exist or may have been moved. -
-- Back home - - ) -} diff --git a/src/routes/blog/$slug.tsx b/src/routes/blog/$slug.tsx new file mode 100644 index 0000000..c7eb38f --- /dev/null +++ b/src/routes/blog/$slug.tsx @@ -0,0 +1,38 @@ +import { createFileRoute, notFound } from '@tanstack/react-router' + +import { BlogNotFound, BlogPostPage } from '@/components/blog-post-page' +import { getPost } from '@/content/posts' +import { SITE_URL, socialMeta } from '@/lib/site' + +export const Route = createFileRoute('/blog/$slug')({ + loader: ({ params }) => { + if (!getPost(params.slug)) throw notFound() + }, + head: ({ params }) => { + const post = getPost(params.slug) + // notFound path: keep the missing post out of search indexes. + if (!post) { + return { + meta: [ + { title: 'Post not found · website-template' }, + { name: 'robots', content: 'noindex' }, + ], + } + } + + const title = `${post.meta.title} · website-template` + const url = `${SITE_URL}/blog/${post.slug}` + + return { + meta: [ + { title }, + { name: 'description', content: post.meta.description }, + ...socialMeta({ title, description: post.meta.description, url, type: 'article' }), + { property: 'article:published_time', content: post.meta.date }, + ], + links: [{ rel: 'canonical', href: url }], + } + }, + component: BlogPostPage, + notFoundComponent: BlogNotFound, +}) diff --git a/src/routes/blog/index.tsx b/src/routes/blog/index.tsx new file mode 100644 index 0000000..9692ebf --- /dev/null +++ b/src/routes/blog/index.tsx @@ -0,0 +1,20 @@ +import { createFileRoute } from '@tanstack/react-router' + +import { BlogIndex } from '@/components/blog-index' +import { SITE_URL, socialMeta } from '@/lib/site' + +const TITLE = 'Blog · website-template' +const DESCRIPTION = 'Notes, updates, and the thinking behind this template.' +const URL = `${SITE_URL}/blog` + +export const Route = createFileRoute('/blog/')({ + head: () => ({ + meta: [ + { title: TITLE }, + { name: 'description', content: DESCRIPTION }, + ...socialMeta({ title: TITLE, description: DESCRIPTION, url: URL }), + ], + links: [{ rel: 'canonical', href: URL }], + }), + component: BlogIndex, +}) diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 6b22a4b..0848da7 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,103 +1,11 @@ -import { Link, createFileRoute } from '@tanstack/react-router' -import { ArrowRight } from 'lucide-react' -import { getPosts, type PostSummary } from '@/lib/blog' -import { formatDate } from '@/lib/format' -import { Button } from '@/components/ui/button' +import { createFileRoute } from '@tanstack/react-router' + +import { LandingPage } from '@/components/landing-page' +import { SITE_URL } from '@/lib/site' export const Route = createFileRoute('/')({ - loader: () => getPosts(), - component: Home, + head: () => ({ + links: [{ rel: 'canonical', href: `${SITE_URL}/` }], + }), + component: LandingPage, }) - -function Home() { - const posts = Route.useLoaderData() - - return ( - - - ) -} - -function Hero() { - return ( -- - - - website-template - - - ) -} - -function BlogStream({ posts }: { posts: PostSummary[] }) { - return ( -- Build fast, ship faster. -
-- A minimal starter built with TanStack Start, React, Tailwind, and - shadcn — server-rendered, type-safe, and ready to grow. -
-- }> - Read the blog --- - - } - > - TanStack Start - - - - ) -} - -function PostCard({ post }: { post: PostSummary }) { - return ( - ---Latest posts
- - {posts.length} {posts.length === 1 ? 'post' : 'posts'} - -- {posts.map((post) => ( -
-- -
- ))} -- - - · - {post.readingMinutes} min read --- {post.title} -
-{post.excerpt}
-- {post.tags.map((tag) => ( - - #{tag} - - ))} -- - ) -} diff --git a/tsconfig.app.json b/tsconfig.app.json index 26c49b5..71062e0 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -23,6 +23,7 @@ "noFallthroughCasesInSwitch": true, /* Aliasing */ + "ignoreDeprecations": "6.0", "baseUrl": ".", "paths": { "@/*": ["./src/*"] diff --git a/tsconfig.json b/tsconfig.json index aa3c04f..58ed6e9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "ignoreDeprecations": "6.0", "baseUrl": ".", "paths": { "@/*": ["./src/*"] diff --git a/vite.config.ts b/vite.config.ts index 3f383bc..8beb961 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,15 +1,28 @@ import { defineConfig } from 'vite' import { tanstackStart } from '@tanstack/react-start/plugin/vite' -import viteReact from '@vitejs/plugin-react' +import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' +import mdx from '@mdx-js/rollup' import path from 'path' // https://vite.dev/config/ export default defineConfig({ plugins: [ + tanstackStart({ + // Prerender the whole site to static HTML, crawling links from the + // homepage to discover every reachable page. + prerender: { enabled: true, crawlLinks: true }, + // Emit the /404 route as dist/client/404.html; static hosts serve it + // (with a 404 status) for unmatched URLs. crawlLinks never reaches /404 + // since nothing links to it, so it is listed explicitly. + pages: [{ path: '/404', prerender: { enabled: true, outputPath: '/404.html' } }], + }), tailwindcss(), - tanstackStart(), - viteReact(), + // MDX must run before react's plugin so the JSX it emits gets transformed. + { enforce: 'pre', ...mdx() }, + // react's plugin must come after start's plugin; include mdx so posts go + // through Fast Refresh and the automatic JSX runtime. + react({ include: /\.(jsx|js|mdx|md|tsx|ts)$/ }), ], resolve: { alias: { From e9ed0e813575859605d12ab321a426b302485b12 Mon Sep 17 00:00:00 2001 From: HasnaeDate: Sat, 25 Jul 2026 15:09:00 +1000 Subject: [PATCH 07/11] use the canonical shadcn components everywhere Replace the hand-rolled ui primitives with the real shadcn (base-luma) components from the site app, and make sure each is actually used. - button: forward render + nativeButton, so `render={}` no longer emits an invalid type="button" on anchors (now role="button") - badge: canonical useRender-based variant set (default/secondary/outline/ ghost/link/destructive) - card: canonical size-aware Card with Header/Title/Description/Content/ Footer/Action - sheet: canonical four-side sheet with built-in close, Header/Footer/Title/ Description - add a features section that uses Card + Badge (Card was previously unused) and link it from the header/footer nav Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/features-section.tsx | 105 +++++++++++++++++++++++ src/components/landing-page.tsx | 2 + src/components/site-footer.tsx | 2 +- src/components/site-header.tsx | 4 +- src/components/ui/badge.tsx | 53 +++++++----- src/components/ui/button.tsx | 6 ++ src/components/ui/card.tsx | 57 +++++++++---- src/components/ui/sheet.tsx | 126 ++++++++++++++++++++-------- 8 files changed, 282 insertions(+), 73 deletions(-) create mode 100644 src/components/features-section.tsx diff --git a/src/components/features-section.tsx b/src/components/features-section.tsx new file mode 100644 index 0000000..1e1ce51 --- /dev/null +++ b/src/components/features-section.tsx @@ -0,0 +1,105 @@ +import { + FileText, + Gauge, + MoonStar, + Palette, + Route, + Server, + type LucideIcon, +} from 'lucide-react' + +import { Badge } from '@/components/ui/badge' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card' + +type Feature = { + icon: LucideIcon + title: string + description: string + tags: string[] +} + +const FEATURES: Feature[] = [ + { + icon: Server, + title: 'Server rendered', + description: 'TanStack Start renders on the server, so readers get real HTML on first paint.', + tags: ['SSR', 'streaming'], + }, + { + icon: Route, + title: 'Type-safe routing', + description: 'File-based routes with params and loader data checked end to end by TypeScript.', + tags: ['router', 'typescript'], + }, + { + icon: Palette, + title: 'Tailwind + shadcn', + description: 'OKLCH design tokens and headless Base UI primitives you fully control.', + tags: ['tailwind', 'base-ui'], + }, + { + icon: FileText, + title: 'MDX content', + description: 'Author posts in Markdown, drop in React components, discovered at build time.', + tags: ['mdx', 'content'], + }, + { + icon: MoonStar, + title: 'Dark mode', + description: 'A pre-paint theme script and system-aware toggle, with no flash of the wrong theme.', + tags: ['theming'], + }, + { + icon: Gauge, + title: 'Static prerendering', + description: 'Every page is crawled from the homepage and written to static HTML for fast, SEO-friendly loads.', + tags: ['SSG', 'SEO'], + }, +] + +export function FeaturesSection() { + return ( + + + ) +} diff --git a/src/components/landing-page.tsx b/src/components/landing-page.tsx index 2a72b09..ad0ebb9 100644 --- a/src/components/landing-page.tsx +++ b/src/components/landing-page.tsx @@ -1,11 +1,13 @@ import { SiteLayout } from '@/components/site-layout' import { HeroSection } from '@/components/hero-section' +import { FeaturesSection } from '@/components/features-section' import { LatestPostsSection } from '@/components/latest-posts-section' export function LandingPage() { return (++ ++ Everything you need to start +
++ A batteries-included foundation, wired up and ready to extend. +
++ {FEATURES.map((feature) => ( +++ + ))} ++ + ++ + {feature.title} +{feature.description} ++ ++ {feature.tags.map((tag) => ( +++ {tag} + + ))} +) diff --git a/src/components/site-footer.tsx b/src/components/site-footer.tsx index 9f864e9..457f5ff 100644 --- a/src/components/site-footer.tsx +++ b/src/components/site-footer.tsx @@ -16,8 +16,8 @@ const FOOTER_SECTIONS: { title: string; links: FooterLink[] }[] = [ { title: 'Explore', links: [ + { label: 'Features', to: '/', hash: 'features' }, { label: 'Blog', to: '/blog' }, - { label: 'Latest', to: '/', hash: 'latest' }, ], }, { diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx index 02a0717..82b151d 100644 --- a/src/components/site-header.tsx +++ b/src/components/site-header.tsx @@ -21,8 +21,8 @@ type NavLink = { } const NAV_LINKS: NavLink[] = [ + { label: 'Features', to: '/', hash: 'features' }, { label: 'Blog', to: '/blog' }, - { label: 'Latest', to: '/', hash: 'latest' }, ] const GITHUB_URL = 'https://github.com/labset' @@ -81,7 +81,7 @@ export function SiteHeader() { + - {SITE_NAME}
Connect with us
-Join the TanStack community
---
-
-
- GitHub
-
-
- -
-
-
- Discord
-
-
- -
-
-
- X.com
-
-
- -
-
-
- Bluesky
-
-
-
-