Nimbu uses “liquid” as templating syntax. A thorough overview can be found here:
https://shopify.github.io/liquid/
Liquid uses a combination of tags, objects, and filters to load dynamic content. They are used
inside Liquid template files, which are the files that make up a Nimbu theme.
site
=> current site objectnow
=> current time in UTCtoday
=> current Datecontent_for_header
=> used to include CSRF meta tags in the headerlocale
=> current localedefault_locale
=> default localecontent_for_header
=> used to include CSRF meta tags in the headerseo.description
=> SEO description for current page, if givenseo.keywords
=> SEO keywords for current page, if givenThe config
variable is a proxy to several Nimbu objects configurable in the back-end:
config.locales
=> all publicly visible localesconfig.all_locales
=> all configured locales, including hidden localesconfig.hidden_locales
=> all hidden locales, used for preparing translationsconfig.customers.<select_field>_items
=> list of select items configured for a field (format:config.customers.countries
=> countries configured for this siteconfig.customers.titles
=> available titles (if single-select custom field “title” is defined)config.channels.<channel_name>.<select_field>_items
=> select items for channelsconfig.products.<channel_name>.<select_field>_items
=> select items for productsconfig.countries
=> all configured countries for this siteconfig.countries_from_the_world
=> all countries in the world (usefull for dropdown)config.available_shipping_methods
=> shipping methods configured and applicable to this siteThe url
variable is a proxy to several information related to the current request or default Nimbu
paths:
url.current
=> current url (includes domain)url.current_path
=> current path (without domain)url.langauge_independent_path
=> current path without language prefixurl.back
/ url.referrer
=> current referrer (if defined?)url.query_string
=> current query parameters (in one string, if defined?)The path
variable gives you the path of the current http request
The
locale_url_prefix
will return “/” for non-default locale, and nothing for default
locale. Use this in multi-lingual templates for all hard-coded links.
The menus
variable is a proxy to navigation:
menus.<slug>
=> selects navigation with slug The blogs
variable is a proxy to all blogs:
blogs.articles
=> returns all articles across all blogsblogs.<slug>
=> selects blog with slug
blogs.<slug>.articles.<method>
=> selects blog with slug and proxies to it’s articles,The channels
variable is proxy to all Nimbu channels:
channels.<channel_name>.<method>
=> channel methods include: slug, name, description, new_entry,The products
variable is a proxy to all Nimbu products:
products.<method>
=> methods include: attributes, attributes_with_counts, attributes_in_use,The product_types
variable is a proxy to all defined product types:
product_types.<method>
=> product type methods include: first, last, all (sorted by name inThe product_vendor
variable is a proxy to all defined product vendors:
product_vendor.<method>
=> product vendor methods include: first, last, all (sorted by name inThe collections
variable is a proxy to all product collections:
collections.<method>
=> collection methods include: first, last, all, count, latest, size,collections.<slug>.products.<method>
=> geeft producten in collection met slug , methodsFor
random
you can userandom_<amount>
to specify the number of random items to be returned.
(i.e.channels.<slug>.random_4
will result in 4 different items each render)
Below are some Nimbu specific liquid filters. These can be used besides the default liquid filters
from https://help.shopify.com/en/themes/liquid/filters
Create Google Analytics Tag from tracking number:
{{ 'UA-xxxxxxxxx-x' | google_analytics_tag }}
Create Google Analytics E-commerce Conversion Tag from order and tracking number
{{ order | google_analytics_ecommerce_code: 'UA-xxxxxxxxx-x' }}
Convert object to JSON representation
{{ variable | json }}
{{ variable | json, without: 'foo', 'bar }} -- skip key foo and bar
Convert JSON representation to object (can be array or object or nested)
{% assign object = valid_json_string | from_json %}
Generate API SSO meta headers from API token
{{ '<< nimbu-api-token >>' | api_sso_info }}
Convert coordinates to JSON representation
{{ variable | to_geopoint }} -- result {"lat":,"lng":,"latitude":,"longitude":}
{{ variable | is_geopoint }} -- detect if variable if valid coordinate object
Generate 'Add to cart' button (valid arguments are: btn_class
, add_label
, default_quantity
,
hide_variants
, input_class
, show_quantity_dropdown
, show_quantity_buttons
,
hide_quantity
)
{{ product | add_to_cart }}
Generate 'Update cart quantity' snippet (valid arguments are show_quantity_dropdown
,
show_quantity_buttons
, show_refresh_btn
, refresh_btn_class
, refresh_btn_label
)
{{ order_line_item | update_cart_item }}
Generate 'Delete from cart' snippet (valid arguments are delete
(= the label), confirm
(the
confirmation), class
)
{{ order_line_item | delete_cart_item }}
Generate 'Delete grouped items from cart' snippet. (valid arguments are delete
(= the label),
confirm
(the confirmation), class
)
{{ order_item_group | delete_cart_group }}
Generate 'Go to checkout' button (valid arguments are label
, class
)
{{ cart | checkout_button }}
Generate 'Select payment method' form, during payment step if checkout (valid arguments are id
,
class
(will be set on the form), title
(the form title), value
(the label on the submit
button), button_class
)
{{ cart | payment_form }}
Automatically apply the coupon when loading the page
{{ '<< coupon code >> | auto_apply_coupon }}
Convert value to currency representation (valid arguments are unit
, precision
, locale
(also
chosen from context), format
, negative_format
, separator
, delimiter
, fancy
(will insert
around the decimals))
{{ 10.3533 | money_with_currency }} -- result: "10.35€"
{{ 10.3533 | number_to_currency }} -- result: "10.35€"
{{ 10.3533 | money_without_currency }} -- result: "10.35"
Convert grams to kg
{{ 1200 | weight }} -- result: 1.2
{{ 1200 | weight_with_unit }} -- result: 1.2 kg
Pretty print a number in a way it is more readable by humans
{{ 1234567890 | number_to_human }} -- result: "1.23 Billion"
Valid arguments are :separator, :delimiter, :precision, :locale (also chosen from context), :units, :format, :strip_insignificant_zeros
Format bytes into a more understandable representation
{{ 1234567890 | number_to_human_size }} -- result: "1.15 GB"
Valid arguments are :precision, :significant, :separator, :delimiter, :locale (also chosen from context), :strip_insignificant_zeros
Formats a number as a percentage string
{{ 302.24398923423 | number_to_percentage }} -- result: "302.243%"
Valid arguments are :precision, :significant, :separator, :delimiter, :locale (also chosen from context), :strip_insignificant_zeros, :format
Formats a number into a phone number (US by default)
{{ 1235551234 | number_to_phone }} -- result: "123-555-1234"
Valid arguments are :area_code, :extension, :country_code, :delimiter, :pattern
Formats a number with grouped thousands using delimiter
{{ 12345678 | number_with_delimiter }} -- result: "12,345,678"
Valid arguments are :delimiter, :separator, :delimiter_pattern, :locale (also chosen from context)
Formats a number with the specified level of :precision
{{ 111.2345 | number_with_precision }} -- result: "111.235"
Valid arguments are :delimiter, :separator, :precision (defaults to 3), :locale (also chosen from context), :significant (If true, precision will be the number of significant_digits.), :strip_insignificant_zeros (If true removes insignificant zeros after the decimal separator)
Convert string to DateTime object
{{ '11-11-2018' | to_datetime }}
Format DateTime object to a String
{{ site.today | localized_date }}
{{ site.today | localized_date: format, locale }}
format can be one of "default", "long", "short", "time", "full", "date_only"
which will use the appropriate format for the given language
or use the format directives from https://apidock.com/ruby/DateTime/strftime
{{ site.today | localized_date: "%m/%d/%Y" }}
Verbalize the time ago
{{ datetime | time_ago_in_words }} -- result => "1 hour ago"
Convert asset name to path on CDN for current site
{{ 'foo/bar/example.jpg' | asset_url }}
-- result: 'https://cdn.nimbu.io/s/123345/foo/bar/example.jpg'
Generate RSS feed discovery tag
{{ '/foo/bar' | auto_discovery_link_tag, rel:'alternate', type:'application/atom+xml', title:'A title' }}
Generate stylesheet tag with correct link to CDN
{{ 'main.css' | stylesheet_tag }}
Valid arguments are are :media, :rel, :type
Generate javascript tag with correct link to CDN
{{ 'app.js' | javascript_tag }}
Valid arguments are are :type
Convert input to image path on the CDN
{{ 'example.png' | theme_image_url }}
Request the CDN to add 'Content-Disposition: attachment' (needed for download)
See https://html.spec.whatwg.org/multipage/links.html#downloading-resources
{{ 'foo/bar/example.pdf' | asset_url | download }}
-- result: 'https://cdn.nimbu.io/s/123345/foo/bar/example.pdf?dl=1'
Generate pagination links from pagination object
{% paginate results by 50 %}
{% for item in paginate.collection %}
...
{% endfor %}
{{ paginate | default_pagination, previous_label:'«', next_label:'»', style:'bootstrap' }}
{% endpaginate %}
Valid arguments are are :previous_label, :next_label, :css (will be used as class)
:style (one of 'zurb', 'bootstrap', 'bootstrap4'
Apply filters or dynamically rescale image on the cdn
{{ 'example.png' | theme_image_url | grayscale }}
{{ 'example.png' | theme_image_url | filter, width: '300px' }}
The input must be a valid CDN path, i.e. output from 'theme_image_url' filter
Valid arguments are are
:width, :height (values in px, i.e. '300px')
:cropping (one of 'scale', 'fill', 'fit', 'crop')
:gravity (used for cropping: 'north_west', 'north','north_east','west','center',
'east','south_west','south','south_east')
:effect ('grayscale', 'sepia' or 'vignette')
Make all characters lowercase
{{ "Parker Moore" | downcase }} -- parker moore
Make all characters uppercase
{{ "Parker Moore" | upcase }} -- PARKER MOORE
Convert Textile markup to HTML
{{ textile_variable | textile }}
Convert Markdown markup to HTML
{{ markdown_variable | markdown }}
Convert text emotions to little icons.
{{ ':-)' | emoticize }}
Following emoticons are supported:
:-) :) ;-) ;) :-| ;| ;-D ;D (sad) :(( :-(( :-( :( :p :-p :-P :P :D :-D :Z :-Z :-* $-) (lol) (zzz) (rip) (brr) (geek) (ninja) (whoops) (oops) O:) 8) 8-) :s :-s :o :-o :O :-O >:-( (hmm) :$ :-$
Replace text using advanced regular expressions
{{ '123 foo bar' | replace_by_regex: '\s*', '-' }}
-- 123-foo-bar
{{ '123-456 foo-bar' | replace_by_regex: '-?(\d+?)-?(\d*)', '\1\2' }}
-- 123456 foo-bar
Convert string to url-safe equivalent
{{ 'Foo Bar!' | parameterize }} -- => result: foo-bar
Transliterate: convert non-latin characters to latin-characters
{{ 'L'Oréal & ŠKODA' | transliterate }}-- 'L'Oreal & SKODA'
Convert string variable to integer value
{{ integer_string | to_i }}
Convert string to float value
{{ integer_string | to_f }}
Split string into an array
{% assign array = 'a,b,c' | split %} -- => array contains ['a','b','c']
{% assign array = 'a, b, c' | split: ', ' %} -- => array contains ['a','b','c']
{% assign array = 'a,b,,d' | split: ',', include_empty: true %} -- => array contains ['a','b','','d']
Choose between a word or its plural value depending on the input
{{ 0 | pluralize: 'word', 'words' }} -- => 0 words
{{ 1 | pluralize: 'word', 'words' }} -- => 1 word
{{ 2 | pluralize: 'word', 'words' }} -- => 2 words
HTML Escape the input
{{'<html></html>' | escape }} -- => result: <html></html>
Sanitize input from 'dangerous' html tags, i.e. <script>
(mainly to prevent xss)
{{'<h1><script>Foo</script> Bar</h1>' | sanitize_html }} -- result: <h1>Foo Bar</h1>
Simply remove all html tags, concatenating multiple line content
{{'<h1><script>Foo</script> Bar</h1>' | strip_html }} -- => result: Foo Bar
More carefully remove all html tags, retaining multiple line content
{{'<p>Foo</p><p>Bar</p>' | to_text }} -- => result: Foo\n\n Bar
Nicely print an array value. Possible options are :words_connector, :two_words_connector,
:last_word_connector or :locale. It will automatically use the locale from the context
{{ 'a,b,c' | split | to_sentence }} -- => result: a, b, and c
{{ 'a,b,c' | split }} -- => result: a + b & c
Make an underscored, lowercase form from the expression in the string.
{{ input | underscore }}
Replaces underscores with dashes in the string.
{{ 'puni_puni' | dasherize }} -- result => 'puni-puni'
Concantenate multiple strings
{{ 'foo' | concat: 'bar', 'value' }} -- result => 'foobarvalue'
Strip all whitespace from variable
{{ ' foo ' | strip }} -- result: 'foo'
Is the variable empty or only whitespace?
{{ '' | is_empty }} -- true
{{ ' ' | is_empty }} -- true
{{ 'a' | is_empty }} -- false
Escape url value
{{ 'foo bar' | uri_encode }} -- result: 'foo%20bar'
Unescape url value
{{ 'foo%20bar' | uri_encode }} -- result: 'foo bar'
Modulo
{{ 5 | modulo: 3 }} -- result: 2
Is variable a valid integer
{{ 5 | is_number }} -- true
{{ "5" | is_number }} -- true
{{ "a" | is_number }} -- false
Is variable a valid floating point
{{ 5.1 | is_float }} -- true
{{ "5.1" | is_float }} -- true
{{ "a" | is_float }} -- false
Generate random integer
{{ 5 | random }} -- integer value between 0 and 5
{{ 100 | random: 50 }} -- integer value between 50 and 100
Sort an array of hashes or drops, provide options key by which to compare.
{{ variable | sort }} -- will use the value or title value for Nimbu objects
{{ variable | sort: 'foo.bar' }} -- will sort using the bar value of each foo value for each item in the array
The key can use dot-notation for deeply nested comparison
Base64 encode
{{ some_variable | base64_encode }}
Base64 decode
{{ some_variable | base64_decode }}
Generate MD5 hash
{{ 'foo' | md5 }}
Generate SHA1 hash
{{ 'foo' | sha1 }}
Generate SHA224 hash
{{ 'foo' | sha224 }}
Generate SHA256 hash
{{ 'foo' | sha256 }}
Generate SHA384 hash
{{ 'foo' | sha384 }}
Generate SHA512 hash
{{ 'foo' | sha512 }}
Generate HMAC256 hash
{{ 'foo' | hmac256, secret:'password' }}
The filters to_jwt
and from_jwt
(aliased to jwt
and encode_jwt
, and decode_jwt
respectively) can be used to encode and decode JSON web token within a liquid template.
{% assign data = params.token | from_jwt, password: "shared key" %}
It accepts following parameters for decoding:
verify_expiration
: verify the exp
(Expiration Time Claim) data parameter in the jwt tokenexp_leeway
: adds leeway (a tolerance on the timing)verify_not_before
: verify the nbf
(Not Before Time) data parameter in the jwt token (defaultsnbf_leeway
: adds leeway for nbf (a tolerance on the timing)verify_iss
: verify the iss
(Issuer Claim) data parameter in the jwt token (defaults to true)iss
: the expected issuerverify_aud
: verify the aud
(Audience Claim) data parameter in the jwt token (defaults toaud
: the expected audienceverify_iat
: verify the iat
(Issued At Claim) data parameter in the jwt token (defaults toverify_sub
: verify the sub
(Subject Claim) data parameter in the jwt token (defaults tosub
: Subject to compare againstalgorithm
: the hashing algorithm used (defaults to 'HS256')When one of the validations failed, the result will be nil.
To encode a JWT token, use the reverse:
{% assign token = "some string" | jwt, password: "shared key" %}
{% set payload, "data", "some string" %}
{% set payload, "iat", site.now %}
{% assign token = payload | jwt, password: "shared key" %}
Use following to generate QR Codes:
{{ 'http://zenjoy.be' | qr }}
{{ 'http://zenjoy.be' | qr: size: '200', fill: 'grey', color: 'red' }}
{{ 'http://zenjoy.be' | qr_datauri }}
{{ 'http://zenjoy.be' | qr_svg }}
{{ 'http://zenjoy.be' | qr_svg: fill: 'AAA', color: 'F00' }}
{{ 'http://zenjoy.be' | qr_html }}
Colors:
- for SVG: hex-color (without #)
- for PNG: ChunkyPNG::Color (see http://www.rubydoc.info/gems/chunky_png/ChunkyPNG/Color)
qr_html
renders the QR-Code as a HTML-table (for use in e.g. an email template). You need to add
CSS to render it properly as a QR-code:
table {
border-width: 0;
border-style: none;
border-color: #0000ff;
border-collapse: collapse;
}
td {
border-left: solid 10px #000;
padding: 0;
margin: 0;
width: 0px;
height: 10px;
}
td.black {
border-color: #000;
}
td.white {
border-color: #fff;
}
There is the download filter as documented above (see CDN helpers)
{{ 'http://example.com/?e=f' | add_query_params, version: "1", type: "pdf" }}
Result: http://example.com/?e=f&version=1&type=pdf
With Nimbu you can easily define editable regions within your template. There are different types of
editable structures:
The default value is taken from the block context:
{% editable_field <name-of-the-field> %}The default value{% endeditable_field %}
Remark: when a default value is provided, it is not possible to provide an empty value by the
user.
Valid configuration keys are:
canvas
and repeatable
tag to provide extra information to the editor)Following is a list of examples:
String
{% editable_field 'some name', label: "Label dat in backend getoond wordt", hint: "Hint die in backend getoond wordt" %}Some text to show as a placeholder.{% endeditable_field %}
Text
{% editable_text 'some name 2' %}
<p>Placeholder Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna.</p>
{% endeditable_text %}
File/Image
{% editable_file 'some name 3' %}http://www.placehold.it/600x900{% endeditable_file %}
Example usage:
<img src="{% editable_file 'some name 3' %}http://www.placehold.it/600x900{% endeditable_file %}" />
It is possible to provide an accept
attribute to limit the MIME types possible to select: see
https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept for various values. i.e. to
limit to only image files, use accept="image/*"
:
{% editable_file 'background_image', accept='image/*' %}{% endeditable_file %}
Select
{% editable_select 'some name 4', options:'none|option_1|option_2|...|option_n|default_value', labels:'None|Option 1|Option 2|...|Option n|Default' %}default_value{% endeditable_select %}
Reference
{% editable_reference "Reference Channel Entry", to: "<<slug-van-channel>>", assign: "variable" %}{% endeditable_reference %}
{% editable_reference "Reference Product", to: "products", assign: "product" %}{% endeditable_reference %}
{% editable_reference "Reference Collection", to: "collections", assign: "collection" %}{% endeditable_reference %}
{% editable_reference "Reference Customer", to: "customers", assign: "customer" %}{% endeditable_reference %}
{% editable_reference "Reference Menu", to: "navigation", assign: "menu" %}{% endeditable_reference %}
{% editable_reference "Page Link", to: "pages", assign: linked_page %}{% endeditable_reference %}
Reference to a channel (e.g. to render a form for it)
{% editable_reference "form_channel", label: "Channel voor formulier", to: "channels", assign: "channel_variable" %}{% endeditable_reference %}
{% form channel_variable %}
{% inputs_for_fields %}
{% submit_tag "Versturen" %}
{% endform %}
Reference to the select options of a channel/customer/product field
{% editable_reference "Channel field options", to: config.channels.<channel_slug>.<field_name>_items, assign: "variable" %}{% endeditable_reference %}
{% editable_reference "Customer field options", to: config.customers.<field_name>_items, assign: "variable" %}{% endeditable_reference %}
{% editable_reference "Product field options", to: config.products.<field_name>_items, assign: "variable" %}{% endeditable_reference %}
{% editable_reference "Reference Many", to: "<<slug-van-channel>>", multiple: true, assign: "variabele_for_many_references" %}{% endeditable_reference %}
{% editable_reference "Reference Many Products", to: "products", multiple: true, assign: "many_products" %}{% endeditable_reference %}
{% editable_reference "Reference Many Collection", to: "collections", assign: "collections", multiple: true %}{% endeditable_reference %}
{% editable_reference "Reference Many Customers", to: "customers", multiple: true, assign: "many_customers" %}{% endeditable_reference %}
The output of an editable_reference is always an object, so it's best to use this with an assign
statement so you can use it further in the template code... i.e.:
{% assign link_for_block = linked_page.full_url %}
It is possible to add various validations to the editable fields by specifying following attributes:
required
: Specifies the field as requiredmaxlength
: Specifies the maximum number of character for an input fieldpattern
: Specifies a regular expression to check the input value againstThe input field can also be given a type to alter how the input is rendered in several browsers.
type
: one of text
, email
, number
, url
, tel
, password
, date
, datetime
,
datetime-local
, time
, month
, week
, color
, range
⚠️ this is rendered as-is on the <input>
and is not validated server-side, except for the
specification type="number"
.
for the type "number", also following attributes are possible and validated server-side:
min
: Specifies the minimum value for an input fieldmax
: Specifies the maximum value for an input fieldfloats
: Specifies floats can be enteres, otherwise only integers are allowedsigned-floats
: Specifies negative floats are possiblesigned
: Specifies negative numbers are possiblestep
: Specifies the legal number intervals for an input fieldIt is possible to create regions that can be creatively constructed from defined blocks (called
“repeatables”):
editable_canvas: creates the outline for the canvas on which multiple “repeatables” can be placed.
{% editable_canvas "Blokken" %}
{% repeatable "header", label: "Header" %}
{% include "repeatables/header" %} -- snippet with other editable_fields
{% endrepeatable %}
{% repeatable "text", label: "Text" %}
{% include "repeatables/text" %} -- snippet with other editable_fields
{% endrepeatable %}
{% endeditable_canvas %}
Nimbu allows to group editable regions. This only has an effect on their appearance in the backend.
They are rendered together in a (by default collapsed) container. Thus only ungrouped editables are
visible by default in the backend. There are two ways to put an editable in a group:
Example:
{% editable_field 'top_margin', group: 'settings' %}{% endeditable_field %}
{% editable_field 'title' %}{% endeditable_field %}
{% editable_field 'subtitle' %}{% endeditable_field %}
{% editable_group 'content' %}
{% editable_canvas 'MyCanvas' %}
{% repeatable 'MyRepeatable' %}
{% editable_field 'margin', group: 'settings' %}{% endeditable_field %}
{% editable_group 'subcontent' %}
{% editable_field 'title' %}{% endeditable_field %}
{% editable_field 'subtitle' %}{% endeditable_field %}
{% endeditable_group %}
{% endrepeatable %}
{% endeditable_canvas %}
{% editable_canvas 'SecondCanvas' %}
{% repeatable 'AnotherRepeatable' %}
{% editable_field 'margin', group: 'settings' %}{% endeditable_field %}
{% editable_field 'title' %}{% endeditable_field %}
{% editable_field 'subtitle' %}{% endeditable_field %}
{% endrepeatable %}
{% endeditable_canvas %}
{% endeditable_group %}
{% editable_field 'bottom_margin', group: 'footer settings' %}{% endeditable_field %}
You can make every bit of content translatable by putting in a translate-tag. This comes in handy
for buttons, footers, etc. You can also pass variables into the translations.
{% translate ‘this.is.the.key’, default: ‘Your great line that has to be translatable, any way %{statement}.’, referrer: item.statement %}
You can overwrite the standard site.name, which returns the name of the site, by adding it as a new
translatable in Copywriting. By doing this you enable the option to translate the sitename in
multiple languages. For example ideal if you want to send mails in multiple languages or have a
different sitename in your footer for each language. Add site.name
as the key to Copywriting.
Since November 21st, 2017 nimbu supports configuring the available languages on a per-page basis.
When one or more languages are selected, then the other languages enabled on the site will be hidden
from:
A page in liquid has some extra methods:
available_locales
: the locales in which the page is availableavailable_in_locale?
: whether the page is available in the active localetranslations
: this is an array with information about the available translationsEach entry in translations
has 4 keys:
locale
: the locale for the translationlanguage
: human-readable form of the language in the active localeurl
: the url for the page in that localelocalized_language
: human-readable form of the language in the locale of the translationNote that by default language
and localized_language
will most probably be the same because
nimbu doesn't provide translations for the human-readable locales. You can add them to copywriting
with the key locale.<locale>
(e.g. locale.en
, locale.fr
, locale.nl
).
All this allows to do something along the following in the layout:
{% if page != blank and page.available_in_locale? == false %}
<p>
{% translate 'locale.page_not_available', default: "Sorry, this page is not available in this
language." %}
</p>
<p>{% translate 'locale.view_page_in', default: "View this page in:" %}</p>
<ul>
{% for tr in page.translations %}
<li><a href="{{ tr.url }}">{{ tr.language }}</a></li>
{% endfor %}
</ul>
{% else %} {{ content_for_body }} {% endif %}
You can restrict the data returned for all channel proxy methods, by using the {% scope %} tag:
{% scope fieldname: value %}
{% for item in channels.[channel_slug].all %}
{{ item.[field_name] }}
{% endfor %}
{% endscope %}
will only iterate over the channel items where equals the value . Value is either
a variable or a literal (i.e. when quoted)
Adding the “sort” tag will overrule the default sorting and can be used for more complex sorting
scenarios
{% sort fieldname asc, other_fieldname desc %}
{% for item in channels.[channel_slug].all %}
{{ item.[field_name] }}
{% endfor %}
{% endsort %}
To sort on references, single selects, etc. You can use the “sort”-filter.
{% assign sortedItem = channels.[channelName].all | sort: [fieldname], name' %}
Complex criteria can be constructed using logical operators (and / or in combination with brackets)
{% scope (fieldname == value) or (other_fieldname >= 0 and yet_another_field < 100) %}
Within the scope criteria, following operators can be used:
==
=> fieldname equals the value!=
=> fieldname differs from the value<
=> greater than>
=> lesser than<=
=> greater than or equal>=
=> lesser than or equalin
=> fieldname in given value (given an array or string with comma-separated values)nin
=> fieldname not in given value (given an array or string with comma-separated values)exists
=> fieldname has a value or not (i.e.contains
=> fieldname of type array contains the given valuestart
=> fieldname (string based) begins with given valueend
=> fieldname (string based) ends with given valueregex
=> fieldname (string based) matches given regular expressionDate Time fields are stored in the database in UTC. Special consideration is required to scope on
these fields. You should either compare with:
site.now
or site.today
2020-10-28T16:55:00+01:00
If you pass a string without time zone information, the database will parse it as UTC!
Date fields are serialized in to the database as midnight UTC. If you want to compare something to
it, compare with either:
today
(not site.today
which is a time object!)2020-10-28
or {{ site.today | date: '%F' }}
For relational queries, often the database fieldname must be used in combinatie with database
ids as values:
Extracting database ids into a variable that can be used for the query is i.e. done with
{% assign db_ids = entry.reference_items | map: '_id' %}
Scopes are typically static, but sometimes it is desired to dynamically construct the scope
expression and evaluate it at runtime:
{% scope {{ variable_with_expression }} %}
{% paginate channels.[channel_slug] by 21 %}
{% for item in paginate.collection %}
...
{% endfor %}
{{ paginate | default_pagination: 'previous_label:«', 'next_label:»' }}
{% endpaginate %}
Once you have set up a channel in Nimbu, you can easily iterate the items in your templates.
{% for item in channels.[channel_slug] %}
{{ item.[field_name] }}
{% endfor %}
The results can be limited, or you can skip ahead a few items in the loop
{% for item in channels.[channel_slug].all, limit: 4, skip: 2 %}
You can also cache the result of a for loop:
{% for item in channels.[channel_slug], cache: channels.[channel_slug].updated_at %}
{{ item.[field_name] }}
{% endfor %}
The value you give to ‘cache’ will be used as the key to cache the rendered output of the for loop.
It is important that it contains everything that can influence the output of the for loop. E.g. if
you use a translate tag inside the for loop, you should also include it in the cache key.
For example:
{% capture technology_slider_cache_key %}{{ channels.technologies.updated_at }}-{% translate lees.meer, default: "Lees meer »" %}{% endcapture %}
{% for item in channels.technologies, cache: technology_slider_cache_key %}
...
{% endfor %}
The menu’s defined in Nimbu can be used in templates using the nav
tag. The tag can also be used
to automatically create navigation for a page or sitemap hierarchy
{% nav main %}
{% nav site, no_wrapper: true, depth: 1, exclude: 'contact|about', id: 'main-nav' }
You can use following parameters to change the markup for the menu
depth
: defines the number of levels deep the submenus will be gerenated when autogenerating theexclude
: pages with matching slug will be excluded from the menuno_wrapper
: omit the wrapping <ul>
surrounding the menu markupid
: sets the html id for the top level node in the menuclass
: adds the given class to the top level node in the menulink_class
: adds the given class to all links in the menuitem_class
: adds the given class to the <li>
element surrounding each link in the menusubmenu_class
: adds the given class to the <ul>
surrounding the child menu itemssubmenu_wrapper_class
: adds the class to the menu item if it has child menu itemsactive_item_class
: adds the given class when the menu item is matching the current pageactive_parent_class
: adds the given class to the parent item when a child is activeTo switch between languages for the same page, create a link using the {%
**localized_path**
'nl' %}
tag.
It is possible to cache certain random code paths, with a custom cache key:
{% cache <expressie> %}
[ ... code waarvan output gecached wordt ... ]
{% endcache %}
i.e. channels.<slug>.updated_at
or a variable captured from multiple combined cache keys
There are two ways to define variables. Assign is typically used for objects or strings, capture for
larger pieces of content.
Assign
{% assign 'varName' = "varValue" | [filter] %}
Assign an editable region to a variable
{% editable_field 'some name', assign: 'varName' %}{% endeditable_field %}
Using dynamic field names
{% assign 'varName' = variable[other_variable_field_name] %}
Capture
{% capture 'varName' %}
...
{% endcapture %}
Construct a key-value object
{% set <variabele>, <key | can be dynamic expression>, <value | can be dynamic expression> %}
{% set {{ <dynamic variable name> }}, <key>, <value> %}
Capturing a for-loop
{% capture 'varName' %}
{% for [var] in channels.[channel_slug] %}
{{ [var].[field_name] }}{% unless forloop.last %}, {% endunless %}
{% endfor %}
{% endcapture %}
Defining a form To define a Nimbu form, you first create a new channel, with the fields you
need. After saving, you can mark this channel (under ‘more - edit structure’) as being a submittable
channel. You can then select the fields that are submittable, check if you want to sent
notification-emails (f.e. to me@mydomain.com, {{ email }} - the last one being the [field_name] of
the submittable email field) and select the mail template to use (you can create one under
‘settings, notifications’.
Adding the form to your template Once you are set up, you can define the form in your template,
using this markup:
{% form channels.[channel_slug], class: "[classnames]" %}
…
{% submit_tag "[text for button]", class: "[classnames]" %}
{% endform %}
Defining the form fields Off course you’ll need your form to show the corresponding inputs. To
print out inputs for all submittable fields of the form channel, you can use:
{% inputs_for_fields %}
This way, you haven’t much freedom in styling the form, but you can just add a submittable field to
your channel and it will show up in the rendered html. By default the label will be taken from the
field label, but you can also overwrite it in copywriting using the key
channels.``<``channel``*_*``slug``>``.label.``<``field``*_*``name``>
.
To have more control over your form, you can print out the single inputs, with the Field Label as
label.
{% input "[field_name]" %}
-->
<div class="input string"><label for="\[channel_slug]_[field_name]">[field_label]</label><div class="wrapper"><input type="text" name="[channel_slug\][[field_name]]" id="[channel_slug]_[field_name]"></div></div>
Then you can start customizing. To give the label a custom value, just add the label property.
{% input "[field_name]", label: "[custom label]" %}
For specific field types you can add an ‘as’ attribute to specify the type of input. Different types
you can use are:
{% input "[field_name]", as:"[email/integer/text/date/select/checkbox/boolean/hidden/radio]"%}
For select or radio inputs, you’ll need to define the items a person can select or choose from. If
you have defined the options in a separate channel (f.e. a list of events your visitors can register
for) or as options in a single select field, you can use the ‘collection’-attribute.
For items defined within the single select field
{% input "[field_name]", as:"[select/radio]", collection: config.channels.[channel_slug].[field_name]_items %}
For items defined in a different channel
{% input "[field_name]", as:"[select/radio]", collection: channels.[channel_slug] %}
For items defined within the customer data-model
{% input "[field_name]", as:"[select/radio]", collection: config.customers.[field_name]_items %}
There are also some special collections available:
collection: config.countries_from_the_world
Or you can just code the options manually
<select name="\[channel_slug\][[field_name]]" id="[channel_slug]_[field_name]">
<option value="value_1"></option>
<option value="value_2"></option>
...
<option value="value_n"></option>
</select>
For a select box, you can also define an item to show, when no selection has been made. This comes
in handy, when you want the select box to be a required field.
{% input "[field_name]", as:"[select/radio]", collection: [collection], prompt: 'Please make your selection' %}
If you want to set other attributes to the input fields, like classes, min and max dates,
placeholders, … you can do this by adding a property.
{% input "[field_name]", label: "[custom label]", [property]: [property_value] %}
You can access the object in the form being edited (but not necessarily saved to the database, i.e.
due to validation errors) by using the form_model
context variable. This allows you to build
custom forms or directly access validation errors via form_model.errors
.
After submitting the form You can add a page to redirect to after submit.
{% form channels.[channel_slug], class: "[classnames]" %}
<input type="hidden" name="redirect_after_submit" value="[url_for_redirect]"/>
…
{% submit_tag "[text for button]", class: "[classnames]" %}
{% endform %}
If there are however any errors, the page will reload with the form still completed. The errors will
be highlighted and the errors will have a class ‘.inline-error’, which you can use to style them. As
stated earlier you can define an e-mail to be sent after the form has been submitted.
Custom input tag layout You can customize how the rendered input tag looks like by adding an
input_tag_template
:
{% form channels.formtest, layout: 'bootstrap4' %}
{% input_tag_template 'boolean, checkbox' %}
.custom-control.custom-checkbox
{% if input_tag.value %}
%input.custom-control-input{:id => "{{ input_tag.id }}", :name => "{{ input_tag.name }}", :type => "checkbox", :checked => true}/
{% else %}
%input.custom-control-input{:id => "{{ input_tag.id }}", :name => "{{ input_tag.name }}", :type => "checkbox"}/
{% endif %}
%label.custom-control-label{:for => "{{ input_tag.id }}"} {{ input_tag.label }}
{% endinput_tag_template %}
{% inputs_for_fields %}
{% endform %}
The variable input_tag
contains:
id
: the id to put on the inputname
: the name to put on the inputlabel
: the label for the inputvalue
: the value for the inputcollection
: an array of [value, id] pairs for optionsselected_values
: normalized selected values in case of a select, etc.multiple
: set to true
if multiple is present on the input
tagNimbu supports login with Facebook and Twitter and Microsoft Identity Platform.
The first step is configuring an integration in the backend at ‘Settings > Push & Integrations’ in
the section ‘Authentication services’
You need the information of a facebook or twitter application. If you need to enter a ‘callback url’
in the settings of such an application, enter ‘http(s)/<primary_domain>/auth//callback’.
There are several liquid helpers to assist in coding a template that supports these. For example, a
login form with links to login with facebook and twitter:
{% form login %}
{% hidden_field_tag "redirect_after_login", value: "/account" %}
{% input "email", label: "Email" %}
{% input "password", as: "password", label: "Password" %}
{% submit_tag "Login" %}
{% endform %}
{% login_with provider: 'facebook' %}
{% login_with provider: 'twitter' %}
The login_with
tag has several use cases:
login with twitter
{% login_with provider: 'twitter' %}
link twitter account to current user (e.g. on account page)
{% login_with provider: 'twitter', action: 'link' %}
unlink twitter account from current user (e.g. on account page)
{% login_with provider: 'twitter', action: 'unlink' %}
shorthand for the above
{% unlink_from provider: 'twitter' %}
It has the following options:
provider
: the provider (default: 'nimbu')action
: one of login
, link
, unlink
(default: login
, unlink
when using unlink_from
)form_class
: class to put on the form tagbutton_class
: class to put on the button tagtext
: text to put on the button (default: see below)return_to
: the path to redirect to after successful login with the providerDefault text used is the value of the translatables login_with.login
, login_with.link
or
login_with.unlink
(these are passed the provider as a variable, so you can use e.g.
Yo, login with %{provider}
). Fallbacks are Login with %{provider}
, Unlink from %{provider}
and
Link %{provider}
There is also support for listing the linked identities. An example of an account overview that show
the linked accounts:
<ul>
{% for identity in customer.identities.all %}
<li>
{% if identity.image %}<img
src="{{ identity.image }}"
alt="{{ identity.provider }} profile image"
/>{% endif %}Linked with {{ identity.provider | capitalize }}
</li>
{% endfor %}
</ul>
{% if customer.identities.facebook == blank %}
<p>{% login_with provider: 'facebook', action: 'link' %}</p>
{% endif %} {% if customer.identities.twitter == blank %}
<p>{% login_with provider: 'twitter', action: 'link' %}</p>
{% endif %}
To activate login with Microsoft Identity Platform
in the nimbu backend, you need a Tenant
and a
Client Id
. To obtain these, you need to create an application registration in the microsoft azure
portal:
You also need to update the default settings of the app to make it work with nimbu. In the
authentication section tick the ‘ID tokens’ checkbox and click ‘save’:
By default, users will be asked for consent when they login to nimbu with their microsoft account.
As an administrator you can give organisation wide consent to avoid this step. Click
Grant admin consent for …
in the ‘API permissions’ section.
This feature allows you to login with another Nimbu site, e.g. to create single sign-on for a group
of Nimbu sites. This is implemented as support for being an “OpenID Connect Provider” on the one
hand and an “login with Nimbu” integration (that is an “OpenID Connect Relaying Party” under the
hood) on the other hand.
The configuration of the “login with Nimbu” integration is identical to the setup of “login with *”
above. For the “issuer”, you fill in the primary domain of the site you want to login with. The
client (id and secret) needs to be configured in the backend of the site you wish to login with (see
below).
To configure the “OpenID Connect Provider”, there are three steps to take:
customers/oauth2_consent.liquid
templateEnable OpenID in the settings Note: only zenjoy employees can enable this feature.
In the backend of the site at ‘Settings > General’ tick the following checkbox:
Configure a client In the backend of the site at ‘Apps’, register a new application (as you
would for a cloud code application). Choose a recognizable and trustworthy name for the application.
As the ‘Authorization Callback URL’ (the name and hint are a bit misleading in this context: it is
actually the callback URL where the user is redirected after it authorized the application), fill
in: http://other.nimbu.site.tld/auth/nimbu/callback
Pay attention to:
https://
if the client site has TLS/SSL enabled on its primary domainAfter registering the application, make it public by clicking on the ‘Make Public Application’ in
the ‘More’ menu:
The credentials needed for the other site are listed at the top of the application’s information
page:
Setup the **customers/oauth2_consent.liquid**
template This page is shown to the user when
he/she uses the ‘login with nimbu’ feature of the client site for the first time. You can start from
this example to setup the template:
<h1>{{ application }} would like to access your account</h1>
<p>{{ application }} will get access to the following:</p>
<ul>
{% for scope in requested_scopes %}
<li>{{ scope.description }} ({{ scope.scope }})</li>
{% endfor %}
</ul>
{% oauth2_consent_form, class: "foo", button_class: "bar" %}
Typical scopes that will be requested are openid, email, profile
. The default for the
scope.description
variable is the capitalized scope. You can overwrite these descriptions in the
Copywriting. The keys are oauth2.consent.scope.<scope>
(where <scope>
is the value of the scope
(e.g. openid or email)). The page title is set to the oauth2.consent.page_title
translation
(default: Authorize %{application}
).
When enabled under integrations, a JWT can be issued to allow a certain user to be logged in (or
created if it did not exist yet).
A JSON Web Token containing all authentication information can be appended to each request in Nimbu,
using the jwt query parameter. If the JSON Web Token is valid, the user specified in the token
will be logged in (regardless of another user begin previously logged in for the session) and the
user information will be updated according to the information specified in the token. If the user
does not exists yet, it will be created.
Nimbu requires an email address to uniquely identify the user. It also requires a “first name” and
“last name”, as it is a required field in the Nimbu user data model. Beyond the required attributes,
which are shown in the table below, you may optionally send additional user profile data and custom
fields. This data is synced between your user management system and Nimbu.
The JSON Web Token should be generated as follows.
Specify HS256 as the JWT algorithm in the header of your JWT payload.
{
"typ": "JWT",
"alg": "HS256"
}
Nimbu does not support the RS256 and ES256 JWT algorithms.
Construct a JSON Web Token with at least following attributes:
iat: Issued At. The time the token was generated, this is used to help ensure that a given token
gets used shortly after it's generated. The value must be the number of seconds since UNIX epoch.
Nimbu allows up to two minutes clock skew, so make sure to configure NNTP or similar on your
servers.
jti: the JSON Web Token ID. A unique id for the token, used by Nimbu to prevent token replay
attacks - a JSON Web Token ID can only be used in one request during the lifetime of the token
email: Email of the user being signed in, used to uniquely identify the user record in Nimbu.
firstname: The surname of this user. The user in Nimbu will be created or updated in accordance
with this.
lastname: The name of this user. The user in Nimbu will be created or updated in accordance with
this.
Each unique JSON Web Token is valid once and for a duration of maximum 2 minutes after the Issued At
timestamp. If a shorter valid lifetime for the token is desired, the “exp” parameter can be added to
explicitly define a UNIX epoch timestamp after which the token is no longer valid.
Most JWT implementations take a hash and a secret, and return a plain string payload. For context,
here's an example in Ruby:
payload = JWT.encode({
:email => "john@bar.com", :firstname => "Foo", :lastname => "Bar", :iat => Time.now.to_i, :jti => rand(2<<64).to_s
}, "Our shared secret")
When "Force SSO" is enabled and a remote login / logout url is specified, the default login and
logout endpoints in Nimbu are disabled and the user will always be redirected to this remote
location if no user is authenticated.
When Nimbu redirects a user to your login script, it will also pass a return_to parameter in the
URL. This parameter contains the page to which the user wants to be redirected when the
authentication succeeds. After authentication with your login script, redirect the user to this
location with the jwt parameter appended containing the JSON Web Token for logging in.
Add a cookie consent popup to your website.
First create a page with your privacy policy and configure the consent options in the back-end at
'Settings > Consent'. The cookies that are used by your website are listed under 'Application' when
inspecting the website in your browser.
Next add the snippet below to your theme.
{% translate consent.message, assign: message, default: 'Wij gebruiken cookies voor volgende
doeleinden: {purposes}.' %} {% translate consent.changed, assign: changed, default: 'We voegden
nieuwe toepassingen toe die cookies gebruiken sinds uw laatste bezoek, gelieve uw voorkeuren aan te
passen.' %} {% translate consent.learn_more, assign: btn_learn_more, default: 'Lees meer' %} {%
translate consent.ok, assign: btn_ok, default: 'OK' %} {% translate consent.save, assign: btn_save,
default: 'Opslaan' %} {% translate consent.decline, assign: btn_decline, default: 'Afwijzen' %} {%
translate consent.close, assign: btn_close, default: 'Sluit' %} {% translate consent.modal.title,
assign: modal_title, default: 'Wij gebruiken cookies' %} {% translate consent.modal.message, assign:
modal_message, default: 'Wij gebruiken cookies. Hieronder kan je kiezen welke toepassingen je wenst
toe te laten.' %} {% translate consent.modal.privacy_policy.name, assign: modal_privacy_policy_name,
default: 'privacy policy' %} {% translate consent.modal.privacy_policy.text, assign:
modal_privacy_policy_text, default: 'Om meer te weten, lees onze {privacyPolicy}.' %} :plain {%
consent_manager, style_prefix: "nimbuCookie" %}
To make sure js-snippets don't run untill people accept the cookies involved, add the correct syntax
when including them.
<!-- GA-tag -->
{{ 'UA-xxxxxxx-x' | google_analytics_tag, consent: 'yourConsentName' }}
<!-- JS-tag -->
{{ 'some_filename.js' | javascript_tag, consent: 'yourConsentName' }}
<!-- Script-tag -->
<script type="opt-in" data-type="text/javascript" data-name="yourConsentName"></script>
For the styling add the scss below.
// cookie general settings
$cookie-notice-fullscreen: false;
$cookie-notice-logo: 'logo_white.svg';
$cookie-border-radius: 0;
$cookie-backdrop-color: $black;
$cookie-backdrop-opacity: 0.5;
// cookie notice settings
$cookie-notice-bg-color: $primary;
$cookie-notice-text-color: $white;
$cookie-notice-border-color: $white;
$cookie-notice-border-width: 0px;
// cookie modal settings
$cookie-modal-bg-color: $white;
$cookie-modal-text-color: $primary;
$cookie-modal-text-color-deemphasized: $gray-light;
$cookie-modal-color-separator: $gray-lighter;
$cookie-modal-switch-color: $primary;
.nimbuCookie {
.cookie-modal {
@extend .modal;
opacity: 1;
display: block;
z-index: 999;
display: flex;
justify-content: center;
align-items: center;
.cm-bg {
@extend .modal-backdrop;
opacity: $cookie-backdrop-opacity;
z-index: -1;
background-color: $cookie-backdrop-color;
}
.cm-modal {
@extend .modal-dialog;
@extend .modal-content;
color: $cookie-modal-text-color;
background-color: $cookie-modal-bg-color;
border-radius: $cookie-border-radius;
z-index: 1;
flex: 0 0 640px;
max-width: 100vw;
max-height: calc(100vh - 20px);
margin: 0;
display: flex;
.cm-header {
@extend .modal-header;
border-bottom: 1px solid $cookie-modal-color-separator;
flex-direction: column;
flex: 0 0 auto;
p {
width: 100%;
}
a {
text-decoration: underline;
}
.hide {
@extend .close;
position: absolute;
right: 32px;
top: 27px;
width: 12px;
height: 12px;
svg {
stroke: $cookie-modal-text-color;
}
}
.title {
font-size: 1.5rem;
line-height: 1.5rem;
text-transform: none;
margin-top: 0;
margin-bottom: 10px;
font-weight: 600;
}
p:last-child {
margin-bottom: 0;
}
}
.cm-body {
@extend .modal-body;
overflow-x: hidden;
overflow-y: scroll;
flex: 1 1 auto;
.cm-apps {
padding-left: 0;
margin-bottom: 0;
.cm-app {
display: block;
padding-left: 45px;
list-style: none;
&.cm-toggle-all {
&:before {
content: '';
display: block;
border-top: 1px solid $cookie-modal-color-separator;
margin-bottom: 1rem;
margin-right: -16px;
margin-left: -61px;
}
}
}
}
.cm-app-label {
position: relative;
user-select: none;
outline: 0;
.cm-app-title {
font-size: 1rem;
font-weight: 600;
cursor: pointer;
}
.cm-required {
color: $cookie-modal-text-color-deemphasized;
margin-left: 10px;
font-size: 0.7rem;
line-height: 1rem;
}
.switch {
@extend .custom-control;
@extend .custom-switch;
cursor: pointer;
position: absolute;
top: 0;
left: -45px;
.slider {
@extend .custom-control-label;
}
}
}
.cm-app-description {
margin-bottom: 0;
}
.purposes {
color: $cookie-modal-text-color-deemphasized;
font-size: 0.7rem;
margin-bottom: 10px;
}
.cm-app-input {
opacity: 0;
width: 0;
height: 0;
&:checked {
+ .cm-app-label {
.switch .slider {
&:before {
color: $white;
border-color: $cookie-modal-switch-color;
background-color: $cookie-modal-switch-color;
}
&:after {
background-color: #fff;
transform: translateX(0.75rem);
}
}
}
}
&#app-item-functional {
+ .cm-app-label {
.cm-app-title,
.switch {
cursor: not-allowed;
}
}
}
}
}
.cm-footer {
@extend .modal-footer;
position: relative;
border-top: 1px solid $cookie-modal-color-separator;
flex: 0 0 auto;
.cm-powered-by {
color: $cookie-modal-text-color-deemphasized;
font-size: 0.7rem;
position: absolute;
left: 15px;
top: 0;
bottom: 0;
margin: auto;
height: 0.7rem;
line-height: 0.7rem;
}
}
}
}
.cookie-notice {
@extend .theme-primary;
z-index: 999;
position: fixed;
display: flex;
flex-direction: column;
@if $cookie-notice-fullscreen {
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: transparent;
justify-content: center;
align-items: center;
background-color: transparentize(
$color: $cookie-backdrop-color,
$amount: 1 - $cookie-backdrop-opacity
);
} @else {
background: $cookie-notice-bg-color;
color: $cookie-notice-text-color;
width: 400px;
min-height: 220px;
bottom: 20px;
left: 20px;
max-width: calc(100vw - 40px);
padding: 15px;
padding-top: 15px;
border-color: $cookie-notice-border-color;
border-width: $cookie-notice-border-width;
border-style: solid;
border-radius: $cookie-border-radius;
}
&.cookie-notice-hidden {
display: none;
}
.cn-body {
display: flex;
flex-direction: column;
@if $cookie-notice-fullscreen {
background: $cookie-notice-bg-color;
color: $cookie-notice-text-color;
width: 600px;
max-width: calc(100vw - 60px);
max-height: calc(100vh - 60px);
height: 400px;
padding: 30px;
border-color: $cookie-notice-border-color;
border-width: $cookie-notice-border-width;
border-style: solid;
border-radius: $cookie-border-radius;
flex: 0 0 auto;
&:before {
content: '';
background-image: url('../images/#{$cookie-notice-logo}');
height: 60px;
background-repeat: no-repeat;
background-size: auto 100%;
margin-bottom: 30px;
}
@include media-breakpoint-down(sm) {
width: 320px;
height: auto;
&:before {
height: 40px;
}
}
} @else {
flex: 1 1 auto;
}
p {
flex: 1 1 auto;
margin-bottom: 0;
strong {
&:before {
content: '';
display: block;
height: 10px;
}
}
&.cn-ok {
margin-top: 0.5rem;
flex: 0 0 auto;
@if $cookie-notice-fullscreen {
display: flex;
.cm-btn {
flex: 1 1 auto;
}
@include media-breakpoint-down(sm) {
margin-top: 30px;
flex-direction: column;
.cm-btn {
margin-right: 0;
margin-bottom: 5px;
&.cm-btn-info {
margin-bottom: 0;
}
}
}
}
}
}
}
}
.cm-btn {
@extend .btn;
@extend .btn-outline-primary;
&.cm-btn-success {
@extend .btn-primary;
}
margin-right: 5px;
&:last-child {
margin-right: 0;
}
}
}
// height fix IE11
_:-ms-fullscreen,
:root .cm-modal {
height: 50vw;
min-height: 300px;
}
.cookie-consent-warning {
padding: 20px 20px;
margin-bottom: 20px;
background-color: $gray-lighter;
display: none;
}
.video-embed {
.cookie-consent-warning.cookie-consent-warning-external {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999999;
}
}
To allow users to open and change their consent settings you can add the link below:
<a href="#" onclick="nimbuConsentManager.show()">Open cookie consent options.</a>
Add a reCAPTCHA to form to protect it from robots.
First configure reCAPTCHA in the backend at ‘Settings > Push & Integrations’ in the section ‘Other
integrations’.
For the "Invisible reCAPTCHA", replace the existing submit tag or button in the form with following
code:
<script>
var submitInvisibleRecaptchaForm = function () {
document.getElementById('invisible-recaptcha-form').submit();
};
</script>
{% translate 'contactform.submit', default: "Send", assign: submit_label %} {% recaptcha_button,
callback: "submitInvisibleRecaptchaForm", class: "button", text: submit_label %}
Make sure you set the id of the surrounding form to invisible-recaptcha-form
or the value from the
callback script.
{% form register, id: "invisible-recaptcha-form" %}
{% endform %}
For the explicit “reCAPTCHA v2", just add following tag somewhere in the form markup:
{% recaptcha_tag %}
The options for the liquid tags are described at
https://github.com/ambethia/recaptcha.
As of June 21st, 2018, nimbu supports a new type of custom field: GeoJSON fields. It is also
possible to restrict the geometry type in the GeoJSON to a specific type. The following types are
supported: Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon,
GeometryCollection.
The current backend UI is very basic: one needs to copy/paste a GeoJSON in a textarea (except for
fields of type Point, see below).
You can use http://geojson.io to create a GeoJSON, but there’s a catch. This tool generates GeoJSON
Feature
objects in a FeatureCollection
. Nimbu only supports the geometry types. You need to copy
the geometry of a feature to nimbu, e.g. given the following GeoJSON generated by geojson.io, you
need to copy/paste the marked part:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[4.3341064453125, 50.91688748924508],
[4.2681884765625, 50.878777255570405],
[4.3011474609375, 50.80246319809847],
[4.3780517578125, 50.77815527465925],
[4.4659423828125, 50.79899141148548],
[4.482421875, 50.87184477102432],
[4.427490234375, 50.91688748924508],
[4.3341064453125, 50.91688748924508]
]
]
}
}
]
}
If you want to have multiple geometries at once (e.g. two disconnected polygons), you need to
convert the FeatureCollection
to a GeometryCollection
. Convert this:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[4.37255859375, 50.91602169392645],
[4.24346923828125, 50.80940599750376],
[4.45220947265625, 50.80680256863399],
[4.37255859375, 50.91602169392645]
]
]
}
},
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[4.698028564453124, 50.89610395554359],
[4.66644287109375, 50.86491125522503],
[4.733734130859375, 50.87271138798818],
[4.698028564453124, 50.89610395554359]
]
]
}
}
]
}
into this:
{
"type": "GeometryCollection",
"geometries": [
{
"type": "Polygon",
"coordinates": [
[
[4.37255859375, 50.91602169392645],
[4.24346923828125, 50.80940599750376],
[4.45220947265625, 50.80680256863399],
[4.37255859375, 50.91602169392645]
]
]
},
{
"type": "Polygon",
"coordinates": [
[
[4.698028564453124, 50.89610395554359],
[4.66644287109375, 50.86491125522503],
[4.733734130859375, 50.87271138798818],
[4.698028564453124, 50.89610395554359]
]
]
}
]
}
Here is a template for a point (note that longitude comes first in the coordinates array, which is
the reverse order of what e.g. google maps uses!):
{
"type": "Point",
"coordinates": [LONGITUDE, LATTITUDE]
}
NOTE: geospatial queries need a custom index on the geo field. Ask Zenjoy Engineering to create
one.
Liquid Assume a channel 'test' with a location GeoJSON field of type "Point" and an area GeoJSON
field of type "Polygon":
{% capture leuven_area %}
{"type":"Polygon","coordinates":[[[4.691591262817383,50.88911990494846],[4.683008193969727,50.88321783657009],[4.681892395019531,50.87926465708721],[4.686613082885742,50.87417376757672],[4.693565368652344,50.87027398670717],[4.702234268188477,50.8676197825757],[4.710559844970703,50.87011148875652],[4.714679718017578,50.874282090165394],[4.715967178344727,50.87785659435118],[4.714851379394531,50.881376671415794],[4.709444046020508,50.8863043325714],[4.701118469238281,50.88955305482511],[4.696741104125977,50.889769628253],[4.691591262817383,50.88911990494846]]]}
{% endcapture %} {% capture leuven_location %}
{"type":"Point","coordinates":[4.69965934753418,50.8793188124966]} {% endcapture %} {% capture
brussels_area %}
{"type":"Polygon","coordinates":[[[4.358482360839844,50.91796971074143],[4.269905090332031,50.8744445735763],[4.280548095703125,50.82177030335975],[4.3231201171875,50.78206275772177],[4.380798339843749,50.764259357116465],[4.4803619384765625,50.79269814077799],[4.447059631347655,50.81070765753148],[4.477272033691405,50.82090273956621],[4.461479187011719,50.852558375579136],[4.420623779296875,50.868161469162345],[4.436073303222656,50.878777255570405],[4.431610107421875,50.89480467658874],[4.399681091308594,50.91364067374232],[4.358482360839844,50.91796971074143]]]}
{% endcapture %} {% capture brussels_location %}
{"type":"Point","coordinates":[4.350929260253906,50.84822325629631]} {% endcapture %} {% capture
belgium_area %}
{"type":"Polygon","coordinates":[[[3.3563232421875,51.364921488259526],[2.5543212890625,51.087997750516124],[5.605773925781249,49.52164252537975],[5.8062744140625,51.14144802734404],[4.46044921875,51.46598592502469],[3.7902832031250004,51.17934297928927],[3.3563232421875,51.364921488259526]]]}
{% endcapture %} {% capture leuven_to_brussels %}
{"type":"LineString","coordinates":[[4.7021484375,50.878777255570405],[4.3581390380859375,50.848440021829276]]}
{% endcapture %} {% capture invalid_geometry %}
{"type":"LineString","coordinates":[4.7021484375,50.878777255570405,4.3581390380859375,50.848440021829276]}
{% endcapture %}
<h2>In leuven</h2>
{% scope location.geoWithin: leuven_area %}
<ul>
{% for entry in channels.test %}
<li>{{ entry.name }}</li>
{% endfor %}
</ul>
{% endscope %}
<h2>In brussels</h2>
{% scope location.geoWithin: brussels_area %}
<ul>
{% for entry in channels.test %}
<li>{{ entry.name }}</li>
{% endfor %}
</ul>
{% endscope %}
<h2>In belgium</h2>
{% scope location.geoWithin: belgium_area %}
<ul>
{% for entry in channels.test %}
<li>{{ entry.name }}</li>
{% endfor %}
</ul>
{% endscope %}
<h2>Area intersects with line leuven to brussels</h2>
{% scope area.geoIntersects: leuven_to_brussels %}
<ul>
{% for entry in channels.test %}
<li>{{ entry.name }}</li>
{% endfor %}
</ul>
{% endscope %}
<h2>Near Leuven (5 km)</h2>
<!-- maxDistance and minDistance are optional and are in meters -->
{% capture five_km_from_leuven %} { "geometry": {{leuven_location}}, "maxDistance": 5000,
"minDistance": 100} {% endcapture %} {% scope location.near: five_km_from_leuven %}
<ul>
{% for entry in channels.test %}
<li>{{ entry.name }}</li>
{% endfor %}
</ul>
{% endscope %}
<h2>Leuven is in ...</h2>
{% scope area.geoIntersects: leuven_location %}
<ul>
{% for entry in channels.test %}
<li>{{ entry.name }}</li>
{% endfor %}
</ul>
{% endscope %}
<h2>Invalid = no-op</h2>
<!-- invalid geometry -->
{% scope area.geoIntersects: invalid_geometry %}
<ul>
{% for entry in channels.test %}
<li>{{ entry.name }}</li>
{% endfor %}
</ul>
{% endscope %}
<!-- name is not a geo field -->
{% scope name.geoIntersects: leuven_location %}
<ul>
{% for entry in channels.test %}
<li>{{ entry.name }}</li>
{% endfor %}
</ul>
{% endscope %}
<!-- near only works with points -->
{% scope area.near: leuven_area %}
<ul>
{% for entry in channels.test %}
<li>{{ entry.name }}</li>
{% endfor %}
</ul>
{% endscope %}
<!-- geoWithin only works with Polygon or MultiPolygon -->
{% scope area.geoWithin: leuven_location %}
<ul>
{% for entry in channels.test %}
<li>{{ entry.name }}</li>
{% endfor %}
</ul>
{% endscope %}
<!-- also works if you give it a value you queried -->
{% for entry in channels.test %} {% if entry.area != blank %}
<h2>Intersects with {{ entry.name }}</h2>
{% scope area.geoIntersects: entry.area %}
<ul>
{% for entry2 in channels.test %}
<li>{{ entry2.name }}</li>
{% endfor %}
</ul>
{% endscope %} {% endif %} {% endfor %}
API Querying via the API is similar to liquid, but beware of url-encoding the GeoJSON
GET /channels/test/entries?only=name&location.near=%7B%22maxDistance%22%3A5000%2C%22geometry%22%3A%7B%22type%22%3A%22Point%22%2C%22coordinates%22%3A%5B4.69965934753418%2C50.8793188124966%5D%7D%7D HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: <REDACTED>
Connection: keep-alive
Host: api.nimbu.io
User-Agent: HTTPie/0.9.9
X-Nimbu-Client-Version: 1.2.0
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Link, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Requested-With
Cache-Control: must-revalidate, private, max-age=0
Connection: keep-alive
Content-Length: 267
Content-Type: application/json; charset=utf-8
Date: Wed, 20 Jun 2018 14:11:40 GMT
ETag: W/"865b3f1dd5edfce37fdfab046a17200f"
Server: Nimbu.io
X-Accepted-OAuth-Scopes: read_channels, write_channels
X-Content-Type-Options: nosniff
X-Nimbu-API-Version: 0
X-OAuth-Scopes: write_channels
X-Rack-Cache: miss
X-Request-Id: 3a67ad92-4e45-42df-8f62-ae663888a16a
X-Runtime: 4.728852
X-Served-By: d5385d53f232a8943340783df509e6d5
[
{
"id": "5b27aae5d5385d4238000004",
"name": "Leuven",
"url": "https://api.nimbu.io/channels/test/entries/5b27aae5d5385d4238000004"
},
{
"id": "5b2a46e7d5385dca1e000011",
"name": "Bertem en Leefdaal",
"url": "https://api.nimbu.io/channels/test/entries/5b2a46e7d5385dca1e000011"
}
]
Support for geoWithin
, geoIntersects
and near
has been added to the javascript SDK (v1.x+
only, use //cdn.nimbu.io/js/sdk/v1/nimbu.min.js).
For points the back-end UI shows two input fields to enter the latitude and longitude. The creation
of a GeoJSON of type Point is handled transparently. To make the use of Points in the front-end
easier, there are 2 liquid filters: to_geopoint
and is_geopoint
:
{% scope area.geoWithin: belgium_area %}
<ul>
{% for entry in channels.test %}
{% assign point = entry.location | to_geopoint %}
{% assign area_is_geopoint = entry.area | is_geopoint %}
<li>
{{ entry.name }} {% if point %}({{ point.latitude }}, {{ point.longitude }}) or ({{ point.lat }}, {{ point.lng }}){% endif %}
{% if area_is_geopoint %}
(area is a geopoint {{ entry.area | is_geopoint }})
{% else %}
(area is not a geopoint)
{% endif %}
</li>
{% endfor %}
</ul>
To allow backwards incompatible changes to our liquid tags and filters, we built in a versioning
scheme. By default the “liquid version” of a theme is set to the day before the theme was created.
To opt-in to a newer (or older) version, use the following code snippet:
{% theme_liquid_version '20191212' %}
This version changes the behaviour of the id
parameter on {% input %}
tags. Before this version,
the custom id was only used for the input
itself, but not for all derived ids and attributes (e.g.
the for
attribute of the label
, the id
s of the options in a select, …). This version corrects
this behaviour and uses the custom id
for derived id
s too.