Routing & URLs
OQO uses a cascading URL system where any content -- posts, pages, collections, or blogs -- can appear at any depth in the URL hierarchy. Posts, collections, and blogs share a single slug namespace per site, ensuring URLs are unique and predictable. Pages live in their own namespace and can intentionally reuse a slug to override content (see Page Overlays).
/ --> Home page
/about --> Page "About"
/food --> Collection "Food"
/best-pizza-recipe --> Post "Best Pizza Recipe"
/food/best-pizza-recipe --> Post with parent context
/italy/food/pizza/best-pizza-recipe --> Deep cascading URL
/tags/italian --> Tag listing
/travel-blog --> Blog "Travel Blog"
/travel-blog/my-trip-to-rome --> Blog post with blog prefix
Posts, collections, and blogs share a single slug namespace -- no two can have the same slug. Content is always reachable by its slug alone, regardless of how deep the URL path is. Pages can reuse a slug to temporarily override what's displayed at that URL.
URL Patterns
| Pattern | Example | Resolves To |
|---|---|---|
/ | / | Home page (system page) |
/{slug} | /about | Page, Post, Collection, or Blog |
/{parent}/{content} | /food/pizza-recipe | Content with parent context |
/{p1}/{p2}/.../content | /italy/food/pizza/best-margherita | Deep cascading URL |
/tags/{slug} | /tags/italian | Tag listing page |
/{blog-slug}/{post-slug} | /travel-blog/my-trip | Blog post (blog prefix required) |
Blog-Prefixed Post URLs
Posts that belong to a blog always include the blog slug as a prefix. This is automatic -- {{ post.url }} returns the correct path:
<!-- Post without a blog -->
{{ post.url }}
<!-- Output: /best-pizza-recipe -->
<!-- Post belonging to a blog called "Travel Blog" (slug: travel-blog) -->
{{ post.url }}
<!-- Output: /travel-blog/my-trip-to-rome -->
You do not need to construct blog-prefixed URLs manually. The url property on a post handles this for you.
Context Variables
When a URL resolves to content, OQO sets several variables in the Liquid template context. These are available in layout files and all sections rendered on the page.
content
The primary content object being rendered. Works for any content type -- posts, pages, collections, and blogs all use the same variable.
{{ content.title }} <!-- Works for any content type -->
{{ content.type }} <!-- "post", "page", "collection", or "blog" -->
{{ content.slug }} <!-- URL slug -->
{{ content.url }} <!-- Full URL path -->
{{ content.canonical_url }} <!-- Shortest path to this content -->
{{ content.description }} <!-- Description or teaser text -->
content_type
A string indicating the type of content being rendered: "post", "page", "collection", or "blog".
{% case content_type %}
{% when 'post' %}
{% section 'post/article' %}
{% when 'collection' %}
{% section 'content/post_grid' %}
{% when 'blog' %}
{% section 'blog/feed' %}
{% when 'page' %}
{% content_for_layout %}
{% endcase %}
Type-Specific Variables
In addition to content, OQO also sets a type-specific variable matching the content type. This lets you write templates that target a specific type directly:
{% if post %}
<article>
<h1>{{ post.title }}</h1>
<div>{{ post.content_html }}</div>
</article>
{% endif %}
{% if collection %}
<h1>{{ collection.title }}</h1>
{% for post in collection.posts %}
<a href="{{ post.url }}">{{ post.title }}</a>
{% endfor %}
{% endif %}
{% if tag %}
<h1>Posts tagged: {{ tag.name }}</h1>
{% endif %}
{% if blog %}
<h1>{{ blog.title }}</h1>
{% for post in blog.posts %}
<a href="{{ post.url }}">{{ post.title }}</a>
{% endfor %}
{% endif %}
parents
An array of parent objects from the URL path. Each parent is a content drop (PostDrop, PageDrop, CollectionDrop, etc.) with all the usual properties.
{{ parents }} <!-- Array of parent objects -->
{{ parent }} <!-- The immediate parent (last item in parents array) -->
For a URL like /italy/food/pizza/best-margherita:
parents[0]= Collection "Italy"parents[1]= Collection "Food"parents[2]= Collection "Pizza"content= Post "Best Margherita"
For a URL like /best-margherita (no parents):
parents= empty arraycontent= Post "Best Margherita"
URL Generation
Post URLs
Use {{ post.url }} to get the URL for a post. Blog-prefixed URLs are handled automatically.
<a href="{{ post.url }}">{{ post.title }}</a>
<!-- Output: /my-post -->
<!-- Or: /travel-blog/my-post (if post belongs to a blog) -->
Collection URLs
Use the collection_url filter to generate a collection URL.
<a href="{{ collection | collection_url }}">{{ collection.title }}</a>
<!-- Output: /food -->
Or use the url property directly:
<a href="{{ collection.url }}">{{ collection.title }}</a>
Tag URLs
Tags use a special /tags/ prefix. Use the tag_url filter:
<a href="{{ tag | tag_url }}">{{ tag.name }}</a>
<!-- Output: /tags/italian -->
{% for tag in post.tags %}
<a href="{{ tag | tag_url }}">{{ tag.name }}</a>
{% endfor %}
Page URLs
Use the page_url filter. The home page returns /, and system template pages return an empty string (they have no public URL).
<a href="{{ page | page_url }}">{{ page.title }}</a>
<!-- Output: /about -->
{{ home_page | page_url }}
<!-- Output: / -->
Blog URLs
Blogs use their slug directly:
<a href="{{ blog.url }}">{{ blog.title }}</a>
<!-- Output: /travel-blog -->
<!-- RSS feed -->
<a href="{{ blog.feed_url }}">Subscribe</a>
<!-- Output: /travel-blog/feed -->
Canonical URLs
Every content object has a canonical_url property -- the shortest possible path to that content. Use this for SEO:
<link rel="canonical" href="{{ content.canonical_url | absolute_url }}">
The canonical_url strips any parent context. For a post at /italy/food/pizza/best-margherita, the canonical URL is /best-margherita (or /travel-blog/best-margherita if it belongs to a blog).
Absolute URLs
Convert any relative path to a full URL with the absolute_url filter:
{{ content.canonical_url | absolute_url }}
<!-- Output: https://yoursite.com/best-margherita -->
{{ post.url | absolute_url }}
<!-- Output: https://yoursite.com/travel-blog/my-post -->
Breadcrumbs
The parents array makes breadcrumb navigation straightforward. Each parent has title and url properties.
Basic Breadcrumbs
<nav class="breadcrumbs">
<a href="/">Home</a>
{% for p in parents %}
<span class="separator">/</span>
<a href="{{ p.url }}">{{ p.title }}</a>
{% endfor %}
<span class="separator">/</span>
<span class="current">{{ content.title }}</span>
</nav>
For the URL /italy/food/pizza/best-margherita, this renders:
Home / Italy / Food / Pizza / Best Margherita
Breadcrumbs with Structured Data
Add schema.org structured data for search engines:
<nav class="breadcrumbs" aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
{% for p in parents %}
<li><a href="{{ p.url }}">{{ p.title }}</a></li>
{% endfor %}
<li aria-current="page">{{ content.title }}</li>
</ol>
</nav>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "{{ '/' | absolute_url }}" }
{% for p in parents %}
,{
"@type": "ListItem",
"position": {{ forloop.index | plus: 1 }},
"name": "{{ p.title }}",
"item": "{{ p.canonical_url | absolute_url }}"
}
{% endfor %}
,{
"@type": "ListItem",
"position": {{ parents.size | plus: 2 }},
"name": "{{ content.title }}"
}
]
}
</script>
System Pages
System pages are template pages that define how different content types are rendered. They are created automatically when a site is set up, cannot be deleted, and do not have their own public URLs.
| System Role | Template For | URL |
|---|---|---|
home | Homepage | / |
post | Individual posts | /{post-slug} |
collection | Collection listings | /{collection-slug} |
tag | Tag listings | /tags/{tag-slug} |
blog | Blog pages | /{blog-slug} |
How System Pages Work
When a visitor navigates to /best-margherita and that slug belongs to a post, OQO:
- Resolves the URL to a Post object
- Finds the post system page (the template)
- Renders the post system page's sections with
post,content, andcontent_typeset in the template context
The same pattern applies to collections, tags, and blogs. The system page provides the layout and sections; the context variables provide the data.
Customizing System Page Templates
System pages use sections just like regular pages. To customize how posts look on your site, edit the sections assigned to the post system page. You can use any section, but the most useful ones are those that read from content or the type-specific variables (post, collection, tag, blog).
Example: Post template section
<article>
<h1>{{ post.title }}</h1>
<div class="meta">
By {{ post.author.name }} | {{ post.published_at | date_format: 'medium' }}
</div>
{% if post.cover_image %}
<img src="{{ post.cover_image | image_url: 'hero' }}"
alt="{{ post.cover_image.alt | default: post.title }}">
{% endif %}
<div class="content">
{{ post.content_html }}
</div>
<div class="tags">
{% for tag in post.tags %}
<a href="{{ tag | tag_url }}">{{ tag.name }}</a>
{% endfor %}
</div>
</article>
Example: Collection template section
<div class="collection">
<h1>{{ collection.title }}</h1>
{% if collection.description %}
<p>{{ collection.description }}</p>
{% endif %}
<div class="posts-grid">
{% for post in collection.posts %}
<article class="post-card">
<a href="{{ post.url }}">
{% if post.image %}
<img src="{{ post.image | image_url: 'medium' }}" alt="">
{% endif %}
<h2>{{ post.title }}</h2>
<p>{{ post.teaser }}</p>
</a>
</article>
{% endfor %}
</div>
</div>
Post Navigation
Navigate between posts within a collection using the navigation filters.
next_post / prev_post
{% if collection %}
{% assign next = collection | next_post: content %}
{% assign prev = collection | prev_post: content %}
<nav class="post-navigation">
{% if prev %}
<a href="{{ prev.url }}" class="nav-prev">
← {{ prev.title }}
</a>
{% endif %}
{% if next %}
<a href="{{ next.url }}" class="nav-next">
{{ next.title }} →
</a>
{% endif %}
</nav>
{% endif %}
first_post / last_post
{% assign first = collection | first_post %}
{% assign last = collection | last_post %}
<a href="{{ first.url }}">Start from the beginning</a>
<a href="{{ last.url }}">Jump to latest</a>
Conditional Rendering by Type
When building layout templates or universal sections, use content_type to render different markup for each content type:
{% case content_type %}
{% when 'post' %}
{% section 'post/article' %}
{% when 'collection' %}
{% section 'content/post_grid' %}
{% when 'blog' %}
{% section 'blog/feed' %}
{% when 'page' %}
{% content_for_layout %}
{% endcase %}
Or use the content.type property:
{% if content.type == 'post' %}
<article class="post">
<h1>{{ content.title }}</h1>
{{ post.content_html }}
</article>
{% elsif content.type == 'collection' %}
<div class="collection">
<h1>{{ content.title }}</h1>
{% for post in collection.posts %}
<a href="{{ post.url }}">{{ post.title }}</a>
{% endfor %}
</div>
{% endif %}
SEO Best Practices
Canonical URLs
Always set canonical URLs to prevent duplicate content issues. Content may be reachable via multiple paths (e.g., /best-margherita and /food/best-margherita), but the canonical URL is always the shortest path.
<link rel="canonical" href="{{ content.canonical_url | absolute_url }}">
Active Navigation Highlighting
Use request.path to highlight the current page in navigation:
<a href="/about"
class="{% if request.path == '/about' %}active{% endif %}">
About
</a>
Related Documentation
- Liquid Objects -- Data objects like
post,collection,blog,page - Liquid Filters -- URL filters (
tag_url,collection_url,absolute_url) - Liquid Tags -- Tags like
{% section %}and{% content_for_layout %}