Issue with non-default content extension when programmatically creating pages

I run a multilang setup, and have my content file extension set to md. For my linkblog, I run a script that parses a special pinboard feed and creates Kirby pages from that.

To do this I run a php script with a cronjob that

  • bootstraps my Kirby instance
  • fetches and parses the feed (with remote::request() and json_decode())
  • checks if an entry exists already, if not creates a new page with $page->createChild() in my linkblog

Since it runs on the server in the background anyway, the script just impersonates the kirby superuser to keep things simple.

This script has worked great, but I discovered recently that it started to ignore my content file extension setting (.md) and now creates the content file with a .txt extension instead.

Unfortunately I don’t know since which exact version of Kirby this is the case, as I have taken some time out from linkblogging. The core has been regularly updated, I have not installed any new plugins or changed much else (blueprint for the linkblog-entry page, etc)

Is this a bug or am I missing something?


Hm, I just set up a test installation with .md files, with a simple create script in the root folder like this:


require __DIR__ . '/kirby/bootstrap.php';

$kirby = new Kirby;

$uid = time();
  'slug'     => $uid,
  'template' => 'default',
  'content'  => [
      'title' => 'My super title'

It creates md files all right.

Tested with Kirby 3.2.3

Please show us your config file. May be there is the issue.

Here’s the config:

return [
    'debug' => false,
    'languages' => true,
    'fatal' => function() {
    	include kirby()->root('templates') . '/fatal.php' ;
    'content' => [
    	'extension' => 'md'
  	'markdown' => [
    	'extra' => true
  	'routes' => [
      // Feeds
            'pattern' => ['feed/index', 'feed/xml', 'feed/atom'],
            'method' => 'GET',
            'action'  => function () {
                $options = [
                    'title'       => 'Master Feed',
                    'description' => 'All content',
                    'link'        => 'home',
                    'snippet'     => 'feed/atom'
                return collection("masterfeed")->limit(30)->feed($options);
    	// etc.
    'api' => [
    	'slug' => 'app'
    'cache' => [
    	'pages' => [
      		'active' => true,
      		'type'   => 'apcu'

The script creating the pages:


// bootstrap kirby
require(__DIR__ . DS . '../html' . DS . 'kirby' . DS . 'bootstrap.php');
$kirby = new Kirby;

// authenticate as super user

echo 'Loaded instance: ' . $kirby->site()->title() . "\r\n";

$pinboardlinks = function(){
		$feeddata = [];
		$pinboardurl = '…';

		echo 'Requesting json-data from ' . $pinboardurl . "\r\n";

		$request = remote::request($pinboardurl);
			if (!empty($request->content)) {
    			$feeddata = json_decode($request->content, true);
  			} else {
  				echo 'Could not get data from the pinboard feed.';
  		return array_reverse($feeddata);

foreach($pinboardlinks() as $linkentry){
	$data = [
  		'timestamp' => $linkentry['dt'],
  		'link' => $linkentry['u'],
  		'title' => $linkentry['d'],
  		'text' => $linkentry['n'],
  		'date' => date("Y-m-d H:i:s", strtotime($linkentry['dt'])),
  		'imported' => date("Y-m-d H:i:s", time()),
  		'tags' => implode(", ",$linkentry['t'])

  	if (page('links')->grandChildren()->filterBy('Link', $data['link'])->isEmpty()) {
		echo 'Creating new entry: ';
		$pageslug = Str::slug(Str::short($data['title'], 60, ''), '-', 'a-z0-9@._-');
		echo  $pageslug . "\r\n";

		try {
			$createNewLinkEntry = page('links/en')->createChild([
 				'slug'     => $pageslug,
  				'template' => 'link',
  				'draft' => true,
  				'content' => [
    				'title' => $data['title'],
    				'autoid' => '',
    				'date' => $data['date'],
    				'imported' => $data['imported'],
    				'link' => $data['link'],
    				'tags' => $data['tags'],
   					'text' => $data['text']

		} catch(Exception $e) {
			echo 'ERROR: ' . $pageslug . ': ' . $e->getMessage() . "\r\n";
	} else {
	    echo 'Entry already exists for ' . $data['link'] . "\r\n";

echo 'Link entries have been updated' . "\r\n";


Are pages created correctly as .md files using the Panel?

On a side note, using publish() and changeStatus() is a bit redundant, you can remove publish().

Could you dump the options in your script to check if it reads the options:


Edit: Come to think of it, there seems to be an issue with your cache setting. As far as I know there is no type option. I might be wrong…

IMO, it should look like this:

  'cache' => [
    'pages' => [
      'active' => true
    'driver' => 'apcu'

Thanks for the suggestions and catching the error on the cache driver in the config!— No influence on this though, in my localhost config caching is not set up and I have the same issue when launching the script in this environment.

Hmmm. Following your suggestion on dump($kirby->options()); something does not seem to be right… It dumps a lot of stuff from the $kirby object (for example the site object with its correct children, the languages, also ok, etc.) but the options are not there:

[options] => Array

To clarify, with dump($kirby->option('content')); or dump($kirby->option('debug')); I should be able to dump a specific option too no?— Both don’t return anything.

Edit: I also noticed this line in the script doesn’t actually echo the site title, which it should!
echo 'Loaded instance: ' . $kirby->site()->title() . "\r\n";

With the config you posted (and the cache driver correctly set up, the output from dump($kirby->options()); should be

    [debug] => 
    [languages] => 1
    [fatal] => Closure Object

    [content] => Array
            [extension] => md

    [markdown] => Array
            [extra] => 1

    [api] => Array
            [slug] => app

    [cache] => Array
            [pages] => Array
                    [active] => 1

            [driver] => apcu



Does the site content file have the correct extension (.md) and the language slug? If not, that’s probably the reason why it doesn’t output the title but shouldn’t cause the issue of the wrong extension.

Following your link:
is the name of your driver correct? I never used this option, but in your link they ALL include something like “cache” in the name?

@mof Seems that this type option does actually exist, at least it is also used in the config, although nowhere documented.

@texnixe Hmm, I think I got it from the Guide on caching

In any way, I don’t think my problem is related to caching, since as I said in my localhost setting I have the same problem as in my production environment; and I don’t have caching configured locally.

I started disabling plugins, after disabling AutoID, dump($kirby->options()); now doesn’t return random stuff from the $kirby object, but instead shows options which are registered in plugins (i.e. in the plugin itself). So far so good, but I get nothing that is set in the config.php, neither core options, nor plugin options. I’ll continue to investigate and report back.

I also checked and both and are there, but the script can’t read from them apparently. It looks to me that there seems to be a problem bootstrapping Kirby correctly from this script— On the website itself, everything works as expected.

Ah ok, thanks, then I have to check if the option docs are wrong…

Looks like something is wrong, do you get the config options if you dump($kirby->options()); in a template?

Yes I get them, that is what I cannot figure out… in the context of the website, everything works fine. Thanks for your help, I think I’ll check my commit history and try to figure out at which point it stopped working.

I think I found the problem. I have two domain-specific config.php files for a multi-environment-setup like so:

Now when my build script is called, locally this happens from a CLI and obviously in both cases not via the respective domain, so the domain-specific config files were ignored by kirby. Setting a regular config.php solves the problem: The script can read the options.

What I missed when setting up the multi-environment configs was this [my emphasis]:

You can set different options based on the domain by adding additional config files containing the domain

I.e. it’s a better idea to have a general config.php and just put environment-specific options (such as debug for localhost) in additional config files.

@texnixe Thanks for all your help!

I had a feeling it was something like tha but I always thought you were talking about config.php.

Edit: I updated the docs to hopefully make this a bit more obvious.