PageSourceSearch

https://docs.flarum.org/assets/js/cbafab31.658cde4c.js

js flarum.org collected 2026-09-24 18:15:15 UTC 17,622 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunkflarum_docs=globalThis.webpackChunkflarum_docs||[]).push([[3008],{4140(e,n,s){s.r(n),s.d(n,{assets:()=>l,contentTitle:()=>i,default:()=>u,frontMatter:()=>a,metadata:()=>t,toc:()=>c});const t=JSON.parse('{"id":"extend/frontend-pages","title":"Frontend Pages and Resolvers","description":"As explained in the Routes and Content documentation, we can use Mithril\'s routing system to show different components for different routes. Mithril allows you to use any component you like, even a Modal or Alert, but we recommend sticking to component classes that inherit the Page component.","source":"@site/docs/extend/frontend-pages.md","sourceDirName":"extend","slug":"/extend/frontend-pages","permalink":"/extend/frontend-pages","draft":false,"unlisted":false,"editUrl":"https://github.com/flarum/docs/tree/main/docs/extend/frontend-pages.md","tags":[],"version":"current","frontMatter":{},"sidebar":"extendSidebar","previous":{"title":"Authorization","permalink":"/extend/authorization"},"next":{"title":"Interactive Components","permalink":"/extend/interactive-components"}}');var o=s(4848),r=s(8453);const a={},i="Frontend Pages and Resolvers",l={},c=[{value:"The Page Component",id:"the-page-component",level:2},{value:"Forum Page Structure",id:"forum-page-structure",level:3},{value:"Setting Page as Homepage",id:"setting-page-as-homepage",level:3},{value:"Page Titles",id:"page-titles",level:3},{value:"PageState",id:"pagestate",level:2},{value:"Admin Pages",id:"admin-pages",level:2},{value:"Route Resolvers (Advanced)",id:"route-resolvers-advanced",level:2},{value:"Using Route Resolvers",id:"using-route-resolvers",level:3},{value:"Custom Resolvers",id:"custom-resolvers",level:3}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.header,{children:(0,o.jsx)(n.h1,{id:"frontend-pages-and-resolvers",children:"Frontend Pages and Resolvers"})}),"\n",(0,o.jsxs)(n.p,{children:["As explained in the ",(0,o.jsx)(n.a,{href:"/extend/routes#frontend-routes",children:"Routes and Content"})," documentation, we can use Mithril's routing system to show different ",(0,o.jsx)(n.a,{href:"/extend/frontend#components",children:"components"})," for different routes. Mithril allows you to use any component you like, even a Modal or Alert, but we recommend sticking to component classes that inherit the ",(0,o.jsx)(n.code,{children:"Page"})," component."]}),"\n",(0,o.jsx)(n.h2,{id:"the-page-component",children:"The Page Component"}),"\n",(0,o.jsxs)(n.p,{children:["We provide ",(0,o.jsx)(n.code,{children:"flarum/common/components/Page"})," as a base class for pages in both the ",(0,o.jsx)(n.code,{children:"admin"})," and ",(0,o.jsx)(n.code,{children:"forum"})," frontends. It has a few benefits:"]}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["Automatically updates ",(0,o.jsxs)(n.a,{href:"#pagestate",children:[(0,o.jsx)(n.code,{children:"app.current"})," and ",(0,o.jsx)(n.code,{children:"app.previous"})," PageState"]})," when switching from one route to another."]}),"\n",(0,o.jsx)(n.li,{children:"Automatically closes the modal and drawer when switching from one route to another."}),"\n",(0,o.jsxs)(n.li,{children:["Applies ",(0,o.jsx)(n.code,{children:"this.bodyClass"})," (if defined) to the '#app' HTML element when the page renders."]}),"\n",(0,o.jsx)(n.li,{children:"It's also good for consistency's sake to use a common base class for all pages."}),"\n",(0,o.jsxs)(n.li,{children:["If the page's ",(0,o.jsx)(n.code,{children:"scrollTopOnCreate"})," attribute is set to ",(0,o.jsx)(n.code,{children:"false"})," in ",(0,o.jsx)(n.code,{children:"oninit"}),", the page won't be scrolled to the top when changed."]}),"\n",(0,o.jsxs)(n.li,{children:["If the page's ",(0,o.jsx)(n.code,{children:"useBrowserScrollRestoration"})," is set to ",(0,o.jsx)(n.code,{children:"false"})," in ",(0,o.jsx)(n.code,{children:"oninit"}),", the browser's automatic scroll restoration won't be used on that page."]}),"\n"]}),"\n",(0,o.jsx)(n.p,{children:"Page components work just like any other inherited component. For a (very simple) example:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"import Page from 'flarum/common/components/Page';\n\n\nexport default class CustomPage extends Page {\n  view() {\n    return <p>Hello!</p>\n  }\n}\n"})}),"\n",(0,o.jsx)(n.h3,{id:"forum-page-structure",children:"Forum Page Structure"}),"\n",(0,o.jsxs)(n.p,{children:["Flarum's forum frontend uses a generic page structure, which is defined in ",(0,o.jsx)(n.code,{children:"flarum/forum/components/PageStructure"}),". This structure is used by all forum pages, and is recommended for use in extensions as well. You will have noticed that each forum page has a hero, sidebar, and content area among other things. These are all defined in ",(0,o.jsx)(n.code,{children:"PageStructure"})," and can be used in your extension as well."]}),"\n",(0,o.jsxs)(n.p,{children:["For example, a custom page component can use the ",(0,o.jsx)(n.code,{children:"PageStructure"})," component as follows:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-tsx",children:"import PageStructure from 'flarum/forum/components/PageStructure';
1\n\nexport default class AcmePage extends Page {\n  view() {\n    return (\n      <PageStructure\n        className=\"AcmePage\" // Optional but recommended.\n        hero={() => <CustomHero />} // Optional. Extends `flarum/forum/components/Hero`\n        sidebar={() => <div>Custom Sidebar</div>} // Optional.\n        loading={this.loading} // Optional.\n      >\n        <div>Custom Content</div>\n      </PageStructure>\n    );\n  }\n}\n"})}),"\n",(0,o.jsxs)(n.admonition,{type:"info",children:[(0,o.jsxs)(n.mdxAdmonitionTitle,{children:["Why use ",(0,o.jsx)(n.code,{children:"PageStructure"}),"?"]}),(0,o.jsxs)(n.p,{children:["Using ",(0,o.jsx)(n.code,{children:"PageStructure"})," is not required, but it is recommended. It provides a consistent structure for all pages, and allows other extensions such as themes to extend and customize pages more easily."]})]}),"\n",(0,o.jsx)(n.h3,{id:"setting-page-as-homepage",children:"Setting Page as Homepage"}),"\n",(0,o.jsxs)(n.p,{children:["Flarum uses a setting to determine which page should be the homepage: this gives admins flexibility to customize their communities.\nTo add your custom page to the homepage options in Admin, you'll need to extend the ",(0,o.jsx)(n.code,{children:"BasicsPage.homePageItems"})," method with your page's path."]}),"\n",(0,o.jsxs)(n.p,{children:["An example from the ",(0,o.jsx)(n.a,{href:"https://github.com/flarum/tags/blob/master/js/src/admin/addTagsHomePageOption.js",children:"Tags extension"}),":"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"import { extend } from 'flarum/common/extend';\nimport BasicsPage from 'flarum/admin/components/BasicsPage';\n\nexport default function() {\n  extend(BasicsPage, 'homePageItems', items => {\n    items.add('tags', {\n      path: '/tags',\n      label: app.translator.trans('flarum-tags.admin.basics.tags_label')\n    });\n  });\n}\n"})}),"\n",(0,o.jsxs)(n.p,{children:["To learn how to set up a path/route for your custom page, see the ",(0,o.jsx)(n.a,{href:"/extend/routes",children:"relevant documentation"}),"."]}),"\n",(0,o.jsx)(n.h3,{id:"page-titles",children:"Page Titles"}),"\n",(0,o.jsx)(n.p,{children:"Often, you'll want some custom text to appear in the browser tab's title for your page.\nFor instance, a tags page might want to show \"Tags - FORUM NAME\", or a discussion page might want to show the title of the discussion."}),"\n",(0,o.jsxs)(n.p,{children:["To do this, your page should include calls to ",(0,o.jsx)(n.code,{children:"app.setTitle()"})," and ",(0,o.jsx)(n.code,{children:"app.setTitleCount()"})," in its ",(0,o.jsx)(n.code,{children:"oncreate"})," ",(0,o.jsx)(n.a,{href:"/extend/frontend",children:"lifecycle hook"})," (or when data is loaded, if it pulls in data from the API)."]}),"\n",(0,o.jsx)(n.p,{children:"For example:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:'import Page from \'flarum/common/components/Page\';\n\n\nexport default class CustomPage extends Page {\n  oncreate(vnode) {\n    super.oncreate(vnode);\n\n    app.setTitle("Cool Page");\n    app.setTitleCount(0);\n  }\n\n  view() {\n    // ...\n  }\n}\n\nexport default class CustomPageLoadsData extends Page {\n  oninit(vnode) {\n    super.oninit(vnode);\n\n    app.store.find("users", 1).then(user => {\n      app.setTitle(user.displayName());\n      app.setTitleCount(0);\n    })\n  }\n\n  view() {\n    // ...\n  }\n}\n'})}),"\n",(0,o.jsxs)(n.p,{children:["Please note that if your page is ",(0,o.jsx)(n.a,{href:"#setting-page-as-homepage",children:"set as the homepage"}),", ",(0,o.jsx)(n.code,{children:"app.setTitle()"})," will clear the title for simplicity.\nIt should still be called though, to prevent titles from previous pages from carrying over."]}),"\n",(0,o.jsx)(n.h2,{id:"pagestate",children:"PageState"}),"\n",(0,o.jsxs)(n.p,{children:["Sometimes, we want to get information about the page we're currently on, or the page we've just come from.\nTo allow this, Flarum creates (and stores) instances of ",(0,o.jsx)(n.a,{href:"https://api.docs.flarum.org/js/2.x/classes/flarum.common_states_pagestate.pagestate",children:(0,o.jsx)(n.code,{children:"PageState"})})," as ",(0,o.jsx)(n.code,{children:"app.current"})," and ",(0,o.jsx)(n.code,{children:"app.previous"}),".\nThese store:"]}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsx)(n.li,{children:"The component class being used for the page"}),"\n",(0,o.jsx)(n.li,{children:"A collection of data that each page sets about itself. The current route name is always included."}),"\n"]}),"\n",(0,o.jsx)(n.p,{children:"Data can be set to, and retrieved from, Page State using:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"app.current.set(KEY, DATA);\napp.current.get(KEY);\n"})}),"\n",(0,o.jsxs)(n.p,{children:["For example, this is how the Discussion Page makes its ",(0,o.jsx)(n.a,{href:"https://api.docs.flarum.org/js/2.x/classes/flarum.forum_states_poststreamstate.poststreamstate",children:(0,o.jsx)(n.code,{children:"PostStreamState"})})," instance globally available."]}),"\n",(0,o.jsxs)(n.p,{children:["You can also check the type and data of a page using ",(0,o.jsx)(n.code,{children:"PageState"}),"'s ",(0,o.jsx)(n.code,{children:"matches"})," method. For instance, if we want to know if we are currently on a discu
1ssion page:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-jsx",children:"import IndexPage from 'flarum/forum/components/IndexPage';\nimport DiscussionPage from 'flarum/forum/components/DiscussionPage';\n\n// To just check page type\napp.current.matches(DiscussionPage);\n\n// To check page type and some data\napp.current.matches(IndexPage, {routeName: 'following'});\n"})}),"\n",(0,o.jsx)(n.h2,{id:"admin-pages",children:"Admin Pages"}),"\n",(0,o.jsxs)(n.p,{children:["See the ",(0,o.jsx)(n.a,{href:"/extend/admin",children:"Admin Dashboard documentation"})," for more information on tools specifically available to admin pages (and how to override the admin page for your extension)."]}),"\n",(0,o.jsx)(n.h2,{id:"route-resolvers-advanced",children:"Route Resolvers (Advanced)"}),"\n",(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.a,{href:"https://mithril.js.org/route.html#advanced-component-resolution",children:"Advanced use cases"})," can take advantage of Mithril's ",(0,o.jsx)(n.a,{href:"https://mithril.js.org/route.html#routeresolver",children:"route resolver system"}),".\nFlarum actually already wraps all its components in the ",(0,o.jsx)(n.code,{children:"flarum/common/resolvers/DefaultResolver"})," resolver. This has the following benefits:"]}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["It passes a ",(0,o.jsx)(n.code,{children:"routeName"})," attr to the current page, which then provides it to ",(0,o.jsx)(n.code,{children:"PageState"})]}),"\n",(0,o.jsxs)(n.li,{children:["It assigns a ",(0,o.jsx)(n.a,{href:"https://mithril.js.org/keys.html#single-child-keyed-fragments",children:"key"})," to the top level page component. When the route changes, if the top level component's key has changed, it will be completely rerendered (by default, Mithril does not rerender components when switching from one page to another if both are handled by the same component)."]}),"\n"]}),"\n",(0,o.jsx)(n.h3,{id:"using-route-resolvers",children:"Using Route Resolvers"}),"\n",(0,o.jsx)(n.p,{children:"There are actually 3 ways to set the component / route resolver when registering a route:"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["the ",(0,o.jsx)(n.code,{children:"resolver"})," key can be used to provide an ",(0,o.jsx)(n.strong,{children:"instance"})," of a route resolver. This instance should define which component should be used, and hardcode the route name to be passed into it. This instance will be used without any modifications by Flarum."]}),"\n",(0,o.jsxs)(n.li,{children:["The ",(0,o.jsx)(n.code,{children:"resolverClass"})," key AND ",(0,o.jsx)(n.code,{children:"component"})," key can be used to provide a ",(0,o.jsx)(n.strong,{children:"class"})," that will be used to instantiate a route resolver, to be used instead of Flarum's default one, as well as the component to use. Its constructor should take 2 arguments: ",(0,o.jsx)(n.code,{children:"(component, routeName)"}),"."]}),"\n",(0,o.jsxs)(n.li,{children:["The ",(0,o.jsx)(n.code,{children:"component"})," key can be used alone to provide a component. This will result in the default behavior."]}),"\n"]}),"\n",(0,o.jsx)(n.p,{children:"For example:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"// See above for a custom page example\nimport CustomPage from './components/CustomPage';\n// See below for a custom resolver example\nimport CustomPageResolver from './resolvers/CustomPageResolver';\n\n// Use a route resolver instance\napp.routes['resolverInstance'] = {path: '/custom/path/1', resolver: {\n  onmatch: function(args) {\n    if (!app.session.user) return m.route.SKIP;\n\n    return CustomPage;\n  }\n}};\n\n// Use a custom route resolver class\napp.routes['resolverClass'] = {path: '/custom/path/2', resolverClass: CustomPageResolver, component: CustomPage};\n\n// Use the default resolver class (`flarum/common/resolvers/DefaultResolver`)\napp.routes['resolverClass'] = {path: '/custom/path/2', component: CustomPage};\n"})}),"\n",(0,o.jsx)(n.h3,{id:"custom-resolvers",children:"Custom Resolvers"}),"\n",(0,o.jsxs)(n.p,{children:["We strongly encourage custom route resolvers to extend ",(0,o.jsx)(n.code,{children:"flarum/common/resolvers/DefaultResolver"}),".\nFor example, Flarum's ",(0,o.jsx)(n.code,{children:"flarum/forum/resolvers/DiscussionPageResolver"})," assigns the same key to all links to the same discussion (regardless of the current post), and triggers scrolling when using ",(0,o.jsx)(n.code,{children:"m.route.set"})," to go from one post to another on the same discussion page:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-js",children:"import DefaultResolver from '../../common/resolvers/DefaultResolver';\n\n/**\n * This isn't exported as it is a temporary measure.\n * A more robust system will be implemented alongside UTF-8 support in beta 15.\n */\nfunction getDiscussionIdFromSlug(slug: string | undefined) {\n  if (!slug) return;\n  return slug.split('-')[0];\n}\n\n/**\n * A custom route resolver for DiscussionPage that generates the same key to all posts\n * on the same discussion. It triggers a scroll when going from one post to another\n * in the same discussion.\n */\nexport default class DiscussionPageResolver extends DefaultResolver {\n  static scrollToPostNumber: number | null = null;\n\n  makeKey() {\n    const params = { ...m.route.param() };\n    if ('near' in params) {\n      delete params.near;\n    }\n    params.id = getDiscussionIdFromSlug(params.id);\n    return this.routeName.replace('.near', '') + JSON.stringify(params);\n  }\n\n  onmatch(args, requestedPath, route) {\n    if (route.includes('/d/:id') && getDiscussionIdFromSlug(args.id) === getDiscussionIdFromSlug(m.route.param('id'))) {\n      DiscussionPageResolver.scrollToPostNumber = parseInt(args.near);\n    }\n\n    return super.onmatch(args, requestedPath, route);\n  }\n\n  render(vnode) {\n    if (DiscussionPageResolver.scrollToPostNumber !== null) {\n      const number = DiscussionPageResolver.scrollToPostNumber;\n      // Scroll after a timeout to avoid clashes with the render.\n      setTimeout(() => app.current.get('stream').goToNumber(number));\n      DiscussionPageResolver.scrollToPostNumber = null;\n    }\n\n    return super.render(vnode);\n  }\n}\n"})})]})}function u(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},8453(e,n,s){s.d(n,{R:()=>a,x:()=>i});var t=s(6540);const o={},r=t.createContext(o);function a(e){const n=t.useContext(r);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:a(e.components),t.createElement(r.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.