Skip to main content

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
Key Concept

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

PatternExampleResolves To
//Home page (system page)
/{slug}/aboutPage, Post, Collection, or Blog
/{parent}/{content}/food/pizza-recipeContent with parent context
/{p1}/{p2}/.../content/italy/food/pizza/best-margheritaDeep cascading URL
/tags/{slug}/tags/italianTag listing page
/{blog-slug}/{post-slug}/travel-blog/my-tripBlog 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 array
  • content = 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 -->

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

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 RoleTemplate ForURL
homeHomepage/
postIndividual posts/{post-slug}
collectionCollection listings/{collection-slug}
tagTag listings/tags/{tag-slug}
blogBlog pages/{blog-slug}

How System Pages Work

When a visitor navigates to /best-margherita and that slug belongs to a post, OQO:

  1. Resolves the URL to a Post object
  2. Finds the post system page (the template)
  3. Renders the post system page's sections with post, content, and content_type set 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">
&larr; {{ prev.title }}
</a>
{% endif %}

{% if next %}
<a href="{{ next.url }}" class="nav-next">
{{ next.title }} &rarr;
</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>

  • 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 %}