Skip to content

Methods for inserting HTML should be exposed on OD_Template_Optimization_Context for before or after all tags have been visited #1931

@westonruter

Description

@westonruter

Originally discussed in #1919 (comment) and in other comments on that PR.

The OD_HTML_Tag_Processor class includes two methods: ::append_head_html() and ::append_body_html(). The OD_HTML_Tag_Processor instance is exposed on an OD_Tag_Visitor_Context instance passed to the tag visitors, but it is not yet exposed on the OD_Template_Optimization_Context class which is passed to the od_start_template_optimization and od_finish_template_optimization actions which run before and after the document has been iterated over by tag visitors, respectively.

The current use cases for ::append_head_html() are:

  • Optimization Detective injecting the LINK HTML markup returned by OD_Link_Collection::get_html().
  • Image Prioritizer adding a STYLE tag via a tag visitor to add a style for lazy loaded background images.
  • Embed Optimizer adding a STYLE tag via a tag visitor to reduce layout shifts.
  • Content Visibility adding a STYLE tag via a tag visitor for CV styles.

The current use cases for ::append_body_html() are:

  • Optimization Detective uses this to insert the detect.js script to the page.
  • Embed Optimizer adding a SCRIPT to the end of the BODY when there is a lazy-loaded embed on the page.
  • Image Prioritizer adding a SCRIPT tag via a tag visitor to lazy load background images.
  • Image Prioritizer adding a SCRIPT tag via a tag visitor to lazy load videos.

Allowing insertion of HTML once via the od_finish_template_optimization avoids the need for tag visitors to keep track of whether they inserted or not. They can use the tag visitor callbacks to get a "lay of the land" by looking at all of the tags, and then at the od_finish_template_optimization action they can finalize what they need to insert in the HEAD or the BODY.

This could, for example, allow tag visitors to better optimize stylesheets they insert into the document. Instead of Embed Optimizer inserting a separate STYLE for each embed to reserve space to reduce layout shifts, it could instead insert a single STYLE at od_finish_template_optimization which combines all the style rules in one stylesheet. For example, this would allow Embed Optimizer to better group styles by media query instead of having to output @media (width <= 480px) {} for each embed on the page. Currently for each embed it inserts a STYLE like:

<style>
@media (width <= 480px) { #embed-optimizer-6040306707fb51ccaafa915c2da8f412 { min-height: 500px; } }
@media (480px < width <= 600px) { #embed-optimizer-6040306707fb51ccaafa915c2da8f412 { min-height: 500px; } }
@media (600px < width <= 782px) { #embed-optimizer-6040306707fb51ccaafa915c2da8f412 { min-height: 500px; } }
@media (782px < width) { #embed-optimizer-6040306707fb51ccaafa915c2da8f412 { min-height: 500px; } }
</style>
<style>
@media (width <= 480px) { #embed-optimizer-96ffd32b51748c70b288af1ef0c14c01 { min-height: 500px; } }
@media (480px < width <= 600px) { #embed-optimizer-96ffd32b51748c70b288af1ef0c14c01 { min-height: 500px; } }
@media (600px < width <= 782px) { #embed-optimizer-96ffd32b51748c70b288af1ef0c14c01 { min-height: 500px; } }
@media (782px < width) { #embed-optimizer-96ffd32b51748c70b288af1ef0c14c01 { min-height: 500px; } }
</style>
<style>
@media (width <= 480px) { #embed-optimizer-f9ff6c9a914366ac3c1aab1994dd8a69 { min-height: 500px; } }
@media (480px < width <= 600px) { #embed-optimizer-f9ff6c9a914366ac3c1aab1994dd8a69 { min-height: 500px; } }
@media (600px < width <= 782px) { #embed-optimizer-f9ff6c9a914366ac3c1aab1994dd8a69 { min-height: 500px; } }
@media (782px < width) { #embed-optimizer-f9ff6c9a914366ac3c1aab1994dd8a69 { min-height: 500px; } }
</style>

With the od_finish_template_optimization action, it could just print the @media at-rules once for each viewport group rather than duplicating them, and output them all in a single STYLE tag rather than in three separate ones, for example:

<style>
@media (width <= 480px) {
	#embed-optimizer-6040306707fb51ccaafa915c2da8f412 { min-height: 500px; }
	#embed-optimizer-96ffd32b51748c70b288af1ef0c14c01 { min-height: 500px; }
	#embed-optimizer-f9ff6c9a914366ac3c1aab1994dd8a69 { min-height: 500px; }
}
@media (480px < width <= 600px) {
	#embed-optimizer-6040306707fb51ccaafa915c2da8f412 { min-height: 500px; }
	#embed-optimizer-96ffd32b51748c70b288af1ef0c14c01 { min-height: 500px; }
	#embed-optimizer-f9ff6c9a914366ac3c1aab1994dd8a69 { min-height: 500px; }
}
@media (600px < width <= 782px) {
	#embed-optimizer-6040306707fb51ccaafa915c2da8f412 { min-height: 500px; }
	#embed-optimizer-96ffd32b51748c70b288af1ef0c14c01 { min-height: 500px; }
	#embed-optimizer-f9ff6c9a914366ac3c1aab1994dd8a69 { min-height: 500px; }
}
@media (782px < width) {
	#embed-optimizer-6040306707fb51ccaafa915c2da8f412 { min-height: 500px; }
	#embed-optimizer-96ffd32b51748c70b288af1ef0c14c01 { min-height: 500px; }
	#embed-optimizer-f9ff6c9a914366ac3c1aab1994dd8a69 { min-height: 500px; }
}
</style>

See #1923 for a proof of concept for this.

Additionally, for tag visitors that insert scripts at the end of the BODY based on whether certain tags are encountered, this could be done via the od_finish_template_optimization action instead. Otherwise the tag visitor needs to keep track with a added_lazy_script class member variable for whether it inserted the script yet, although the difference here is not as great as with the stylesheets in Embed Optimizer. Example: #1922.

Being able to insert HTML via these actions would also be useful to a plugin like Optimization Detective Admin UI which needs to insert the current response's URL Metric data for rendering in the admin bar. See westonruter/od-admin-ui#11 for an example of how this could be implemented.

Currently when tag visitors call ::append_head_html() or ::append_body_html(), they must be sure to pass raw HTML which is valid in the HEAD and BODY context respectively. If they don't, then they'll cause a parsing error for the browser (e.g. adding an IMG in the HEAD will prematurely open the BODY).

Optimization Detective could continue to use the ::append_head_html() and ::append_body_html(), but for extensions there would need to be new methods like:

  • ::append_head_style() to append an inline stylesheet
  • ::append_head_script() (but also differentiate between inline vs non-inline?)
  • ::append_body_script() (ditto)

We wouldn't need to include an ::append_head_link() since this is what the OD_Link_Collection takes care of, but we may need to add support for rel=stylesheet.

Other potential methods that will be useful:

  • ::append_body_stylesheet_link() (not used yet, but useful for adding non-blocking external stylesheets)
  • ::append_head_meta() to add a META tag
  • ::append_body_comment() to add a comment to the end of the BODY
  • ::append_document_comment() to add a comment after the closing </html> tag.

Note that we should perhaps not allow script modules to be inserted in the HEAD because import map scripts (SCRIPT[type="importmap"]) are printed in the footer in Classic Themes, and if a script module appears before an import map then it prevents the import map from working.

If these methods were exposed on both OD_Tag_Visitor_Context and OD_Template_Optimization_Context then the ::append_html_html() and ::append_body_html() methods could be deprecated and eventually not exposed at all.

See also the following from #1546:

We should consider not directly exposing the OD_HTML_Tag_Processor to the tag visitors and template optimization start/finish action handlers since this then exposes the append_head_html() and append_body_html() methods which may not be desirable. This also exposes a lower-level API than which may be appropriate which will complicate switching between using WP_HTML_Tag_Processor and WP_HTML_Processor implementations. Namely, for WP_HTML_Processor there may not be a need to add a get_xpath() method directly to the class, and we may not need to subclass it at all. If we instead passed a wrapper class, then we could expose a get_xpath() on the wrapper subclass which is then able to use the appropriate implementation based on whether it is using WP_HTML_Tag_Processor or WP_HTML_Processor. In fact, this could allow Optimization Detective to use an unsubclassed original WP_HTML_Tag_Processor instance which is provided by core for output-buffer manipulation instead of allowing filtering the underlying string (if that is so desired, per Core-43258).

Metadata

Metadata

Assignees

No one assigned

    Labels

    [Plugin] Embed OptimizerIssues for the Embed Optimizer plugin (formerly Auto Sizes)[Plugin] Image PrioritizerIssues for the Image Prioritizer plugin (dependent on Optimization Detective)[Plugin] Optimization DetectiveIssues for the Optimization Detective plugin[Type] EnhancementA suggestion for improvement of an existing feature

    Projects

    Status

    Not Started/Backlog 📆

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions