Skip to main content

Theme Development Overview

OQO themes are self-contained, Liquid-based template packages inspired by Shopify's theme architecture. Each theme includes everything it needs to render a complete website: layouts, sections, snippets, stylesheets, JavaScript, and configuration files. Nothing is shared between themes.

Themes use the Liquid templating language with OQO-specific tags and filters. If you have experience with Shopify theme development, you will feel right at home.

Core Principles

  • Self-contained -- Every theme bundles its own CSS, JavaScript, fonts, and images. No shared dependencies between themes.
  • Framework agnostic -- Use Tailwind CSS, Bootstrap, hand-written CSS, or any framework you prefer. OQO does not impose a CSS framework.
  • Settings-driven -- Colors, fonts, layout options, and content are configurable through a JSON schema. Site owners customize themes without editing code.
  • Live preview -- The visual editor shows changes instantly. Color and image settings hot-reload via CSS; everything else re-renders on the server.

Theme File Structure

my-theme/
├── assets/
│ ├── css/
│ │ ├── _variables.css.liquid # CSS variables from theme settings
│ │ ├── _base.css # Base element styles
│ │ ├── _components.css # Custom component classes
│ │ └── theme.css # Entry point (@imports partials)
│ ├── fonts/ # Self-hosted web fonts (.woff2)
│ ├── images/ # Theme images (logos, icons)
│ ├── framework.css # Pre-compiled CSS framework output
│ ├── theme.css # Bundled CSS (auto-generated by OQO)
│ └── theme.js # Theme JavaScript
├── config/
│ ├── settings_schema.json # Theme settings definition
│ └── settings_data.json # Current setting values
├── layouts/
│ ├── default.liquid # Standard page layout
│ ├── sidebar.liquid # Layout with sidebar zones
│ └── minimal.liquid # No header/footer
├── sections/ # Section templates (organized by category)
│ ├── nav/
│ ├── hero/
│ ├── content/
│ ├── cta/
│ ├── footer/
│ └── ...
├── snippets/ # Reusable Liquid partials
└── templates/ # Page-specific templates

Sections: The Building Blocks

Sections are the core building blocks of OQO themes. Each section is a single .liquid file that contains three parts: the template, the style block, and the schema. All three live together in one file.

Section Anatomy

Every section follows this structure:

{%- comment -%}
Section description
{%- endcomment -%}

{%- comment -%} ========== TEMPLATE ========== {%- endcomment -%}

{% section_wrapper 'aside' class: 'my-cta' %}
<div class="cta-content">
<h2>{{ section.settings.title }}</h2>
<p>{{ section.settings.body }}</p>

<a href="{{ section.settings.button.href }}" class="cta-btn">
{{ section.settings.button.text }}
</a>

{% if section.settings.image != blank %}
<img
src="{{ section.settings.image | image_url: 'hero' }}"
alt=""
loading="lazy"
>
{% endif %}
</div>
{% endsection_wrapper %}

{%- comment -%} ========== STYLE BLOCK ========== {%- endcomment -%}

{% style %}
[data-section-wrapper][data-section-id="{{ section.id }}"] {
--section-bgcolor: {{ section.settings.bgcolor | color_alpha_to_rgba }};
--section-text: {{ section.settings.text_color | color_alpha_to_rgba }};

background-color: var(--section-bgcolor);
color: var(--section-text);

.cta-btn {
background-color: var(--theme-button-primary-bg);
color: var(--theme-button-primary-text);
}
}
{% endstyle %}

{%- comment -%} ========== SCHEMA ========== {%- endcomment -%}

{% schema %}
{
"name": "Call to Action",
"description": "Simple CTA with title, body, image, and button.",
"category": "cta",
"settings": [
{
"type": "text",
"id": "title",
"label": "Title",
"default": "Get Started Today"
},
{
"type": "text",
"id": "body",
"label": "Body Text",
"html": true,
"nb_rows": 4
},
{
"type": "image",
"id": "image",
"label": "Image"
},
{
"type": "link",
"id": "button",
"label": "Button",
"with_text": true
},
{
"type": "color_alpha",
"id": "bgcolor",
"label": "Background Color"
},
{
"type": "color_alpha",
"id": "text_color",
"label": "Text Color"
}
],
"blocks": []
}
{% endschema %}

The Three Parts Explained

1. Template

The HTML structure of the section. Wrap it with {% section_wrapper %} to get automatic scoping attributes:

{% section_wrapper 'section' class: 'my-hero' %}
<!-- Your HTML here -->
{% endsection_wrapper %}

The first argument is the HTML tag (section, div, aside, nav, article, footer). OQO automatically adds:

  • A data-section-id attribute for CSS scoping
  • A data-section-type attribute for identification
  • A gz-section class plus a type-specific class like gz-cta-cta_01

Access section settings with {{ section.settings.setting_id }} and iterate over blocks with {% for block in section.blocks %}.

2. Style Block

The {% style %}...{% endstyle %} block defines CSS variables and simple rules that change based on section settings. It is extracted and compiled into a separate CSS file per section instance.

{% style %}
[data-section-wrapper][data-section-id="{{ section.id }}"] {
--section-bgcolor: {{ section.settings.bgcolor | color_alpha_to_rgba }};

background-color: var(--section-bgcolor);
}
{% endstyle %}

Key rules for style blocks:

  • Always scope with [data-section-wrapper][data-section-id="{{ section.id }}"]
  • Use --section-* prefix for section-level CSS variables
  • Reference theme variables with --theme-* (e.g., var(--theme-heading-color))
  • Keep it lightweight -- only settings-driven values belong here
  • Animations, hover effects, and static styles go in the theme stylesheet (_components.css or _sections.css), not here

Style blocks enable CSS hot-reload in the visual editor. When a user changes a color, only the CSS reloads -- no HTML re-render needed.

3. Schema

The {% schema %} block is a JSON definition that declares:

  • name and category -- used in the section picker UI
  • settings -- configurable fields (text, color, image, checkbox, range, link, etc.)
  • blocks -- repeatable nested content groups (cards, slides, menu items)
  • presets -- default configurations for the section picker
{
"name": "Features",
"category": "content",
"settings": [ ... ],
"blocks": [
{
"type": "feature",
"name": "Feature Card",
"settings": [
{ "type": "text", "id": "title", "label": "Title" },
{ "type": "text", "id": "description", "label": "Description" }
]
}
]
}

Blocks are iterated in the template:

{% for block in section.blocks.feature %}
<div class="card">
<h3>{{ block.settings.title }}</h3>
<p>{{ block.settings.description }}</p>
</div>
{% endfor %}
tip

Filter blocks by type with section.blocks.feature or iterate all blocks with section.blocks. Blocks can also be nested -- access children with block.blocks.

Layouts

Layouts define the overall page structure -- the HTML wrapper around page content. Every layout must include three required tags:

TagPurpose
{% theme_css %}Loads all theme and section CSS
{% theme_javascript %}Loads theme JavaScript and editor overlay
{% content_for_layout %}Outputs page-specific sections

Basic Layout

<!DOCTYPE html>
<html lang="{{ request.locale | default: 'en' }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ page.title | default: settings.site_name }}</title>
{% theme_css %}
</head>
<body class="gz-theme">

{% zone 'header' tag: 'header' %}{% endzone %}

<main>
{% content_for_layout %}
</main>

{% zone 'footer' tag: 'footer' %}{% endzone %}

{% theme_javascript %}
</body>
</html>

Zones

Zones are named placeholders where site-wide sections (navigation, footer, sidebars) are placed. Define them with the {% zone %} tag:

{% zone 'header' %}{% endzone %}
{% zone 'sidebar' tag: 'aside' class: 'site-sidebar' %}{% endzone %}
{% zone 'footer' %}{% endzone %}
  • Zone names are free-form -- use any name. They become labels in the admin UI: left_sidebar becomes "Left Sidebar".
  • Zones are auto-discovered -- just add them to a layout template and they appear in the admin.
  • Zone options -- add tag, class, or id to wrap zone content in an HTML element.
  • Default content -- put fallback HTML between the zone tags for when no sections are assigned.

Zone sections are shared across all pages that use the same layout.

Layout Inheritance

Layouts can extend other layouts with {% extends %}, enabling DRY architecture:

{%- comment -%} layouts/base.liquid {%- endcomment -%}
<!DOCTYPE html>
<html>
<head>{% theme_css %}</head>
<body>
{% zone 'header' %}{% endzone %}
{% zone 'main' %}{{ content_for_layout }}{% endzone %}
{% zone 'footer' %}{% endzone %}
{% theme_javascript %}
</body>
</html>
{%- comment -%} layouts/landing.liquid -- extends base {%- endcomment -%}
{% extends 'base' %}

{% zone 'header' %}
<header class="landing-header">
<h1>Landing Page</h1>
</header>
{% endzone %}

{%- comment -%} main and footer zones are inherited from base {%- endcomment -%}
{% endextends %}

Child layouts override specific zones while inheriting everything else. Use {{ zone.super }} inside an override to include the parent zone's content.

Multi-Column Layouts

Use {% content_for_layout %} with filters to distribute page sections into columns:

<div class="grid-3col">
<aside>{% content_for_layout column: 'left' %}</aside>
<main>{% content_for_layout column: 'center' %}</main>
<aside>{% content_for_layout column: 'right' %}</aside>
</div>

For this to work, sections need a column setting in their schema that matches the filter values.

The post Tag

The {% post %} tag resolves and renders a single blog post. It handles URL parameters, preview keys, and fallback defaults.

{% post use_params: true default: 'my-default-post' %}
{% if found %}
<article>
<h1>{{ post.title }}</h1>
<time>{{ post.published_date }}</time>

{% if post.has_elements %}
{% for element in post.elements %}
{% case element.type %}
{% when 'rich_text' %}
<div class="prose">{{ element.content }}</div>
{% when 'media' %}
{% if element.image? %}
<img src="{{ element.image | image_url: 'large' }}" alt="{{ element.image.alt }}">
{% endif %}
{% endcase %}
{% endfor %}
{% else %}
<div class="prose">{{ post.content_html }}</div>
{% endif %}
</article>
{% else %}
<p>Post not found</p>
{% endif %}
{% endstory %}

Tag Attributes

AttributeTypeDefaultDescription
use_paramsbooleantrueResolve post from URL parameters
param_namestring"post"URL parameter name to read the slug from
defaultstring--Fallback post slug when no URL parameter is present

Resolution Order

  1. Preview key -- ?preview_key=xxx for shareable preview links
  2. URL parameters -- ?post=slug or the custom param_name
  3. Default slug -- fallback from the default attribute

Block Variables

Inside the {% post %}...{% endstory %} block, these variables are available:

VariableTypeDescription
foundbooleantrue if the post was resolved
postPostDropThe post object (see Liquid Objects)

CSS Compilation

OQO handles CSS compilation automatically. You only need to understand the separation of concerns:

What You Edit vs. What OQO Generates

FileWho EditsPurpose
assets/css/theme.cssYouEntry point that @imports your CSS partials
assets/css/_variables.css.liquidYouCSS variables derived from theme settings
assets/css/_components.cssYouCustom component styles
assets/framework.cssYour build toolPre-compiled framework (Tailwind, Bootstrap, etc.)
assets/theme.cssOQO (auto-generated)Bundled output: framework + variables + theme CSS
assets/bundle.cssOQO (auto-generated)All CSS merged (theme + all section CSS)
assets/sections/*.cssOQO (auto-generated)Per-section CSS from {% style %} blocks

How It Works

  1. You provide a CSS framework output (framework.css) and theme stylesheets
  2. OQO bundles them into a single theme.css by resolving @import statements and rendering Liquid variables
  3. Section CSS from {% style %} blocks is extracted and compiled into separate files per section instance
  4. In the editor, section CSS hot-reloads when settings change -- no page refresh needed

For Tailwind Themes

If you use Tailwind CSS, compile it locally:

tailwindcss -i assets/css/tailwind.css -o assets/framework.css --watch

Your tailwind.css source should scan your theme's Liquid files:

@import "tailwindcss";

@source "../../sections/**/*.liquid";
@source "../../layouts/**/*.liquid";
@source "../../snippets/**/*.liquid";

For Non-Framework Themes

If you prefer hand-written CSS, put all your styles in framework.css and skip any build step. The _variables.css.liquid file still handles dynamic CSS variables from theme settings.

Snippets

Snippets are reusable Liquid partials stored in the snippets/ directory. Include them with {% render %}:

{% render 'post_card', post: post, show_date: true %}

Variables must be passed explicitly -- snippets do not inherit the parent scope.

caution

The {% render %} tag requires literal snippet names. You cannot use variables: {% render my_variable %} will not work. Use conditionals instead.

Developer CLI (oqo)

OQO provides a command-line tool for local theme development. Work in your own editor with your own toolchain, and sync changes to your site's sandbox theme in real time.

oqo login                     # authenticate with your site
oqo theme init my-theme # scaffold a new theme
cd my-theme
oqo theme dev # watch + live sync on save

The CLI also includes AI-powered section creation -- describe what you want in plain language and oqo generates the template, schema, and CSS for you.

See the OQO CLI page for installation, full command reference, and workflow examples.


Best Practices

Section Development

  • One file per section -- template, style block, and schema all live in the same .liquid file
  • Use section_wrapper -- always wrap section content for proper scoping and editor support
  • Style block for settings-driven CSS only -- animations, hover effects, and complex styles belong in _components.css or _sections.css
  • Use CSS variables -- reference --theme-* for global values and --section-* for section-specific values
  • Use color_alpha_to_rgba for color settings -- it handles both hex strings and {color, alpha} objects
  • Check for blank before rendering optional content: {% if section.settings.image != blank %}

CSS

  • Never edit auto-generated files -- theme.css (in assets root) and bundle.css are overwritten on compilation
  • Never edit framework.css manually -- it is regenerated by your CSS framework build tool
  • Use --section-* prefix for section CSS variables, --theme-* for global variables
  • Use hyphens in CSS variable names -- --section-bg-color, not --section-bg_color
  • Keep {% style %} blocks lightweight -- they are re-rendered on every settings change

Theme Settings

  • Define all settings in settings_schema.json -- group them into labeled sections
  • Provide sensible defaults -- every setting should work out of the box
  • Use _variables.css.liquid to map settings to CSS custom properties

General

  • Organize sections by category -- sections/nav/, sections/content/, sections/footer/, etc.
  • Extract repeated markup into snippets -- keep sections DRY
  • Test with the visual editor -- verify settings, blocks, and live preview all work correctly
  • Use {% comment %} blocks to document complex template logic