A few arbitrary tips on Omeka-S theming.

Retain existing classes on analogous elements as far as possible, as they are sometimes referred to by internal Omeka CSS classes. Obviously you can imagine edge cases where this isn't desirable. But it's a good first rule to follow. Obviously if you omit some design-element entirely you must also omit the class associated with it.

In a similar vein, be wary of doing less than the requirements of a theme. There's no real verification or way of testing that a theme handles all paths sensibly and in a manner consistent with the expectations of the user.

For example, there are certain site settings your theme should support: "embed media on item page" is a site specific setting that you must handle within your theme, but there is no validation that your theme does in fact handle this.

Do not ever use <p></p> elements in your module markup. Use custom divs or spans with your own classes. Why? The Html blocklayout generates <p> tags, so any attempt to style page text will often inadvertently style your more-irregular p tags.


Debugging templates: a very rudimentary debugging method is to fill your partials with introductory HTML comments naming their source file.


<!-- BEGIN view/omeka/site/item/show.phtml -->
<p>This is content this is content this is content</p>
<!-- END view/omeka/site/item/show.phtml -->

One way to modularize your CSS is to break out the per-page CSS into individual CSS files, then include them in layout.phtml


Here, item-show.css contains rules associated to the item detail page.

Don't try to remove the stock JS loads shown below from your layout.phtml. These are internal Omeka files that in turn may cause the loading of more than one JS file.

$this->headScript()->prependFile($this->assetUrl('js/global.js', 'Omeka'));
$this->headScript()->prependFile($this->assetUrl('vendor/jquery/jquery.min.js', 'Omeka'));

If you need to style the 'page title' block which is available in the page editor, you don't have a lot of good options, because it's not addressable by any specific CSS style, being defined in its own PHP implementation. You just have the choice to do the following:

.my-page-content > .blocks > h2 {
    background-color: var(--accent2);
    text-align: center;
    font-family: "Quicksand Bold";
    padding: 1em 1em;
    margin: 0 0;
    margin-bottom: 1em;

Where my-page-content is a class applied to the page content using an override on view/omeka/site/page/show.phtml. .blocks is created by the framework. You have to just assume that every h2 block is a page title -- this means your own modules and markup can't generally use h2; you must stick to div and theme-specific classes.

Module development

Which modules define new blocks? The answer, current as of 2020-11-24, is:

  • FieldsAsTags
  • Folksonomy
  • Reference
  • Collecting
  • Scripto
  • SimpleCarousel
  • UniversalViewer
  • Mapping


Conventions for naming data values stored in blocks: use snake_case. There is limited precedent in the upstream source for this convention, specifically the ListOfSites block-layout.

When passing data items through to partials, you should translate them into camelCase. That will cause the .phtml file to continue to look relatively idiomatic.

Javascript-laden pages

There are some cases where you have some significant amount of JavaScript involved with the default theme. This JS will hardcode certain IDs and classes to identify HTML elements. This is a big problem if you plan to significantly restructure the HTML. For those pages I suggest, where possible, to stick to the default templates as closely as possible, while wrapping the whole piece in a theme-specific div in order to scope your specific styles. The goal is to stick to the original markup 100%, while applying all tweaks in CSS, a la "Zen Garden". This may not always be possible.

Creating global config values

There are many levels that configuration can exist at within Omeka-S. An incomplete list:

  • Configuration can obviously exist at the level of individual blocks within a page.
  • Configuration can exist specific to a theme; this is declared within the [config] block of theme.ini.
  • Configuration can exist that is specific to a site. An example of this is Collecting: this module defines its "terms of service" field as a site-specific config value.
  • A module can provide its own module-level configuration which is accessible via a form within the module admin page.

We are going to talk about the last one of those here. There are two points of contact with the API surface: the first is the mandatory Module.php file required for a module, the second is the form-specific class, although this second is actually not strictly necessary. The key methods are getConfigForm() and handleConfigForm(), the latter using the Omeka\Settings service. This is stored in the setting table in MySQL. Note that there's a flagrant bug in the below code: we should be explicitly fetching the setting value from the service before we render the form; as shown, we won't show the existing value when we load up the form. This is left as an exercise to the reader.

Note that the string-valued name of the setting, here qhs_module_action_bubble_text, exists in a global namespace, so should be chosen carefully so as not to clash with any other installed modules nor those of Omeka-S itself.

class Module extends AbstractModule {
    public function getConfig() {
        return include __DIR__ . '/config/module.config.php';

    public function getConfigForm(PhpRenderer $renderer) {
        $services = $this->getServiceLocator();
        $form = $services->get('FormElementManager')->get(ConfigForm::class);
        $html = $renderer->formCollection($form);
        return $html;

    public function handleConfigForm(AbstractController $controller) {
        $settings = $this->getServiceLocator()->get('Omeka\Settings');
        $form = new ConfigForm;
        if ($form->isValid()) {
            $formData = $form->getData();
            $settings->set('qhs_module_action_bubble_text', $formData['content']);
            return true;
        } else {
            return false;
namespace QhsModule\Form;

use Zend\Form\Form;
use Zend\Form\Element;

class ConfigForm extends Form {
    public function init() {
            'name' => 'content',
            'type' => Element\Text::class,
            'options' => [
                'label' => 'Action bubble content',
                'info' => 'This text will be displayed inside the action bubble.',

Collecting module

Collecting is one of the most complex modules available for Omeka-S; its job is to solicit contributions to the archive from outside visitors. We consider Collecting v1.3.0. Collecting has its documentation within the Omeka-S manual. It's arguably on the verge between being a core feature (it creates its own database tables, for instance).

Database tables

  • collecting_form
  • collecting_input
  • collecting_item
  • collecting_prompt
  • collecting_user

Collecting will automatically try to send out a confirmation email. That assumes it can actually access an MTA.

Each form is made out of prompts, which can be for a variety of things; they can be prompts for any resource value which will be stored onto the created item

You add collecting forms to pages by using the special block layout that the module creates.

User public and user private need to be added as separate prompts.

The collecting form generates its own H2, using its custom partials which appear under the collecting directory in the view part. collecting/site/index/success.phtml for example. So any page that includes a collecting form should probably not have its own page title, which also generates an H2 element.

Forms will receive ids like 'collecting_form' rather than any id, so you may attempt to use that to target them via CSS as below; yes, this is hacky and disgusting. If you've got a better idea, I'm all ears.

form[id^="collecting_form_"] {