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

The built-in member screens

The screens the platform serves under /my-account: each one's route name, the template it renders and the variables it receives, which ones the framework ships a template for, and the account menu they share.

Who this is for. The theme developer restyling a member area. Afterwards you know which template to write for each screen, what it receives, and which ones need a template from you.

The screens

Paths are after /my-account. Route names belong to the MyAccount module, as in $_url->getPath('MyAccount', 'ordersListing'). Templates are paths under templates/, and a file at the same path in your site replaces the framework's (Template resolution order).

PathRouteTemplateVariablesFramework copy
/accountusers/users/show.htmluser; orders and quotes, two helpers with getRecentOrders(n) and getRecentQuotes(n)Yes
/addresses, /addresses/{page}addressesusers/person-addresses/index.htmlpersonAddressesWrite your own (below)
/address (GET form, POST to add)addAddressusers/person-addresses/add.htmlcountries, typesYes
/address/{id} (GET)addressusers/person-addresses/show.htmlpersonAddress (also as record), countries, typesYes
/address/{id} (PUT, DELETE)editAddress, deleteAddressNone: they redirect
/orders, /orders/{page}ordersListingorders/orders/index.htmlordersYes
/order/{id}orderDetailsorders/orders/show.htmlorder (also as record)Yes
/quotes, /quotes/{page}quotesListingquotes/quotes/index.htmlquotesNo
/quote/{id}quoteDetailsquotes/quotes/show.htmlquote (also as record)No
/wishlist, /wishlist/{page}listWishlistproducts/wishlist/index.htmlwishlistNo
/wishlist/{id} (DELETE)deleteWishlistNone: it redirects
/linked-accountslistUserAuthenticatorso-auth/user-authenticators/index.htmlenabledAuthenticators, userAuthenticatorsNo
/passkeyspasskeysusers/passkeys/index.htmlpasskeys, passkeyEnabled, passkeyAllowed, hasEmailYes
/applicationsapplicationso-auth/user-o-auth-clients/index.htmluserOAuthClientsNo
/current-activitycurrentActivityusers/users/current-activity.htmlcurrentActivityYes

Every screen requires a signed-in visitor, and every one except current activity also checks the CSRF token on writes. The lists are the signed-in member's own records: their addresses, their orders, their quotes. The screens for sign-in methods, passkeys and applications are covered in Authenticators, SSO and linked accounts; the address book in Addresses and the address book.

Which screens need a template from you

A screen with no template at its path, in your site or in the framework, does not fail with an error on a Smarty site. It answers HTTP 200 with a page that reads only Template not found: quotes/quotes/index.html, without your header or footer. On a Twig site it is an error. So:

  • Write the four that have no framework copy before linking to them: quotes (list and detail), the wishlist, linked accounts and authorised applications. The framework does have files under users/user-authenticators/ and users/user-o-auth-clients/, but the routes look under o-auth/ and never read them.
  • Write your own address book list before offering the address book, rather than relying on the framework's copy.
  • Treat the framework copies as a starting point, not a design. They are plain Smarty with bootstrap-style class names and extend the framework's bare includes/frontend/base.html. On a Twig site they are Smarty source and cannot render, so a Twig site writes every screen it offers.

The account menu

Every framework member screen extends users/frontend/includes/account.html, which draws a side menu and gives each screen a block called accountContent. The menu links the account home, applications, current activity, linked accounts, passkeys, addresses, orders and quotes, in that order; the wishlist is not in it. Override that one file and every framework screen you keep picks up your menu. Take out the links to screens your site does not offer, since a link to quotes on a site without a quotes template leads to the "Template not found" page.

Template code templates/users/frontend/includes/account.html
{extends 'includes/frontend/base.html'}
{block 'content'}
  <nav class="account-menu">
    <a href="{$_url->getPath('MyAccount', 'account')}"{if $_url->isCurrent('MyAccount', 'account')} aria-current="page"{/if}>Your account</a>
    <a href="{$_url->getPath('MyAccount', 'ordersListing')}"{if $_url->isCurrent('MyAccount', 'ordersListing')} aria-current="page"{/if}>Orders</a>
    <a href="{$_url->getPath('MyAccount', 'addresses')}"{if $_url->isCurrent('MyAccount', 'addresses')} aria-current="page"{/if}>Addresses</a>
    <a href="{$_url->getPath('logout')}">Sign out</a>
  </nav>
  <main>{block 'accountContent'}{/block}</main>
{/block}
{% extends 'includes/frontend/base.html' %}
{% block content %}
  <nav class="account-menu">
    <a href="{{ _url.getPath('MyAccount', 'account') }}"{% if _url.isCurrent('MyAccount', 'account') %} aria-current="page"{% endif %}>Your account</a>
    <a href="{{ _url.getPath('MyAccount', 'ordersListing') }}"{% if _url.isCurrent('MyAccount', 'ordersListing') %} aria-current="page"{% endif %}>Orders</a>
    <a href="{{ _url.getPath('MyAccount', 'addresses') }}"{% if _url.isCurrent('MyAccount', 'addresses') %} aria-current="page"{% endif %}>Addresses</a>
    <a href="{{ _url.getPath('logout') }}">Sign out</a>
  </nav>
  <main>{% block accountContent %}{% endblock %}</main>
{% endblock %}

Use your own base template and block names. Your own catch-all pages can extend the same file, so the custom pages and the built-in ones share one menu (Adding a custom member page).

The screens that change something

Adding, editing and deleting an address and removing a wishlist item are form posts to the routes above, each carrying the CSRF token as a _csrf field. PUT and DELETE are sent as a POST with a _method field. None of them renders a page: each redirects with a message, to the address list after adding or deleting, to the address after editing, to the wishlist after removing. A failed save redirects the same way with the error as the message, so your templates need to print the site's messages for the member to see it.

Template code Deleting an address from the list
{foreach $personAddresses as $saved}
  <form method="post" action="{$_url->getPath('MyAccount', 'deleteAddress', ['id' => $saved->getId()])}">
    <input type="hidden" name="_method" value="DELETE">
    <input type="hidden" name="_csrf" value="{$_csrfToken}">
    <button>Delete</button>
  </form>
{/foreach}
{% for saved in personAddresses %}
  <form method="post" action="{{ _url.getPath('MyAccount', 'deleteAddress', {'id': saved.getId()}) }}">
    <input type="hidden" name="_method" value="DELETE">
    <input type="hidden" name="_csrf" value="{{ _csrfToken }}">
    <button>Delete</button>
  </form>
{% endfor %}
Go deeper

Was this page helpful?

Your answer goes to the docs team.