Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
12 changes: 12 additions & 0 deletions app/Console/Commands/GenerateSitemapCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions app/Http/Controllers/Landing/LandingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
19 changes: 12 additions & 7 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down
46 changes: 46 additions & 0 deletions public/images/app-store-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading