From 93a4c9ab70c63c17c5921b6b8ea8d2e9dcc31963 Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Thu, 9 Jul 2026 15:41:22 +0200 Subject: [PATCH 1/6] Add dynamic OG images for docs pages --- src/components/Layout/Page.tsx | 8 ++- src/pages/api/og.tsx | 108 +++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/pages/api/og.tsx diff --git a/src/components/Layout/Page.tsx b/src/components/Layout/Page.tsx index aa39fe5fc2b..d958281ac4c 100644 --- a/src/components/Layout/Page.tsx +++ b/src/components/Layout/Page.tsx @@ -129,7 +129,13 @@ export function Page({ title={title} titleForTitleTag={meta.titleForTitleTag} isHomePage={isHomePage} - image={`/images/og-` + section + '.png'} + image={ + isHomePage || !title + ? `/images/og-` + section + '.png' + : `/api/og?title=${encodeURIComponent( + title + )}§ion=${encodeURIComponent(section)}` + } searchOrder={searchOrder} /> {(isHomePage || isBlogIndex) && ( diff --git a/src/pages/api/og.tsx b/src/pages/api/og.tsx new file mode 100644 index 00000000000..01862e371c5 --- /dev/null +++ b/src/pages/api/og.tsx @@ -0,0 +1,108 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {ImageResponse} from 'next/og'; +import type {NextRequest} from 'next/server'; + +export const config = { + runtime: 'edge', +}; + +const SECTION_LABELS: Record = { + learn: 'Learn React', + reference: 'API Reference', + community: 'Community', + blog: 'Blog', +}; + +// Satori doesn't support woff2, so the site fonts can't be reused here. +// Source Code Pro matches the code-styled titles used across the docs. +const fontPromise = fetch( + 'https://raw.githubusercontent.com/adobe-fonts/source-code-pro/release/TTF/SourceCodePro-Semibold.ttf' +).then((res) => res.arrayBuffer()); + +export default async function handler(request: NextRequest) { + const {searchParams} = new URL(request.url); + const title = searchParams.get('title')?.slice(0, 80) ?? 'react.dev'; + const section = searchParams.get('section') ?? ''; + const label = SECTION_LABELS[section] ?? 'react.dev'; + const fontData = await fontPromise; + + return new ImageResponse( + ( +
+
+ + + + + + + + +
+ {label} +
+
+
24 ? 64 : 96, + color: '#f6f7f9', + lineHeight: 1.1, + wordBreak: 'break-word', + }}> + {title} +
+
+ react.dev +
+
+ ), + { + width: 1200, + height: 630, + fonts: [ + { + name: 'Source Code Pro', + data: fontData, + style: 'normal', + weight: 600, + }, + ], + } + ); +} From 533541a186fb68f0fa03c4b7f779c58705c67701 Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Thu, 9 Jul 2026 17:12:05 +0200 Subject: [PATCH 2/6] Match OG card design to existing static images --- public/fonts/Optimistic_Display_W_Bd.ttf | Bin 0 -> 60800 bytes public/fonts/Optimistic_Display_W_Md.ttf | Bin 0 -> 61004 bytes src/components/Layout/Page.tsx | 2 +- src/pages/api/og.tsx | 102 ++++++++++++----------- 4 files changed, 55 insertions(+), 49 deletions(-) create mode 100644 public/fonts/Optimistic_Display_W_Bd.ttf create mode 100644 public/fonts/Optimistic_Display_W_Md.ttf diff --git a/public/fonts/Optimistic_Display_W_Bd.ttf b/public/fonts/Optimistic_Display_W_Bd.ttf new file mode 100644 index 0000000000000000000000000000000000000000..9d0cff450f5d555915aa7d910209da4748d56e26 GIT binary patch literal 60800 zcmeFa2~-qE_b6V~-Lo(ZvoZ_o%s6baGwcd7vI(e&3IZwu3W6&NqKG26E58o`lh4E@W-&2|xFserGR?cyGa#D$zW1H;&j0-1dFLCf>F(<4s=L*# zdvDzy5k?5f;fq1CUdbt`9_Ku6A+$9HYCrCk)op!8tt79uh6ubNuJqe*M%MhZ+jw`O2z*Gy~hWj

!G<2#H3+{RUAe*OUDw zIpD*j2EKEE?s-513dv9iDu&?_`2T`Bj2LDK^$y}8#H-;s!z1TxN5+CqQUirj%27a5 z6#QeF5UMaElmTskD+nd`?AsGHA;h-XU=XxYDyUvT?$Hl<93_ww^9d>;HF&$NP$D$Q zUee*1PPew*r^cZ9bSv^BN5FhD)Ys5UQ4Ji6pbQ{|eiIczA;$rvFZwRv4peGCQtP4r z-=OpbiMR&%iQpW#gX+lWwhylajiT=%U*=bo#r%xA^9Gc3N z5S}+eX@Zgk#RSFKex47Jh;HFQlNAh)+F0UbHjRzmEc_-;ocT zcm22RK6u`a60%-&8j2Ofqgc^sIF=xVAPPzrQZTns026_FGxtyquR985+K`f`MMBj>7p;D2Ji^3Z)ndxxbIljR{84@LV6B0(n#8k%fMSCXvEB zkFt3;Q5M}7NvN+-U;aH*#NUGQm~7OII)%Kbhsa1LqR1D@T@(rYh@?J6Q=quewJ4Aj zsu|>{8TnIJQ4$n?>J}PJ6(AodT1JO_plAj2Q7G_h5c;m|edNZv@=l^i-Wg=!y^8Yq zVmM|(Q6dZ98LnNBg{nsu<`~lP_QUaaWDyL4dr;5shVpoI_TyQ2rYi~qK6l}Li5Bx_ zAcNo*^5^wO2I$YgD?|&JOq9fYiW-F*S>)~L;Iix`Uwi5OX0a~sDhb{y!k7TKNE#C(2pC)TQYMH^@hI4vlV;| zQVD%+g=a=8QuvWjCO};UiWL6E;kcML5N+o_L`i}vFxGDF*#UfU+g-j(+g*Ya^LyL9 z4txpjaF6*B+MaCtmcT+CLDCL53CyeD9^ipcTL=^#eHZKCm=7h_c4T4puy>$vN4OtI z=-*C5Li=`l6I!>|bG!on08e}goe3>F=t1acqg^qfTSq;iJJ1?vf;gH1eeAGg!f_Ui z4H|}h2g+iQ(GT|3_hpMhrp$9X6_04|5;__qDn-hca;+&J7AZC5zj6MozKgXgC~DF%Ew{7acvmF=P7_(mb7 zbBxR-j<=oX4$PBIFn-w1V_vI`o}dG`IRP{r%<*>}QgJfh!Q-~Ab{Z~#F)Luq>2L&X z&CY}KC7t@-_fPl}x$8vNW&eaTH(xm!;^g-e==V#IqoGi8fM;3l=iFF8Lk?#m&jdG{ zOsyn%6a2ZcfNq?;fc$c}0KExKL1s$GTn0Ll6_|$N4BxQA!jWOf+Fpaqu$IQanG>wD zv2ZmQ6`@)*18qYG&|Byz7GNcI!$ur{!*LIshI`{2JOq!yGblIeL+U2Y&_Y^FD`*vM zr2Ra-J)=G6dT#RkRqv>m>z(zkdQW{9y^lUr-%X#a-=jaSf7j5Zi{cr6Mzd{gZ7?nw zS&u?dH`Egift7w5tkG}6$ZwZ=fCMARL>~IXoOs!ZYv!{3_mv_u%9B9sB|Q1ph$sDKRAhto4)^w1f}Uh4P{N zD1SIJ*=o32xNiS+wA9;cXt2Y;Za@Q2Uo;MAJ{>HL3N#e)(PXfzW`Gn3fck}KI9N*y zLGRY1nZS>cI2_GFqtR?M0;!NA8i=w%LUK?Uo`Wg@kNKzsO+{6>3{618P&sNuKcH%~ z0A-dI2N0+I}XH)@oL-+_e5jRSTqOap%-365xbwc;Ipf1!IKhR% z08i*swB^Zq3og>9SW+jKH>MON2L<8-j-n)EQmH5?5FHRX!l@&iS^SI>4q(3?m^-2T zQo0?WkkE+?YoWbUic2h6{qs_i-3*3&t_~$}eOdTP76I3nev}1ZMvL?Z0#7%-a#V(f z7X^q*j3vcG^DJ~RJk&_1G&at+$O9~0jmeg-wcl%jfTfl|V{(cmzzBf#9?&rWw(z`V zMt$QG1i%|x?g7-DYKrYOd~ewkL~arkJIG|g#qH-{iY5VsfSRLjWXw8T;oQL*Z?=4@BtQ z4uzu#luzKWsFQ#xJ}HfPB_l1mBDWHd&yo5(H-p8T59G@?=9T6XE*WK(uD4-0_y7aB zrvSH?TDLa_v@HT}p+1jtqw@*3phBMtU&h2ZxFNIfZI^_jiE;Wo?1tK#!6^0?y$ zep}xtH1;0YNJci=J4E`%bPEEJHN$j`QAF4V#HpYujj6>(FjZ0;i;uQ73~xMOHaAX4 zDJt&<^Ptg~UeahBm=_1ko=@rp#zYIOc%TP4bOz=fZ9Dw}KL3p6KR4$3FuzO^QmeP<`{x0?MIC(; z^tn4Q;K006_ppC51J@xlddVDiI$q19Gp@c)|pCDrPVmLuo|OkOEr~MjVD!==*(-OzO4#Z z~pI;vk+>nzK2F%7KOh`Qj?T zbSa5yT{0ANqR=sQ>I#!iw*!B@qiqL;cIa(i<{h_pFlw|NC*B6zXFE1;$7s83yMFtG z?M>S)+xcjF=yvn=OWPS4L2%#p;ILG*O}0(HjqZ`Ptzp};ZB5%AZsVD^Wo^3+Ck%pX z%Qjk!w&HH5wqmqZzqMg2jo^zag0I_K=~G+UwopY|mTjTXmIgSS-g0RR&$8wAmWNwt z^NNNQRKtp<6_jp8!V0RW=28vSRD)-m8ggct>T_nAX5=VMiX6E~mg8g+=kQESj?^T{ zaWsi?1SWnCHlZ9T9}68!!W`N}<{sIfi89XHK0LDx5=Pq81JQ90bJO<_+q??*N67zdoO2|2a>imF!VO0UoKUI^e zCRM{ft_-Uh2B&bJ9ID|2j+3ebFlmm+7o4F0sI7+cNl>aLaW&)?JXK}8fM)|>2wP92 zE4V`*MO6XXVPMYl-hy~g4RV2^1>eyCe$0-2pooVh<2=d5u|2j$prb zr|y%;FVeQ%MOspa4x?4*6qHrm75Q3?Hj~0MVm^Y_gkC_gx6!+(8F~eek9rd|p?bcY z`IMi__lEu_LMetw!Sm0T=wsB(US)x%|NZ=z0{^AJe<|=EQy>kc{Kswo+wlLG0u;ne z;WGdwbEWV9afkmucF@+pEu3v-ze7%Kzrx?2Z4gWR@8|zY3P{@?{a*>!|ARo`t6;Nz ziiB-{{y&J^e~ywHWQ35vD3l42`}q(bZ-eN04aC-8!#nW>{0O&Ej+BP-|F?f7)NJZB z?LbG<^XcpK6DFJ4!C081%xUHvbBVde+-B}F51BuCp}b7qeBP(Lr~GvOCjN1ONH9V$ zR!}XNA($^%CRitUL$F)$mf&5%XF{IPT^KFwD;y!5D*VYI++nH1Em4N3N%V_jgyTBL zodfxvdS*j?%ubz2zcvNpYFu^0lkXHO%#x>r=NV zw;H#1-5$Hoa^LU%xrfxFkH;{NH$A@alz3)&zUp~Y=b%f_jn}Q#UDm7h{q+;|ALy?c zJPp-`Z@UzA*=eL9^mN3F?={-%V{giPuJ<24Lwr8)WqkYlPWL_PC-TeoTjICc?_Sr? zu9LgI(e<%^g#RS}Zv$ijDFIUhW(DjB_%z_xK-a*;z)^v(25t!48+a=4v%v2H{|Isj zatjIyN(dSlR2DQXXmQZypaVhg2YnTEKNtlog1v&Hf-{1L22Ti{6TB{XPw;!ep9kLx zZVhn@@d}9z$qX40QWLT&WKYO@Azy_25{g1qq5h!>q1mD1Lg$5U2(^Ts4gD(gk1%nV zAuKAacUVzaby#EA=CHTIE`sm8sG6y6LFtqUi^dHQYHoAUrWV zKfE%0aro-+_rmXmKZy`U=prH_(j$gNR7EU^crD^!#D@_#A|6HZBi$lHBU2&=M^;46 ziCh=CFY^7!uOc5rIYfCyMMtGa4T!3aS{wCx)Z0-PqP~s#GukQID>^nhGkQdHP4wdE zH=>V3e-eE&`f&^$qm2oQNsP&i84*(xvpD9Bm?JUY$I4?}W7A`TmPi#-^7KKA=q zYd3YbkZ#G{^1D@bThwi9w|BdJ(d|KZVRw)25#7_f*LL6A{q63bb-&mBNt`S$ATBvB zH*Rd)%(&HYyW>vBU5@)c?vHp;ye>W}zIS|4d}aKc_;s*bTEb3X%E4=-VOLCiNProH zLN0_{2n|!p4RUXT+)%IcQ8Fr)XdVZAd7VGophigBQPU>^=)0r{pbA+=e86?}{& z&Xh0<^ThoE0wPdCB#e^Iz7D%O-C;LR{FXyCqG6Z|FpknUD%$7yP+NyotGlNT>(_6@ zZ1(kaiPcVndSn!ity*#yM&x?nM&#Q@Y-8;sssKc}20Lb9p|pKOs_lN;kMvdE1*Ag( z$ipnrh>=gVULwwrD%R;?^j1@gNoM_29vd2lBch_BqoblCBYcd;E&|^uuW-Pepw1Tv z_k|VEHcL2Kc;8S zg?VfC6z>{8u%;w3I;Lb0t8b?v@M}I${(FvJQY3pDiK_8h1|Yj-8Ghn*d@ZWx3s8dfQyD>-!?zPY zFa#mL4Sz#B#?ES>K(vO>7^!Z65Mll@JDRPTTmMjS7NA@e^hW_68 zgR#>P3+Vf;3Ir0dx$Q?m7)UP2U^+s+h@rlNg$ZO4@{>SBAiu2$zsfrV^TFvY2$sMO zXo4a(#75c;M(k>&i>QItz0ec1q&3je2lkjr4^DV4*FzDVjgNv1NdQ_JO?ME0Y$t@@ zY#~C!K?^hzaFT-gX~SmD%BfMuCg;-GmEDIg=&ZwLGP z@OOJP4OdnzT2ify$PL2%tjFoY?3TB_n)UN*F0+^KC@!ih8(HZfx`@O6xWb-s`*H80 z%aE=?fz5>&D?lWIu?=RalM}^Y@k)_EKncYZ0|W(Fnysd01&|d45k{7WtBkhQB9mH; zQ?6cRe`fO_+aUNFUD>+(340j_KOyNG7*znaJD_hb6m|^B+HRY*N_CoSgA_}}Wh$*! zj+rvl#mTu;4&$^CvJ<>pOa-QZ(6H#2Q1(?CR0cYV6C_UW0$aQ)PNu$5uADS`c9k-< zvitQ=da8B0GGca`+pJ5q^*=Sa?PPy{vSV1mr1J7f!6D_;6)Ss1?^AVU*|IB>X4xh8 z1jvC71tGmz=3C~hR_mQ)Wnv*uZx`Wqpg1-VN#+#4M&M%;i%#G?murD1MXg`chSsme z!Z+PkwPYMM&*|w}zfD?w-F^BewGDSSX_aFd8pbI143`X;=&}q4{rn8)S*pvoW%Rgm zdw=C83m09l|7&tt`Q&92XT<;=!S`o+aMCV)n}9#!R!` zcveDkqQ(L34*+g%s4I#_o@S|vZz}Wf2=FSC@B-TBC(yek7Ul|w7-)FgGTVU?r?nU( z{ao%~zA9h1q8hC1&lTefhEBP)-gCpP$wP;ZAJ6{6dbk%?Ru;P#RaO?cPnb1#ocq|h zGhKc>TD>nUpV+Zq$1?WnFVEN=7V62eiL;|8mz7N+ zn$rky9tJr1$atJwHv#F#bDZ*;O0L^A4Ie=~9ghxQY=`~D_!odNfkaQ6dllIwW5Udmku{!C_fY};MPV&lWy zb^{AXQLkRArqm1gGbczIB$Q+U`@h{T+;*>f)`X}n>}sZ(2ITTY(bA`!=@ zrpAlod-c+Oe*F07q`WhxynO7~^71htsR|er!(K*by*vY;enBq_e7(cv*k?~UrdlYM z>1&T!;W3Q-;Mvmvt1Hr(<>I9d*kOT_Y$k5dV(mQAMN0uu%>_j2BfZ;AF`}8heq&PC6r%Bu`dMo=rmHt-YguP3(=>uxQ!ovKzhB?lgVp%^+RXb$TD}+~I zvr_k{N1P@GZREtzjtzXwpXY!vW1}I_#0AZqfd*qm;&7shRTvhkO>8-?zcXtVJN@X! zSMmF7_Rfts1zzmH$O~{h12`H`H?y~9vt&Q;{q<$8O|F!yD+*C#n@G%4&Y;O)3#bls zTM8zEdMIcq2q)OSh%8W@sAE9~>5aJJUz2LP4o?(EnG>QM1Cx!*JgKhiWCwj_SLw7Z z`&Ma}%?OADq{H1*y*r7%9Q0c38j7pu6rPV6m0g`?6cp8@_Bukx(NhT?YUHr zny)1x_=hcNHh+3wzbe z#;@P8S|&Ed$D721BBfc{0Xt{Te5W)ey4puqJgRW+_=#Kgm;$>PA zHj8*mAxCZ=ZR3kgC~c~^9A~jr)F}1@)&0;LR^n;-00%AthALQRx}rd{i;Tzfng5m= zmT$Dw9lI|O&1}$P{Y&`8$~n3Ng3+Aw!%a*9S^0=Yj*I|xrg8>tY(K+QoA=G1ux+_) z`PK>Z_id7h%gV&%<>IMR#kJa*Wiw(T#}2Qn8$LENW=7de{Bv4*dK!Cd0DOQ0T>-wY z>>P+N>z8>pc~YLQJK97=f|hz??*+~?U!Dyu1%wSSA77XYHvMZ?B(`N5V1$Lmu6=R- zOAOesbG2+lQ(@uuv9vfRN}3f&xS>srp5(0`KB{QmxQSZ^?3mT?&PaSRE+yO);1@!) zTr5>Zi+Rf_0jz9@S_Cx;Y7vEbQ=ultUgHNfr@1z<9c`+ptz4UK_8LC4xyPFhPj$D~ zcu`f<8Lmy7y#~gje&+gt_k_5y0^q5Yys2O(_psOaK%0wPn*@7JSE%`lt1*)rgjp|G z8Sa4Qroo5CBn>qo0c=!A17*FK4cO;dDX#`>Q=7(?05^!nR+$~u3qoO9&a|uQgo5Xe z_djdujayEi-Xhz2^5j;jBtAJgUJ{>@(xItOmD@jrhHZBRG;bML(!I$$J8-S zhrD5`5o~;h_L^Q z@J%RVgWWeFZKd|Mi@CPl8RH9WX?5qe{`R)-+1mmy0CvJz(l71HwFPPiwCS0h{5)8H z1Ca^!FzYq0uKs1RP(hhnTUXq*s|>4^O1X>|#_-F$Ac-6-@C)H!^_k!x50~47+ysU= zHyzo$@G3t zE*yX5X11m0?DT-hrHw=WyuofR$jHj2T&q%#-9GQxa(KH3E^%-QAF`zE(07XS=Oweo z@6H@3VRs8f!9&+oANyAJ5pyl6ryPel^|$uPOqH>x)F3atA%?-iimwIj;%in&|MPUqz4wlf+7o%kExGHU zxZ^cf*KN zdV^gG5EG}z*AN;0+^B0;^w$f07SNZQSFq?XdqQo4e!r9A}``3_T5iQPGsVk9^3Nrw#VV%%;O~&*%LU0J;DBT z^3v2x+wh?6tJ$OL*W)pz7pFfKEx?ECQ;Ap>%Ulm~o`u!DS>r#vLPfnL8zP zr$Y1z`a=CF{Y5=bub24ymIVYkDO6>02@e7i?QU&ILG!I)1qFn}1X?9P3M-5;GCax_ z($c8xo(%vC<}ycMH7u}E(JC;l^mv)e1>m2MF;)7<7>i^Hwfk7=5U zVyW)`YII4T88LlD7xpo`IuRc`?Wy-No?7}@m9ulJvu6C*QwF0!e|pR~nbTvhKfVII z;2k68SD?B($~60FG%^LI^$X&!*B zIMVsF2#XX>Bc(P6&3g705E=+xSWrNSZGcEhq`^e&L?AM;77c0*Sa1+SGpZR4O)qCp zt-_0+hFE)3hg_FWW2=}Y?7cYU*=7&+(PQZwIC9wnOpp9z-HEh*pPf!GJhA;$_0!pi zBKt=?B~VZeIw08W#&=mMQL0B$GFmFBkkm_-N*Ia6S*%$p;QVZuW6ihP1;95lJQBDs zH|QCaGh^U{6vPx0r-_6cgtu?sraU1V---Q|eaJq+1K53R3vRo2Z}ls$?Ekp-0k!(k zQ0BQA-ILP@9d`DfmtBTTkP#)f{Y7IuN zu7Td=jxwo0*ye~GQAoip(+8kwLFJMYP`zLS$-`~@gcF!szQh)SZw)c|HPISk4+%5^ zU*h%oM%y5^TN*#F-kY4fclCiWixi8-99XlDyBPh7;+4^V$c9%{4VM)|2HWbr<5#V! z*t_aLZ0y0+``~>1-c=SjpI%lzZCZKRbl~_V>KM}vXiGdA2O3@{ffq_3Y|aVa^bnZ) zFp1z5B8bFp!UKQD16k{Z3)C^{l(i>rX2FQSXbZA3k<2uRy6VjeDPPB@_>5qch`b0! z3E_Uog<)hqUI0tVTMkkINCO+>_%V9`_xla^V-Hwy+I^hH9=*>VCH+w;Oe7r+Yn#JC z3ev+uNRS+aX$$=-m2&?+tkzp#Mny88Q9ke<%pkB+3J|iM0InRxrHr0Y!jl3NVD?~E z@!)wnl)gmMu|+i2)2C^QTxZcV4eXd$Ijo@Nmc4fjBOIm6Ti4R%wC=ax*wo*CgMOBx z6-+vF2XK>uz2a$>Nnyilx>}6DXqk>AtHdb8daI=XCMjUoIeJUF>wHa(Ngp~iJ!9xl z>sRz~wu}9O-ZnV3SAKr4)WK}i{rhZY`xQ{(FtX5hm~+6F6th3reta@LdB7az0gW%w z_!NyB=w);h4Z0g=(KvzN2XC5$6xcuEAYU#aTCic|;UHVy@D7ih$D{AGUZO*-cPKXt z-gp~tWS_KJq2phXg)wrYrJ4iyJc`0JyiEh)Vgdbt!Iv0(iop%cGNy^4D;S)`-~WAh=^s)wyG4>AJ9)8O{Up2AH?5+TAN{#GJ0^tK7Cx0Fk<2XX~bYAjhZS9v!eqQ_eO6v3K z3$quWAOEz8U-Yzz&=g`-%stR~Zm^z2n%$+2!j(K%HD2kiWONECPsgKRqB%J*8WJ$K z4KlGA))dfU#H;|h3|GRU!j*8^pbV#BmE($0X`qz`rD1ZfwIB9ae!ADwURVlWufAuy zU?26GjHNx;M>z8)?s5}nvIj}I$=G_Qk-Pg5Pi8yeSQ=^A;@q zfxrIgNN6(&OXvmA#u1|YVn)zgL^7tan8)q$#D;{(09=?somK7<_6O|xHFjg~UPw0I%+5l`-V%82uD6sS^3L#RX9v=!2KwYW?bIZJhxXKWnGA$n(o}E$fajI4iClhqZs_R#1 zE6+}!doen$B5CyAkqZ~D8Qyn***~#wJbQ8a_C>#qV81PoPkPh?CwXdmygI55=5`ij zc7umab_u|v?rHXxYW+eSVohSV*eE0B^Fk_>IvslGjvxAAcRwX__e7+gNZatsNHk| zNU8)@Wwiq%q8%wQb^;Luh02GppcD#W<8zHlbSn0Z_Kw!j;dHcy2R0CPJ#>iuE#;Z| zrutb5``e*IuTZ;ZUNm1Gg=Or+QJ2jZXZ}EBt1o?sS`4~^#1~9vXUVc6ghSyiSE>P4 z7YjNK>kD}sLgX6&s@`3-jBIC;wIBe@0WbyZYxX|}A4;ch+&EoEz9izXxH!nhh>N2S zt)4t(_39~;SHG4J8VW&=&;)>@hBU0c9Y1<;;fkg4X?zEQ-G zEl-9EMUr*GhuEfMp-@M2TfSp1`SBuHe)YC)ndx zuCT{XG}pIm-FC0O{@%8&E%j5cAD?=xylQyX)aer^)S9~W8#)dY82p-uk#Pf=AUn7r zpmVyJJ-tn#2Hzl)NpDchH=icN@YkD9L%gcNWxkMz1YqxavzdIiz_bgwV3NVkD<>ah z>Etl8t&z|7#R%rU;8#GsK&>(Q`1l%CN+Y;rkr9;V@~@^&{c5@PRdRS$qh59J;41aX zg9qI|8(J_pDtwMAMAI`fGqV?Eti3vK{?*#rtMlhwtzETe#mYTcm$DkFX|_%1~}}3x}ZFBTt6r5v`B=_q9PF`dR;CBWlFU{Wcdx1jvvBL z;L{cSdj3*A&F9PexmG}c{B^CS2~cm6$Z2q)TML@QTPA{MSuhcfm9mzJ6HODrM6%^1 zghL>f+b8BwSAlTyeNEAk#58k$IAr3i1uJU8dbv*tSTa~KYsLQbtdgL~=1A(N^4!6d z^i;iHVRFjyg=~G{m}LV@-a0dZ-bme{E`VMnGgWp(@;Yi#zs?6hT3Y{i^Kks?!-rQZ z*Bm~)MlMV4+c#O3)UO|P=k)5;r%BmX)W3gmasU2A#3S*3+KD&;#Fwai?n^{c*YN%y zegI#>-U6(`$=nXszrD8ic<7&cq-T&GZ7l>F{;7pvq`k$IHc#p>?*{l3lEV%{xsrLVuT0F;6Zi|`v`gqY;&bEL0fZ?+z#6VBDsN-Bi2}-wA&w@+d+zg%|_wD6rwZPpR7-? z1{xCRPPTbb>v<RanNWf^9Tg_5@fopy9$*C-IF>KDohPf9F}~9eBbYp5WV` zpsEB4x{5Dre*%1Y(2H{a^XKu68+VvXcQ{)p37*&mPk13OvqG#EDypP1S7D_mc!FJQ z!x6)wC6=gqZa>%uvYm`z!T?&6z}^~t`NTx82AbY<&Bpi=>U=NVl+Mqdkd$*W)vRa!*l>|qqCQaD<&;p zK55djW%%f;m6flOLZ0$LGpH203Vc!7VJ<)bZYIz%1ne$vI#OwXts*a$1+(%mES`a@ zM7V~X$Ic~BYT?N!`;*`@W5M$$!Cf*aRkSDOv%g`3)j9)yY+)L^Si~-XSqURDLU_&x zK{p^dbMgD!?1cX4r(FF!u6`b=2VD}%&JgtHO$Hw+8zqAyU>bhqu-)7Hza)!OL&QpEOt;LpHLW)`4_59E3bb zTja&2{<$>BiS4;}7uu8EfXA#WrEHr*m%i{+XQ5(0;*uu|3n^vCV-)zah_?WIFgc0r znDLe{PB7~b28VD@<8XzZ8X@CGUpNceZIaZAG zhacOLz+O}axmb_81S#LdyTZoC#EiX*zr)u)|D5$>J?>_0%g)}GMfF^beH$CuFG06i zVElnFz67F*o**;6LZOBylczhnBxvv{4Gz&HXbLr(HTyLeHJ}=hEFDuW>BKIFyf2U$ zFc%dh9!q=@5{fl&6l%{YflO24Y18Bgf@7h^KKgUSj7`hgr+@TkKg5X>M{Q#l;;Aj; zcr~YXmA&rkeRTHW^UOkaXLjCh>$~jEJ%e&!_T+)*Fc?-yJ#sONo#Yu}A?>cs@F4z> z-P3>38f$lXh#_K^P)J|=^Aq;+_FXvSL$CALBkLE_jw$YA_mo`Q>R$V%=|L8I`%_3o zOT76d&Nz1B^;xAg<1erI_QBX;GuIx3c}1vn7^oydO3>coG)y_A*=b-y+HL`Ih>|H{ zq2h1iggtxM_ikeLJM7VX;|8zhFx!0oJllMjscv2J_``GX0tR@vV3=Kjo*j{^*-4Pb z<2f+UyF+{LKw+=~Y>`6ZSsGLpMs9VaO064c0k5WwJ=4mbZnO7efqop2#_Y&T1GkZe zer)~|*fO?Gz=$TDSQup0QG>19p##KjZnNGckPs>_2P%7`E@rt$x=yEclG#aZ=tOE8 zM4gS_zmc2sh9#7dmVUvW+qoZiyJUnAe9ph(_ttf(I5g_|i7t~~3uhat#eUm=-Q;oz z_x$2Z-20t3Us+c?w&LpAub!HGZa=m*`4uGE@Zyc)=o1C9>A<6zG@U3!v{^(;MLJP} zsGq1pR4-a8+Alf-e)^9W@Q>*l(x&VH|F?x6#e^EIh+)*#ay&$hsl%cLj1bJ4hgHPLRQkMugC|nu3MFNn9ih z2^q9T%4zr0j9nP+4mX&tNqKc|C%%G z{zco^73cQg^l8>*?Q*sXMuC9NVNAi#9&$6dojew-r_Sk8i!yaUEzo4k}EzM*8eVjm1j#1M5oY&Y8Xk-Q8<-hB-;(^*fMP zdp>H0kXB?6-rMt0Uv|!3%x|L|udpBQ+J&R8cwNEKyLPc3T`_h!c>VgpE(gE*%H;&^ z1lUznjWc~kpTz;XmHU+3sr*K z_g9W#KlU2-TRwk2uZC^Cd6TuW=}ax#ed5H4V4V}(5Qm!vW+P;*%T&n8MGqLdWGD%W zp8}3V<#IyzBBJ1N8OXZ(Z_!=5a8VKaNzc6Og8u>O57@JX`}fw>DgTD~B*0t_xm)gL ziP%AvE|kfTBHcde$a)aYEmU#?*=K_ADOsvui?lNpD&RM?luMW8@94`mr>sq9kKsu6 zJdT8YCSJ{N7gv7~R>%IjWG?(N6)RA%C2%_w8O?HEfmVV%^cn72 zy7L_5LcZXoa;t%DD(Z zymI!yg@0`K*oWoC2L>J~cc0!^J>*K{%H>7u7i^nL`8b$u%@WIPQ&pthB zPTij3tbu9i35u?wz`+}8KKh=>I)pHpkAPkRBz>EY(_qsD=1&5*MQB$^_y;zQc@xSc z%&@J`AU_PF#UR&x;3*aCL^;YCMkM7?BD-Wmc!bMOAl4R9G0&9^gdeEuC)ht1Vcqt| zE{)r<=OXs$2^WZG?Wg;+9@(&a_Xaw>^{xHvGqQ7Ufc0!BjG-W_juZ8@!vhD(!2t{o z0$?m`c56x23V2~;cbjZ)bNkzrfRYs)YD&OS!INgErp%obghP5Xy;bl9zJ|a1@hCfm ztvP%c-sf6)^hef%_4|Tg1Hb7ecnd~yCb~qR6pDmGNI|A4c_&>`XN&Y0;D>Jw3WX8^ z$MDTv(KK!vcQzSEg)Hdv6bJm-=apa_l6-a?yXdRyxHokf4`J_F_puM@Ut689j2ZwN z4%@83>|wg5bpaWj;28>|yP-a2uSV^9?ax|zly<6?8VuQnGLci-T+u4g9T7cJG+9LD zz^?AmwufdZ)Qf1jmf^dpgid_0!bt2}6B__KN~C~#g8lv%cmC~y4mkjZ;rDB%{!)Z{ zC+^70+tm|C5C3KI)RtoQ?WEoL`Fncq$D?;{!)3dsv-cWiVny9PcHNHc?3z7wSUYnz zdk+#nZBPdR)H2|XqZDJJVC=jB0jLY%WWXSYb)X~n014!jb|L$06pl-@6ck#_*fff5 zT}bgS1P91H67i`thQb2dl3;?&ctDWkf5Xn5 zLB4>xZcSi(tiIGwpVO~n>RRh?mIV!!)OL^l8eR@?fvDP>=ILrtnmA8P>BLwl#@EHT zR*Xl9F<0jz?kbKI(@-xJ3q@KHMZRK0v{IBSr(Hb>s&d_b>)KtpgGVpic?2&W^T6!r!95W;~hA1;Q2=dIlT(dQ4&Z0>&``Ff+< z*`L^FF?%(6Qw`^b=QzKyVE%sICleQykG^%yy7$$rLD%{u_39Q;4)kaM{WKe*1qzTk zH?u^UE0;LB=F&U?hM$XuNd@~+@Xxm15=CUs>4*JnRW!Irpr%N4P#{;U8?bcDaku)j z6DFLkcRN|}y>e_r!&qhd1fy{R?=9<*O>hX9Gt`e$Z3k zhmNz6Ww~OZz)h3uZleyj8^+<^5wz>12kj;@SSv{@zi(*xZk2n(&G-vGBh9X5wbRO6 zdXDpZKlxwvE_E&2-)xyNbK;!%l(lo`uI(1FV#HXoR>8;&iIdndmx#p3Ef>-{l)18Y z-f)JPeQSasPj+yC>^A36Zi|5zg1JNxkq`3zr8 zB_J#X(WroYL~wG`q7J$F4AT2~x(Rw5a*}e?3BLivu?0QDZ%qV?mTaEdl59w9gzOR# zsU^uHWEOD=8n)SFf-DV_4SZYP>Wh(JnnVq_>{W179imOT(O(=r!(L{8z5CY*(})8< zeC)DjY_~P00C$tK*(>GFd6jHXLBSyMwd(Pz#jBp`J<@TA(BNTSy)>ZZLC8SKbawe^=obt7A$LZ@^&2{n^*=!4E6jC^Lhbu_m+3Po5^HdWmvHYee)K$ecLZ_9qzq zk~HFpkeA$?h|o4CB-qx!cB+9hVz=%f0TO67_FOjZUjA*(^w!DjTf?r6>lvJ(_|81DdUdY?*>@HG@tlM~oP2=W~7^>k?pheqZa-c6vnvM9#!o5{pF0CQBWl02~|^b}(L$CPCC| zrS5MYwo4QJaC3fPXJNAI&+*W^-H*u?3KY3SaoYWu&bo@YlrPRMDs$uA`ue*Y-8bHC zXt=x4&2@Zz{dm`Lpe`3YSij-^f(7?CtbefJ@oTeYZ`?S0)@wvpHn2mP*)ZPol}hU9 z#;sI*hyp!dsgz&3mwIo3l&LPB8mJqdmJ zBk)d)DuD#Kc;snS0xlngGf?aaDR%Fq^GhZ6ER=uDKQZFJ-uNc1soR6er4Sh#py*z64FenaB3x2}5an{zIuCF?R0f&;vRl?KDg2`eomqa-k(i=W)6TU)#A zAc0WT_L%Mt|MP$bqKr~n&N$_W;2ly!-^iP3EeCRVq%EiHzqGuU6^LUWKg!i5OHIBB;;BVn!+8?2KhdZMCj z)^{u2XZ_gg&9Iq+>X(f<5~b)xOz~dCCUva;Vg33a>Q~pb)*b)7c0|$E_=F{^ve!S( zO15`rn%A$TcZ%B^`e4b!paaYg{NsbG^BGGn?B{UOz9-EuWbDJ^tj_&Oo{ ze-R-q;e-tY9kTfj*t{@#$g=PPIxkJ7x|>VgXWUADGk9{=R`e0d0@c?K;DRpyI2fA<0*7=bkqv55BATYjY^vDXPX*Vxh$}R=5QG~fp3D> zKUlQ?VBr#7{-Tw8?ZT;;k0YEEg251A} zjUdQp2(Js83NU+i!0d|ya_BAqb60N;=;nmKK~3IY|Mzg)40EEUxWCRO-b+Y)@=muR zbwu)jLh5V4Ae3Oi>(M7KuTS5+yuYI({89{vwaN%Gq&&CU2xe>&hgmdALWyQ$kGhzt z0YN$bzS0-)GvO#3ei8ozPl(X`#~8v}XYDhbNErG1&yYQ50pqhbVPIUceSYIS$XPCJ)pDzL``om0i^>xvEUNl~UcEpPI@{s4 zy_3^87NUis5Nz`R*c}BHcL>744)17) zn#XQovaN|!u=O%^vDKO7hh%%fD+%KZkKe_2@U<)U%|JXjf8RiA02Mr&{P!%}K8bJ! zM9$qIQqaQ;?w(GGrOFUxg0fJ_C<$yzrIR{ME0m@=QGR5{&m+Ty>@tw38aN^CX@LM= zr(Hj_23F?|K#BE6b{t?WwWq(qB1-*nfxWVA8xFeQHMu#BzSIG_F0Fa8vCFt!#l^eE zb=i6GV#j_>Uta_AaQ9Ja zGQ176yRh?K01;(aEJ@Cqebof%=-8g)qHXZ=8ukSrgvIuY4~)kCW;dYo(UMEKZzN2~ zR2EEY7^fUC2oLD7A=6|1S@xUN*zE(|RvcEw?m9X7t5q%qBi~7lpD<$hc*R*9s?Z$R z)cP)+z8?!-&FFspYu)`#b3dL4eAo_gvb`WB3bNA<5kjT2L?DK=!A_fqkoO4ia)R6e zD!@k8cMM1B&E0?Yx))m8+qRo{eU9}y*14?PTwj*iv7-n-86DT!O!6L+;D1co1r|~e z$Y&3fWezp!^ZfXxL9relYA-*>;5@Zpni!83<5)2ki*l9IobYHT91EdVnII}Jz;3OF zL+BI;NCK#~7#ev2ju>cUvb_{cwfo;M#?;7y0xwG1BWoB;N1{ud@vgncKl=RP`)~9; zNaCzFM|9g;&V}1zog3Q2ZEmwa?jC)SnwMLp!0$lR%_=FGsyaVB8{)9@EWA%9G?tIv zmYMrJ2vm&|~6?|j;O-MXKc_&5A&&cw#NS3})sN3iwJ zm;LwALkr8EBRG6$-bh=d(Yn9Gr3VWW-WZ$!dOj4gs*S!fPY-Qwmmq<}-(Qv^mEcp7 zHux<73F?A9M2sMpZuj>{rZMc1L*52V5=by18;Um3BjKfXJV-WHJ3?fdJWu(w{{#Yc zAY!o5knXSORWWt&Wc{R3gNG`I3}YXUJHJG|^!(V7LmHO;bXqGFKM+fbiVKT=m@znK zQr)Ch1*4pLZfeD`@uLq18;-3z{yFe!09e6^TwF~9Z}l5ycz$-o~GcJzHPZ967clk_gNU%G#Q3m#WWrz@08^M7}>2**eP$3qE&RHSqH_@6$bY z6R)U!Z&XV5w?&Y!f+gvW@Z(7KElBtU46?9;$K4)^fd0ui_=jiDYlx^^?Ot0OyzN#4v4QS~7=aLinWxRJA_!(uB1oz+!kNw*JW54 z=Cc|9kzrW4z9}O))kvl#VOAA5CU^uV1v6XG6Og$*?F|m-1UOq9uy2mO=%mh&J{kMK z813wGc#F3=eESymJCwz0(464XdhC9RjpE%sd7>=ZiMAU->T=8+QySU4*#yV)hhky} z8{kG#Gfp)GdqSv1RO1wyiZ}@e`s`gIFFi11YLA`+i%+X-#g4LX=bJbDeb!5_Wu_jD zM9!R6W4`ij20q$m*GKGT`Hi5mLkkfOV@|p&ZtWNlEyzpG%4o_>Eos2s(9Fc5#F>d9 ztiNW8?hT#oCW4qT;E(DoI3|A45R&Rqa|@^ex zPgZXH@`a!Ls-ylYPpBD=+dzTRB)cf!hvzF--GdfgVOuOFXXSElDSWzVplyL`|OFDj=QnkJdJQuEWT)s8sO$z^Bu+_TI}GfX3Xb=icoXy(3d%+g;i znJ^i%_ucDGyUFPQy!oTo$Go}>Q}|iG`O#~qy?OG@aZ+n2w0 zLuPir7oWSe>Ha&W_00UHyyd2y4VT4s9e&0VtNwKB=4f>C zRaY#Sb$vPJ1hM>ueSt}trohf|do_->qp&jUTM}$BWs#>R-cnHf6_TPS8w$| zN}phEE{9Ue3MXHeVIU=8Oc}{Hg$%L{m4;7d?=4XuS@If)~LAOT2DKhqe*nt<_g@knJF^g zY3-_R+cf<*&Q7blpv%dpo;`m2xocXZYX>zX*F{fjXj*X5tH9OJItXV`uk0|chDmd~ z4IOpP850_>Y8|w0(1esTqhqJfTX@laq6^<^xi8@<;L3VDlCZ)|T82+(j%*zYs`M!% z=PpqL#9T4){4OVtnil#|(}ExUYUrB5Q$g66X~<#htJuln$GCg06EJ1S)J+i~sqE;c z44gd2h6d;v>}<%T#u6>mIps5_4LG^HWaz1@uHRK(KX_Jkuf8dFW-OXLcROMGJ+NJi zdjzt84Q)7Kqw;Bs%|3B5!fyg*!=NDq<(&OYHTKm`{x%e5ztvEKK{%jWXPKK(`G2 z-o$}lk^Gn(SW;~k)b|;P(*jkoFzrJG%nk)(;7u@v18U6*1yc+d?E3@EZ8i)xbrOt7 z?qY-#%(T`)wA&gmn;GU~v!{Zo>|JFRDVW`W!Omd7+@xTP!+=S95<>LIo}5CF1~59o z066OP0uGyDK=;SS9ST<{(h20f9 zYn;3R#+$c~4V|~7Y@N50Vkd{M=Cn3&TK^S7Cz1 zc$RlNF`T1dc!y5HQ-FDpVLmaiS|PHZ6sGk~G-+I2MsdB_eJtNojZ$3E8VzAr2B05d z=sy@vE?@ALc4KQ)KFV;9DmeOjH>GTZGnf@NF0{+Qwa3PFj)IYSMa+J{^ly#&a`^|t z&Shc6ASaiA@#gXmhMmjHyOii$j*6XZzQ<|Z!)bkL&OlmR+L1lgeT{4|gQRpkN*jB$ zKoboBBjPi&jA=4*6n4Q?;tJP7!et%>+`rpt?o@D6 z=IkjcG}4_ku}a7#@I1qPVw?%M%xH>{Qa#vAt2ZK*%zo0!>t$7#R_T%+m8g>=U#+ad z6$a)#yJX*w5Y~imXiIBiLYABssRvXF-R^dW7}-_9$wyyChqV>KBVs>CQr4=3o^mc$ zG_mUUC?G|D8_-;Sza07z0td!^MOr8^wBC3R=|*t12J$S7*wgSC%9LQ=@ZK=)8zFM` z;S?^0U7%`M@gi>k<=ga(Dyn?Ge*(&bWh3KV0;b&-1TnQJmG|9IEt-&tL{L6D447vdk{@);e5 zWZfxKOp;1)TCVC@0_7%B~o zBQ<0w0iuozGi{u!F^0Q@ArAneHy|-5k2^?GLWOYWM>9?1to(xPw0bkOTb+@bk7#`c zvQ%P;HnfFfvhGC8C}NgMM*h+)XJdEJpUBiyx4WzUYmJC(zvD_B3tWTTg& z#yGpQ4BE-aD7ES4J+utTZ37@rx9OhGWhR|6qdl)>49U6-kff_guS_g1<62&l)Q<7l=pItoxs?7-Xu9Kk>Bw*)o z1=3g$p>~g31c^=9*p*FE*%C+tkk}JR)Ns21y5|8mL7thLkNPaby0j^W%?!C$QDKl) zryQOdkYC_5Ga2#;JBPN8kmqM+i3mzN)8+;5l43}1SCP8r#ne81(d5XSJ~kxXDBg=9 zS-%5voK5HG+B}>62gH=faTB+Bq6-2^WsQue5H zp@v=wtrt6kEfa3rsryLwyTAU=O#i=qd7=)gM zuJmd7YS!}5frj-^G^uNMSJB%#(5JtvS$~tpv>uEmVFQ?>!U0WR_LQHqE`Y9rG*`0@ z*U;gJaMU>A4r)zwyTN&;1h~42NQ}TE;DagTKy{%%2WZaLnspmbbFXVdHOgLJ%Cgv(n{QCUzj7t=un>Km%eaqI zi`{>e@&cs1Df}nQh2T_YcFOp0=J-TTU0uDFU5q>Bi`Bhrg$AZTaOv9C(US)CFB$Xl z+C{Kb7Q+%+Upb<3_07BckLB5+&$Pelb}u%3=`dL%}!r;-9P zSVu>39i`Sxbn;YCKq?J47ww+v$BuEi)U(1>Z^%3PV8(k9f>ioyjGOn=d{vX$e^~Vb^0nvWvVA?_f@Rb$omG5rm1!MDk3rD%X?7E)H=NbQCj57QBcFwIxV^A zlIyA$&>7S&wX9vZG`r-wss)fi%~H#ng&*2T#C z)-XY;e%fd^?}2h(!F?!#RDGz?P9G|@pgc|=iXiuLX^wVvu@z177CmWK#VLYTJ%iCM zrwYnlP6c#jNSfP7TeZ^g(3Au62M>@-V_*N}x@*6Q@z@Y|5Mbo$)0P|uy2y^xGkQWMMUD^u zhR3=F*u!I81yuVtg#D0m=jh#;w%*+qIv|_EHzQV`y|0P)1nw=hp8ILPs*Gx{Y+&#a!DW!!B&DLO?lf-vO6ktCkkvu{-gtZ!V}=<1@B44*C{ zNbhK~kw=?M?XROr-HY4-Cs7+5aW*<5&dlmTz7c0JsQo2#0j&pd!9b1TKC?)|w}vo| zOtkyx(0h$yfX!Z84PvU-QAJhn?wVNERExUAOt*uAK#H-vW@{G`U<&#gYlQ znyyl}UP3*8k|DP+olUNO4~1FB@n9c$SGt&RDqY;<>f*e7*2SIe96=kJe`CF=#*H(b zabxeE)S2^*8^hnCK9!04IrUi~Xa>3;zZ5tB@wdBz+Ips|txI~l9YfH!0;7~##-&Dm zv*bS*mrT&O0O1?$&2%+Y9`Vl|?M(-+Ih+GEhMOt%7_PKa?lH%3Go3M9r;1A77>?G6 z{|#D!XGl@AW*bqiS;5iOOsPj#B2fOSjk_5optSc~M|V3<8vLZ zB^RrHh&{%)?nL=FGo;eEq5iD<)5$G6gs8pB1|GtWl>!Av4sh} znAvwASC+mV{nY$BdaWWz3kV`b{-^S?E@CK4^}}Rkl9f zN@L0JE7&{JH=368ALhH}pK(9*Yglw-y<9z(-j9{`9-Ht}Zg8NE-~ItLI#^s9KpVye zqi0-_xbx{(6E>t?6M6ONouTzRc3yeqPMWNWtxf0zJv{-~`$SWc%xG>`>3p}fFHX^t5vJEb{Rv*zG& zIj%H-=$oNlb!zg`o>IDD%#F=oxy1+9kiNtkV>PpkY~G! z{j*)F_e^Q1`MlNKd*&FSk^*$2aBingP3qZHsa`b2Q1g1Lxfjhbj2>Mu&NV^>n315# zJv)Zt?nJ~;++7Rqq8EU|SvIc}cV9sa#oaZICe119=j{ip zQ{`cTe2}SGtkUH*H$H zc=L{7eJh3ytLQrny7E`ywQ^wOSxzDA@!U<(gcV{_I?hhf^>)WeWX(PItjVNDdg@6d zM&N5+BSwVR?zm$4jvdRd*m3_URn@1QQeAZlX_o&Ctu!-`kBGcQ=}l`!XoI+uwLiX^ zdj(|*sdZ9W;vCt#O5Kg4V3jo`p-}9?{DA$fT`0XktcgGP!93>ao7!PHP_Ur2JG|f&C%4gFkNavA)=^+R%Wr=ahP^SnSfVE{+$=PCiyV z{q%8XpFQsM>5f)C8?vG5{WjxGYpclLYZ|@rQIIFBZ})Uli@WrtU+&GQKGd^_q4d`_ z)?cZQ8@LitEy=oTM>OhHeeE4?U%TW4UtgOv==D72R{CTI^oi;_=9KsI=@Zq?FFt!wpZ=B-X>fF+El&0h3jqVdK(d#Su`&3aPlfbD)AQ190Psy|0~pX0o4cl&bz zP6svicBhw?R7wh4DQlv2MXWE~-2j|uWx>DOS?^+vTjb-~KgGW}vA$RFeij>&Rh6mirjxXTCrR)YaFeKV`6Hv*v zJ>-E2i>{78)+inLh*%~|ZTLh{C~vdvNgjKO7$~2y{Zn1L6rxOiq5QFPJkRKi6M+kH zm-l62kvLzRC+3M3QG~rum7+=v#d#01Rl{B+#)wATRxwx10{lX8A$G|X;STKi_!oKN zE@szUu^5nxaR2XIF^3^11M~UtEfP(L-OQ;h0LIJU(+nRZvoDQ;$n__5!0{`&m!c8VlUwI7UN0jQM!v@TL4?zobJsW={LadI_O5) z6|n`+o3P7BVi(aZcy5PYr|*i~f#+S&Q4+dk7oHDd|AxdqmPhg2gT9j#PoO^}(Zl%} zp3h>AO^RROZ-@i<8{$LpdrXGyug7A=Pqg%=?fYcU5GI9p`Sq^z?C>;|L_-KON2=dQ?B0R`19mG+Cp6}zo zALcOIK9k=VAXXZDdP`D-BWXuY; z81q6|#yo^H!w(P$uQX?w^UX!!RSd^5$!6|1UkhcKheBDv2}r^dZUUYe=G*X_L3ZDXE;jYQ?a)O*FC&_v8e0iZkMzg=6{n>Aj16u>wIe=XP|3hLN@-PlI z6@MkL?MBUbThsvJKK9qPL!t(-HGt5ujv#fSAsuiB@dYj5U|R*?5F&@ePr|nzx-1bj zJq_XMFc~nJFj+A8P6HGME)`Z$S99@Y{?$V$~elsr?+x5mz8J%Ed~&SHl>Ps|v^!@hA}`mV@U$kp`0v zlL3jUvK>kIL$g>zBX7qTzjQM^yV`)s_Y<9#FEHvx_~oDS@} z1-{#fXAM8+us^=GCdY|;I{M0y9-?lnr=KgP8&Q5AZ%0=7%sBBb`g|z7*y%n58h61K%?EUkN+D zupw9Cy$NPB>^I=O1!gPEgD?-lJOV>JA%4FAejf$D4}p_Z2dHjVP`v<$HTS7*X;(QQ z2O*rKMa5w#$O5_E#@py~8|mqaX#ab~B)W5??GL;YkQ@Q2>F|%v;kZAFeXJkNIfIt( z`z!yx5sw!SwJPwlo>Ly0T5Gp;pY@^jx(2hJ(Kh|okcx-ewVXE}u=dAM#%qU)ofHFI3-u1$ye-_|xSW`CILqLkRYZ+#I=)rHbF z>wsf(YJewJIrp_Ri2c^N^m<$G2c^~8ufqLx5q|aFc>f%`hhiT>_)}le`T5|+Io@$C z$19C^krX$nzF3E;uB!Cx=Wf)fnJ}!4tbx|G7Un?wKZ5kqNG0z6EX;%)>qp^)(`r5z zRJ&o{uinypVh4U6IdddVM+2{BYCFCC6656uhx&NpjThG*(9Yn$E$1x7Gre&@hmD(I z<|uxXonf7rDvkr&g8l}649B>wbt(<@^rcWruGm4ZAKBZ=8gT4$<-=;VV>r3-%UE!&(kQvicEd{n#1;^E)pdf>Orst6iEi+8umz z{TKoanUsS1x%qKzPT2d$YJr0A$4gyfwcl|$6@c;8$zYh@9i)arJ$`Xhqi_$5AL0L$ zYgWe)LmnKhqiAFuMv}w50=QZrEi6w8qmm-BdwrE#l@04D){!cPhScyD$p*>d89F@P z+xj$~Z+m|6pmlof#)(rd)>I3#v)0|HLo@NN1%FthbA6pke)O|mM;@;RXP7U5ZGmAv z;tk$044rH8{5Z#y>jT!c9&ffHZVp*u-%_g@qa85YtPk;|)Jf|*F&GQd!-xn!+_!L8 zYyp&_1Nsf{+HRMtqKlyE&Ik1iw&mI%Z9@5zsj5EFpWDF(dbIhV!rJg(oIV{y(+9jzfWnaa2Id%~(l2L!>!ZpBo3 zI^MBM?YOkr0H`lJCp!)~E*&n0woxdI%|5r^2${VLzEmS#SK-t%g$uRB`mIghQR`LM zmbkdNjrM64Z)z@_*Vk)@>~l`7m%YAi?HZGQw}oiyL$K{}+7JC*u=-=hcO7F?;ET`^ zICnnC7gd#?BPISYmG6Icu7YX!-H?)Y<-ia5-CZ5mkH^Lx1j}M&H{IUBknreb3yA!Sh{`GE`1K=6;xYYkM z;vKmOT)ojRr2ZRLI`IqiZ?xQU7#@U0+v&~jH(Y;zDEyB?(zz53qhW zy~{Gx7R&!FpP_P_@E;ts5~P@LnzXxQ)d_R#XZ;s+yXeEJ;>_q!DoJzbk0qOHq= z+2LsWP#);EKmnic0vm|>|I+zC!4Hj7zdORX?*iKqeE)*^7YAGpppT$tkf^u7HtIR^ zNSph4Rs;GW?h20^k>EY89@!NJyw}0dSe+m+XQChj@U)KT_9Q)c$?C$pF08)CvH1(<(zyfVp5Ypc30?ew#w=yXuT`oEq zaO(j3?{>%FjOl6(!SuDE!wv0_O^siivc;^6N>#~3Gv$+W%kzZR=?bUy5vbjQH4-i% zK#&Y!#gplUB(TkX!_NFqPMtXXG@KG?O3mB50H~CpNQhwNGmp3Ga=qw~ZggpapJ!9K3CyP07o%c351oV{(Qa{^rL5J8F zcUxbQWYB7|KSWdI^<};}sk$=mh53GVKQ82$v<6nmxrR2eozvwqfxOUw!74-BeK{(X@BE~)WV!Ri47xo*@vMqk{iJzBa7Ob3Z>+7W znoYIp0REmbB78d6usI>#bmIBi_z&=UB(1u%VBc<6?9ZjTT!McmeA}@C``XXKzpI!c z8n9A&HvWY;Wz&SSDCgr}gq`qDQj zDY#mEfd2-ZEojBr1WTIYRvDH_VkbW9P>H?W1LY~=B{^39P`rT?1ec3H$}8kbDe+~< ztujI0A@|5G*byF*kMK@=+HF4s=W7OwDBP*`U)i#}Id5a{1E2mljWt4y6a&OSQH$?2 z|1*E1fvf_$n{L-Ssa?eVIAoX|egeLAa5_$rRbo#x)`9U$^Za{)%hmX%G`-1;LAl^- zyf9;NzKniJ;!J#-jZO{KVW%~nr6|EUX4*|p=Vd0q6ywH%JnV)aj~lSYp@h357JZul zx0vHb2wDNh&8R3-bv7xCuudpv(iF z&gkSJZ3CyP)M*`pssY9@$LK-MIKR$JVS!CIQJVzJIBftbiH>eIb*#&fmI+!q1@I(+ z3lyh>^R9B8NOyiAzjRs%_nmP}Iw?r`ryPcmJIY%aHuY2K5{2DNkDyZV|0a&i`~VbN{mWLl@9I)b8_4pIC=wAl|dI9r>0Qh2&P|PL%At;Yr`Ru zl|*o<030DsN!~dW0$;j;2ULP4;;4F-2`DmsAd@FTmNT7trfjMt6fJF^Js>kBkW#8+ zy-`~Sqt^Yie?_Rh$Nm*_u&Qmu%|g`6A*eY+QRC?6t!G`qfih?VdXpIj9(7|=gBndg zDqDkX`W>}h%1vQ8;3d990j)Fwob1ffLwuItd_K5qLif;<>VvcmNfyM?!0gKjoa)RCRi zqIxEM7>Ex#J_V>;N04L6FKHo)r($YaGJplQ33J}G6svRzo?@!BAbAYuP0$bLx+q2- zjEa!}JLOwrcQJTrrTljG4rF{PK58`8dso_)cU*a>gg~_-HIh9Uf(iu!Tfl22t zar&}R&XTj~{3V^fq_dZF@{-P7@~KM+dEUV10I$b6z#DL0l+FUuSy6Qo@OGR`y9+1s z@4-H-`*BkAL7cgMn9qsQDbXj;nm>is{AW0;_Z&`a{~VMF8ku>F43oy+gxqXG?lyss zn*{mEa3+pz$6UMiYv-K+y2v&7>uBgiN> zbnS-=ZjZ|tXrKi)qVUGg7-O8FZ0uU)+7UL%n1<)M%Fmc%T;PTp7a6=8fMYE)RvGI& zVb>ZrxGrGW>bi{EjJrLq2aHEOt|tt8L}2VAUwli$`5CVnha3lmz3sY;_l=K@KWVq| zmGO6N_rD>231%w!nRaiPbi=m+Uf)?X?st6T`p7^B)X<4#UqQ7-I(8XE6M`?32wtiEJ-s zdz531WBWMrH}4?Krm`<(|KW_`8;*4*+Y`wjx9n#_Qvll5eNouP@+tJr;*(?Ox;T0c zF%6ttKfrpn892$bz|8X4wcmGn&ubGO`X#)Fv<#oi7L*^@Z$bJANS&Uk@ZsE=+YxH} z$djndLWOuzj;W7)r;K+7zOyJoXm=4Bb;{%-F>pfN8AW2=#4}DU5)&uYO@JNx%|f4m zun_JDxR(TM=#|1h+4iR%ef*GcyCHPK+>MhuXa4!;iE(q97cLO>O^X_5i>dP$&Ymx3 z@pB$Oo0~6Qut+RfxM)tZSbh;4;>U{@S5}Gjcvg!WTF7TRQ$#&zYV~L&p--{ZWaY7#?iHC`A(ga9ItwGz@nwEJtgzPZr7%@?5zbt=>WTm62og zi#%Y|gl-HS#n9xbNM&Sw^2j=UH7Yho%e5e8~=$MKsuNxcZVML<0%TvA(PKcQk_ z)3I{cU%POp2)oxK2imU~e#xq5B6E?q8ztI9j9h!czMJj1fKrxvY5*>nK|agT&pwqP z(M~_i5M(F(q}PZS%C#FV)zi^olp8lJ0=uRq3n41>bzs^JmqZI6FP!duxRJxR#0v-Y zc6?bHH#}FzmpPuCQSTrNa!nF@0J8x$o#29i+I8=kbI{W~z>0HxT zlyK862DrDt6Hitn#6rycz81)f#sB3q_ zspMS75=ysFBxB@Dqq;2A?PAOb;p@j33D@zv7rU3+ z?&TOKlO9UPJdT2*H-(avl%f~!$gJ@Wq&CNvS=@FBDoMJOq|)6e$~GQpnMmtQgwm}P zgD`6}4Y$#(LhpYMr1npc+Ei%HYRtIJluPA}at|<3eh3@gzCG%U&UNpZ&GC;GU0unGD(nm5tvkO``D#?tpw2|&B6(>Toy?|H` zpk;WG-@9=`i2?UZcs`8TT+;l%#Pcbn6v8bf%0>|6qc9tgnGiH1-6eG~!)?Gg^B%r! zNT*$l^fbJL6jNKO!|0A1Ey?8P_*>twe_zP3&mS=KU*Nzh3Z9oqm@%e)S_tD=OWrIW H7UI7F@EF9w literal 0 HcmV?d00001 diff --git a/public/fonts/Optimistic_Display_W_Md.ttf b/public/fonts/Optimistic_Display_W_Md.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3fbdb483dbcf66dc60fdccccf84e1f57c8d2c331 GIT binary patch literal 61004 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 literal 0 HcmV?d00001 diff --git a/src/components/Layout/Page.tsx b/src/components/Layout/Page.tsx index d958281ac4c..0ae0bde847a 100644 --- a/src/components/Layout/Page.tsx +++ b/src/components/Layout/Page.tsx @@ -134,7 +134,7 @@ export function Page({ ? `/images/og-` + section + '.png' : `/api/og?title=${encodeURIComponent( title - )}§ion=${encodeURIComponent(section)}` + )}&path=${encodeURIComponent(cleanedPath)}` } searchOrder={searchOrder} /> diff --git a/src/pages/api/og.tsx b/src/pages/api/og.tsx index 01862e371c5..f566486b9f6 100644 --- a/src/pages/api/og.tsx +++ b/src/pages/api/og.tsx @@ -5,34 +5,30 @@ * LICENSE file in the root directory of this source tree. */ +import fs from 'fs'; +import path from 'path'; import {ImageResponse} from 'next/og'; -import type {NextRequest} from 'next/server'; +import type {NextApiRequest, NextApiResponse} from 'next'; -export const config = { - runtime: 'edge', -}; +// Satori can't read woff2, so the site fonts are also vendored as +// TTF in public/fonts (converted from the woff2 files there). +const fontsDir = path.join(process.cwd(), 'public', 'fonts'); +const bold = fs.readFileSync( + path.join(fontsDir, 'Optimistic_Display_W_Bd.ttf') +); +const medium = fs.readFileSync( + path.join(fontsDir, 'Optimistic_Display_W_Md.ttf') +); -const SECTION_LABELS: Record = { - learn: 'Learn React', - reference: 'API Reference', - community: 'Community', - blog: 'Blog', -}; +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const title = String(req.query.title ?? 'react.dev').slice(0, 80); + const pagePath = String(req.query.path ?? '').slice(0, 120); + const footer = ('react.dev' + pagePath).toUpperCase(); -// Satori doesn't support woff2, so the site fonts can't be reused here. -// Source Code Pro matches the code-styled titles used across the docs. -const fontPromise = fetch( - 'https://raw.githubusercontent.com/adobe-fonts/source-code-pro/release/TTF/SourceCodePro-Semibold.ttf' -).then((res) => res.arrayBuffer()); - -export default async function handler(request: NextRequest) { - const {searchParams} = new URL(request.url); - const title = searchParams.get('title')?.slice(0, 80) ?? 'react.dev'; - const section = searchParams.get('section') ?? ''; - const label = SECTION_LABELS[section] ?? 'react.dev'; - const fontData = await fontPromise; - - return new ImageResponse( + const image = new ImageResponse( (

- - + + - - - + + +
- {label} + React
24 ? 64 : 96, + marginTop: 'auto', + marginBottom: 'auto', + fontSize: title.length > 24 ? 72 : 96, + fontFamily: 'Optimistic Display Bold', color: '#f6f7f9', lineHeight: 1.1, wordBreak: 'break-word', @@ -84,11 +78,12 @@ export default async function handler(request: NextRequest) {
- react.dev + {footer}
), @@ -97,12 +92,23 @@ export default async function handler(request: NextRequest) { height: 630, fonts: [ { - name: 'Source Code Pro', - data: fontData, + name: 'Optimistic Display Bold', + data: bold, style: 'normal', - weight: 600, + weight: 700, + }, + { + name: 'Optimistic Display Medium', + data: medium, + style: 'normal', + weight: 500, }, ], } ); + + const buffer = Buffer.from(await image.arrayBuffer()); + res.setHeader('Content-Type', 'image/png'); + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); + res.end(buffer); } From 5e41056020db44611d8d8b3881d0d4e2a49a746a Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Thu, 9 Jul 2026 17:18:31 +0200 Subject: [PATCH 3/6] Generate OG images statically at build time --- .gitignore | 3 + package.json | 4 +- scripts/generateOgImages.mjs | 208 ++++++++++++++++++++++++++++++++ src/components/Layout/Page.tsx | 9 +- src/pages/api/og.tsx | 114 ------------------ yarn.lock | 209 ++++++++++++++++++++++++++++++++- 6 files changed, 424 insertions(+), 123 deletions(-) create mode 100644 scripts/generateOgImages.mjs delete mode 100644 src/pages/api/og.tsx diff --git a/.gitignore b/.gitignore index 92626a1e63a..ed9efe38d02 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ public/rss.xml # worktrees .worktrees/ + +# Generated OG images (scripts/generateOgImages.mjs) +public/images/og/ diff --git a/package.json b/package.json index 359f30d3e21..97bc1432c5e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "analyze": "ANALYZE=true next build", "dev": "next-remote-watch ./src/content", "prebuild:rsc": "node scripts/buildRscWorker.mjs", - "build": "node scripts/buildRscWorker.mjs && next build && node --experimental-modules ./scripts/downloadFonts.mjs", + "build": "node scripts/buildRscWorker.mjs && next build && node --experimental-modules ./scripts/downloadFonts.mjs && node ./scripts/generateOgImages.mjs", "lint": "next lint && eslint \"src/content/**/*.md\"", "lint:fix": "next lint --fix && eslint \"src/content/**/*.md\" --fix", "format:source": "prettier --config .prettierrc --write \"{plugins,src}/**/*.{js,ts,jsx,tsx,css}\"", @@ -51,6 +51,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.18.6", "@babel/preset-react": "^7.18.6", "@mdx-js/mdx": "^2.1.3", + "@resvg/resvg-js": "^2.6.2", "@types/body-scroll-lock": "^2.6.1", "@types/classnames": "^2.2.10", "@types/debounce": "^1.2.1", @@ -102,6 +103,7 @@ "retext": "^7.0.1", "retext-smartypants": "^4.0.0", "rss": "^1.2.2", + "satori": "^0.26.0", "tailwindcss": "^3.4.1", "typescript": "^5.7.2", "unist-util-visit": "^2.0.3", diff --git a/scripts/generateOgImages.mjs b/scripts/generateOgImages.mjs new file mode 100644 index 00000000000..65f4270fcbf --- /dev/null +++ b/scripts/generateOgImages.mjs @@ -0,0 +1,208 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// Generates a static OG image for every content page, so social +// cards show the page title without any runtime image generation. +// Run after `next build` (see the `build` script in package.json). + +import fs from 'fs'; +import path from 'path'; +import satori from 'satori'; +import {Resvg} from '@resvg/resvg-js'; +import matter from 'gray-matter'; + +const ROOT = process.cwd(); +const CONTENT_DIR = path.join(ROOT, 'src', 'content'); +const OUT_DIR = path.join(ROOT, 'public', 'images', 'og'); +const CONCURRENCY = 8; + +const bold = fs.readFileSync( + path.join(ROOT, 'public', 'fonts', 'Optimistic_Display_W_Bd.ttf') +); +const medium = fs.readFileSync( + path.join(ROOT, 'public', 'fonts', 'Optimistic_Display_W_Md.ttf') +); + +function el(type, style, children) { + return {type, props: {style, children}}; +} + +function card(title, pagePath) { + const footer = ('react.dev' + pagePath).toUpperCase(); + const logo = { + type: 'svg', + props: { + width: 80, + height: 72, + viewBox: '-10.5 -9.45 21 18.9', + fill: 'none', + children: [ + {type: 'circle', props: {cx: 0, cy: 0, r: 2, fill: '#58c4dc'}}, + { + type: 'g', + props: { + stroke: '#58c4dc', + strokeWidth: 1, + fill: 'none', + children: [ + {type: 'ellipse', props: {rx: 10, ry: 4.5}}, + { + type: 'ellipse', + props: {rx: 10, ry: 4.5, transform: 'rotate(60)'}, + }, + { + type: 'ellipse', + props: {rx: 10, ry: 4.5, transform: 'rotate(120)'}, + }, + ], + }, + }, + ], + }, + }; + return el( + 'div', + { + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column', + padding: '72px 80px', + backgroundColor: '#23272f', + backgroundImage: + 'radial-gradient(circle at 25% 30%, #343a46 0%, #23272f 55%)', + }, + [ + el('div', {display: 'flex', alignItems: 'center', gap: '20px'}, [ + logo, + el( + 'div', + { + fontSize: 48, + fontFamily: 'Optimistic Display Bold', + color: '#f6f7f9', + }, + 'React' + ), + ]), + el( + 'div', + { + marginTop: 'auto', + marginBottom: 'auto', + fontSize: title.length > 24 ? 72 : 96, + fontFamily: 'Optimistic Display Bold', + color: '#f6f7f9', + lineHeight: 1.1, + wordBreak: 'break-word', + }, + title + ), + el( + 'div', + { + fontSize: 28, + fontFamily: 'Optimistic Display Medium', + color: '#99a1b3', + letterSpacing: '0.08em', + }, + footer + ), + ] + ); +} + +async function renderCard(title, pagePath) { + const svg = await satori(card(title, pagePath), { + width: 1200, + height: 630, + fonts: [ + { + name: 'Optimistic Display Bold', + data: bold, + weight: 700, + style: 'normal', + }, + { + name: 'Optimistic Display Medium', + data: medium, + weight: 500, + style: 'normal', + }, + ], + }); + return new Resvg(svg, {fitTo: {mode: 'width', value: 1200}}).render().asPng(); +} + +function collectSidebarTitles() { + const titles = new Map(); + for (const name of fs.readdirSync(path.join(ROOT, 'src'))) { + if (!/^sidebar.*\.json$/.test(name)) continue; + const walk = (node) => { + if (node.path && node.title) { + titles.set(node.path, node.title); + } + for (const child of node.routes ?? []) walk(child); + }; + walk(JSON.parse(fs.readFileSync(path.join(ROOT, 'src', name), 'utf8'))); + } + return titles; +} + +const sidebarTitles = collectSidebarTitles(); + +function collectPages(dir, out) { + for (const entry of fs.readdirSync(dir, {withFileTypes: true})) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + collectPages(full, out); + } else if (entry.name.endsWith('.md')) { + const rel = path.relative(CONTENT_DIR, full).replace(/\.md$/, ''); + const segments = rel.split(path.sep); + if (segments[segments.length - 1] === 'index') { + segments.pop(); + } + const pagePath = '/' + segments.join('/'); + const {data} = matter(fs.readFileSync(full, 'utf8')); + const title = data.title ?? sidebarTitles.get(pagePath); + if (title) { + out.push({title: String(title), pagePath}); + } + } + } + return out; +} + +export function ogImageFileName(pagePath) { + return pagePath.replace(/^\//, '').replace(/\//g, '-') + '.png'; +} + +async function main() { + const pages = collectPages(CONTENT_DIR, []); + fs.mkdirSync(OUT_DIR, {recursive: true}); + let done = 0; + const queue = [...pages]; + async function worker() { + for (;;) { + const page = queue.shift(); + if (!page) return; + const png = await renderCard(page.title, page.pagePath); + fs.writeFileSync(path.join(OUT_DIR, ogImageFileName(page.pagePath)), png); + done++; + if (done % 100 === 0) { + console.log(`og-images: ${done}/${pages.length}`); + } + } + } + await Promise.all(Array.from({length: CONCURRENCY}, worker)); + console.log(`og-images: generated ${done} images in public/images/og`); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/src/components/Layout/Page.tsx b/src/components/Layout/Page.tsx index 0ae0bde847a..eee98af1847 100644 --- a/src/components/Layout/Page.tsx +++ b/src/components/Layout/Page.tsx @@ -130,11 +130,12 @@ export function Page({ titleForTitleTag={meta.titleForTitleTag} isHomePage={isHomePage} image={ - isHomePage || !title + // OG images are generated per page at build time by + // scripts/generateOgImages.mjs. Error pages have dynamic + // titles, so they keep the static section image. + isHomePage || !title || cleanedPath.startsWith('/errors') ? `/images/og-` + section + '.png' - : `/api/og?title=${encodeURIComponent( - title - )}&path=${encodeURIComponent(cleanedPath)}` + : `/images/og/${cleanedPath.slice(1).replace(/\//g, '-')}.png` } searchOrder={searchOrder} /> diff --git a/src/pages/api/og.tsx b/src/pages/api/og.tsx deleted file mode 100644 index f566486b9f6..00000000000 --- a/src/pages/api/og.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import fs from 'fs'; -import path from 'path'; -import {ImageResponse} from 'next/og'; -import type {NextApiRequest, NextApiResponse} from 'next'; - -// Satori can't read woff2, so the site fonts are also vendored as -// TTF in public/fonts (converted from the woff2 files there). -const fontsDir = path.join(process.cwd(), 'public', 'fonts'); -const bold = fs.readFileSync( - path.join(fontsDir, 'Optimistic_Display_W_Bd.ttf') -); -const medium = fs.readFileSync( - path.join(fontsDir, 'Optimistic_Display_W_Md.ttf') -); - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const title = String(req.query.title ?? 'react.dev').slice(0, 80); - const pagePath = String(req.query.path ?? '').slice(0, 120); - const footer = ('react.dev' + pagePath).toUpperCase(); - - const image = new ImageResponse( - ( -
-
- - - - - - - - -
- React -
-
-
24 ? 72 : 96, - fontFamily: 'Optimistic Display Bold', - color: '#f6f7f9', - lineHeight: 1.1, - wordBreak: 'break-word', - }}> - {title} -
-
- {footer} -
-
- ), - { - width: 1200, - height: 630, - fonts: [ - { - name: 'Optimistic Display Bold', - data: bold, - style: 'normal', - weight: 700, - }, - { - name: 'Optimistic Display Medium', - data: medium, - style: 'normal', - weight: 500, - }, - ], - } - ); - - const buffer = Buffer.from(await image.arrayBuffer()); - res.setHeader('Content-Type', 'image/png'); - res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); - res.end(buffer); -} diff --git a/yarn.lock b/yarn.lock index ce7b74ed632..596c25ad83d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1687,11 +1687,97 @@ resolved "https://registry.npmjs.org/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz" integrity sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg== +"@resvg/resvg-js-android-arm-eabi@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz#e761e0b688127db64879f455178c92468a9aeabe" + integrity sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA== + +"@resvg/resvg-js-android-arm64@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz#b8cb564d7f6b3f37d9b43129f5dc5fe171e249e4" + integrity sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ== + +"@resvg/resvg-js-darwin-arm64@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz#49bd3faeda5c49f53302d970e6e79d006de18e7d" + integrity sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A== + +"@resvg/resvg-js-darwin-x64@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz#e1344173aa27bfb4d880ab576d1acf1c1648faca" + integrity sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw== + +"@resvg/resvg-js-linux-arm-gnueabihf@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz#34c445eba45efd68f6130b2ab426d76a7424253d" + integrity sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw== + +"@resvg/resvg-js-linux-arm64-gnu@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz#30da47087dd8153182198b94fe9f8d994890dae5" + integrity sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg== + +"@resvg/resvg-js-linux-arm64-musl@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz#5d75b8ff5c83103729c1ca3779987302753c50d4" + integrity sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg== + +"@resvg/resvg-js-linux-x64-gnu@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz#411abedfaee5edc57cbb7701736cecba522e26f3" + integrity sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw== + +"@resvg/resvg-js-linux-x64-musl@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz#fe4984038f0372f279e3ff570b72934dd7eb2a5c" + integrity sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ== + +"@resvg/resvg-js-win32-arm64-msvc@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz#d3a053cf7ff687087a2106330c0fdaae706254d1" + integrity sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ== + +"@resvg/resvg-js-win32-ia32-msvc@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz#7cdda1ce29ef7209e28191d917fa5bef0624a4ad" + integrity sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w== + +"@resvg/resvg-js-win32-x64-msvc@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz#cb0ad04525d65f3def4c8d346157a57976d5b388" + integrity sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ== + +"@resvg/resvg-js@^2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js/-/resvg-js-2.6.2.tgz#3e92a907d88d879256c585347c5b21a7f3bb5b46" + integrity sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q== + optionalDependencies: + "@resvg/resvg-js-android-arm-eabi" "2.6.2" + "@resvg/resvg-js-android-arm64" "2.6.2" + "@resvg/resvg-js-darwin-arm64" "2.6.2" + "@resvg/resvg-js-darwin-x64" "2.6.2" + "@resvg/resvg-js-linux-arm-gnueabihf" "2.6.2" + "@resvg/resvg-js-linux-arm64-gnu" "2.6.2" + "@resvg/resvg-js-linux-arm64-musl" "2.6.2" + "@resvg/resvg-js-linux-x64-gnu" "2.6.2" + "@resvg/resvg-js-linux-x64-musl" "2.6.2" + "@resvg/resvg-js-win32-arm64-msvc" "2.6.2" + "@resvg/resvg-js-win32-ia32-msvc" "2.6.2" + "@resvg/resvg-js-win32-x64-msvc" "2.6.2" + "@rushstack/eslint-patch@^1.0.6": version "1.10.3" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz#391d528054f758f81e53210f1a1eebcf1a8b1d20" integrity sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg== +"@shuding/opentype.js@1.4.0-beta.0": + version "1.4.0-beta.0" + resolved "https://registry.yarnpkg.com/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz#5d1e7e9e056f546aad41df1c5043f8f85d39e24b" + integrity sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA== + dependencies: + fflate "^0.7.3" + string.prototype.codepointat "^0.2.1" + "@stitches/core@^1.2.6": version "1.2.8" resolved "https://registry.npmjs.org/@stitches/core/-/core-1.2.8.tgz" @@ -2442,7 +2528,12 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.3.1: +base64-js@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978" + integrity sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw== + +base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -2575,6 +2666,11 @@ camelcase-css@^2.0.1: resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== +camelize@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" + integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== + caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297, caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001579: version "1.0.30001636" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz#b15f52d2bdb95fad32c2f53c0b68032b85188a78" @@ -2739,7 +2835,7 @@ color-name@1.1.3: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@^1.0.0, color-name@~1.1.4: +color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -2869,6 +2965,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +css-background-parser@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/css-background-parser/-/css-background-parser-0.1.0.tgz#48a17f7fe6d4d4f1bca3177ddf16c5617950741b" + integrity sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA== + css-blank-pseudo@^0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz" @@ -2876,6 +2977,21 @@ css-blank-pseudo@^0.1.4: dependencies: postcss "^7.0.5" +css-box-shadow@1.0.0-3: + version "1.0.0-3" + resolved "https://registry.yarnpkg.com/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz#9eaeb7140947bf5d649fc49a19e4bbaa5f602713" + integrity sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg== + +css-color-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" + integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== + +css-gradient-parser@^0.0.17: + version "0.0.17" + resolved "https://registry.yarnpkg.com/css-gradient-parser/-/css-gradient-parser-0.0.17.tgz#9a523419f328ad2777ae790c296a6932d470aa9a" + integrity sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg== + css-has-pseudo@^0.10.0: version "0.10.0" resolved "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz" @@ -2891,6 +3007,15 @@ css-prefers-color-scheme@^3.1.1: dependencies: postcss "^7.0.5" +css-to-react-native@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" + integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== + dependencies: + camelize "^1.0.0" + css-color-keywords "^1.0.0" + postcss-value-parser "^4.0.2" + cssdb@^4.4.0: version "4.4.0" resolved "https://registry.npmjs.org/cssdb/-/cssdb-4.4.0.tgz" @@ -3156,6 +3281,11 @@ electron-to-chromium@^1.5.73: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.80.tgz#ca7a8361d7305f0ec9e203ce4e633cbb8a8ef1b1" integrity sha512-LTrKpW0AqIuHwmlVNV+cjFYTnXtM9K37OGhpe0ZI10ScPSxqVSryZHIY3WnCS5NSYbBODRTZyhRMS2h5FAEqAw== +emoji-regex-xs@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz#012f4e4d88ecec8397df32d5b4ef89d422559c0e" + integrity sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" @@ -3419,7 +3549,7 @@ escape-carriage@^1.3.1: resolved "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.1.tgz" integrity sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw== -escape-html@~1.0.3: +escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== @@ -3959,6 +4089,11 @@ fault@^2.0.0: dependencies: format "^0.2.0" +fflate@^0.7.3: + version "0.7.4" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.7.4.tgz#61587e5d958fdabb5a9368a302c25363f4f69f50" + integrity sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw== + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" @@ -4416,6 +4551,11 @@ hermes-parser@^0.25.1: dependencies: hermes-estree "0.25.1" +hex-rgb@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/hex-rgb/-/hex-rgb-4.3.0.tgz#af5e974e83bb2fefe44d55182b004ec818c07776" + integrity sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw== + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz" @@ -5049,6 +5189,14 @@ lilconfig@^2.0.5, lilconfig@^2.1.0: resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== +linebreak@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/linebreak/-/linebreak-1.1.0.tgz#831cf378d98bced381d8ab118f852bd50d81e46b" + integrity sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ== + dependencies: + base64-js "0.0.8" + unicode-trie "^2.0.0" + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" @@ -6312,6 +6460,11 @@ p-try@^1.0.0: resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz" integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= +pako@^0.2.5: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -6319,6 +6472,14 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-css-color@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/parse-css-color/-/parse-css-color-0.2.1.tgz#b687a583f2e42e66ffdfce80a570706966e807c9" + integrity sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg== + dependencies: + color-name "^1.1.4" + hex-rgb "^4.1.0" + parse-entities@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz" @@ -6796,7 +6957,7 @@ postcss-selector-parser@^6.0.2: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: +postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== @@ -7410,6 +7571,23 @@ safe-regex-test@^1.0.3: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +satori@^0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/satori/-/satori-0.26.0.tgz#bd99df054f86ffb4939ac8d60aa92a3a5b0d9c80" + integrity sha512-tkMFrfIs3l2mQ2JEcyW0ADTy3zGggFRFzi6Ef8YozQSFsFKEqaSO1Y8F9wJg4//PJGQauMalHGTUEkPrFwhVPA== + dependencies: + "@shuding/opentype.js" "1.4.0-beta.0" + css-background-parser "^0.1.0" + css-box-shadow "1.0.0-3" + css-gradient-parser "^0.0.17" + css-to-react-native "^3.0.0" + emoji-regex-xs "^2.0.1" + escape-html "^1.0.3" + linebreak "^1.1.0" + parse-css-color "^0.2.1" + postcss-value-parser "^4.2.0" + yoga-layout "^3.2.1" + scheduler@^0.25.0: version "0.25.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015" @@ -7777,6 +7955,11 @@ string-width@^5.0.0: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string.prototype.codepointat@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc" + integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg== + string.prototype.includes@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz#8986d57aee66d5460c144620a6d873778ad7289f" @@ -8040,6 +8223,11 @@ through@^2.3.8: resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +tiny-inflate@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" + integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== + tiny-warning@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" @@ -8261,6 +8449,14 @@ unherit@^1.0.4: inherits "^2.0.0" xtend "^4.0.0" +unicode-trie@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-trie/-/unicode-trie-2.0.0.tgz#8fd8845696e2e14a8b67d78fa9e0dd2cad62fec8" + integrity sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ== + dependencies: + pako "^0.2.5" + tiny-inflate "^1.0.0" + unified@^10.0.0: version "10.1.1" resolved "https://registry.npmjs.org/unified/-/unified-10.1.1.tgz" @@ -8715,6 +8911,11 @@ yaml@^2.1.1: resolved "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz" integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== +yoga-layout@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/yoga-layout/-/yoga-layout-3.2.1.tgz#d2d1ba06f0e81c2eb650c3e5ad8b0b4adde1e843" + integrity sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ== + zod-validation-error@^3.0.3: version "3.4.0" resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.4.0.tgz#3a8a1f55c65579822d7faa190b51336c61bee2a6" From 0145ae8113997d2c94d9627c4303c3d50ee5c521 Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Thu, 9 Jul 2026 17:27:36 +0200 Subject: [PATCH 4/6] Clean up orphaned lockfile entry --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 596c25ad83d..dba28706ebe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2533,7 +2533,7 @@ base64-js@0.0.8: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978" integrity sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw== -base64-js@^1.3.1, base64-js@^1.5.1: +base64-js@^1.3.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== From e457db2c0bb503746137418c9ce671dba182884f Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Thu, 9 Jul 2026 17:30:25 +0200 Subject: [PATCH 5/6] Show section label on OG cards and guard pages without a card --- scripts/generateOgImages.mjs | 18 ++++++++++++++---- src/components/Layout/Page.tsx | 12 ++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/scripts/generateOgImages.mjs b/scripts/generateOgImages.mjs index 65f4270fcbf..f18d78d6af6 100644 --- a/scripts/generateOgImages.mjs +++ b/scripts/generateOgImages.mjs @@ -20,6 +20,13 @@ const CONTENT_DIR = path.join(ROOT, 'src', 'content'); const OUT_DIR = path.join(ROOT, 'public', 'images', 'og'); const CONCURRENCY = 8; +const SECTION_LABELS = { + learn: 'Learn React', + reference: 'API Reference', + community: 'Community', + blog: 'Blog', +}; + const bold = fs.readFileSync( path.join(ROOT, 'public', 'fonts', 'Optimistic_Display_W_Bd.ttf') ); @@ -32,7 +39,8 @@ function el(type, style, children) { } function card(title, pagePath) { - const footer = ('react.dev' + pagePath).toUpperCase(); + const section = pagePath.split('/')[1] ?? ''; + const label = SECTION_LABELS[section] ?? 'React'; const logo = { type: 'svg', props: { @@ -105,12 +113,11 @@ function card(title, pagePath) { el( 'div', { - fontSize: 28, + fontSize: 32, fontFamily: 'Optimistic Display Medium', color: '#99a1b3', - letterSpacing: '0.08em', }, - footer + label ), ] ); @@ -167,6 +174,9 @@ function collectPages(dir, out) { segments.pop(); } const pagePath = '/' + segments.join('/'); + if (pagePath === '/' || pagePath.startsWith('/errors')) { + continue; + } const {data} = matter(fs.readFileSync(full, 'utf8')); const title = data.title ?? sidebarTitles.get(pagePath); if (title) { diff --git a/src/components/Layout/Page.tsx b/src/components/Layout/Page.tsx index eee98af1847..59cf4386d8f 100644 --- a/src/components/Layout/Page.tsx +++ b/src/components/Layout/Page.tsx @@ -131,10 +131,14 @@ export function Page({ isHomePage={isHomePage} image={ // OG images are generated per page at build time by - // scripts/generateOgImages.mjs. Error pages have dynamic - // titles, so they keep the static section image. - isHomePage || !title || cleanedPath.startsWith('/errors') - ? `/images/og-` + section + '.png' + // scripts/generateOgImages.mjs. Pages without a generated + // card (home, errors, 404, 500) use the static section image. + isHomePage || + !title || + cleanedPath.startsWith('/errors') || + cleanedPath === '/404' || + cleanedPath === '/500' + ? `/images/og-${section ?? 'unknown'}.png` : `/images/og/${cleanedPath.slice(1).replace(/\//g, '-')}.png` } searchOrder={searchOrder} From 2aad56c502d40f6a9087a792e2af1c8f0fe7edfa Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Thu, 9 Jul 2026 18:51:46 +0200 Subject: [PATCH 6/6] Center title and enlarge section label on OG cards --- scripts/generateOgImages.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/generateOgImages.mjs b/scripts/generateOgImages.mjs index f18d78d6af6..937727a1cb1 100644 --- a/scripts/generateOgImages.mjs +++ b/scripts/generateOgImages.mjs @@ -100,8 +100,9 @@ function card(title, pagePath) { el( 'div', { - marginTop: 'auto', - marginBottom: 'auto', + flexGrow: 1, + display: 'flex', + alignItems: 'center', fontSize: title.length > 24 ? 72 : 96, fontFamily: 'Optimistic Display Bold', color: '#f6f7f9', @@ -113,7 +114,7 @@ function card(title, pagePath) { el( 'div', { - fontSize: 32, + fontSize: 40, fontFamily: 'Optimistic Display Medium', color: '#99a1b3', },