Creating a custom block type from scratch
Kirby's blocks field comes with quite a few default block types that are great for most purposes and can be customized to your needs. But you probably wouldn't be reading this if you wouldn't be striving for more, and this recipe will hopefully be a great first step into your bright future as a custom block type developer 😉.
Prerequisites
- A Kirby Plainkit or Starterkit for testing
- A code editor
- Basic understanding how to create plugins in Kirby
- For the single file approach, Node 14+ and kirbyup. But don't worry, we also show you how to build this block without such a build process (see link below).
- Probably a big pot of hot coffee and a cool head. We are going to dive deep.
Resources
Useful links
- Registering extensions
- Block previews
- To bundle or not to bundle: differences of creating plugins with or without a build process
What we will be building
We are going to use the example of an audio block type to show you how to create custom block types for the blocks field from scratch, complete with an inline editable preview for the Panel using a single file component with a build process.
A block type plugin basically consists of the following files:
- An
index.php
to register the plugin (required for all plugins) - A block yaml file that defines the fields available for the block type (required)
- A block snippet that renders the block on the frontend (required)
- An
index.js
file to render a Panel preview (optional) - A
.vue
single component file (optional, requires build process) - A package.json file for the bundler (optional)
After finishing this tutorial, you should have a solid understanding about block types that will enable you to start building your own, even if you will never ever need an audio block at all.
Final plugin folder structure
When we will be finished, the resulting folder structure of our plugin will look like this. When you are unsure where to put what, you can come back to this overview.
Register a new plugin
Let's start with the most important plugin file, the index.php
, where we register the blueprints for the block, two file blueprints to restrict file uploads, and the snippet to render the audio block on the frontend.
The block blueprint
In the audio block blueprint, we define the fields for the block that will later show up in the drawer in the Panel. In addition to the files field we need for the audio file itself, we add some additional settings for the audio tag like the controls and the autoplay attributes. And to make it less boring and more enlightening, our block gets a poster image, headlines and a description.
You can adapt this blueprint to your needs, particularly if you want to add track files for audio transcriptions to make your audio files more accessible.
In our blueprint, we create two tabs, one for the general stuff and a second one for the audio settings. This is just to show (off) that you can use tabs to separate your different settings.
When this block is open, the drawer will look like this:
File blueprints
Since we want to prevent users from uploading non-audio files in the source
field, we need a file blueprint, in which we can restrict file uploads via the accept
property.
We keep this simple, but of course, you can add fields to allow users to add meta data for the audio files.
And while we are at it, let's add the poster.yml
blueprint to restrict the file types allowed for the poster field as well. We also want an alt
field to fill the alt
attribute.
Snippet for the frontend
Inside the block snippet, you have access to the full $block
Block object with all kinds of properties and methods, and of course to the fields we defined for the block in the block blueprint.
In the first line, we check if we can convert the filename stored in the source field into a file object, so that the markup of the snippet is only rendered if there is an audio file.
We then check if we have a poster file object, and if that is the case, we render a figure tag with the image.
For the audio element's source tag, we need the file's URL and the mime type, which are available through the file object.
We also check whether to show the controls, and if the audio should start auto-playing on load.
In most cases, controls should be present to allow users to interact with the audio, and autoplay should be off. It's up to you whether you want to make these setting available to the user at all. If not, remove the corresponding fields from the audio block blueprint. Or add other attributes available for the audio element.
We should probably also add some basic styling:
For the sake of this tutorial, we put the styles within a style tag at the top of the audio.php
block snippet. But you can also provide a sample CSS file with your block plugin. Users of your block type can then properly include those styles in their own frontend code.
At this point, our new block is ready to be used in the Panel and can be rendered on the frontend. Time to give yourself a first pat on the shoulder.
Using the new block in the blocks field
To use the new block type, for example in a page blueprint, let's add it to our blocks field.
If you are using the Starterkit, open the file /site/blueprints/pages/note.yml
, where we already have a blocks field. It currently looks like this:
To add our custom block, we need to list all fieldsets we want to use for this block:
The order we use here will determine the order in which these blocks will appear in the block list.
If you use a Plainkit, add this blocks field definition in the /site/blueprints/pages/default.yml
blueprint or create a new blueprint.
For more information how to list fieldsets, create groups of fieldsets etc., check out the blocks field docs.
Now head over to the Panel, fill in some data, and open the page on the frontend. It will look similar to this:
However, our block in the Panel currently looks like this:
We can surely do better. In the next step, we are going to change this and create a slightly more pleasant preview with an audio tag.
Simple index.js
We can start very basic with an index.js
next to the main index.php
file:
Not really what we want to end up with, but easy, right?
Let's replace this with an audio tag and the title from our block (we leave the other fields for later to prevent being too repetitive):
We have access to the fields through content
, so we can get the title with content.title
. The content.source
files field returns an array of file objects, so we can fetch the first one with the index and the URL from the url
property.
Now listen to this! We have a simple audio tag preview with a title (provided that we selected a file before).
But hold on! If we tried to add an audio block now in the Panel, we will run into an error. Try it out. Why's that? Because we haven't made sure that we actually have a file. Let's change this with a computed method to pass to the src
attribute:
The computed source
method checks if the file exists and returns an empty object otherwise. In the template we can now use the v-if
/v-else
directive to render either the audio element or a message that no audio file was selected if the URL is missing in that object.
Theoretically, we could now go ahead and put all our methods and the complete template into this file. Additional styling could be applied by creating an index.css
file for our plugin.
But that gets a bit tedious for more complex block previews. We can make our lives a lot easier with single file components.
Feel free to stop here and relax if your blocks don't require more than that. Or head over to our To bundle or not to bundle: differences of creating plugins with or without a build process recipe to learn how to create our complete example without a build process.
index.js
with single file component
Since we want to use a single file component in this example, let's move our current index.js
file one level up into a new src
folder. Our build process will then auto-generate the main index.js
file from this file.
We also need a package.json
next to index.php
with the following content:
This file tells the kirbyup bundler which files to compile. Since our plugin setup is always the same in our documentation, it's the same file we also use in our other Panel related plugin recipes.
You don't have to install kirbyup locally or globally, since it will be fetched remotely once when running your first command. This may take a short amount of time.
The package.json
file has two script commands, dev
and build
. The dev
command runs a watcher that compiles the source files whenever we are making changes, the build
command builds our production-ready file.
Back to our index.js
, where we import the yet to create Audio.vue
component and assign it to the audio block:
From now on, all the stuff we had in the old index.js
now moves into the component file.
Audio.vue
single file component
Create an Audio.vue
file in /src/components
. Then let's start by recreating exactly what we had before:
The only differences to our previous code is that we wrapped the HTML in a template tag, and export the JavaScript within script tags.
Compiling…
To see the result of our endeavors, we compile the file. Open a terminal, cd
into the plugin folder and run the command…
to start the watch process.
If all went well, you will find a compiled index.js
in the root of the audio-block plugin folder. In the Panel, everything should look exactly the same as before.
Refining
Now for some refinements. After all, our poster is not there yet, nor have we made use of the other fields.
A placeholder and the missing pieces
As our first refinement step, we wrap the current HTML in a k-block-figure
component to get a nice placeholder when there is no audio file selected yet. The exact same core component is also used for the image and video blocks.
We also add some missing pieces, i.e. the subtitle and the description.
Note how we use the v-html
directive to render the contents of the descriptions field, which is a writer field that contains HTML. If we would try to render it without this directive, all HTML tags would be shown as plain text.
v-html
should only be used if you trust the HTML that's being entered. Our writer field already sanitizes the HTML so we are fine here.
Adding the poster and some styling
For the poster that is stored in the poster
field, we take advantage of Kirby's k-aspect-ratio
Vue component to display a square image.
We use a hard-coded ratio and set the cover
attribute to true
, but this can of course be made configurable from the blueprint.
We haven't defined the posterUrl
method yet, which does the same as the source
method for the audio.
Here is the complete code for this step with some added styling:
It starts to look like what we anticipated at the beginning:
Side quest: Getting the mime type through the API
If you look closely, you will notice that we hard-coded the mime-type of the audio file all this time, because we don't have access to the mime type of the file through content.source[0]
. In our example it doesn't really matter much because we limited the uploadable file types to .mp3
files. But once we want to allow multiple files types or want to provide multiple file formats for the same audio file, we better find out how to get at the file object (who knows, maybe you need exactly that piece of information in one of your next custom blocks).
Long story short, Kirby's API to the rescue. In this case, we need access to the endpoint api/pages/:id/files/:filename
.
To this purpose, let's fetch more information about the file in a watch method.
Watch methods are a fantastic concept in Vue.js to react on changes in your component. In this case, we watch for any changes to the link key of our source object. Isn't it fantastic that we can even watch nested keys with the dot syntax?
Whenever the source file changes, the handler method will run and fetch information about the file from the API with this.$api.get(link)
. With immediate: true
we can tell the watch method to be called for the first time when the component has been created.
Once we get back the file from the API, we put the mime type into our new mime property, which we've defined in the component's data method. This will make it available in our template.
New status of our code:
Congratulations. Side quest completed! 👏
Making the text fields editable
Ok, that was a lot of stuff. But we are not ready yet. As promised in the intro, we want to make the headlines and the description editable.
To do this, we will replace all text instances with k-writer
components:
So instead of the h1
element
We use
The writer component is a simple WYSIWYG editor for inline styles (bold, italic, etc.) With an enabled inline
option, we will tell the editor to insert <br>
instead of paragraphs for line breaks. With the marks
option, we can activate or deactivate particular inline styles. We don't need them here. We could also use a simple input field instead, but the writer will adapt its size to the content and will also work better for the description field.
Another speciality of a block preview component is the built-in field
method. We can use it to access blueprint settings for fields in our drawer. This is super handy to get field properties for our WYSIWYG preview. In this case, we get the placeholders from the blueprint: field('title').placeholder
If we repeat this procedure for every field, we end up with our final result:
Note that we ignored the audio settings like controls and autoplay for the preview because they are not relevant here. If you want to change that, you should know enough by now to adapt all settings to your liking.
You also might have noticed this line:
The <k-block-figure>
component has a built-in handler to open the block drawer on double-click. This is cool, but it's not ideal for writer components. I.e. you might want to double-click to select some text. With @dblclick.stop
we can prevent the double-click event from bubbling up. The drawer will not open when we double-click on one of the writers.
You should definitely check out the docs for Vue’s awesome event handling shortcuts for more tricks like this.
End result
As a last step, we run the final build step with
This will create the final index.js
and index.css
files that are now ready to be shipped. Your plugin is finished!
In its different states the block now looks like this: