For a long time, the practice of "hard-coding" blocks into your theme template has been the generally accepted solution to the problem of having block content on all pages (either across the entire site or of a certain page type) and ensuring that site editors don't need to remember to add it and also prevent them from accidentally removing it or changing its settings.
Heck, I even wrote a blog post a while back that went into detail on how to do this with the autonav block.
But since then, I've come around and realized that hard-coding blocks in theme templates is not a good idea, for several reasons:
- Hardcoding blocks bypasses C5 caching. This can be especially problematic for autonav menus on very large sites.
- Most blocks cannot actually be successfully hard-coded. There are many reasons why this might be the case, but the biggest one is that it only works if there is a single table of data in the block's database schema (db.xml file). So blocks that contain multiple sub-items (such as the built-in "slideshow" block and most image galleries on the marketplace) just plain don't work and there's no way to make them work. This inconsistency about how some blocks work and others don't only makes theme development more confusing than it already is for newbies.
- It is challenging to figure out exactly what the available settings are for a block, and there is generally very little documentation. Your best bet is to examine the block's db.xml file, but even knowing what the fields are doesn't necessarily tell you what valid values are.
Fortunately, there is a solution available that works all the way back to Concrete5.5: use a "Global Area" and disable its editing controls on all pages except the "Page Defaults", like so:
<?php $a = new GlobalArea('Your Area Name'); if ($c->getMasterCollectionID() != $c->getCollectionID()) { $a->disableControls(); } $a->display(); ?>
Once you have that in your theme template, go to the dashboard "Page Defaults" for any page type that it appears on and place the block into the area there. Now it will show up on all pages, and users will not be able to remove it or change its settings when in edit mode. Plus, it works in almost every situtuation and you get to leverage C5's caching. Sweet!
Comments
This might be one of *those* situations, but is it possible to pass variables to blocks inside GlobalAreas?
For example I have a multilingual Autonav hardcoded as it takes a custom cID.
$autonav = BlockType::getByHandle('autonav');
$autonav->controller->displayPagesCID = $page_id;
Hmm... that is a good question. Usually there is a way to configure the block settings when you add it to a page to get the results you want (for example with the autonav block, you can tell it to show pages beneath a particular parent, or "at the current level", etc.). But if the parent page is truly arbitrary and needs to be fed dynamically by some php code, then yeah I guess hardcoding is the only option. (Although at that point I'd probably build my own custom block that utilizes the PageList library... but that is definitely a bit more advanced then some people are willing or able to get).
Can I ask where the $page_id in your example comes from exactly?
$lh = Loader::helper('section', 'multilingual');
$lang_dir = $lh->section();
$page = (empty($lang_dir)) ? Page::getByID('1') : Page::getByPath($lang_dir);
$page_id = $page->getCollectionID();
I unfortunately don't have much experience with multi-lingual sites, so this is just a guess here (so if I'm wrong, then yeah it might be one of those situations where hardcoding a block is unavoidable)...
You could create different global areas for each language. Using a technique similar to "Sectionwide Areas" ( http://concrete5tricks.com/blog/sectionwide-areas/ ), you could actually use the current locale name as part of the area name, which will cause C5 to dynamically create separate global areas for each locale. For example, you could make a global area like the following:
<?php
$lang_dir = Loader::helper('section', 'multilingual')->section();
$nav_area_name = 'Nav Menu' . (empty($lang_dir) ? '' : ' - ' . $lang_dir);
$a = new GlobalArea($nav_area_name);
if ($c->getMasterCollectionID() != $c->getCollectionID()) {
$a->disableControls();
}
$a->display();
?>
Now you can add a separate autonav block to the same global area for each language of your site, and set the language's top-level page in the autonav block settings as the parent page.
I'll try it out and report back.
One of the main reasons why we hard-code blocks is to try and separate development changes from content (the database) as much as possible.
I realise changes could still be made to block templates but setting things programatically and pushing changes to the site without having to replicate local changes in the database is less time consuming (and safer).
While napping on the job, I sometimes dream of a CMS where content and development is completely separate (even split into separate databases) - impossible but it would be nice!
Ah yes, that is a good use-case for hardcoding the block.
And I partially share your dream of a better split between content and configuration/design... the challenge is where to draw the line, because in most situations you do want your users to have some flexibility to modify configuration via the dashboard. One approach that I think works nicely is to save certain things to flat files instead of to the database, but have a dashboard interface for editing the data in those files -- this gives you the best of both worlds, because non-technical people still have a GUI to work with, but the result of it is saved to a file which can live in version control.
There are quite a few CMS's that work this way (and to varying extents). Check out Statamic and Kirby (neither of which is free or open source).
There are a lot of things about Concrete5 that are not perfect, but at the end of the day its content editing interface is so much better than every other system out there (that I've encountered) that I find it's worth making my development job a bit more difficult so that my clients' editing job can be as easy as possible (in my opinion that's what I'm getting paid to do).
Cheers,
Jordan
A quick followup... I just saw that version 2 of the "Bolt" CMS was recently released. Playing around with it really quickly, I can see that they put a LOT of configuration stuff in yaml files: https://bolt.cm/
It's a free/open-source CMS so worth playing around with. It lacks front-end editing, but is very flexible for setting up page types with custom fields, etc. Although in my opinion it seems to take the "everything in configuration files" too far... for example, the nav menus are generated by you putting page entries into a config file (instead of being automatically populated from the site structure itself)... which just seems wrong to me. Also it doesn't seem to actually show an overall "sitemap" in the admin, instead it just has flat lists of each "page type" you set up... which is very confusing to me. I think they are coming from a wordpress mindset, where everything is really an extension of a "blog post", whereas the C5 mindset is more "everything is a page in the site, blog posts are a special case of that".
Anyway, I could ramble on about this stuff for days... but suffice to say I think bolt is a great example of seeing the benefits and the drawbacks of separating content from configuration entirely.
-Jordan
Thanks for the article. Sometimes I hardcode a pagelist to get the 3 latest blogs and display them on the home page. I used two different methods.
1. $pl = BlockType::getByHandle('page_list'); //does not seem to work in 5.7
In the view I call the custom template.
2. $pl = new PageList();
I wonder if method two by passes the C5 cache? And what would be your advice in this situation?
I don't know how it works in 5.7 because I haven't been using it yet. But in 5.6, just using a call to the PageList class in your template will *not* be cached. It's possible that this isn't really a big issue... if it's a simple page list that only pulls a few pages and you don't have thousands of pages total on the site, I doubt there would be a noticeable difference. But if you did want to benefit from C5's block caching, what I'd do is create a super basic block that contains the pagelist functionality. For example: http://pastie.org/9855230
However, note that this will only work when you are not using pagination on the page list. If you want pagination, then you can't use block caching because it causes the url querystring args to be ignored.
-Jordan
After checking Block workflow, hardcoded block seems to be cached.
This is the workflow :
1/ Get block by handle
2/ Return block type controller
3/ This Controller extend BlockController wiich build the whole block (block record + block view) and use cache if enabled/available
-moosh
Most of the code I see around the forums and that I've used before doesn't actually return a block type controller. For example: http://c5blog.jordanlev.com/blog/2012/04/hard-coded-autonav-options/
Do you have any sample code you could show so I can see how it is you're achieving this? (Maybe link to http://pastie.org or https://gist.github.com because there is no code formatting in these comments sections).
Thanks!
-Jordan
Commenting has been disabled.