feat(site): add the tx landing page and publish it to GitHub Pages - #36
Conversation
Add a single-page site under site/ built with Vite, React, and @fx/ui, covering what tx is, how to install it, marketplaces, updating, and the types-only plugin contract. It ships a CNAME for tx.fx.gd and deploys from a Pages workflow on pushes that touch the site. @fx/ui resolves from GitHub Packages, which requires a token even for public packages, so site/.npmrc reads one from the environment. The workflow prefers a PACKAGES_TOKEN secret and falls back to the run's GITHUB_TOKEN. The pin is a preview build rather than a release: 1.0.0 emits a ButtonProps with none of its members, and its globals.css imports tw-animate-css without depending on it, so neither the components nor the stylesheet are usable from it. Both are fixed upstream and carried by this build.
There was a problem hiding this comment.
Pull request overview
Adds a new site/ subproject that serves as tx’s landing page and introduces a GitHub Actions workflow to build and deploy it to GitHub Pages (custom domain via CNAME). This fits the repo by providing an official marketing/docs entry point that links back into the existing CLI/docs/plugin system.
Changes:
- Add a Vite + React site under
site/with theming, command-copy blocks, and content describing tx’s core concepts. - Add a Pages workflow that builds
site/distand deploys it whensite/changes. - Update repo-wide tooling/docs (Biome config,
.gitignore, andREVIEW.md) to support the new subproject and document the dev-server networking rationale.
Reviewed changes
Copilot reviewed 14 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
site/vite.config.ts |
Vite config with Tailwind plugin and tailnet-friendly dev server settings. |
site/tsconfig.json |
Site TypeScript configuration for React/Vite build. |
site/src/main.tsx |
React entrypoint wiring ThemeProvider and rendering App. |
site/src/index.css |
Imports @fx/ui globals and configures Tailwind source scanning for UI dist. |
site/src/components/ThemeToggle.tsx |
Theme toggle button using @fx/ui theme hook. |
site/src/components/CommandBlock.tsx |
Copyable command block UI + clipboard fallback logic. |
site/src/App.tsx |
Main landing page layout/content and navigation links to repo docs. |
site/public/favicon.svg |
New site favicon. |
site/public/CNAME |
Custom domain for GitHub Pages artifact. |
site/package.json |
Site dependencies and build scripts. |
site/index.html |
HTML shell including pre-hydration theme class toggle to avoid flash. |
site/bun.lock |
Lockfile for reproducible site installs/builds. |
site/.npmrc |
GitHub Packages registry config for @fx/ui installs via token env var. |
REVIEW.md |
Documents why the site dev server binds 0.0.0.0 and keeps allowedHosts protection. |
biome.json |
Update ignores for nested dist/node_modules and enable Tailwind directive parsing. |
.gitignore |
Ignore Playwright MCP working directory. |
.github/workflows/pages.yml |
Build + deploy workflow for GitHub Pages using Bun and Pages actions. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
navigator.clipboard can reject inside a secure context when permission is denied, not just be absent outside one. Route both cases through the selection-based copy so the button only reports success when something was actually copied.
copy awaits the clipboard write, so the block can be gone by the time it resolves. Starting the reset timer then leaves it running past the effect cleanup that exists to clear it.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 17 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
site/vite.config.ts:13
server.allowedHostsonly allows.ts.net. WhenallowedHostsis set, Vite will also rejectlocalhost/127.0.0.1Host headers, which makes local (non-tailnet) development fail. Consider allowing localhost explicitly while keeping the.ts.netsuffix restriction.
allowedHosts: [".ts.net"],
site/index.html:27
- Accessing
localStoragecan throw (e.g., storage disabled / strict privacy modes). Since this script runs before React mounts, an exception here can prevent the page from rendering at all. WraplocalStorage.getItemin a try/catch and treat failures likenull.
(() => {
const stored = localStorage.getItem("theme");
const dark =
stored === "dark" ||
((stored === null || stored === "system") &&
configure-pages exists to hand a base path to a static site generator, and nothing in this workflow reads its outputs — Vite builds against the site root because Pages serves it from a custom domain. Keeping the step would mean granting the build job pages: write so a no-op could call the Pages API.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 17 changed files in this pull request and generated no new comments.
Suppressed comments (2)
site/src/components/CommandBlock.tsx:46
commands.mapuseskey={command}, which will produce duplicate React keys when the same command string appears more than once (and keys should be unique among siblings). SinceCommandBlockis a reusable component, it should generate a guaranteed-unique key (e.g., include the index).
{commands.map((command) => (
<code key={command} className="block">
<span className="select-none text-muted-foreground">$ </span>
{command}
</code>
site/index.html:27
- The inline theme bootstrap script assumes
localStorageis always available. In some browser/privacy configurations,localStorage.getItem(...)can throw (e.g., blocked storage), which would abort the script and potentially prevent the page from initializing. Wrap the storage access in a try/catch and fall back to the system preference when storage is unavailable.
(() => {
const stored = localStorage.getItem("theme");
const dark =
stored === "dark" ||
((stored === null || stored === "system") &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.classList.toggle("dark", dark);
})();
Adds a single-page site under
site/and a workflow that deploys it to tx.fx.gd.Vite 7 + React 19 +
@fx/ui. The page covers what tx is, installing it, marketplaces, updating, and the types-only plugin contract — with copyable command blocks and a light/dark toggle that has no flash on load.Deployment
.github/workflows/pages.ymlbuildssite/distand deploys on pushes that touchsite/.site/public/CNAMEcarries the custom domain into the artifact. Pages is already configured on this repo fortx.fx.gdwith Actions as the source, and DNS is in place, so this goes live on merge.@fx/uiresolves from GitHub Packages, which requires a token even for public packages, sosite/.npmrcreads one from the environment — no secret is committed. The workflow prefers aPACKAGES_TOKENsecret and falls back to the run'sGITHUB_TOKEN.Why the
@fx/uipin is a preview build1.0.0cannot render this page. ItsButtonemits aButtonPropswith none of its members, sochildrenandclassNameare type errors; and itsglobals.cssimportstw-animate-csswithout depending on it, so the stylesheet fails to resolve at all. Both were fixed upstream in fx/ui (#15, #16) and this pins the preview build carrying them. It moves to a release tag once one ships.Repo-level changes
biome.jsongainscss.parser.tailwindDirectivesso the root Biome run can parse@source, and its ignore globs become**/-prefixed so they cover the sub-project'snode_modulesanddistrather than only the top level. Without that,bun run checkfails onsite/.REVIEW.mdrecords why the dev server binds0.0.0.0, since that reads as a finding without the container/tailnet context.Verification
bun run lint,typecheck, andbuildpass against a clean export of this branch — 45 files linted, no fixes.site:bun install --frozen-lockfile+bun run buildreproduces from a clean checkout withCNAMEindist.Hostheader is rejected, confirmingallowedHostsis doing its job.