Update site content programmatically

Hi,

It seems that I can’t update the site content programmatically. It only work for its title. I’m using Kirby 4.0.1 .

These scripts are working :

        $kirby->impersonate('kirby');
        try {
          $site->update([
            'title' => 'My site',
          ]);
        } catch (\Throwable $th) {
          throw new Exception($th);
        }
        $kirby->impersonate('kirby');
        try {
          $page->update([
            'description' => 'New description',
          ]);
        } catch (\Throwable $th) {
          throw new Exception($th);
        }

But not this one :

        $kirby->impersonate('kirby');
        try {
          $site->update([
            'description' => 'New site description',
          ]);
        } catch (\Throwable $th) {
          throw new Exception($th);
        }

However there is a description field in the site blueprint. It doesn’t work with any other fields than the title.

I don’t understand why. Can you help me ?

Thank you.

EDIT : I’ve made few tests. The update works for fields that doesn’t exists in the site.txt (it creates the data entry) but if it exists I can’t modify it (only for site, not for pages).

Cannot reproduce this.

What does the site.yml blueprint look like?
Do you have any permissions set, if so, which ones?
Can you reproduce your issue in a fresh Starterkit?

Where can I check the permissions ?

Here you are :

title: Site

tabs:
  contentTab:
    label: Page
    columns:
      - width: 1/3
        sections:
          listed:
            label: Menu
            type: pages
            templates:
              - free
              - repository
              - external
            status: listed
            image:
              cover: true
              back: white
      - width: 1/3
        sections:
          unlisted:
            label: Page non-indexées
            type: pages
            query: site.index.filterBy('status', 'unlisted').without('error')
            help: Ces pages peuvent être pointées par des liens mais ne sont pas affichées dans le menu.
      - width: 1/3
        sections:
          drafts:
            label: Brouillon
            type: pages
            status: draft
            create:
              - free
              - repository
              - external

  newsletterTab:
    label: Newsletter
    fields:
      newsletterCover:
        label: Image de section
        type: files
        multiple: false
        layout: cards
        width: 1/2
        size: huge
        image:
          ratio: 16/11
          cover: true
      newsletterIntro:
        label: Texte d'invitation
        type: writer
        width: 1/2
        maxlength: 500
        help: Affiché dans la section newsletter pour inviter à s'y abonner.

  mapTab: tabs/map

  configTab:
    label: Contact
    fields:
      address:
        label: Adresse postale
        type: text
        width: 1/3
        icon: pin
      mail:
        label: Email
        type: email
        width: 1/3
      tel:
        label: Téléphone
        type: tel
        width: 1/3

So the field you want to update is not defined in site.yml?

When it exists it doesn’t work. If I try to update for exemple the address field it doesn’t work while it works if i update the description field that doesn’t exist.

As I wrote above, works fine for me in both cases. Please download a fresh Starterkit and check if you can reproduce your issue there.

Here is a streamable video of the kind of strange behaviors I observe : Enregistrement de l’écran 2023-12-13 à 18.48.05

The mapData field exists in the blueprint, its update doesn’t work.
The description field doesn’t exist, its update works.
(The address field exists, its update doesn’t work neither.)

If it can help you to understand…

I’ll try with a fresh starterkit.

Well, unfortunately, the mapdata field is missing from your blueprint. Maybe it’s an issue with the field type and what you are trying to store in it.

It’s the same for the address field that is shown in the blueprint. No idea ?

Envoyé de mon mobile

I understand better what’s happening but still can’t fix it.

The function is executed through a hook :

[
      'pattern' => '/import-map-data.json',
      'action' => function() {
        $site = site();
        $kirby = kirby();
        
        $csvString = $site->mapDataFile()->toFile()->read();
        $mapData = csvStringToYaml($csvString);
        
        $kirby->impersonate('kirby');
        try {
          $site->update([
            'mapdata' => Yaml::encode($mapData),
          ]);

          foreach ($mapData as $item) {
            if (!isset($item['street']) && !isset($item['town'])) return;
            $parenthesisRegex = '/\s*\([^)]*\)/';
            $adress = $item['street'] . ', ' . preg_replace($parenthesisRegex, '', $item['town']);
            try {
              $item['location'] = getLocationData($adress);
            } catch (\Throwable $th) {
              return json_encode('Couldn\'t get locations data through open street map. Error : ' . $th);
            }
          }

          F::write('./assets/map-data.json', json_encode($mapData));
        } catch (\Throwable $th) {
          return json_encode('Couldn\'t write data file. Error : ' . $th);
        }

        return json_encode('Map data update succeed.');
      }
    ]

The foreach loop takes time because it does multiple fetches to an API. During this time, the “mapdata” field is correctly filled. But right after the loop and the whole script is over, the field is cleared. As if Kirby has automatically performed an update based on the current content of the front-end field (wich is empty). Is it possible ? Does this gives you any idea @texnixe ?

just a guess. if you are logged in the panel and call the endpoint at the same time try not to change the user with impersonating

$kirby->impersonate(kirby()->user()?->id() ?? 'kirby');

It doesn’t resolve the problem, but thanks !

And if you remove this api stuff, it works?

No, it doesn’t work neither.

Ok, let’s go one step back.

If you try to update that site field from a template instead of from your route, does that work?

Have you ever tested to move your site.yml into a starterkit and test that?

Probably unrelated, but in this loop you’re editing a copy of each $item. This doesn’t mutate the original $mapData that you then save in './assets/map-data.json'.
You either have to use a reference pointer to $item when looping over mapData (foreach ($mapData as &$item) {) or explicitly put the value back into $mapData:

foreach ($mapData as $i => $item) {
         //...
         $mapData[$i]['location'] = getLocationData($adress);
         //...
}
1 Like

I can indeed update the site content through a template !

Following your advice, I did some test through the home template and it works if I call the route from here. As well as if I put the script in the template and execute it without calling the route. So the problem seems to come from the plugin where is located the button that calls the route. Here is the component :

<template>
  <k-button
    class="import-btn"
    variant="filled"
    :icon="icon"
    @click="importMapData"
    >Importer les données</k-button
  >
</template>

<script setup>
import { ref } from "vue";

const icon = ref("merge");

function importMapData() {
  icon.value = "loader";
  fetch("/import-map-data.json")
    .then((res) => res.json())
    .then((json) => {
      icon.value = "check";
      console.log(json);
    });
  fetch("/test");
}
</script>

<style>
.import-btn {
  margin-top: 2rem;
}
</style>

And, as a reminder, the route :

    [
      'pattern' => '/import-map-data.json',
      'action' => function() {
        $site = site();
        $kirby = kirby();
        
        $csvString = $site->mapDataFile()->toFile()->read();
        $mapData = csvStringToYaml($csvString);
        
        $kirby->impersonate(kirby()->user()?->id() ?? 'kirby');
        try {
          $site->update([
            'mapData' => Yaml::encode($mapData),
          ]);

          foreach ($mapData as $item) {
            if (!isset($item['street']) && !isset($item['town'])) return;
            $parenthesisRegex = '/\s*\([^)]*\)/';
            $adress = $item['street'] . ', ' . preg_replace($parenthesisRegex, '', $item['town']);
            try {
              $item['location'] = getLocationData($adress);
            } catch (\Throwable $th) {
              return json_encode('Couldn\'t get locations data through open street map. Error : ' . $th);
            }
          }

          F::write('./assets/map-data.json', json_encode($mapData));
        } catch (\Throwable $th) {
          return json_encode('Couldn\'t write data file. Error : ' . $th);
        }

        return json_encode('Map data update succeed.');
      }
    ],

I don’t understand why but it finally works.

Here is the final script :

        $site = site();
        $kirby = kirby();

        $csvString = $site->mapDataFile()->toFile()->read();
        $mapData = csvStringToYaml($csvString);

        $kirby->impersonate(kirby()->user()?->id() ?? 'kirby');
        $errors = [];

        try {
          $site->update(['mapData' => Yaml::encode($mapData)]);

          foreach ($mapData as $i => $item) {
            try {
              if (!isset($item['street']) && !isset($item['town'])) {
                  throw new Exception("Street or town data is missing for item $i", 400);
              }
              $parenthesisRegex = '/\s*\([^)]*\)/';
              $address = Html::decode($item['street']) . ', ' . preg_replace($parenthesisRegex, '', $item['town']);
      
              $mapData[$i]['location'] = getLocationData($address);
            } catch (\Throwable $th) {
              $errors[$i] = $th->getMessage();
            }
          }

          F::write('./assets/map-data.json', json_encode($mapData));
        } catch (\Throwable $th) {
          $errors['general'] = $th->getMessage();
        }

        if (empty($errors)) {
          http_response_code(200);
          echo json_encode(['success' => 'Map data update succeeded.']);
          exit;
        } else {
          http_response_code(500);
          echo json_encode(['error' => $errors]);
          exit;
        }

And the final component :

<template>
  <div>
    <k-button variant="filled" :icon="icon" @click="importMapData"
      >Importer les données</k-button
    >
    <k-info-field
      v-if="requestStatus === 'pending'"
      :text="requestMessage"
      theme="neutral"
    />
    <k-info-field
      v-if="requestStatus === 'success'"
      :text="requestMessage"
      theme="positive"
    />
    <k-info-field
      v-if="requestStatus === 'error'"
      :text="requestMessage"
      theme="negative"
    />
  </div>
</template>

<script setup>
import { ref } from "vue";

const icon = ref("merge");

const requestMessage = ref(
  "Attention, l'import de données écrasera les données du tableau ci-dessus."
);

const requestStatus = ref("pending");

function importMapData() {
  icon.value = "loader";
  fetch("/import-map-data.json")
    .then((res) => {
      return res.json();
    })
    .then((json) => {
      if (json.error) {
        requestStatus.value = "error";
        requestMessage.value = `Erreur lors de l'ajout de certaines données. Aucune correspondances trouvées pour les adresses : ${json.error.join(
          ", "
        )}`;
        throw new Error(json.error);
      }
      requestStatus.value = "success";
      requestMessage.value = `Données ajoutées. <a href="${window.location.href}" alt="Recharger la page pour rafraîchir les données.">Rechargez la page</a>.`;
      icon.value = "check";
      console.log(json);
    })
    .catch((error) => {
      requestStatus.value = "error";
      requestMessage.value = "Erreur lors des données. Veuillez réessayer.";
      console.error("Error:", error);
    });
}
</script>

<style scoped>
.k-button {
  margin-top: 2rem;
}
.k-info-field {
  margin-top: 1rem;
}
</style>