Airtable in Automad — a fexible yet simple to use Automad extension that let’s you easily integrate Airtable bases into your site by using Handlebars templates.
Airtable is a great tool to quickly create your own database using a intuitive UI. While the possibilities of structuring data go far beyond the capabilities of Automad as a blogging platform, you might find out that Airtable lacks of flexibility and design options when it comes to sharing tables publicly. This is where Airmad comes in. The concept is rather simple. Airmad pulls a table — and optionally also its linked tables — using Airtable’s REST API. To speed things up and align them with the user experience of a small and lightweight Automad site, all retrieved recordes are cached on your server. Updated data is pulled from time to time.
Attention
Airmad requires your webserver to run PHP 7+ in order to work properly!
Airmad can be installed by using the Automad dashboard. However in case you would like to install the package by using Composer, just run the following command on your command line:
$ composer require airmad/airmad
Airtable requires an API token to authenticate when
accessing bases using their REST API. In case you don’t have one, you can easily
create one on your Airtable profile page. After successfully creating such token,
it has to be added to Automad’s config/config.php
file. That can be done by
navigating to System Settings > More > Edit Configuration File in the Automad
dashboard as demonstrated below. Aside from the authentictaion, there you can also
configure the Airtable cache lifetime and the model cache lifetime in seconds.
{
"AIRMAD_TOKEN": "keyXXXXXXXXXXXXXX",
"AIRMAD_CACHE_LIFETIME": 7200,
...
}
In some cases you want to be able to trigger an update of the cached tables and models instantly.
A fresh sync of the Airtable data with your Automad site can be forced by appending the airmad_force_sync
parameter
the URL of a page including an Airmad instance as follows:
https://domain.com/page?airmad_force_sync=1
That also comes in handy in case you want to automate the database sync using cron or bash:
curl -s 'https://domain.com/page?airmad_force_sync=1' > /dev/null
Airmad can either be used in template files as part of a theme or, as recommended, in a snippet block. The latter one allows for integrating Airmad into any existing theme that supports Automad’s block editor. The markup looks as follows:
Attention
You can simply paste an Airmad snippet directly into a code field of the new Template Snippet block on any page in the Automad dashboard.
<@ Airmad/Airmad {
base: 'appXXXXXXXXXXXXXX',
table: 'Design projects',
view: 'All projects',
fields: 'Name, Client',
filters: 'Client, Category',
formula: 'SEARCH(LOWER("@{ ?search }"), LOWER({Name}))',
linked: 'Client => Clients[Name Address]',
prefix: ':example',
template: '
{{#records}}
{{#fields}}
<div class="card">
<h3>{{Name}}</h3>
<p>
{{#Client}}
{{Name}}
{{/Client}}
</p>
</div>
{{/fields}}
{{/records}}
',
partials: '/packages/some/partials',
limit: 20,
page: @{ ?Page | def(1) }
} @>
The code above doesn’t produce any output. Instead it populates some Runtime
variables that can be used in the
Automad template at any point after the Airmad instance above. Note the prefix
parameter. The prefix is required to make sure that all runtime variables have unique names.
To display the generated output, the :exampleOutput
variable can be used in a
template for example as follows.
<div class="cards">
@{ :exampleOutput }
</div>
Attention
In case you want to use multiple Airmad instances on your site, you will have to define unique prefixes for each one in order to avoid conflicts between them.
The example above shows a typical use case of an Airtable integration. Find below a list of all availabe options.
Name |
Description |
---|---|
|
The Airtable base ID |
|
The main table to be used to pull records from |
|
The view of the main table to be used |
|
A comma separated list of fields that should be included in the records, leave empty to get all fields |
|
A required prefix for the generated runtime variables — prefixes have to be unique, in case more than one Airmad instance is used on the site |
|
A comma separated list of tables that are linked to a field
of the main table records — note that it is only required to list linked tables
here that include information that you want to display.
In case the field name differs from the actual table name to be linked,
it is also possible to pass a list of strings like
|
|
The Handlebar template to be used to render the model (the collection of records) — can be either a string, a variable containing a string or a file path |
|
You can optionally specify a path to a directory containing partials. That path must be
an absolute path in relation to your Automad installation directory, like
for example |
|
A comma separated list of fields that can be used to filter the records by — check out the examples below for more information about filtering |
|
A formula
to be used to for the |
|
The maximum number of records to be displayed on a page |
|
The current page of records (pagination) |
Aside from the output, Airmad provides more variables as shown in the table below. Note that :prefix
can be
replaced with any other valid string and is just used for demonstration here.
Name |
Description |
---|---|
|
The rendered output of the table records |
|
The number of found records |
|
The current page number — this has to be seen in context to
the |
|
The amount of pages the records are spread over,
also related to the |
|
The max memory used by Automad in bytes |
Attention
Note that you must define an unique prefix to be used instead of :prefix*
in the
Airmad options when creating a new instance.
Airmad offers two ways of searching an Airtable base — filters and formulas. While filters are very easy to use and allow for automatic filtering of records whenever there is a query string parameter with a column name present, formulas are way more flexible and powerful. In contrast to filters, formulas allow for searching across multiple fields by a custom formula. Take a look at the official formula documentation provided by Airtable for a full list of available options and examples.
<@ Airmad/Airmad {
base: 'appXXXXXXXXXXXXXX',
table: 'Design projects',
view: 'All projects',
formula: 'SEARCH(LOWER("@{ ?search }"), LOWER(CONCATENATE({Name}, {Client})))',
prefix: ':example',
template: '
{{#records}}
{{#fields}}
<h3>{{Name}}</h3>
{{/fields}}
{{/records}}
'
} @>
The example above will demonstrates how you can implement searching the Name
and the Client
fields of records
at the same time by only a single search parameter in the query string.
As mentioned earlier, Airmad uses Handlebars to render table data. Basically all model data is structured in a multi-dimensional array and can be accessed in a template. You can see the actual data while developing your templates by enabling debugging.
The model contains seven main elements — records
, filters
, filteredFilters
, query
,
count
, pages
and automad
. A typical structure looks as follows:
records
record
id
fields
column
column
...
record
id
fields
column
column
...
filters
...
filteredFilters
...
query
...
count
pages
automad
title
date
...
Name |
Description |
---|---|
|
The records element basically contains all rows in the given table |
|
A collection of items of columns specified in the filters option |
|
The |
|
The query element contains all parameters of the query string |
|
The count of records in filtered the model |
|
The number of pages needed to display all records in the model |
|
A bridge to a merged array of shared and page data. |
In a template you can therefore iterate all records using the Handlebars syntax as demonstarted below.
Note that column
just represents any column name in your table.
{{#records}}
{{#fields}}
{{column}}
{{/fields}}
{{/records}}
Attention
Airtable provides some example bases when setting up an account. The Airmad repository includes some example snipptes that are made to work with the Project tracker example base of Airtable. Take a look at the example template on GitHub.
To access a parameter in a query string like for example https://domain.com?parameter=value
you can simply use:
{{query.parameter}}
You can also access Automad’s page and shared data as follows as shown below. For example to get the title of the current page within a template you can simply use:
{{automad.title}}
Since the actual record ID is by default not a field, Airmad provides the dedicated _ID
field
that contains the actual record ID.
{{#records}}
{{ _ID }}
{{/records}}
In case you have fields that actually link to other tables in your base, the content of such a field is just a
bunch of record IDs. In most cases you would want to be able to actually get the values of the one or more
fields of that record. Fortunately Airmad automatically looks up the linked fields for you and replaces the ID string
with an array of the actual fields. The replaced ID is then moved to the _ID
field of the record’s array.
Let’s assume you have a Type
table and you want to access the Name
of each type linked to your product.
The data returned by the Airtable API looks for example as follows:
{
"fields": {
"Type": [
"recmD5WiE2GeV3ZIW",
"recuBUENcDgqnzSww",
"recj0zpg9qo8M7SeM"
]
}
}
Airmad will look up all contained fields automatically and expose the following data to the render engine:
{
"fields": {
"Type": [
{
"Name": "Chair",
"Product": ["recUtSDeLJ4HQI0uD", "recJcjDC9IN8Vws16"],
"_ID": "recmD5WiE2GeV3ZIW"
},
{
"Name": "Table",
"Product": ["recUtSDeLJ4HQI0uD"],
"_ID": "recuBUENcDgqnzSww"
},
{
"Name": "Carpet",
"Product": ["recJcjDC9IN8Vws16"],
"_ID": "recj0zpg9qo8M7SeM"
}
]
}
}
In a template you can therefore simple loop over the types and get the Name
as follows:
{{#Type}}
{{Name}}
{{/Type}}
To quickly understand the actual structure of the model returned by the Airtable API, you can enable the
Debug Mode in Automad and then take a look at the browser console.
Since there will be a lot of output, you can then simply filter the console by Airmad->Airmad
.
While there are all official Handlebars helpers available in templates, Airmad also provides some additional useful helpers as listed here below.
In case your table has an attachement field, you can use the {{#slider images}}
or
{{#sliderLarge images}}
helper functions to create an image slider containing all
provided images as that are listed in a field called images
in the field context of a record.
By default the slide will have an aspect ratio of 1:1 — in other words a height of 100% relative to the width.
You can pass an optional second argument to the helper to define a custom height as follows:
{{#slider images 75%}}
The normal slider uses resized thumbnails as source files. It is also possible to get the original image in a slider as follows:
{{#sliderLarge images 75%}}
In order to use values in a query string, it is good practice to sanitize those before as follows:
{{#sanitize field}}
The replace
helper can be used to search and replace within a field value using Regex:
{{#replace "/regex/", "replace", field}}
To get the actual context data instead of a rendered HTML output, you can render JSON output instead. To get all data simply use this as your template:
{{#json this}}
To just get the records for example, you can use:
{{#json records}}
Rich text content in Airtable fields is returned from the Airtable API in the Markdown format. In order to convert such content to actual HTML, the markdown helper can be used as follows:
{{#markdown field}}
In case you quickly want to compare a field value with any other value or string you can use the if==
helper:
{{#if== field, "value"}} ... {{/if==}}
{{#if== field, otherField}} ... {{/if==}}
Note that it might sometimes be required to compare sanitized values. This can be done as follows as well:
{{#ifsan== field, "value"}} ... {{/ifsan==}}
{{#ifsan== field, query.field}} ... {{/ifsan==}}
The counterpart to if==
helper is the if!=
helper that lets you check for inequality:
{{#if!= field, "value"}} ... {{/if!=}}
{{#if!= field, otherField}} ... {{/if!=}}
To compare sanitized values, please use:
{{#ifsan!= field, "value"}} ... {{/ifsan!=}}
{{#ifsan!= field, query.field}} ... {{/ifsan!=}}
Handlebars provides a great feature to enhance the use of lists. While it is possible to simply loop over items like:
{{#Type}}
{{Name}}
{{/Type}}
You can alternatively use the {{#each Type}} ... {{/each}}
helper to get more access to
built-in data variables like @first
, @last
and @index
. This is for example very
useful in case you need to concatenate a list of items with a comma:
{{#each Type}}
<i>{{Name}}</i>{{#unless @last}},{{/unless}}
{{/each}}
You can find more about the use of data variables in here.
The sorted
loop works exactly like a normal each
loop except the fact that all items
are sorted.
{{#sorted Category}}
{{this}}
{{/sorted}}
As a second argument, a key can be specified in order to sort an array of objects by the value for the given key.
{{#sorted Type, "Name"}}
<i>{{Name}}</i>{{#unless @last}},{{/unless}}
{{/sorted}}
The unique
loop works exactly like a normal each
loop except the fact that duplicate items are ignored.
{{#unique Type}}
<i>{{Name}}</i>{{#unless @last}},{{/unless}}
{{/unique}}
Aside from the examples above, Handlebars offers even more helpers that can be used in templates
such as with
, if
, unless
and others.
You can find the documentation
of those features as well on GitHub.
In order to use Handlebars partials, you have to define an absolute path in relation to your Automad directory
as the value for the partials
parameter when creating a new Airmad instance. All *.handlbars
files in that
directory can be used as partials as follows:
{{> myPartial }}
In many cases, the amount of records in a table is simple to much for a single page.
You will probably break down the list of records into multiple pages by setting the limit
option to a fixed number. To help you building a simple pagination navigation,
Airmad provides the :[prefix]Page
and :[prefix]Pages
runtime
variables. As mentioned before, please replace :[prefix] with a unique string in the Airmad options.
A very simple example for a pagination within an Automad snippet could look as follows:
<ul class="uk-pagination">
<@ if @{ ?Page } > 1 @>
<li><a href="?<@ queryStringMerge { Page: @{ ?Page | -1 } } @>">←</a></li>
<@ end @>
<@ for @{ :prefixPage | -3 } to @{ :prefixPage | +3 } @>
<@ if @{ :i } > 0 and @{ :i } <= @{ :prefixPages } @>
<li>
<a
href="?<@ queryStringMerge { Page: @{ :i } } @>"
<@ if @{ ?Page | def(1) } = @{ :i } @>class="uk-active"<@ end @>
>
@{:i}
</a>
</li>
<@ end @>
<@ end @>
<@ if @{ ?Page } < @{ :prefixPages } @>
<li><a href="?<@ queryStringMerge { Page: @{ ?Page | +1 } } @>">→</a></li>
<@ end @>
</ul>
You can simply copy and paste this into a snippet block after creating an Airmad instance and change the prefix. The classes in use will work out of the box with the Standard and Adam themes.
Searching and filtering are essential functions for displaying database content. In Airmad filtering records is pretty straight forward. The following example demonstrates the basic idea:
<form action="">
<input type="text" name="Category">
<input type="text" name="Client">
</form>
<@ Airmad/Airmad {
base: 'appXXXXXXXXXXXXXX',
table: 'Design projects',
view: 'All projects',
filters: 'Client, Category',
linked: 'Client => Clients',
template: '
{{#records}}
{{#fields}}
<div class="card">
<h3>{{Name}}</h3>
<p>
{{#Client}}
{{Name}}
{{/Client}}
</p>
</div>
{{/fields}}
{{/records}}
',
limit: 20,
page: @{ ?Page | def(1) }
} @>
In the snippet above, we have a simple form at the top including two input fields
with the names Category
and Client
. The Airmad instance below that form has those names defined as filters
as you
can see in the highlighted line. Note that since in this example Client is a linked table, defining the linked
parameter
allows for searching in linked records as well.
By default, all items with a field that contain the filter string are included in the list of matched records. However in case you prefer to only include exact matches where the filter string equals the actual field value or one of the field values, the filter value has to be wrapped in double quotes.
So basically a query string like
https://domain.com?name=value
has to be changed to
https://domain.com?name="value"
to only match records with a name
that is equals value
.
It is also possible to combine multiple values to filter a particular field. In that case, all provided filter values have to match the a record field in order to include that record in the resulting set. The commonly used array-like notation for query strings can be used as follows:
https://domain.com?name[]=value1&name[]=value2
Now only records with a field called name
that matches both, value1
as well as value2
are include
in the filtered collection.
To enhance the user experience for your visitors, you might want to provide an autocompletion list of categories
for the Category input and Client names for the second input field.
The Airmad data model contains a filters element at the top level for such purpose. It contains lists of
records that are contained in one ore more items in records for each name defined in the filters
option.
In the following example, such filters element contains all Client elements that match any record in the
records list. Note that it is also possible to just have a reduced list of filters that actually match an
already filtered set of records. Such a reduced list can be accessed by using the filteredFilters element.
You can use filters as follows:
{{#with filteredFilters}}
<form action="">
<input type="text" list="Categories" name="Category">
<datalist id="Categories">
{{#each Category}}
<option value="{{this}}">
{{/each}}
</datalist>
<input type="text" list="Clients" name="Client">
<datalist id="Clients">
{{#each Client}}
<option value="{{Name}}">
{{/each}}
</datalist>
<button type="submit">Apply</button>
</form>
{{/with}}
<@ Airmad/Airmad {
base: 'appXXXXXXXXXXXXXX',
table: 'Design projects',
view: 'All projects',
linked: 'Client => Clients',
filters: 'Client, Category',
template: '
{{#records}}
{{#fields}}
<div class="card">
<h3>{{Name}}</h3>
<p>
{{#Client}}
{{Name}}
{{/Client}}
</p>
</div>
{{/fields}}
{{/records}}
',
limit: 20,
page: @{ ?Page | def(1) }
} @>
The following example is supposed to wrap all features of Airmad like getting records, filtering and building a pagination navigation. To allow for quick testing, the base for this example is the Project tracker database that serves as sample content when creating a new account on Airtable. Therefore it should be easy to just copy and paste the code — by replacing the app ID of course — after setting up authentication.
Attention
Make sure that you already have added the AIRMAD_TOKEN to your configuration as described in the Get Stared guide. And don’t forget to replace the base ID in the snippet below with the one in your API documentation!
You can simply paste the following snippets to a snippet block — don’t forget to change the base ID. The first part is the template string:
<@ set {
:base: 'appXXXXXXXXXXXXXX',
:tmplt: '
<form id="example" action="" class="uk-flex uk-flex-space-between">
{{#with filteredFilters}}
<input
class="uk-button uk-width-medium-3-10"
type="text" list="Categories"
name="Category"
placeholder="Category"
value="{{../query.Category}}"
>
<datalist id="Categories">
{{#each Category}}<option value="{{this}}">{{/each}}
</datalist>
<input
class="uk-button uk-width-medium-3-10"
type="text" list="Clients"
name="Client"
placeholder="Client"
value="{{../query.Client}}"
>
<datalist id="Clients">
{{#each Client}}<option value="{{Name}}">{{/each}}
</datalist>
{{/with}}
<button class="uk-button reset uk-width-medium-3-10">↺ Reset</button>
</form>
<div class="am-stretched grid cards">
{{#records}}
{{#fields}}
<div class="card">
<div class="card-content uk-panel uk-panel-box">
<div class="uk-panel-teaser">
{{#slider Project images}}
</div>
<div class="uk-panel-title">
{{Name}}
</div>
<p>
{{#Client}}
{{Name}}
{{/Client}}
</p>
</div>
</div>
{{/fields}}
{{/records}}
</div>
'
} @>
The next snippet contains the actual plugin initialization of Airmad:
<@ Airmad/Airmad {
base: @{ :base },
table: 'Design projects',
view: 'All projects',
linked: 'Client => Clients',
filters: 'Client, Category',
template: @{ :tmplt },
limit: 8,
prefix: ':design',
page: @{ ?Page | 1 }
} @>
@{ :designOutput }
Now we can add the pagination:
<ul class="uk-pagination">
<@ if @{ ?Page } > 1 @>
<li><a href="?<@ queryStringMerge { Page: @{ ?Page | -1 } } @>">←</a></li>
<@ end @>
<@ for @{ :designPage | -4 } to @{ :designPage | +4 } @>
<@ if @{ :i } > 0 and @{ :i } <= @{ :designPages } @>
<li><a href="?<@ queryStringMerge { Page: @{ :i } } @>" <@ if @{ ?Page | def(1) } = @{ :i } @>
class="uk-active"
<@ end @>>@{:i}</a></li>
<@ end @>
<@ end @>
<@ if @{ ?Page } < @{ :designPages } @>
<li><a href="?<@ queryStringMerge { Page: @{ ?Page | +1 } } @>">→</a></li>
<@ end @>
</ul>
And finally, we need a tiny bit of Javascript too:
<script>
let form = document.getElementById('example'),
inputs = form.querySelectorAll('input'),
reset = form.querySelector('.reset');
inputs.forEach((input) => {
input.addEventListener('change', () => {
form.submit();
});
});
reset.addEventListener('click', () => {
inputs.forEach((input) => {
input.value = '';
});
form.submit();
});
</script>
The above example is also included as a snippet file
in the Airmad repository. Like the example above, the snippet is tailored to work with Airtable’s default Project tracker base.
After adding the Airtable token as described before, you can load the example.php
snippet in a block.
To actually make it work with the base in your account, as a last step you have to define the base ID as follows right before
the Include Snippet File dropdown in the block editor:
<@ set { :base: 'appXXXXXXXXXXXXXX' } @>