{"id":16921423,"url":"https://github.com/thediveo/spaserve","last_synced_at":"2026-02-11T17:32:47.036Z","repository":{"id":57706987,"uuid":"502330584","full_name":"thediveo/spaserve","owner":"thediveo","description":"Serving SPAs with client-side DOM routing and behind different routes without rebuilding.","archived":false,"fork":false,"pushed_at":"2025-01-05T20:58:19.000Z","size":57,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-01T10:03:24.359Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thediveo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2022-06-11T11:35:12.000Z","updated_at":"2025-01-05T20:55:07.000Z","dependencies_parsed_at":"2025-01-05T23:55:48.179Z","dependency_job_id":null,"html_url":"https://github.com/thediveo/spaserve","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/thediveo/spaserve","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thediveo%2Fspaserve","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thediveo%2Fspaserve/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thediveo%2Fspaserve/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thediveo%2Fspaserve/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thediveo","download_url":"https://codeload.github.com/thediveo/spaserve/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thediveo%2Fspaserve/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260468984,"owners_count":23014006,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-13T19:51:47.204Z","updated_at":"2026-02-11T17:32:42.008Z","avatar_url":"https://github.com/thediveo.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SPA Serve\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/thediveo/spaserve.svg)](https://pkg.go.dev/github.com/thediveo/spaserve)\n![GitHub](https://img.shields.io/github/license/thediveo/spaserve)\n![build and test](https://github.com/TheDiveO/spaserve/actions/workflows/buildandtest.yaml/badge.svg?branch=master)\n[![Go Report Card](https://goreportcard.com/badge/github.com/thediveo/spaserve)](https://goreportcard.com/report/github.com/thediveo/spaserve)\n![Coverage](https://img.shields.io/badge/Coverage-95.8%25-brightgreen)\n\n`spaserve` serves \"Single Page Applications\" (SPAs) from Go that are using...\n\n- ...client-side DOM routing,\n- ...varying base paths in different deployments or even within the same\n  deployment because of multiple access paths.\n\nAnd all this **without the need to rebuild your SPA production code** just\nbecause the (HTML) \"[base\nURL](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base)\" changes.\n\nFor devcontainer instructions, please see the [section \"DevContainer\"\nbelow](#devcontainer).\n\n## Usage\n\n1. prepare your SPA `index.html` template (such as `public/index.html`) by\n   adding a `base` element, if not done already; set its `href` attribute to\n   `./`:\n\n   ```html\n   \u003c!doctype html\u003e\n   \u003chtml lang=\"en\"\u003e\n   \u003chead\u003e\n       \u003cbase href=\"./\" /\u003e\n       \u003c!-- ... --\u003e\n    \u003c/head\u003e\n    \u003cbody\u003e\n        \u003c!-- ... --\u003e\n    \u003c/body\u003e\n    \u003c/html\u003e\n   ```\n\n2. In case of CRA (Create React App), set the `homepage` field in `package.json`\n   to `\".\"` (**not** the root slash ~~`\"/\"`~~):\n\n   ```json\n   {\n     \"homepage\": \".\",\n   }\n   ```\n\n3. add a basename helper to your SPA sources, such as a new file\n   `src/util/basename.ts`:\n\n   ```ts\n   export const basename = new URL(\n           ((document.querySelector('base') || {}).href || '/')\n       ).pathname.replace(/\\/$/, '')\n   ```\n\n4. in your `App.tsx` ensure that you tell your client-side DOM router to\n   correctly pick up the basename; this makes reloading the SPA from any route\n   and bookmarking routes possible:\n\n   ```tsx\n   import { basename } from 'utils/basename'\n\n   \u003cRouter basename={basename}\u003e\n       \u003c!-- your app components here --\u003e\n   \u003c/Router\u003e\n   ```\n\n5. **Make sure that all links (and asset references) are relative**, such as\n   `./view2`, et cetera.\n\n6. in your service, create your HTTP route muxer and set up your API handlers as\n   usual, then create a `SPAHandler` and register it as the route handler to be\n   used when all other handlers don't match:\n\n   ```go\n   r := mux.NewRouter() // or whatever you prefer\n   // (set up all your API routes)\n\n   // finally create a suitable fs.FS to be used with the SPAHandler\n   // and register it so that it serves on all routes not handled by\n   // the more specific (API) handlers. Here, we assume the SPA assets\n   // to be rooted in web/build.\n   spa := spaserve.NewSPAHandler(os.DirFS(\"web/build\"), \"index.html\")\n   r.PathPrefix(\"/\").Handler(spa)\n   ```\n\n## References\n\nUseful background knowledge when dealing with serving HTTP resources,\nbase(names), et cetera...\n\n- [An elegant solution of deploying React app into a\n  subdirectory](https://skryvets.com/blog/2018/09/20/an-elegant-solution-of-deploying-react-app-into-a-subdirectory/)\n  (_Sergey Kryvets_) – a rare competent analysis and introduction to the `base`\n  HTML element. This post shows how to get SPAs working with `base` for a\n  _fixed_, _hardcoded_ base path. In contrast, `spaserver` _dynamically_\n  rewrites the `base` element when serving an SPA, as needed.\n\n- [answer to \"_Golang. What to use? http.ServeFile(..) or\n  http.FileServer(..)?_\"](https://stackoverflow.com/a/28798174/6632214)\n  (stackoverflow) – and yes, `spaserve` uses `http.FileServer` which supports\n  `fs.FS` via an `http.FS` adaptor.\n\n## DevContainer\n\n\u003e [!CAUTION]\n\u003e\n\u003e Do **not** use VSCode's \"~~Dev Containers: Clone Repository in Container\n\u003e Volume~~\" command, as it is utterly broken by design, ignoring\n\u003e `.devcontainer/devcontainer.json`.\n\n1. `git clone https://github.com/thediveo/enumflag`\n2. in VSCode: Ctrl+Shift+P, \"Dev Containers: Open Workspace in Container...\"\n3. select `enumflag.code-workspace` and off you go...\n\n## Go Version Support\n\n`spaserve` supports versions of Go that are noted by the [Go release\npolicy](https://golang.org/doc/devel/release.html#policy), that is, major\nversions _N_ and _N_-1 (where _N_ is the current major version).\n\n## Copyright and License\n\n`spaserve` is Copyright 2022, 2025 Harald Albrecht, and licensed under the\nApache License, Version 2.0.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthediveo%2Fspaserve","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthediveo%2Fspaserve","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthediveo%2Fspaserve/lists"}