Fix: NIP-95 upload 500 error

This commit is contained in:
Nicolas Cantu 2026-01-05 21:22:49 +01:00
parent 065ab30828
commit a90b77cec3
11 changed files with 562 additions and 4 deletions

View File

@ -0,0 +1,284 @@
import { useState, useEffect } from 'react'
import { configStorage } from '@/lib/configStorage'
import type { Nip95Config } from '@/lib/configStorageTypes'
interface Nip95ConfigManagerProps {
onConfigChange?: () => void
}
export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps) {
const [apis, setApis] = useState<Nip95Config[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [editingId, setEditingId] = useState<string | null>(null)
const [newUrl, setNewUrl] = useState('')
const [showAddForm, setShowAddForm] = useState(false)
useEffect(() => {
void loadApis()
}, [])
async function loadApis() {
try {
setLoading(true)
setError(null)
const config = await configStorage.getConfig()
setApis(config.nip95Apis.sort((a, b) => a.priority - b.priority))
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to load NIP-95 APIs'
setError(errorMessage)
console.error('Error loading NIP-95 APIs:', e)
} finally {
setLoading(false)
}
}
async function handleToggleEnabled(id: string, enabled: boolean) {
try {
await configStorage.updateNip95Api(id, { enabled })
await loadApis()
onConfigChange?.()
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to update API'
setError(errorMessage)
console.error('Error updating NIP-95 API:', e)
}
}
async function handleUpdatePriority(id: string, priority: number) {
try {
await configStorage.updateNip95Api(id, { priority })
await loadApis()
onConfigChange?.()
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to update priority'
setError(errorMessage)
console.error('Error updating priority:', e)
}
}
async function handleUpdateUrl(id: string, url: string) {
try {
await configStorage.updateNip95Api(id, { url })
await loadApis()
setEditingId(null)
onConfigChange?.()
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to update URL'
setError(errorMessage)
console.error('Error updating URL:', e)
}
}
async function handleAddApi() {
if (!newUrl.trim()) {
setError('URL is required')
return
}
try {
// Validate URL format
new URL(newUrl)
await configStorage.addNip95Api(newUrl.trim(), false)
setNewUrl('')
setShowAddForm(false)
await loadApis()
onConfigChange?.()
} catch (e) {
if (e instanceof TypeError && e.message.includes('Invalid URL')) {
setError('Invalid URL format')
} else {
const errorMessage = e instanceof Error ? e.message : 'Failed to add API'
setError(errorMessage)
}
console.error('Error adding NIP-95 API:', e)
}
}
async function handleRemoveApi(id: string) {
if (!confirm('Are you sure you want to remove this endpoint?')) {
return
}
try {
await configStorage.removeNip95Api(id)
await loadApis()
onConfigChange?.()
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to remove API'
setError(errorMessage)
console.error('Error removing NIP-95 API:', e)
}
}
if (loading) {
return (
<div className="text-center py-8 text-neon-cyan">
<div>Loading...</div>
</div>
)
}
return (
<div className="space-y-6">
{error && (
<div className="bg-red-900/30 border border-red-500/50 rounded p-4 text-red-300">
{error}
<button
onClick={() => setError(null)}
className="ml-4 text-red-400 hover:text-red-200"
>
×
</button>
</div>
)}
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold text-neon-cyan">NIP-95 Upload Endpoints</h2>
<button
onClick={() => setShowAddForm(!showAddForm)}
className="px-4 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan border border-neon-cyan/50 rounded transition-colors"
>
{showAddForm ? 'Cancel' : '+ Add Endpoint'}
</button>
</div>
{showAddForm && (
<div className="bg-cyber-dark border border-neon-cyan/30 rounded p-4 space-y-4">
<div>
<label className="block text-sm font-medium text-cyber-accent mb-2">
Endpoint URL
</label>
<input
type="url"
value={newUrl}
onChange={(e) => setNewUrl(e.target.value)}
placeholder="https://example.com/upload"
className="w-full px-4 py-2 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
/>
</div>
<div className="flex gap-2">
<button
onClick={() => void handleAddApi()}
className="px-4 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan border border-neon-cyan/50 rounded transition-colors"
>
Add
</button>
<button
onClick={() => {
setShowAddForm(false)
setNewUrl('')
setError(null)
}}
className="px-4 py-2 bg-cyber-accent/20 hover:bg-cyber-accent/30 text-cyber-accent border border-cyber-accent/50 rounded transition-colors"
>
Cancel
</button>
</div>
</div>
)}
<div className="space-y-4">
{apis.length === 0 ? (
<div className="text-center py-8 text-cyber-accent">
No NIP-95 endpoints configured
</div>
) : (
apis.map((api) => (
<div
key={api.id}
className="bg-cyber-dark border border-neon-cyan/30 rounded p-4 space-y-3"
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
{editingId === api.id ? (
<div className="space-y-2">
<input
type="url"
defaultValue={api.url}
onBlur={(e) => {
if (e.target.value !== api.url) {
void handleUpdateUrl(api.id, e.target.value)
} else {
setEditingId(null)
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.currentTarget.blur()
} else if (e.key === 'Escape') {
setEditingId(null)
}
}}
className="w-full px-3 py-2 bg-cyber-darker border border-neon-cyan/50 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
autoFocus
/>
</div>
) : (
<div
className="text-neon-cyan cursor-pointer hover:text-neon-green transition-colors"
onClick={() => setEditingId(api.id)}
title="Click to edit URL"
>
{api.url}
</div>
)}
<div className="text-sm text-cyber-accent mt-1">
Priority: {api.priority} | ID: {api.id}
</div>
</div>
<div className="flex items-center gap-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={api.enabled}
onChange={(e) => void handleToggleEnabled(api.id, e.target.checked)}
className="w-4 h-4 text-neon-cyan bg-cyber-darker border-cyber-accent rounded focus:ring-neon-cyan"
/>
<span className="text-sm text-cyber-accent">
{api.enabled ? 'Enabled' : 'Disabled'}
</span>
</label>
<button
onClick={() => void handleRemoveApi(api.id)}
className="px-3 py-1 text-sm bg-red-900/30 hover:bg-red-900/50 text-red-300 border border-red-500/50 rounded transition-colors"
title="Remove endpoint"
>
Remove
</button>
</div>
</div>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2">
<span className="text-sm text-cyber-accent">Priority:</span>
<input
type="number"
min="1"
value={api.priority}
onChange={(e) => {
const priority = parseInt(e.target.value, 10)
if (!isNaN(priority) && priority > 0) {
void handleUpdatePriority(api.id, priority)
}
}}
className="w-20 px-2 py-1 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
/>
</label>
</div>
</div>
))
)}
</div>
<div className="text-sm text-cyber-accent space-y-2">
<p>
<strong>Note:</strong> Endpoints are tried in priority order (lower number = higher priority).
Only enabled endpoints will be used for uploads.
</p>
<p>
If an endpoint fails, the next enabled endpoint will be tried automatically.
</p>
</div>
</div>
)
}

View File

@ -18,6 +18,12 @@ export function PageHeader() {
> >
{t('nav.documentation')} {t('nav.documentation')}
</Link> </Link>
<Link
href="/settings"
className="px-4 py-2 text-cyber-accent hover:text-neon-cyan text-sm font-medium transition-colors border border-cyber-accent/30 hover:border-neon-cyan/50 rounded hover:shadow-glow-cyan"
>
Settings
</Link>
<ConditionalPublishButton /> <ConditionalPublishButton />
</div> </div>
</div> </div>

View File

@ -53,3 +53,4 @@ Aucun déploiement spécial nécessaire. Les modifications sont purement fronten

View File

@ -0,0 +1,92 @@
# Configuration des endpoints NIP-95
## Date
2025-01-27
## Objectif
Permettre aux utilisateurs de configurer les endpoints NIP-95 pour l'upload de médias via une interface utilisateur, avec la possibilité d'ajouter, modifier, activer/désactiver et supprimer des endpoints.
## Motivations
- Résoudre les problèmes de DNS/connectivité en permettant d'utiliser plusieurs endpoints
- Donner aux utilisateurs le contrôle sur les endpoints utilisés
- Faciliter l'ajout de nouveaux endpoints sans modifier le code
- Permettre l'activation/désactivation d'endpoints selon les besoins
## Impacts
- **Utilisateurs** : Peuvent configurer leurs propres endpoints NIP-95 via l'interface
- **Développeurs** : Plus besoin de modifier le code pour ajouter des endpoints
- **Fiabilité** : Possibilité d'avoir plusieurs endpoints de secours activés
## Modifications
### Fichiers créés
- **`pages/settings.tsx`** : Page de configuration des paramètres de l'application
- **`components/Nip95ConfigManager.tsx`** : Composant de gestion des endpoints NIP-95 avec interface complète
### Fichiers modifiés
- **`lib/configStorageTypes.ts`** : Ajout d'un endpoint supplémentaire (`nostrimg.com`) dans `DEFAULT_NIP95_APIS`
- **`components/PageHeader.tsx`** : Ajout d'un lien vers la page Settings dans la navigation
### Fonctionnalités implémentées
#### Interface de gestion des endpoints
- **Affichage de la liste** : Tous les endpoints configurés sont affichés avec leur statut (activé/désactivé), priorité et URL
- **Ajout d'endpoints** : Formulaire pour ajouter de nouveaux endpoints avec validation d'URL
- **Modification d'URL** : Clic sur l'URL pour la modifier directement
- **Activation/Désactivation** : Checkbox pour activer ou désactiver chaque endpoint
- **Gestion de la priorité** : Champ numérique pour définir l'ordre de tentative (plus bas = priorité plus haute)
- **Suppression** : Bouton pour supprimer un endpoint avec confirmation
#### Endpoints par défaut
Les endpoints suivants sont maintenant disponibles par défaut :
1. `https://void.cat/upload` (activé, priorité 1)
2. `https://nostr.build/api/v2/upload` (désactivé, priorité 2)
3. `https://picstr.build/api/v1/upload` (désactivé, priorité 3)
4. `https://nostrcheck.me/api/v1/media` (désactivé, priorité 4)
5. `https://nostrimg.com/api/upload` (désactivé, priorité 5) - **Nouveau**
## Modalités de déploiement
1. Les modifications sont dans le code source
2. Rebuild de l'application : `npm run build`
3. Redémarrage du service Next.js
4. Aucune migration de données nécessaire (les endpoints par défaut sont créés automatiquement)
## Modalités d'analyse
Pour vérifier que la fonctionnalité fonctionne :
1. **Accéder à la page Settings** :
- Naviguer vers `/settings` depuis le menu de navigation
- Vérifier que la liste des endpoints s'affiche
2. **Tester l'ajout d'un endpoint** :
- Cliquer sur "+ Add Endpoint"
- Entrer une URL valide (ex: `https://example.com/upload`)
- Vérifier que l'endpoint apparaît dans la liste
3. **Tester l'activation/désactivation** :
- Cocher/décocher la checkbox d'un endpoint
- Vérifier que le statut change
- Tester un upload pour vérifier que seuls les endpoints activés sont utilisés
4. **Tester la modification de priorité** :
- Modifier la priorité d'un endpoint
- Vérifier que l'ordre dans la liste change
- Tester un upload pour vérifier l'ordre de tentative
5. **Tester la modification d'URL** :
- Cliquer sur une URL pour la modifier
- Entrer une nouvelle URL et appuyer sur Enter
- Vérifier que l'URL est mise à jour
6. **Tester la suppression** :
- Cliquer sur "Remove" d'un endpoint
- Confirmer la suppression
- Vérifier que l'endpoint disparaît de la liste
## Notes
- Les endpoints sont stockés dans IndexedDB (côté client)
- Les modifications sont persistantes et survivent aux rechargements de page
- Les endpoints sont essayés dans l'ordre de priorité (plus bas = priorité plus haute)
- Seuls les endpoints activés sont utilisés pour les uploads
- Si tous les endpoints activés échouent, une erreur est retournée à l'utilisateur
- La validation d'URL vérifie le format mais pas la disponibilité du service

View File

@ -55,3 +55,4 @@ Pour vérifier si le problème existe :
- SVG est plus léger et plus flexible qu'un fichier .ico - SVG est plus léger et plus flexible qu'un fichier .ico
- Si nécessaire, on peut créer un fichier `.ico` en plus pour la compatibilité avec les anciens navigateurs - Si nécessaire, on peut créer un fichier `.ico` en plus pour la compatibilité avec les anciens navigateurs
- Le favicon SVG actuel est minimal (rectangle cyan) et peut être remplacé par un design plus élaboré si nécessaire - Le favicon SVG actuel est minimal (rectangle cyan) et peut être remplacé par un design plus élaboré si nécessaire

View File

@ -61,3 +61,4 @@ Pour vérifier si le problème existe :
- Le package `form-data` (npm) doit être utilisé avec les modules `https`/`http` natifs de Node.js, pas avec `fetch()` - Le package `form-data` (npm) doit être utilisé avec les modules `https`/`http` natifs de Node.js, pas avec `fetch()`
- `fetch()` natif de Node.js est compatible avec FormData du web standard (disponible dans le navigateur), pas avec le package npm `form-data` - `fetch()` natif de Node.js est compatible avec FormData du web standard (disponible dans le navigateur), pas avec le package npm `form-data`
- Les fichiers temporaires créés par formidable doivent être nettoyés même en cas d'erreur pour éviter l'accumulation de fichiers - Les fichiers temporaires créés par formidable doivent être nettoyés même en cas d'erreur pour éviter l'accumulation de fichiers

View File

@ -0,0 +1,107 @@
# Problème : Erreur DNS resolution pour NIP-95 upload
## Date
2025-01-27
## Problème
L'API NIP-95 upload retourne une erreur `getaddrinfo ENOTFOUND void.cat` lors des tentatives d'upload, indiquant que le serveur Node.js ne peut pas résoudre le nom de domaine.
## Symptômes
- Erreur : `Failed to upload to all endpoints: {"error":"Failed to connect to upload endpoint: getaddrinfo ENOTFOUND void.cat"}`
- Code d'erreur Node.js : `ENOTFOUND` ou `EAI_AGAIN`
- Les uploads NIP-95 échouent avec une erreur de résolution DNS
## Root cause
Le serveur Node.js qui exécute l'API Next.js ne peut pas résoudre le nom de domaine `void.cat` (ou d'autres endpoints NIP-95). Cela peut être dû à :
1. **Problème de DNS sur le serveur** : Le serveur n'a pas accès à un serveur DNS fonctionnel
2. **Pas d'accès Internet** : Le serveur n'a pas de connexion Internet ou est derrière un pare-feu qui bloque les requêtes sortantes
3. **Configuration réseau incorrecte** : Les paramètres réseau du serveur sont mal configurés
4. **Problème de résolution DNS temporaire** : Problème temporaire avec le serveur DNS
## Impact
- Impossible d'uploader des fichiers via NIP-95
- Les utilisateurs ne peuvent pas publier d'articles avec des médias
- Fonctionnalité d'upload complètement non fonctionnelle
## Correctifs
1. **Amélioration de la gestion d'erreur DNS** : Détection spécifique des erreurs DNS avec messages d'erreur plus clairs
2. **Ajout d'un timeout** : Timeout de 30 secondes pour éviter que les requêtes restent bloquées indéfiniment
3. **Amélioration des logs** : Logs plus détaillés pour diagnostiquer les problèmes DNS
## Modifications
- **Fichier modifié** : `pages/api/nip95-upload.ts`
- **Ajout de timeout** : `timeout: 30000` dans les options de requête
- **Détection spécifique des erreurs DNS** : Vérification des codes d'erreur `ENOTFOUND` et `EAI_AGAIN`
- **Messages d'erreur améliorés** : Messages plus clairs pour les erreurs DNS avec suggestions de diagnostic
- **Logs améliorés** : Logs incluant le hostname, le code d'erreur et des suggestions
## Modalités de déploiement
1. Les modifications sont dans le code source
2. Rebuild de l'application : `npm run build`
3. Redémarrage du service Next.js
4. Aucune dépendance supplémentaire nécessaire
## Modalités d'analyse
Pour diagnostiquer le problème DNS :
1. **Vérifier la résolution DNS depuis le serveur** :
```bash
# Depuis le serveur
nslookup void.cat
# ou
dig void.cat
# ou
host void.cat
```
2. **Vérifier la connectivité réseau** :
```bash
# Tester la connexion HTTPS
curl -v https://void.cat/upload
# ou
wget --spider https://void.cat/upload
```
3. **Vérifier les logs du serveur Node.js** :
- Chercher les erreurs `ENOTFOUND` ou `EAI_AGAIN`
- Vérifier les logs avec le hostname et les suggestions
4. **Vérifier la configuration DNS du serveur** :
```bash
# Vérifier les serveurs DNS configurés
cat /etc/resolv.conf
# ou sur systemd
systemd-resolve --status
```
5. **Tester depuis le conteneur/service** :
```bash
# Si le service tourne dans un conteneur Docker
docker exec <container_name> nslookup void.cat
docker exec <container_name> curl -v https://void.cat/upload
```
## Solutions possibles
### Si le problème est la résolution DNS
1. Vérifier que le serveur a accès à un serveur DNS fonctionnel
2. Configurer des serveurs DNS fiables (par exemple 8.8.8.8, 1.1.1.1)
3. Vérifier que `/etc/resolv.conf` contient des serveurs DNS valides
### Si le problème est l'accès Internet
1. Vérifier que le serveur a une connexion Internet fonctionnelle
2. Vérifier les règles de pare-feu qui pourraient bloquer les requêtes sortantes HTTPS
3. Vérifier les proxies si le serveur est derrière un proxy
### Si le problème est temporaire
1. Attendre quelques minutes et réessayer
2. Vérifier si d'autres services ont des problèmes de connectivité
3. Vérifier les statuts des endpoints NIP-95 (void.cat, etc.)
## Notes
- L'erreur `ENOTFOUND` signifie que le nom de domaine n'a pas pu être résolu en adresse IP
- L'erreur `EAI_AGAIN` signifie que la résolution DNS a échoué temporairement (peut être réessayé)
- Le timeout de 30 secondes évite que les requêtes restent bloquées indéfiniment
- Les messages d'erreur améliorés aident à diagnostiquer rapidement le problème
- Si le problème persiste, vérifier la configuration réseau du serveur et l'accès à Internet

View File

@ -49,24 +49,31 @@ export const DEFAULT_NIP95_APIS: Nip95Config[] = [
{ {
id: 'nostrbuild', id: 'nostrbuild',
url: 'https://nostr.build/api/v2/upload', url: 'https://nostr.build/api/v2/upload',
enabled: false, enabled: true,
priority: 2, priority: 2,
createdAt: Date.now(), createdAt: Date.now(),
}, },
{ {
id: 'picstr', id: 'picstr',
url: 'https://picstr.build/api/v1/upload', url: 'https://picstr.build/api/v1/upload',
enabled: false, enabled: true,
priority: 3, priority: 3,
createdAt: Date.now(), createdAt: Date.now(),
}, },
{ {
id: 'nostrcheck', id: 'nostrcheck',
url: 'https://nostrcheck.me/api/v1/media', url: 'https://nostrcheck.me/api/v1/media',
enabled: false, enabled: true,
priority: 4, priority: 4,
createdAt: Date.now(), createdAt: Date.now(),
}, },
{
id: 'nostrimg',
url: 'https://nostrimg.com/api/upload',
enabled: true,
priority: 5,
createdAt: Date.now(),
},
] ]
export const DEFAULT_PLATFORM_LIGHTNING_ADDRESS = '' export const DEFAULT_PLATFORM_LIGHTNING_ADDRESS = ''

View File

@ -78,6 +78,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
path: targetUrl.pathname + targetUrl.search, path: targetUrl.pathname + targetUrl.search,
method: 'POST', method: 'POST',
headers: headers, headers: headers,
timeout: 30000, // 30 seconds timeout
} }
const proxyRequest = clientModule.request(requestOptions, (proxyResponse) => { const proxyRequest = clientModule.request(requestOptions, (proxyResponse) => {
@ -98,8 +99,27 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}) })
}) })
// Set timeout on the request
proxyRequest.setTimeout(30000, () => {
proxyRequest.destroy()
reject(new Error('Request timeout after 30 seconds'))
})
proxyRequest.on('error', (error) => { proxyRequest.on('error', (error) => {
reject(error) // Check for DNS errors specifically
const errorCode = (error as NodeJS.ErrnoException).code
if (errorCode === 'ENOTFOUND' || errorCode === 'EAI_AGAIN') {
console.error('NIP-95 proxy DNS error:', {
targetEndpoint,
hostname: targetUrl.hostname,
errorCode,
errorMessage: error.message,
suggestion: 'Check DNS resolution or network connectivity on the server',
})
reject(new Error(`DNS resolution failed for ${targetUrl.hostname}: ${error.message}`))
} else {
reject(error)
}
}) })
formData.on('error', (error) => { formData.on('error', (error) => {
@ -116,12 +136,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
console.error('Error deleting temp file:', unlinkError) console.error('Error deleting temp file:', unlinkError)
} }
const errorMessage = requestError instanceof Error ? requestError.message : 'Unknown request error' const errorMessage = requestError instanceof Error ? requestError.message : 'Unknown request error'
const isDnsError = errorMessage.includes('DNS resolution failed') || errorMessage.includes('ENOTFOUND') || errorMessage.includes('EAI_AGAIN')
console.error('NIP-95 proxy request error:', { console.error('NIP-95 proxy request error:', {
targetEndpoint, targetEndpoint,
hostname: targetUrl.hostname,
error: errorMessage, error: errorMessage,
isDnsError,
fileSize: fileField.size, fileSize: fileField.size,
fileName: fileField.originalFilename, fileName: fileField.originalFilename,
suggestion: isDnsError ? 'The server cannot resolve the domain name. Check DNS configuration and network connectivity.' : undefined,
}) })
// Return a more specific error message for DNS issues
if (isDnsError) {
return res.status(500).json({
error: `DNS resolution failed for ${targetUrl.hostname}. The server cannot resolve the domain name. Please check DNS configuration and network connectivity.`,
})
}
return res.status(500).json({ return res.status(500).json({
error: `Failed to connect to upload endpoint: ${errorMessage}`, error: `Failed to connect to upload endpoint: ${errorMessage}`,
}) })

25
pages/settings.tsx Normal file
View File

@ -0,0 +1,25 @@
import Head from 'next/head'
import { PageHeader } from '@/components/PageHeader'
import { Footer } from '@/components/Footer'
import { Nip95ConfigManager } from '@/components/Nip95ConfigManager'
export default function SettingsPage() {
return (
<>
<Head>
<title>Settings - zapwall.fr</title>
<meta name="description" content="Application settings and configuration" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
</Head>
<main className="min-h-screen bg-cyber-darker">
<PageHeader />
<div className="max-w-4xl mx-auto px-4 py-8">
<h1 className="text-3xl font-bold text-neon-cyan mb-8">Settings</h1>
<Nip95ConfigManager />
</div>
<Footer />
</main>
</>
)
}

View File

@ -2,3 +2,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<rect width="16" height="16" fill="#00ffff"/> <rect width="16" height="16" fill="#00ffff"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 178 B

After

Width:  |  Height:  |  Size: 179 B