Bonjour!
Welcome to the Bundles 2.0 Live Demo. The main audience for this demo is existing developers working within Expression looking for a "crash course" on the new stuff Bundles 2.0 provides - and analogues to existing techniques that will be familiar to individuals with experience developing within Expression.
If you're not familiar with Expression - find us over at expression.cloud. Expression is a fully customizable SaaS CMS, Digital Commerce, and Development Platform that empowers developers and marketers to build amazing digital experiences together. Bundles are just one part of that experience so if you've stumbled upon us, take a look - If you've been looking for an alternative to Wordpress, Drupal, Magento, and all the usual suspects, you've found what you're looking for.
Entry Point
This page, like all pages in Expression, is rendered with an Element
. An Element is an HTML template for marking up data pulled from Datasources, used to render blocks of content. Elements are based on the Handlebars templating language. Elements can reference other Elements, allowing for modular markup construction
The Element rendering this page (template.hbs)
can be viewed in GitHub here: xpr/element/template.hbs
Each Element can optionally have a configuration file. The configuration for this Element can be viewed in GitHub here: xpr/element_config/template.json
This page is using Elements to render links to files in GitHub: {{{XprElement bundlePath="git_link" options.path="xpr/element/git_link.hbs"}}}.
Bundle Web Directory

Bundles can now include static media. This is great for custom images or assets which should be developer managed, rather than client managed. Static media lives in the bundle's web directory: xpr/web/
Using for CSS and JS
This is how we include the CSS and JS for this bundle:href="{{{WebRoot xpr.bundle}}}/css/devon-custom.css"
Developers may be interested in using this feature to use CSS/JS compilers or js package managers to compile custom css, and place the end result in the /xpr/web/
directory.
Request Context
Let's see how you can dump context variables using the XprJson
. We will dump the xpr
variable
xpr
variable you may be used to from working with Elements in traditional Bundles.
{{{XprJson xpr}}}
{"settings":[],"request":{"debug":false,"uri":"\/","pageLandingType":"section","cookies":[],"domain":{"_links":{"self":{"href":"\/api\/sections\/7","title":"Section","name":"\/"},"sectionTypes":{"href":"\/api\/sectionTypes\/","title":"SectionType"},"find":{"href":"\/api\/sections\/{id}","templated":true}},"Id":7,"SortOrder":1,"Path":null,"Status":1,"Type":"domain","UsesPlaylists":false,"Variables":"","Popup":null,"UsesPermissions":false,"UsesSSL":true,"Target":"default","Config":null,"ModId":null,"Aliasing":null,"LockParent":null,"CreatedBy":null,"DefaultPageTitle":"","AdminTemplate":null,"Package":null,"ComponentDir":null,"ComponentFile":null,"RestrictedLink":"","DocType":"strict","LastModified":"2020-05-05 08:50:35","CreatedOn":null,"UrlRemainderPattern":"","ComputedDisplayDate":null,"RendererBundlePath":":bundles-demo\/element\/template","Name":"bundles-demo.xpr.cloud","Description":"","MetaTagKeywords":"","MetaTagDescription":"","Title":"bundles-demo.xpr.cloud","Slug":"bundles-demo.xpr.cloud","Invisible":0,"_locale":"en_CA","CanonicalUrl":"https:\/\/bundles-demo.xpr.cloud\/","IsActive":true,"_UsesPermissions":"unrestricted","_embedded":{"Links":[{"_links":{"self":{"href":"\/api\/sections\/sectionlink\/","title":"Section","name":"\/"}},"CanonicalUrl":"https:\/\/bundles-demo.xpr.cloud\/","_locale":"en_CA"}],"Children":[{"_links":{"self":{"href":"\/api\/sections\/5024","title":"Section","name":"\/"}},"Id":5024}],"DomainLanguages":[{"_links":{"self":{"href":"\/api\/domainlanguages\/27","title":"DomainLanguage","name":"\/"}},"Id":27}]}},"store":{"_links":{"self":{"href":"\/api\/store\/stores\/1","title":"Store","name":"\/"},"find":{"href":"\/api\/store\/stores\/{id}","templated":true}},"Id":1,"Name":"Default Store","StreetAddress":null,"Apartment":null,"PostalCode":null,"City":null,"PhoneNumber":null,"CurrencyId":1,"DefaultLocale":"","InventoryManagement":"physical","BackOrdersEnabled":false,"AdditionalOrderEmails":null,"UserGroupId":1,"IsDefault":null,"DefaultCustomerType":0,"LoginOnRegistration":false,"UseEmailAsLogin":false,"TimezoneId":0,"InitialOrderStatus":"Pending","ActiveCategoryIdVarName":null,"ActiveProductIdVarName":null,"DisplayOutOfStock":false,"InvoiceTemplateId":0,"SubscriptionEmailTemplateId":0,"DomainId":7,"_embedded":{"Country":{"_links":{"self":{"href":"\/api\/store\/countries\/1","title":"Country","name":"\/"}},"Id":1},"Region":{"_links":{"self":{"href":"\/api\/store\/countryRegions\/1","title":"CountryRegion","name":"\/"}},"Id":1},"Domain":{"_links":{"self":{"href":"\/api\/sections\/7","title":"Section","name":"\/"}},"Id":7}}},"users":[],"section":{"_links":{"self":{"href":"\/api\/sections\/7","title":"Section","name":"\/"},"sectionTypes":{"href":"\/api\/sectionTypes\/","title":"SectionType"},"find":{"href":"\/api\/sections\/{id}","templated":true}},"Id":7,"SortOrder":1,"Path":null,"Status":1,"Type":"domain","UsesPlaylists":false,"Variables":"","Popup":null,"UsesPermissions":false,"UsesSSL":true,"Target":"default","Config":null,"ModId":null,"Aliasing":null,"LockParent":null,"CreatedBy":null,"DefaultPageTitle":"","AdminTemplate":null,"Package":null,"ComponentDir":null,"ComponentFile":null,"RestrictedLink":"","DocType":"strict","LastModified":"2020-05-05 08:50:35","CreatedOn":null,"UrlRemainderPattern":"","ComputedDisplayDate":null,"RendererBundlePath":":bundles-demo\/element\/template","Name":"bundles-demo.xpr.cloud","Description":"","MetaTagKeywords":"","MetaTagDescription":"","Title":"bundles-demo.xpr.cloud","Slug":"bundles-demo.xpr.cloud","Invisible":0,"_locale":"en_CA","CanonicalUrl":"https:\/\/bundles-demo.xpr.cloud\/","IsActive":true,"_UsesPermissions":"unrestricted","_embedded":{"Links":[{"_links":{"self":{"href":"\/api\/sections\/sectionlink\/","title":"Section","name":"\/"}},"CanonicalUrl":"https:\/\/bundles-demo.xpr.cloud\/","_locale":"en_CA"}],"Children":[{"_links":{"self":{"href":"\/api\/sections\/5024","title":"Section","name":"\/"}},"Id":5024}],"DomainLanguages":[{"_links":{"self":{"href":"\/api\/domainlanguages\/27","title":"DomainLanguage","name":"\/"}},"Id":27}]}},"article":{"_links":{"self":{"href":"\/api\/articles\/5","title":"Article","name":"\/"}},"Id":5,"Title":"Content Management Article #1","PageTitle":"","Description":"","Html":"<p><b>Hola! <\/b>I am content within a content management article. If you're seeing this content, It was pulled dynamically and rendered by an Element!<\/p>\n","CreatedOn":"2020-04-29 20:24:38","Active":1,"PendingArticleId":0,"MetaTagKeywords":"","MetaTagDescription":"","CreatedBy":7,"Homepage":false,"SortOrder":0,"Invisible":false,"Searchable":false,"StartDate":null,"EndDate":null,"DisplayDate":null,"LanguageParentId":5,"ArticleLink":false,"ArticleLinkUrl":"","ArticleLinkRedirect":false,"RenderLinkedArticleUrl":false,"LastModified":"2020-05-04 10:19:27","LastModifiedBy":7,"Slug":"content-management-article-1","CanonicalUrl":"https:\/\/bundles-demo.xpr.cloud\/content-management-article-1","RendererBundlePath":"","_embedded":{"Section":{"_links":{"self":{"href":"\/api\/sections\/7","title":"Section","name":"\/"}},"Id":7},"Language":{"_links":{"self":{"href":"\/api\/languages\/1","title":"Language","name":"\/"}},"Id":1},"Domain":{"_links":{"self":{"href":"\/api\/sections\/7","title":"Section","name":"\/"}},"Id":7}}},"urlParams":{"section_id":"7","section_copy_id":"0"},"user":null,"language":{"_links":{"self":{"href":"\/api\/languages\/1","title":"Language","name":"\/"},"find":{"href":"\/api\/languages\/{id}","templated":true}},"Id":1,"Name":"English","Default":true,"Active":true,"IsoCode":"en","Abbreviation":"EN","IsoCode2":"CA","Locale":"en_CA"},"client":{"ip_address":"3.229.120.26","http_accept":"text\/html,application\/xhtml+xml,application\/xml;q=0.9,*\/*;q=0.8","http_accept_charset":null,"http_accept_encoding":"gzip","http_accept_language":"en-US,en;q=0.5","http_referer":null,"http_user_agent":"CCBot\/2.0 (https:\/\/commoncrawl.org\/faq\/)"},"body":"","headers":{"X-Varnish":"981536259","Accept-Encoding":"gzip","X-Forwarded-For":"3.229.120.26","Host":"bundles-demo.xpr.cloud","Accept-Language":"en-US,en;q=0.5","Accept":"text\/html,application\/xhtml+xml,application\/xml;q=0.9,*\/*;q=0.8","User-Agent":"CCBot\/2.0 (https:\/\/commoncrawl.org\/faq\/)","X-Forwarded-Proto":"https","X-Real-Ip":"3.229.120.26"}},"site":{"home":{"section":{"_links":{"self":{"href":"\/api\/sections\/7","title":"Section","name":"\/"},"sectionTypes":{"href":"\/api\/sectionTypes\/","title":"SectionType"},"find":{"href":"\/api\/sections\/{id}","templated":true}},"Id":7,"SortOrder":1,"Path":null,"Status":1,"Type":"domain","UsesPlaylists":false,"Variables":"","Popup":null,"UsesPermissions":false,"UsesSSL":true,"Target":"default","Config":null,"ModId":null,"Aliasing":null,"LockParent":null,"CreatedBy":null,"DefaultPageTitle":"","AdminTemplate":null,"Package":null,"ComponentDir":null,"ComponentFile":null,"RestrictedLink":"","DocType":"strict","LastModified":"2020-05-05 08:50:35","CreatedOn":null,"UrlRemainderPattern":"","ComputedDisplayDate":null,"RendererBundlePath":":bundles-demo\/element\/template","Name":"bundles-demo.xpr.cloud","Description":"","MetaTagKeywords":"","MetaTagDescription":"","Title":"bundles-demo.xpr.cloud","Slug":"bundles-demo.xpr.cloud","Invisible":0,"_locale":"en_CA","CanonicalUrl":"https:\/\/bundles-demo.xpr.cloud\/","IsActive":true,"_UsesPermissions":"unrestricted","_embedded":{"Links":[{"_links":{"self":{"href":"\/api\/sections\/sectionlink\/","title":"Section","name":"\/"}},"CanonicalUrl":"https:\/\/bundles-demo.xpr.cloud\/","_locale":"en_CA"}],"Children":[{"_links":{"self":{"href":"\/api\/sections\/5024","title":"Section","name":"\/"}},"Id":5024}],"DomainLanguages":[{"_links":{"self":{"href":"\/api\/domainlanguages\/27","title":"DomainLanguage","name":"\/"}},"Id":27}]}}}},"server":{"date":"2021-04-10"},"bundle":"bundles-demo"}
About Element
Demonstrates using {{{XprElement}}}
to pull in an Element
The code for this Element (about.hbs)
can be viewed in GitHub here: xpr/element/about.hbs
{{{XprElement bundlePath="about"}}}
... Hello! I am a Sub Element!
This repository intends to practically demonstrate how to accomplish the main mechanics of development in Expression using the Bundles 2.0 framework. This Element is a simple snippet of static HTML.Attached Data

For these examples, we will be pulling data from bundles-demo.xpr.cloud's Content Management API's
(template.hbs)
can be viewed in GitHub here: xpr/element/template.hbs
An Element's configuration includes the Datasources attached, and any options to configure those Datasources. This template
Element has a Datasource attached which uses the datasource/articles_by_section.json
Datasource Definitionto populate the variable {{PageArticles}}
for this Element.
{{#each PageArticles}}
<li> {{Title}} </li>
{{/each}}
- Content Management Article #1
- Content Management Article #2
Context Passing
Elements can have data from the root
(also known as parent
) context passed into them to be rendered. Here we are going to loop over the same data as in the Attached Data context, but instead of rendering each article inline, pass the article to a sub Element for rendering. xpr/element/details/article_details.hbs
{{#each PageArticles}}
{{{XprElement bundlePath="details/article_details" context.Article=this}}}
{{/each}}
I am a Sub Element. I accept "Article" objects and render them. Here is a rendering of the article that was passed into me:
Title:Content Management Article #1
Hola! I am content within a content management article. If you're seeing this content, It was pulled dynamically and rendered by an Element!
I am a Sub Element. I accept "Article" objects and render them. Here is a rendering of the article that was passed into me:
Title:Content Management Article #2
This is a second article from Content Management
Element Options
"Sub" Elements can also have their own Datasources attached. While it is usually better to "fetch up front and pass down", sometimes you want an Element which controls and fetches its own data. Here we demonstrate an Element which will fetch an article based on Id, and we will use the PageArticles
variable from the previous example to supply the list of Ids.
Note that we are "wasting" some effort here by refetching from the API, this is just an illustration
These are the files in use for this demonstration, and an overview of the flow:
-
This Element configuration file receives the
options
and passesArticleId
to the downstream Datasource: xpr/element_config/details/article_details_fetching.json -
This Datasource receives
options.ArticleId
and uses it to parameterize an API call: xpr/datasource/details/article_by_id.json - Finally, this template renders the result of the Datasource: xpr/element/details/article_details_fetching.hbs
{{#each PageArticles}}
{{{XprElement bundlePath="details/article_details_fetching" options.ArticleId=this.Id}}}
{{/each}}
I am a Sub Element. I fetch "Article" objects based on an option passed in, and then render them.
Title:Content Management Article #1
Hola! I am content within a content management article. If you're seeing this content, It was pulled dynamically and rendered by an Element!
I am a Sub Element. I fetch "Article" objects based on an option passed in, and then render them.
Title:Content Management Article #2
This is a second article from Content Management
SSJS - Bind Directly To Element
Element Inclusion
{{{XprElement bundlePath="details/ssjs" options.message='Hello World!'}}}
Element Configuration
xpr/element_config/details/ssjs.json
{
"datasources": [
{
"__comment" : "Note that there is no 'path' specified for this Datasource, in Bundles 2 there is no need to create 'null' adapters when only requiring a script binding",
"name" : "script_output",
"scripts" : [
{
"_comment" : "Notice the preceeding / in the path which resolves to the script in /server_js/simple_echo. If the path was set to 'simple_echo' with no preceeding /, it would look for a 'neighbor file' which would be in /server_js/details/simple_echo",
"path" : "/simple_echo",
"options" : {
"message" : "{{options.message}}"
}
}
]
}
]
}
Script
xpr/server_js/simple_echo.js
//this is a "simple echo" JS script to demonstrate binding scripts to Elements and Datasources.
exports.process = function(context, options) {
return options["message"];
}
Result
I am a sub element. I have an attached SSJS script which I can render the output of. The output looks like this:
"Hello World!"
SSJS - Datasources & Modules
Coming Soon...
Bundle Routes
Bundle Routes are specified in the "routes.json" file at the root of the Bundle's xpr
folder.
xpr/routes.json
A route binds an endpoint (URL) and it's HTTP request verb to a script. and provides a formatter Element to render the result of the script.
This demo shows an ajax GET route, running the same script but supplying different formatters: one for HTML and one for JSON.
- xpr/routes.json
- xpr/server_js/ajax/script.js
- xpr/element/ajax/html_formatter.hbs
- xpr/element/ajax/json_formatter.hbs
Result:
Result: