diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index c68ba94..d4d147f 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -156,10 +156,21 @@ jobs:
cat /tmp/ssr.log >&2 || true
exit 1
+ # The whole suite, not one file.
+ #
+ # This ran only HomepageRenderTest.php, which left 43 of 222 tests
+ # skipping on every push: the `php` job above builds no bundles, so every
+ # `ssrHtml()` assertion short-circuits there, and this job — the one place
+ # they could execute — was scoped to a single file. All of
+ # LandingStructureTest, ProductHuntBadgeTest and TopBannerTest, plus the
+ # rendered halves of LandingSeoTest, FundingModelTest and
+ # LicenseFeaturesTest, were green-by-skip for their whole lives.
+ #
+ # REQUIRE_SSR turns a skip into a failure, so this cannot go quiet again.
- name: SSR render tests
env:
REQUIRE_SSR: 1
- run: ./vendor/bin/pest tests/Feature/Landing/HomepageRenderTest.php
+ run: ./vendor/bin/pest
- name: SSR service log
if: always()
diff --git a/CLAUDE.md b/CLAUDE.md
index 80027e3..4ee0410 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -17,7 +17,7 @@ web middleware group in `bootstrap/app.php`. Consequences that bite:
- `session()`, `->with('flash')` and `redirect()->back()->with(...)` do not work.
- Inertia's `useForm().post()` must not be used. Writes go through plain
`fetch` and hold their result in React state — copy the `useEmailForm`
- pattern in `resources/js/components/landing/footer-cta.tsx`.
+ pattern in `resources/js/hooks/use-email-form.ts`.
See `docs/architecture.md` before touching anything that posts data.
diff --git a/app/Console/Commands/GenerateSitemapCommand.php b/app/Console/Commands/GenerateSitemapCommand.php
index 1a547ff..9ed99cc 100644
--- a/app/Console/Commands/GenerateSitemapCommand.php
+++ b/app/Console/Commands/GenerateSitemapCommand.php
@@ -55,6 +55,18 @@ public function handle(): int
->setPriority(0.9)
->setLastModificationDate($this->sourceModifiedAt('resources/js/pages/Download.tsx')),
)
+ /*
+ * Weekly at 0.9, matching /download rather than the 0.8 monthly the
+ * database pages get. It is the other half of the same question —
+ * how do I get this — and it changes on the app's release cadence,
+ * not on a content edit here.
+ */
+ ->add(
+ Url::create($baseUrl . '/ios')
+ ->setChangeFrequency(Url::CHANGE_FREQUENCY_WEEKLY)
+ ->setPriority(0.9)
+ ->setLastModificationDate($this->sourceModifiedAt('resources/js/pages/Ios.tsx')),
+ )
->add(
Url::create($baseUrl . '/privacy')
->setChangeFrequency(Url::CHANGE_FREQUENCY_MONTHLY)
diff --git a/app/Http/Controllers/Landing/LandingController.php b/app/Http/Controllers/Landing/LandingController.php
index ae6c5ce..e6d5e86 100644
--- a/app/Http/Controllers/Landing/LandingController.php
+++ b/app/Http/Controllers/Landing/LandingController.php
@@ -39,6 +39,23 @@ public function download(): Response
]);
}
+ /**
+ * The iPhone and iPad app.
+ *
+ * Takes no App Store data of its own. Everything the page states about the
+ * iOS app is a literal in `resources/js/pages/Ios.tsx`, because there is no
+ * public App Store API this site could read without a credential, and this
+ * repository has none. `downloadUrls` is here only because `Header`
+ * requires it.
+ */
+ public function ios(): Response
+ {
+ return Inertia::render('Ios', [
+ 'downloadUrls' => $this->fetchDownloadUrls(),
+ 'githubStars' => $this->fetchGitHubStars(),
+ ]);
+ }
+
public function privacy(): Response
{
return Inertia::render('Privacy');
diff --git a/docs/architecture.md b/docs/architecture.md
index deb8284..d92b91d 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -8,7 +8,7 @@ database and no credentials: every page is built from markdown in
```
/ homepage
-/download /faq product pages
+/download /ios /faq product pages
/privacy /terms /refund-policy
/blog /blog/{slug} markdown in resources/blog
/compare/{slug} data in resources/data/comparisons.json
@@ -21,10 +21,9 @@ enough — Inertia is configured without an explicit `resolve`.
## What it does not serve
-Buying a licence, signing in to an account, subscribing to the newsletter and
-joining the beta are handled by the TablePro backend, which is a separate
-application and not part of this repository. Requests to those paths never
-reach this code.
+Buying a licence, signing in to an account and subscribing to the newsletter are
+handled by the TablePro backend, which is a separate application and not part of
+this repository. Requests to those paths never reach this code.
That is why this app runs **without a session**: it has nothing to keep state
for. `StartSession`, `PreventRequestForgery`, `ShareErrorsFromSession` and
@@ -36,7 +35,8 @@ Two consequences worth knowing before you write code here:
- `csrf_token()` throws, and there is no CSRF meta tag. Nothing should add one.
- `session()`, `redirect()->back()->with(...)` and Inertia's `useForm().post()`
do not work. Forms use plain `fetch` and keep their result in React state —
- see `useEmailForm` in `resources/js/components/landing/footer-cta.tsx`.
+ see `useEmailForm` in `resources/js/hooks/use-email-form.ts`, whose
+ remaining caller is the footer newsletter.
## The endpoints the pages call
@@ -47,12 +47,17 @@ All anonymous, all rate limited, none needs a token.
| `POST /checkout` | `{tier, cycle, seats?, discount_code?, attribution?}` | `{url}` — passed to the checkout SDK |
| `POST /discount/preview` | `{code}` | `{valid, amount_type?, amount?}` |
| `POST /newsletter/subscribe` | `{email}` | `{type, message}` |
-| `POST /beta/signup` | `{email}` | `{type, message}` |
| `GET /api/newsletter/stats` | — | `{count}` |
Checkout takes a tier and billing cycle rather than a product identifier, which
is why no payment-provider identifier appears anywhere in this repository.
+`POST /beta/signup` was the fifth row here until 2026-09-22. The endpoint still
+exists on the backend and nginx still proxies `/beta`, but nothing on this site
+calls it: the iPhone app shipped on the App Store, so the TestFlight invite form
+in the closing call to action became a link. Do not add a caller back without
+checking the endpoint is still wired.
+
## Purchase attribution
Neither half of this system can answer "where did this customer come from" on
diff --git a/public/images/app-store-dark.svg b/public/images/app-store-dark.svg
new file mode 100644
index 0000000..16c0496
--- /dev/null
+++ b/public/images/app-store-dark.svg
@@ -0,0 +1,46 @@
+
diff --git a/public/images/app-store-light.svg b/public/images/app-store-light.svg
new file mode 100644
index 0000000..072b425
--- /dev/null
+++ b/public/images/app-store-light.svg
@@ -0,0 +1,46 @@
+
diff --git a/public/images/blog/tablepro-for-iphone-table.png b/public/images/blog/tablepro-for-iphone-table.png
new file mode 100644
index 0000000..b18dc4c
Binary files /dev/null and b/public/images/blog/tablepro-for-iphone-table.png differ
diff --git a/public/images/ios/row-dark.png b/public/images/ios/row-dark.png
new file mode 100644
index 0000000..1d36fca
Binary files /dev/null and b/public/images/ios/row-dark.png differ
diff --git a/public/images/ios/row-light.png b/public/images/ios/row-light.png
new file mode 100644
index 0000000..0fa4d0a
Binary files /dev/null and b/public/images/ios/row-light.png differ
diff --git a/public/images/ios/structure-dark.png b/public/images/ios/structure-dark.png
new file mode 100644
index 0000000..0be5746
Binary files /dev/null and b/public/images/ios/structure-dark.png differ
diff --git a/public/images/ios/structure-light.png b/public/images/ios/structure-light.png
new file mode 100644
index 0000000..54de57c
Binary files /dev/null and b/public/images/ios/structure-light.png differ
diff --git a/public/images/ios/table-dark.png b/public/images/ios/table-dark.png
new file mode 100644
index 0000000..1c012ac
Binary files /dev/null and b/public/images/ios/table-dark.png differ
diff --git a/public/images/ios/table-light.png b/public/images/ios/table-light.png
new file mode 100644
index 0000000..b18dc4c
Binary files /dev/null and b/public/images/ios/table-light.png differ
diff --git a/public/og/blog/tablepro-for-iphone.png b/public/og/blog/tablepro-for-iphone.png
new file mode 100644
index 0000000..92d016a
Binary files /dev/null and b/public/og/blog/tablepro-for-iphone.png differ
diff --git a/public/og/database/beancount-client.png b/public/og/database/beancount-client.png
new file mode 100644
index 0000000..6a02e3b
Binary files /dev/null and b/public/og/database/beancount-client.png differ
diff --git a/public/og/database/pglite-client.png b/public/og/database/pglite-client.png
new file mode 100644
index 0000000..f3dbace
Binary files /dev/null and b/public/og/database/pglite-client.png differ
diff --git a/public/og/database/surrealdb-client.png b/public/og/database/surrealdb-client.png
new file mode 100644
index 0000000..5a71e93
Binary files /dev/null and b/public/og/database/surrealdb-client.png differ
diff --git a/public/og/database/teradata-client.png b/public/og/database/teradata-client.png
new file mode 100644
index 0000000..7437f7f
Binary files /dev/null and b/public/og/database/teradata-client.png differ
diff --git a/public/og/database/trino-client.png b/public/og/database/trino-client.png
new file mode 100644
index 0000000..a46f31d
Binary files /dev/null and b/public/og/database/trino-client.png differ
diff --git a/public/site.webmanifest b/public/site.webmanifest
index 9ac339c..f6dad99 100644
--- a/public/site.webmanifest
+++ b/public/site.webmanifest
@@ -1,7 +1,7 @@
{
"name": "TablePro",
"short_name": "TablePro",
- "description": "Fast, native macOS database client for MySQL, PostgreSQL, SQLite, MongoDB, Redis, and more.",
+ "description": "Native database clients for Mac, iPhone and iPad. Free and open source.",
"start_url": "/",
"display": "browser",
"background_color": "#ffffff",
@@ -9,7 +9,7 @@
"icons": [
{
"src": "/logo.png",
- "sizes": "512x512",
+ "sizes": "256x256",
"type": "image/png"
}
]
diff --git a/resources/blog/tablepro-for-iphone.md b/resources/blog/tablepro-for-iphone.md
new file mode 100644
index 0000000..e0599a6
--- /dev/null
+++ b/resources/blog/tablepro-for-iphone.md
@@ -0,0 +1,70 @@
+---
+slug: tablepro-for-iphone
+title: "TablePro for iPhone and iPad"
+description: The iOS app is on the App Store. Ten engines with every driver built in, SSH tunnels, Face ID, and the same connections you already use on the Mac. Free, with no in-app purchases.
+date: 2026-09-22
+author: TablePro Team
+tags: [release, ios, ipados, icloud-sync]
+ogPunchline: Ten engines on the phone. Every driver built in.
+---
+
+TablePro for iPhone and iPad is on the App Store. It is free, there are no in-app purchases, and it needs iOS or iPadOS 18.
+
+It is not the Mac app on a smaller screen. It opens the same connections, and then it does the part of the job that makes sense on a phone: read a table, check a row, run a query you already know you need.
+
+
+ `, never inlined as JSX. Twenty-four of the white
+ * badge's twenty-five paths carry no `fill` of their own and rely on the SVG
+ * default, so an ancestor `fill-current` would silently recolour Apple's mark;
+ * and both files carry `id="livetype"`, so inlining the pair would duplicate a
+ * DOM id on any page showing both.
+ *
+ * `h-10` is Apple's minimum rendered height of 40px. `w-auto` keeps the aspect
+ * ratio coming from the file rather than from the two attributes, which are
+ * rounded and exist only to reserve layout space.
+ */
+export default function AppStoreBadge({ location, className }: { location: string; className?: string }) {
+ return (
+ trackDownload(location, 'ios')}
+ className={`inline-block ${className ?? ''}`}
+ >
+
- {flash.message} -
- ); -} - /** * The iPhone frame is decorative chrome; the screenshot inside carries the alt * text. The positioning percentages belong to this exact SVG, so they move with @@ -54,7 +35,7 @@ function PhoneMockup() {