A template for single files : Possible or not?

Is it possible to use a template for files like images or pdf? Just retrieve the file list of a page and display them one by one in a template (eg for presenting images with exifs on new page without creating a subpage for each file)

Thanks!

Do you need a template for the page as such? If not, I think the easiest way to achieve that would probably be by pagination the image collection.

$image = $page->images()->paginate(1);

ha yes, good idea but I need a unique template for the file (it’s for displaying website mockups in full size, marvel/invision like)

A few options:

Put the file collection inside a subpage:

For instance:

parent/page/
  page.txt
  files/
    files.txt
    file1.pdf
    file2.jpg
    file3.svg

In the page.php template (and/or its controller), you can still retrieve the child 'files' page and its files to show at the /parent/page URL.

Then in the files.php template (and/or its controller), you can choose what to output. Maybe rely on a query string parameter, e.g. create URLs looking like /parent/page/files/?f=file2.jpg and use that parameter to render information about that file.

Variant for cleaner URLs: create a route for a pattern like (:any)/files/(:any), and in the associated function use the two parameters to retrieve the page and the file. If any of those don’t exist, you will need to return the error page (I think some examples in the cookbook show how to do that). If both exist, return the parent/page/files page and pass it the file name as a parameter (not sure about the syntax from the top of my head).

Variant without a subpage: you can have the same kind of route – (:any)/files/(:any) – but just look inside the parent page instead of a child ‘files’ page. I think when you do that you should be able to render a template and return a custom Response. Something like:

<?php
// site/config/config.php

c::set('routes', [
  ['pattern' => '(:all)/file/(:any)', 'action' => 'fileViewRoute']
]);
<?php
// could be a plugin script, for example:
// site/plugins/fileview.php

function fileViewRoute($pageUrl, $fileName) {
  $page = is_string($pageUrl) ? page($pageUrl) : null;
  $file = is_string($fileName) && $page ? $page->file($fileName) : null;
  if (!$page || !$file) {
    return page($kirby->get('option', 'error'));
  }
  $html = Tpl::load($kirby->roots->template() . '/file.php', [
    'page' => $page,
    'file' => $file
  ]);
  return new Response($html);
}

Note: I haven’t run this code, I’m just writing from memory and checking a few things in the docs.

I was just going to suggest the route alternative. Note that you probably have to pass some more variables you use in the head, e.g. the site object, when calling templates from a route.

I thought of a parameter but it seems a little bit overkill, I was hoping a “single.php” option (like wordpress). If I have to use the url, maybe is more simple to use the kirby parameters like this:

clientname/mockup:my_filename.jpg

And in mockup.php template use <img <?php echo param('filename') ?> /> where the image is needed?

Don’t know if this approach is good or not.

You can use a URL parameter (Kirby-style or standard query string), there’s no need for a custom route.

A custom route buys you two advantages at the cost of a bit of PHP code:

  1. Cleaner URLs.
  2. Ability to show the error page if the requested file doesn’t exist.

Another constraint: those page must be password protected. Don’t know if it change anything.

The route technique seems good to me but if I understand the concept globally, I think it’s way above my coding level and I have hard time to really understand how routes work. The only things I’ve done with root is a little route copy/pasted from the doc and fortunatelly it worked.

I’ll try to understand the route solution but maybe it’s a little bit… perfectionnist for me :wink:

Don’t worry: The community is here to help!

OK, sooo… I’m already stuck. I don’t know how to link the route action and the plugin file. is it supposed to work atomatically? For the moment if I click on the /clients/client-name/filename.jpg it goes on /content/clients/client-name/filename.jpg.

I don’t know why the content folder is exposed, I have constructed my url “manually” with <?php echo $page->url().'/'.$image->filename() ?> (and not $image->url()) maybe a route conflict?

The 'pattern' => '(:all)/clients/(:any)', doesn’t seem to work, I also tried 'pattern' => '(:all)/clients/(:any)/(:any)', without results. I don’t know how to test if the route works or not.

This is due to an internal Kirby route. I think you have to modify the URL a bit and use something like:

<?php echo $page->url() . '/imageviewer/' . $image->filename() ?>

Ok thank you, there is no more conflict, but I have an error page. I’m really not sure how to make it work, how to link the plugin file and the route action, neither what the plugin file is doing :frowning:

The plugin file I suggested is just a script defining a function.

A route is two things: a URL pattern, and a function. When Kirby detects that the requested URL matches the pattern, it will call the function, passing it the value from any placeholders in the pattern – stuff like (:any) in the pattern.

You don’t have to put it in a plugin file, it’s just a convenient place to store a function (because Kirby will load PHP scripts from site/plugins/*). You could put your function in index.php at the root of the site, for what it’s worth.

The “standard” way to define a routes function in Kirby is to put it directly in your config file:

<?php
// site/config/config.php

c::set('routes', /* array with one or several routes */ array(
  // And each route is itself an array with 'pattern' and 'action' keys
  array(
    'pattern' => 'hello/(:any)',
    'action' => function($urlFragment) {
      $content = 'Hello ' . $urlFragment;
      return new Response($content);
    }
  )
));

You can try the code above, and if you navigate to /hello/ or /hello/whatever you should see a simple message.

A few notes:

  • What pattern you use is up to you. I recommend using a part that is a little bit specific, like the /imageviewer/ part that texnixe suggested. So instead of trying to match on /clients/ (which describes your content), you should have a pattern like '(:all)/imageviewer/(:any)'

  • In your action function, you can name the parameters however you want. I’ve used different names in my exemples, but you can name them $a and $b if you want. That’s common to most programming languages (PHP, JS, etc.).

  • Your action function should return either a Kirby Page object (e.g. page('error') if your error page lives at content/error) or a Response object. You can make a HTML response easily with new Response('The HTML code').

  • It’s possible that the example I provided (and did not test ;)) does not work because Kirby loads the config scripts first, then the plugin scripts (then the relevant controller and template and voilĂ ). I’m not even sure it accepts a function name for the ‘action’ parameter. :stuck_out_tongue: So maybe stick with keeping the function inline in the config script (like in my latest example and in the Kirby docs).

Now, your main issue: you’re trying to show a page for an image but you have no page for the image. That’s why you have to find a workaround.

Workaround 1: make a page:

parent/page/
  page.txt
  gallery/
    image1/
      galleryitem.txt
      image1.jpg
    image2/
      galleryitem.txt
      image2.jpg
    image3/
      galleryitem.txt
      image3.jpg

If you do that’, you’re golden:

  • You can just navigate to /parent/page/gallery/image1, it matches a real page in your content folder, there’s no custom routing to do.
  • You can write a galleryitem.php template.

Only issue is: it’s a pain to edit.

Workaround 2: you reuse the page’s template to show two very different types of content depending on a parameter.

parent/page/
  page.txt
  image1.jpg
  image2.jpg
  image3.jpg

Now you can do all the work in the page.php template, which will look like:

<?php
$image = $page->image( param('show') );

if ($image->exists()): ?>
 
  Let's show the image and the surrounding HTML content we want.

<?php else: ?>

  Let's show a full page with text, a list of small
  or medium images maybe, and each image should
  also be a link to /page/url/show:image_name.jpg

<?php endif; ?>

That means treating your template like it’s two templates in one.
Note: I used the image name in the URL but you can use an index instead, e.g. if your URL looks like /page/url/show:5, you can retrieve get the right image with $image = $page->images()->nth( param('show') );

Workaround 3: use a route, and in that route render a custom template (see above examples).
Upside: you have a bit more freedom with URLs, and you can keep your “project page” and “big image layout” templates separated.
Downside: you have to use a route, and Tpl::load() to render the “big image layout” template.

1 Like

Thanks, @fvsch, very thorough explanation :slight_smile:

yeah thanks, I will read that slowly and try to understand, seems great.
For the moment, I tried the param approach and I have to say that it’s pretty simple and straightforward, I’ll post my code later, and then try the route approach for my personnal culture.

And to answer this question:

This is possible with all methods. Just keep in mind that if you don’t want images to be accessible, you will also have to implement an assets firewall as explained in the docs.

Some followup: I finished the mockup-presentation page, with access via direct url or login/pass. I used the parameters for now but I will look forward for the route solution. If you’re curious you can see the result here : http://judbd.com/espace-client/dummy-images

I hope I don’t forget anything or leave huge breaches in my website :slight_smile:

thanks again @texnixe and @fvsch