# Table of contents for Kirby 3

**URL:** <https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780>\
**Category:** Questions\
**Tags:** v3\
**Created:** [April 12, 2019, 2:39pm UTC](https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780 "2019-04-12T14:39:45Z")\
**Posts on this page:** 13\
**Page:** 3

<div class="post-metadata">

**Author:** ![claraloustau](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/claraloustau/32/12322_2.png) [@claraloustau](https://forum.getkirby.com/u/claraloustau)\
**Post date:** [July 31, 2022, 7:03pm UTC](https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780/43 "2022-07-31T19:03:16Z")

</div>

> If you now add a `(toc)` placeholder to a page that contains headlines of the given level, you should see a ToC appear where you added the placeholder.

I don’t understand where sould I add (toc) placeholder, is it in note.yml ?

Thanks for your time

---

<div class="post-metadata">

**Author:** ![texnixe](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/texnixe/32/5754_2.png) [@texnixe](https://forum.getkirby.com/u/texnixe)\
**Post date:** [July 31, 2022, 7:13pm UTC](https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780/44 "2022-07-31T19:13:43Z")

</div>

> [@claraloustau](#):
>
> where sould I add (toc) placeholder,

The `toc` placeholder would not go into the blueprint but into a `textarea` field. But as I already mentioned above, the Starterkit doesn’t use any textarea fields anywhere. So if you want to use it, you have to add such a field to the blueprint where you want to use it, e.g. note.yml.

Currently, in `note.yml` a blocks field is used for long form text. If you want to create a toc from a blocks field: [Of anchors and ToCs, part 1 | Kirby CMS](https://getkirby.com/docs/cookbook/extensions/table-of-contents#extra-toc-from-blocks-field)

---

<div class="post-metadata">

**Author:** ![claraloustau](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/claraloustau/32/12322_2.png) [@claraloustau](https://forum.getkirby.com/u/claraloustau)\
**Post date:** [July 31, 2022, 7:26pm UTC](https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780/45 "2022-07-31T19:26:48Z")

</div>

Thanks @texnixe

For the blocks should I replace `$headlines` by :

```auto
// assuming a blocks field called text
$headlines = $page->text()->toBlocks()->filterBy('type', 'heading')->filterby('level', 'h2');

```

And remove `'kirbytext:after' => [` ?

```auto
<?php

use Kirby\Toolkit\Collection;
use Kirby\Toolkit\Str;
use Kirby\Toolkit\Obj;

Kirby::plugin('k-cookbook/toc', [
  'hooks' => [
//
// Premiere fonction pour ancrer les titres
//

      function($text) {

        // assuming a blocks field called text
        $headlines = $page->text()->toBlocks()->filterBy('type', 'heading')->filterby('level', 'h2');

        // create the regex pattern to be used as first argument in `preg_replace_callback()`
        $headlinesPattern = is_array($headlines) ? implode('|', $headlines) : $headlines;

        // use `preg_replace_callback()` to replace matches with anchors
        $text = preg_replace_callback('!<(' . $headlinesPattern . ')>(.*?)</\\1>!s', function ($match) {

            // create the id from the headline text
            $id = Str::slug(Str::unhtml($match[2]));

            // return the modified headline:
            // $match[1] contains the match for the first subpattern, i.e. `h2`, `h3` etc.
            // $match[2] contains the match for the second subpattern, i.e. the actual headline text
            return '<' . $match[1] . ' id="' . $id . '"><a href="#' . $id . '">' . $match[2] . '</a></' . $match[1] . '>';

        }, $text);

        return $text;
      },

//
// Deuxième fonction pour générer le ToC
//

      function($texto) {

        // the pattern allows passing an optional headline level `(toc: h3)`
        $pattern = '!\(toc(?::\s?(h[1-6]))?\)!';

        $texto = preg_replace_callback($pattern, function($match) use($texto) {

          // get the headline level from the match
          $headline = $match[1] ?? 'h2';

          // find all headline matches
          preg_match_all('!<' . $headline . '.*?>(.*?)</' . $headline . '>!s', $texto, $matches);

          // create a new collection for the headlines…
          $headlines = new Collection();

          // …and add all matches
          foreach ($matches[1] as $texto) {
              $headline = new Obj([
                  'id' => $id = '#' . Str::slug(Str::unhtml($texto)),
                  'url' => $id,
                  'texto' => trim(strip_tags($texto)),
              ]);

              $headlines->append($headline->url(), $headline);
          }

          // return the html for the ToC
          return snippet('toc', ['headlines' => $headlines], false);

        }, $texto);

        return $texto;

      },
],
  'snippets' => [
    'toc' => __DIR__. '/snippets/toc.php'
  ],
]);

```

---

<div class="post-metadata">

**Author:** ![texnixe](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/texnixe/32/5754_2.png) [@texnixe](https://forum.getkirby.com/u/texnixe)\
**Post date:** [July 31, 2022, 7:27pm UTC](https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780/46 "2022-07-31T19:27:31Z")

</div>

Yes, you don’t need the kirbytext hooks for the blocks field.

---

<div class="post-metadata">

**Author:** ![claraloustau](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/claraloustau/32/12322_2.png) [@claraloustau](https://forum.getkirby.com/u/claraloustau)\
**Post date:** [July 31, 2022, 7:36pm UTC](https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780/47 "2022-07-31T19:36:12Z")

</div>

I’ve got an error

> Undefined variable: headlines

But I don’t understand why it don’t found $headlines

index.php

```auto
<?php

use Kirby\Toolkit\Collection;
use Kirby\Toolkit\Str;
use Kirby\Toolkit\Obj;

Kirby::plugin('k-cookbook/toc', [
  'hooks' => [
//
// Premiere fonction pour ancrer les titres
//

      function($text) {

        // assuming a blocks field called text
        $headlines = $page->text()->toBlocks()->filterBy('type', 'heading')->filterby('level', 'h2');

        // create the regex pattern to be used as first argument in `preg_replace_callback()`
        $headlinesPattern = is_array($headlines) ? implode('|', $headlines) : $headlines;

        // use `preg_replace_callback()` to replace matches with anchors
        $text = preg_replace_callback('!<(' . $headlinesPattern . ')>(.*?)</\\1>!s', function ($match) {

            // create the id from the headline text
            $id = Str::slug(Str::unhtml($match[2]));

            // return the modified headline:
            // $match[1] contains the match for the first subpattern, i.e. `h2`, `h3` etc.
            // $match[2] contains the match for the second subpattern, i.e. the actual headline text
            return '<' . $match[1] . ' id="' . $id . '"><a href="#' . $id . '">' . $match[2] . '</a></' . $match[1] . '>';

        }, $text);

        return $text;
      },

//
// Deuxième fonction pour générer le ToC
//

      function($texto) {

        // the pattern allows passing an optional headline level `(toc: h3)`
        $pattern = '!\(toc(?::\s?(h[1-6]))?\)!';

        $texto = preg_replace_callback($pattern, function($match) use($texto) {

          // get the headline level from the match
          $headline = $match[1] ?? 'h2';

          // find all headline matches
          preg_match_all('!<' . $headline . '.*?>(.*?)</' . $headline . '>!s', $texto, $matches);

          // create a new collection for the headlines…
          $headlines = new Collection();

          // …and add all matches
          foreach ($matches[1] as $texto) {
              $headline = new Obj([
                  'id' => $id = '#' . Str::slug(Str::unhtml($texto)),
                  'url' => $id,
                  'texto' => trim(strip_tags($texto)),
              ]);

              $headlines->append($headline->url(), $headline);
          }

          // return the html for the ToC
          return snippet('toc', ['headlines' => $headlines], false);

        }, $texto);

        return $texto;

      },
],
  'snippets' => [
    'toc' => __DIR__. '/snippets/toc.php'
  ],
]);

```

toc.php

```auto
  <nav class="toc">
    <h2>Table of Contents</h2>
    <ol>
      <?php foreach($headlines as $headline): ?>
          <li><a href="<?= $page->url() . '/#' . Str::slug($headline->text()) ?>"><?= $headline->text() ?></a></li>
      <?php endforeach ?>
    </ol>

  </nav>
<?php endif ?>
type or paste code here

```

And `<?php snippet('toc') ?>` in `note.php`  
I also add the line of code in `/site/snippets/blocks/heading.php` for overwrite the default heading block snippet and add an `id` attribute

---

<div class="post-metadata">

**Author:** ![claraloustau](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/claraloustau/32/12322_2.png) [@claraloustau](https://forum.getkirby.com/u/claraloustau)\
**Post date:** [July 31, 2022, 7:52pm UTC](https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780/48 "2022-07-31T19:52:48Z")

</div>

Ok I replace `<?php snippet('toc') ?` by `<?php snippet('toc', ['headlines' => $page->text()->headlines('h2')]) ?>` and I don’t have the error anymore ✅

But now I have the error

> Object of class Kirby\Cms\Field could not be converted to int

---

<div class="post-metadata">

**Author:** ![texnixe](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/texnixe/32/5754_2.png) [@texnixe](https://forum.getkirby.com/u/texnixe)\
**Post date:** [July 31, 2022, 8:30pm UTC](https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780/49 "2022-07-31T20:30:51Z")

</div>

> [@claraloustau](#):
>
> \<?php snippet('toc', ['headlines' =\> $page-\>text()-\>headlines('h2')]) ?\>

But you now have to pass the filtered blocks as headlines\_

```auto
<?php snippet('toc', ['headlines' => $page->text()->toBlocks->filterBy('type', 'heading')->filterby('level', 'h2')]) ?>

```

---

<div class="post-metadata">

**Author:** ![claraloustau](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/claraloustau/32/12322_2.png) [@claraloustau](https://forum.getkirby.com/u/claraloustau)\
**Post date:** [July 31, 2022, 8:48pm UTC](https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780/50 "2022-07-31T20:48:51Z")

</div>

Ok I understand better now, thank you @texnixe !

I now have an error for this line;

> syntax error, unexpected ‘-\>’ (T\_OBJECT\_OPERATOR), expecting identifier (T\_STRING) or variable (T\_VARIABLE) or ‘{’ or ‘$’

---

<div class="post-metadata">

**Author:** ![texnixe](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/texnixe/32/5754_2.png) [@texnixe](https://forum.getkirby.com/u/texnixe)\
**Post date:** [July 31, 2022, 8:55pm UTC](https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780/51 "2022-07-31T20:55:05Z")

</div>

I added an arrow too many when copy/pasting, corrected above.

Tip: Learn to understand PHP error messages. Saves a lot of time.

---

<div class="post-metadata">

**Author:** ![claraloustau](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/claraloustau/32/12322_2.png) [@claraloustau](https://forum.getkirby.com/u/claraloustau)\
**Post date:** [July 31, 2022, 9:01pm UTC](https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780/52 "2022-07-31T21:01:22Z")

</div>

🤦‍♀️ sorry for that ! I should have seen it

---

<div class="post-metadata">

**Author:** ![claraloustau](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/claraloustau/32/12322_2.png) [@claraloustau](https://forum.getkirby.com/u/claraloustau)\
**Post date:** [July 31, 2022, 9:25pm UTC](https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780/53 "2022-07-31T21:25:09Z")

</div>

I now have an error about the variable $headlines (Undefined variable: headlines), don’t found from where it come, probably from index.php…

---

<div class="post-metadata">

**Author:** ![texnixe](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/texnixe/32/5754_2.png) [@texnixe](https://forum.getkirby.com/u/texnixe)\
**Post date:** [July 31, 2022, 9:33pm UTC](https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780/54 "2022-07-31T21:33:31Z")

</div>

You don’t actually need the plugin if you only want to use the toc with blocks.

Put the toc.php snippet into /site/snippets and call the snippet with like above. That’s it.

---

<div class="post-metadata">

**Author:** ![claraloustau](https://dub1.discourse-cdn.com/flex017/user_avatar/forum.getkirby.com/claraloustau/32/12322_2.png) [@claraloustau](https://forum.getkirby.com/u/claraloustau)\
**Post date:** [July 31, 2022, 10:06pm UTC](https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780/55 "2022-07-31T22:06:45Z")

</div>

Thank you a lot @texnine !  
I spent lot of time between code for textarea and blocks for finaly so simple and logical lines of code.  
It looks so easier to do it for other headings now, thanks!

[Previous page](https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780.md?page=2)
