@@ -195,8 +195,8 @@ const adminLimiter = rateLimit({
195195app . use ( '/api/' , apiLimiter ) ;
196196
197197app . get ( '/' , ( req : Request , res : Response ) => {
198- const userParam = req . query . user || req . query . username ;
199- const { theme } = req . query ;
198+ const userQuery = typeof req . query . user === 'string' ? req . query . user : ( typeof req . query . username === 'string' ? req . query . username : '' ) ;
199+ const themeQuery = typeof req . query . theme === 'string' ? req . query . theme : '' ;
200200 const indexPath = path . join ( __dirname , '../../../../public/index.html' ) ;
201201
202202 if ( ! fs . existsSync ( indexPath ) ) {
@@ -215,58 +215,87 @@ app.get('/', (req: Request, res: Response) => {
215215 let targetUsername = 'creativecode' ;
216216 let targetTheme = 'radical' ;
217217
218- if ( typeof userParam === 'string' && / ^ [ a - z \d ] (?: [ a - z \d ] | - (? = [ a - z \d ] ) ) { 0 , 38 } $ / i. test ( userParam ) ) {
219- targetUsername = userParam ;
218+ if ( userQuery && / ^ [ a - z \d ] (?: [ a - z \d ] | - (? = [ a - z \d ] ) ) { 0 , 38 } $ / i. test ( userQuery ) ) {
219+ targetUsername = userQuery ;
220220 }
221- if ( typeof theme === 'string' && / ^ [ a - z \d _ ] { 1 , 50 } $ / i. test ( theme ) ) {
222- targetTheme = theme ;
221+ if ( themeQuery && / ^ [ a - z \d _ ] { 1 , 50 } $ / i. test ( themeQuery ) ) {
222+ targetTheme = themeQuery ;
223223 }
224224
225- const safeImageUrl = escapeXml ( `${ baseUrl } /api/stats?username=${ encodeURIComponent ( targetUsername ) } &theme=${ encodeURIComponent ( targetTheme ) } ` ) ;
225+ const encodedUser = encodeURIComponent ( targetUsername ) ;
226+ const encodedTheme = encodeURIComponent ( targetTheme ) ;
227+
228+ const safeImageUrl = escapeXml ( `${ baseUrl } /api/stats?username=${ encodedUser } &theme=${ encodedTheme } ` ) ;
226229 const safeTitle = escapeXml ( `Tarjetas de estadísticas para @${ targetUsername } | GitHub Helpers` ) ;
227230 const safeDescription = escapeXml ( `Mira las estadísticas, lenguajes más usados y trofeos de GitHub para @${ targetUsername } generados dinámicamente.` ) ;
228231
229- // Dynamically replace SEO / OpenGraph tags safely
232+ // Dynamically replace SEO / OpenGraph tags safely using replacer functions
230233 html = html
231234 . replace (
232235 / < m e t a p r o p e r t y = " o g : i m a g e " c o n t e n t = " [ ^ " ] * " \/ ? > / gi,
233- `<meta property="og:image" content="${ safeImageUrl } " />`
236+ ( ) => `<meta property="og:image" content="${ safeImageUrl } " />`
234237 )
235238 . replace (
236239 / < m e t a p r o p e r t y = " t w i t t e r : i m a g e " c o n t e n t = " [ ^ " ] * " \/ ? > / gi,
237- `<meta property="twitter:image" content="${ safeImageUrl } " />`
240+ ( ) => `<meta property="twitter:image" content="${ safeImageUrl } " />`
238241 )
239242 . replace (
240243 / < m e t a p r o p e r t y = " o g : t i t l e " c o n t e n t = " [ ^ " ] * " \/ ? > / gi,
241- `<meta property="og:title" content="${ safeTitle } " />`
244+ ( ) => `<meta property="og:title" content="${ safeTitle } " />`
242245 )
243246 . replace (
244247 / < m e t a p r o p e r t y = " t w i t t e r : t i t l e " c o n t e n t = " [ ^ " ] * " \/ ? > / gi,
245- `<meta property="twitter:title" content="${ safeTitle } " />`
248+ ( ) => `<meta property="twitter:title" content="${ safeTitle } " />`
246249 )
247250 . replace (
248251 / < m e t a p r o p e r t y = " o g : d e s c r i p t i o n " c o n t e n t = " [ ^ " ] * " \/ ? > / gi,
249- `<meta property="og:description" content="${ safeDescription } " />`
252+ ( ) => `<meta property="og:description" content="${ safeDescription } " />`
250253 )
251254 . replace (
252255 / < m e t a p r o p e r t y = " t w i t t e r : d e s c r i p t i o n " c o n t e n t = " [ ^ " ] * " \/ ? > / gi,
253- `<meta property="twitter:description" content="${ safeDescription } " />`
256+ ( ) => `<meta property="twitter:description" content="${ safeDescription } " />`
254257 )
255258 . replace (
256259 / < m e t a n a m e = " d e s c r i p t i o n " c o n t e n t = " [ ^ " ] * " \/ ? > / gi,
257- `<meta name="description" content="${ safeDescription } " />`
260+ ( ) => `<meta name="description" content="${ safeDescription } " />`
258261 )
259- . replace ( / < t i t l e > [ ^ < ] * < \/ t i t l e > / gi, `<title>${ safeTitle } </title>` ) ;
262+ . replace ( / < t i t l e > [ ^ < ] * < \/ t i t l e > / gi, ( ) => `<title>${ safeTitle } </title>` ) ;
260263
261264 res . setHeader ( 'Content-Type' , 'text/html' ) ;
265+ res . setHeader ( 'Cache-Control' , 'no-cache, must-revalidate' ) ;
262266 res . status ( 200 ) . send ( html ) ;
263267} ) ;
264268
265269app . get ( '/admin/metrics' , adminLimiter , ( _req : Request , res : Response ) => {
270+ res . setHeader ( 'Cache-Control' , 'no-cache, must-revalidate' ) ;
266271 res . sendFile ( path . join ( __dirname , '../../../../public/admin/metrics.html' ) ) ;
267272} ) ;
268273
269- app . use ( express . static ( path . join ( __dirname , '../../../../public' ) , { extensions : [ 'html' ] } ) ) ;
274+ app . use (
275+ express . static ( path . join ( __dirname , '../../../../public' ) , {
276+ extensions : [ 'html' ] ,
277+ setHeaders : ( res , filePath ) => {
278+ if ( filePath . includes ( '/_astro/' ) ) {
279+ res . setHeader ( 'Cache-Control' , 'public, max-age=31536000, immutable' ) ;
280+ } else if ( filePath . endsWith ( '.html' ) ) {
281+ res . setHeader ( 'Cache-Control' , 'no-cache, must-revalidate' ) ;
282+ }
283+ }
284+ } )
285+ ) ;
286+
287+ // Explicit 404 handler for missing static assets to prevent default Express HTML error pages from breaking MIME checks in browsers
288+ app . use ( '/_astro' , ( _req : Request , res : Response ) => {
289+ res . status ( 404 ) . type ( 'text/plain' ) . send ( 'Asset not found' ) ;
290+ } ) ;
291+
292+ app . use ( ( req : Request , res : Response , next : ( ) => void ) => {
293+ if ( / \. (?: c s s | j s | p n g | j p g | j p e g | g i f | s v g | i c o | t x t | x m l | w o f f 2 ? ) $ / i. test ( req . path ) ) {
294+ res . status ( 404 ) . type ( 'text/plain' ) . send ( 'File not found' ) ;
295+ return ;
296+ }
297+ next ( ) ;
298+ } ) ;
270299
271300function checkMetricsKey ( req : Request , res : Response , next : ( ) => void ) {
272301 const expectedKey = process . env . METRICS_KEY ;
0 commit comments