Guide 07 · Developer docs

Members, accounts and gated content

Building a member area on Core dna: who a member is in the data, the sign-in flows and their templates, single sign-on and passkeys, the my-account routing convention, members-only content, sessions and the page cache, and the security checks to run before launch.

L2
Unit 04 · The my-account area and its routing convention

How the member area is routed

Every URL under /my-account goes through one dispatcher: named routes for the built-in screens, then a catch-all that turns a template in templates/my-account/ into a members-only page. The mapping rule, what wins when names collide, and what the template receives.

Who this is for. The theme developer adding a page to a member area, or working out why a template under templates/my-account/ renders or does not. Afterwards you can tell, from a URL alone, which template answers it.

One dispatcher, named routes and a catch-all

The member area is the MyAccount module. Its dispatcher holds a list of named routes, one for each built-in screen (the account home, addresses, orders, quotes, the wishlist, linked accounts, passkeys, authorised applications and current activity), and one last route, default, that accepts whatever path is left. A request is matched against the routes in that order, and the first match answers it. The built-in member screens lists the named routes with their templates.

Every route in the list, the catch-all included, requires someone to be signed in. A signed-out visitor is redirected to /login with the message "You have to log in to view this page", and the URL they asked for is kept in their session; once they sign in, they are sent back to it.

/my-account is the default prefix. The platform team can change it for a site, so link with the URL helper rather than writing the prefix into templates (below).

The catch-all

Any path under /my-account that no named route takes is mapped onto a template of the same name:

/my-account/<path> renders templates/my-account/<path>.html, from your site's templates folder.

URLTemplate
/my-account/downloadstemplates/my-account/downloads.html
/my-account/courses/certificatestemplates/my-account/courses/certificates.html

The rest follows from the rule:

  • There is no index file. /my-account/courses looks for my-account/courses.html, never my-account/courses/index.html. A file and a folder of the same name can sit side by side: courses.html answers /my-account/courses, and courses/certificates.html the longer path.
  • Only your site's folder counts. The controller looks for the file in the site's templates/ folder and nowhere else. A framework template at the same path is never used.
  • Case matters. The file is looked up on a case-sensitive file system, so /my-account/Downloads does not find downloads.html. Keep URLs and file names in lower case.
  • No full stops. The catch-all's pattern does not accept a dot, so /my-account/downloads.html matches no route at all and is a 404 before sign-in is even checked. Use letters, digits, hyphens, underscores and slashes.
  • GET only. The catch-all's controller answers GET and refuses any other method (When the catch-all is not enough).
Every file under templates/my-account/ is a page

Any .html file in that folder, at any depth, answers a URL for every signed-in member, partials included: my-account/includes/card.html is served on its own at /my-account/includes/card. Keep partials somewhere else, such as templates/includes/my-account/, and include them from there.

A named route always wins

The named routes are tried first, so a path one of them takes never reaches the catch-all. A template named after one of them never renders. The built-in screen answers instead or, if your site has not written that screen's template, a page that reads only Template not found: quotes/quotes/index.html (on a Smarty site; on a Twig site, an error). Either is the symptom of a collision.

These paths are taken. Do not use them, or a first folder of the same name, for your own pages:

Path after /my-accountTaken by
/The account home
/addresses, /addresses/<one segment>The address book; the segment is read as a page number
/address, /address/<number>Adding, showing, editing and deleting an address
/orders, /orders/<one segment>The orders list
/order/<number>One order
/quotes, /quotes/<one segment>The quotes list
/quote/<number>One quote
/wishlist, /wishlist/<one segment>The wishlist
/linked-accountsLinked accounts
/passkeysPasskeys
/applicationsAuthorised applications
/current-activitySigned-in sessions

The list routes take any single segment as the page number, not only a number. /my-account/orders/archived is the orders list asked for page "archived", and my-account/orders/archived.html is never read.

A missing template is a 404

When the catch-all finds no file at the mapped path, it answers 404, rendered with your site's errors/not-found.html like any other missing page. It never renders a blank page. A typo in a link or a file name therefore shows on the first click, and a 404 under /my-account for a signed-in member means one thing: there is no file at templates/my-account/<path>.html on the server. Check the path, its case and whether the file was deployed. (A signed-out visitor never gets that far: they are sent to sign in first.)

The two failures look different on purpose. A missing catch-all template is a 404; a named route whose template is missing answers 200 with the "Template not found" line above.

What your template receives

One variable, user: the signed-in user's record, with getPerson() for their name and contact details. Every global is there as well (Global template variables), including _currentUser and _csrfToken.

The template needs no sign-in check. The route has already redirected anyone who is not signed in, so by the time the template runs, user is always set. A check of your own is redundant, not defensive: it can never fail.

Template code templates/my-account/welcome.html
{$person = $user->getPerson()}
<h1>Hello, {$person->getFirstName()|escape}</h1>
<p>You are signed in as {$user->getUsername()|escape}.</p>
<p><a href="{$_url->getPath('MyAccount', 'account')}">Back to your account</a></p>
{% set person = user.getPerson() %}
<h1>Hello, {{ person.getFirstName()|e }}</h1>
<p>You are signed in as {{ user.getUsername()|e }}.</p>
<p><a href="{{ _url.getPath('MyAccount', 'account') }}">Back to your account</a></p>

Wrap it in your site's base template as any page is (Adding a custom member page shows the whole file). Neither engine escapes output on this platform, so escape what a member typed.

The route passes nothing from the URL into the template. Read the query string with $_request->getParameter('tab') (_request.getParameter('tab') in Twig), and fetch records with $_query. Nothing rendered for a signed-in visitor is stored in the page cache, so a member page is always rendered fresh (Member pages and the page cache).

Linking into the member area

Link to a built-in screen by its route name in the MyAccount module, and to your own pages from the module's path, so the links follow the prefix if it is ever changed:

Template code
<a href="{$_url->getPath('MyAccount', 'ordersListing')}">Your orders</a>
<a href="{$_url->getPath('MyAccount', 'orderDetails', ['id' => $order->getId()])}">Order {$order->getId()}</a>
<a href="{$_url->getModulePath('MyAccount')}/downloads">Downloads</a>
<a href="{{ _url.getPath('MyAccount', 'ordersListing') }}">Your orders</a>
<a href="{{ _url.getPath('MyAccount', 'orderDetails', {'id': order.getId()}) }}">Order {{ order.getId() }}</a>
<a href="{{ _url.getModulePath('MyAccount') }}/downloads">Downloads</a>
Go deeper

Was this page helpful?

Your answer goes to the docs team.