Shell extraction ignores a third capture device document (tablet), so no shared header/footer parts are extracted

Open
#4,638 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
74/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
php, wordpress
Domain
backend, testing

Research direction

Start with ShellExtraction::isResponsiveVariantClass() and CaptureEvidence::DEVICE_DOCUMENT; the issue identifies both the recognition rule and the capture contract to follow. Add a contract test based on tests/contract/capture-variant-plain-page-shell.php that includes desktop, mobile, and tablet documents across three routes. Done means shared header and footer parts include the tablet variant, pages have no inline header or footer, and no shell_retained_* diagnostic appears; check the three existing contracts named in the issue too.

Written by the indexing model from the issue text.

Description

A third capture device document disables shared header/footer extraction

Data Liberation now emits an additional device document for dual routes (data-blocks-engine-capture-tablet-document, alongside the desktop and mobile documents) through the existing capture contract: CaptureEvidence::DEVICE_DOCUMENT allows any profile id ([a-z][a-z0-9_-]{0,63}). Shell extraction recognizes only the two hardcoded document classes:

// ShellExtraction::isResponsiveVariantClass()
return in_array($class, array(CaptureEvidence::DESKTOP_DOCUMENT_CLASS, CaptureEvidence::MOBILE_DOCUMENT_CLASS), true)
    || 1 === preg_match('/^site-document-variant-[a-z][a-z0-9_-]{0,31}$/', $class);

So the tablet copy of each landmark has no responsive variant class, scopedVariantLandmarks() returns null for every route, absorbResponsiveVariantLandmarks() returns false, and both areas end in wordpress_site_plan_shell_retained_incomplete ("shell candidates do not establish a dominant semantic cluster"). Every page keeps its own inline header and footer instead of binding shared template parts.

Observed

A 4fires.org (Wix) import with SSI 1.33.366 / DLA 0.33.136: 41 pages, all with desktop + mobile + tablet documents, produced no parts/ and the two shell_retained_incomplete diagnostics. Probing the materialized pages: on every route the desktop and mobile SITE_HEADER / SITE_FOOTER landmarks resolve to their variant classes and the SITE_HEADER--data-blocks-engine-capture-tablet copy resolves to null.

Reduced reproduction (trunk 15317e2)

Three routes, identical chrome, block markup injected per route the way tests/contract/capture-variant-plain-page-shell.php does. The only difference between the two runs is the third document class:

desktop+mobile          parts=["footer","header"] pages-with-inline-header=0 retained=[]
desktop+mobile+tablet   parts=["footer"] pages-with-inline-header=3 retained=["wordpress_site_plan_shell_retained_incomplete footer","wordpress_site_plan_shell_retained_incomplete header"]
Repro script
declare(strict_types=1);

require getenv('BE') . '/php-transformer/vendor/autoload.php';

use Automattic\BlocksEngine\PhpTransformer\ArtifactCompiler\ArtifactCompiler;
use Automattic\BlocksEngine\PhpTransformer\Contract\CaptureEvidence;
use Automattic\BlocksEngine\PhpTransformer\WordPressSitePlan\WordPressSitePlan;

// Three routes, each captured as three device documents: desktop, mobile, and
// an additional profile (tablet) declared through the same capture contract.
$tablet = 'data-blocks-engine-capture-tablet-document';
$navigation = '<!-- wp:navigation {"overlayMenu":"never"} --><!-- wp:navigation-link {"label":"Home","url":"/","kind":"custom"} /--><!-- wp:navigation-link {"label":"About","url":"/about/","kind":"custom"} /--><!-- /wp:navigation -->';
$header = static fn(string $class): string => '<!-- wp:group {"className":"' . $class . '","tagName":"header"} --><header class="wp-block-group ' . $class . '"><!-- wp:paragraph --><p><a href="/">Four Fires</a></p><!-- /wp:paragraph -->' . $navigation . '</header><!-- /wp:group -->';
$footer = static fn(string $class): string => '<!-- wp:group {"className":"' . $class . '","tagName":"footer"} --><footer class="wp-block-group ' . $class . '"><!-- wp:paragraph --><p>Four Fires Ministries</p><!-- /wp:paragraph --></footer><!-- /wp:group -->';
$main = static fn(string $title): string => '<!-- wp:group {"tagName":"main"} --><main class="wp-block-group"><!-- wp:heading --><h2 class="wp-block-heading">' . $title . '</h2><!-- /wp:heading --></main><!-- /wp:group -->';
$document = static fn(string $class, string $suffix, string $title): string => '<!-- wp:group {"className":"' . $class . '"} --><div class="wp-block-group ' . $class . '">' . $header('site-header' . $suffix) . $main($title) . $footer('site-footer' . $suffix) . '</div><!-- /wp:group -->';
$capture = static fn(string $title, array $classes): string => implode('', array_map(static fn(string $class, int $i): string => $document($class, 0 === $i ? '' : ' compact', $title), $classes, array_keys($classes)));

foreach (array('desktop+mobile' => array(CaptureEvidence::DESKTOP_DOCUMENT_CLASS, CaptureEvidence::MOBILE_DOCUMENT_CLASS), 'desktop+mobile+tablet' => array(CaptureEvidence::DESKTOP_DOCUMENT_CLASS, CaptureEvidence::MOBILE_DOCUMENT_CLASS, $tablet)) as $label => $classes) {
    $result = (new ArtifactCompiler())->compile(array('entrypoint' => 'index.html', 'files' => array(
        'index.html' => '<main><h1>Home</h1></main>',
        'about/index.html' => '<main><h1>About</h1></main>',
        'contact/index.html' => '<main><h1>Contact</h1></main>',
    )))->toArray();
    foreach ($result['source_reports']['compiled_site']['pages'] as &$page) $page['block_markup'] = $capture(ucfirst(basename(dirname('/' . $page['source_path'])) ?: 'Home'), $classes);
    unset($page);
    $plan = (new WordPressSitePlan())->fromResult($result);
    $areas = array_column($plan['template_parts'], 'area');
    $retained = array_values(array_filter(array_map(static fn(array $d): string => ($d['code'] ?? '') . ' ' . ($d['area'] ?? ''), $plan['diagnostics']), static fn(string $c): bool => str_contains($c, 'shell_retained')));
    $inline = count(array_filter($plan['pages'], static fn(array $p): bool => str_contains((string) $p['canonical_block_markup'], 'site-header')));
    echo str_pad($label, 24), 'parts=', json_encode($areas), ' pages-with-inline-header=', $inline, ' retained=', json_encode($retained), "\n";
    if (str_contains($label, 'tablet')) {
        $header = array_values(array_filter($plan['template_parts'], static fn(array $p): bool => 'header' === $p['area']))[0]['canonical_block_markup'] ?? '';
        echo '  tablet document in shared header part: ', var_export(str_contains($header, $tablet), true), "\n";
    }
}
Expected

Any capture device document class (data-blocks-engine-capture-<profile>-document, profile per the DEVICE_DOCUMENT grammar) is a responsive document variant for shell extraction, the same as desktop and mobile. With three documents the routes bind one shared header part and one shared footer part that render all three variants, and no page keeps an inline copy, matching the two-document result.

Acceptance
  • A contract test (three routes × desktop/mobile/tablet documents) fails on trunk and passes on the fix: shared header and footer parts exist, each holds the tablet variant, no page retains site-header/site-footer, and no shell_retained_* diagnostic is emitted.
  • The existing two-document contracts (capture-variant-plain-page-shell.php, unlabeled-capture-device-shell.php, viewport-hidden-document-variants.php) still pass.
  • Recognition derives from the capture contract (one helper on CaptureEvidence for "is a device document class") rather than adding a third hardcoded constant.

AI disclosure: filed by Franklin (Claude Opus via Pi/Roadie). Root cause found by probing the 4fires import's materialized pages with ShellExtraction on trunk; the reduced repro was run on trunk.

Dominant language
PHP
Stars
25
Forks
3
Avg merge
3h 25m
Merged PRs (30d)
935

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Automattic/blocks-engine

All issues in Automattic/blocks-engine

Similar issues

More PHP issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.