feat: improve SEO feature

This commit is contained in:
2026-08-31 23:12:38 +07:00
parent 025a1d66f7
commit c525e8e3a2
28 changed files with 715 additions and 29 deletions
+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient id="brand" x1="72" y1="48" x2="440" y2="464" gradientUnits="userSpaceOnUse">
<stop stop-color="#3ecf8e"/>
<stop offset="1" stop-color="#24b47e"/>
</linearGradient>
</defs>
<rect width="512" height="512" rx="112" fill="url(#brand)"/>
<path d="M285 70 132 283h118l-23 159 153-229H262L285 70Z" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="18"/>
</svg>

After

Width:  |  Height:  |  Size: 482 B

+30
View File
@@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
<defs>
<linearGradient id="background" x1="70" y1="10" x2="1120" y2="630" gradientUnits="userSpaceOnUse">
<stop stop-color="#f7fffb"/>
<stop offset="1" stop-color="#eefaf5"/>
</linearGradient>
<linearGradient id="brand" x1="76" y1="72" x2="240" y2="250" gradientUnits="userSpaceOnUse">
<stop stop-color="#3ecf8e"/>
<stop offset="1" stop-color="#24b47e"/>
</linearGradient>
<filter id="shadow" x="-30%" y="-30%" width="160%" height="160%">
<feDropShadow dx="0" dy="18" stdDeviation="24" flood-color="#0f5132" flood-opacity=".14"/>
</filter>
</defs>
<rect width="1200" height="630" fill="url(#background)"/>
<circle cx="1085" cy="-5" r="280" fill="#3ecf8e" opacity=".08"/>
<circle cx="80" cy="650" r="230" fill="#24b47e" opacity=".07"/>
<g transform="translate(80 74)" filter="url(#shadow)">
<rect width="150" height="150" rx="36" fill="url(#brand)"/>
<path d="m84 21-47 65h36l-7 45 47-68H77l7-42Z" fill="#fff" stroke="#fff" stroke-linejoin="round" stroke-width="6"/>
</g>
<text x="260" y="176" fill="#102a20" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="84" font-weight="750" letter-spacing="-4">upwatch</text>
<text x="80" y="330" fill="#173c2e" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="48" font-weight="650" letter-spacing="-1.4">Fast, dependable uptime monitoring</text>
<text x="80" y="390" fill="#557066" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="34">from Cloudflares edge.</text>
<g transform="translate(80 474)" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="24" font-weight="600">
<g><rect width="252" height="68" rx="34" fill="#fff" stroke="#dceae4"/><circle cx="34" cy="34" r="9" fill="#24b47e"/><text x="55" y="42" fill="#254a3b">API operational</text></g>
<g transform="translate(272)"><rect width="268" height="68" rx="34" fill="#fff" stroke="#dceae4"/><circle cx="34" cy="34" r="9" fill="#24b47e"/><text x="55" y="42" fill="#254a3b">Website operational</text></g>
<g transform="translate(560)"><rect width="246" height="68" rx="34" fill="#fff" stroke="#dceae4"/><circle cx="34" cy="34" r="9" fill="#24b47e"/><text x="55" y="42" fill="#254a3b">Edge operational</text></g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+38
View File
@@ -0,0 +1,38 @@
import { copyFile, mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import pngToIco from 'png-to-ico';
import sharp from 'sharp';
const root = path.resolve(import.meta.dirname, '..');
const assets = path.join(import.meta.dirname, 'assets');
const output = path.join(root, 'public');
const iconSource = path.join(assets, 'icon.svg');
const ogSource = path.join(assets, 'og.svg');
await mkdir(output, { recursive: true });
await copyFile(iconSource, path.join(output, 'favicon.svg'));
async function renderIcon(name: string, size: number) {
await sharp(iconSource).resize(size, size).png().toFile(path.join(output, name));
}
async function renderMaskableIcon() {
const icon = await sharp(iconSource).resize(512, 512).png().toBuffer();
await sharp({ create: { width: 512, height: 512, channels: 4, background: '#24b47e' } })
.composite([{ input: icon }])
.png()
.toFile(path.join(output, 'icon-512-maskable.png'));
}
await Promise.all([
renderIcon('apple-touch-icon.png', 180),
renderIcon('icon-192.png', 192),
renderIcon('icon-512.png', 512),
renderMaskableIcon(),
sharp(ogSource).resize(1200, 630).png().toFile(path.join(output, 'og.png')),
]);
const faviconPngs = await Promise.all([16, 32, 48].map((size) => sharp(iconSource).resize(size, size).png().toBuffer()));
await writeFile(path.join(output, 'favicon.ico'), await pngToIco(faviconPngs));
console.log(`Generated Upwatch icons in ${output}`);