Add support for iOS app icons and PWA favicons

Updates `client/index.html` and `client/public/manifest.json` to include proper iOS icon links and specifies PNG format for favicons. Enhances the `/api/favicon` endpoint to generate PNG favicons using `sharp` for various sizes and includes the "go4" text for larger icons. Also adds `@types/compression` to `package.json` and `package-lock.json`.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ab9cd02a-d0b2-4288-9ceb-1964d0059648
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/ab9cd02a-d0b2-4288-9ceb-1964d0059648/mbX83Mu
This commit is contained in:
sebastjanartic 2025-08-31 20:25:15 +00:00
parent 7c02d0310a
commit 87e07be40f
5 changed files with 85 additions and 40 deletions

View File

@ -37,19 +37,19 @@
<meta name="format-detection" content="telephone=no" />
<!-- iOS ikone za različne velikosti -->
<link rel="apple-touch-icon" sizes="57x57" href="/api/favicon?size=57" />
<link rel="apple-touch-icon" sizes="60x60" href="/api/favicon?size=60" />
<link rel="apple-touch-icon" sizes="72x72" href="/api/favicon?size=72" />
<link rel="apple-touch-icon" sizes="76x76" href="/api/favicon?size=76" />
<link rel="apple-touch-icon" sizes="114x114" href="/api/favicon?size=114" />
<link rel="apple-touch-icon" sizes="120x120" href="/api/favicon?size=120" />
<link rel="apple-touch-icon" sizes="144x144" href="/api/favicon?size=144" />
<link rel="apple-touch-icon" sizes="152x152" href="/api/favicon?size=152" />
<link rel="apple-touch-icon" sizes="180x180" href="/api/favicon?size=180" />
<link rel="apple-touch-icon" sizes="57x57" href="/api/favicon?size=57&format=png" />
<link rel="apple-touch-icon" sizes="60x60" href="/api/favicon?size=60&format=png" />
<link rel="apple-touch-icon" sizes="72x72" href="/api/favicon?size=72&format=png" />
<link rel="apple-touch-icon" sizes="76x76" href="/api/favicon?size=76&format=png" />
<link rel="apple-touch-icon" sizes="114x114" href="/api/favicon?size=114&format=png" />
<link rel="apple-touch-icon" sizes="120x120" href="/api/favicon?size=120&format=png" />
<link rel="apple-touch-icon" sizes="144x144" href="/api/favicon?size=144&format=png" />
<link rel="apple-touch-icon" sizes="152x152" href="/api/favicon?size=152&format=png" />
<link rel="apple-touch-icon" sizes="180x180" href="/api/favicon?size=180&format=png" />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/api/favicon" />
<link rel="apple-touch-icon" href="/api/favicon?size=180" />
<link rel="apple-touch-icon" href="/api/favicon?size=180&format=png" />
<link rel="manifest" href="/manifest.json" />
</head>

View File

@ -14,67 +14,67 @@
"prefer_related_applications": false,
"icons": [
{
"src": "/api/favicon?size=57",
"src": "/api/favicon?size=57&format=png",
"sizes": "57x57",
"type": "image/png",
"purpose": "any"
},
{
"src": "/api/favicon?size=60",
"src": "/api/favicon?size=60&format=png",
"sizes": "60x60",
"type": "image/png",
"purpose": "any"
},
{
"src": "/api/favicon?size=72",
"src": "/api/favicon?size=72&format=png",
"sizes": "72x72",
"type": "image/png",
"purpose": "any"
},
{
"src": "/api/favicon?size=76",
"src": "/api/favicon?size=76&format=png",
"sizes": "76x76",
"type": "image/png",
"purpose": "any"
},
{
"src": "/api/favicon?size=114",
"src": "/api/favicon?size=114&format=png",
"sizes": "114x114",
"type": "image/png",
"purpose": "any"
},
{
"src": "/api/favicon?size=120",
"src": "/api/favicon?size=120&format=png",
"sizes": "120x120",
"type": "image/png",
"purpose": "any"
},
{
"src": "/api/favicon?size=144",
"src": "/api/favicon?size=144&format=png",
"sizes": "144x144",
"type": "image/png",
"purpose": "any"
},
{
"src": "/api/favicon?size=152",
"src": "/api/favicon?size=152&format=png",
"sizes": "152x152",
"type": "image/png",
"purpose": "any"
},
{
"src": "/api/favicon?size=180",
"src": "/api/favicon?size=180&format=png",
"sizes": "180x180",
"type": "image/png",
"purpose": "any"
},
{
"src": "/api/favicon?size=192",
"src": "/api/favicon?size=192&format=png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/api/favicon?size=512",
"src": "/api/favicon?size=512&format=png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"

11
package-lock.json generated
View File

@ -43,6 +43,7 @@
"@radix-ui/react-tooltip": "^1.2.0",
"@tanstack/react-query": "^5.60.5",
"@types/bcryptjs": "^2.4.6",
"@types/compression": "^1.8.1",
"@types/memoizee": "^0.4.12",
"@types/multer": "^2.0.0",
"@types/node-fetch": "^2.6.13",
@ -4326,6 +4327,16 @@
"integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==",
"license": "MIT"
},
"node_modules/@types/compression": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz",
"integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==",
"license": "MIT",
"dependencies": {
"@types/express": "*",
"@types/node": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",

View File

@ -45,6 +45,7 @@
"@radix-ui/react-tooltip": "^1.2.0",
"@tanstack/react-query": "^5.60.5",
"@types/bcryptjs": "^2.4.6",
"@types/compression": "^1.8.1",
"@types/memoizee": "^0.4.12",
"@types/multer": "^2.0.0",
"@types/node-fetch": "^2.6.13",

View File

@ -13,6 +13,7 @@ import multer from "multer";
import { randomUUID } from "crypto";
import path from "path";
import session from "express-session";
import sharp from "sharp";
// Extend express session
declare module "express-session" {
@ -690,32 +691,64 @@ export async function registerRoutes(app: Express): Promise<Server> {
});
// Favicon generation endpoint
app.get('/api/favicon', (req, res) => {
app.get('/api/favicon', async (req, res) => {
try {
const size = req.query.size ? parseInt(req.query.size as string) : 32;
const format = req.query.format as string || 'svg';
const padding = Math.max(2, size * 0.1);
const logoSize = size - (padding * 2);
const cornerRadius = Math.max(2, size * 0.15);
// Generate SVG favicon with just the logo
const svg = `<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="logoGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:1" />
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Logo background -->
<rect x="${padding}" y="${padding}" width="${logoSize}" height="${logoSize}" rx="${cornerRadius}" fill="url(#logoGradient)"/>
<!-- Play triangle -->
<polygon points="${padding + logoSize * 0.35},${padding + logoSize * 0.25} ${padding + logoSize * 0.35},${padding + logoSize * 0.75} ${padding + logoSize * 0.65},${padding + logoSize * 0.5}" fill="white"/>
</svg>`;
if (format === 'png') {
// Ustvarimo PNG ikono za iOS PWA
// Ustvarimo SVG za pretvorbo v PNG
const svg = `<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="logoGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:1" />
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:1" />
</linearGradient>
</defs>
<!-- iOS stil ozadje z rounded corners -->
<rect x="0" y="0" width="${size}" height="${size}" rx="${cornerRadius * 1.5}" fill="url(#logoGradient)"/>
<!-- Bel play triangle v sredini -->
<polygon points="${padding + logoSize * 0.35},${padding + logoSize * 0.25} ${padding + logoSize * 0.35},${padding + logoSize * 0.75} ${padding + logoSize * 0.65},${padding + logoSize * 0.5}" fill="white" opacity="0.95"/>
<!-- go4 besedilo pod trikotnikom -->
${size >= 120 ? `<text x="${size/2}" y="${size * 0.8}" font-family="Arial, sans-serif" font-size="${size * 0.12}" font-weight="bold" fill="white" text-anchor="middle" opacity="0.9">go4</text>` : ''}
</svg>`;
res.setHeader('Content-Type', 'image/svg+xml');
res.setHeader('Cache-Control', 'public, max-age=86400'); // Cache for 24 hours
res.send(svg);
// Pretvorimo SVG v PNG z Sharp
const pngBuffer = await sharp(Buffer.from(svg))
.png({ quality: 100, compressionLevel: 0 })
.toBuffer();
res.setHeader('Content-Type', 'image/png');
res.setHeader('Cache-Control', 'public, max-age=86400'); // Cache for 24 hours
res.send(pngBuffer);
} else {
// Originalni SVG favicon
const svg = `<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="logoGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:1" />
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Logo background -->
<rect x="${padding}" y="${padding}" width="${logoSize}" height="${logoSize}" rx="${cornerRadius}" fill="url(#logoGradient)"/>
<!-- Play triangle -->
<polygon points="${padding + logoSize * 0.35},${padding + logoSize * 0.25} ${padding + logoSize * 0.35},${padding + logoSize * 0.75} ${padding + logoSize * 0.65},${padding + logoSize * 0.5}" fill="white"/>
</svg>`;
res.setHeader('Content-Type', 'image/svg+xml');
res.setHeader('Cache-Control', 'public, max-age=86400'); // Cache for 24 hours
res.send(svg);
}
} catch (error) {
console.error('Error generating favicon:', error);
res.status(500).send('Error generating favicon');