Create custom blocks programmatically

I also have the problem that blocks do not return image paths, which I need in a headless setup.
I read this post: Update blocks field programmatically if we use Layout? - #5 by bastianallgeier
which does come pretty close to what I need.

My plan currently is to create a custom block for each image block I find. On the update hook I loop through all blocks and if I find an image block I want to create a custom image block, which includes the full URL.
(This because I did not succeed in changing an existing block).

Unfortunately I don’t succeed either in creating a custom block in the factory.
I followed this cookbook recipe: Creating a custom block type from scratch | Kirby CMS
so I have that wanted custom block type which also can be added normally.
But I can’t use the factory to create a custom block.

Thanks for any help.
cheers

Just to understand this correctly: you have successfully created a custom block that works as expected and can be used when manually creating blocks in the Panel, right?

Now you want to update your existing blocks programmatically and exchange all image block fields with this newly created block?

And you want to do this in a layout field, not in a blocks field?

Yes the block is there and works.
I want to either update the existing image block, which I was incapable.
The only thing I managed was adding blocks, like so:


Kirby::plugin('seriamlo/mh-image-block', [
  'blueprints' => [
    'blocks/mh-image' => __DIR__ . '/blueprints/blocks/mh-image.yml',
    'files/mh-image'  => __DIR__ . '/blueprints/files/mh-block-image.yml',
  ],
  'hooks' => [
    'page.update:after' => function ($newPage, $oldPage) {
      $blocks = $newPage->teaserText()->toBlocks();
      
      $mh = new Kirby\Cms\Block([
        'content' => [
          'text' => '<p>Hug them if you like. They might not appreciate it though.</p><p>Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vestibulum id ligula porta felis euismod semper. Donec sed odio dui. Etiam porta sem malesuada magna mollis euismod.</p>',
        ],
        'type' => 'text',
      ]);

      $blocks = $blocks->add(new Kirby\Cms\Blocks([$mh]));

      $kirby = kirby();
      $kirby->impersonate('kirby');

      $newPage->update([
        'teaserText' => $blocks,
      ]);
    }
  ]
]);

This works fine.
Now I thought, when I am not able to update a block, and I can add blocks, I could add a custom block to include more info.

but when adding a custom block I fail.

      $mh = new Kirby\Cms\Block([
        "content" => [
          "image" => [
            "screenshot-2021-04-14-at-20.41.20.png"
          ],
          "alt" => "alttext",
          "caption" => "acse",
          "link" => ""
        ],
        "type" => "mh-image"
      ]);

This would not create that block.

I then tried to use the factory which bastian proposed:

     $blocks = Kirby\Cms\Blocks::factory([
        [
          'content' => [
            'text' => 'oo Nice heading'
          ],
          'type' => 'heading'
        ],
        [
          "content" => [
            "image" => [
              "screenshot-2021-04-14-at-20.41.20.png"
            ],
            "alt" => "alttext",
            "caption" => "acse",
            "link" => ""
          ],
          "type" => "mh-image"
        ],
        [
          'content' => [
            'text' => 'oo This is some text'
          ],
          'type' => 'text'
        ],
      ]);

Again the heading and the text are created but not my mh-image block…

Your question about layout fields and block fields I cannot answer.
I don’t fully understand what layout fields are. do you mean this: Layout | Kirby CMS
So far I have not used them and read about them the first time now.
So I guess I want to do it in a blocks field:

title: Blog Entry

columns:
  left:
    width: 1/2
    sections:
      content:
        type: fields
        fields:
          date:
            label: Date and start time of show
            type: date
            time: true
          teasertext: // <------------------
            label: Teaser Text
            type: blocks
            fieldsets:
              - heading
              - text
              - list
              - image
              - mh-image

Thanks in advance. I struggled for a whole day now and can’t figure it out ;S.
Cheers

I just tried it with one of my custom blocks and that worked without issues. I wonder if the issue is related to your field name with the dash, which might not work.

I think I removed every dash now, but it still does not work with the factory…

'blueprints' => [
    'blocks/mhimage' => __DIR__ . '/blueprints/blocks/mhimage.yml',
    'files/mhimage'  => __DIR__ . '/blueprints/files/mhblockimage.yml',
  ],
teasertext:
            label: Teaser Text
            type: blocks
            fieldsets:
              - heading
              - text
              - list
              - image
              - mhimage

So again I have a working custom block type, which I can add without problems.
When using the factory like:

$blocks = Kirby\Cms\Blocks::factory([
        [
          'content' => [
            'text' => 'oo Nice heading'
          ],
          'type' => 'heading'
        ],
        [
          "content" => [
            "image" => [
              "screenshot-2021-04-14-at-20.41.20.png"
            ],
            "alt" => "alttext",
            "caption" => "acse",
            "link" => ""
          ],
          "type" => "mhimage"
        ],
        [
          'content' => [
            'text' => 'oo This is some text'
          ],
          'type' => 'text'
        ],
      ]);

It would create the heading and the text but the mhimage in between is not created…

And now I just tried the other method:

      $mh = new Kirby\Cms\Block([
        "content" => [
          "image" => [
            "screenshot-2021-04-14-at-20.41.20.png"
          ],
          "alt" => "alttext",
          "caption" => "acse",
          "link" => ""
        ],
        "type" => "mhimage"
      ]);


      $blocks = $blocks->add(new Kirby\Cms\Blocks([$mh]));

Which did not work either…


Edit: I just thought that maybe upgrading from 3.5.1 to 3.5.4 helps, but no change either…

I just built a new audio custom block type:

<?php

Kirby::plugin('cookbook/superaudio', [
  'blueprints' => [
    'blocks/audio' => __DIR__ . '/blueprints/blocks/audio.yml',
  ],
  'hooks' => [
    'page.update:after' => function ($newPage, $oldPage) {
      $blocks = $newPage->teaserText()->toBlocks();

      $mh = new Kirby\Cms\Block([
        "content" => [
          "description" => "what a nice description",
        ],
        "type" => "audio"
      ]);


      $blocks = $blocks->add(new Kirby\Cms\Blocks([$mh]));


      $kirby = kirby();
      $kirby->impersonate('kirby');

      $newPage->update([
        'teaserText' => $blocks,
      ]);
    }
  ]
]);

the blueprint:

name: Audio
icon: file-audio
tabs:
  main:
    label: Main
    fields:
      description:
        type: writer
        icon: text
        inline: true
        placeholder: Description
        marks:
          - bold
          - italic

And within my page template I have now:

         teasertext:
            label: Teaser Text
            type: blocks
            fieldsets:
              - heading
              - text
              - list
              - audio

I can again add the audio block type, save a description etc.
But when exeuting the update hook, it would not create a new audio block…
:frowning:
What am I missing?

Thanks for the help btw!
Really appreciate it.
And sorry for the spam on discord as well. Just thought that maybe someone more knowledgeable there could help me as well…

Cheers

Hm, I don’t know. I thought it had worked, but now when I tried it again, the field is updated but with a default block type. Too tired now to have a deeper look what’s happening and why.

Hm, alright. I am glad that there seems to be a problem and that I am not completely nuts :).

For now, it would help as well, if you could tell me, how I could loop through the blocks, and if the type is $block->type()=== image I would like to change one field $block['content]['image'].
I got all the code except for changing a specifics blocks field… can this be done?

Hm, there seems to be a bug of some sort, because even when modifying such a block type slighty, the block gets ignored.

Ok, I played around a bit more, and it seems to work when I convert the blocks to array when updating the page:

$newBlocks = new Kirby\Cms\Blocks();
$blocks    = $page->text()->toBlocks();
foreach ($blocks as $block) :
  if ($block->type() !== 'image') {
    $newBlocks->add($block);
  } else {
    $url      = $block->image()->toFile()->url();
    $images   = $block->image()->toFiles()->pluck('filename');
    $newBlock = new \Kirby\Cms\Block(
      [
        "content" => [
          'location' => 'kirby',
          'image'    => $images,
          'url'      => $url,
          'alt'      => $block->alt()->value(),
          'caption'  => $block->caption()->value(),
          'link'     => $block->link()->value(),
        ],
        'id'   => $block->id(),
        'type' => $block->type()
      ],
    );
    $newBlocks->add($newBlock);
  }
endforeach;

$page->update(
  [
    'text' => $newBlocks->toArray(),
  ]
);

In the example above I added a new url field to the image block blueprint to store the url of the image.

2 Likes

Thanks a lot for looking into this and for the help.
As for a kirby noob like me this answer is not fully complete, I post my code here to maybe help someone in the future:

A prerequisite for this to work is, that you override the image.yml:

and add a url property only then it can be set.

Also the part of how to convert it to an array is missing. As I did not know there is a kirby method for that I needed to find out first:

<?php

Kirby::plugin('seriamlo/mh-image-block', [
  // we dont need any blueprints, as this plugin just adds a hook to change image blocks
  'hooks' => [
    'page.update:after' => function ($newPage, $oldPage) {
        $newBlocks = new Kirby\Cms\Blocks();
        $blocks    = $newPage->teaserText()->toBlocks();

        foreach ($blocks as $block) :
        if ($block->type() !== 'image') {
            $newBlocks->add($block);
        } else {
            $url      = $block->image()->toFile()->url();
            $images   = $block->image()->toFiles()->pluck('filename');
            $newBlock = new \Kirby\Cms\Block(
                [
              "content" => [
                'location' => 'kirby',
                'image'    => $images,
                'alt'      => $block->alt()->value(),
                'caption'  => $block->caption()->value(),
                'url'  => $url,
                'link'     => $block->link()->value(),
              ],
              'id'   => $block->id(),
              'type' => $block->type()
            ],
            );
            $newBlocks->add($newBlock);
        }
        endforeach;

      $newPage->update([
        'teaserText' => $newBlocks->toArray(),
      ]);
    }
  ]
]);

Hope this helps somebody.
And thanks a lot for helping out on sundays! Appreciate it a lot!
:raised_hands:


Just a last question, because the title of this thread is Create custom blocks programmatically. I assume this will now also work with custom block types, right?

If I can I will try it out, but right now I need to proceed a bit as I have waisted some time getting here.

1 Like

Yes, that also works for custom blocks

Thanks so much for your input regarding the toArray() method. I came across the same issue, that my custom subline block would automatically be converted to type: text when updating the page. Super weird issue, a type: heading did work fine. Using the toArray() method in the update method fixes my issue, too. Maybe you could add it as a hint in your cookbook article? (Quicktip: Add blocks to a blocks field programmatically | Kirby CMS)