feat(www): landing page (#25)

* design: landing page spec + app icon asset

* chore(www): add anime.js, fonts, and landing page screenshot assets

* feat(www): add design tokens, fonts, shared layout

* feat(www): add landing page sections and styling

* feat(www): add theme toggle and anime.js viewport animations

* feat(www): feedback updates — no hero card, os-aware download, footer email, lowercase colored heading

* feat(www): remove screenshot borders, move macos hint below ctas, mobile github fallback

* feat(www): keep hero CTAs side by side

* feat(www): set initial nav border to transparent

* feat(www): add Rybbit prod-only, sitemap, SEO head, OG image, robots.txt, 404 page

* docs(AGENTS): include www landing page version in release sync checklist

* chore(www): add railpack.json for Dokploy deploy (force node provider, TZ)

* chore(www): use bun for Railpack deploy (packageManager + standalone www/bun.lock)

* docs(AGENTS): note www/bun.lock sync for Railpack deploys

* chore(www): add vitest test tooling

* feat(www): add FAQ data source

* feat(www): add SEO JSON-LD builders

* feat(www): add Seo JSON-LD component

* feat(www): wire Seo component and social/meta tags into Layout

* feat(www): add homepage JSON-LD, keyword H1, data-driven FAQ

* fix(www): point Roadmap footer link to GitHub

* feat(www): add lastmod to sitemap

* perf(www): self-host anime.js

* refactor(www): move nav and footer into shared Layout

* feat(www): add changelog page

* fix(www): make shared component styles global in Layout

* perf(www): optimize screenshots with Astro Image (webp/avif + srcset)

* feat(www): add web app manifest

* feat(www): add helpful links to 404 page

* fix(www): shorten homepage H1 copy

* fix(www): OS-aware navbar download + footer layout

* feat(www): build full changelog from v0.7.0 to v0.7.10
This commit is contained in:
2026-08-17 09:16:58 +08:00
committed by GitHub
parent 62eed15844
commit 501fed962c
33 changed files with 2361 additions and 16 deletions
+108
View File
@@ -0,0 +1,108 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import satori from "satori";
import sharp from "sharp";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, "..");
const literataPath = path.resolve(__dirname, "fonts/or3PQ6P12-iJxAIgLa78DkrbXsDgk0oVDaDPYLanFLHpPf2TbBG_F_Y.ttf");
const outfitPath = path.resolve(__dirname, "fonts/QGYyz_MVcBeNP4NjuGObqx1XmO1I4TC1C4E.ttf");
const iconPath = path.resolve(root, "public/gridline-icon.svg");
const fonts = [
{ name: "Literata", data: fs.readFileSync(literataPath), weight: 400, style: "normal" },
{ name: "Outfit", data: fs.readFileSync(outfitPath), weight: 400, style: "normal" },
];
const iconSvg = fs.readFileSync(iconPath, "utf-8");
const iconDataUri = `data:image/svg+xml;base64,${Buffer.from(iconSvg).toString("base64")}`;
const element = {
type: "div",
props: {
style: {
width: 1200,
height: 630,
background: "#0a0a0a",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: 80,
fontFamily: "Outfit",
color: "#fafafa",
position: "relative",
},
children: [
{
type: "div",
props: {
style: {
position: "absolute",
inset: 0,
background:
"radial-gradient(circle at 50% 0%, rgba(59,130,246,0.15), transparent 60%)",
},
},
},
{
type: "img",
props: {
src: iconDataUri,
width: 140,
height: 140,
style: { marginBottom: 40 },
},
},
{
type: "h1",
props: {
style: {
fontFamily: "Literata",
fontSize: 84,
fontWeight: 400,
lineHeight: 1.05,
textAlign: "center",
margin: 0,
letterSpacing: "-0.02em",
},
children: "Gridline",
},
},
{
type: "p",
props: {
style: {
fontSize: 32,
color: "rgba(255,255,255,0.55)",
marginTop: 24,
textAlign: "center",
maxWidth: 800,
},
children: "The open-source database GUI for PostgreSQL, MySQL, SQLite, and Redis.",
},
},
{
type: "div",
props: {
style: {
position: "absolute",
bottom: 48,
fontSize: 22,
color: "rgba(255,255,255,0.35)",
fontFamily: "Outfit",
},
children: "getgridline.app",
},
},
],
},
};
const svg = await satori(element, { width: 1200, height: 630, fonts });
const png = await sharp(Buffer.from(svg)).png().toBuffer();
const outPath = path.resolve(root, "public/og.png");
fs.writeFileSync(outPath, png);
console.log(`OG image written to ${outPath}`);