Advanced Features
Site search, customer login providers, analytics integration, payment gateways, and security features
Nimbu provides advanced features for search, authentication, analytics, payments, and security to build sophisticated web applications.
Search
Nimbu has two ways to add site search to a theme:
- The
/searchpage: aGET /search?q=...request renderstemplates/search.liquidwith the results already loaded. - The
{% search %}tag: runs a query anywhere in a template, for example for a live-search endpoint or a "related articles" block.
The search page (templates/search.liquid)
Point a search form at /search (with the locale prefix on multilingual sites):
<form action="{{ locale_url_prefix }}/search" method="get" role="search">
<input type="search" name="q" value="{{ params.q | escape }}" placeholder="Search...">
<button type="submit">Search</button>
</form>templates/search.liquid receives a results hash:
| Key | Contents |
|---|---|
results.products | Matching products (active, sold out or coming soon) |
results.pages | Matching published pages |
results.articles | Matching blog articles |
results.db | Matching channel entries (see below) |
results.all | All of the above in one list |
results.any? | true when all has at least one result |
results.products?, results.pages?, results.articles?, results.db? | true when that list has results |
Each list holds up to 100 results; ?page=2 fetches the next 100. Every result has a title and a url, so results.all can be rendered with one loop. page.title is set from the translation html.titles.search.
Channel entries are searched in channels that have a public page (a published, unprotected channel template page), or in the channels listed in ?type=. Only entries with a URL are returned.
<div class="search-page">
<h1>{{ page.title }}</h1>
{% if params.q == blank %}
<p>Type something to search for.</p>
{% elsif results.any? %}
<p>{{ results.all.size }} results for "{{ params.q | escape }}"</p>
{% if results.products? %}
<h2>Products</h2>
{% for product in results.products %}
{% include 'product-card', product: product %}
{% endfor %}
{% endif %}
{% if results.pages? or results.articles? or results.db? %}
<h2>Pages and articles</h2>
<ul>
{% for result in results.pages %}
<li><a href="{{ result.url }}">{{ result.title }}</a></li>
{% endfor %}
{% for result in results.articles %}
<li><a href="{{ result.url }}">{{ result.title }}</a></li>
{% endfor %}
{% for result in results.db %}
<li><a href="{{ result.url }}">{{ result.title }}</a></li>
{% endfor %}
</ul>
{% endif %}
{% else %}
<p>No results for "{{ params.q | escape }}".</p>
{% endif %}
</div>When q is empty, the template is rendered with an empty results hash.
{% search %}
{% search <scope>, query: <query>[, channel: <slug>, page: <number>, limit: <number>] %}
...
{% endsearch %}scope(required, unquoted):all,pages,articles,products,channelsorcustomers.query(required): a variable such asparams.q, or a quoted string.channel: channel slug for thechannelsscope. Without it, all channels with search enabled are searched.page,limit: which page of results to fetch and how many. Default page size is 30 (25 for channels).
Inside the block, results holds the matching products, pages, articles, channel entries or customers as regular drops. An invalid scope or a missing query: is a Liquid syntax error; an empty query or a failing search gives an empty results.
{% if params.q != blank %}
{% search products, query: params.q, page: params.page, limit: 24 %}
{% paginate results by 24 %}
<div class="product-grid">
{% for product in paginate.collection %}
{% include 'product-card', product: product %}
{% else %}
<p>No products found.</p>
{% endfor %}
</div>
{{ paginate | default_pagination }}
{% endpaginate %}
{% endsearch %}
{% endif %}For a single scope, {% paginate %} uses the page numbers of the search itself, so pass page: params.page and use the same number for limit: and by.
Search within one channel:
{% search channels, channel: 'recipes', query: params.q, limit: 10 %}
{% for recipe in results %}
<a href="{{ recipe.url }}">{{ recipe.title }}</a>
{% endfor %}
{% endsearch %}The customers scope returns customer records. Never render it on a page that is visible to other visitors.
How matching works
- Every word must match. Words of at least 2 letters or digits also match as part of a longer word (
shoefindssnowshoes), for the first 6 such words in the query. Longer queries still work, but later words only match whole words. - Channel entries are only indexed when Search Enabled is on for the channel (the default). Turning it off removes the channel's entries from the index.
- Put words in double quotes (
"red wine") to search for that exact phrase. Partial-word matching does not apply then. - Queries that look like SQL-injection payloads return no results without searching.
GET /searchis rate limited to 30 requests per minute per IP address, including locale-prefixed paths such as/nl/search. Don't fire a request on every keystroke; debounce live search.
Customer Login with External Providers
{% login_with %}, {% unlink_from %} and {% oauth2_consent_form %} let customers sign in with Microsoft, Facebook, SAML, another Nimbu site or Twitter, and render the consent screen when your site is an OpenID Connect provider. See Customer Login with External Providers.
{% login_with provider: 'microsoft', text: 'Sign in with Microsoft', button_class: 'btn' %}Analytics Integration
Analytics integrations (recommended)
Add Google Analytics (GA4, optionally through Google Tag Manager), Matomo or Plausible under Settings → Push & Integrations. Nimbu injects their scripts into every page itself, before </head> and </body>, and can tie them to a consent application of the consent manager. You don't need any Liquid for this.
google_analytics_tag
Outputs the GA4 gtag.js snippet for a measurement ID. Pass consent: to load it only after the visitor accepts that consent application. The filter outputs nothing when an analytics integration is enabled, so the two never double-count.
{{ 'G-XXXXXXXXXX' | google_analytics_tag }}
{{ 'G-XXXXXXXXXX' | google_analytics_tag: consent: 'google-analytics' }}{% google_analytics 'UA-XXXXX-Y' %} and the google_analytics_ecommerce_code filter still exist for old Universal Analytics (ga.js) setups, which Google no longer processes. There is no Google Tag Manager filter; use the Google Analytics integration with Use Google Tagmanager instead.
Custom Event Tracking
<button
onclick="gtag('event', 'add_to_cart', {
'event_category': 'ecommerce',
'event_label': {{ product.name | json }},
'value': {{ product.price }}
})">
Add to Cart
</button>Payment Integration
Payment providers such as Stripe and Mollie are set up as payment methods in the Nimbu admin, not in the theme. On the checkout page, payment_form renders the form for the active payment method, or a selection form when several are active:
{{ cart | payment_form }}
{{ cart | payment_form: class: 'checkout-form', button_class: 'btn-checkout' }}See Payment Form.
Legacy payment tags
Two older tags are still available:
{% stripe_checkout amount %}renders the legacy Stripe Checkout (checkout.js) button for the site's Stripe payment method. Extra options becomedata-attributes, for example{% stripe_checkout cart.total, description: 'Order' %}.{% paypal_button business: '...', item_name: '...', amount: '...', paypalCertificate: ..., certificateID: ..., merchantCertificate: ..., merchantKey: ... %}...{% endpaypal_button %}renders an encrypted PayPal Payments Standard button. It takes PayPal's HTML variables as options and renders nothing unless all four certificate options are given. The block content replaces the default submit button.
Security Features
Spam protection: reCAPTCHA and Turnstile
Enable Google reCAPTCHA or Cloudflare Turnstile under Settings → Push & Integrations and pick the Protected Forms there. The site key comes from the integration, so the tags don't take one:
{% form channels.contact %}
...
{% recaptcha_tag %}
{% submit_tag 'Send' %}
{% endform %}
<!-- Invisible reCAPTCHA: renders the submit button itself -->
{% form channels.contact %}
...
{% recaptcha_button text: 'Send message', class: 'btn btn-primary' %}
{% endform %}{% recaptcha_tag %}renders the challenge widget. Options are passed to the widget, for exampletheme: 'dark'.{% recaptcha_button %}renders an invisible-reCAPTCHA submit button. Options includetext:andclass:.- The reCAPTCHA script is added to the page automatically; pass
skip_install_script: trueif you load it yourself. - When Turnstile is enabled, both tags render Turnstile instead (Turnstile also has its own
{% turnstile_tag %}and{% turnstile_button %}). Without an enabled integration they output only an HTML comment.
config.recaptcha, config.turnstile and config.form_challenge tell you in Liquid which provider is active.
CSRF Protection
{% form %}, {% login_with %} and the other form tags include the CSRF token automatically:
{% form channels.contact %}
<!-- CSRF token included -->
{% endform %}For a hand-written form, add the token from the auth_token variable:
<form method="post" action="/cart/coupons">
<input type="hidden" name="authenticity_token" value="{{ auth_token }}">
<!-- form fields -->
</form>Consent Management
{% consent_manager %}
Renders the cookie consent banner. The purposes, applications (with their cookies) and privacy policy link are configured in the Nimbu admin under Settings → Consent; the tag loads the consent manager script with that configuration:
{% consent_manager %}Optional settings:
{% consent_manager version: 'latest', theme: 'light', must_consent: 'true', accept_all: 'true', hide_decline_all: 'false' %}| Option | Description |
|---|---|
version | Consent manager version: 1.0.4, 1.1.2, 1.1.3, 1.1.4, 2.0.0 or latest (currently 1.1.4). Sites created before July 2021 default to 1.0.4. |
style_prefix | Use your own CSS instead of the built-in styles. |
theme, must_consent, accept_all, embedded, group_by_purpose, cookie_expires_after_days, hide_decline_all, hide_learn_more, notice_as_modal, additional_class | Passed to the consent manager as data- attributes. |
config | Your own JSON configuration instead of the one generated from the admin settings. |
consent_manager.consented_apps lists the applications the visitor accepted:
{% if consent_manager.consented_apps contains 'youtube' %}
<iframe src="https://www.youtube-nocookie.com/embed/..."></iframe>
{% endif %}Consent-Aware Scripts
javascript_tag with consent: outputs an opt-in script that the consent manager only runs after the visitor accepts that application:
{{ 'analytics.js' | javascript_tag: consent: 'analytics' }}Practical Examples
Contact Form with Spam Protection
<div class="contact-section">
<h2>Contact Us</h2>
{% form channels.contact, class: 'contact-form' %}
{% error_messages_for form_model %}
<div class="form-row">
<div class="col-md-6">
{% input 'first_name', label: 'First Name', required: true %}
</div>
<div class="col-md-6">
{% input 'last_name', label: 'Last Name', required: true %}
</div>
</div>
<div class="form-group">
{% input 'email', as: 'email', label: 'Email', required: true %}
</div>
<div class="form-group">
{% text_area 'message', rows: 6, label: 'Message', required: true %}
</div>
<div class="form-group">
{% recaptcha_tag %}
</div>
{% submit_tag 'Send Message', class: 'btn btn-primary' %}
{% endform %}
</div>Best Practices
1. Keep Secrets Out of Templates
Put keys you need in the browser (publishable keys, public IDs) under Settings → Variables and read them with site.env:
<script>window.MAPS_KEY = {{ site.env.MAPS_PUBLIC_KEY | json }};</script>Everything a template prints is public, including site.env values of variables marked secret. Never print secret API keys.
2. HTTPS for OAuth
All OAuth flows require HTTPS in production.
3. Escape Search Input
The search query comes straight from the URL. Liquid does not escape output, so escape it wherever you print it:
<!-- ✅ Good -->
<p>Results for "{{ params.q | escape }}"</p>
<!-- ❌ Bad: reflected XSS -->
<p>Results for "{{ params.q }}"</p>4. Consent Before Tracking
<!-- ✅ Good: Respect consent -->
{{ 'analytics.js' | javascript_tag: consent: 'analytics' }}
<!-- ❌ Bad: Track without consent -->
{{ 'analytics.js' | javascript_tag }}Next Steps
- Forms - Form building and validation
- Customer Login - External login providers
- SEO, robots.txt & llms.txt - Crawler and agent settings
- Multilingual - Internationalization
- Global Variables - Available drops
- Cloud Code - Custom server logic
Build advanced features with Nimbu!