The Directus2 Plugin is an extension for Grav CMS for using grav as frontend for a directus headless CMS.
With this plugin we import information from directus collections into flex-object collections. It is meant to run grav without the admin interface and fetch data only via webhooks, plus having some static content in your page tree (depending on your usage).
Requires PHP 8.0 or newer. Tested with directus 9.x
We use grav as the front end for a directus headless CMS in several projects. There is a collection in which the pages are managed. Various data and metadata can be maintained there, as well as a list of content elements. These are then displayed one after the other on the respective page in a similar way to grav Modulars. Multilingualism is also relevant for one of the projects.
We use the term "curated page" for these pages. This stems from the fact that in our first projects we tried to automate the creation or presentation of pages (e.g. by recognising/interpreting the content provided). However, we were asked too often for manual intervention, which is why we then switched to this type of page composition.
As a rule, a matching page must be created in the page tree (user/pages/) for each page, as it is easiest to get the matching entry from the collection via the slug of the respective page.
Why so much effort? directus offers many advantages for some of our customers that go far beyond the storage of data for a website. In addition, the website can benefit from gaining access to more extensive information from the backend.
We only describe the relevant fields and characteristics of the basic collections here.
Note
To reduce the PHP memory usage make sure to have a date_updated
field in every collection. This way the sync will run also quicker.
In addition to the things that a page can use (hero image, meta description, headline, etc.), the most important field for us is 'slug'. We use it to specify which page the content is intended for.
Just as important is the content_elements
field. This is a Many to Any field that defines the actual content of the page.
Currently (2003/2024) it is recommended to provide a separate collection for each content type. For example, we use elements for image & text, enumerations with icons, contact persons, accordions, sliders, galleries, downloads and more.
This structure has the advantage that only the necessary fields are available in the collection for each type, instead of a collection that provides countless fields because it has to map every conceivable function.
The editors can then select elements from these areas or create new ones and define the sequence.
For content such as news (blog), we can also use the Content Elements Collections in the entries. However, we do not use a curated page to display an overview of the news, but use grav's Flex Object functions.
This can also be transferred to other data types such as job vacancies, products, team members or contact persons.
More details below.
Blueprints in the configured folder will automatically be activated, no modification of flex-objects config neccassary. Blueprints only need to contain the bare minimum of information to make grav happy. Additionally they contain parameters for the API requests and filtering/conditions for directus.
Example: user/blueprints/flex-objects/directus/sjm_jobs.yaml
title: Jobs
description: Jobs
type: flex-objects
config:
directus:
depth: 3
filter:
location:
value: 'Dreseden'
operator: _eq
sjm_group_members:
mm_field: sjm_group_members_id
value: 1
operator: _in
data:
object: 'Grav\Plugin\Directus2\Flex\Types\Directus2\Directus2Object'
collection: 'Grav\Plugin\Directus2\Flex\Types\Directus2\Directus2Collection'
index: 'Grav\Common\Flex\Types\Generic\GenericIndex'
storage:
class: 'Grav\Framework\Flex\Storage\FileStorage'
options:
folder: user-data://directus/sjm_jobs
config.directus
is where we store the information about what we want to get from directus when we do a fetch (getting all content from directus). In the example we demand a depth
of 3 levels so we might get a good amout of recursive data, which can be important for information about referenced files for example.
Warning
Keep the depth as low as possible because with all the recursive data the directus API provides, it will sadly fill up your PHP memory while syncing.
In the filter
we can setup conditions on which data to include. In the example we only want entries with a certain location (filter.location
). The operators can be found in the directus docs.
In the example you can see the filter for the content of the relational field sjm_group_members
. This field is a many to many field, for which we have the mm_field
option. You will need to note the field name from the contingency collection. If it's set up as a n:1 connection, you can use an _eq 1
comparsion without the mm_field
.
The config.data
part is the regular Flex Object stuff. We decided to default the stroage folder to user/data/directus
(config.data.storage.folder
) in order to distinguish from other Flex Objects.
We plan to add a CLI command to quickly create basic blueprints.
Also we might add custom classes for Flex Objects and Flex Collections, which may then alter theconfig.data.object
andconfig.data.collection
notation
In the themes templates/partials/base.html.twig
one of the first lines is this:
{% set pageInfo = grav.get('flex').collection( 'curated_pages' ).filterBy( { 'slug': page.rawRoute } ).first %}
It querys the Flex Object matching the current pages's slug. This way we always have the information relating the page on hand. Depending on the thing you cover in your collection you can access metadata, page title, and so on.
If we want to output the related content elements, we use a template (templates/curated.html.twig
) which's important part is this:
{% for row in pageInfo.content_elements %}
{% set module = grav.get('flex').object( row.item.id, 'row.collection' )%}
{% set module = directusTranslate( module.jsonSerialize(), currentLang ) %}
{% include 'partials/directus/' ~ row.collection ~ '.html.twig' with { module: module } %}
{% endfor %}
Line by line:
- Loop through the entries
- Query the corresponding Flex Object by the item's ID from their collection (don't forget to add all blueprints needed!)
- Translate the Item (optional).
directusTranslate
will overwrite the item's contents with information available in thetranslations
object inside the item. - Include the template and pass the item to it
You can extend this to your preferences. For example you might need some layout specific settings in you collection. How you handle this depends on the impact these need to have. For example some collections might have compact or extended options, you could use the collection name as folder name and have a default.html.twig plus optional layouts:
{% set layout = module.layout|default( 'text' ) %}
{% include 'partials/directus/' ~ row.collection ~ '/' ~ '.html.twig' with { module: module } %}
Since we store the data from directus just like normal Flex Objects, we can query them in Twig like any other collection.
{% set services = grav.get('flex').collection( 'services' ).filterBy( { 'status': 'published' } ) %}
{% for service in services %}
{% set service = localize( service.jsonSerialize(), currentLang ) %}
{% set data = {
icon: service.icon,
title: service.name,
text: service.short_description.
} %}
{% include 'partials/service-overview.html.twig' with { item: data } %}
{% endfor %}
The Enpoints will be populated under the endpointName
from the config. For example: example.com/your-prefix/sync.
Endpoint | Function |
---|---|
create | Add a new item. Requires Payload, needs to be called via directus webhooks. |
update | Update one ore more items. Requires Payload, needs to be called via directus webhooks. |
delete | Delete one or more items. Requires Payload, needs to be called via directus webhooks. |
sync | Clear the current Flex Objects (managed by this plugin) and get all the content fresh from the directus server. Automatically creates a backup of the fresh data. |
restore | If we encounter a server error, the backuped content might not be restored automatically, trigger it with this enpoint. |
assets-reset | Remove all stored assets in case of name mismatch or other issues. |
assets-remove?id=uuid | Remove a specific stored asset by id. |
Take note: create and update will request the the respective entry in return. This is necassary since the payload can be elaborate to process.
In the old directus plugin we used to have an action that created folders per entry of specific collections like blog entries (relict of pre Flex Objects times). This can be done with dynamic page creation now.
TODO: Expample Code
The Twig function directus_file()
will download the requested file, saves the file in the accets folder (assets
in plugin configuration) and outputs the URL to the file inside grav's file structure.
Make sure to use directusFileInfo()
to get the array with file information for directus_file()
to work with. Background; If your deph is too low (may also happen when using translations), only the file's UUID is provided. If the input is already an array directusFileInfo()
returns it right away.
In the following example we request the first image from the field sjm_images in an element. We provide the function with the whole image object (includes id, filename_disk, filename_download, etc.).
{% set mediaObj = directusFileInfo( post.sjm_images[0].directus_files_id ) %}
<img class="card__thumbnail"
src="{{ directusFile( mediaObj, { width: '200', height: '300', quality: 70 } ) }}"
width="200"
height="300"
loading="lazy"
decoding="async"
alt="{{ post.sjm_images[0].directus_files_id.description }}" />
The file is going to be saved as user/data/assets/imagefilename-592d40567ccab4aef750b7a1a3f555a8.png
. The second part of the filename is a hash of the options (like size and quality).
For files like PDFs you just omit the options in the function call.
<a class="download__link" href="{{ directusFile( post.manual_file.directus_files_id ) }}">
Download Instructions
</a>
To work with translations you set up your grav as usual. In directus, you setup translations for you collections, which will provide a translations
object in every API response for these collections.
The Twig function directusTranslate
will provide you a copy of the original entry but overwrites all fields available in the translation.
{% set translated = directusTranslate( post.jsonSerialize(), 'en' ) %}
{{ translated.sjm_description|markdown|raw }}
The language string 'en'
from the example should be replaced with a variable holding the current language. It depends on the way you handle this in your theme.
The plugin fires events so other plugins can extend functionality depending on the need of the project. For example, we make use of this in order to create an index of multilangual post for cathing the necessary slugs.
- onDirectusSyncSuccess
- onDirectusCreateSuccess
- onDirectusUpdateSuccess
- onDirectusRestore (after restoring)
- onDirectusAssetReset (after clearing)
Before configuring this plugin, you should copy the user/plugins/directus2/directus2.yaml
to user/config/plugins/directus2.yaml
and only edit that copy.
Here is the default configuration and an explanation of available options:
enabled: true
disableCors: true
endpointName: d2action
blueprints: user/blueprints/flex-objects/directus
storage: user/data/directus
assets: user/data/assets
logging: false
lockfileLifetime: 120
modDateField: 'date_updated'
directus:
token: 1234567
email: [email protected]
password: supersavepassword
directusAPIUrl: http://your.api.com
statusFilter:
'_in': [ 'published' ]
Configuration Key | Meaning/Notes |
---|---|
disableCors | CORS can be an issue of connection problems. For time your DevOps figure this out, you can disable it. |
endpointName | Defines the slug where your API endpoints are located. For example http://example.com/d2action/sync |
blueprints | Location where the Flex Object Blueprints related to directus are. |
storage | Location where the Flex Object data is stored. Needs to match the data.storage.options.folder setting in your blueprints and will be used to create new blueprints via CLI. Do not use the same folder as redular flex objects, since this folder will be emptied in the process of a complete sync. |
assets | Location where requested files are stored. |
logging | Creates extensive log files. You should only use this in development or for debugging. |
lockfileLifetime | Lifetime of the Lock File in seconds |
modDateField | Name of the field with the date of last modification in your collections (mandatory to have to work with this plugin) |
directus.token | API Access Token. If set email and passwort are unnecessary |
directus.email | Email (username) to access the API |
directus.password | password to access the API |
directus.directusAPIUrl | URL of your directus server. |
statusFilter | Define which status types need to be synced (see env based config) |
You might need to have some kind of preview system, where editors can see their changes before publishing. Depending on your infrastructure and workflows the following might be helpful.
The example below can be added to your enviroment config like user/env/preview.example.com/config/plugins/directus2.yaml
.
It assumes that you have a custom status 'preview'. This is not displayed in the live system because our default configuration only filters for 'published'. The env configuration changes the spectrum of allowed values.
But make shure all your collections should have a status
field. If it's not important for a collection hide it and set published
as default.
statusFilter:
'_in': [ 'published', 'preview' ]
Using this way, we don't clutter our templates with status checks.
To install the plugin automaticall with bin/grav install
, add the following to the git section of your user/.dependecies
file:
git:
directus2:
url: https://github.com/mindbox/grav-plugin-directus2
path: user/plugins/directus2
branch: main
To install the plugin manually, download the zip-version of this repository and unzip it under /your/site/grav/user/plugins
. Then rename the folder to directus2
. You can find these files on GitHub.
You should now have all the plugin files under
/your/site/grav/user/plugins/directus2
NOTE: This plugin is a modular component for Grav which may require other plugins to operate, please see its blueprints.yaml-file on GitHub.
Big thanks to Erik Konrad, who created the original directus plugin which this on relies heavily on.
- CLI command for creating blueprints
- Custom Flex Classes for Collection or Item