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
15 changes: 5 additions & 10 deletions .claude/skills/announce/references/blog-post.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Headings are plain statements or names. No questions, slogans or gerund stacks.
---
slug: tablepro-0-81 # equals the filename
title: "TablePro 0.81: Tabs per Database, Firebase and Bigtable" # quote a title with a colon
description: One or two sentences, at most 155 characters (SeoLengthsTest).
description: One or two sentences, at most 155 characters.
date: 2026-10-16 # the publish date, never changed later
release: "TablePro 0.81" # marks a release post; a guide has none
author: TablePro Team
Expand All @@ -61,7 +61,7 @@ ogPunchline: Two or three short sentences under about 90 characters. The OG card
- Every tag after `release` is a key of `PostTopics::PAGES` or an engine id in
`resources/data/engines.json`; `BlogTest` fails on any other.
- `ogPunchline` is what someone reads on a shared link, not the description restated. It counts no
engines (`NoTypedCountsTest`), and neither field makes a privacy claim (`BannedClaimsTest`).
engines, and neither field makes a privacy claim.

## 5. Figures

Expand Down Expand Up @@ -96,19 +96,14 @@ Dev to dev, plain and short: about 900 words at most, three or four figures.

## 7. Wire it up

1. `tests/Feature/Landing/BlogTest.php`:
- the slug and date at the top of `BLOG_PUBLISHED`;
- in the `/blog` test, the newest post's `dateFormatted`;
- in the `/vi/blog` test, `posts.0` (slug, url, title, Vietnamese date) and the `slice(N, $guides)`
offset, which counts the release posts newer than the newest guide.
2. Render the OG card and look at it: a long title wraps to three lines and can push the punchline
1. Render the OG card and look at it: a long title wraps to three lines and can push the punchline
off. `WEB_DOMAIN=tablepro.app php artisan og:generate --type=blog --slug=<slug>`.
3. Run:
2. Run:

```bash
python3 .claude/skills/announce/scripts/lint-draft.py --blog resources/blog/<slug>.md
../TablePro/scripts/check-banned-words.sh resources/blog/<slug>.md
php artisan test --compact --filter='BlogTest|SeoLengthsTest|OgCardsTest|AssetManifestTest'
php artisan test --compact --filter='BlogTest|OgCardsTest|AssetManifestTest'
php artisan test --compact
```

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/announce/scripts/lint-draft.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def check_blog(text, draft_path, add):
add(HARD, "frontmatter", "a title with a colon must be quoted")
description = unquote(fields.get("description", ""))
if len(description) > DESCRIPTION_MAX:
add(HARD, "description", f"{len(description)} chars, SeoLengthsTest allows {DESCRIPTION_MAX}")
add(HARD, "description", f"{len(description)} chars, the limit allows {DESCRIPTION_MAX}")
punchline = unquote(fields.get("ogPunchline", ""))
if len(punchline) > OG_PUNCHLINE_MAX:
add(SOFT, "ogPunchline", f"{len(punchline)} chars, keep it under about {OG_PUNCHLINE_MAX}")
Expand Down
6 changes: 1 addition & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,7 @@ jobs:
- name: Typecheck
run: npm run typecheck

# Node's own runner over `tests/js`, no test dependency to install. The
# attribution module decides which source a sale is credited to and is
# the only frontend logic here that cannot be checked by rendering a
# page, so it is covered by execution rather than by the source-reading
# assertions the Pest suite uses.
# Node's own runner over `tests/js`, no test dependency to install.
- name: Frontend tests
run: npm run test:js

Expand Down
10 changes: 3 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,11 @@ server (`docs/architecture.md`, "The language bar").
browser agree.
- Vietnamese text is NFC and follows the glossary
(`docs/rebuild/design/positioning.md` §11): the reader is "bạn", developer
terms stay in English. `tests/Support/vi-forbidden-variants.php` lists the
wording that is ruled out.
terms stay in English.
- In every language, the names of paid features, plans, modes, Safe Mode
levels and app commands stay in English ("Safe Mode", "Data Rewind", "Team").
`tests/Support/locale-forbidden-variants.php` lists the wording ruled out per
language.
- A label inside `<ui>` is what the app shows in that language:
`tests/Support/app-ui-labels.php` holds the app's wording where it is
translated, followed in the copy by the English label in parentheses. Every
- A label inside `<ui>` is what the app shows in that language. Where the app
translates it, the copy follows it with the English label in parentheses. Every
other language keeps the English label as written.
- French takes a no-break space before `; : ! ?` and inside `« »`. Japanese and
Chinese put a space between Latin and CJK text, list joiners included.
Expand Down
2 changes: 1 addition & 1 deletion app/Console/Commands/GenerateSitemapCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*
* - A translated page lists every translation as an `xhtml:link` alternate,
* itself included, plus `x-default` pointing at the English URL. These are
* exactly the head's alternates (`SitemapAlternatesTest`).
* exactly the head's alternates (`Seo/SitemapTest`).
* - An untranslated page (an English-only release post, `/blog`) has none.
* - A page that renders but is not indexed (`/vi/blog`) is absent, as are
* redirect and 410 sources (the registry never lists them), error pages and
Expand Down
3 changes: 1 addition & 2 deletions app/Http/Controllers/DownloadController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@
* Everything the page links to outside this site arrives here, as props: the
* DMG URL and size from the release service, the App Store and GitHub
* Releases URLs from `platforms.json`, and the docs, source and licence links
* from `facts.json`. No URL is typed into the page or its copy, so the
* external-URL guard in `Data/FactsDataTest` holds by construction.
* from `facts.json`. No URL is typed into the page or its copy.
*
* The DMG link is in the server-rendered HTML; nothing starts a download on
* its own.
Expand Down
4 changes: 2 additions & 2 deletions app/Support/Content/Slugs/DatabaseSlugs.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
* `resources/data/engines.json`, in that file's order. The four pages merged
* into family pages (`mariadb-client`, `cockroachdb-client`, `pglite-client`,
* `scylladb-client`) are not here: `resources/data/redirects.json` answers them
* with a 301 to their section before routing. `Data/EnginesDataTest` and
* `Databases/DatabasePagesTest` pin this list against `engines.json` and
* with a 301 to their section before routing. `Pages/DatabasesTest` and
* `Localization/LocaleRoutingTest` pin this list against `engines.json` and
* `resources/data/content/{en,vi}/databases/*.json`.
*
* Never add `ios`, a locale prefix such as `vi`, or another root path such as
Expand Down
3 changes: 1 addition & 2 deletions app/Support/Content/Slugs/FeatureSlugs.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
* A slug renders only in the locales where
* `resources/data/content/{locale}/features/{slug}.json` exists; the registry
* decides that, not the route. `Localization/LocaleRoutingTest` pins this
* list to the content files, and `Features/FeatureContentTest` checks each
* file against the schema in `resources/js/components/features/README.md`.
* list to the content files.
*/
final class FeatureSlugs
{
Expand Down
2 changes: 1 addition & 1 deletion app/Support/Features/FeatureFacts.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\File;

// What each {token} and engines list in feature copy means, so the copy stays true when the data changes; FeatureContentTest fails on a name it does not know.
// What each {token} and engines list in feature copy means, so the copy stays true when the data changes; Pages/FeaturesTest fails on a name it does not know.
/**
* @phpstan-type EngineItem array{name: string, href: string|null, detail: string|null, since: string|null}
* @phpstan-type Fact array{kind: 'number', value: int}|array{kind: 'text', value: string}|array{kind: 'names', items: list<string>}|array{kind: 'engines', items: list<EngineItem>}
Expand Down
3 changes: 1 addition & 2 deletions config/banner.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
| Config holds the switch and the link, and nothing else. The words live in
| the `banner` UI catalog, once per language
| (resources/js/i18n/messages/{locale}/banner.ts), so the banner reads in
| the page's language and its length limits are checked in each
| (TopBannerTest).
| the page's language.
|
| It states a fact and never pleads: no "we need your help", no countdown,
| no orange fill, only a tint of the brand hue (app.css). It cannot be
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ in production, shared with every page as the `apiOrigin` prop and read through
Every call is a plain `fetch` with `credentials: 'omit'`, so no cookie is sent
or kept. tablepro-cloud answers CORS for `https://tablepro.app` only, without
credentials. `ContentSecurityPolicy` adds the API origin to `connect-src`.
`PublicContractsTest` holds every call.
`Security/PrivacyTest` holds every call.

Errors are RFC 9457 problems whose `title` and `detail` are English, so the
cards show their own catalog's words for each `code`
Expand Down
4 changes: 2 additions & 2 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ the host's. On this origin:

nginx matches those paths at the root only, so `/vi/account` comes here and
answers 404 with a link to the portal. `PLATFORM_PATHS` keeps the site from
prefixing them, and `LocalizedUrlTest` holds it to the file. Ninety days after
prefixing them, and `LocaleRoutingTest` holds it to the file. Ninety days after
the cutover, remove the two proxied locations from the host and from the file;
the redirects stay.

Expand Down Expand Up @@ -373,7 +373,7 @@ www-data ALL=(root) NOPASSWD: /usr/bin/systemctl reload php8.5-fpm, /usr/bin/sup
```

sudo matches each command line literally — path, verb and unit — and
`scripts/deploy.sh` runs exactly the first two (`tests/Feature/DeployScriptTest.php`
`scripts/deploy.sh` runs exactly the first two (`tests/Feature/Repo/DeployScriptTest.php`
checks its calls against the rule above). The third restarts the platform app's
SSR program (see "The SSR process"). A PHP upgrade renames the FPM unit, and
this rule has to follow it; a deploy prints the unit it reloads.
Expand Down
5 changes: 1 addition & 4 deletions docs/shared-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Merge the tablepro-cloud side first only when its CI tolerates the difference
repository first.

A shared file imports nothing: each app reaches it through its own path, and
strings arrive as arguments. `SharedFilesTest` checks the imports.
strings arrive as arguments.

## Shared files

Expand All @@ -53,9 +53,6 @@ both change together. No hash pins them.
`web/packages/ui/src/styles/fonts.css`: the same faces, unicode ranges and
declaration order (latin-ext, vietnamese, latin); the `url()` paths follow
each app's bundler.
- `tests/Support/vi-forbidden-variants.php` and tablepro-cloud's
`web/packages/i18n/test/vi-forbidden-variants.ts`: the same rows, as PCRE
here and as JavaScript patterns there.

## Changing a header

Expand Down
2 changes: 1 addition & 1 deletion lang/vi/errors.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
|--------------------------------------------------------------------------
|
| Nội dung cho resources/views/errors/{500,503}.blade.php. Giữ cùng khóa và
| cùng tham số với lang/en/errors.php (LangParityTest kiểm tra điều này).
| cùng tham số với lang/en/errors.php (TranslationsTest kiểm tra điều này).
|
*/

Expand Down
10 changes: 5 additions & 5 deletions resources/js/components/compare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ No benchmark, no rating, no `Review` or `FAQPage` markup, no competitor screensh

## The page file

Keys and their order are fixed (`CompareContentTest`). Both languages have the **same keys in the same order, the same list lengths and the same `{tokens}`** (`ContentParityTest`).
Keys and their order are fixed. Both languages have the **same keys in the same order, the same list lengths and the same `{tokens}`** (`TranslationsTest`).

```jsonc
{
Expand Down Expand Up @@ -63,7 +63,7 @@ Keys and their order are fixed (`CompareContentTest`). Both languages have the *

### Tokens

Prose may use only these, filled from the product's data (anything else prints literally and fails `CompareContentTest`):
Prose may use only these, filled from the product's data (anything else prints literally and fails `CompareTest`):

| Token | Value |
|---|---|
Expand All @@ -79,7 +79,7 @@ Tags: `<link>…</link>` and `<ui>…</ui>` (an app label or menu path). Nothing

### Never in a compare file

- A price, a URL, a download size, a speed, memory or startup figure (`CompareContentTest` and positioning §12). Prices render from data in the table.
- A price, a URL, a download size, a speed, memory or startup figure (positioning §12). Prices render from data in the table.
- A count of TablePro's engines, MCP tools, AI providers, Safe Mode levels or paid features.
- "every/any/all database(s)", "every/all platforms", "cross-platform" about TablePro, "unlock", "faster than", "lighter", "free forever", "no feature gating", hype words. Vietnamese: "mọi cơ sở dữ liệu", "mọi nền tảng", "mở khóa", "nhanh hơn", "nhẹ hơn". "cross-platform", "macOS 14", "Mac App Store" and "Setapp" are allowed only about the other product.
- "Free" alone for TablePro, adversarial words ("too slow", "bloated"), guesses. A fact nobody verified is left out.
Expand All @@ -102,11 +102,11 @@ Every competitor fact on the page must be in the product's `comparisons.json` en
## Checks

```bash
php artisan test --compact --filter='ComparisonsDataTest|CompareContentTest|ComparePagesTest|ContentParityTest|LocaleRoutingTest'
php artisan test --compact --filter='ComparisonsDataTest|CompareTest|TranslationsTest|LocaleRoutingTest'
npm run typecheck && npm run test:js
```

`ComparisonsDataTest` → "gives every compared product a page in both languages" and `LocaleRoutingTest` → "pins each slug constant…" fail until every comparison exists in both languages. The server-rendered cases in `ComparePagesTest` run where SSR is available.
`ComparisonsDataTest` and `LocaleRoutingTest` fail until every comparison exists in every language.

## Per page (sitemap §A.4)

Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/compare/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ function cellSources(cell: Pick<ComparisonCell, 'source'>): string[] {
/**
* The source ids behind a list item's `cite` entries: `platforms`, `licence`,
* `status`, `mac`, `technology`, `prices` or `cells.{key}`. An entry that
* names nothing in the data is ignored here; `CompareContentTest` rejects it.
* names nothing in the data is ignored here; `CompareTest` rejects it.
*/
export function citedSources(product: ComparisonProduct, cite: readonly string[]): string[] {
const ids: string[] = [];
Expand Down
10 changes: 5 additions & 5 deletions resources/js/components/databases/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ already holds.
| `resources/data/content/{en,vi}/databases/index.json` | The hub's copy, plus `labels`: the shared words every engine page uses |
| `resources/data/content/{en,vi}/databases/{slug}.json` | One engine page's copy |

`tests/Feature/Databases/DatabasePagesTest.php` validates every page file against the schema below, and
`Localization/ContentParityTest` holds `vi` to the same keys, `{tokens}` and asset ids as `en`.
`tests/Feature/Pages/DatabasesTest.php` checks the tokens, tags and links of every page file, and
`Localization/TranslationsTest` holds every other language to the same keys, `{tokens}` and asset ids as `en`.

## What a page contributor writes

Expand Down Expand Up @@ -111,7 +111,7 @@ Every string in `header.lead`, `sections`, `family`, `otherTools`, `faq` and `no
statement).
- `<name>text</name>` where `name` is a key of the page's `links` (camelCase letters and digits). A value is a site
path (`/features/connections#network`), a same-page fragment (`#mariadb`) or a docs path (`docs:/databases/redis`).
Never a full URL: external URLs live only in data files (`Data/FactsDataTest`).
Never a full URL: external URLs live only in data files.
- These `{tokens}`, filled from data: `{name}` (the engine's name), `{dumpTool}` (only if the engine has a backup
tool), `{importFormats}` (only if it imports), `{exportFormats}`, `{engineVersion}` (only if it embeds its engine,
such as SQLite or DuckDB), and `{compareSyncTier}`, `{queryInsightsTier}`, … (the plan name of a paid feature:
Expand All @@ -135,11 +135,11 @@ Tags do not nest. A number that belongs to the product (a cap, a count) belongs
index, export…), "khóa chính" and "khóa ngoại", "dòng" and "cột", "chỉ đọc", tone marks on the first vowel
(khóa, xóa, tùy, hủy). An app label appears in Vietnamese first with the English in parentheses, as the app shows
it, for example "Tệp từ xa (Remote File)". Feature names stay English (Preview SQL, Users & Roles, Copy To,
Compare & Sync). Text is NFC. The forbidden variants are in `tests/Support/vi-forbidden-variants.php`.
Compare & Sync). Text is NFC.

## Checks

```bash
php artisan test --compact --filter='DatabasePagesTest|EnginesDataTest|ContentParityTest|AssetManifestTest'
php artisan test --compact --filter='DatabasesTest|EnginesDataTest|TranslationsTest|AssetManifestTest'
npm run typecheck
```
3 changes: 1 addition & 2 deletions resources/js/components/databases/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/**
* The props `App\Http\Controllers\DatabaseController` sends, and the
* shape of the database pages' copy. README.md in this folder documents the
* content schema; `tests/Feature/Databases/DatabasePagesTest.php` validates
* every content file against it, so these types and the files cannot drift.
* content schema.
*/
import type { Requirements } from '@/lib/data/platforms';

Expand Down
6 changes: 3 additions & 3 deletions resources/js/components/download/app-store-badge-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
* The markup of `<AppStoreBadge>`, as a plain function of the locale.
*
* Written with `createElement` rather than JSX so `node --test` can render it
* through `react-dom/server` with no build step: tests/js/app-store-badge.test.ts
* renders it in every locale. `app-store-badge.tsx` is the component that binds
* it to the page's locale, its catalog label and the download event.
* through `react-dom/server` with no build step. `app-store-badge.tsx` is the
* component that binds it to the page's locale, its catalog label and the
* download event.
*
* Imports are relative and type-only, so Node resolves them without the `@/`
* alias.
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/download/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* The props `App\Http\Controllers\DownloadController` sends, and the
* page copy's type. The copy's shape is the English file's: `ContentParityTest`
* page copy's type. The copy's shape is the English file's: `TranslationsTest`
* holds every other locale to the same keys.
*/
import type { PostSummary } from '@/components/blog/post-list';
Expand Down
Loading