cordbolte / statamic-alpine
Pass Statamic data to Alpine.js (or any JavaScript) as JSON: select fields, join relations, add Glide images and rendered HTML.
Package info
github.com/cordbolte/statamic-alpine
Type:statamic-addon
pkg:composer/cordbolte/statamic-alpine
Requires
- php: ^8.2
- statamic/cms: ^5.0 || ^6.0
Requires (Dev)
- orchestra/testbench: ^9.0 || ^10.0 || ^11.0
- phpunit/phpunit: ^11.0 || ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Pass Statamic data to Alpine.js as JSON, right from your Antlers templates:
<div x-data="{ posts: {{ alpine:blog limit="6" select="title|url|image" glide="image@thumb?width:400" }} }"> <template x-for="post in posts"> <a :href="post.url"> <img :src="post.image.thumb" :alt="post.title"> <span x-text="post.title"></span> </a> </template> </div>
The alpine tag turns entries, terms, assets or any value of the context into JSON and lets you
decide what goes in: only the fields you need, related entries resolved, image variants
generated by Glide, rich text as HTML, even fields rendered with your own templates. The output
is escaped for an HTML attribute, so it goes straight into x-data.
Despite its name, nothing about it is specific to Alpine: the output is plain JSON for any
JavaScript, e.g. Vue props or a data- attribute read by your own script (see
Other frameworks).
Why
Statamic offers several ways to hand data to JavaScript, each with a catch:
| Way | Catch |
|---|---|
{{ value | to_json }} |
outputs everything, including fields that should not reach the browser; relations and images stay raw |
| REST API or GraphQL | an extra request, an endpoint to configure and protect, and the data arrives after the page |
| JSON written by hand in Antlers | quotes, escaping and nesting by hand, in every component |
The alpine tag keeps the data in the page and gives you control over its shape.
Requirements
- Statamic 5 or 6
- PHP 8.2+
Installation
composer require cordbolte/statamic-alpine
There is nothing to configure.
Tags
| Tag | Data |
|---|---|
{{ alpine :src="variable" }} |
any value of the context: an entry, a list of entries, an array, a grid or replicator field, an asset … (alpine:data is the same) |
{{ alpine:collection from="blog" }} |
entries of one or more collections, with every parameter of Statamic's collection tag: from, sort, limit, filter, conditions … |
{{ alpine:blog }} |
shortcut for alpine:collection from="blog" |
{{ alpine:taxonomy from="tags" }} |
terms of one or more taxonomies, in the current site (site picks another one), with every parameter of Statamic's taxonomy tag |
{{ alpine:blog sort="date:desc" limit="6" title:contains="climate" select="title|url|date" }}
{{ alpine:taxonomy from="topics" select="title|slug|url" }}
{{ alpine :src="team" select="name|role|portrait" glide="*:portrait@small?square:200" }}
All tags take the parameters below.
Parameters
| Parameter | Purpose | Example |
|---|---|---|
select |
only these fields | select="title|url" |
ignore |
leave out these fields | ignore="internal_notes" |
expose |
add fields that are not output by default | expose="url|author" |
join |
resolve relations (entries, terms, users) into objects | join="author|topics" |
link |
output relations as ID and API URL | link="related" |
glide |
add image variants to assets | glide="image@thumb?width:400" |
bard |
Bard fields with sets as HTML | bard="*:content" |
parse |
render fields with a template | parse="*:content@partials/rich_text" |
key |
an object keyed by a field, instead of a list | key="slug" |
absolute |
absolute Glide URLs (true/false; default: as Glide is configured) |
absolute="true" |
Several values are separated by |.
What the output contains
Entries, terms and assets become objects with:
- their stored fields. Fields defined in the blueprint are augmented: a text field is its text, a date a timestamp, an asset an object, a grid or replicator a list of objects. Values without a field in the blueprint are output as stored.
id,slugand, in dated collections,date,- not the fields
author,updated_by,updated_at,blueprintandsuper.
Values Statamic computes, like url, permalink, api_url or last_modified, are only output
when you ask for them with select or expose.
Relations (entries, terms and users fields) are output as IDs, unless you join them.
Assets become objects with id, url and the fields of the asset (e.g. alt), plus any
Glide variants.
Dates become Unix timestamps in seconds: new Date(post.date * 1000) in JavaScript.
Your own arrays (passed with src) are output as they are, nested values included. Only
select and ignore apply to them, see below.
{{ alpine:blog limit="1" expose="url" }}
[{
"title": "Hello",
"content": "<p>Welcome …</p>",
"image": { "id": "assets::photo.jpg", "url": "/assets/photo.jpg", "alt": "A photo" },
"topics": ["climate", "energy"],
"id": "1f7d…",
"slug": "hello",
"date": 1730073600,
"url": "/blog/hello"
}]
Field paths
join, link, glide, bard and parse address fields by path:
| Path | Matches |
|---|---|
image |
the field image of the entries (or of the top level of src) |
author:portrait |
the field portrait within the field author |
*:image |
every field named image, at any depth |
* |
glide only: every asset |
In a list passed with src, the position is part of the path (0:image, 1:image); use
*:image there.
select, ignore and expose take plain field names and apply at every level (see below).
Selecting fields
Without select, the output contains all fields except the ones in ignore and the default
exclusions above. expose adds a default exclusion back, or a computed value:
{{ alpine:blog ignore="internal_notes|seo" expose="author|url" }}
With select, the output contains only the selected fields, including computed values like
url. id, slug and date are only included when selected. select applies to every
level: joined entries, assets and nested objects keep only the selected fields as well, so
select their fields too. Glide variants you request are always kept.
{{ alpine:blog select="title|url|author|name|image|alt" join="author" glide="image@thumb?width:400" }}
[{
"title": "Hello",
"url": "/blog/hello",
"author": { "name": "Jane", "url": "/team/jane" },
"image": { "url": "/assets/photo.jpg", "alt": "A photo", "thumb": "/img/…?w=400" }
}]
Your own arrays: select and ignore apply to their objects (not to lists) at every level,
the default exclusions do not. Without these parameters, data you prepared yourself comes out
unchanged.
Prefer select for anything public: without it, every field of the blueprint reaches the
browser, including fields added later.
Relations
Entries, terms and users fields are output as IDs. join resolves them into objects, following
the same rules as the top level:
{{ alpine:blog join="author|topics" }}
[{
"title": "Hello",
"author": { "name": "Jane", "id": "jane", "slug": "jane" },
"topics": [{ "title": "Climate", "id": "topics::climate", "slug": "climate" }]
}]
Paths join deeper levels, e.g. join="author|author:company".
link outputs only the ID and API URL of each related item, e.g. to load it later. The API URL
is null unless the REST API is enabled for it.
{{ alpine:blog link="related" }}
[{ "title": "Hello", "related": [{ "id": "2a9c…", "api_url": "/api/collections/blog/entries/2a9c…" }] }]
Images (Glide)
glide adds image URLs generated by Glide to assets:
glide="{field}@{name}?{param}:{value},{param}:{value}"
| Part | Meaning |
|---|---|
field |
the asset field, as path; * for every asset |
name |
the key of the URL in the asset object; default glide |
param:value |
any parameter of Statamic's glide tag, e.g. width:800, square:400, fit:crop_focal, format:webp |
Several variants and fields are separated by |:
{{ alpine:team select="name|portrait" glide="portrait@small?square:200|portrait@large?width:1200,format:webp" }}
[{ "name": "Jane", "portrait": { "small": "/img/…?s=200", "large": "/img/…?w=1200&fm=webp" } }]
URLs are relative unless Glide is configured with an absolute URL; absolute="true" forces
absolute ones.
Rich text (Bard)
Bard fields without sets are already HTML. Bard fields with sets are a list of text and
sets; bard turns them into HTML, leaving out the sets:
{{ alpine:blog select="title|content" bard="*:content" }}
To include the sets, render the field with a template (parse).
Rendered HTML (parse)
parse renders fields with an Antlers or Blade template and puts the HTML into the data, e.g. a
Bard field with sets, or a card you already have a partial for:
parse="{field}@{template}"
{{ alpine:blog select="title|content" parse="*:content@partials/rich_text" }}
The template is a view name, as for Statamic's partial tag (partials/rich_text or
partials.rich_text). It receives the context of the page and the field's value under the
field's name:
{{# resources/views/partials/rich_text.antlers.html #}} <div class="prose"> {{ content }} {{ if type == 'text' }}{{ text }}{{ else }}{{ partial :src="'sets/' + type" }}{{ /if }} {{ /content }} </div>
Line breaks and repeated spaces of the result are collapsed. An alpine tag within such a
template outputs an empty list, which prevents endless nesting.
Keyed output (key)
key turns the list into an object keyed by a field of each item, e.g. for lookups in
JavaScript:
{{ alpine:blog key="slug" select="slug|title" }}
{ "hello": { "slug": "hello", "title": "Hello" }, "second": { "slug": "second", "title": "Second" } }
The key field must be part of the output: with select, select it as well.
Escaping
The tag outputs JSON with HTML entities (" …), ready for an attribute. The browser decodes
the attribute before Alpine evaluates it:
<div x-data="{ items: {{ alpine:blog select="title" }} }">
Use it in attributes only. Within a <script> element entities are not decoded; pass the data
through an attribute there as well (see below).
Other frameworks
The output is plain JSON, so any framework can use it:
{{# Vue: a prop #}} <post-list :posts="{{ alpine:blog select="title|url" }}"></post-list> {{# Plain JavaScript: a data attribute #}} <div id="map" data-places="{{ alpine:places select="title|lat|lng" }}"></div> <script> const places = JSON.parse(document.getElementById('map').dataset.places); </script>
Testing
composer install vendor/bin/phpunit
Support
Questions, bug reports and feature requests go to GitHub Issues. Support is provided there on a best-effort basis.
License
MIT, see license.