Skip to content

fix: every outbound link points back into the app's own router, so all 68 of them 404 - #251

Merged
houko merged 4 commits into
mainfrom
fix/external-links-point-at-the-app
Sep 19, 2026
Merged

houko merged 4 commits into
mainfrom
fix/external-links-point-at-the-app

Conversation

@houko

@houko houko commented Sep 18, 2026

Copy link
Copy Markdown
Member

Description

https://pace.yldm.ai/upgrade/pro/self-hosted?plan=month 404s. So do the other 67 outbound links in the repo, for the same reason.

83b5eec4 chore: point this fork at its own domain replaced six upstream hosts with one:

upstream host what it was occurrences
plane.so marketing site 43
app.plane.so the vendor's cloud app, which hosted the self-hosted licence purchase flow 7
docs.plane.so documentation 7
forum.plane.so community forum 6
go.plane.so short links 3
developers.plane.so API docs 2

All of them became pace.yldm.ai — which is this repo's own apps/web deployment. Its route table (apps/web/app/routes/core.ts) holds only :workspaceSlug/..., auth, onboarding and invitation routes. There is no /pricing, /docs, /forum, /upgrade, /talk-to-sales, /changelog or /legals/*.

Because the root route is the dynamic :workspaceSlug segment, these do not even fail as clean routing 404s. /pricing is read as a workspace named "pricing" and fails as a missing workspace; only a deeper path like /upgrade/pro/self-hosted matches nothing and 404s outright. That difference is why this went unnoticed — most of the broken links look like a permissions or workspace problem rather than a bad URL.

The fix is configuration, not new hardcoded URLs. VITE_MARKETING_SITE_URL, VITE_DOCS_URL, VITE_FORUM_URL, VITE_STATUS_URL, VITE_TERMS_OF_SERVICE_URL and VITE_PRIVACY_POLICY_URL all default to empty, which is the call the comment already beside SUPPORT_EMAIL in endpoints.ts made for exactly this reason:

Whoever runs this installation, not upstream. Left empty rather than guessed […] mailing support@yldm.ai about an account on someone else's fork helps nobody.

The rule at every call site: an empty URL renders no control. Not an anchor with an empty href — that navigates to the current page and reads as a broken button — and never a fallback to WEBSITE_URL, which is the mistake being fixed. WEBSITE_URL keeps its default because the app's own public home is a real destination, and the canonical/og:url metadata pointing there was already correct.

Set the env vars for whichever sites exist and the links come back with no code change.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • Improvement (change that would cause existing functionality to not work as expected)

Screenshots and Media (if applicable)

None. Nothing gains a visual element; several controls disappear when their destination is unconfigured, which is the point.

Test Scenarios

  • pnpm turbo run build check:types check:lint check:format — check:types/check:lint/check:format 56/56 and every build target green. Note: the combined run failed twice on admin#build with Prerender: Request failed for /: The operation was aborted due to timeout, both times while this machine was carrying an unrelated Rust build at load average ~200-300. pnpm turbo run build --filter=admin --force passes on its own. This is a real CI flake risk, not something this PR introduces — the admin prerender step has no timeout headroom under load, and it is worth raising separately.
  • apps/api: go build ./..., go vet ./..., gofmt -l . clean, go test ./... 0 failures (non-Docker path; the database-backed tests skip themselves by design).
  • git grep for pace.yldm.ai outside the locale files, the three app/root.tsx metadata sites, endpoints.ts, metadata.ts and the Dockerfiles returns one hit: packages/propel/.storybook/manager.ts brandUrl, which points at the app's own home in a dev-only tool and is correct.
  • The 20 remaining pace.yldm.ai strings under packages/i18n/src/locales/*/template.json are placeholder values — example text in a URL input, not links. Untouched.

Not verified, needs a browser: that each guarded control's surrounding layout still reads correctly with the control absent. The three sites where removal could leave visible debris were restructured rather than guarded (see below), but they were reasoned through, not observed.

References

Three sites needed more than a guard, and are the ones to review hardest:

  • product-updates/footer.tsx — the link row is now data-driven with its dot separator rendered before each surviving link instead of between fixed pairs. The original shape would leave a dangling dot whenever one link dropped out.
  • apps/space/app/error.tsx — picks one of four sentences depending on whether an email, a forum, both or neither is configured, so the prose stays grammatical in each case.
  • apps/admin/.../general/form.tsx — the telemetry copy loses its "in line with our Telemetry Policy" citation when there is no policy to cite, keeping the substantive disclosure that no PII is collected.

Two decisions that are not mine to make and should be confirmed before merge:

  1. The legal links. The sign-in and sign-up screens said "By creating an account, you understand and agree to our Terms of Service and Privacy Policy" with both links 404ing. This PR drops the whole sentence when neither document is configured, on the reasoning that an installation publishing no terms should not tell people they agreed to them. If those documents exist, set VITE_TERMS_OF_SERVICE_URL and VITE_PRIVACY_POLICY_URL and the sentence returns. Worth a look from whoever owns that question.
  2. The paid upgrade CTA is removed, not redirected. Pro and Business both have is_active: true, so those were live buttons opening a 404. handleCheckout is now optional on PlanUpgradeCard, nothing passes it, and all three plans render the existing TalkToSalesCard. PlanCheckoutButton is deliberately left in the tree — wiring a real checkout is what would make it reachable again.

Out of scope, but found while tracing this and worse than the reported bug: the Go transactional email templates were never touched by the brand rename. project_invitation.html, user_activation.html, user_deactivation.html and webhook-deactivate.html still carry x.com/planepowers, linkedin.com/company/planepowers, forum.plane.so, Plane's public roadmap at plane.sh/plane/..., github.com/makeplane/plane, prose reading "Plane is still in its early days", and a hotlinked Plane logo from media.docs.plane.so. These go out in real email to invited users. Fixing them needs decisions about this fork's own social accounts, so they are left alone here.

Also noted: the onboarding seed screenshots still load from upstream's CDN at media.docs.plane.so/seed_assets/*.png, a third-party dependency for content shown to every new workspace.

@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

React Doctor found 3 new issues in 3 files · 3 warnings · score 82 / 100 (Needs work) · 4 fixed · vs main

3 warnings

app/(all)/workspace-invitations/page.tsx

  • ⚠️ L36 React function has high control-flow complexity no-high-complexity-react-function

core/components/instance/setup-form.tsx

  • ⚠️ L65 React function has high control-flow complexity no-high-complexity-react-function

core/components/workspace/billing/comparison/plan-detail.tsx

  • ⚠️ L45 window.open without noopener window-open-without-noopener

Reviewed by React Doctor for commit f6360cd. See inline comments for fixes.

`chore: point this fork at its own domain` replaced six upstream hosts -- plane.so, app.plane.so, docs.plane.so, forum.plane.so, go.plane.so and developers.plane.so -- with the single host pace.yldm.ai. That host is this repo's own apps/web deployment, whose route table holds only `:workspaceSlug/...`, auth, onboarding and invitation routes, so all 68 of those links now land inside the app. Because the root route is the dynamic `:workspaceSlug` segment they do not even fail as routing 404s: `/pricing` is read as a workspace named "pricing" and fails as a missing workspace, while a deeper path like `/upgrade/pro/self-hosted` matches nothing and 404s outright.

The four sites this fork does not have an equivalent of are now configuration rather than constants -- VITE_MARKETING_SITE_URL, VITE_DOCS_URL, VITE_FORUM_URL, VITE_STATUS_URL, plus VITE_TERMS_OF_SERVICE_URL and VITE_PRIVACY_POLICY_URL -- and all of them default to empty, following the call the SUPPORT_EMAIL comment beside them already made for the same reason.

The rule every call site follows: when the URL is empty the control is not rendered. Not an anchor with an empty href, which navigates to the current page and reads as a broken button, and never a fallback to WEBSITE_URL, which is the mistake being fixed. WEBSITE_URL keeps its default because the app's own public home is a real destination, and the canonical and og:url metadata that points there was already correct.

Three sites needed more than a guard. The product-updates footer row is now data-driven with its dot separator rendered before each surviving link rather than between fixed pairs, because the old shape would leave a dangling dot. The space app's error page picks one of four sentences depending on which contact channels exist. The admin telemetry copy loses its "in line with our Telemetry Policy" citation when there is no policy to cite, keeping the disclosure that no PII is collected.

Two behaviour changes worth naming. The sign-in and sign-up screens drop the whole "you understand and agree to our Terms of Service and Privacy Policy" sentence when neither document is configured, rather than keeping the claim and dropping the links -- an installation that publishes no terms should not tell people they agreed to them. And the production error screen no longer offers a status page: pace-status.yldm.ai is NXDOMAIN, and a dead link on the screen a user reaches because something already broke is worse than nothing.
Upstream, a self-hosted installation sent buyers to the vendor's own cloud app to purchase a licence. This fork has no such service, so after the domain rename the Pro and Business "Upgrade to ..." buttons opened pace.yldm.ai/upgrade/pro/self-hosted -- a path the app does not route. Both plans have `is_active: true`, so these were live buttons, not dead code; the button's own label ("Redirecting to Stripe") describes an integration that is not here either.

`handleCheckout` becomes optional on PlanUpgradeCard and nothing passes it, so all three plans now render the existing TalkToSalesCard. PlanCheckoutButton is left in place: wiring up a real checkout is exactly what would make it reachable again. SUBSCRIPTION_REDIRECTION_URLS is gone, along with the `?? TALK_TO_SALES_URL` fallbacks that read as safety but could never fire, because that record was total over the enum.

The billing comparison's button keeps only its talk-to-sales branch, and the plan pages behind "See full features list" are omitted when no marketing site is configured.
Four of the work items seeded into every new workspace ended on a "To learn more..." paragraph pointing at /docs/core-concepts/..., which after the domain rename resolves inside the app rather than at any documentation. The seeds are embedded JSON with no interpolation anywhere in the eight files and internal/config exposes no documentation URL, so there is nothing to make these configurable through without inventing a templating pass for four sentences.

Where a paragraph existed only to carry the link it goes with the horizontalRule above it, since a divider with nothing after it reads as broken; where the link sat inside a tip that teaches something, only the clause goes. Both description_html and description_stripped are edited so the plain-text mirror stays consistent, by exact substring removal rather than re-serialising, leaving formatting and escaping untouched.

The //go:embed comment claimed these files are copies of upstream's, so it now records where issues.json deviates and why. The seed screenshots still load from upstream's CDN at media.docs.plane.so, which is a third-party dependency this change does not address.
@houko
houko force-pushed the fix/external-links-point-at-the-app branch from 18899c3 to f6360cd Compare September 19, 2026 12:45
const redirectUrl = SUBSCRIPTION_REDIRECTION_URLS[subscriptionType][frequency] ?? TALK_TO_SALES_URL;
// Open the URL in a new tab
window.open(redirectUrl, "_blank");
window.open(TALK_TO_SALES_URL, "_blank");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/window-open-without-noopener (warning)

This window.open call leaves the opened page able to redirect your tab via window.opener, so pass 'noopener' in the features argument.

Fix → Pass 'noopener' in the third features argument of window.open so the opened page can't control your tab through window.opener. Add 'noreferrer' too when the destination must not receive the referrer.

Docs

@houko
houko merged commit 5f41e89 into main Sep 19, 2026
14 checks passed
@houko
houko deleted the fix/external-links-point-at-the-app branch September 19, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant