6 Commits
8 changed files with 68 additions and 5 deletions
+1 -3
View File
@@ -10,9 +10,7 @@ export default defineConfig({
compressHTML: true, compressHTML: true,
trailingSlash: 'ignore', trailingSlash: 'ignore',
integrations: [ integrations: [
sitemap({ sitemap(),
lastmod: new Date(),
}),
react(), react(),
], ],
vite: { vite: {
+1
View File
@@ -0,0 +1 @@
/* /404.html 404
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

+6
View File
@@ -12,6 +12,12 @@
"sizes": "any", "sizes": "any",
"type": "image/svg+xml", "type": "image/svg+xml",
"purpose": "any" "purpose": "any"
},
{
"src": "/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png",
"purpose": "any"
} }
] ]
} }
+7 -1
View File
@@ -36,8 +36,13 @@ const isProd = import.meta.env.PROD;
<link rel="canonical" href={canonicalURL} /> <link rel="canonical" href={canonicalURL} />
<link rel="sitemap" href="/sitemap-index.xml" /> <link rel="sitemap" href="/sitemap-index.xml" />
<link rel="icon" type="image/svg+xml" href="/gridline-icon.svg" /> <link rel="icon" type="image/svg+xml" href="/gridline-icon.svg" />
<link rel="apple-touch-icon" href="/gridline-icon.svg" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<link rel="preload" as="image" href="/og.png" />
<link rel="preconnect" href="https://stat.ranio.xyz" crossorigin />
<link rel="preconnect" href="https://git.ranio.xyz" />
<link rel="dns-prefetch" href="https://stat.ranio.xyz" />
<link rel="dns-prefetch" href="https://git.ranio.xyz" />
<meta property="og:title" content={title} /> <meta property="og:title" content={title} />
<meta property="og:description" content={description} /> <meta property="og:description" content={description} />
@@ -54,6 +59,7 @@ const isProd = import.meta.env.PROD;
<meta name="twitter:title" content={title} /> <meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} /> <meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImageURL} /> <meta name="twitter:image" content={ogImageURL} />
<meta name="twitter:image:alt" content="Gridline — the open-source database GUI" />
<meta name="twitter:site" content="@AdrianBonpin" /> <meta name="twitter:site" content="@AdrianBonpin" />
<meta name="twitter:creator" content="@AdrianBonpin" /> <meta name="twitter:creator" content="@AdrianBonpin" />
+15
View File
@@ -6,6 +6,7 @@ import {
buildWebSiteJsonLd, buildWebSiteJsonLd,
buildBreadcrumbJsonLd, buildBreadcrumbJsonLd,
buildTitle, buildTitle,
buildProductJsonLd,
} from "./seo"; } from "./seo";
describe("seo builders", () => { describe("seo builders", () => {
@@ -70,4 +71,18 @@ describe("seo builders", () => {
it("buildTitle appends the brand", () => { it("buildTitle appends the brand", () => {
expect(buildTitle("PostgreSQL GUI")).toBe("PostgreSQL GUI | Gridline"); 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);
});
}); });
+23
View File
@@ -108,3 +108,26 @@ export function buildBreadcrumbJsonLd(
export function buildTitle(primary: string, brand = "Gridline"): string { export function buildTitle(primary: string, brand = "Gridline"): string {
return `${primary} | ${brand}`; 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,
},
};
}
+15 -1
View File
@@ -6,6 +6,8 @@ import {
buildFaqJsonLd, buildFaqJsonLd,
buildOrganizationJsonLd, buildOrganizationJsonLd,
buildWebSiteJsonLd, buildWebSiteJsonLd,
buildProductJsonLd,
buildBreadcrumbJsonLd,
} from "../lib/seo"; } from "../lib/seo";
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import HeroDbViewer from "../components/hero/HeroDbViewer"; import HeroDbViewer from "../components/hero/HeroDbViewer";
@@ -42,9 +44,21 @@ const jsonLd = [
name: "Gridline", name: "Gridline",
url: `${site}/`, 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}/` },
]),
]; ];
--- ---
<Layout title="Gridline — Open-Source Database GUI" jsonLd={jsonLd}> <Layout title="Gridline — Free Open-Source Database GUI" jsonLd={jsonLd}>
<script src="/landing.js" defer></script> <script src="/landing.js" defer></script>
<!-- Hero --> <!-- Hero -->