1"use strict";(globalThis.webpackChunkflarum_docs=globalThis.webpackChunkflarum_docs||[]).push([[9953],{7952(e,n,t){t.r(n),t.d(n,{assets:()=>i,contentTitle:()=>l,default:()=>h,frontMatter:()=>a,metadata:()=>r,toc:()=>d});const r=JSON.parse('{"id":"extend/routes","title":"Routes and Content","description":"A fundamental part of extending Flarum is adding routes \u2014\xa0both to expose new resources in the JSON-API, and to add new pages to the frontend.","source":"@site/docs/extend/routes.md","sourceDirName":"extend","slug":"/extend/routes","permalink":"/extend/routes","draft":false,"unlisted":false,"editUrl":"https://github.com/flarum/docs/tree/main/docs/extend/routes.md","tags":[],"version":"current","frontMatter":{},"sidebar":"extendSidebar","previous":{"title":"Frontend Development","permalink":"/extend/frontend"},"next":{"title":"Models and Migrations","permalink":"/extend/models"}}');var s=t(4848),o=t(8453);const a={},l="Routes and Content",i={},d=[{value:"Backend Routes",id:"backend-routes",level:2},{value:"Defining Routes",id:"defining-routes",level:3},{value:"Controllers",id:"controllers",level:3},{value:"Route Parameters",id:"route-parameters",level:3},{value:"Generating URLs",id:"generating-urls",level:3},{value:"Views",id:"views",level:3},{value:"Frontend Routes",id:"frontend-routes",level:2},{value:"Route Parameters",id:"route-parameters-1",level:3},{value:"Route Resolvers",id:"route-resolvers",level:3},{value:"Generating URLs",id:"generating-urls-1",level:3},{value:"Linking to Other Pages",id:"linking-to-other-pages",level:3},{value:"Content",id:"content",level:2}];function c(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.header,{children:(0,s.jsx)(n.h1,{id:"routes-and-content",children:"Routes and Content"})}),"\n",(0,s.jsx)(n.p,{children:"A fundamental part of extending Flarum is adding routes \u2014\xa0both to expose new resources in the JSON-API, and to add new pages to the frontend."}),"\n",(0,s.jsx)(n.p,{children:"Routing happens on both the PHP backend and the JavaScript frontend."}),"\n",(0,s.jsx)(n.h2,{id:"backend-routes",children:"Backend Routes"}),"\n",(0,s.jsx)(n.p,{children:"On the backend, Flarum has three collections of routes:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"forum"})," These routes are accessible under ",(0,s.jsx)(n.code,{children:"yourforum.com/"}),". They include routes that show pages in the frontend (like ",(0,s.jsx)(n.code,{children:"yourforum.com/d/123-title"}),") and other utility routes (like the reset password route)."]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"admin"})," These routes are accessible under ",(0,s.jsx)(n.code,{children:"yourforum.com/admin/"}),". By default, there is only one ",(0,s.jsx)(n.code,{children:"admin"})," route on the backend; the rest of the admin routing happens on the frontend."]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"api"})," These routes are accessible under ",(0,s.jsx)(n.code,{children:"yourforum.com/api/"})," and make up Flarum's JSON",":API","."]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"defining-routes",children:"Defining Routes"}),"\n",(0,s.jsxs)(n.p,{children:["You can add routes to any of these collections using the ",(0,s.jsx)(n.code,{children:"Routes"})," extender. Pass the name of the collection in the extender's constructor, then call its methods to add routes."]}),"\n",(0,s.jsxs)(n.p,{children:["There are methods to register routes for any HTTP request method: ",(0,s.jsx)(n.code,{children:"get"}),", ",(0,s.jsx)(n.code,{children:"post"}),", ",(0,s.jsx)(n.code,{children:"put"}),", ",(0,s.jsx)(n.code,{children:"patch"}),", and ",(0,s.jsx)(n.code,{children:"delete"}),". All of these methods accept three arguments:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"$path"})," The route path using ",(0,s.jsx)(n.a,{href:"https://github.com/nikic/FastRoute#defining-routes",children:"FastRoute"})," syntax."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"$n
1ame"})," A unique name for the route, used for generating URLs. To avoid conflicts with other extensions, you should use your vendor name as a namespace."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"$handler"})," The name of the controller class that will handle the request. This will be resolved through the container."]}),"\n"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-php",children:"<?php\n\nuse Flarum\\Extend;\nuse Acme\\HelloWorld\\HelloWorldController;\n\nreturn [\n (new Extend\\Routes('forum'))\n ->get('/hello-world', 'acme.hello-world', HelloWorldController::class)\n];\n"})}),"\n",(0,s.jsxs)(n.admonition,{type:"info",children:[(0,s.jsx)(n.mdxAdmonitionTitle,{children:(0,s.jsx)(n.a,{href:"https://github.com/flarum/cli",children:"Flarum CLI"})}),(0,s.jsx)(n.p,{children:"You can use the CLI to automatically generate your routes:"}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"$ flarum-cli make backend route\n"})})]}),"\n",(0,s.jsx)(n.h3,{id:"controllers",children:"Controllers"}),"\n",(0,s.jsxs)(n.p,{children:["In Flarum, ",(0,s.jsx)(n.strong,{children:"Controller"})," is just another name for a class that implements ",(0,s.jsx)(n.a,{href:"https://github.com/php-fig/http-server-handler/blob/master/src/RequestHandlerInterface.php",children:"RequestHandlerInterface"}),". Put simply, a controller must implement a ",(0,s.jsx)(n.code,{children:"handle"})," method which receives a ",(0,s.jsx)(n.a,{href:"https://github.com/php-fig/http-message/blob/master/src/ServerRequestInterface.php",children:"Request"})," and must return a ",(0,s.jsx)(n.a,{href:"https://github.com/php-fig/http-message/blob/master/src/ResponseInterface.php",children:"Response"}),". Flarum includes ",(0,s.jsx)(n.a,{href:"https://github.com/laminas/laminas-diactoros",children:"laminas-diactoros"})," which contains ",(0,s.jsx)(n.code,{children:"Response"})," implementations that you can return."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-php",children:"<?php\n\nnamespace Acme\\HelloWorld;\n\nuse Laminas\\Diactoros\\Response\\HtmlResponse;\nuse Psr\\Http\\Message\\ResponseInterface as Response;\nuse Psr\\Http\\Message\\ServerRequestInterface as Request;\nuse Psr\\Http\\Server\\RequestHandlerInterface;\n\nclass HelloWorldController implements RequestHandlerInterface\n{\n public function handle(Request $request): Response\n {\n return new HtmlResponse('<h1>Hello, world!</h1>');\n }\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Controllers are resolved from the ",(0,s.jsx)(n.a,{href:"https://laravel.com/docs/12.x/container",children:"container"})," so you can inject dependencies into their constructors."]}),"\n",(0,s.jsxs)(n.admonition,{title:"What are Controllers?",type:"tip",children:[(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"handle"})," method of a Controller is the code that runs when someone visits your route (or sends data to it via a form submission). Generally speaking, Controller implementations follow the pattern:"]}),(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsx)(n.li,{children:"Retrieve information (GET params, POST data, the current user, etc) from the Request object."}),"\n",(0,s.jsx)(n.li,{children:"Do something with that information. For instance, if our controller handles a route for creating posts, we'll want to save a new post object to the database."}),"\n",(0,s.jsx)(n.li,{children:"Return a response. Most routes will return an HTML webpage, or a JSON api response."}),"\n"]})]}),"\n",(0,s.jsx)(n.h3,{id:"route-parameters",children:"Route Parameters"}),"\n",(0,s.jsxs)(n.p,{children:["Sometimes you will need to capture segments of the URI within your route. You may do so by defining route parameters using the ",(0,s.jsx)(n.a,{href:"https://github.com/nikic/FastRoute#defining-routes",children:"FastRoute"})," syntax:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-php",children:" (new Extend\\Routes('forum'))\n ->get('/user/{id}', 'acme.user', UserController::class)\n"})}),"\n",(0,s.jsxs)(n.p,{children:["The values of these parameters will be merged with the request's query params, which you can access in your controller by calling ",(0,s.jsx)(n.code,{children:"$request->getQueryParams()"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-php",children:"use Illuminate\\Support\\Arr;\n\n$id = Arr::get($request->getQueryParams(), 'id');\n"})}),"\n",(0,s.jsx)(n.h3,{id:"generating-urls",children:"Generating URLs"}),"\n",(0,s.jsxs)(n.p,{children:["You can generate URLs to any of the defined routes using the ",(0,s.jsx)(n.code,{children:"Flarum\\Http\\UrlGenerator"})," class. Inject an instance of this into your controller or view, and call the ",(0,s.jsx)(n.code,{children:"to"})," method to select a route collection. Then, you can generate a URL to a route using the name you gave it when it was defined. You can pass an array of parameters as the second argument. Parameters will fill in matching URI segments, otherwise they will be appended as query params."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-php",children:"$url = $this->url->to('forum')->route('acme.user', ['id' => 123, 'foo' => 'bar']);\n// http://yourforum.com/user/123?foo=bar\n"})}),"\n",(0,s.jsx)(n.h3,{id:"views",children:"Views"}),"\n",(0,s.jsxs)(n.p,{children:["You can inject Laravel's ",(0,s.jsx)(n.a,{href:"https://laravel.com/docs/12.x/views",children:"View"})," factory into your controller. This will allow you to render a ",(0,s.jsx)(n.a,{href:"https://laravel.com/docs/12.x/blade",children:"Blade template"})," into your controller's response."]}),"\n",(0,s.jsxs)(n.p,{children:["First, you will need to tell the view factory where it can find your extension's view files by adding a ",(0,s.jsx)(n.code,{children:"View"})," extender to ",(0,s.jsx)(n.code,{children:"extend.php"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-php",children:"use Flarum\\Extend;\nuse Illuminate\\Contracts\\View\\Factory;
1\n\nreturn [\n (new Extend\\View)\n ->namespace('acme.hello-world', __DIR__.'/views');\n];\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Then, inject the factory into your controller and render your view into an ",(0,s.jsx)(n.code,{children:"HtmlResponse"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-php",children:"class HelloWorldController implements RequestHandlerInterface\n{\n protected $view;\n \n public function __construct(Factory $view)\n {\n $this->view = $view;\n }\n \n public function handle(Request $request): Response\n {\n $view = $this->view->make('acme.hello-world::greeting');\n \n return new HtmlResponse($view->render());\n }\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"frontend-routes",children:"Frontend Routes"}),"\n",(0,s.jsxs)(n.p,{children:["Adding routes to the frontend actually requires you to register them on ",(0,s.jsx)(n.em,{children:"both"})," the frontend and the backend. This is because when your route is visited, the backend needs to know to serve up the frontend, and the frontend needs to know what to display on the page."]}),"\n",(0,s.jsxs)(n.p,{children:["On the backend, instead of adding your frontend route via the ",(0,s.jsx)(n.code,{children:"Routes"})," extender, you should use the ",(0,s.jsx)(n.code,{children:"Frontend"})," extender's ",(0,s.jsx)(n.code,{children:"route"})," method. This always assumes ",(0,s.jsx)(n.code,{children:"GET"})," as the method, and accepts a route path and name as the first two arguments:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-php",children:" (new Extend\\Frontend('forum'))\n ->route('/users', 'acme.users')\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Now when ",(0,s.jsx)(n.code,{children:"yourforum.com/users"})," is visited, the forum frontend will be displayed. However, since the frontend doesn't yet know about the ",(0,s.jsx)(n.code,{children:"users"})," route, the discussion list will still be rendered."]}),"\n",(0,s.jsxs)(n.p,{children:["Flarum builds on ",(0,s.jsx)(n.a,{href:"https://mithril.js.org/index.html#routing",children:"Mithril's routing system"}),", adding route names and an abstract class for pages (",(0,s.jsx)(n.code,{children:"common/components/Page"}),")."]}),"\n",(0,s.jsxs)(n.p,{children:["To register the route on the frontend, there is a ",(0,s.jsx)(n.code,{children:"Routes"})," extender which works much like the backend one. Instead of a controller, however, you pass a component instance as the third argument:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-jsx",children:"import Extend from 'flarum/common/extenders';\nimport FoobarPage from './components/FoobarPage';\n\nexport default [\n new Extend.Routes()\n .add('acme.foobar', '/foobar', FoobarPage),\n];\n"})}),"\n",(0,s.jsxs)(n.admonition,{type:"info",children:[(0,s.jsxs)(n.p,{children:["Remember to export the ",(0,s.jsx)(n.code,{children:"extend"})," module from your entry ",(0,s.jsx)(n.code,{children:"index.js"})," file:"]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"export { default as extend } from './extend';\n"})})]}),"\n",(0,s.jsxs)(n.p,{children:["Now when ",(0,s.jsx)(n.code,{children:"yourforum.com/users"})," is visited, the forum frontend will be loaded and the ",(0,s.jsx)(n.code,{children:"UsersPage"})," component will be rendered in the content area. For more information on frontend pages, please see ",(0,s.jsx)(n.a,{href:"/extend/frontend-pages",children:"that documentation section"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["Advanced use cases might also be interested in using ",(0,s.jsx)(n.a,{href:"/extend/frontend-pages#route-resolvers-advanced",children:"route resolvers"}),"."]}),"\n",(0,s.jsx)(n.h3,{id:"route-parameters-1",children:"Route Parameters"}),"\n",(0,s.jsx)(n.p,{children:"Frontend routes also allow you to capture segments of the URI:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-jsx",children:" new Extend.Routes()\n .add('acme.user', '/user/:id', UsersPage)\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Route parameters will be passed into the ",(0,s.jsx)(n.code,{children:"attrs"})," of the route's component. They will also be available through ",(0,s.jsx)(n.a,{href:"https://mithril.js.org/route.html#mrouteparam",children:(0,s.jsx)(n.code,{children:"m.route.param"})})]}),"\n",(0,s.jsx)(n.h3,{id:"route-resolvers",children:"Route Resolvers"}),"\n",(0,s.jsxs)(n.p,{children:["Optionally, the ",(0,s.jsx)(n.code,{children:"Routes"})," extender also allows passing a custom resolver class as the fourth argument when adding routes."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-jsx",children:"import Extend from 'flarum/common/extenders';\nimport FoobarUserPage from './components/FoobarUserPage';\nimport UserPageResolver from 'flarum/forum/resolvers/UserPageResolver';
1\n\nexport default [\n new Extend.Routes()\n .add('user.foobar', '/u/:username/foobar', FoobarUserPage, UserPageResolver),\n];\n\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Custom route resolvers let you control how Mithril identifies and keys route instances \u2014 for example, treating ",(0,s.jsx)(n.code,{children:"/d/5-wrong-slug"})," and ",(0,s.jsx)(n.code,{children:"/d/5-correct-slug"})," as the same page to prevent unnecessary remounts \u2014 and hook into the render lifecycle to perform side effects like auto-correcting URLs or triggering scroll-to-post behavior."]}),"\n",(0,s.jsxs)(n.p,{children:["Extensions that add user profile routes should use the ",(0,s.jsx)(n.code,{children:"UserPageResolver"})," to ensure consistent slug canonicalization across the multiple profile routes."]}),"\n",(0,s.jsx)(n.h3,{id:"generating-urls-1",children:"Generating URLs"}),"\n",(0,s.jsxs)(n.p,{children:["To generate a URL to a route on the frontend, use the ",(0,s.jsx)(n.code,{children:"app.route"})," method. This accepts two arguments: the route name, and a hash of parameters. Parameters will fill in matching URI segments, otherwise they will be appended as query params."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"const url = app.route('acme.user', { id: 123, foo: 'bar' });\n// http://yourforum.com/users/123?foo=bar\n"})}),"\n",(0,s.jsx)(n.p,{children:"The extender also allows you to define a route helper method:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:" new Extend.Routes()\n .add('acme.user', '/user/:id', <UsersPage />)\n .helper('acmeUser', (user) => app.route('acme.user', { id: user.id() }))\n"})}),"\n",(0,s.jsxs)(n.p,{children:["This allows you to generate URLs to the route using the ",(0,s.jsx)(n.code,{children:"acmeUser"})," helper method:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"const url = app.route.acmeUser(user);\n// http://yourforum.com/users/123\n"})}),"\n",(0,s.jsx)(n.h3,{id:"linking-to-other-pages",children:"Linking to Other Pages"}),"\n",(0,s.jsxs)(n.p,{children:["A forum wouldn't be very useful if it only had one page.\nWhile you could, of course, implement links to other parts of your forum with HTML anchor tags and hardcoded links, this can be difficult to maintain, and defeats the purpose of Flarum being a ",(0,s.jsx)(n.a,{href:"https://en.wikipedia.org/wiki/Single-page_application",children:"Single Page Application"})," in the first place."]}),"\n",(0,s.jsxs)(n.p,{children:["Flarum uses Mithril's routing API to provide a ",(0,s.jsx)(n.code,{children:"Link"})," component that neatly wraps links to other internal pages. Its use is fairly simple:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-jsx",children:'import Link from \'flarum/common/components/Link\';\n\n// Link can be used just like any other component:\n<Link href="/route/known/to/mithril">Hello World!</Link>\n\n// You\'ll frequently use Link with generated routes:\n<Link href={app.route(\'settings\')}>Hello World!</Link>\n\n// Link can even generate external links with the external attr:\n<Link external={true} href="https://google.com">Hello World!</Link>\n\n// The above example with external = true is equivalent to:\n<a href="https://google.com">Hello World!</a>\n// but is provided for flexibility: sometimes you might have links\n// that are conditionally internal or external.\n'})}),"\n",(0,s.jsx)(n.h2,{id:"content",children:"Content"}),"\n",(0,s.jsx)(n.p,{children:"Whenever you visit a frontend route, the backend constructs a HTML document with the scaffolding necessary to boot up the frontend JavaScript application. You can easily modify this document to perform tasks like:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["Changing the ",(0,s.jsx)(n.code,{children:"<title>"})," of the page"]}),"\n",(0,s.jsx)(n.li,{children:"Adding external JavaScript and CSS resources"}),"\n",(0,s.jsxs)(n.li,{children:["Adding SEO content and ",(0,s.jsx)(n.code,{children:"<meta>"})," tags"]}),"\n",(0,s.jsx)(n.li,{children:"Adding data to the JavaScript payload (eg. to preload resources which are going to be rendered on the page immediately, thereby preventing an unnecessary request to the API)"}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["You can make blanket changes to the frontend using the ",(0,s.jsx)(n.code,{children:"Frontend"})," extender's ",(0,s.jsx)(n.code,{children:"content"})," method. This accepts a closure which receives two parameters: a ",(0,s.jsx)(n.code,{children:"Flarum\\Frontend\\Document"})," object which represents the HTML document that will be displayed, and the ",(0,s.jsx)(n.code,{children:"Request"})," object."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-php",children:"use Flarum\\Frontend\\Document;\nuse Psr\\Http\\Message\\ServerRequestInterface as Request;\n\nreturn [\n (new Extend\\Frontend('forum'))\n ->content(function (Document $document, Request $request) {\n $document->head[] = '<script>alert(\"Hello, world!\")<\/script>';\n })\n];\n"})}),"\n",(0,s.jsx)(n.p,{children:"You can also add content onto your frontend route registrations:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-php",children:"return [\n (new Extend\\Frontend('forum'))\n ->route('/users', 'acme.users', function (Document $document, Request $request) {\n $document->title = 'Users';\n })\n];\n"})})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},8453(e,n,t){t.d(n,{R:()=>a,x:()=>l});var r=t(6540);const s={},o=r.createContext(s);function a(e){const n=r.useContext(o);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),r.createElement(o.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.