Skip to content

Instantly share code, notes, and snippets.

@julrich
Last active August 4, 2020 12:51
Show Gist options
  • Save julrich/d91c1ac1968418e6ae8290f2a2e90afb to your computer and use it in GitHub Desktop.
Save julrich/d91c1ac1968418e6ae8290f2a2e90afb to your computer and use it in GitHub Desktop.
Fully cached TYPO3 HMENU navigation example with expAll and 'active', 'current' states
#
# Main navigation in Sidebar
#
# General idea: Don't render & cache 'active' and 'current' states in 'expAll' menu, so it becomes cacheable
# over all pages. To regain 'active' and 'current' states, the result of the cached menu is parsed by
# 'stdWrap.replacement', utilizing specific information about the resulting menu item markup to insert them.
# Use COA to decouple the stdWrap ('lib.navSidebar.stdWrap.replacement') needed for RegExp replacement from
# the cached menu ('lib.navSidebar.10'). This way the complete menu can be generically cached without current
# and active states, but the stdWrap is still run, re-adding those
lib.navSidebar = COA
# Definition of the general menu object
lib.navSidebar.10 = HMENU
lib.navSidebar.10 {
cache {
# Use unique key for combination of '$page.uid.root' (current instance, multi-site specific)
# and the chosen language ('TSFE:sys_language_uid')
key = navSidebar-{$page.uid.root}-{TSFE:sys_language_uid}
key.insertData = 1
# 'lifetime = default' in this case refers to config.cache_period = 43200 (12 hours)
lifetime = default
}
# Start at the root of the page
entryLevel = 0
# Actual menu, crucially we render 'page_{field:uid}', e.g. 'page_123', into each relevant
# item. This gives us the option to later replace those uniquely identifiable strings to include
# additional classes like 'active' or 'current'
1 = TMENU
1 {
# 'expAll = 1' to expand all nodes recursively
expAll = 1
# Markup for items that have no submenu-items
NO = 1
NO {
wrapItemAndSub = <li class="page_{field:uid} nav-sidebar__list__item">|</li>
wrapItemAndSub.insertData = 1
ATagTitle.field = title // subtitle
ATagParams = tabindex="0"
}
# Markup for items that have submenu-items
IFSUB = 1
IFSUB {
wrapItemAndSub = <li class="page_{field:uid} nav-sidebar__list__item nav-sidebar__list__item--has-submenu">|</li>
wrapItemAndSub.insertData = 1
before = <span id="nav-sidebar_{field:uid}" role="button" aria-haspopup="true" aria-owns="nav-sidebar__submenu_{field:uid}" aria-controls="nav-sidebar__submenu_{field:uid}" aria-expanded="false">
before.insertData = 1
after = <svg class="nav-sidebar__icon"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#icon-arrow-down"></use></svg></span>
after.insertData = 1
doNotLinkIt = 1
}
}
# Additionally add a class denoting the level for subsequent menu levels (5 supported overall right now)
# Submenu Level 2
2 < .1
2 = TMENU
2 {
stdWrap.dataWrap = <ul class="nav-sidebar__submenu nav-sidebar__submenu--level-2">|</ul>
}
# Submenu Level 3
3 < .1
3 = TMENU
3 {
stdWrap.dataWrap = <ul class="nav-sidebar__submenu nav-sidebar__submenu--level-3">|</ul>
}
# Submenu Level 4
4 < .1
4 = TMENU
4 {
stdWrap.dataWrap = <ul class="nav-sidebar__submenu nav-sidebar__submenu--level-4">|</ul>
}
# Submenu Level 5
5 < .1
5 = TMENU
5 {
stdWrap.dataWrap = <ul class="nav-sidebar__submenu nav-sidebar__submenu--level-5">|</ul>
}
}
# Add replacement stdWrap, to augment the uniquely identifable strings ('page_{field:uid} nav', e.g. 'page_123 nav'),
# embedded with the classes=".." of each menuitem, with additional classes for 'active' and 'current' states.
# the classes
# Don't add replacement function for pages where the resulting regular expression would be empty, triggering an error
# Those currently are the root page itself, and all publicly visible pages, that are not children of the root page.
[globalVar = TSFE:id={$page.uid.root}] || [globalVar = TSFE:id={$page.uid.404}] || [globalVar = TSFE:id={$page.uid.noTranslation}] || [globalVar = TSFE:id={$page.uid.search}]
# Do nothing here, we just need the negation
[else]
# Also see 'replacement' TypoScript reference:
# https://docs.typo3.org/typo3cms/TyposcriptReference/8.7/Functions/Replacement/
lib.navSidebar.stdWrap.replacement.10 {
# Construct search string of the form '#a (Cat|Dog|Tiger)#i', Cat/Dog/Tiger in this case being all the values
# we want to replace. All the menu items, to be precise their unique string (e.g. 'page_123 nav'),
# that are in the rootline need replacement here.
search.cObject = COA
search.cObject.10 = HMENU
search.cObject.10 {
# Construct a rootline menu, including all pages from the current page to the root page ('1|-1')
special = rootline
special.range = 1|-1
# Wrap the whole menu with the structure we need for the 'replacement.10.search' RegExp ('#(...)#i')
wrap = #(|)#i
# For all menuitems of this rootline, discard the actual output ('<li><a>...</a></li>')
# by setting 'doNotLinkIt = 1' and 'doNotShowLink = 1'. Generate inner part of RegExp,
# e.g. 'page_123 nav|page_1231 nav|page_2123 nav', using 'before'
1 = TMENU
1 {
NO {
# Use option split, because we don't want a '|' after the last item
before = page_{field:uid} nav| |*| page_{field:uid} nav| |*| page_{field:uid} nav
before.insertData = 1
doNotLinkIt = 1
doNotShowLink = 1
}
}
}
# Rootline looks something like this: 'root (uid: 1) > page1 (uid: 10) > page10 (uid:100) > page100 (uid:1000)'
# Only the last item in the rootline is the current item, all the items before it are active items.
# Thus we option split again, for us 'nav-sidebar__list__item--submenu-is-open' equals 'active',
# 'nav-sidebar__list__item--current' equal 'current'.
replace = nav-sidebar__list__item--submenu-is-open ${1} |*| nav-sidebar__list__item--submenu-is-open ${1} |*| nav-sidebar__list__item--current ${1}
# Enable option split for replace and RegExp for search
useRegExp = 1
useOptionSplitReplace = 1
}
[global]
# If there is a user logged in to the specific (current) instance, use a different cache key, which in addition to
# '$page.uid.root' and 'TSFE:sys_language_uid' also encodes the user uid of the logged in user, because every user
# might have his own set of visible pages, resulting in menu cache entry unique per user + instance + language.
[usergroup = {$page.uid.frontendUserGroupUid}]
lib.navSidebar.10 {
cache {
key = navSidebarLoggedIn-{$page.uid.root}-{TSFE:sys_language_uid}-{TSFE:fe_user|user|uid}
key.insertData = 1
}
}
[global]
@julrich
Copy link
Author

julrich commented Jun 6, 2018

This is based on: https://gist.github.com/pgampe/cb29bc0fc1111d1370cc
It's generalised to work for 'current' and 'active' classes, and menus of variable depth (max depth 5).

Also handles cases like the special pages (index page, search page, pages not inside the root node of the menu, where the replacement RegExp otherwise would be an empty string, replacing everything), logged in users and languages.

@t3easy
Copy link

t3easy commented May 28, 2019

I have another solution for the cache problem of logged in feusers:

lib.menu.cache {
  key.cObject = COA
  key.cObject.10 = TEXT
  key.cObject.10.value = lib_menu_site{$page.uid.root}_lang{TSFE:sys_language_uid}
  key.cObject.10.insertData = 1
  key.cObject.20 = TEXT
  key.cObject.20.data = TSFE:fe_user|user|usergroup
  key.cObject.20.wrap = _groups|
  key.cObject.20.insertData = 1
  key.cObject.20.replacement.10.search = ,
  key.cObject.20.replacement.10.replace = -
  key.cObject.20.if.isTrue.data = TSFE:fe_user|user|usergroup
  lifetime = default
}

@julrich
Copy link
Author

julrich commented Dec 13, 2019

Ah, that's really useful. Dynamic content as part of the cached menu is a common challenge here (e.g. show some part of the menu for logged in users only. Especially if you don't want to lose the performance gains completely for those cases.

@t3easy
Copy link

t3easy commented Dec 13, 2019

Instead of using my constants {$page.uid.root}, I use {leveluid:0} now.

@t3easy
Copy link

t3easy commented Apr 30, 2020

# Limit the L parameter to the possible language IDs!!!
# If not, a hit to an unused L parameter will write the cache for the default language E.G.:
config.linkVars = L(1-3)

lib.menu = COA
lib.menu.cache {
  key.cObject = COA
  key.cObject.10 = TEXT
  key.cObject.10.value = lib_menu_site{leveluid:0}_lang{TSFE:sys_language_uid}
  key.cObject.10.insertData = 1
  key.cObject.20 = TEXT
  key.cObject.20.if.isTrue.data = TSFE:fe_user|user|usergroup
  key.cObject.20.data = TSFE:fe_user|user|usergroup
  key.cObject.20.wrap = _groups|
  key.cObject.20.insertData = 1
  key.cObject.20.replacement.10.search = ,
  key.cObject.20.replacement.10.replace = -
  lifetime = default
}

@julrich
Copy link
Author

julrich commented Apr 30, 2020

And another nice gotcha. Should probably start revisiting our current implementation... ;)

@MiladinBojic
Copy link

MiladinBojic commented Jul 22, 2020

In order to avoid listing of all pages where the resulting regular expression would be empty, like it is here done

[globalVar = TSFE:id={$page.uid.root}] || [globalVar = TSFE:id={$page.uid.404}] || [globalVar = TSFE:id={$page.uid.noTranslation}] || [globalVar = TSFE:id={$page.uid.search}]

maybe the better solution would be

page.stdWrap.replacement.10 {
search.cObject = COA
search.cObject.10 = HMENU
search.cObject.10 {
....
stdWrap.ifEmpty.cObject = TEXT
stdWrap.ifEmpty.cObject.value = somethingwhatever
stdWrap.wrap = #(|)#i
}
....
}

works for me, at least :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment