# Virtual Pages with API and Caching

**URL:** <https://forum.getkirby.com/t/virtual-pages-with-api-and-caching/28090>\
**Category:** Questions\
**Tags:** v3\
**Created:** [March 14, 2023, 3:01pm UTC](https://forum.getkirby.com/t/virtual-pages-with-api-and-caching/28090 "2023-03-14T15:01:54Z")\
**Posts on this page:** 11\
**Page:** 1

<div class="post-metadata">

**Author:** ![Reluma](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/reluma/32/12543_2.png) [@Reluma](https://forum.getkirby.com/u/Reluma)\
**Post date:** [March 14, 2023, 3:01pm UTC](https://forum.getkirby.com/t/virtual-pages-with-api-and-caching/28090/1 "2023-03-14T15:01:54Z")

</div>

Hi!

I create virtual pages following this guide [Content from an API | Kirby CMS](https://getkirby.com/docs/guide/virtual-pages/content-from-api)  
and it works very well.

Since the contents are so many, I try to save in the kirby cache the content of the json as suggested here: [Caching | Kirby CMS](https://getkirby.com/docs/guide/cache#custom-caches)

ans I set cache variable is valid for 12h.

my config.php file

```auto
return [
    'debug' => true,
    'panel.install' => true,
    'cache' => [
        'api' => true
    ]
];

```

my page models

```auto
class ReviewsPage extends Page
{
    public function children()
    {
        $corsi = [];
        $pages = [];
        $apiKey = '';
        $user = 'myUser';
        $password = 'MyPassword';

        $kirby = kirby();

        $apiCache = $kirby->cache('api');
        $apiData = $apiCache->get('apiData');

        if ($apiData === null) {
            $apiData= Remote::get('https://api.website.com/thefile.json', [
              'headers' => [
                'Authorization: Basic ' . base64_encode($user . ':' . $password)
              ]
            ]);
            $apiCache->set('apiData', $apiData->content(), 720);
        }

        if ($apiData->code() === 200) {
            $corsi = $apiData->json(false)->corsi;
        }

        foreach ($corsi as $key => $review) {
            $pages[] = [
                'slug' => Str::slug($review->titolo),
                'num' => $key+1,
                'template' => 'review',
                'model' => 'review',
                'content' => [
                    'title' => $review->titolo,
                    'headline' => $review->sottotitolo,
                    'byline' => $review->codice,
                    'summary' => $review->sedeCorso,
                    'date' => $review->datainizio,
                    'link' => $review->allegati,
                    'linkText' => $review->destinatari,
                    'cover' => $review->urlImmagine
                ]
            ];
        }

        return Pages::factory($pages, $this);
    }
}

```

obviously this model doesn’t work but I can’t figure out what I’m doing wrong, it seems the error is on the line  
if ($apiData-\>code() === 200)

can you help me to create the virtual pages every 12 hours without reloading all the data via api every time?  
I hope I made myself clear, thank you very much!

---

<div class="post-metadata">

**Author:** ![moonwalk](https://avatars.discourse-cdn.com/v4/letter/m/53a042/32.png) [@moonwalk](https://forum.getkirby.com/u/moonwalk)\
**Post date:** [March 14, 2023, 3:22pm UTC](https://forum.getkirby.com/t/virtual-pages-with-api-and-caching/28090/2 "2023-03-14T15:22:19Z")

</div>

In your cache, you store `$apiData->content()`. So when $apiData is not null, $apiData-\>code() will not exist. The check for the response code should go into your if statement.

After that, you just work with the $apiData variable.

---

<div class="post-metadata">

**Author:** ![Reluma](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/reluma/32/12543_2.png) [@Reluma](https://forum.getkirby.com/u/Reluma)\
**Post date:** [March 14, 2023, 4:28pm UTC](https://forum.getkirby.com/t/virtual-pages-with-api-and-caching/28090/3 "2023-03-14T16:28:00Z")

</div>

something like this?

The page works, but the foreach loop is empty but there should actually be 200 elements.

I check the cache/api/apiData.cache file which has been generated and inside there is all the json data.

```auto
class ReviewsPage extends Page
{
    public function children()
    {
        $corsi = [];
        $pages = [];
        $apiKey = '';
        $user = 'myUser';
        $password = 'MyPassword';

        $kirby = kirby();

        $apiCache = $kirby->cache('api');
        $apiData = $apiCache->get('apiData');

        if ($apiData === null) {

            $apiData= Remote::get('https://api.website.com/thefile.json', [
              'headers' => [
                'Authorization: Basic ' . base64_encode($user . ':' . $password)
              ]
            ]);

            

            $apiCache->set('apiData', $apiData->content(), 720);

            if ($apiCache->code() === 200) {
                $corsi = $apiCache->json(false)->corsi;
            }

        }
        

        foreach ($corsi as $key => $review) {
            $pages[] = [
                'slug' => Str::slug($review->titolo),
                'num' => $key+1,
                'template' => 'review',
                'model' => 'review',
                'content' => [
                    'title' => $review->titolo,
                    'headline' => $review->sottotitolo,
                    'byline' => $review->codice,
                    'summary' => $review->sedeCorso,
                    'date' => $review->datainizio,
                    'link' => $review->allegati,
                    'linkText' => $review->destinatari,
                    'cover' => $review->urlImmagine
                ]
            ];
        }

        return Pages::factory($pages, $this);
    }
}

```

---

<div class="post-metadata">

**Author:** ![moonwalk](https://avatars.discourse-cdn.com/v4/letter/m/53a042/32.png) [@moonwalk](https://forum.getkirby.com/u/moonwalk)\
**Post date:** [March 14, 2023, 4:44pm UTC](https://forum.getkirby.com/t/virtual-pages-with-api-and-caching/28090/4 "2023-03-14T16:44:12Z")

</div>

No, unfortunately that is a similar mistake, now $corsi is only defined when $apiData is null. And you are using a wrong variable:

```auto
public function children()
    {
        $corsi = [];
        $pages = [];
        $apiKey = '';
        $user = 'myUser';
        $password = 'MyPassword';

        $kirby = kirby();

        $apiCache = $kirby->cache('api');
        $apiData = $apiCache->get('apiData');

        if ($apiData === null) {

            $response = Remote::get('https://api.website.com/thefile.json', [
              'headers' => [
                'Authorization: Basic ' . base64_encode($user . ':' . $password)
              ]
            ]);

          
            if ($response->code() === 200) {
                  $apiData = $response->content();
                  $apiCache->set('apiData', $apiData, 720);
            }

        }
        
     
       $corsi = $apiData->json(false)->corsi;

        foreach ($corsi as $key => $review) {
            $pages[] = [
                'slug' => Str::slug($review->titolo),
                'num' => $key+1,
                'template' => 'review',
                'model' => 'review',
                'content' => [
                    'title' => $review->titolo,
                    'headline' => $review->sottotitolo,
                    'byline' => $review->codice,
                    'summary' => $review->sedeCorso,
                    'date' => $review->datainizio,
                    'link' => $review->allegati,
                    'linkText' => $review->destinatari,
                    'cover' => $review->urlImmagine
                ]
            ];
        }

        return Pages::factory($pages, $this);
    }

```

---

<div class="post-metadata">

**Author:** ![Reluma](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/reluma/32/12543_2.png) [@Reluma](https://forum.getkirby.com/u/Reluma)\
**Post date:** [March 15, 2023, 8:58am UTC](https://forum.getkirby.com/t/virtual-pages-with-api-and-caching/28090/5 "2023-03-15T08:58:59Z")

</div>

it seems to work partially, now it generates the list but it doesn’t create the virtual pages. in the backend there are no pages and if I try to open one, the line with $courses = $apiData-\>json(false)-\>courses gives me error: Call to a member function json() on string

---

<div class="post-metadata">

**Author:** ![pixelijn](https://avatars.discourse-cdn.com/v4/letter/p/7ab992/32.png) [@pixelijn](https://forum.getkirby.com/u/pixelijn)\
**Post date:** [March 15, 2023, 9:09am UTC](https://forum.getkirby.com/t/virtual-pages-with-api-and-caching/28090/6 "2023-03-15T09:09:47Z")

</div>

> [@moonwalk](#):
>
> `Remote::get('https://api.website.com/thefile.json'`

What does this actually return?

---

<div class="post-metadata">

**Author:** ![Reluma](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/reluma/32/12543_2.png) [@Reluma](https://forum.getkirby.com/u/Reluma)\
**Post date:** [March 15, 2023, 9:53am UTC](https://forum.getkirby.com/t/virtual-pages-with-api-and-caching/28090/7 "2023-03-15T09:53:00Z")

</div>

A page with a Basic Auth, with the credentials returns a json file. I tested it with Postman and it works.

The original code (content from an API), without the chaching, works very well, but evrey time I open the page it take 6 seconds to load the contents (there are a loto of courses in the json)

---

<div class="post-metadata">

**Author:** ![rasteiner](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/rasteiner/32/4788_2.png) [@rasteiner](https://forum.getkirby.com/u/rasteiner)\
**Post date:** [March 15, 2023, 1:41pm UTC](https://forum.getkirby.com/t/virtual-pages-with-api-and-caching/28090/8 "2023-03-15T13:41:12Z")

</div>

> [@moonwalk](#):
>
> ```php
> $apiData = $response->content();
> //...
> $corsi = $apiData->json(false)->corsi;
> 
> ```

`content()` returns a `string`.  
Therefore you can’t call `->json()` on it. You would have to call `->json()` on the `$response` (an instance of the `Remote` class); but actually, since both the cache and the API call give you strings, it’s easier to just use something like PHPs `json_decode` on the result of either one.

If you are using Kirby 3.8 or later, we can use `$cache->getOrSet()` to make the code a bit more concise.  
I’d refactor the code like this:

```php
    public function children() {
        $pages = [];

        $apiData = kirby()
            ->cache('api')
            ->getOrSet('apiData', fn() => $this->fetchData(), 720);
     
        $apiData = json_decode($apiData);
        $corsi = $apiData->corsi;

        foreach ($corsi as $key => $review) {
            $pages[] = [
                'slug' => Str::slug($review->titolo),
                'num' => $key+1,
                'template' => 'review',
                'model' => 'review',
                'content' => [
                    'title' => $review->titolo,
                    'headline' => $review->sottotitolo,
                    'byline' => $review->codice,
                    'summary' => $review->sedeCorso,
                    'date' => $review->datainizio,
                    'link' => $review->allegati,
                    'linkText' => $review->destinatari,
                    'cover' => $review->urlImmagine
                ]
            ];
        }

        return Pages::factory($pages, $this);
    }

    public function fetchData(): string {
        $apiKey = '';
        $user = 'myUser';
        $password = 'MyPassword';

        $response = Remote::get('https://api.website.com/thefile.json', [
            'headers' => [
                'Authorization: Basic ' . base64_encode($user . ':' . $password)
            ]
        ]);

        if ($response->code() === 200) {
            return $response->content();
        }

        //@todo: handle error case better than this
        throw "Could not fetch data";
    }

```

---

<div class="post-metadata">

**Author:** ![Reluma](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/reluma/32/12543_2.png) [@Reluma](https://forum.getkirby.com/u/Reluma)\
**Post date:** [March 16, 2023, 8:51am UTC](https://forum.getkirby.com/t/virtual-pages-with-api-and-caching/28090/9 "2023-03-16T08:51:54Z")

</div>

for completeness I’ll show you how I solved it with the help of Chat GPT 🤭

```auto
class ReviewsPage extends Page
{
    public function children()
    {
        $corsi = [];
        $pages = [];
        $apiKey = '';
        $user = 'myUser';
        $password = 'myPassword';

        $kirby = kirby();

        $apiCache = $kirby->cache('api');
        $apiData = $apiCache->get('apiData');

        if ($apiData === null) {

            $response = Remote::get('https://api.website.com/thefile.json', [
              'headers' => [
                'Authorization: Basic ' . base64_encode($user . ':' . $password)
              ]
            ]);

          
            if ($response->code() === 200) {
                  $apiData = $response->content();
                  $apiCache->set('apiData', $apiData, 720);
            }

        }
        
     
        if ($apiData && is_string($apiData)) {
            $jsonData = json_decode($apiData);
            if ($jsonData !== null) {
                $corsi = $jsonData->corsi;
            }
        }

       

        echo count($corsi) . ' courses retrieved from ' . ($apiData ? 'cache' : 'API') . '<br>';

        foreach ($corsi as $key => $review) {
            $pages[] = [
                'slug' => Str::slug($review->titolo),
                'num' => $key+1,
                'template' => 'review',
                'model' => 'review',
                'content' => [
                    'title' => $review->titolo,
                    'headline' => $review->sottotitolo,
                    'byline' => $review->codice,
                    'summary' => $review->sedeCorso,
                    'date' => $review->datainizio,
                    'link' => $review->allegati,
                    'linkText' => $review->destinatari,
                    'cover' => $review->urlImmagine
                ]
            ];
        }

        return Pages::factory($pages, $this);
    }

}

```

this solution seems to work but yours is much more compact. I will do some tests in the next few days to see if everything is ok.

Thanks a lot Rasteiner!

---

<div class="post-metadata">

**Author:** ![bnomei](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/bnomei/32/775_2.png) [@bnomei](https://forum.getkirby.com/u/bnomei)\
**Post date:** [March 17, 2023, 4:34pm UTC](https://forum.getkirby.com/t/virtual-pages-with-api-and-caching/28090/11 "2023-03-17T16:34:47Z")

</div>

you might consider adding a cache to the virtual pages children. its not in the guide yet but thats how the core kirby logic works as well.

> <https://github.com/getkirby/getkirby.com/issues/1948>
>
> https://discord.com/channels/525634039965679616/1030095535236980746/103011678544…7522344
> https://discord.com/channels/525634039965679616/1077497021902114847/1077525554678480917
> 
> \`\`\`diff
> class AnimalsPage extends Page
> {
> public function children()
> {
> + if ($this-\>children instanceof Pages) {
> + return $this-\>children;
> + }
> 
> $csv = csv($this-\>root() . '/animals.csv', ';');
> 
> $children = array\_map(function ($animal) {
> return \[
> 'slug' =\> Str::slug($animal\['Scientific Name'\]),
> 'template' =\> 'animal',
> 'model' =\> 'animal',
> 'num' =\> 0,
> 'content' =\> \[
> + 'uuid' =\> Str::slug($animal\['Scientific Name'\]),
> 'title' =\> $animal\['Scientific Name'\],
> 'commonName' =\> $animal\['Common Name'\],
> 'description' =\> $animal\['Description'\],
> \]
> \];
> }, $csv);
> 
> \- return Pages::factory($children, $this);
> + $this-\>children = Pages::factory($children, $this);
> + return $this-\>children; 
> }
> }
> \`\`\`

---

<div class="post-metadata">

**Author:** ![bnomei](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/bnomei/32/775_2.png) [@bnomei](https://forum.getkirby.com/u/bnomei)\
**Post date:** [February 19, 2025, 1:21pm UTC](https://forum.getkirby.com/t/virtual-pages-with-api-and-caching/28090/12 "2025-02-19T13:21:17Z")

</div>

FYI: I created a plugin to help pulling data from public apis and those protected by authentification and making them virtual pages.

> **[GitHub - bnomei/kirby-api-pages: Virtual Pages from APIs](https://github.com/bnomei/kirby-api-pages)**
>
> Virtual Pages from APIs
