Markup regions give you the best of both worlds—the simplicity of direct output with the power of delayed output, just using HTML.

ProcessWire.com uses Markup Regions as its output strategy.

Introduction

Markup regions are an easy-to-use template file output strategy available in ProcessWire 3.0.61 or newer. Markup regions are a type of delayed output that lets you work as if it was direct output. They work on top of your existing HTML structure and enable you to selectively populate markup anywhere in your HTML document using nothing but HTML (no PHP programming needed). Despite their simplicity, markup regions also represent one of the most powerful and flexible output strategies in ProcessWire.

A markup region is defined as simply an HTML tag with an id attribute. Any HTML tag with an id attribute can be manipulated by this output strategy. That HTML tag can optionally contain markup within it, which would be its default value.

Conceptual example

To introduce the concept of markup regions, let's consider the following markup:

<div id='content'>
  <p>Hello</p>
</div>

<div id='content'>
  <p>World</p>
</div>

When viewed in the web browser, the above markup would render the words “Hello” and “World” in separate paragraphs. But it wouldn't be technically valid HTML because the div ID of “content” is repeated twice, and ID attributes in an HTML document are only supposed to be used once.

With markup regions enabled, the rules change. If some later markup uses the same ID attribute, it completely replaces the original markup that used that ID, taking over the original location. The final output would only include the second div#content above, but in the location of the first one. In the browser, it would only output the “World” paragraph (with wrapping div#content tag).

While the default behavior of markup regions is to replace an originally defined block of markup with a new one, markup regions also let you prepend markup, append markup, insert markup before other markup, insert markup after other markup, add attributes to previously defined markup, and more.

This example helps to explain the concept of markup regions, but it is illustrative as a concept only. While it's not important to understand, if you are interested, see the technical details of how markup regions work.

Definitions and actions

We refer to the introduction of an HTML tag having an ID attribute as the “region definition.” This typically appears in a /site/templates/_main.php file as part of the markup within the HTML document. Any markup output by your template files (like /site/templates/basic-page.php) can use an HTML tag with the same ID attribute to populate that original region definition. We refer to that markup that replaces or manipulates the defined region as the "region action".

Region definition:
Any HTML tag having an ID attribute within your <html>...</html> content.

Region action:
Any markup output by a page's template file that uses that same ID attribute.

If this doesn't make sense just yet, don't worry, it'll become very clear as we move forward.

Prerequisites

Before you can use markup regions in ProcessWire, you have to tell ProcessWire that this is your intended output strategy. To do this, add the following to your /site/config.php file (if not there already):

$config->useMarkupRegions = true;
$config->appendTemplateFile = '_main.php';

The first line above says that you want to use markup regions. The next line says that you want ProcessWire to append a _main.php file in your /site/templates/ directory on every request. This _main.php file is where your main document HTML markup will be contained, from <!DOCTYPE html> to </html> and everything in between, which is where regions will be defined. ProcessWire will automatically include this _main.php file on every page, unless you specifically disable it for any templates, which can be done from Setup > Templates > [template] > Files (tab).

Now you are ready to start using markup regions. To get started, you'll want to define your markup regions…

Defining markup regions

Define markup regions anywhere in your <html>...</html> with existing HTML tags (typically in _main.php), and identify them with HTML id attributes:

<div id="hello">
  <p>Hello World</p>
</div> 

The “Hello World” paragraph in the region definition represents the fallback/default value for that region. That's what it will contain if nothing replaces it. This default value can be anything that you would put anywhere else in an HTML or PHP document, whether static markup, markup dynamically generated from PHP. It can also be completely blank if you don't need a fallback/default value.

If you don't like using HTML id attributes to identify your region, you are also welcome to use ProcessWire-specific pw-id or data-pw-id attributes instead. They do the same thing, but only ProcessWire can see them. They are removed from the final output and thus not visible to front-end markup, CSS or JS.

<div data-pw-id="hello">
  <p>Hello World</p>
</div>

Note that we are using <div> tags in many of our examples here, but regions can be defined or populated by any HTML tag.

Defining a placeholder region

If you want to define a region where only the inner HTML will be used, and the wrapping tags won't appear in the final markup, you can define such a region using either the <region> tag or <pw-region> tag (they are the same thing). The following two examples are functionally identical:

<region id="hello">...</region>
<pw-region id="hello">...</pw-region>

Defining an optional region

Available in ProcessWire 3.0.117 or newer

If you want to define a region that should automatically be removed from the document markup if nothing populates it, add a pw-optional or data-pw-optional boolean attribute to the region definition, and leave the inner HTML blank:

<div id='hello' pw-optional></div>

The above div#hello markup will only appear in the final output if it gets populated. If it doesn't get populated, the entire div#hello is removed and will not appear in the final output.

Consider a <ul>, which [according to HTML5 spec] is required to have one or more <li> elements within it, otherwise it's invalid HTML. Using the pw-optional attribute, we can define that <ul> and have it automatically removed from the markup if it turns out that it's going to be blank:

<ul id='subnav'></ul>

If something populates one or more <li> list items to #subnav, then the entire <ul id='subnav'> and all of its <li> elements will appear in the final markup. If nothing populates it, then it is removed.

Populating markup regions

Once there are regions defined within the <html>...</html> you can populate them. This is done from your site template files (or other files included from them). Below is a simple example of populating a region, which we are doing from our /site/templates/basic-page.php template file:

<div id="hello">
  <p>This paragraph will replace the default value in div#hello.</p>
</div>

The above example simply replaces whatever appears in the originally defined div#hello, and replaces it with the markup that you specify. The next example below is functionally identical to the above example, just using different syntax (which we'll use to introduce action attributes):

<div pw-replace="hello">
  <p>This paragraph will replace the default value in div#hello.</p>
</div>

Notice that pw-replace attribute above. This is what's called an action attribute. The default action is to replace, which is why we didn't have to specify it in the example before this one. Maybe that's all you will ever need. But you can do quite a bit more…

Action attributes for populating regions

In the above examples we've done simple replacements of markup in a region. However, using action attributes you can also append markup, prepend markup, insert markup before, or insert markup after any defined region using these pw-* action attributes (or data-pw-* attributes if you prefer). Below is a list of all the available action attributes:

  • pw-replace replaces a region’s markup
  • pw-append appends markup to a region
  • pw-prepend prepends markup to a region
  • pw-before inserts markup before a region
  • pw-after inserts markup after a region

Action attributes can specify a value, i.e., or they can be used as a boolean attribute, i.e. . How you use them affects how they work:

Action attributes with value (the region ID) apply the outer HTML:

<div pw-append="hello">
  <h2>Action attribute with value</h2>
  <p>outer HTML includes wrapping div, h2 and p tags</p>
</div>

Boolean action attributes apply the inner HTML:

<div id="hello" pw-append>
  <h2>Boolean action attribute</h2>
  <p>inner HTML includes only the h2 and p tags</p>
</div>

First let's take a closer look at action attributes with value, below. Following that we'll look closer at boolean action attributes.

Action attributes with value (outer HTML)

When you specify the region ID as the action attribute value, all of the markup that you specify (the outer HTML) becomes part of the final document markup (except for the pw-* attributes):

<p pw-append="hello">
  This paragraph will APPEND to div#hello
</p>

<p pw-prepend="hello">
  This paragraph will PREPEND to div#hello
</p>

<p pw-before="hello">
  This will insert this paragraph BEFORE div#hello
</p>

<p pw-after="hello" class="world">
  This will insert this paragraph with class "world" AFTER div#hello.
</p>

Lets say the above is in our basic-page.php template file and this region definition below appears in our _main.php file:

<div id='hello'>
  <h2>Hello World</h2>
</div>

Here's the result after the regions have been populated (the final output):

<p>
  This will insert this paragraph BEFORE div#hello
</p>
<div id='hello'>
  <p>
    This paragraph will PREPEND to div#hello
  </p>
  <h2>Hello World</h2>
  <p>
    This paragraph will APPEND to div#hello
  </p>
</div>
<p class="world">
  This will insert this paragraph with class "world" AFTER div#hello.
</p>

Any of the pw-[action] attributes above can also be specified as data-pw-[action] if you prefer it. Regardless of what format you use, the action attributes are only seen by ProcessWire and they are removed from the final markup.

Boolean action attributes (inner HTML)

Boolean action attributes simply specify the action attribute, but with no value, i.e. just pw-append rather than pw-append=hello. The region they will apply to is instead specified in an id attribute (or pw-id or data-pw-id attribute if you prefer it). By using a boolean attribute, we are telling ProcessWire to apply only the inner HTML to the region. Lets use the same example that we did for action attributes with value, but change them to use boolean attributes, and observe the difference:

<p id='hello' pw-append>
  This text will APPEND to div#hello
</p>

<p id='hello' pw-prepend>
  This text will PREPEND to div#hello
</p>

<p id='hello' pw-before>
  This will insert this text BEFORE div#hello
</p>

<p id='hello' pw-after>
  This will insert this text AFTER div#hello
</p>

This is the region definition again. It's the same as before, but I'm repeating it here so you don't have to scroll up:

<div id='hello'>
  <h2>Hello World</h2>
</div>

And here's the result in the final output:

This will insert this text BEFORE div#hello
<div id="hello">
  This text will PREPEND to div#hello
  <h2>Hello World</h2>
  This text will APPEND to div#hello
</div>
This will insert this text AFTER div#hello

As you can see, using the boolean attributes made it only use the inner HTML and it discarded all of the <p> tags that had the region actions on them. When using boolean attributes, it doesn't really matter what tag you use with the boolean attribute, since the tag itself doesn't contribute to the final document markup. So while we used paragraph tags here, they could have been any other tag (like <div>) and the result would be the same.

Adding HTML attributes

When using action attributes with value, along with a replace, prepend or append action, any HTML attributes you add to the action tag that do not begin with pw- or data-pw- will be added to the originally defined region tag. Meaning, you can add new attributes in the region definition tag. Lets say that you've got this region defined in your /site/templates/_main.php file…

<ul id="foo" class="bar">
  <li>First item</li>
</ul>

…and you've got this in your /site/templates/basic-page.php template file:

<ul pw-append="foo" title="Hello">
  <li>Second item</li>
</ul>

The final output would be this:

<ul id="foo" class="bar" title="Hello">
  <li>First item</li>
  <li>Second item</li>
</ul>

Note above how it added the title="Hello" attribute to the region tag. We could do the same with any other attribute. However, when it comes to class attribute, there is an even more useful behavior…

Adding and removing classes from class attribute

When specifying class attributes, the classes from the action tag and the region tag are merged. Meaning, classes you specify in a action tag behave as an "add class":

<!-- Region definition -->
<ul pw-id="mylist" class="foo">
  <li>One</li>
</ul>

<!-- Region action -->
<ul pw-append="mylist" class="bar">
  <li>Two</li>
</ul>

<!-- Resulting output -->
<ul class="foo bar">
  <li>One</li>
  <li>Two</li>
</ul>

If needed, you can specify "remove class" in the region action by prepending the class name you want to remove with a minus sign:

<!-- Region action -->
<ul pw-append="mylist" class="-foo bar">
  <li>Two</li>
</ul>

<!-- Resulting output -->
<ul class="bar">
  <li>One</li>
  <li>Two</li>
</ul>

Debugging regions

If you are getting an unexpected result with your markup regions, you may want some more information about what's happening with the markup regions behind the scenes. Included is a special comment you can add somewhere in your <html>...</html>, typically in your _main.php file:

<!--PW-REGION-DEBUG-->

That comment gets automatically replaced with a pre.pw-region-debug element that contains some basic debugging information about what it did:

3. replace => #content-head ... <h1 id='content-head'>
6. replace => #sidebar ... <aside id='sidebar'>
26. replace => #content-body ... <div class='uk-margin-top' id='content-body'>

Above we can see there were 3 replace actions in your template file replacing the contents of tags in our _main.php file. The number at the beginning of each line is a number PW uses internally to identify the region. The format is this:

number. action => region-id ... tag

Below the list of regions is a timer indicating the amount of time that was spent processing the regions.

Technical details of how markup regions work

  • Any markup output before the <html> tag (beginning of an HTML document) is considered to contain region actions – tags that will populate or modify regions.

  • Any markup output after the <html> tag is considered to contain region definitions – tags that can be populated and/or modified by the region actions.

That's it in a nutshell. This blends very well with the delayed output methodology, where your main HTML document markup is in your _main.php file, and the rest of your site template files output just the portions they want to populate or modify. An _init.php file is not typically necessary with markup regions, since regions are defined directly in the document markup rather than as PHP variables.

Markup regions example

Here we have a full example of using markup regions. First, lets look at a /site/templates/_main.php file, where our regions are defined. This is a complete HTML document. Every element with an ID attribute is accessible as a markup region, and all the values present in them represent the default output if the markup region is not replaced.

_main.php

<!DOCTYPE html>
<html lang='en'>
<head id='html-head'>
  <meta http-equiv='content-type' content='text/html; charset=utf-8'>
  <title id='html-title'><?=$page->title?></title>
</head>
<body id='html-body'>
  <div id='masthead'>
    <ul id='topnav'>
      <?php foreach($pages->get('/')->children as $item): ?>
        <li><a href='<?=$item->url?>'><?=$item->title?></a></li>
      <?php endforeach; ?>
    </ul>
  </div>
  <div id='content'>
    <h1 id='headline'>
      <?=$page->title?>
    </h1>
    <div id='bodycopy'>
      <?=$page->body?>
    </div>
    <div id='sidebar'>
      <?=$page->sidebar?>
    </div>
  </div>
  <div id='footer'>
    <p>Powered by ProcessWire</p>
    <p>Copyright <?=date('Y')?></p>
  </div>
</body>
</html>

The above _main.php represents our common document markup, though if we need something entirely different for any particular templates (like our homepage, RSS feed, or the like) we can do so by editing the template settings in the ProcessWire admin to use a different file, or no file at all, when appropriate. (This is found in Setup > Templates > [template] > Files [tab]).

Note all the elements defined with ID attributes in the _main.php file. Any of those can be targeted as a markup region, enabling us to replace, prepend, append, insert before or insert after any of them.

basic-page.php

Now lets look at a template file used by a Page. Here we have our basic-page.php template file. Let's say that we want to prepend sub-navigation in the sidebar. Here's how we might do that:

<ul class='subnav' pw-prepend='sidebar'>
  <?=$page->children->each("<li><a href='{url}'>{title}</a></li>")?>
</ul>

Maybe we want to insert a secondary headline after the h1 headline:

<h2 pw-after='headline'>
  <?=$page->summary?>
</h2>

Or maybe we want this template file to have a custom stylesheet:

<link rel='stylesheet' href='/custom.css' pw-append='html-head' />

The above gives you just a taste of what's possible with markup regions. For a more thorough example, see the “Regular” site profile that is included with a new copy of ProcessWire.

Latest news

  • ProcessWire Weekly #520
    In the 520th issue of ProcessWire Weekly we'll check out some of the latest additions to the ProcessWire module's directory, share some highlights from the latest weekly update from Ryan, and more. Read on!
    Weekly.pw / 27 April 2024
  • ProFields Table Field with Actions support
    This week we have some updates for the ProFields table field (FieldtypeTable). These updates are primarily focused on adding new tools for the editor to facilitate input and management of content in a table field.
    Blog / 12 April 2024
  • Subscribe to weekly ProcessWire news

“Yesterday I sent the client a short documentation for their ProcessWire-powered website. Today all features already used with no questions. #cmsdoneright—Marc Hinse, Web designer/developer