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
Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

+15
View File
@@ -0,0 +1,15 @@
---
import type { JsonLd } from "../lib/seo";
interface Props {
jsonLd?: JsonLd[];
}
const { jsonLd = [] } = Astro.props;
---
{
jsonLd.map((ld) => (
<script type="application/ld+json" set:html={JSON.stringify(ld)} />
))
}
+79
View File
@@ -0,0 +1,79 @@
export interface ChangelogEntry {
version: string;
date: string;
highlights: string[];
}
export const changelogEntries: ChangelogEntry[] = [
{
version: "0.7.10",
date: "2026-08-13",
highlights: [
"Copy connection URL from the connection card menu — with or without the password (password fetched from your OS keychain on demand, percent-encoded, sslmode appended for PostgreSQL).",
"First-launch documentation improvements for macOS (Gatekeeper / quarantine instructions).",
],
},
{
version: "0.7.9",
date: "2026-08-08",
highlights: ["Fix macOS launch issues on Apple Silicon."],
},
{
version: "0.7.8",
date: "2026-08-08",
highlights: [
"MySQL & SQLite backup and restore — SQLite `.dump` portability plus mysqldump for MySQL, matching the pg_dump UX.",
"DB-to-DB sync extended beyond PostgreSQL (MySQL & SQLite).",
"Excel (.xlsx) export alongside JSON / CSV / SQL / Markdown.",
"Cancel long-running queries from the toolbar (pg_cancel_backend / MySQL KILL / SQLite interruption).",
"Settings export/import (theme, accent, editor options, page sizes, defaults).",
"Windows/Linux title-bar fix — native title bar for dragging, macOS keeps the overlay drag strip.",
"Visual Create Table / Edit Table extended to SQLite (SQLite-aware type mapping and ALTER TABLE limits).",
],
},
{
version: "0.7.7",
date: "2026-08-07",
highlights: [
"PostgreSQL roles & grants — create/edit/drop roles, per-role privilege explorer grouped by object class, and GRANT/REVOKE staging with WITH GRANT OPTION.",
"Table maintenance — right-click any table for VACUUM / ANALYZE / REINDEX.",
"Visual Create Table editor — columns grid with type dropdowns, drag-to-reorder, single & composite primary keys, live SQL preview.",
"Edit Table as a robust column diff — ADD/DROP/RENAME/ALTER TYPE/SET|DROP DEFAULT/SET|DROP NOT NULL staged one per queue item, with a stale-write guard.",
"Atomic column-reorder table rebuild — single transaction, preserves constraints/indexes/FKs/grants/sequences, fail-closed for triggers/RLS/partitioning.",
"Foreign keys — multi-column FK composer, cross-schema references, ON DELETE / ON UPDATE actions, edit/remove from the form.",
"Table options — tablespace picker and row-level security toggle.",
"Fixes: rolconnlimit cast, aclexplode-based privilege queries (works on PG 15+), auto schema-tree refresh after schema-modifying commits.",
],
},
{
version: "0.7.6",
date: "2026-08-06",
highlights: [
"PostgreSQL object management CRUD — right-click / ⋮ create, edit, and drop every PG object type, opened as workspace tabs with a Visual ⇄ SQL toggle and staged through the changes queue with generated-SQL preview.",
"Objects view now uses the shared tabbed workspace — object details open as tabs, plus an inline manual query tab.",
"Keychain toggle (default ON) — opt out to keep DB passwords session-only and purge existing keychain entries; SSH secrets stay keychain-only.",
],
},
{
version: "0.7.5",
date: "2026-08-05",
highlights: [
"Bundled PostgreSQL client tools — pg_dump, pg_restore, and psql ship as Tauri resources with system-first, bundled-fallback resolution.",
"Schema CRUD — create, rename, and drop schemas from the object tree with CASCADE-drop typed-name confirmation and a dependency warning.",
"Global object search (Cmd+K) — current-schema scope across all object types; results open a table tab or jump to the Objects view.",
"Copy as DDL for any object — CREATE DDL for every browsable type (tables via pg_dump, pg_get_*def passthrough, synthesized sequences/enums/extensions/views).",
"Object dependencies — a pg_depend “what depends on this?” view shown before destructive drops (table, schema).",
],
},
{
version: "0.7.0",
date: "2026-08-04",
highlights: [
"Revamped New Connection screen — two-stage entry: paste a connection URI or pick from a 6-card provider grid (PostgreSQL / MySQL / SQLite / Redis / Supabase / NeonDB), then configure label, tags, environment, folder, and General | SSH·SSL tabs.",
"MySQL DB viewer — connect (SSL + SSH tunnel), browse databases/tables/columns/FKs, query with pagination, inline cell editing with a changes queue, DDL copy, CSV/JSON import.",
"SQLite connection via file-path picker instead of a URI field.",
"DB viewer capability gating — per-database-type feature matrix; Redis browsing gated with a clean “not supported” state.",
"Schema/database selector loading states, table toolbar renders during load, tag overflow scroll on connection cards.",
],
},
];
+20
View File
@@ -0,0 +1,20 @@
import { describe, it, expect } from "vitest";
import { faqItems } from "./faq";
describe("faqItems", () => {
it("has exactly 5 items", () => {
expect(faqItems).toHaveLength(5);
});
it("every item has a non-empty question and answer", () => {
for (const item of faqItems) {
expect(item.question.trim().length).toBeGreaterThan(0);
expect(item.answer.trim().length).toBeGreaterThan(0);
}
});
it("questions are unique", () => {
const questions = faqItems.map((i) => i.question);
expect(new Set(questions).size).toBe(questions.length);
});
});
+32
View File
@@ -0,0 +1,32 @@
export interface FaqItem {
question: string;
answer: string;
}
export const faqItems: FaqItem[] = [
{
question: "Is Gridline really free for commercial use?",
answer:
"Yes. Gridline is released under the Apache 2.0 license. You can use it for personal, commercial, or internal company work without paying or signing up.",
},
{
question: "Which databases are supported?",
answer:
"PostgreSQL and SQLite are fully supported today, including browsing, querying, editing, backups, and visual tools. MySQL and Redis connections are in active development.",
},
{
question: "How is this different from TablePlus, Beekeeper Studio, or DB Pro?",
answer:
"No paywalled features, no tab or connection limits, and DB-to-DB sync without intermediate files. We also expose deeper PostgreSQL objects like sequences, enums, extensions, roles, and grants.",
},
{
question: "Do you store my database credentials?",
answer:
"Passwords are stored in your operating system keychain — macOS Keychain, Linux Secret Service, or Windows Credential Manager — never in Gridline's local SQLite database.",
},
{
question: "Can I self-host or contribute?",
answer:
"Gridline is a desktop app, not a SaaS, so there's nothing to self-host. The repo is open for contributions: code, docs, translations, and issue reports are all welcome.",
},
];
+250
View File
@@ -0,0 +1,250 @@
---
import "../styles/global.css";
import Seo from "../components/Seo.astro";
import type { JsonLd } from "../lib/seo";
export interface Props {
title: string;
description?: string;
ogImage?: string;
noIndex?: boolean;
jsonLd?: JsonLd[];
}
const {
title,
description = "Gridline — the open-source database GUI for PostgreSQL, MySQL, SQLite, and Redis. No paywalls, no tab limits, fully open source.",
ogImage = "/og.png",
noIndex = false,
jsonLd = [],
} = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const ogImageURL = new URL(ogImage, Astro.site);
const isProd = import.meta.env.PROD;
---
<!doctype html>
<html lang="en" data-theme-auto>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
<link rel="sitemap" href="/sitemap-index.xml" />
<link rel="icon" type="image/svg+xml" href="/gridline-icon.svg" />
<link rel="apple-touch-icon" href="/gridline-icon.svg" />
<link rel="manifest" href="/site.webmanifest" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:url" content={canonicalURL} />
<meta property="og:image" content={ogImageURL} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Gridline — the open-source database GUI" />
<meta property="og:site_name" content="Gridline" />
<meta property="og:locale" content="en_US" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImageURL} />
<meta name="twitter:site" content="@AdrianBonpin" />
<meta name="twitter:creator" content="@AdrianBonpin" />
<meta name="theme-color" content="#0a0a0a" />
<meta name="color-scheme" content="dark light" />
{noIndex && <meta name="robots" content="noindex, nofollow" />}
<Seo jsonLd={jsonLd} />
{
isProd && (
<script
src="https://stat.ranio.xyz/api/script.js"
data-site-id="846d0ea9e3d0"
defer
/>
)
}
</head>
<body>
<nav id="nav" class="fixed top-0 inset-x-0 z-50 transition-all duration-300 border-b border-transparent">
<div class="max-w-[1200px] mx-auto px-6 py-4 flex items-center justify-between">
<a href="/" class="flex items-center gap-2.5 group">
<img src="/gridline-icon.svg" alt="Gridline" class="w-7 h-7" />
<span class="text-lg font-semibold tracking-tight">Gridline</span>
</a>
<div class="hidden md:flex items-center gap-6 text-sm">
<a href="/#features" class="nav-link">Features</a>
<a href="https://github.com/AdrianBonpin/gridline" target="_blank" rel="noopener" class="nav-link">GitHub</a>
<a href="/#download" data-download data-download-label class="nav-link">Download</a>
<button id="theme-toggle" class="p-2 rounded-full hover:bg-[var(--surface-elevated)] transition-colors" aria-label="Toggle theme">
<span class="theme-icon-sun hidden">☀</span>
<span class="theme-icon-moon">☾</span>
</button>
</div>
</div>
</nav>
<slot />
<footer class="border-t border-[var(--border)] mt-20 md:mt-28">
<div class="max-w-[1200px] mx-auto px-6 py-16 md:py-20">
<div class="grid md:grid-cols-4 gap-10 md:gap-8">
<div class="md:col-span-1">
<a href="/" class="flex items-center gap-2.5 mb-4">
<img src="/gridline-icon.svg" alt="Gridline" class="w-6 h-6" />
<span class="text-lg font-semibold">Gridline</span>
</a>
<p class="text-sm text-[var(--text-muted)] leading-relaxed">
The open-source database GUI that doesn't hold features hostage.
</p>
</div>
<div>
<h4 class="font-semibold text-sm mb-4">Product</h4>
<ul class="space-y-2 text-sm text-[var(--text-muted)]">
<li><a href="/#download" class="footer-link">Download</a></li>
<li><a href="/#features" class="footer-link">Features</a></li>
<li><a href="/changelog" class="footer-link">Changelog</a></li>
</ul>
</div>
<div>
<h4 class="font-semibold text-sm mb-4">Project</h4>
<ul class="space-y-2 text-sm text-[var(--text-muted)]">
<li><a href="https://github.com/AdrianBonpin/gridline" target="_blank" rel="noopener" class="footer-link">GitHub</a></li>
<li><a href="https://github.com/AdrianBonpin/gridline/issues" target="_blank" rel="noopener" class="footer-link">Issues</a></li>
<li><a href="https://github.com/AdrianBonpin/gridline/blob/prod/ROADMAP.md" target="_blank" rel="noopener" class="footer-link">Roadmap</a></li>
</ul>
</div>
<div>
<h4 class="font-semibold text-sm mb-4">Connect</h4>
<ul class="space-y-2 text-sm text-[var(--text-muted)]">
<li><a href="https://x.com/AdrianBonpin" target="_blank" rel="noopener" class="footer-link">X / Twitter</a></li>
<li><a href="mailto:adrianbonpin@gmail.com" class="footer-link">Email</a></li>
</ul>
</div>
</div>
<div class="mt-12 pt-8 border-t border-[var(--border)] flex flex-col md:flex-row justify-between gap-3 text-xs text-[var(--text-muted)] font-mono">
<span>© 2026 Gridline contributors · Apache 2.0</span>
<span>Made with care for databases.</span>
</div>
</div>
</footer>
</body>
</html>
<style is:global>
.nav-link {
position: relative;
color: var(--text-muted);
transition: color 0.2s;
}
.nav-link::after {
content: "";
position: absolute;
left: 0;
bottom: -2px;
width: 0;
height: 1px;
background: var(--primary);
transition: width 0.3s ease;
}
.nav-link:hover {
color: var(--text);
}
.nav-link:hover::after {
width: 100%;
}
.primary-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: 9999px;
background: var(--primary);
color: white;
font-weight: 500;
transition: transform 0.2s, box-shadow 0.2s, background 0.2s;
}
.primary-btn:hover {
transform: scale(1.02);
background: var(--primary-hover);
box-shadow: 0 10px 30px -10px rgba(59, 130, 246, 0.4);
}
.ghost-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: 9999px;
border: 1px solid var(--border);
color: var(--text);
font-weight: 500;
background: transparent;
transition: background 0.2s, border-color 0.2s;
}
.ghost-btn:hover {
background: var(--surface);
border-color: var(--surface-elevated);
}
.glass-card {
padding: 1.5rem;
border-radius: 1rem;
border: 1px solid var(--border);
background: var(--surface);
transition: background 0.2s, transform 0.2s;
}
.glass-card:hover {
background: var(--surface-elevated);
transform: translateY(-2px);
}
.feature-card {
padding: 1.25rem;
border-radius: 1rem;
border: 1px solid var(--border);
background: var(--surface);
}
.faq-item {
border: 1px solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
overflow: hidden;
}
.faq-summary {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
cursor: pointer;
font-weight: 500;
list-style: none;
}
.faq-summary::-webkit-details-marker {
display: none;
}
.faq-chevron {
transition: transform 0.3s;
font-size: 1.25rem;
}
.faq-item[open] .faq-chevron {
transform: rotate(180deg);
}
.faq-body {
padding: 0 1.25rem 1.25rem;
}
.footer-link {
transition: color 0.2s, transform 0.2s;
display: inline-block;
}
.footer-link:hover {
color: var(--text);
transform: translateX(2px);
}
</style>
+73
View File
@@ -0,0 +1,73 @@
import { describe, it, expect } from "vitest";
import {
buildSoftwareApplicationJsonLd,
buildFaqJsonLd,
buildOrganizationJsonLd,
buildWebSiteJsonLd,
buildBreadcrumbJsonLd,
buildTitle,
} from "./seo";
describe("seo builders", () => {
it("buildSoftwareApplicationJsonLd returns valid schema", () => {
const ld = buildSoftwareApplicationJsonLd({
name: "Gridline",
url: "https://getgridline.app/",
description: "Open-source database GUI",
image: "https://getgridline.app/og.png",
operatingSystem: "macOS, Windows, Linux",
applicationCategory: "DeveloperApplication",
offersPrice: 0,
offersCurrency: "USD",
downloadUrl: "https://github.com/AdrianBonpin/gridline/releases",
});
expect(ld["@type"]).toBe("SoftwareApplication");
expect(ld.name).toBe("Gridline");
expect((ld.offers as { price: number }).price).toBe(0);
});
it("buildFaqJsonLd maps questions to mainEntity", () => {
const ld = buildFaqJsonLd([
{ question: "Q1", answer: "A1" },
{ question: "Q2", answer: "A2" },
]);
expect(ld["@type"]).toBe("FAQPage");
const main = ld.mainEntity as { name: string }[];
expect(main).toHaveLength(2);
expect(main[0].name).toBe("Q1");
});
it("buildOrganizationJsonLd includes sameAs", () => {
const ld = buildOrganizationJsonLd({
name: "Gridline",
url: "https://getgridline.app/",
logo: "https://getgridline.app/gridline-icon.svg",
sameAs: ["https://github.com/AdrianBonpin/gridline"],
});
expect(ld["@type"]).toBe("Organization");
expect(ld.sameAs).toContain("https://github.com/AdrianBonpin/gridline");
});
it("buildWebSiteJsonLd includes SearchAction", () => {
const ld = buildWebSiteJsonLd({
name: "Gridline",
url: "https://getgridline.app/",
searchUrl: "https://getgridline.app/?q={search_term_string}",
});
expect(ld["@type"]).toBe("WebSite");
expect(ld.potentialAction).toBeDefined();
});
it("buildBreadcrumbJsonLd builds itemListElement", () => {
const ld = buildBreadcrumbJsonLd([
{ name: "Home", url: "https://getgridline.app/" },
{ name: "Changelog", url: "https://getgridline.app/changelog" },
]);
expect(ld["@type"]).toBe("BreadcrumbList");
expect((ld.itemListElement as unknown[]).length).toBe(2);
});
it("buildTitle appends the brand", () => {
expect(buildTitle("PostgreSQL GUI")).toBe("PostgreSQL GUI | Gridline");
});
});
+110
View File
@@ -0,0 +1,110 @@
export interface JsonLd {
"@context": "https://schema.org";
"@type": string;
[key: string]: unknown;
}
export function buildSoftwareApplicationJsonLd(opts: {
name: string;
url: string;
description: string;
image: string;
operatingSystem: string;
applicationCategory: string;
offersPrice: number;
offersCurrency: string;
downloadUrl: string;
}): JsonLd {
return {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
name: opts.name,
url: opts.url,
description: opts.description,
image: opts.image,
operatingSystem: opts.operatingSystem,
applicationCategory: opts.applicationCategory,
offers: {
"@type": "Offer",
price: opts.offersPrice,
priceCurrency: opts.offersCurrency,
},
downloadUrl: opts.downloadUrl,
};
}
export function buildFaqJsonLd(
questions: { question: string; answer: string }[],
): JsonLd {
return {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: questions.map((q) => ({
"@type": "Question",
name: q.question,
acceptedAnswer: {
"@type": "Answer",
text: q.answer,
},
})),
};
}
export function buildOrganizationJsonLd(opts: {
name: string;
url: string;
logo: string;
sameAs: string[];
}): JsonLd {
return {
"@context": "https://schema.org",
"@type": "Organization",
name: opts.name,
url: opts.url,
logo: opts.logo,
sameAs: opts.sameAs,
};
}
export function buildWebSiteJsonLd(opts: {
name: string;
url: string;
searchUrl?: string;
}): JsonLd {
const ld: JsonLd = {
"@context": "https://schema.org",
"@type": "WebSite",
name: opts.name,
url: opts.url,
};
if (opts.searchUrl) {
ld.potentialAction = {
"@type": "SearchAction",
target: {
"@type": "EntryPoint",
urlTemplate: opts.searchUrl,
},
"query-input": "required name=search_term_string",
};
}
return ld;
}
export function buildBreadcrumbJsonLd(
items: { name: string; url: string }[],
): JsonLd {
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: items.map((item, i) => ({
"@type": "ListItem",
position: i + 1,
name: item.name,
item: item.url,
})),
};
}
export function buildTitle(primary: string, brand = "Gridline"): string {
return `${primary} | ${brand}`;
}
+20
View File
@@ -0,0 +1,20 @@
---
import Layout from "../layouts/Layout.astro";
const title = "Page not found — Gridline";
const description = "That page doesnt exist. Head back to Gridline, the open-source database GUI.";
---
<Layout title={title} description={description} noIndex>
<main class="min-h-screen flex flex-col items-center justify-center px-6 text-center">
<p class="font-mono text-xs tracking-[0.15em] uppercase text-[var(--accent)] mb-4">404</p>
<h1 class="font-serif text-5xl md:text-7xl tracking-tight mb-4 lowercase">Lost in the schema.</h1>
<p class="text-[var(--text-muted)] max-w-md mb-8">
We couldnt find that page. It may have been renamed, removed, or never existed.
</p>
<div class="flex flex-wrap items-center justify-center gap-3">
<a href="/" class="primary-btn">Back home</a>
<a href="/changelog" class="ghost-btn">Changelog</a>
</div>
</main>
</Layout>
+47
View File
@@ -0,0 +1,47 @@
---
import Layout from "../layouts/Layout.astro";
import { changelogEntries } from "../data/changelog";
import { buildBreadcrumbJsonLd, buildTitle } from "../lib/seo";
const site = "https://getgridline.app";
const jsonLd = [
buildBreadcrumbJsonLd([
{ name: "Home", url: `${site}/` },
{ name: "Changelog", url: `${site}/changelog` },
]),
];
---
<Layout
title={buildTitle("Changelog")}
description="Gridline release notes. Free, open-source database GUI for PostgreSQL, MySQL, SQLite, and Redis."
jsonLd={jsonLd}
>
<main class="pt-32 pb-20 md:pt-44 md:pb-28">
<div class="max-w-[800px] mx-auto px-6">
<p class="font-mono text-xs tracking-[0.15em] uppercase text-[var(--accent)] mb-4">Changelog</p>
<h1 class="font-serif text-4xl md:text-6xl leading-[1.05] tracking-tight mb-10">Release notes</h1>
<div class="space-y-6">
{
changelogEntries.map((entry) => (
<article class="rounded-xl border border-[var(--border)] bg-[var(--surface)] p-6">
<h2 class="text-xl font-semibold mb-1">v{entry.version}</h2>
<p class="text-xs text-[var(--text-muted)] mb-3">{entry.date}</p>
<ul class="space-y-2 text-sm text-[var(--text-muted)]">
{entry.highlights.map((h) => <li>• {h}</li>)}
</ul>
</article>
))
}
</div>
<p class="mt-8 text-sm text-[var(--text-muted)]">
Full release history on{" "}
<a href="https://github.com/AdrianBonpin/gridline/releases" target="_blank" rel="noopener" class="text-[var(--primary)] hover:text-[var(--primary-hover)]">
GitHub Releases
</a>.
</p>
</div>
</main>
</Layout>
+197 -14
View File
@@ -1,17 +1,200 @@
---
import "../styles/global.css";
import Layout from "../layouts/Layout.astro";
import { faqItems } from "../data/faq";
import {
buildSoftwareApplicationJsonLd,
buildFaqJsonLd,
buildOrganizationJsonLd,
buildWebSiteJsonLd,
} from "../lib/seo";
import { Image } from "astro:assets";
import queryEditor from "../assets/screenshots/query-editor.png";
import erDiagram from "../assets/screenshots/er-diagram.png";
import home from "../assets/screenshots/home.png";
import dataGrid from "../assets/screenshots/data-grid.png";
const version = "0.7.10";
const site = "https://getgridline.app";
const jsonLd = [
buildSoftwareApplicationJsonLd({
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`,
operatingSystem: "macOS, Windows, Linux",
applicationCategory: "DeveloperApplication",
offersPrice: 0,
offersCurrency: "USD",
downloadUrl: "https://github.com/AdrianBonpin/gridline/releases",
}),
buildFaqJsonLd(faqItems),
buildOrganizationJsonLd({
name: "Gridline",
url: `${site}/`,
logo: `${site}/gridline-icon.svg`,
sameAs: [
"https://github.com/AdrianBonpin/gridline",
"https://x.com/AdrianBonpin",
],
}),
buildWebSiteJsonLd({
name: "Gridline",
url: `${site}/`,
}),
];
---
<Layout title="Gridline — Open-Source Database GUI" jsonLd={jsonLd}>
<script src="/landing.js" defer></script>
<!-- Hero -->
<header id="hero" class="relative pt-32 pb-20 md:pt-44 md:pb-28 overflow-hidden">
<div class="max-w-[1200px] mx-auto px-6 text-center">
<div class="hero-animate">
<p class="font-mono text-xs tracking-[0.15em] uppercase text-[var(--accent)] mb-5">Open-Source Database GUI</p>
<h1 class="font-serif text-5xl md:text-7xl lg:text-8xl leading-[1.05] tracking-tight max-w-4xl mx-auto lowercase">
the open-source database <span class="text-[var(--accent)]">gui</span>
</h1>
<p class="mt-6 text-lg md:text-xl text-[var(--text-muted)] max-w-xl mx-auto">
Query, explore, sync, and edit PostgreSQL, MySQL, SQLite, and Redis — with no paywalls, no tab limits, no account required.
</p>
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
<a id="download" href="https://github.com/AdrianBonpin/gridline/releases" class="primary-btn" data-download>
Download
</a>
<a href="https://github.com/AdrianBonpin/gridline" target="_blank" rel="noopener" class="ghost-btn">
View on GitHub
</a>
</div>
<div id="macos-hint" class="hidden max-w-lg mx-auto w-full rounded-xl border border-[var(--border)] bg-[var(--surface)] p-3 text-left mt-4">
<p class="text-xs text-[var(--text-muted)] mb-2">First launch on macOS? Run this in Terminal:</p>
<div class="flex items-center justify-between gap-3 rounded-lg bg-[var(--bg)] border border-[var(--border)] px-3 py-2">
<code class="font-mono text-xs truncate">sudo xattr -dr com.apple.quarantine /Applications/Gridline.app</code>
<button id="copy-quarantine" class="text-xs font-medium text-[var(--primary)] hover:text-[var(--primary-hover)] transition-colors shrink-0" aria-label="Copy command">Copy</button>
</div>
</div>
</div>
<div class="hero-screenshot mt-14 md:mt-20 mx-auto max-w-5xl">
<Image
src={queryEditor}
alt="Gridline query editor and data grid"
widths={[640, 1024, 1600]}
sizes="(max-width: 1024px) 100vw, 1024px"
formats={["webp", "avif"]}
loading="eager"
fetchpriority="high"
class="w-full h-auto"
/>
</div>
<p class="mt-8 text-xs md:text-sm text-[var(--text-muted)] font-mono">
Free for personal and commercial use · Apache 2.0 · v{version}
</p>
</div>
</header>
<!-- Why Gridline -->
<section id="why" class="section-animate py-20 md:py-28">
<div class="max-w-[1200px] mx-auto px-6">
<h2 class="font-serif text-3xl md:text-5xl text-center max-w-2xl mx-auto">
Everything the paid apps lock behind a subscription.
</h2>
<div class="mt-12 grid md:grid-cols-3 gap-5">
<article class="glass-card">
<p class="font-mono text-xs text-[var(--accent)] mb-3">01</p>
<h3 class="text-xl font-semibold mb-2">No limits</h3>
<p class="text-[var(--text-muted)] text-sm leading-relaxed">
Unlimited connections, open tabs, saved queries, and exports. The free plan is the full plan.
</p>
</article>
<article class="glass-card">
<p class="font-mono text-xs text-[var(--accent)] mb-3">02</p>
<h3 class="text-xl font-semibold mb-2">DB-to-DB sync</h3>
<p class="text-[var(--text-muted)] text-sm leading-relaxed">
Pipe <code class="font-mono text-xs">pg_dump</code> directly into <code class="font-mono text-xs">pg_restore</code> between two live connections. No file round-trip needed.
</p>
</article>
<article class="glass-card">
<p class="font-mono text-xs text-[var(--accent)] mb-3">03</p>
<h3 class="text-xl font-semibold mb-2">Deeper PG explorer</h3>
<p class="text-[var(--text-muted)] text-sm leading-relaxed">
Full detail views for functions, triggers, sequences, enums, extensions, roles, and grants.
</p>
</article>
</div>
</div>
</section>
<!-- Feature highlights -->
<section id="features" class="section-animate py-20 md:py-28">
<div class="max-w-[1200px] mx-auto px-6">
<p class="font-mono text-xs text-[var(--accent)] uppercase tracking-[0.15em] text-center mb-3">Features</p>
<h2 class="font-serif text-3xl md:text-5xl text-center mb-14">Explore, query, and move data.</h2>
<div class="grid md:grid-cols-2 gap-5">
<article class="feature-card">
<p class="font-mono text-xs text-[var(--text-muted)] mb-3">QUERY & EDIT</p>
<h3 class="text-xl font-semibold mb-2">SQL editor + virtualized grid</h3>
<p class="text-[var(--text-muted)] text-sm mb-4">Run queries with autocomplete, inline edit cells, and browse 100k+ rows without breaking a sweat.</p>
<Image src={queryEditor} alt="SQL query editor" widths={[640, 1024]} sizes="(max-width: 1024px) 100vw, 640px" formats={["webp", "avif"]} loading="lazy" class="w-full h-auto" />
</article>
<article class="feature-card">
<p class="font-mono text-xs text-[var(--text-muted)] mb-3">VISUALIZE</p>
<h3 class="text-xl font-semibold mb-2">ER diagram</h3>
<p class="text-[var(--text-muted)] text-sm mb-4">Auto-layout schema graphs with crows foot relationships across schemas, right inside the app.</p>
<Image src={erDiagram} alt="ER diagram" widths={[640, 1024]} sizes="(max-width: 1024px) 100vw, 640px" formats={["webp", "avif"]} loading="lazy" class="w-full h-auto" />
</article>
<article class="feature-card">
<p class="font-mono text-xs text-[var(--text-muted)] mb-3">MOVE DATA</p>
<h3 class="text-xl font-semibold mb-2">Backup, restore, and sync</h3>
<p class="text-[var(--text-muted)] text-sm mb-4">Point-and-click <code class="font-mono text-xs">pg_dump</code>/<code class="font-mono text-xs">pg_restore</code>, SQLite dumps, and live DB-to-DB sync.</p>
<Image src={home} alt="Gridline home screen" widths={[640, 1024]} sizes="(max-width: 1024px) 100vw, 640px" formats={["webp", "avif"]} loading="lazy" class="w-full h-auto" />
</article>
<article class="feature-card">
<p class="font-mono text-xs text-[var(--text-muted)] mb-3">MANAGE</p>
<h3 class="text-xl font-semibold mb-2">Objects, roles, and grants</h3>
<p class="text-[var(--text-muted)] text-sm mb-4">Create and edit tables, foreign keys, indexes, functions, triggers, sequences, roles, and privileges.</p>
<Image src={dataGrid} alt="Data grid with foreign key preview" widths={[640, 1024]} sizes="(max-width: 1024px) 100vw, 640px" formats={["webp", "avif"]} loading="lazy" class="w-full h-auto" />
</article>
</div>
</div>
</section>
<!-- Open source -->
<section id="oss" class="section-animate py-20 md:py-28 text-center">
<div class="max-w-[800px] mx-auto px-6">
<p class="font-mono text-xs text-[var(--accent)] uppercase tracking-[0.15em] mb-3">Open Source</p>
<h2 class="font-serif text-3xl md:text-5xl mb-5">Built in public.<br />No VC roadmap, no rug pulls.</h2>
<p class="text-[var(--text-muted)] mb-8 max-w-md mx-auto">
Gridline is Apache 2.0 and developed openly. Contributions, issues, and feature requests are welcome.
</p>
<a href="https://github.com/AdrianBonpin/gridline" target="_blank" rel="noopener" class="ghost-btn">
Star on GitHub
</a>
</div>
</section>
<!-- FAQ -->
<section id="faq" class="section-animate py-20 md:py-28">
<div class="max-w-[800px] mx-auto px-6">
<h2 class="font-serif text-3xl md:text-5xl text-center mb-12">Questions? Answered.</h2>
<div class="faq-list space-y-3">
{
faqItems.map((item, i) => (
<details class="faq-item" open={i === 0}>
<summary class="faq-summary">
<span>{item.question}</span>
<span class="faq-chevron">⌄</span>
</summary>
<div class="faq-body">
<p class="text-[var(--text-muted)] text-sm leading-relaxed">{item.answer}</p>
</div>
</details>
))
}
</div>
</div>
</section>
</Layout>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body>
<h1>Astro</h1>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
import type { APIRoute } from "astro";
const getRobotsTxt = (sitemapURL: URL) =>
`User-agent: *\nAllow: /\n\nSitemap: ${sitemapURL.href}\n`;
export const GET: APIRoute = ({ site }) => {
const sitemapURL = new URL("sitemap-index.xml", site);
return new Response(getRobotsTxt(sitemapURL), {
headers: { "Content-Type": "text/plain" },
});
};
+49
View File
@@ -1 +1,50 @@
@import "tailwindcss";
@import "@fontsource-variable/literata";
@import "@fontsource-variable/outfit";
@import "@fontsource/space-mono/400.css";
@import "@fontsource/space-mono/700.css";
:root {
--font-serif: "Literata Variable", Georgia, serif;
--font-sans: "Outfit Variable", system-ui, sans-serif;
--font-mono: "Space Mono", ui-monospace, monospace;
--bg: #0a0a0a;
--surface: rgba(255, 255, 255, 0.04);
--surface-elevated: rgba(255, 255, 255, 0.07);
--border: rgba(255, 255, 255, 0.10);
--text: rgba(255, 255, 255, 0.90);
--text-muted: rgba(255, 255, 255, 0.55);
--primary: #3b82f6;
--primary-hover: #60a5fa;
--accent: #f59e0b;
}
html.light {
--bg: #fafafa;
--surface: rgba(0, 0, 0, 0.03);
--surface-elevated: rgba(0, 0, 0, 0.06);
--border: rgba(0, 0, 0, 0.10);
--text: #171717;
--text-muted: #737373;
--primary: #2563eb;
--primary-hover: #1d4ed8;
--accent: #d97706;
}
html {
background-color: var(--bg);
color: var(--text);
font-family: var(--font-sans);
scroll-behavior: smooth;
}
body {
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
::selection {
background-color: rgba(59, 130, 246, 0.3);
}