From 08b0f79f0732b791124c6924582e9cef6813b2ed Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Wed, 2 Sep 2026 09:10:50 +0000 Subject: [PATCH] feat(www): SEO technical + on-page improvements (#12) --- www/astro.config.mjs | 4 +--- www/public/_redirects | 1 + www/public/apple-touch-icon.png | Bin 0 -> 6505 bytes www/public/site.webmanifest | 6 ++++++ www/src/layouts/Layout.astro | 8 +++++++- www/src/lib/seo.test.ts | 15 +++++++++++++++ www/src/lib/seo.ts | 23 +++++++++++++++++++++++ www/src/pages/index.astro | 16 +++++++++++++++- 8 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 www/public/_redirects create mode 100644 www/public/apple-touch-icon.png diff --git a/www/astro.config.mjs b/www/astro.config.mjs index d3fc86a..99e83c9 100644 --- a/www/astro.config.mjs +++ b/www/astro.config.mjs @@ -10,9 +10,7 @@ export default defineConfig({ compressHTML: true, trailingSlash: 'ignore', integrations: [ - sitemap({ - lastmod: new Date(), - }), + sitemap(), react(), ], vite: { diff --git a/www/public/_redirects b/www/public/_redirects new file mode 100644 index 0000000..aa33418 --- /dev/null +++ b/www/public/_redirects @@ -0,0 +1 @@ +/* /404.html 404 diff --git a/www/public/apple-touch-icon.png b/www/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9569c8dff228eafa442ea670fffb0cad30666494 GIT binary patch literal 6505 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 literal 0 HcmV?d00001 diff --git a/www/public/site.webmanifest b/www/public/site.webmanifest index a84ed6b..4389cc8 100644 --- a/www/public/site.webmanifest +++ b/www/public/site.webmanifest @@ -12,6 +12,12 @@ "sizes": "any", "type": "image/svg+xml", "purpose": "any" + }, + { + "src": "/apple-touch-icon.png", + "sizes": "180x180", + "type": "image/png", + "purpose": "any" } ] } diff --git a/www/src/layouts/Layout.astro b/www/src/layouts/Layout.astro index 82e8d05..fb18161 100644 --- a/www/src/layouts/Layout.astro +++ b/www/src/layouts/Layout.astro @@ -36,8 +36,13 @@ const isProd = import.meta.env.PROD; - + + + + + + @@ -54,6 +59,7 @@ const isProd = import.meta.env.PROD; + diff --git a/www/src/lib/seo.test.ts b/www/src/lib/seo.test.ts index b3886a7..1d1e516 100644 --- a/www/src/lib/seo.test.ts +++ b/www/src/lib/seo.test.ts @@ -6,6 +6,7 @@ import { buildWebSiteJsonLd, buildBreadcrumbJsonLd, buildTitle, + buildProductJsonLd, } from "./seo"; describe("seo builders", () => { @@ -70,4 +71,18 @@ describe("seo builders", () => { it("buildTitle appends the brand", () => { expect(buildTitle("PostgreSQL GUI")).toBe("PostgreSQL GUI | Gridline"); }); + + it("buildProductJsonLd returns valid Product schema", () => { + const ld = buildProductJsonLd({ + name: "Gridline", + url: "https://getgridline.app/", + description: "Open-source database GUI", + image: "https://getgridline.app/og.png", + offersPrice: 0, + offersCurrency: "USD", + }); + expect(ld["@type"]).toBe("Product"); + expect(ld.name).toBe("Gridline"); + expect((ld.offers as { price: number }).price).toBe(0); + }); }); diff --git a/www/src/lib/seo.ts b/www/src/lib/seo.ts index e7c9994..7c9fee5 100644 --- a/www/src/lib/seo.ts +++ b/www/src/lib/seo.ts @@ -108,3 +108,26 @@ export function buildBreadcrumbJsonLd( export function buildTitle(primary: string, brand = "Gridline"): string { return `${primary} | ${brand}`; } + +export function buildProductJsonLd(opts: { + name: string; + url: string; + description: string; + image: string; + offersPrice: number; + offersCurrency: string; +}): JsonLd { + return { + "@context": "https://schema.org", + "@type": "Product", + name: opts.name, + url: opts.url, + description: opts.description, + image: opts.image, + offers: { + "@type": "Offer", + price: opts.offersPrice, + priceCurrency: opts.offersCurrency, + }, + }; +} diff --git a/www/src/pages/index.astro b/www/src/pages/index.astro index 431614e..82f3fc8 100644 --- a/www/src/pages/index.astro +++ b/www/src/pages/index.astro @@ -6,6 +6,8 @@ import { buildFaqJsonLd, buildOrganizationJsonLd, buildWebSiteJsonLd, + buildProductJsonLd, + buildBreadcrumbJsonLd, } from "../lib/seo"; import { Image } from "astro:assets"; import HeroDbViewer from "../components/hero/HeroDbViewer"; @@ -42,9 +44,21 @@ const jsonLd = [ name: "Gridline", url: `${site}/`, }), + buildProductJsonLd({ + name: "Gridline", + url: `${site}/`, + description: + "The open-source database GUI for PostgreSQL, MySQL, SQLite, and Redis. No paywalls, no tab limits, fully open source.", + image: `${site}/og.png`, + offersPrice: 0, + offersCurrency: "USD", + }), + buildBreadcrumbJsonLd([ + { name: "Home", url: `${site}/` }, + ]), ]; --- - +