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 0000000..9569c8d
Binary files /dev/null and b/www/public/apple-touch-icon.png differ
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}/` },
+ ]),
];
---
-
+