AFL

How the page scan works

Explanation: how real pages are matched to legacy widgets and templates, what the confidence levels mean, and what the scan cannot see.

The page scan answers one question: which legacy widgets does each real page use? It reads the HTML that Pulse produced, not the CMS, so everything it reports is an inference from markup. The steps are in Scan the live sites for widgets.

Where the widgets are in the HTML

The legacy code marks widgets in two ways:

  • A data-widget="name" attribute on the widget's root element. The value often does not match the widget's folder, and one element can carry more than one data-widget attribute, so the parser reads attributes as events instead of building a DOM, which would keep only the first.
  • React widgets, which have no data-widget. They mount on an element with a CSS class listed in the legacy react/js/bootstrap.jsx.

The crosswalk

data/crosswalk.json is built from the legacy source, so the scan never needs the legacy repos. It records, for aflm-web (afl.com.au) and aflc-web (club sites):

  • every template that declares a data-widget or data-script, with the static attributes on that tag
  • the default markers that shared macros fall back to
  • every widget folder and its templates
  • the React mount classes
  • the page templates and the classes each one renders

How a marker becomes a widget

Each instance on a page is resolved against the crosswalk for that site:

  • declared: a template in the source declares the marker. If several templates declare it, they are scored on data-script, data-component-name, data-template, shared classes, a data-title that matches a file name and campaign names (AFLW, Gather Round, Finals Hub). A second pass uses the rest of the page: a template named by the page's article class, or in a folder the page already uses confidently, scores higher.
  • js-handler: no template declares it, but one folder's script handles that marker.
  • folder-name: nothing declares it, but a widget folder has the same name. This covers widgets the CMS composes.
  • react-class: the element has a React mount class.

The result has a kind:

  • widget and react are real widgets.
  • generic markers are declared in eight or more widget folders (lazy-load-images). They are cross-cutting behaviour, kept but left out of widget counts.
  • shared markers are declared only in shared partials, or are the default marker of a shared macro. The share button is the example: its macro writes data-widget="${ js_widget }" with a default of social-share-page, so that marker is shared and is not credited to the one template that happens to spell it out.
  • unresolved markers match nothing.

Confidence says how the match was made. high is a single declaration or a clear winner on attributes. medium is a weaker winner, a folder known but not a single template, or a match with no declaration. low is several folders tied. The evidence for each match is stored with it.

A widget declared in a shared partial (such as includes/_promo-listing.ftl for the promo list) cannot be tied to one of the folder's templates. Those matches are shown as possible.

Page templates

Pulse leaves almost no trace of the page template in the HTML. The guess works in three steps:

  1. markup: each page template renders its own static classes. A template matches when all of its classes, minus those every shell shares, appear on the page. The most specific match wins. Where several copies share markup (the AFLW and club help variants), the page's shell hints choose, otherwise the default copy is chosen at medium confidence.
  2. url-path: a URL segment names a template with no markup of its own, such as trade or vfl. Low confidence.
  3. shell-default: nothing leaf-specific was found, so the page content is assumed to sit directly in the shell (header-footer, or its AFLW, Gather Round or Finals Hub variant). Low confidence.

Most hub pages end up at the last step, because the base shell is a template in its own right and the markup cannot tell it from its siblings. The guess is labelled as a guess everywhere it appears.

Anchors

Some widgets can have an anchor, written as a.content-anchor with an id, usually a GUID, as the first child of the widget's wrapper. The scan attaches each anchor to the widget it sits in, so page-url#id should jump to that widget.

It does not always work. The legacy CSS is .content-anchor { position: absolute; top: 0; left: 0 }, which only lands in the right place inside a container that is position: relative. On 29 October 2026, in headless Chromium, the podcast anchors tested scrolled to their widgets. The promo-list wrapper is position: static, so all eight promo-list anchors tested jumped to the top of the page or to the wrong place. Most anchors found were on the promo list, so treat anchor links as unverified. A rebuilt widget needs an id on its own root and a scroll offset for the fixed header.

Regions

Each instance is placed by where it sits relative to main: before it is header, inside it is main, inside an aside within it is sidebar, after it is footer. A page with no main gets unknown. The header and footer hold the page chrome that appears on every page.

What the scan cannot see

  • Only what is in the HTML. Widgets drawn by script after load are seen only through their mount element, and anything personalised or conditional appears only if the server sent it.
  • Only pages the sitemaps list. Some sitemaps are partial, and some list pages that no longer exist, which answer 404.
  • Pulse usage figures are separate. The Pulse export counts widgets by Pulse name and links one Pulse widget to a folder, so it can understate a folder with several variants.
  • Classification. The page type comes from URL rules, and many pages are unclassified, which limits grouping by type.