Two things about customising a Pages "html" page that each cost an hour, and both fail in a way that looks like something else.
1. page_content is not run through the template engine
A theme's custom header is compiled — Theme::getHeaderAndFooter() calls Theme::compileTemplate(), so you can write {{if}}, {{foreach}} and {expression="..."} in it and they execute.
A Pages page's page_content is not. Pages parses its own tags — {database="kb"}, {block="..."}, {media="..."} — and emits everything else verbatim. Put template logic in a page and visitors see this at the top of the screen:
{{$edKbPath = rtrim( (string) parse_url( $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH ), '/' );}}
{{$edKbIndex = ( $edKbPath === '/kb' or $edKbPath === '' );}}
{{if !$edKbIndex}}
The nasty part: it does not fail consistently across routes. A page with a database attached serves the listing, each category, each record and the submit form from the same page_content. In testing, the listing route swallowed the logic and rendered correctly, while the record and submit routes printed the raw source. Checking the index, seeing clean output, and assuming the rest matched was enough to ship it.
If one page serves several routes, check every route for leaked syntax, not just the one you were looking at. HTTP 200 on the others proves nothing.
What to do instead
Generate the HTML somewhere that can run code and write the result in as static markup — a small script that reads the database and updates cms_pages.page_content is enough, and it can be re-run whenever the content changes. For anything genuinely dynamic, use a Pages block (which is compiled) or a theme template. Keep logic out of the page body entirely.
A plain <script> in the page body is fine, incidentally — scripts run regardless of what the parser did or did not interpret. That is a reasonable place for behaviour that has to differ by route.
2. Removing an element from the DOM does not stick
Having replaced a listing with your own index, the obvious next step is to delete the stock one:
var listing = document.querySelector('.ipsData--cms-category-articles');
listing.closest('.ipsBox').remove(); // comes straight back
It disappears and then returns. Invision Community 5 wraps content listings in an <i-data> custom element, and custom elements hydrate after DOMContentLoaded and rebuild their contents. Anything you removed in the meantime is re-created, with no error and no clue as to why.
Hide it with CSS instead — a stylesheet rule cannot be undone by a re-render:
/* inline, before paint, so the old listing never flashes */
<script>
if ( /\/kb$/.test( location.pathname.replace(/\/+$/, '') ) ) {
document.documentElement.classList.add('kb-index');
}
</script>
html.kb-index .ipsBox--cmsCategoryArticles { display: none !important; }
Setting the class on <html> from an inline script — not a deferred one — means the rule applies before first paint, so there is no flash of the thing you are suppressing.
3. And while you are here: inline scripts run before the rest of the page exists
Page content is emitted before the record markup further down the document. A script in it that looks for the article body finds nothing, and if the same script also touches something above it, half your code appears to work:
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', start );
} else {
start();
}
Which is also why the class in the previous section is set immediately rather than inside start() — one job needs to happen before paint, the other needs the document to exist.
Recommended Comments