How to save a custom draft from the panel (using the API)

Greetings,

I’m working on a custom plugin to save an already-published page as a draft (while keeping it published but not changing the published page). My motivation is a “drafts for published content” implementation. I’m using Kirby as a headless CMS and have created preview buttons for content editors to preview their changes before saving them. When clicking one of these buttons, the idea is to create a draft entry of the page (even though it’s technically published) and build a preview pulling draft content from the API (and then open that page in a new tab). I’m currently stuck on the “create a draft entry of the page” step, but close.

Here’s where I’m currently at:

Kirby::plugin('namespace/myPlugin', [
   'api' => [
      'routes' => function ($kirby) {
         return [
            [
               'pattern' => 'save-custom-draft',
               'method' => 'POST',
               'action'  => function () use ($kirby) {
                  $bodyRaw = file_get_contents('php://input');
                  $bodyJson = json_decode($bodyRaw);

                  // Create the temporary draft instance.
                  $tempDraft = new Page([
                     "isDraft" => true,
                     "slug" => $bodyJson->pageId
                  ]);
                  
                  // Write the temporary draft instance to disk
                  $tempDraft->writeContent([
                     'data' => json_encode($bodyJson->fileContent)
                  ]);
               }
            ]
         ];
      }
   ]
])

That works, except that the format of the file created does not follow the format of the published file. It’s missing the Title, and all the data that’s stored is inside of a data property. I think I need to pass a template option when creating the page, but so far haven’t been able to get that to work.

Grateful for any feedback!

You can find the props you can pass to the new Page() constructor here: new Page() | Kirby CMS

The page content lives inside the content array.

 $tempDraft = new Page([
  "isDraft" => true,
  "slug" => $bodyJson->pageId,
  'template' => 'mytemplate',
  'content' => [
    'title' => 'Some title',
  ],
    
]);

Thanks @texnixe.

That was helpful for getting the structure of the created file correct, but now I’m having an issue with the actual content. I’m passing the content from JavaScript (in the request) after getting it via this.$root.$store.getters['content/values'](). That works, but it’s not formatting my blocks in the same way that the published page’s blocks are getting formatted. How to go about duplicating the structure created for the production file for the draft I’m trying to manually create? Should I get the page content differently?

I thought that the issue could be that I’m not passing a blueprint option when creating the page. However, after testing this out it seems that that’s not the problem.

Best I can tell, the issue is with the actual content I’m passing in the request (as it’s not transformed in any way).

Here’s the relevant portion of code on the JS side (which calls the PHP endpoint):

clickedViewPreview(userType) {
   // TODO: I think the value here needs to be gotten in a different way that makes it conform to the block 
   // content structure that's being saved to the published version of the file. 
   var contentValues = this.$root.$store.getters['content/values']()

   // Save the current in-memory state of the page as a draft version
   return fetch(`/api/save-custom-draft`, {
      method: 'POST',
      credentials: 'same-origin',
      cache: 'no-store',
      headers: {
         'x-requested-with': 'xmlhttprequest',
         'content-type': 'application/json',
         'x-csrf': window.panel.csrf,
      },
      body: JSON.stringify({
         pageId: 'home',
         pageTitle: 'Home',
         fileContent: contentValues.content,
      }),
   }).then(() => {
      // ...
   })
}

What does the decoded data in the route look like and how do you pass it to the content array in the route. In what way is your current result different from the original page content?

For the second part, the difference seems to be that the current result is the raw JSON (as stored in localstorage by Kirby, with the changes and originals objects merged together) and the original/production page content is the “kirbyified” JSON. The page itself is a big collection of blocks.

I think I need to pass it through the appropriate Kirby function to “kirbyify” it, but I don’t know which function to use and how exactly to go about doing that.

Trying to understand how Kirby does this internally, I found this one, which seems like it’s the one to use:

My attempt at trying to use it is like this:

use Kirby\Api\Api;
$bodyRaw = file_get_contents('php://input');
$kirbyifiedContent = Api::resolve(json_decode($bodyRaw)); // where `$bodyRaw` is the block `content` array

But that throws the exception:

Attempt to read property “fileContent” on string

So it seems I need to somehow convert the raw JSON data into a Kirby object. Any ideas on how to do this?

Another difference in the data between the two (my custom draft and the published) is with booleans:

// Custom draft
"auto_slide": true

// Published
"auto_slide": "true"

@isaac You should probably check out and play with json decoding flags

Thanks

Thanks @adamkiss, but I don’t think that’s the issue (though I realize the boolean example I gave suggested that it could have been).

Why? There are a number of other differences in the output beyond type conversion. Especially in regards to extended fields. Here’s an example (note the differences in the furthest-down “image” node):

// Custom draft version (problem here!)

Content: [
   {
      "columns": [
         {
            "blocks": [
               {
                  "content": {
                     "slides": [
                        {
                           "image": [
                              {
                                 "filename": "my-image.jpg",
                                 "dragText": "(image: images/my-image.jpg)"
                              }
                           ]
                        }
                     ]
                  }
               }
            ]
         }
      ]
   }
]

// Published version (this is what the above should look like)

Content: [
   {
      "columns": [
         {
            "blocks": [
               {
                  "content": {
                     "slides": [
                        {
                           "image": [
                              "my-image.jpg"
                           ]
                        }
                     ]
                  }
               }
            ]
         }
      ]
   }
]

There are lots of other differences, but this is a clear example of what’s going on. This is what makes me think I need to somehow pass the content through a Kirby method (so that the data is resolved in the same way it is in the published version).

Another thing I’ve noticed is that the request payloads for both (saving edits and running my custom “create draft” function) are the same (minus actual content differences). This confirms that the Kirby is processing the content somehow before saving it to disk. The task now is figuring how to replicate this (which is proving quite tough).

(accidentally deleted the message originally sharing this… apparently there’s no “undo delete” feature in Discord.)

Alright, this isn’t a solution to the problem discussed so far, but it’s another approach that’s working well enough for now (and uses simpler code).

I learned that through the page methods created the same JSON output as the published version, which of course makes sense since this is what Kirby uses internally. Here’s the solution I’m using for now:

// Creates a preview version of the file with the in-memory data (i.e. the data that's yet to be saved) for use 
// in our Lambda function that builds a Netlify preview. 

// Create the name, using our naming structure of `internal-preview-[slug]` 
$tempName = 'internal-preview-' . $this->requestBody('slug'); 

// Duplicate the published file (this creates a Kirby draft by default) 
$tempCopy = $kirby->page($tempName) ? $this->page($tempName) : $this->page($this->requestBody('slug'))->duplicate($tempName); 

// Update the duplicated file with our in-memory data 
$tempCopy->update($this->requestBody('data'), $this->language(), false); return new Response(null, null, 204); 

The one downside/note about this approach is that I couldn’t come up with a workable way to name the file the same as the published version. After thinking this through, however, I realized that it actually seems desirable to use a different name anyways (to explicitly indicate that it’s for preview purposes, rather than an actual draft file that would be indistinguishable from Kirby’s draft files).

However, since settling on this approach this code is now returning a 403 (permission denied) error. No idea what, if anything, changed.

Update: Figured out the reason for the permission issue (Permission problems trying to duplicate a page via custom API endpoint - #4 by isaac)