How to get blocks<>page relationship for site-wide media?

Hello,
I am finishing an archive website. The media has been made available site-wide so that editors can upload all files in one place and tag them (location, theme etc), and then after use it in pages. To link a piece of media to a page, they create a new block and select the file.

Now as a last thing, i need to make a page where users can filter media by tags WHILE being able to see what pages this media is used in.

I looked around the var_dump for blocks, but it seems that there aren’t many points to grab there? I also didn’t see the information i need. It is actually quite challenging, because i cannot seem to get a url from the block. I also get the sense that i have to scrub both the files AND the pages separately AND THEN do some sort of comparison?

Am i way off, or is there something built into kirby to make this easier?

My prototype code is below, just a general structure of the way i’m thinking about this…

<?php foreach ($site->files() as $file) :
    foreach ($file->tags()->split() as $tagcheck) :
        if ($tagcheck === $currenttag) :

            //add $file->url() to array x;

        endif;
    endforeach;
endforeach ?>

<?php foreach ($page->children() as $card) :
    foreach ($card->media()->toBlocks() as $mediablock) :
        //add $mediablock-url() and $mediablock->parentpagetitle to array i
    endforeach;
    //return array for each page
endforeach ?>

if array x[a] === array[i]{
    return file + title
}
<div>
<img src="<?= $file->url() ?>" alt="<?= $file->title() ?>">
As seen on <?= $file->parentpagetitle() ?>
</div>

I guess with the first part, you want to filter your files by tag. Then can be achieved a lot easier.

<?php
$files = $site->files();
$tag = param('tag'); // or `get('tag')` if you use query syntax
if ($tag) {
  $files = $files->filterBy('tags', $tag, ',');
}

// Then loop through the files and get information about the file
foreach ($files as $file) {
  // whatever else you want to do here
  // get information about pages in which file is used
  $usedIn = $file->getUsageInformation('text'); // pass the fieldname of the blocks field here
  foreach ($usedIn as $p {
    // link to page with $page->url();
  }
}

With this general structure in place, we create a file method call getUsageInformation() in a plugin.

/site/plugins/file-methods/index.php:

<?php

use Kirby\Cms\App;

App::plugin('texnixe/file-methods', [
    'fileMethods' => [
        'getUsageInformation' => function (string $fieldName) {
            $blocks = new Kirby\Cms\Blocks();
			$pages  = new Kirby\Cms\Pages();
            $index  = site()->index();
            $fields = $index->pluck($fieldName);

            foreach ($fields as $field) {
                $blocksForFile = $field->toBlocks()->filter(fn ($block) => $block->type() === 'image' && $block->image()->toFile() === $this); // if you are not on PHP 8, you have to modify the code a bit
                $blocks->add($blocksForFile);
            }
 
			foreach ($blocks as $block) {
                $pages->add($block->parent());
            }
            
            return $pages;
        }
    ]
]);

The code above assumes that you always use the same field name for the blocks field in all pages.

Although, I’m wondering why you use a blocks field to link media to a page? Unless that is within other blocks, of course.

1 Like

@texnixe Thank you for the thoughtful reply. Much appreciated.

I am indeed using the same field for the blocks field in all pages (because all pages use the same blueprint), so your method is able to go through the pages but I think it also creates the problem where the function is unable to differentiate between pages where the respective block is?

I am currently getting back the titles of all pages with blocks, not the title of the pages where the block is contained.

Also in response to this

Although, I’m wondering why you use a blocks field to link media to a page? Unless that is within other blocks, of course.

I am using blocks to link media to a page, because the pages are made up of multiple media items that the editor can give a desired order to. I’m basically mostly using blocks as a page-building interface.

i think the problem is this loop. Somehow the filtering is not sending back the right scope?

foreach ($fields as $field) {
    $blocksForFile = $field->toBlocks()->filter(fn ($block) => $block->toFile()->is($this));
    $blocks->add($blocksForFile);
            }

I made a mistake and forgot the field name to convert to file, corrected above.

1 Like

Oh thanks, that seems to work. However I have images, audio files and videos to go through. How would you recommend expanding that function?

Thanks again for your help.

I tried adding individual clauses, but this one for example doesn’t work.
Sorry for so many questions, just finding hte block structure hard to navigate. I have tried almost all block methods and none work, your $block->image() for example is not even listed.

$blocksForFileaudio = $field->toBlocks()->filter(fn ($block) => $block->type() === 'audio' && $block->audio()->toFile() === $this);

Ok, I thought you had images only. I think the best option would be to pass the file type and file field in the block as arguments, like so:

 'getUsageInformation' => function (string $fieldName, string $blockType, string $fileField) {
            $blocks = new Kirby\Cms\Blocks();
			$pages  = new Kirby\Cms\Pages();
            $index  = site()->index();
            $fields = $index->pluck($fieldName);

            foreach ($fields as $field) {
                $blocksForFile = $field->toBlocks()->filter(fn ($block) => $block->type() === $blockType && $block->{$fileField}()->toFile() === $this);
                $blocks->add($blocksForFile);
            }
 
			foreach ($blocks as $block) {
                $pages->add($block->parent());
            }
            
            return $pages;
        }

The file field in a block of type image is called image.

I guess you created custom blocks for your audio and video files, so you know best what you called your files field in those blocks.

1 Like

hi @texnixe thanks again so much for your help.

I implemented the function and it is working for 2/3 block types. However, i have an issue with one of them…
There are audio, images and videos. For videos, I am using vimeo to host the files so what i do is add screenshots of the video to the site-wide files, and under the file blueprint there is a toggle to ask: video? y/n and then add the URL

In the individual pages there is a block type video, so the editor selects this and chooses out of the site-wide files the screenshot for the corresponding video (which will say video=yes and insert the corresponding vimeo embed)

The question is then, how can i account for this in the getUsageInformation function?

The video block looks like this:

<?php if ($videocover = $block->image()->toFile()) {
    $videourl = $videocover->vidurl();
}
?>

I have tried adding this to the function you wrote and it doesn’t return the title pages for the video blocks. No errors either.

 if ($blockType === 'video') {
    foreach ($fields as $field) {
      $blocksForFile = $field->toBlocks()->filter(fn ($block) => $block->type() === $blockType && $block->image()->toFile()->{$fileField}() === $this);
      $blocks->add($blocksForFile);
   }
  } else { 
    foreach ($fields as $field) {
      $blocksForFile = $field->toBlocks()->filter(fn ($block) => $block->type() === $blockType && $block->{$fileField}()->toFile() === $this);
      $blocks->add($blocksForFile);
                }
            }

Fees like we are very close…

But if you pass the file type and the fieldname, this shouldn’t be an issue at all?

That what i thought too, but i am passing this with the videos:
All have the same fieldname (its only one page blueprint)

if (($file->type() === 'image') && ($file->vidurl()->isNotEmpty())) {
        $blocktype = 'video';
        $filefield = 'vidurl';
    }

wouldnt the vidurl not work because its a property of the img and not its own filetype?

This doesn’t make sense, because the file field is obviously called differently and you are passing the url field which cannot be converted to a file.

Right that’s true, but isn’t it a problem that i am checking a url and not a file?

This is my video blueprint, i also tried $filefield = ‘image’

# Video block
name: Video
icon: video
preview: image
fields:
  # Cover image
  info:
    type: info
    headline: Video block
    text: |
      You can select or add the cover image for a video block
  image:
    label: Cover image
    type: files
    # Image upload options
    uploads:
      parent: site
      template: media-file
    # Query only cover images for select option
    options: query
    query: site.files.filterBy('template', 'media-file').filterBy('toggle', true)
  

Well, you are looping through files, so you have an image that belongs to a video. This file is referenced in a video block type and the field in the block is called image.

So in your loop, it should work if you call:

if ($file->type() === 'image' && $file->vidurl()->isNotEmpty()) {
        $usedIn = $file->getUsageInformation('text', 'video', 'image');

}
1 Like

Sadly doesn’t work and doesn’t throw any errors. What do you recommend for step debugging? I don’t know what else to do, i am mostly a javascript developer.