Page MenuHomePhabricator

[Minerva TOC] Build TOC
Closed, ResolvedPublic3 Estimated Story Points

Description

User story

As a mobile user, I want easy access to a table of contents to aid my navigation throughout an article's content.

Requirements

Other than the detail that can easily be extracted from looking at the designs (see below) or acceptance criteria (below), here's what else to look out for:

Since we chose to respect the NOTOC directive, we can probably re-use the DOM that already exists on-page (document.querySelector( '#toc > ul' )).
If for whatever reason that does not suffice, we can fetch it from the parse API.
Below is a quick code sample that would produce the same HTML the parsers currently do:

async function getTocHtml() {
    const response = await new mw.Api().get( {
        action: 'parse',
        oldid: mw.config.get( 'wgRevisionId' ),
        prop: 'tocdata'
    } );

    const sectionsByParent = response.parse.tocdata.sections.reduce( ( restructured, section ) => {
        const parent = section.number.replace( /(^|\.)[^/.]+$/, '' );
        return { ...restructured, [ parent ]: ( restructured[ parent ] || [] ).concat( section ) };
    }, {} );

    const parseSectionsFor = ( parent ) => {
        if ( !( parent in sectionsByParent ) ) {
            return '';
        }
        return '<ul>' + 
            sectionsByParent[ parent ].map( ( section ) => `<li class="toclevel-${ section.tocLevel } tocsection-${ section.index }">` +
                `<a href="#${ section.anchor }">` +
                    `<span class="tocnumber">${ section.number }</span>` +
                    `<span class="toctext">${ section.line }</span>` + // note: can contain HTML (and is allowed to), e.g. italics
                '</a>' +
                parseSectionsFor( section.number ) +
            '</li>' ).join( '' ) +
        '</ul>';
    };
    return parseSectionsFor( '' );
}

Note that when fetching from the API, we ought to:

  • make sure it is for the correct revision (mw.config.get( 'wgRevisionId' ))
  • reduce calls as much as possible
    • only load as needed
    • cache the results
    • insofar possible, potentially even attempt to use existing TOC first, before falling back to API

Design

This ticket only concerns the TOC, not the thing that invokes it (sticky header of floating button) and how exactly it presents in either design.
This ticket is specifically for building the TOC and whatever functionality is similar in both designs - the specific that differ (e.g. positioning/design of the container) will be tackled in other tickets.

Acceptance criteria

  • Existing Minerva TOC is guaranteed to be hidden, also on wide screens
  • TOC does not show when editors explicitly disabled it through NOTOC (e.g. Main page)
  • TOC gets an additional first entry (Top) which, when clicked, scroll the user to the top of the page
  • Clicking anything in the TOC closes it and scrolls to the position of the item clicked
  • With TOC open, keyboard navigation is trapped within the TOC container
  • Escape key closes TOC
  • Nice to have for instrumentation outside of repo: TOC open/close emits readerExperiments.toc.open & readerExperiments.toc.close, while initialization of the TOC emits readerExperiments.toc.init
  • Nice to have for link sharing: TOC open/close is also triggered by url hash (#toc) changes, and opening/closing TOC programmatically triggers those

Event Timeline

SherryYang-WMF set the point value for this task to 3.Jan 6 2026, 5:48 PM
Known Minerva TOC Edge Cases
    1. 1 Minerva skin–specific suppression logic
  • TOC may be hidden if the page has fewer than a certain number of headings.
  • This logic exists at the skin level, beyond core parser behavior.

    1. 2 Parser differences (Parsoid vs. legacy parser)
  • TOC visibility behavior may differ depending on which parser is used.
  • This can result in inconsistent TOC rendering across contexts.

    1. 3 Parse API behavior
  • Likely uses default/core TOC logic.
  • May not fully reflect Minerva’s additional suppression rules.

1.2 may be moot if T413404 ends up with not allowing Parsoid output.
1.3 action=parse&prop=tocdata still returns data when NOTOC is present, so if we're going with that and want to respect NOTOC, we'd need an additional check for the NOTOC directive.

Change #1227483 had a related patch set uploaded (by LWatson; author: LWatson):

[mediawiki/extensions/ReaderExperiments@master] Minerva TOC: build TOC

https://gerrit.wikimedia.org/r/1227483

Change #1233677 had a related patch set uploaded (by Matthias Mullie; author: Matthias Mullie):

[mediawiki/extensions/ReaderExperiments@master] Preserve #toc history entry on keyboard close

https://gerrit.wikimedia.org/r/1233677

Change #1227483 merged by jenkins-bot:

[mediawiki/extensions/ReaderExperiments@master] Minerva TOC: build TOC

https://gerrit.wikimedia.org/r/1227483

Change #1233677 merged by jenkins-bot:

[mediawiki/extensions/ReaderExperiments@master] Preserve #toc history entry on keyboard close

https://gerrit.wikimedia.org/r/1233677

Notes for QA:

  • Navigate to a Wikipedia article page (eg. Hedy Lemarr) and ensure the Minerva mobile view and experiment is enabled. URL example: https://en.wikipedia.org/wiki/Hedy_Lamarr?mpo=minerva-toc-sticky:treatment
  • Enable Minerva mobile site via footer link that toggles desktop and mobile.
  • Enable the experiment using URL params. There are two versions (sticky header and floating button):
    • Sticky header version: ?mpo=minerva-toc-sticky:treatment
    • Floating button version: ?mpo=minerva-toc-button:treatment

Noting that this "nice to have" was not completed.

Nice to have for instrumentation outside of repo: TOC open/close emits readerExperiments.toc.open & readerExperiments.toc.close, while initialization of the TOC emits readerExperiments.toc.init

These are now confirmed done on beta:

  • TOC gets an additional first entry (Top) which, when clicked, scroll the user to the top of the page
  • With TOC open, keyboard navigation is trapped within the TOC container
  • Escape key closes TOC

woohoo!

This acceptance criteria seems iffy still:

  • TOC does not show when editors explicitly disabled it through NOTOC (e.g. Main page)

If passed the main page on beta, it explodes with console errors. It shouldn't show anything really? But we could maybe punt that to later and say that's the problem of the code showing the ToC button?

lwatson changed the task status from Open to In Progress.Jan 30 2026, 2:31 PM

This acceptance criteria seems iffy still:

  • TOC does not show when editors explicitly disabled it through NOTOC (e.g. Main page)

This was supposed to be done, but looks like I got some booleans messed up :p
Should now be fixed with https://gerrit.wikimedia.org/r/c/mediawiki/extensions/ReaderExperiments/+/1235343

Etonkovidova subscribed.

>>! In T413400#11569629, @matthiasmullie wrote:

This acceptance criteria seems iffy still:

  • TOC does not show when editors explicitly disabled it through NOTOC (e.g. Main page)

This was supposed to be done, but looks like I got some booleans messed up :p
Should now be fixed with https://gerrit.wikimedia.org/r/c/mediawiki/extensions/ReaderExperiments/+/1235343

Confirmed as fixed.


There is T415794: [QA Task] testing TOC - minerva-toc-sticky with a list of issues in production and in beta - should it be sorted out before deployment?

In beta ?mpo=minerva-toc-sticky:treatment triggers Console error:
Uncaught (in promise) TypeError: can't access property "$el", stickyHeadingRef.value is null

on articles without TOC or with __NOTOC__ - seems like expected?

Thanks @Etonkovidova! Regarding T415794, I've addressed some issues in a separate task that focused on the sticky header TOC (T413402). I can open up patches for the remaining ones and link them to T415794.

Regarding the console error, I haven't noticed the error but a null check should resolve that error for those cases. I submitted a fix: https://gerrit.wikimedia.org/r/c/mediawiki/extensions/ReaderExperiments/+/1236322

Uncaught (in promise) TypeError: can't access property "$el", stickyHeadingRef.value is null

Nice to have for link sharing: TOC open/close is also triggered by url hash (#toc) changes, and opening/closing TOC programmatically triggers those

Navigating to a URL with #toc hash displays an open toc. Is anything else needed here?

Note: Please ignore the wide TOC on wide screens. The max width and other TOC styles are not live on production.