{"id":26909882,"url":"https://github.com/stack-spot/citron-navigator","last_synced_at":"2026-02-07T19:02:13.733Z","repository":{"id":238747895,"uuid":"734364484","full_name":"stack-spot/citron-navigator","owner":"stack-spot","description":"Citron Navigator is a navigator for React web applications written with and for Typescript.","archived":false,"fork":false,"pushed_at":"2025-09-01T19:58:01.000Z","size":359,"stargazers_count":0,"open_issues_count":1,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-09-01T21:26:32.804Z","etag":null,"topics":["code","library","maintain","navigation","portal","react","typed","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","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/stack-spot.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","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":"2023-12-21T13:55:20.000Z","updated_at":"2025-09-01T19:58:03.000Z","dependencies_parsed_at":"2024-05-07T21:43:47.237Z","dependency_job_id":"4a2a933c-fa57-42f1-800f-20dc5182a63b","html_url":"https://github.com/stack-spot/citron-navigator","commit_stats":null,"previous_names":["stack-spot/citron-navigator"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/stack-spot/citron-navigator","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stack-spot%2Fcitron-navigator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stack-spot%2Fcitron-navigator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stack-spot%2Fcitron-navigator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stack-spot%2Fcitron-navigator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stack-spot","download_url":"https://codeload.github.com/stack-spot/citron-navigator/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stack-spot%2Fcitron-navigator/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29204953,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-07T17:44:10.191Z","status":"ssl_error","status_checked_at":"2026-02-07T17:44:07.936Z","response_time":63,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["code","library","maintain","navigation","portal","react","typed","typescript"],"created_at":"2025-04-01T13:29:52.720Z","updated_at":"2026-02-07T19:02:13.712Z","avatar_url":"https://github.com/stack-spot.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Citron Navigator\nCitron Navigator is a navigator for React web applications written with Typescript.\n\nMain features:\n- Render content based on the current URL.\n- Use hooks to get the current route and url parameters from anywhere.\n- Easily figure if a route or any of its sub-routes are active.\n- Concentrate all route definitions in a single file.\n- Automatic serialization/deserialization of variables in the URL.\n- Type-safe! No more guessing which route accepts which parameters.\n- Context-aware: navigating from `/resourceA/{resourceAId}` to `/resourceA/{resourceAId}/resourceB/{resourceBId}` should take only\n`resourceBId` as a required parameter, `resourceAId` should be optional in this context.\n- Support for modular applications (e.g. Module Federation).\n\n## Motivation\nWe, at StackSpot, have some very large web applications, with an enormous amount of routes. Navigation became a problem where we couldn't\nknow for sure the contract of each page, i.e. what parameters they could receive from the URL (route and search parameters). Moreover,\nit was impossible to easily see every route that existed in the application, since they would be all spread.\n\nThe current solutions for navigating a React web application don't embrace Typescript for a type-safe way of navigating, we normally have\nto implement type-safe mechanisms on our own in order to guarantee a navigation will always be performed correctly. Unfortunately, it is\nhard to force a developer to always use the \"correct way of navigating\" that we created locally, creating a big mess over time. Some\ntimes we'd have no type-check at all and it can become very easy to go to page that requires a variable without passing this variable.\n\nAnother big problem we faced without typed-navigation was getting the search parameters in a page. How would the developer know what search\nparameters the page can receive, where are they defined? How does the programmer make changes to these parameters? What's the correct way\nto deserialize the string in the URL?\n\nIt can become quite complex to manage url variables in a large application, we needed a library that would take care of this for us, so we\ncreated Citron Navigator.\n\n## How does it work?\nCitron Navigator is based in a yaml file that declares the whole navigation tree and the parameters for each of the routes. See the example\nbelow:\n\n```yaml\n+ root (/):\n  + account (/account):\n    + profile: (/profile):\n    + changePassword: (/password):\n    + billing (/billing):\n      year: number\n  + photoAlbums (/albums):\n    search: string\n    year: number\n    month: number (1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12)\n    limit: number\n    page: number\n    type: string ('ownedByMe' | 'sharedWithMe' | 'all')\n    + album (/{albumId}):\n      limit: number\n      page: number\n      + photo (/{photoId}):\n```\n\nThe file above defines that we have 8 routes in our application: root, account, profile, changePassword, billing, photoAlbums, album and\nphoto.\n\n#### Paths\n- root: /\n- account: /account\n- profile: /account/profile\n- changePassword: /account/password\n- billing: /account/billing\n- photoAlbums: /albums\n- album: /albums/{albumId}\n- photo: /albums/{albumId}/{photoId}\n\n#### Route parameters\n- root, account, profile, changePassword, billing and photoAlbums are pages that don't accept any route parameter.\n- album accepts an `albumId` as a route parameter.\n- photo accepts a `photoId` and an `albumId` (from the parent) as route parameters.\n\n#### Search parameters\n- root, account, profile, changePassword and photo are pages that don't accept any search parameter.\n- billing accepts one search parameter: \"year\", and it must be a number.\n- photoAlbums accepts many search parameters, they are:\n  - search: must be a string;\n  - year: must be a number;\n  - month: must be a number and be typed as `1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12`, not any number;\n  - limit: must be a number;\n  - page: must be a number;\n  - type: must be a string and be typed as `'ownedByMe' | 'sharedWithMe' | 'all'`, not any string.\n- album accepts 2 search parameters:\n  - limit: must be a number;\n  - page: must be a number.\n\n### Code generation\nThe code for the navigator is generated based on the yaml file. It uses the runtime library to instantiate a navigator based on your\nspecifications.\n\n### Usage\nLet's render some content according to the current route:\n\n```tsx\nexport const Page = () =\u003e {\n  const [content, setContent] = useState\u003cReact.Element\u003e(\u003c\u003e\u003c/\u003e)\n  \n  useNavigationContext((context) =\u003e {\n    context\n      .when('root', props =\u003e setContent(\u003cHome {...props} /\u003e))\n      .when('root.account', props =\u003e setContent(\u003cAccountDashboard {...props} /\u003e))\n      .when('root.account.profile', props =\u003e setContent(\u003cProfile {...props} /\u003e))\n      .when('root.account.changePassword', props =\u003e setContent(\u003cChangePassword {...props} /\u003e))\n      .when('root.account.billing', props =\u003e setContent(\u003cBilling {...props} /\u003e))\n      .when('root.photoAlbums', props =\u003e setContent(\u003cPhotoAlbums {...props} /\u003e))\n      .when('root.photoAlbums.album', props =\u003e setContent(\u003cAlbum {...props} /\u003e))\n      .when('root.photoAlbums.album.photo', props =\u003e setContent(\u003cPhoto {...props} /\u003e))\n  })\n\n  return content\n}\n```\n\nLet's see the implementation for `Album` which is rendered when the route is `root.photoAlbums.album`:\n\n```tsx\nconst Album = ({ route, params: { albumId, limit, page } }: ViewPropsOf\u003c'root.photoAlbums.album'\u003e) =\u003e (\n  \u003cp\u003eAlbum {albumId}\u003c/p\u003e\n  \u003cp\u003elimit is {limit}\u003c/p\u003e\n  \u003cp\u003epage is {page}\u003c/p\u003e\n  \u003cp\u003e\u003cLink to={route.$parent}\u003eGo back to albums\u003c/Link\u003e\u003c/p\u003e\n  \u003cp\u003e\u003cLink to={route.photo} params={{ photoId: '001' }}\u003eCheck out this picture\u003c/Link\u003e\u003c/p\u003e\n)\n```\n\nNotice that, from \"album\", we can easily create a link to \"photo\" by passing only the `photoId`, the `albumId` is implicit.\n\nWhen creating links or navigating to other pages, the type will always be checked by Typescript. In the example above, if we didn't pass\n`photoId` when creating a link to \"photo\", we'd get a type error and the code wouldn't build.\n\nAttention: we used the component [Link](docs/navigation.md#the-link-component)\nfrom Citron Navigator. This is necessary if you're not using hash based URLs (flag `--useHash=false`). If you are using hash based URLs\n(`/#/path`), then you can safely use a simple `a` tag instead. Example: `\u003ca href={route.$parent.$link()}\u003eGo back to albums\u003c/a\u003e`.\n\n## Installation\nWe're going to use [PNPM](https://pnpm.io) throughout this documentation, but feel free to use either NPM or YARN.\n```sh\npnpm add -D @stack-spot/citron-navigator-cli\npnpm add @stack-spot/citron-navigator\n```\n\n`citron-navigator-cli` is responsible for generating the code while `@stack-spot/citron-navigator` is the runtime dependency.\n\n## Configuration\n1. If you're using git, ignore the file generated by the CLI. In `.gitignore`, add: \"src/generated\".\n2. Create the file `navigation.yaml` in the root of your project. This file should contain the definition for the routes in your\napplication as showed in the first code example of this document.\n3. This is optional, but to make it easier to import navigation related structures, create an alias to `src/generated/navigation.ts`. In\nyour tsconfig.json, add:\n```json\n\"paths\": {\n  \"navigation\": [\"./src/generated/navigation\"],\n}\n```\nYou should do the same to whatever bundler you're using. In Vite, for instance, you should add the following to `vite.config.ts`:\n```ts\n{\n  resolve: {\n    alias: {\n      navigation: resolve(__dirname, './src/generated/navigation'),\n    },\n  },\n}\n```\n\n## Source code generation\n```sh\npnpm citron\n```\n\nBy default it will get the definitions from `navigation.yaml` and output the generated code to `src/generated/navigation.ts`. If you need\nto change this, pass the options `--src` and `--out`.\n\nThe navigator uses hash-based URLs by default (/#/route). To change this behavior, you can pass the option `--useHash=false` to the command\n`citron`.\n\nIt's a good idea to call `citron` before running the application locally or building, check the example below for Vite:\n\n`package.json:`\n```json\n{\n  \"scripts\": {\n    \"dev\": \"citron \u0026\u0026 vite\",\n    \"build\": \"citron \u0026\u0026 tsc \u0026\u0026 vite build --mode production\",\n  }\n}\n```\n\n## Documentation\n- [Sample project](packages/sample)\n- [Configuration file (yaml)](docs/configuration-file.md)\n- [Route-based rendering](docs/route-based-rendering.md)\n- [Loading routes asynchronously](docs/async-route-rendering.md)\n- [Navigation](docs/navigation.md)\n- [The Route object](docs/route-object.md)\n- [Hooks](docs/hooks.md)\n- [Parameter serialization/deserialization](docs/serialization.md)\n- [Modular applications](docs/modular.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstack-spot%2Fcitron-navigator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstack-spot%2Fcitron-navigator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstack-spot%2Fcitron-navigator/lists"}