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-idattribute for CSS scoping - A
data-section-typeattribute for identification - A
gz-sectionclass plus a type-specific class likegz-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.cssor_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 %}
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:
| Tag | Purpose |
|---|---|
{% 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_sidebarbecomes "Left Sidebar". - Zones are auto-discovered -- just add them to a layout template and they appear in the admin.
- Zone options -- add
tag,class, oridto 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
| Attribute | Type | Default | Description |
|---|---|---|---|
use_params | boolean | true | Resolve post from URL parameters |
param_name | string | "post" | URL parameter name to read the slug from |
default | string | -- | Fallback post slug when no URL parameter is present |
Resolution Order
- Preview key --
?preview_key=xxxfor shareable preview links - URL parameters --
?post=slugor the customparam_name - Default slug -- fallback from the
defaultattribute
Block Variables
Inside the {% post %}...{% endstory %} block, these variables are available:
| Variable | Type | Description |
|---|---|---|
found | boolean | true if the post was resolved |
post | PostDrop | The 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
| File | Who Edits | Purpose |
|---|---|---|
assets/css/theme.css | You | Entry point that @imports your CSS partials |
assets/css/_variables.css.liquid | You | CSS variables derived from theme settings |
assets/css/_components.css | You | Custom component styles |
assets/framework.css | Your build tool | Pre-compiled framework (Tailwind, Bootstrap, etc.) |
assets/theme.css | OQO (auto-generated) | Bundled output: framework + variables + theme CSS |
assets/bundle.css | OQO (auto-generated) | All CSS merged (theme + all section CSS) |
assets/sections/*.css | OQO (auto-generated) | Per-section CSS from {% style %} blocks |
How It Works
- You provide a CSS framework output (
framework.css) and theme stylesheets - OQO bundles them into a single
theme.cssby resolving@importstatements and rendering Liquid variables - Section CSS from
{% style %}blocks is extracted and compiled into separate files per section instance - 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.
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
.liquidfile - 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.cssor_sections.css - Use CSS variables -- reference
--theme-*for global values and--section-*for section-specific values - Use
color_alpha_to_rgbafor 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) andbundle.cssare overwritten on compilation - Never edit
framework.cssmanually -- 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.liquidto 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