Navigation to subheadings

I would like to implement a drop-down menue to headings within a subpage. something like this:

So what I have done so far is to:

  1. put a counting variable in my template, which goes over the different blocks in my content
  2. whenever it is a heading I apply an id=$count to the heading
  3. I save the heading and the ID in a file

So far so good. I end up with a file with all headings and corresponding IDs. I can then use the file and generate the drop down menu, probably via js .!?
But I now realized, that the file will be freshly generated each time the subpage is opened, which is maybe not ideal. So what I actually need is, when I change the page in the panel, then the file and IDs should be generated once and thats it. Can anyone point me to a description of what happens, when I save the panel, so I may figure out, where to sneak my php in. Or maybe someone has a better idea.
so any thoughts would be highly appreciated.

Best
Hagen

You’re talking about “blocks”, I guess therefore you’re using the “editor” plugin…

If there are just like 6 pages in total, you might even not need to cache the headings and ids at all and generate the menu “on the fly”.

something like:

<nav>
  <ul>
    <?php foreach($pages as $p): ?>
      <li>
        <a href="<?= $p->url(); ?>"><?= $page->title()->html() ?></a>
        <ul class="dropdown">
          <?php foreach($p->blocks()->toBlocks()->filterBy('type', 'h1') as $block): ?>
            <li>
              <a href="<?= $p->url() . '#' . $block->hash() ?>">
                <?= $block->content() ?>
              </a>
            </li>
          <?php endforeach; ?>
        </ul>
      </li>
    <?php endforeach; ?>
  <ul>
</nav>

for the frontend part you can use something like this: https://css-tricks.com/solved-with-css-dropdown-menus/
No JS required

ps: this is written without having tested any of this. There might be typos, but maybe the concept’s clear.

I only need it for one page, I just thought doing it on the fly each time generates too much overhead.
But that would of course make it much easier. I’ll try it out …

Thanks
Hagen

people are doing far worse stuff than this and are getting away with it.

If it really is too slow (but I’d bet it isn’t), you could consider caching the whole website: https://getkirby.com/docs/guide/cache before trying to optimize otherwise. Even if you save the ids in a file, you still would have to load that file and then “dynamically” generate the menu from that file.
You would be still doing the loops, but over a cache file instead of the loops over the content. Not much of a difference if you ask me.

If you really want to “pre-generate” those ids, you probably would want to do that in a hook. https://getkirby.com/docs/reference/system/options/hooks
Probably a 'page.update:after' hook.

that is of course true. It is anyway a site that will not be visited frequently. So I gues it will be ok. I’ll implement it and see how it works.

Thanks
Hagen

ok so I got it to work faster than I thought. Since I’m using the Builder and in part nested Builders, it was a bit more complicated. The only thing I did not get to work was $block->hash() so I was falling back to my $count variable. So thi is how my navigation looks like now.

<div class="nav-main-wrapper">
  <nav role="navigation" class="nav-main">
    <ul>
      <?php foreach ($pages->listed() as $page) : ?>
        <li class="nav-main__item<?= r($page->isOpen(), ' active') ?>">
          <a href="<?= $page->url() ?>"><?= $page->title()->html() ?></a>
          <?php if ($page->title() == 'DAS PROJEKT') :
            $count = 1; ?>
            <ul class="dropdown">
              <?php foreach ($page->mybuilder()->toBuilderBlocks() as $block) :
                if ($block->header()->isNotEmpty()) : ?>
                  <li>
                    <a href=<?= $page->url() . '#' . $count ?>>
                      <?= $block->header()->content() ?>
                    </a>
                  </li>
                <?php endif;
                if ($block->_key() == "history") :
                  foreach ($block->historylist()->toBuilderBlocks() as $history) :
                    if ($history->header()->isNotEmpty()) : ?>
                      <li>
                        <a href=<?= $page->url() . '#' . $count ?>>
                          <?= $history->header()->content() ?>
                        </a>
                      </li>
                    <?php  endif;
                  endforeach;
                endif;
                $count++;
              endforeach; ?>
            </ul>
            <?php  endif; ?>
        </li>
      <?php endforeach ?>
    </ul>
  </nav>
</div>

Unfortunately the code looks really ugly here …

So it actually works quite well I just have to fiddle a bit more with the CSS, since it jumps a bit and is left aligned, probably because it is within the li element.

My page

So thanks a lot!
Hagen

It will look much better once you stop echoing your html tags…

This is really discouraged…

Your code should always look like this

<h1><?= $page->title() ?></h1>

And instead of using the counter, I’d sluggify the headline

done, still looks really bad. Apparently tones of tabs coming from my editor. It is perfectly fine in Atom locally though. Anyway I essentially have no clue about php … unfortunately

What does that mean?

Best
Hagen

A “slug” is a url safe representation of a text. So, for example, “Drei Säulen” becomes “drei-saulen” (or “drei-saeulen”, depending on config). With the editor plugin I thought you were using, ->hash() would have done this.
If the Builder plugin works like I think it does (I’ve never used it), you should be able to do it with the slug() field method.

In other words:

<a href="<?= $page->url() . '#' . $block->header()->slug() ?>">
  <?= $block->header()->html() ?>
</a>

Also consider putting the repeating parts, i.e. the li’s in the dropdown into snippets to keep the code a bit cleaner.

This indeed works! So all my $counts are gone …

@texnixe
ok so while I was at it, I was going over all my snippets and removed the echos, but I assume I did not invent that myself… I also put a snippet in
I also realized you not only formated my code (thanks!) but also edited it. In particular taking out the curly brackets from my if statements. As far as I have googled both is possible. Any particular reason why?

So my code looks like this now:

<div class="nav-main-wrapper">
  <nav role="navigation" class="nav-main">
    <ul>
     <?php foreach($pages->listed() as $p): ?>
	<li class="nav-main__item<?= r($p->isOpen(), ' active') ?>">
	<a href="<?= $p->url() ?>"><?= $p->title()->html() ?></a>
	<?php if ($p->title() == 'DAS PROJEKT') : ?>
	    <ul class="dropdown">
	    <?php foreach($p->mybuilder()->toBuilderBlocks() as $block):
		if ($block->header()->isNotEmpty()) :
		    snippet('blocks/dropdown-menue', array('data' => $block, 'sourcepage' => $p));
		endif;
		if ($block->_key() == "history") :
		     foreach($block->historylist()->toBuilderBlocks() as $history):
			if ($history->header()->isNotEmpty()) :
			    snippet('blocks/dropdown-menue', array('data' => $history, 'sourcepage' => $p));
			endif;
		      endforeach;
		endif;
          endforeach; ?>
        </ul>
	<?php	endif; ?>
	</li>
      <?php endforeach ?>
    </ul>
 </nav>
 </div>

So thanks for all the input!
Hagen

It’s possible, yes, but not really recommended. Also you mixed “colon style” (alternative syntax) with curly braces, so I unified it.

ok thx, I’ll check the 101 …

Cheers
Hagen