Special characters in a content file, stored in html


I save all the messages from the contact forms in the panel to have traceability.

Everything is properly stored. Except that it transforms special characters into html encoding (for example: ' for an apostrophe).

My problem is that I want to be able to see the messages in the panel. But as soon as there is a special character, the message is cut off.

It’s just a display issue I guess.

I tried several things html_entity_decode htmlspecialchars_decode

Sample code in the HTML template:

<textarea id="text" name="text" rows="5" class="form-control" placeholder="Votre message" required><?= $data['text']?? '' ?></textarea>

example of stored text:

  text: hello
c&#039;est moi

View panel:
Capture d’écran 2022-09-19 à 17.22.05

How do you save these form fields? And how is the content displayed in the Panel?


here is the complete code of the controller (it is adapted from the form available in the cookbook)

// Fonction pour lister les messages dans une liste (structure)
function toStructure($data, $exist) {
    $result = "";
    if(!$exist) {
        // Nom de la structure
        $result = ucfirst("messageriecontact:\n\n");
    $result .= "-\n";
    foreach($data as $name => $value) {
        $result .= "  ".$name.": " . str_replace(CHR(10),"",$value) ."\n";
    return $result;

// Formulaires
return function($kirby, $pages, $page, $site) {

    $alert = null;
    $attachments = null;
    $fileURL = null;

    if($kirby->request()->is('POST')) {
        //else if(get('contact_submit')){


            $data = [
                'firstname'  => get('firstname'),
                'name'  => get('name'),
                'email' => get('email'),
                'phone'  => get('phone'),
                'text'  => get('text'),
                'checkboxvalid'  => get('checkboxvalid')

            $rules = [
                'firstname'  => ['required', 'min' => 3],
                'name'  => ['required', 'min' => 3],
                'email' => ['required', 'email'],
                'phone'  => ['required', 'num'],
                'text'  => ['required', 'min' => 3, 'max' => 3000],
                'checkboxvalid' => ['required'],

            $messages = [
                'firstname'  => 'Merci d\'ajouter un prénom valide',
                'name'  => 'Merci d\'ajouter un nom valide',
                'email' => 'Merci d\'ajouter une adresse e-mail valide',
                'phone'  => 'Merci d\'ajouter un téléphone valide',
                'text'  => 'Merci d\'écrire un texte entre 3 et 3000 caractères',
                'checkboxvalid'  => 'Merci de cocher la case'

            // Recaptcha V3
            $secretKey = "6LevfXcdAAAAAD6n83xtAS3ll9zs0EB5yb9xK0Pp";

            // Chemin d'enregistrement des messages
            $file = 'content/forms/form-mail-contact/form-mail-contact.txt';

            // some of the data is invalid
            if($invalid = invalid($data, $rules, $messages)) {
                $alert = $invalid;

                // the data is fine, let's send the email
            } else {
                $messagerie = [
                    'firstname'     => esc($data['firstname']),
                    'name'          => esc($data['name']),
                    'email'         => esc($data['email']),
                    'phone'         => esc($data['phone']),
                    'text'          => esc($data['text']),
                    'checkboxvalid' => esc($data['checkboxvalid']),
                    'referer'       => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '',
                    'ip'            => $_SERVER['REMOTE_ADDR'],
                    // Enregistrement d'un message dans une liste (structure)
                    // Ajout de la date
                    'date'          => date('Y-m-d H:i:s')

                // Chemin du fichier
                $file_content = file_get_contents($file);                
                $fp = fopen($file, 'a');
                fwrite($fp, toStructure($messagerie, strlen($file_content)) .PHP_EOL);

                // DEBUT Recaptcha v3
                if(!get('g-recaptcha-response')) {
                    //$this->session->set_flashdata('error_message', get_phrase('form_contact_error'));
                $captcha = get('g-recaptcha-response');

                // post request to server
                $url = 'https://www.google.com/recaptcha/api/siteverify';
                $dataCaptcha = array('secret' => $secretKey, 'response' => $captcha);

                $options = array(
                    'http' => array(
                        'header'  => "Content-type: application/x-www-form-urlencoded\r\n",
                        'method'  => 'POST',
                        'content' => http_build_query($dataCaptcha)
                $context  = stream_context_create($options);
                $response = file_get_contents($url, false, $context);
                $responseKeys = json_decode($response,true);

                if(!$responseKeys["success"]) {
                    // DEBUT Vérification SPAM
                    // load the data and delete the line from the array 
                    $lines = file($file); 
                    $last = sizeof($lines) -1; 

                    while($last > 0 && trim($lines[$last]) != '-') {

                    // write the new data to the file 
                    $fp = fopen($file, 'w'); 
                    fwrite($fp, implode('', $lines)); 
                    // FIN Vérification SPAM

                    //$this->session->set_flashdata('error_message', get_phrase('form_contact_error'));
                // FIN recaptcha v3

                try {
                        'template' => 'email',
                        'from'     => 'x@x.fr',
                        'replyTo'  => $data['email'],
                        'to'       => 'x@x.fr',
                        'subject'  => 'x.fr/contact ' . esc($data['firstname']) .' '. esc($data['name']) . ' vous a envoyé un message depuis votre site',
                        'data'     => $messagerie,

                } catch (Exception $error) {
                    //$alert['error'] = "Le formulaire n'a pas pu être envoyé.";
                    $alert['error'] = $error->getMessage();

                // no exception occured, let's send a success message
                if (empty($alert) === true) {
//                    $success = 'Votre message a été envoyé. Merci. Nous reviendrons vers vous bientôt !';
//                    $data = [];
                    go($site->url() . "/" . $site->page('contact/contact-merci'));
    } // fin if($kirby->request()->is('POST'))

    return [
        'alert'   => $alert,
        'data'    => $data ?? false,
        'success' => $success ?? false


# Each page blueprint must have a title, the title may be different from the file name
title: Messagerie Contact

# Each page can have an icon that is shown in page listings when no preview image is available.
icon: 📖

# Limit the possible page statuses to `draft` and `listed`.
# More about page statuses: https://getkirby.com/docs/reference/panel/blueprints/page#statuses
  unlisted: true

# Page options allow you to control how users can interact with the page.
# Here we prevent changing the page slug and page deletion
# More about page options: https://getkirby.com/docs/reference/panel/blueprints/page#options
  changeTitle: false
  changeSlug: false
  delete: false
  changeStatus: false
  duplicate: false
  preview: false

# Kirby has many different field types, from simple text fields to the more complex structure field that contains subfields
# All available field types: https://getkirby.com/docs/reference/panel/fields
    type: info
    headline: Info
    text: |
      Historique des messages reçu sur la page Contact.
      Ne pas supprimer les messages.
    label: Messagerie page Contact
    type: structure
    limit: 20
    #prepend: true
    sortBy: date desc
    max: 1
        label: Nom
        type: text
        width: 1/2
        disabled: true
        label: Prénom
        type: text
        width: 1/2
        disabled: true
        label: E-mail
        type: text
        width: 1/2
        disabled: true
        label: Téléphone
        type: text
        width: 1/2
        disabled: true
        label: Message
        type: textarea
        size: medium
        width: 1/1
        disabled: true
        label: Case à cocher validée
        type: text
        width: 1/2
        disabled: true
        label: Page de référence
        type: text
        width: 1/1
        disabled: true
        label: Ip
        type: text
        width: 1/2
        disabled: true
        label: Date
        type: date
        time: true
        width: 1/2
        disabled: true

content page:

Title: Messages page: Contact


Messageriecontact: -
  firstname: mickaaaaa
  name: aaaaa
  email: aaaa@zzzz.com
  phone: 511611
  text: coucou 
c&#039;est moi
  checkboxvalid: confirmée
  referer: http://XXX.local/contact
  date: 2022-09-19 17:09:16

  firstname: mickael
  name: ssss
  email: ssss@xxx.fr
  phone: 55221
  text: hello
c&#039;est moi
  checkboxvalid: confirmée
  referer: http://XXX.local/contact
  date: 2022-09-19 17:10:44

I think the problem result from your very manual approach to saving the content. Why don’t you use Kirby’s $page->update() method? Also, you should be yaml-encoding the array you store in the structure field, see the addToStructure()method for example here: Add To Structure not working