Customizing the Contentful Slugger
Before we go further into customizing the look and feel of our site, I want to fix the skill URLs so that instead of just /mashing
, the page is /skills/mashing
. Remember: the fact that our Contentful content instantly has URLs on our site comes from the Contentful package we installed earlier. But that magic has nothing to do with Layouts. So, customizing this URL is also specific to Contentful, not Layouts. But... I really want to fix it.
Creating the Slugger Class
Over in the src/Layouts/
directory, create a new class called ContentfulSlugger
. Make this implement EntrySluggerInterface
... and then generate the one method we need: getSlug()
:
// ... lines 1 - 2 | |
namespace App\Layouts; | |
use Netgen\Layouts\Contentful\Entity\ContentfulEntry; | |
use Netgen\Layouts\Contentful\Routing\EntrySluggerInterface; | |
class ContentfulSlugger implements EntrySluggerInterface | |
{ | |
public function getSlug(ContentfulEntry $contentfulEntry): string | |
{ | |
// TODO: Implement getSlug() method. | |
} | |
} |
We're going to set things up so that this method is called when the dynamic URLs for all Contentful entries are being created. It will allow us to control the "slug", which is really the URL for each item.
To make life easier, use FilterSlugTrait
to get access to a method we'll use in a minute:
// ... lines 1 - 5 | |
use Netgen\Layouts\Contentful\Routing\EntrySlugger\FilterSlugTrait; | |
// ... lines 7 - 8 | |
class ContentfulSlugger implements EntrySluggerInterface | |
{ | |
use FilterSlugTrait; | |
// ... lines 12 - 20 | |
} |
Ok, on Contentful, we have both Skills and Advertisements. But we don't really want advertisements to have their own page. Unfortunately, with the Contentful integration, there's no way to disable URLs for one specific content type. I'll talk about how to work around that in a minute.
Anyways, this method will be passed both skills and advertisements. Use the new PHP match()
function to match $contentfulEntry->getContentType()->getId()
. That will return the internal name for each type, which you can find in Contentful. If it's skill
, return /skills/
then $this->filtersSlug()
- that comes from the trait - passing $contentfulEntry->get('title')
:
// ... lines 1 - 8 | |
class ContentfulSlugger implements EntrySluggerInterface | |
{ | |
// ... lines 11 - 12 | |
public function getSlug(ContentfulEntry $contentfulEntry): string | |
{ | |
return match ($contentfulEntry->getContentType()->getId()) { | |
'skill' => '/skills/'.$this->filterSlug($contentfulEntry->get('title')), | |
// ... lines 17 - 18 | |
}; | |
} | |
} |
For advertisement
, return /_ad
for all of them:
// ... lines 1 - 8 | |
class ContentfulSlugger implements EntrySluggerInterface | |
{ | |
// ... lines 11 - 12 | |
public function getSlug(ContentfulEntry $contentfulEntry): string | |
{ | |
return match ($contentfulEntry->getContentType()->getId()) { | |
'skill' => '/skills/'.$this->filterSlug($contentfulEntry->get('title')), | |
'advertisement' => '/_ad', | |
// ... line 18 | |
}; | |
} | |
} |
At least, at this point, only one ad could ever have a page on our site: if the user went to /_ad
, it would match the first one.
At the bottom, throw a new Exception with "Invalid Type":
// ... lines 1 - 8 | |
class ContentfulSlugger implements EntrySluggerInterface | |
{ | |
// ... lines 11 - 12 | |
public function getSlug(ContentfulEntry $contentfulEntry): string | |
{ | |
return match ($contentfulEntry->getContentType()->getId()) { | |
'skill' => '/skills/'.$this->filterSlug($contentfulEntry->get('title')), | |
'advertisement' => '/_ad', | |
default => throw new \Exception('Invalid type'), | |
}; | |
} | |
} |
So, yes, at this point, advertisements will still have their own page. There's no way to turn that off out-of-the-box. But if you care enough, I would map all advertisements to the same URL or URL pattern like this. Then I would create a route & controller with the same URL and return a 404. That route will take precedence over the dynamic one.
Tagging & Configuring the Slugger
To tell Contentful to use our slugger, we need to, of course, give it tag! Add #[AutoconfigureTag]
and this one is called netgen_layouts.contentful.entry_slugger
. This also needs a type
option... which you can set to any string. Let's use default_slugger
:
// ... lines 1 - 7 | |
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; | |
'netgen_layouts.contentful.entry_slugger', ['type' => 'default_slugger']) | (|
class ContentfulSlugger implements EntrySluggerInterface | |
{ | |
// ... lines 13 - 22 | |
} |
How is that used? In config/packages/
, we need to create a new config file for the layouts contentful package. Let's call it netgen_layouts_contentful.yaml
.
Repeat that for the root key. Below, add entry_slug_type
, then default
set to the type we used in our tag: default_slugger
:
netgen_layouts_contentful: | |
entry_slug_type: | |
default: default_slugger |
This funny syntax says:
For every content type in Contentful, use
default_slugger
when generating the URL. So, use ourContentfulSlugger
.
Ok, done! But... this is not called when we reload the page. Nope. This is called when we "sync" our content from Contentful. Ok, let's re-sync! At your terminal, run:
symfony console contentful:sync
This updates our local database with the latest data from Contentful... and it worked just fine. But when we run:
symfony console contentful:routes
The URLs didn't change! This is a quirk... or maybe a feature so that existing pages don't break. Either way, once a route is imported the first time, it's URL never changes.
The easiest way to reset things is to drop the routes table and reimport everything.
And, this is kind of fun. We can run:
symfony console doctrine:migrations:migrate current-1
That will reverse the most recent migration, causing the contentful and route tables to be dropped. Put them back with:
symfony console doctrine:migrations:migrate
Re-sync the content again:
symfony console contentful:sync
And now check the routes:
symfony console contentful:routes
Yes! The URL is /skills/mashing
! So, over on /mashing
, we get a good-old fashioned 404. But /skills/mashing
works.
Next: we don't yet have a page that lists all of the skills. Let's fix that!