feat: Full sub-path hosting support (BASE_URL with path)#1455
Conversation
Context.BaseURL() was always calling Request.BaseURL() which only returns scheme://host:port, stripping any path component from the configured BASE_URL. This broke all redirects and frontend navigation when Fider is hosted under a sub-path (e.g., BASE_URL=https://example.com/feedback). The package-level web.BaseURL() function already handled this correctly by returning env.Config.BaseURL in single-host mode. This change aligns the Context method with that behavior. Fixes getfider#1452 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add basePath() utility that extracts the path component from BASE_URL,
enabling Fider to be hosted under a subfolder (e.g., example.com/feedback).
Backend: Replace all hardcoded c.Redirect("/") calls with c.BaseURL()
to ensure redirects respect the configured base path.
Frontend: Update all hardcoded href attributes and location.href
assignments to use basePath() prefix. Add automatic URL resolution
in the http service so API calls include the base path. Fix
navigator.goHome/goTo/replaceState to prepend the base path.
Fixes getfider#1453
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TS6133 error - Fider was imported but only basePath is used. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
339da4a to
86469a4
Compare
Documents all installation types (single-host vs multi-host, with/without sub-path) and their test coverage. Includes unit test results and manual testing verification for the production deployment. Addresses maintainer feedback requesting testing across all supported installation types. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add docker-compose test setup with Caddy reverse proxy and self-signed certificates to test all deployment scenarios locally: - Scenario 1: Single-host without sub-path (https://fider.local) - Scenario 2: Single-host WITH sub-path (https://app.local/feedback) - Scenario 3: Multi-host with subdomains (https://*.multi.local) Includes detailed test checklists, setup scripts, and quick-start guide. Addresses maintainer request to test against all supported installation types before merging PRs getfider#1454 and getfider#1455. Files added: - docker-compose-test.yml: Multi-scenario test environment - Caddyfile.test: Reverse proxy config with automatic HTTPS - TEST-SCENARIOS.md: Detailed test checklists for each scenario - QUICK-TEST.md: Fast setup guide - TEST-README.md: Overview and architecture - setup-hosts.sh/ps1: Host file configuration scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Context.BasePath() method that extracts just the path prefix from BASE_URL for building redirect paths. Only hardcoded redirects are changed to use BasePath(); handlers already using BaseURL() are left as-is since the getfider#1452 fix makes them correct. Also fixes lint issues: prettier formatting in ContentModeration and SideMenu, adds rel="noreferrer" to Legal.tsx target="_blank" links. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Thanks for this — sub-path hosting is a useful feature. A few things to address before this can merge. 1. Root-level file bloat8 test infrastructure files have been added to the repo root: 2. Missed components — still broken under sub-path hostingThe manual
These are the exact bugs that prove the core concern below. 3. Fragile pattern — future bugs guaranteedThe fundamental problem with this PR is that every developer must remember to call Frontend: a) Create a b) Auto-resolve inside c) Create a d) Add an ESLint rule to flag hardcoded root-relative Backend (Go): e) Add a 4. Duplicated basePath logic
These should be consolidated into one |
mattwoberts
left a comment
There was a problem hiding this comment.
Hi - I added a comment that is basically the review :)
|
Thanks Matt, will get to it today and push the updates. |
# Conflicts: # commercial/pages/Administration/ContentModeration.page.tsx # public/pages/Administration/pages/ContentModeration.page.tsx # public/pages/Administration/pages/ManageBilling.page.tsx
Addresses review item 1: these 8 files were local test scaffolding that should not have been included in the PR. - TESTING.md - TEST-README.md - TEST-SCENARIOS.md - QUICK-TEST.md - docker-compose-test.yml - Caddyfile.test - setup-hosts.sh - setup-hosts.ps1
Addresses review items 3a and 4: three near-duplicate implementations of
basePath-prepending ('navigator.ts' basePath(), 'navigator.ts' goTo()
inline, 'http.ts' resolveUrl() inline) replaced with a single exported
resolveHref() utility in navigator.ts.
- resolveHref(href) is idempotent: non-root-relative hrefs and already-
prefixed hrefs are returned unchanged, so it is safe to apply at any
layer.
- navigator.goTo() now calls resolveHref() directly.
- http.ts replaces its private resolveUrl() with resolveHref().
- http.ts's 401 re-auth redirect (`/signin?redirect=...`) was previously
bare root-relative and broken under sub-path hosting; now also uses
resolveHref().
Addresses review item 3b: both components render <a href={props.href}>
and previously passed the href through unchanged, forcing every caller
to remember to prepend basePath() for sub-path hosting.
Callers now pass natural root-relative hrefs and the component handles
resolution internally via resolveHref(). The idempotency guard in
resolveHref() (!href.startsWith(bp)) makes existing call sites that
still pass ${basePath()}/... continue to work without a double-prefix,
so this change is safe to land independently from call-site cleanup.
Addresses review items 2 and 3c together since migrating the six flagged components onto a new <Link> primitive is a cleaner fix than sprinkling basePath() calls at each site. Item 3c — new <Link> component: public/components/common/Link.tsx is a thin wrapper around <a> that runs href through resolveHref(). Callers write natural root-relative paths; sub-path hosting is handled internally. Re-exported from @fider/components via ./common. Item 2 — six missed bare hrefs fixed by swapping <a> for <Link>: - public/pages/Home/components/ListPosts.tsx (two hrefs, lines 30 and 84 of the PR snapshot) - public/components/NotificationIndicator.tsx (line 17) - public/components/ShowPostResponse.tsx (line 51) - public/pages/MyNotifications/MyNotifications.page.tsx (line 41) - public/components/ShowTag.tsx (line 37) Rationale for <Link> over a bare basePath() call at each site: The PR having missed six sites across five components is direct evidence that the "remember to call basePath()" convention does not scale. A wrapper component inverts that — the default path is correct and developers would have to go out of their way to get it wrong by reaching for <a> instead of <Link>. The ESLint rule added in a later commit enforces this as a safety net.
Addresses review item 3e: manual c.Redirect(c.BasePath() + "/...") sites
are boilerplate that every developer must remember. New helper takes a
natural root-relative path and prepends BasePath() internally.
Migrated 8 c.Redirect(c.BasePath() + "/...") sites in:
- app/handlers/oauth.go (3)
- app/handlers/signin.go (1)
- app/handlers/signup.go (1)
- app/middlewares/tenant.go (3)
Also migrated 3 bare c.Redirect("/...") sites that were missing basePath
entirely, brought in by the upstream merge of OAuth allowed-roles work:
- app/handlers/oauth.go: /access-denied
- app/middlewares/user.go: /signin and /signin?redirect=...
These would have been sub-path hosting bugs on their own — using
RedirectTo() makes them correct and consistent with the rest of the
codebase.
Addresses review item 3d: local lint rule that flags hardcoded
root-relative hrefs in JSX (both string literals and template literals),
enforcing the <Link>/<Button>/<Dropdown.ListItem>/resolveHref() pattern
as the safety net for sub-path hosting.
Rule:
eslint-rules/no-bare-root-href.js — zero new npm dependencies, loaded
via --rulesdir. Flags <a href="/..."> and <a href={`/posts/${id}`}>.
Exempts <Link>, <Button>, and Dropdown.ListItem (all three auto-resolve
hrefs internally). Exempts template literals that begin with
${basePath()}/... and non-/-prefixed hrefs (absolute URLs, fragments,
mailto: etc.).
Rationale for local rule vs. adding eslint-plugin-* dependency:
Reviewer explicitly preferred --rulesdir, and the rule is small and
project-specific. A plugin would add a package, a publish story, and
a dependency bump in the lockfile; none of that is warranted for a
~60-line rule file used only by this repo.
Follow-up:
Also swapped window.location.href = link for navigator.goTo(link) in
public/pages/Administration/pages/ContentModeration.page.tsx (this
file arrived from upstream during the merge and had a bare
root-relative string literal assigned to location.href, which the
rule cannot catch because it is not a JSX href attribute —
navigator.goTo() runs resolveHref() internally).
Without this, the rule file's CommonJS `module.exports` trips the no-undef rule from the default eslint:recommended config.
Addresses implicit test-coverage expectation for the new sub-path
primitives introduced in earlier commits.
Coverage:
public/services/navigator.spec.ts (10 tests)
- idempotency guard under sub-path hosting
- trailing-slash handling in BASE_URL
- pass-through for absolute URLs, fragments, mailto:, empty, relative
- graceful fallback when BASE_URL is malformed
public/components/common/Link.spec.tsx (3 tests)
- bare href at domain root
- prefix insertion under sub-path
- passthrough of className/target/rel props
All 140 Jest tests pass.
Collapsed multi-line message string onto one line so it fits within prettier's printWidth: 160. Caught when running the full `make lint` inside a Linux container (Windows CRLF was masking the issue locally).
|
Hi @mattwoberts — thanks for the detailed review. Pushed fixes on top of the existing branch (not force-pushed, so your "viewed" state should still be intact). Here's a per-item mapping with commit SHAs. First, a note on the merge: upstream had moved significantly since this PR was opened (Go 1.25 bump, commercial module removed, OAuth allowed-roles, zh-TW locale, DoS fix, etc.) and our branch modified a file that got deleted upstream (
Item 1 — Root-level file bloat ✅Commit: Item 2 — Missed components ✅Commit: Item 3a + 4 — Consolidate basePath logic,
|
Summary
example.com/feedback/) #1453: Adds comprehensive sub-path hosting support so Fider can be hosted at e.g.example.com/feedback/behind a reverse proxy.example.com/feedback/behind a reverse proxy.Context.BaseURL()fix that preserves the path component fromBASE_URL.basePath()frontend utility (innavigator.ts) that extracts the path portion fromFider.settings.baseURLfor use inhrefattributes and navigation.httpservice'sfetch()wrapper to automatically prepend the base path to all root-relative API calls (/api/v1/...,/_api/...).navigator.goHome(),goTo(), andreplaceState()to respect the base path.hrefattributes across 15+ React components.c.Redirect("/...")calls in 5 Go handler/middleware files to usec.BaseURL().views/index.htmlAtom feed<link>tags to use the templatebaseURLvariable.Files changed (24 total)
Backend (Go):
app/handlers/oauth.go— 3 redirects fixedapp/handlers/post.go— 1 redirect fixedapp/handlers/signin.go— 1 redirect fixedapp/handlers/signup.go— 1 redirect fixedapp/middlewares/tenant.go— 3 redirects fixedFrontend services:
public/services/navigator.ts— NewbasePath()utility, fixedgoHome/goTo/replaceStatepublic/services/http.ts— Auto-prepend base path to fetch URLspublic/services/index.ts— Re-exportbasePathFrontend components (href fixes):
public/components/Header.tsxpublic/components/UserMenu.tsxpublic/components/ReadOnlyNotice.tsxpublic/components/common/Legal.tsxpublic/pages/Administration/components/SideMenu.tsx(12 links)public/pages/Administration/pages/Export.page.tsxpublic/pages/Administration/pages/GeneralSettings.page.tsxpublic/pages/Administration/pages/ManageBilling.page.tsxpublic/pages/Administration/pages/ContentModeration.page.tsxpublic/pages/Home/Home.page.tsxpublic/pages/Home/components/ShareFeedback.tsxpublic/pages/SignIn/CompleteSignInProfile.page.tsxpublic/pages/SignIn/LoginEmailSent.page.tsxcommercial/components/ModerationIndicator.tsxcommercial/pages/Administration/ContentModeration.page.tsxTemplates:
views/index.html— Atom feed link hrefsTest plan
go vetpasses on changed Go packagesBASE_URL=https://example.com/feedbackbehind a reverse proxy/feedbackprefix/feedback/api/v1/.../feedback/not/🤖 Generated with Claude Code