@@ -4,6 +4,7 @@ import StaggeredMenu from './components/StaggeredMenu/StaggeredMenu.jsx'
44import Footer from './components/Footer/Footer.jsx'
55import Home from './components/Home/Home.jsx'
66import PageTransition from './components/PageTransition/PageTransition.jsx'
7+ import { NavContext } from './navContext.js'
78import logoUrl from './assets/EvanGongIcon.png'
89import './App.css'
910
@@ -47,7 +48,7 @@ function waitForImagesReady(container, timeout = IMAGE_LOAD_TIMEOUT) {
4748 return new Promise ( ( resolve ) => {
4849 const settle = ( ) => requestAnimationFrame ( ( ) => requestAnimationFrame ( resolve ) )
4950 if ( ! container ) return settle ( )
50- const imgs = Array . from ( container . querySelectorAll ( 'img' ) )
51+ const imgs = Array . from ( container . querySelectorAll ( 'img[data-transition-critical="true"] ' ) )
5152 const pending = imgs . filter ( ( img ) => {
5253 if ( img . loading === 'lazy' ) return false
5354 return ! img . complete || img . naturalWidth === 0
@@ -74,6 +75,93 @@ function waitForImagesReady(container, timeout = IMAGE_LOAD_TIMEOUT) {
7475 } )
7576}
7677
78+ const PAGE_META = {
79+ '/' : {
80+ title : 'Evan Gong | Programming, AI, Robotics & Photography' ,
81+ description : 'Evan Gong builds tangible experiences across programming, AI, robotics, 3D printing, and photography.'
82+ } ,
83+ '/about' : {
84+ title : 'About Evan Gong | Evan Gong' ,
85+ description : 'Learn about Evan Gong, his technical interests, creative practice, and current skills.'
86+ } ,
87+ '/projects' : {
88+ title : 'Featured Projects | Evan Gong' ,
89+ description : 'Selected software, AI, robotics, hardware, and interactive projects by Evan Gong.'
90+ } ,
91+ '/gallery' : {
92+ title : 'Photography Gallery | Evan Gong' ,
93+ description : 'Photography by Evan Gong from Paris, Chaoshan, Beijing, and elsewhere.'
94+ } ,
95+ '/blog' : {
96+ title : 'Field Notes | Evan Gong' ,
97+ description : 'Technical notes and reflections on AI agents, hardware, software, and learning.'
98+ } ,
99+ '/awards' : {
100+ title : 'Awards & Recognition | Evan Gong' ,
101+ description : 'Selected awards, competition results, program milestones, and project records.'
102+ } ,
103+ '/blog/hardware-agent-runtime' : {
104+ title : 'Giving AI Agents a Safe Path to Real Hardware | Evan Gong' ,
105+ description : 'Hardware Agent Runtime connects coding agents to embedded devices through observable hardware-in-the-loop workflows.'
106+ } ,
107+ '/blog/kards-ai-simulator' : {
108+ title : 'Teaching a Card Game Agent to Think in States | Evan Gong' ,
109+ description : 'Kards AI turns a complex card game into a deterministic environment for simulation and reinforcement-learning research.'
110+ } ,
111+ '/blog/openkyrozen-agent' : {
112+ title : 'Building an Agent That Learns From Its Work | Evan Gong' ,
113+ description : 'OpenKyrozen explores how an AI agent can improve through the work it already performs.'
114+ }
115+ }
116+
117+ function setMeta ( attribute , key , content ) {
118+ let element = document . head . querySelector ( `meta[${ attribute } ="${ key } "]` )
119+ if ( ! element ) {
120+ element = document . createElement ( 'meta' )
121+ element . setAttribute ( attribute , key )
122+ document . head . appendChild ( element )
123+ }
124+ element . setAttribute ( 'content' , content )
125+ }
126+
127+ function PageMeta ( ) {
128+ const { pathname } = useLocation ( )
129+
130+ useEffect ( ( ) => {
131+ const isBlogPost = pathname . startsWith ( '/blog/' )
132+ const slug = isBlogPost ? pathname . split ( '/' ) . filter ( Boolean ) . at ( - 1 ) : ''
133+ const meta = PAGE_META [ pathname ] || ( isBlogPost
134+ ? {
135+ title : `${ slug . replace ( / - / g, ' ' ) } | Evan Gong` ,
136+ description : 'A field note by Evan Gong.'
137+ }
138+ : PAGE_META [ '/' ] )
139+ const canonicalUrl = `https://evangong.tech${ pathname === '/' ? '/' : pathname } `
140+
141+ document . title = meta . title
142+ setMeta ( 'name' , 'description' , meta . description )
143+ setMeta ( 'property' , 'og:title' , meta . title )
144+ setMeta ( 'property' , 'og:description' , meta . description )
145+ setMeta ( 'property' , 'og:type' , isBlogPost ? 'article' : 'website' )
146+ setMeta ( 'property' , 'og:url' , canonicalUrl )
147+ setMeta ( 'name' , 'twitter:card' , 'summary_large_image' )
148+
149+ let canonical = document . head . querySelector ( 'link[rel="canonical"]' )
150+ if ( ! canonical ) {
151+ canonical = document . createElement ( 'link' )
152+ canonical . rel = 'canonical'
153+ document . head . appendChild ( canonical )
154+ }
155+ canonical . href = canonicalUrl
156+ } , [ pathname ] )
157+
158+ return null
159+ }
160+
161+ function RouteFallback ( ) {
162+ return < div className = "route-loading" role = "status" aria-live = "polite" > Loading page…</ div >
163+ }
164+
77165// StaggeredMenu renders menu items as <a href={link}> (official React Bits
78166// implementation — left untouched). To enable client-side routing without
79167// modifying the official component source, we intercept clicks on
@@ -204,7 +292,9 @@ function Layout() {
204292 useClientSideNav ( triggerTransition )
205293
206294 return (
207- < div className = "app" >
295+ < NavContext . Provider value = { triggerTransition } >
296+ < div className = "app" >
297+ < PageMeta />
208298 { ! isGalleryCategory && (
209299 < StaggeredMenu
210300 position = "right"
@@ -224,37 +314,37 @@ function Layout() {
224314 < Routes >
225315 < Route path = "/" element = { < Home /> } />
226316 < Route path = "/about" element = {
227- < Suspense fallback = { null } >
317+ < Suspense fallback = { < RouteFallback /> } >
228318 < About />
229319 </ Suspense >
230320 } />
231321 < Route path = "/projects" element = {
232- < Suspense fallback = { null } >
322+ < Suspense fallback = { < RouteFallback /> } >
233323 < Projects />
234324 </ Suspense >
235325 } />
236326 < Route path = "/gallery" element = {
237- < Suspense fallback = { null } >
327+ < Suspense fallback = { < RouteFallback /> } >
238328 < Gallery />
239329 </ Suspense >
240330 } />
241331 < Route path = "/gallery/:category" element = {
242- < Suspense fallback = { null } >
332+ < Suspense fallback = { < RouteFallback /> } >
243333 < GalleryCategory />
244334 </ Suspense >
245335 } />
246336 < Route path = "/blog" element = {
247- < Suspense fallback = { null } >
337+ < Suspense fallback = { < RouteFallback /> } >
248338 < Blog />
249339 </ Suspense >
250340 } />
251341 < Route path = "/blog/:slug" element = {
252- < Suspense fallback = { null } >
342+ < Suspense fallback = { < RouteFallback /> } >
253343 < BlogPost />
254344 </ Suspense >
255345 } />
256346 < Route path = "/awards" element = {
257- < Suspense fallback = { null } >
347+ < Suspense fallback = { < RouteFallback /> } >
258348 < Awards />
259349 </ Suspense >
260350 } />
@@ -263,7 +353,8 @@ function Layout() {
263353 </ main >
264354 { ! isGalleryCategory && < Footer /> }
265355 < PageTransition phase = { phase } />
266- </ div >
356+ </ div >
357+ </ NavContext . Provider >
267358 )
268359}
269360
0 commit comments