Liquid runs in every HTML field on PageWeave. These are the patterns that cover 90% of real work.
The assign-first rule
Filter chains inside {% for %} silently render EVERY row — no error, just wrong output. Always assign first:
{% assign published = site.tables.posts | where: "published", "true" | sort_by: "created_at", "desc" %}
{% for post in published limit: 10 %}
<a href="{{ post.url }}">{{ post.title }}</a>
{% endfor %}
Grouped navigation (the docs pattern)
{% assign sections = "Design|Build|Ship" | split: "|" %}
{% for section in sections %}
{% assign docs_section = site.tables.guides | where: "section", section | sort_by: "order" %}
{% if docs_section.size > 0 %}
<p>{{ section }}</p>
<ul>{% for doc in docs_section %}<li><a href="{{ doc.url }}">{{ doc.title }}</a></li>{% endfor %}</ul>
{% endif %}
{% endfor %}
Markdown from tables
<article class="prose">{{ row.content | markdownify }}</article>
markdownify is safe by default — raw HTML in the source is neutralized. GFM tables, task lists, and autolinks all work.
Conditional chrome per category
{% if row.category == "themes" %}
<div class="preview-frame">{{ row.theme_css }}</div>
{% endif %}
Pagination
{% paginate site.tables.posts by 12 order_by: "created_at" order_dir: "desc" %}
{% for post in site.tables.posts %}...{% endfor %}
{% if paginate.next %}<a href="{{ paginate.next.url }}">Next</a>{% endif %}
{% endpaginate %}
Default limit 20, hard cap 100.
Useful filters
upcase, downcase, date, truncate, append, prepend, strip_html, plus table filters where / sort_by.
Traps
- Filter chains directly inside
{% for %}— the silent killer. row.slugcomes from YOUR data; there's no auto-generation.- Snippet URLs stay canonical (
/snippets/css/site.css) — never hardcode content-hash variants; the platform rewrites them at render time. - Liquid works only in HTML fields — llms.txt, agents_md, manifests are verbatim.