Skip to content

fix(portal): bundle the Geist fonts instead of loading them from Google Fonts - #366

Merged
saeedjamshaid merged 9 commits into
devfrom
saeedjamshaid/self-host-portal-fonts
Sep 25, 2026
Merged

saeedjamshaid merged 9 commits into
devfrom
saeedjamshaid/self-host-portal-fonts

Conversation

@saeedjamshaid

@saeedjamshaid saeedjamshaid commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

A generated portal was slow to show anything on its first load. It waited on the Google Fonts stylesheet. The portal's own files all arrived within about 60 ms, but nothing appeared until fonts.googleapis.com answered. The portal now ships Geist and Geist Mono with its other assets, so a page makes no requests to other domains.

Why

The stylesheet blocked the first paint and hydration. __root.tsx linked the Geist stylesheet from Google Fonts. A stylesheet in <head> blocks rendering, and module scripts wait for it before running. So every page stayed blank and inert until a connection to Google was set up and the CSS came back. From here that took about 1.1 s on a warm connection and 3.4 s on a cold first attempt, most of it connection and TLS setup. On a later visit the connection and CSS are cached, which is why only the first load felt slow.

Taking the tag out of the HTML is not enough. TanStack's HeadContent renders the route's head() links again after hydration. React 19 then holds its updates until a stylesheet it manages has loaded. With the tag stripped from the HTML only, the page painted early, but code highlighting still waited about 2.5 s for the stylesheet to arrive.

What changes

  • Fonts are bundled. @fontsource-variable/geist and @fontsource-variable/geist-mono 5.3.0 are new CLI dependencies. app.css imports them, and Vite copies the woff2 files into assets/. It's the same variable font as before (weights 100–900), and unicode-range means only the subsets a page uses are downloaded. The Google links are removed from __root.tsx.
  • The font packages are copied into the build project, not linked. Tailwind rewrites the url()s in an imported stylesheet relative to the project. A linked package resolves to the CLI's install, so on Windows, with the project and the CLI on different drives, the rewritten path (../../C:/...) can't resolve. Every font would 404, and the published CSS would carry the install path. Copied, the files sit inside the project, and portal serve loads them with no extra Vite setting. portal-project-service.ts now keeps COPIED_DEPENDENCIES and LINKED_DEPENDENCIES as separate lists, and TEMPLATE_DEPENDENCIES is the two together.
  • The font-sans and font-mono classes get Geist too. The theme used to set only Tailwind's page defaults, so everything Fumadocs styles with font-mono stayed in the system font. That covers the method badges, type tables, table of contents, search dialog and playground output: 33 elements on an operation page. --font-sans and --font-mono are set now, and the page defaults derive from them.
  • NOTICE credits Geist under the SIL Open Font License 1.1, since the portal now redistributes the font files. Each woff2 file also carries the copyright notice and the licence URL in its own metadata, which the OFL FAQ (1.10) says is legally sufficient, so the published portal needs no separate licence file.

This also removes a third-party request from every published portal. That helps readers where Google is blocked, and removes a request that is a known GDPR complaint in the EU.

Verification

Measured in headless Chrome over the DevTools protocol against the test-source portal from portal generate, served locally with gzip and the browser cache disabled:

Before After
API page, first paint 1,244 ms 108 ms
API page, interactive 1,263 ms 157 ms
API page, first paint, throttled 4G 1,292 ms 324 ms
Home page, first paint, throttled 4G 1,560 ms 268 ms
Layout shift (CLS) from the font swap, API page 0.0002 0.0002
  • The generated output has no Google URLs and serves both fonts from /assets/. In the browser, body text, code and font-mono elements render in Geist and Geist Mono.
  • Under portal serve both font files return 200 from inside the build project.
  • A new unit test checks that the font packages are real folders inside the project. It fails when they are linked. The e2e font test checks that both Geist files are bundled and that the theme variables are set.
  • A packaging test scans every linked package and fails if any of its stylesheets has a relative url(), so a future package like the fonts can't be linked by mistake. Data URIs, absolute URLs and root-relative paths are ignored, since they resolve anywhere. None of the 18 linked packages has one today. I confirmed the test fails, naming both packages, when the fonts are moved to the linked list. A second test holds the copied list to packages that need the copy (they have a relative url()) and can take it (no dependencies of their own, which pnpm keeps beside a package rather than inside it). It fails if fumadocs-ui is added.
  • The swap from the fallback font to Geist moves almost nothing. CLS is 0.0002 on an operation page (Google's "good" threshold is 0.1) and 0 on a guide page, the same as with Google Fonts. A build with the font files removed shows 0, so the swap accounts for all of it. Only the glyphs change, not the layout.
  • pnpm test passes (1,134 tests, lint clean), and so does the end-to-end build suite run with APIMATIC_E2E=1 (33 tests).
  • I couldn't run a real cross-drive build, because this machine has only one drive and the Windows CI runner keeps everything on D:. That's why the packaging test above checks the condition directly.

Left as is

  • No font preload. Measured over HTTP/2, which is what most hosts serve. Preloading only the latin Geist file brings it in sooner: about 390 ms instead of 510 ms on fast 4G, and 1,650 ms instead of 2,160 ms on slow 4G. But first paint comes 65–150 ms later, because the font competes with the render-blocking stylesheet, and on slow 4G the text is still redrawn about 300 ms after it first appears. Since the swap moves nothing (CLS above), first-paint time is the better thing to keep. Over HTTP/1.1 a preload looked worse, because of the per-host connection limit, so that earlier result doesn't carry over.
  • Real italics. As with the old Google URL, only the upright font loads, so italic text is a slanted copy of it. Both Fontsource packages ship wght-italic.css, and the files are already in the build project. Importing it would change how every <em> looks, so it's a follow-up.
  • portal serve is still slow on its first page load: 2.5–3.6 s, then about 1 s. That's the Vite dev server preparing code on the first request, not the fonts, and it needs its own change.

🤖 Generated with Claude Code

…le Fonts

The Google Fonts stylesheet was render-blocking, so every generated page
waited on a connection to fonts.googleapis.com before its first paint and
before hydrating: about 1.1 s warm and 3.4 s cold, while the portal's own
files arrived in 60 ms. After hydration the route's head re-inserted the
stylesheet, and React 19 held every commit until it loaded.

Geist and Geist Mono now come from @fontsource-variable and are bundled with
the portal. The preview allows the two font packages explicitly, because the
stylesheet reaches their files through url(), which Vite does not treat as an
import, so it refused them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
saeedjamshaid and others added 6 commits September 25, 2026 11:57
… linking them

Tailwind rewrites the url()s in an imported stylesheet relative to the
project, so the linked font packages resolved to the CLI's install. On
Windows, with the project and the CLI on different drives, no relative path
reaches the font files from there: every font 404'd and the published CSS
carried the install path. Copied into the project, they resolve beside it,
and the dev server serves them without an allow-list, so vite.config.ts is
back to what it was.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The theme set only Tailwind's page defaults, so everything Fumadocs styles
with the font-mono class kept Tailwind's system stack: the method badges,
type tables, table of contents, search dialog and playground output, 33
elements on an operation page. Setting --font-sans and --font-mono covers
those classes, and the page defaults derive from them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…the fonts

The copied packages were named twice, and nothing stopped a later package
whose stylesheet uses url() from being linked, where CI, keeping the CLI and
the build on one drive, would never see it break. A test now fails for any
package the stylesheet reaches through a link if its CSS has a url(), and
COPIED_DEPENDENCIES is declared once and spread into TEMPLATE_DEPENDENCIES.

Also brings the wording that said every dependency is linked up to date, drops
a directory creation the copy already does, and amends the plans that still
said the fonts load from Google.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t-portal-fonts

# Conflicts:
#	src/infrastructure/portal-project-service.ts
#	test/infrastructure/portal-project-service.test.ts
… direct imports

The guard read only the packages app.css imports directly, so a stylesheet
one of them imports in turn (fumadocs-openapi's preset pulls in
@fumadocs/api-docs) went unchecked, as did CSS imported from a module, whose
url() files the dev server refuses too. It now scans every linked package,
which is simpler and flags nothing today, and ignores data URIs, absolute URLs
and root-relative paths, which resolve wherever the stylesheet lives.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
saeedjamshaid and others added 2 commits September 25, 2026 14:30
… lists

The url() guard's regex backtracked past its optional quote, so it took every
quoted data URI, absolute URL and root-relative path for a relative url(). It
passed only because no linked package has a quoted url() yet. The quote,
whitespace and closing parenthesis now end the match, as its comment says.

LINKED_DEPENDENCIES and COPIED_DEPENDENCIES are declared apart, and
TEMPLATE_DEPENDENCIES is the two together, so linkDependencies is its original
loop over the linked list and a small copyDependencies handles the fonts. A new
test holds the copied list to packages that need the copy (a relative url())
and survive it (no dependencies of their own, which pnpm keeps beside a package
rather than inside it).

Also rewords the font import comment as a reason instead of history, and
amends the plan's release-notes line that still promised Google Fonts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The e2e font test now asserts that Tailwind's page defaults still read
--font-sans and --font-mono, so a preset setting them directly would show.
The copied-package check counts optionalDependencies, which pnpm also keeps
beside a package. Also brings two reworded comments down to one line and
amends the plan's template section, which still said __root.tsx links Google
Fonts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@saeedjamshaid
saeedjamshaid merged commit 95f2f7e into dev Sep 25, 2026
10 checks passed
github-actions Bot pushed a commit that referenced this pull request Sep 28, 2026
# [2.0.0-beta.1](v1.5.0...v2.0.0-beta.1) (2026-09-28)

* build!: move to Node 24 and TypeScript 7 ([#347](#347)) ([2528d6e](2528d6e))
* feat(quickstart)!: one funnel, and the language question ([#363](#363)) ([c270135](c270135)), closes [#361](#361)
* feat(sdk)!: retire v3 generation and let a publish record itself ([#359](#359)) ([c48f91b](c48f91b)), closes [#358](#358) [#358](#358) [apimatic-io#2233](https://github.com/apimatic-io/issues/2233)

### Bug Fixes

* **config:** keep the package configuration a publish did not write ([#392](#392)) ([bc52735](bc52735))
* **config:** nest publishing under the language ([#350](#350)) ([fbacf74](fbacf74))
* **deps:** take fumadocs 16.15.15, which fixes bold text in a built portal ([#388](#388)) ([fa178ab](fa178ab))
* **plugin:** ask only for the plugin's identity when the languages are recorded ([#389](#389)) ([d721819](d721819)), closes [apimatic/apimatic-io#2258](https://github.com/apimatic/apimatic-io/issues/2258)
* **portal:** bundle the Geist fonts instead of loading them from Google Fonts ([#366](#366)) ([95f2f7e](95f2f7e))
* **portal:** drop the TypeScript definitions panel from operation pages ([#353](#353)) ([042eb89](042eb89))
* **portal:** give a run the temp directory the filesystem names, not an alias ([#377](#377)) ([d71d23f](d71d23f))
* **portal:** keep the encoding a page slug carries ([#396](#396)) ([0bec5f4](0bec5f4)), closes [apimatic/apimatic-io#2270](https://github.com/apimatic/apimatic-io/issues/2270)
* **portal:** keep the header's links on one line ([#398](#398)) ([40dc472](40dc472))
* **portal:** keep the portal name on one line in the header ([#395](#395)) ([f3d352c](f3d352c))
* **portal:** keep the preview starting until its first page answers ([#399](#399)) ([5359442](5359442))
* **portal:** name the operations a shared page comes from, in words a spec's author knows ([#387](#387)) ([01197a7](01197a7))
* **portal:** offer the page actions on API reference pages ([#382](#382)) ([22f4873](22f4873)), closes [apimatic/apimatic-io#2247](https://github.com/apimatic/apimatic-io/issues/2247) [#383](#383)
* **portal:** refuse tag and operationId names a URL or a Windows folder cannot hold ([#400](#400)) ([0ea01b2](0ea01b2)), closes [apimatic/apimatic-io#2262](https://github.com/apimatic/apimatic-io/issues/2262) [#396](#396)
* **portal:** render a request body in a media type Fumadocs has no adapter for ([#386](#386)) ([9f89ad3](9f89ad3))
* **portal:** render an operation whose response example has no value ([#373](#373)) ([1dc84ad](1dc84ad))
* **portal:** render split OpenAPI specifications correctly ([#354](#354)) ([d97cda6](d97cda6))
* **portal:** stop printing an operation's raw description under its title ([#383](#383)) ([25b9f6e](25b9f6e)), closes [apimatic/apimatic-io#2242](https://github.com/apimatic/apimatic-io/issues/2242)
* **quickstart:** download the sample spec from its new openapi.json path ([#401](#401)) ([ebc6743](ebc6743))
* the release test's high and medium findings ([#375](#375)) ([7fe41e1](7fe41e1))

### Features

* code samples support ([#352](#352)) ([7b82568](7b82568))
* **config:** replace portal.json and plugin-config.json with apimatic.json ([#348](#348)) ([506f67f](506f67f))
* local plugin generation ([#358](#358)) ([5ba70de](5ba70de)), closes [#351](#351) [#346](#346) [#348](#348) [#318](#318)
* **portal:** build the documentation portal locally with Fumadocs ([#343](#343)) ([073d09a](073d09a))
* **portal:** build the SDK and Context Plugin pages from real templates and the portal artifacts ([#369](#369)) ([0efd7b3](0efd7b3))
* **portal:** fetch the artifacts from /portal-artifacts ([#361](#361)) ([0e823f3](0e823f3))
* **portal:** generate the SDK and context plugin pages ([#360](#360)) ([338b14a](338b14a))
* **portal:** give the portal block its v2 shape, with branding and tabs ([#355](#355)) ([d0fb189](d0fb189))
* **portal:** lead each SDK card with its install command ([#376](#376)) ([119e9d6](119e9d6)), closes [#375](#375)
* **portal:** let the root nav.json decide the tabs, and fix the nav.json QA findings ([#371](#371)) ([ecf7666](ecf7666))
* **portal:** order the sidebar from nav.json ([#346](#346)) ([306e4f9](306e4f9))
* **portal:** point to api transform when src/spec has no OpenAPI 3.x document ([#364](#364)) ([eefc917](eefc917))
* **portal:** the SDK Overview design, on the SDKs, language and plugin pages ([#391](#391)) ([fd3fe34](fd3fe34))
* **quickstart:** suggest validating with an AI agent when a spec fails ([#385](#385)) ([36fd2bb](36fd2bb))

### Reverts

* **portal:** offer the page actions on API reference pages ([#382](#382)) ([#397](#397)) ([c087ef9](c087ef9)), closes [#388](#388) [apimatic/apimatic-io#2247](https://github.com/apimatic/apimatic-io/issues/2247)

### BREAKING CHANGES

* `apimatic sdk save-changes` is removed, along with
`--codegen-version`, `--stability`, `--track-changes` and `--skip-changes`
on `sdk generate` and `--codegen-version`, `--stability` and
`--update-plugin-config` on `sdk publish`. Java, Ruby, Go and PHP cannot be
generated until they reach v4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sdk): keep --stability, which outlived the version it came with

Retiring v3 took `--stability` with it on the grounds that there was one
answer left. There is one answer *today*: v4 renders every language at beta
and they reach stable one at a time, so the level is a choice that survives
the version being fixed.

The flag is back on both commands, defaulting to stable as before, and the
level a language offers is a table rather than a constant — `stableLevels`
of one is answered without asking, and the interactive flow puts the
question back the moment a language has two. `sdk publish` names the level
in its summary only when the user chose it, read from oclif's parse
metadata, because `--stability stable` and the default are the same string
and the doc's block has no such row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sdk): keep --codegen-version, with one version in it

Retiring v3 removed the flag along with the version, on the grounds that a
choice of one is not a choice. But the flag is how a caller says which
generator they want, and there will be a v5: removing it makes that move a
release that changes what people get, rather than a value they pass.

`CodeGenerationVersion` now holds `V4` alone, and stays an enum so the next
one is a line in it. The flag is back on both commands, accepting `v4`,
defaulting to `v4`, and refusing `v3` by name rather than generating
something else quietly. `sdk publish` names it in its telemetry again.

It is not written down. `codegenVersion` left `apimatic.json` when the
service took over deciding the generator, so the flag says what to generate
with and nothing records what was used.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(config): drop codegenVersion from the published schema
* `apimatic quickstart` no longer offers a choice between a
portal and an SDK. It always builds a portal, and `apimatic sdk generate`
is the command for an SDK alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(quickstart): ask which languages the portal covers

The one question quickstart asks besides the spec. Every language a plugin
can carry comes up checked, because a first portal covering everything it
can is what a single Enter should give, and the four it cannot are named in
the prompt rather than left out in silence.

The answer is written to `apimatic.json` through `recordLanguages` before
anything is built, because that file is what says which SDKs a portal
documents — every command after this one reads it rather than the answer
that produced it.

`languageLabel` moves to `types/sdk/generate.ts`, where both prompts that
name a language can reach it, and TypeScript is spelled the way its own
project spells it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(quickstart): name the plugin after the folder, without asking

The `plugin` block is what asks the service for a context plugin, so
quickstart has to write one — and the journey allows it no question about
plugins. The identity comes from the directory the user is standing in.

A folder name is not a plugin id. `Acme Payments` and `acme_payments` are
ordinary names and neither is the kebab-case `pluginId` has to be, so the
id is made from the name while the name itself is kept as typed — the id
goes to `gh repo create`, the name is what a reader sees. A directory whose
name survives none of that falls back, because writing an id the config
would then refuse is worse than not deriving one.

`0.1.0` for the version: every plugin starts somewhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(quickstart): keep generated files out of the repository

Every command that builds a portal writes `src/static/sdk/`,
`src/static/plugin.zip` and an expanded `./plugin` into the project. None
of it belongs in a repository, and `portal serve` writes it too — so
without this a preview leaves a clean checkout dirty.

`/plugin/` is the one that matters. `plugin publish` runs `git init` inside
that directory and pushes it as its own repository; a parent tracking it
would nest one repository inside another.

Quickstart writes the entries because quickstart is what makes the project.
It appends what is missing and rewrites nothing, so a `.gitignore` the user
wrote survives and re-running in an adopted directory stacks no duplicates.

The paths are ignored here before anything on this branch produces them:
the placement lands with the `/portal-artifacts` call, and a user whose
first build dirties their tree has already been failed by then.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(quickstart): end in the preview again, and say the coming languages under the answer

Two things, both about where a line belongs.

The rebase onto dev quietly took dev's ending for the wizard -- the one that
stops at the scaffold because "the wizard does not ask for the project's SDK
languages yet". It does, three lines above that comment. The handoff to
`portal serve`, and the `onAfterServe` the serve action took for it, had both
gone; nothing failed, because no test described the funnel's own ending. One
does now.

`Next Steps` loses its first two items with them. It told the user to name
their SDK languages and then start a preview; the wizard has just done both.
What is left is what to change now that the portal is on screen.

The coming-soon languages move out of the multiselect's message and under the
answer, in grey. Inside the message they rendered above the languages the user
had picked, in the green a submitted answer is drawn in, reading like a fourth
option rather than a footnote to the three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(portal): give quickstart the api transform hint, with the format it needs

Quickstart checks the spec itself before it writes the project, so the
transform hint portal generate and portal serve now give never reached it.
A Postman collection or a RAML file was told only that it was not an
OpenAPI document, with nothing to do about it.

Both now end on one sentence, which names --format=openapi3yaml: the
transform refuses to run without a format, so the command as printed
before would have failed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(quickstart): ignore only what is still generated into the project
* engines.node is now >=24.0.0. Node 22 is no longer supported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sdk): build the validation service once configDir is assigned

`validationService` was initialised from `this.configDir` in a property
initialiser, but `configDir` is a constructor parameter property. The code only
worked because TypeScript's downlevel emit assigns parameter properties before
property initialisers; under the native class-field semantics that a modern
target selects, `this.configDir` is still undefined at that point and the
service would be constructed with no config directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* build: compile with TypeScript 7, keeping TypeScript 6 for linting

TypeScript 7 is the native compiler. On this repo it builds `src` in ~1.3s
against ~4.7s, and type-checks the suite in ~1.0s against ~4.8s. Emitted
output is unchanged apart from sourcemaps, temp-variable naming and
declaration ordering.

typescript-eslint cannot run on it: every published version, canary included,
declares `typescript >=4.8.4 <6.1.0` and throws at module init on TS 7, which
would take out `lint`, `posttest` and the pre-commit hook. So the two sit side
by side in Microsoft's documented arrangement — `tsc` is TypeScript 7 and
builds, bare `typescript` is the TypeScript 6 shim that only the linter loads.
Naming them this way rather than aliasing TypeScript 7 keeps the compiler each
script gets written down, and survives `pnpm update --latest`.

TypeScript 6 stopped sweeping node_modules/@types, so the test project names
the globals the suite needs; the source project resolves its types through
`node:` imports and needs no list.

`target` moves to es2024 now that the engine floor is Node 24, which also
selects native class-field semantics. `ts-node` goes: it drives the classic
compiler API, which TypeScript 7 no longer ships, and the suite runs on tsx.
`@types/mocha` was six years behind mocha and had to be named explicitly.

The `typescript.tsdk` pin is dropped. It is deprecated in favour of
`js/ts.tsdk.path`, inert unless a developer opts in by hand, and would now
point at a TypeScript 6 build within a patch of what VS Code already bundles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* ci: test on Node 24 and 26, and release on 24

The engine floor is Node 24, so the 22 legs go. The second leg is 26 rather
than the floor alone: 26 becomes LTS on 2026-10-28, inside this major's life,
so it is what most developers will be running. The job count is unchanged.

Releasing on Node 24 drops the `npm install -g npm@latest` step, whose only
purpose was that Node 22 bundles npm 10.9.8 while OIDC trusted publishing
needs 11.5.1. Node 24 bundles 11.19.0. The constraint moves to a comment on
the matrix, since pinning that job back to 22 would silently break publishing.

`npm-tag-latest.yml` carried a `node: [ '14' ]` matrix that no step consumed;
the job never sets up Node at all.

The standalone pnpm in test.yml is kept rather than reverted to corepack. Its
original reason is gone, but it is a SHA-pinned executable that needs no Node
of its own, where corepack would resolve pnpm from the registry at run time and
need the three-step setup-node dance the other workflows use. The comment now
states the reason that actually holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(vscode): repair the CLI debug configuration

`preLaunchTask` named a task the TypeScript extension provides, and that
provider is not registered when the extension stands down for TypeScript 7, so
the configuration would fail to start. The npm task provider supplies `build`
regardless, and still rebuilds before launching.

`program` pointed at `bin/run`, deleted in the ESM migration; it only launched
because Node appends `.js`. The forty commented-out argument lines carried
another machine's absolute paths.

`.gitignore` re-ignored `.vscode/launch.json` in its custom section, cancelling
the allowlist above it. Inert while the file is tracked, but it would have
refused a re-add.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: record the two-compiler toolchain and how to remove it

The `typescript` dependency no longer being the compiler is surprising enough
to look like a mistake, so state why it is there, which command gets which
compiler, and the condition for deleting the arrangement.

Also corrects the build commands, which documented `tsc -b` although the
project has no references and the scripts call plain `tsc`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* ci: remove unused action

* fix: broken test

* fix: mock fs broken in node 26

* ci: install pnpm standalone in the build and release workflows

Corepack is not distributed with Node 25 and newer, so the Node 26 leg of
check_build could not bootstrap pnpm. Also correct the release floor: npm
11.5.1, which trusted publishing needs, first ships in Node 24.5.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(hooks): check the running Node against the declared engine range

Read engines.node from the manifest oclif has already parsed and compare
with semver, so package.json is the single source of the floor and a
minor-version floor is honoured. Test the hook's behaviour on both sides
of the range, and hold the CLI's range to the Node floors the portal
template's dependencies declare.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: name the e2e test that depends on the two-compiler arrangement

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: untrack launch.json and drop leftover config

launch.json goes back to being a personal, ignored file. mocha.opts named
ts-node and has been unused since mocha 8. ESLint now skips Claude Code
worktrees under .claude.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(portal): bind the prerender preview server to 127.0.0.1

The prerender pass fetched pages from a preview server on `localhost`.
On macOS it listened on `::1` only; a stalled connect under load fell
back to 127.0.0.1 and was refused, failing the portal build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 2.0.0-beta.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants