Display all tags except current tag?

Hello,

Is is possible to display tags across all projects except for the currently displayed/active tag?

I have a projects page setup to display the currently selected tag plus grouped tags that span (and are linked to) all additional projects. However, as you can see from the screenshot, the ‘website design’ tag is currently selected/active but still appears within the tags list.

I would like the currently selected tag not to appear within the tags list.

My projects template:

 <?php if(param('tag')): ?>
<h1>Metadata</h1>
		<h2 class="aggregate-tags">Project<?php if($projects->count() > 1) echo 's' ?> tagged as <span class="tag"><mark><?= $tag ?></mark><a class="close" href="<?= $page->url() ?>" aria-label="All projects">&times;</a></span>
			</h2>
			
<div class='all-tags'>
  	<?php $tags = $page->children()->listed()->not($page)->pluck('tags', ',', true);
				foreach ($tags as $tag): ?>
  				<a href="<?= url('projects/tag:' . urlencode($tag)) ?>"><?= $tag; ?></a>
		<?php endforeach ?>
</div>

I’ve tried…

->not($page)

…but don’t think this can work due to the fact the parent page isn’t technically a child.

My controller:

<?php

return function($page) {

  // fetch pages
  $projects = $page->children()->listed()->sortBy('date', 'desc');

  // add tag filter
  if($tag   = urldecode(param('tag') ?? '')) {
    $projects = $projects->filterBy('tags', $tag, ',');
  }

  // fetch tags
  $tags = $projects->pluck('tags', ',', false);

  // pagination
  $perpage    = $page->perpage()->int();
  $projects   = $projects->paginate(($perpage >= 1)? $perpage : 6);
  $pagination = $projects->pagination();

  return compact('projects', 'tags', 'tag', 'pagination');

};

I’ve also seached the Kirby forum for a previous solution, but the closest post I’ve found is in relation to site pages not tags.

I would be very grateful if someone can point this PHP novice in the right direction. :slight_smile:

There’s probably a bunch of different way to do this.
Easiest is to simply skip it inside the foreach loop

foreach ($tags as $tag) :

    // Skip if the tag in the loop is the same as the one in the url
    if ($tag == param('tag')) continue;


Easy and simple is good! :wink:

Thank you for the solution.

I’ve just added the code you suggested and it works as expected. However, it works with tags that consist of a single word only.

For example when the ‘music’ tag is selected and is active it does not display within the aggregate tags list. However, if the ‘search engines’ or ‘website design’ tags are selected and active they still appear within the aggregate list.

I’ve used slug on the same projects template (now including your code) to generate unique tag-based selectors that I can target for layout purposes…

<div class='all-tags'>
  	<?php $tags = $page->children()->listed()->not($page)->pluck('tags', ',', true);
				foreach ($tags as $tag): if ($tag == param('tag')) continue; ?>
  				<a href="<?= url('projects/tag:' . urlencode($tag)) ?>"><?= $tag; ?></a>
		<?php endforeach ?>
</div>

		</header>
		
		<ul class="tag-grid <?php echo str::slug($tag)?>">

Could this be the issue?

Sily, me, I didn’t consider that your tags might be converted to lower case or even be multi words.
You have to convert your tag into a slug version first before comparing the two.

if ( Str::slug($tag) == param('tag')) continue;

I’d also update this:

<a href="<?= url('projects/tag:' . urlencode($tag)) ?>"><?= $tag; ?></a>

with this

<a href="<?= url('projects' , ['params' => ['tag' => $tag]]) ?>"><?= $tag; ?></a>

just to stick with native kirby methods.

Thank you Manuel,

I’ve updated the projects template code as per your suggestion:

<?php if(param('tag')): ?>
<h1>Metadata</h1>
		<h2 class="aggregate-tags">Project<?php if($projects->count() > 1) echo 's' ?> tagged as <span class="tag"><mark><?= $tag ?></mark><a class="close" href="<?= $page->url() ?>" aria-label="All projects">&times;</a></span>
			</h2>
			
<div class='all-tags'>
  	<?php $tags = $page->children()->listed()->not($page)->pluck('tags', ',', true);
				foreach ($tags as $tag): if ( Str::slug($tag) == param('tag')) continue; ?>
  				<a href="<?= url('projects' , ['params' => ['tag' => $tag]]) ?>"><?= $tag; ?></a>
		<?php endforeach ?>
</div>

		</header>
		
		<ul class="tag-grid <?php echo str::slug($tag)?>">

However, tags that consist of two words are still being shown within the aggregate list when selected.

Unsure if this helps/is an issue or not, but when I check the source code being generated it looks like a space is being inserted:

<a href="http://localhost:8888/tags-proto/projects/tag:website%20design">website design</a>

Since the tag is urlencoded, you need to compare to the urldecoded value:

if ($tag === urldecode(param('tag'))) { continue; }
1 Like

Yeah I was doing some testing and I was surprised to see that the url() doesn’t actually run the param through Str::slug() first.

I’d personally remove the urldecode() in the controller and do this in the template

url('projects' , ['params' => ['tag' => Str::slug($tag)]])

I don’t think that’s a good idea, the url() method already urlencodes the parameters, why should you sluggify them (given that there is no unsluggify, and you would have to change the filter method in the controller to also filter by sluggified values)

It’s fundamentally the same thing I think. Either you simply use url() but then you have to url decode/encode or you convert to slug and go with that. I personally find it easier to work with slugs that with encoded urls but I don’t think it makes a ton of difference in this case.

Added bonus are cleaner urls because this:

/tag:My%20Long%20Tag looks a lot worse than this test/tag:my-long-tag IMO

Thank you to texnixe & manuelmoreale

Tags are now working as expected and thread marked as solved.

I really appreciate your assistance with this. :smiley:

1 Like

/tag:My%20Long%20Tag looks a lot worse than this test/tag:my-long-tag IMO

IMO too.
I personally prefer the latter (cleaner) URLs.

As I already wrote above, the url method, when passed the parameter, takes care of url encoding for you automatically. Of course, you can work around this by passing a sluggified tag, which urlencoded will look the same as not urlencoded.

However, if you choose to sluggify the tag (which does look nicer, yes), but then your filter would have to look like this in the controller, otherwise it won’t work:

// sluggified parameter
if($tag   = param('tag')) {
  $projects = $projects->filter(fn ($child) => in_array($tag, array_map(fn ($tag) => Str::slug($tag), $child->tags()->split())));
}

In rare cases, where different word combinations have the same sluggified version, this might have wrong results.

Ok… I’m a wee bit confused now (sorry).

Currently I have the following:

Controller:

<?php

return function($page) {

  // fetch pages
  $projects = $page->children()->listed()->sortBy('date', 'desc');

  // add tag filter
  if($tag   = param('tag')) {
  $projects = $projects->filter(fn ($child) => in_array($tag, array_map(fn ($tag) => Str::slug($tag), $child->tags()->split())));
}

  // fetch tags
  $tags = $projects->pluck('tags', ',', false);

  // apply pagination
  $perpage    = $page->perpage()->int();
  $projects   = $projects->paginate(($perpage >= 1)? $perpage : 6);
  $pagination = $projects->pagination();

  return compact('projects', 'tags', 'tag', 'pagination');

};

and project template:

<?php if(param('tag')): ?>
<h1>Metadata</h1>
		<h2 class="aggregate-tags">Project<?php if($projects->count() > 1) echo 's' ?> tagged as <span class="tag"><mark><?= $tag ?></mark><a class="close" href="<?= $page->url() ?>" aria-label="All projects">&times;</a></span>
			</h2>
			
<div class='all-tags'>
  	<?php $tags = $page->children()->listed()->not($page)->pluck('tags', ',', true);
				foreach ($tags as $tag): if ($tag === urldecode(param('tag'))) { continue; } ?>
  				<a href="<?= url('projects' , ['params' => ['tag' => Str::slug($tag)]]) ?>"><?= $tag; ?></a>
		<?php endforeach ?>
</div>

		</header>
		
		<ul class="tag-grid <?php echo str::slug($tag)?>">

I’m back to the original issue - selected two-word tags still appear within aggregate tag list.

However, selected/active two-word tags now display sluggified, which I don’t want. I prefer the selected/active two-word tag to display with a space.

Ok, Changed back to:

Controller

<?php

return function($page) {

  // fetch the basic set of pages
  $projects = $page->children()->listed()->sortBy('date', 'desc');

  // add the tag filter
  if($tag   = urldecode(param('tag') ?? '')) {
    $projects = $projects->filterBy('tags', $tag, ',');
  }

  // fetch all tags
  $tags = $projects->pluck('tags', ',', false);

  // apply pagination
  $perpage    = $page->perpage()->int();
  $projects   = $projects->paginate(($perpage >= 1)? $perpage : 6);
  $pagination = $projects->pagination();

  return compact('projects', 'tags', 'tag', 'pagination');

};

Template

  <?php if(param('tag')): ?>
<h1>Metadata</h1>
		<h2 class="aggregate-tags">Project<?php if($projects->count() > 1) echo 's' ?> tagged as <span class="tag"><mark><?= $tag ?></mark><a class="close" href="<?= $page->url() ?>" aria-label="All projects">&times;</a></span>
			</h2>
			
<div class='all-tags'>
  	<?php $tags = $page->children()->listed()->not($page)->pluck('tags', ',', true);
				foreach ($tags as $tag): if ($tag === urldecode(param('tag'))) { continue; } ?>
  				<a href="<?= url('projects' , ['params' => ['tag' => $tag]]) ?>"><?= $tag; ?></a>
		<?php endforeach ?>
</div>

		</header>
		
		<ul class="tag-grid <?php echo str::slug($tag)?>">

And tags work as expected. However, I would prefer those cleaner URLs that Manuel mentioned.

If you want to do it the @manuelmoreale way:

<div class='all-tags'>
  	<?php $tags = $page->children()->listed()->not($page)->pluck('tags', ',', true);
				foreach ($tags as $tag): 
                                   // compare the sluggified $tag to the tag parameter and skip if same
                                   if (Str::slug($tag) === param('tag')) { continue; } ?>
  				<a href="<?= url('projects' , ['params' => ['tag' => Str::slug($tag)]]) ?>"><?= $tag; ?></a>
		<?php endforeach ?>
</div>

And in your controller the code I posted above:

// ...
$projects = $page->children()->listed()->sortBy('date', 'desc');

  // add the tag filter
if($tag   = param('tag')) /** $tag will now contain sth. like "hidden-champion" while your stored tag is "hidden champion" */ {
  $projects = $projects->filter(fn ($child) => in_array($tag, array_map(fn ($tag) => Str::slug($tag), $child->tags()->split(','))));
}
//...

This more complicated filtering code is necessary, because you cannot de-sluggify the parameter (like you can with urlencoding and urldecoding).
(Should work, but not tested)

1 Like

Thank you so much @texnixe, this works exactly as I require.

Ideally I’d like the URL to be a slug but the actual visual tag to display with a space.

Your solution - URL and site’s active (rendered) tag are both slugs.

Ideal solution - Only URL is a slug. Active (rendered) tag is not sluggified.

I’ve been experimenting with your solution and urlencode/urldecode but it looks like I can have one (slug) or the other (space in URL) but not both?

That being said, I can happily live with this and manually remove the slug from the visual tag after I export as static and before pushing.

Solved! :smiley:

Again a big thank you to both @texnixe & @manuelmoreale for your kind and knowledgeable assistance. Appreciated! :slight_smile:

Yes, that’s a problem with the sluggified tag. But there is a solution (I left some stuff out of the controller and changed the order a bit;

$tags           = $projects->pluck('tags', ',', true); // we only want unique tags, use `true` as last argument
$sluggifiedTags = array_map(fn ($tag) => Str::slug($tag), $tags);

if($tag   = param('tag')) /** $tag will now contain sth. like "hidden-champion" while your stored tag is "hidden champion" */ {
  $projects = $projects->filter(fn ($child) => in_array($tag, array_map(fn ($tag) => Str::slug($tag), $child->tags()->split(','))));
$keyOfSelectedTag = array_search($sluggifiedTags, fn($sluggifiedTag) => $sluggifiedTag === $tag);

$unSluggifiedTag = $tags[$keyOfSelectedTag] ?? null;
}

return [
  'projects' => $projects,
  // ...
  'unsluggifiedTag' => $unSluggifiedTag ?? $tag,
];

(not tested for typos, missing braces, or if it works)

Then where you output $tag in the above quoted code, you replace that with $unsluggifiedTag (or give it a better name)

1 Like

Thanks again @texnixe, very much appreciated. :smiley:

I’ll experiment with the new code you very kindly composed and see how I get on with it. Either way, I’m happy with the solution. :slight_smile: