Amid the WordPress drama, Bryant builds a multi-site CMS system that lets you manage content for multiple websites from a single Directus instance. Watch as he sets up relationships between sites, pages, and users, then configures advanced permissions to ensure content editors can only access sites they're assigned to—all while creating a secure API structure for your frontend applications.
Speaker 0: Welcome back to the next episode of 100 apps, one hundred hours. I'm your host, Brian Gillespie. And today, we are going to be building a multi site CMS. It is been impossible to ignore the WordPress drama over the last month or two, and that has left a lot of folks wondering, hey, should I be switching our CMS from WordPress? WordPress has the multi site mode, so we're going to walk through how to build that today with Directus.
If you are new to 100 apps one hundred hours, there are only two rules. Number one, sixty minutes to plan and build, nothing more, nothing less. Everyone knows the rules. And, the second rule, the anti rule, one that I'm definitely going to invoke today, use whatever you have at your disposal. So let's dive right in.
We're gonna start the clock. Sixty minutes on the timer. Let's talk about multi site CMS. So what are we trying to do here? We want to manage content for multiple sites.
That could be two, that could be three, it could be 25, it could be 50 from a single CMS instance. So we will share some of our data models amongst those sites, but the actual content will be different. So, as far as the functionality that we're looking for out of the system, let's just mock that up. Functionality. Right?
We want to be able to manage pages and posts and other types of content for multiple sites within one CMS. We wanna make sure that there, are users can only edit and access data for sites that they are assigned to, owners of, assigned to, etcetera. That's the main functionality that we are looking for. Right? So one of the other things that I always do on these is basically map out our data model.
Right? And if we're going after WordPress here, we have, like, pages and we typically have posts. Those are kind of the two pieces of content that, you know, when I think of WordPress, I think of, okay, here's what's inside, the data model for that. But we're also gonna need to go up to a higher level. Right?
Because each page, each post is going to belong to a certain site. Now in this scenario, I'm thinking each site or each page only belongs to a single site and if you need to duplicate a page for another site, you just create a a entire copy of it. But, you know, you could potentially set this up if you are sharing the same page across multiple sites. You could model that relationship as well. You know, that's one of the advantages of Directus as a CMS is that you can model your data however you want, and your table structure inside your database will follow suit.
Directus just makes those edits for you. And then, you know, we're going to need a relationship to users. Right? So users are going to be kind of a mini to mini relationship. Right?
Love drawing these arrows. We need an arrow over here. And, let's just say mini to mini. Right? One user could be working on three, four, five sites or could be a single site, and, you know, we could have many users working on the same site.
Alright. So as far as our data model, that's that's pretty good. You know, we'll dive into the nitty gritty details in just a moment, but I'm definitely gonna invoke rule number two here, and I'm going to go into Directus Cloud. And you can call this cheating. You can call this taking advantage of what we've already built previously.
But we are going to do a new project using our simple CMS template because it gives us most of the functionality we need right out of the gate. So we're gonna call this multi site CMS test or 100 apps. That's good. See how long we can get the URL for this particular project. And then I'm just gonna click simple CMS here.
I'm gonna switch this to monthly, and I'm gonna check out. And this will spin up our project for us. So, if we think more along the lines here of of what we actually need to do, You know, this simple CMS template also has forms. It has navigation, and I'll show you that in just a moment after we get it fired up. And then we have a global settings collection, inside there, which is actually, we're gonna have to do a bit of work to that.
Can't get my arrows straight here. Jeez. Okay. There we go. Alright.
So those all roll up to the sites. There are many users, many sites. We're just gonna wait for this Directus instance to spin up. And I'm assuming I've probably already got an email with this thing. Great.
We'll just copy this. And feel free to steal this password here because this project should be turned off by the time we get to airing this actual episode. Alright. So we're just waiting for the CDN. Are we ready yet?
We are ready. Okay. So great. Now we've got our login. Let's just go ahead and fire this up.
We'll get logged into our simple CMS instance. And let's just kind of go through this actual CMS. I'll zoom way in and we can kind of dive through the content here. So we've already got some pages. We've got posts.
We have forms, form submissions, navigation. So within pages, we can see we've got standard stuff, title, a permalink, what the status is. We've got some blocks here, of different content. So this is using the mini to any relationships inside Directus. I can drag and drop this to build a page dynamically, which is nice for our content editors or the marketing folk that are actually working on the content for the site.
Then we have our blog posts. So lots of bunny related content here. We've got slugs. We've got status. Is this published?
Yes or no? We got description, author, actual content. There's some AI features baked into this template. It is, a really nice starter kit for any Directus project if that's what you're looking to do. We got form fields.
Great. There's a newsletter sign up, we need a first name, we need an email address, etcetera. And then, you know, every site needs a menu. So here we have some navigation, pretty cut and dry. And then there's the globals collection where we're storing settings that are going to be per site in our specific use case like logo, favicon, favicon, five icon.
Great. Never know how to say that one. And we will dive in. Right? So we've got pages.
We've got, you know, I could go in and query this particular content. So if I just drop this admin off the URL and I go to pages, we could query this content. If I wanted to see the individual content of the blocks, I can add a fields param to this and just do an asterisk, which will essentially act as a wildcard. It'll give me all the root level fields, and then I'm gonna say blocks, and I could do blocks dot collection, blocks dot item, And I can see I can start to fetch all the content within, and and I could keep drilling into this if I wanted to. But that doesn't cover our use case of multisite.
Right? So the big glaring problem with all of this is we don't have sites. So let's go in, we're going to create our sites collection. We'll go to the settings, we'll just create sites. I'm going to use a generated UUID for this.
We can add some of these in the canned system fields that add functionality like timestamps and users automatically. I'm just gonna leave this blank for now. And we're gonna give this site a name. And let's add a little hint for the user here. So I'm going to go to Advanced Field Creation Mode.
I'm going to go to the Field tab and we're just going to say the internal name for this site. Great. That'll give us a hint. And voila, now we have sites. I'm gonna give this a nice looking icon, maybe a globe.
That stands for, like, website. Right? We'll drag this up to the very tippy top. I've got globals as well. Maybe we need to change the icon.
Let's do, like, settings or something. I used to be a designer. I like to refer to myself as a recovering designer, so, icons and user experience and small little details like that matter a lot to me. So let's go ahead and create a new site. Right?
We're gonna do Agency OS. That's one of our other, open source projects that we released. And this is gonna be site b. Right? The other site.
The site that I don't like. Site b. Don't even care about it enough to give it a nice name. Alright, so now what do we do with these specific sites? Right?
Now that we have a site, maybe we want to go in and establish the relationship between all the different pages and things. Let's start with our globals though. Alright? These are settings, like the title, the URL, the logo, the favicon. Some of these we're gonna use for SEO.
Some of them, maybe in the the footer like social links, things like that. We want to have these per site. Alright? So one of the nice things about Directus is this visual data modeling. I can come in and I can copy these fields across collections.
So we will go to sites. We're gonna create a duplicate of that title field. I'm gonna duplicate the URL. I could even get fancy if I wanted to and potentially create, like, a flow to duplicate these things. You'll notice here on the ones that are relational, like, this is based on a many to one relationship.
I cannot duplicate those fields just because the underlying schema changes that happen. But just regular any other fields that are not relational, I can easily duplicate. So we'll do that. We'll add our description. Copy that over.
And let's do our social links. Now you'll also see there's this credentials group, like our open AI API key. Gosh. That's a mouthful. I wish, I wish their name was a little bit different because I'm dealing with these API keys in a lot of episodes, but I digress.
So these things are probably gonna be they're gonna stay global. Right? We don't actually need to remove those. So here, let's just remove title. Let's remove URL now that we've copied it.
The logo is just a nice little divider there. We'll remove the favicon. We'll delete all of these fields that are no longer global in our data model. And today's exes episode is is really just a, a big exercise in data modeling. Alright.
So now if I go to the globals, all we have is the direct Us URL. So we're gonna serve all the the content for those sites out of a single instance. That's good. You know, they would probably share an API key or, you know, we could set it up for where that API key is per site if we wanted to. But now we can see we have these things, like, agency OS, the best operating system for your agency.
Alright. We'll give this a URL, agencyos.dev. Run your business better. Alright. We can add social links.
Great. Cool. Gravy. Right? And now, I could go in and, you know, I just got this option here to copy the API URL, and now I could see that individual site.
So this is just my directus URL slash items slash site slash ID, or I can get a list of all the sites. What I don't see are my pages. Right? So how do we set up ownership of the pages and posts and things like that? So we want to use the relationships inside Directus.
We're gonna set these up. We're gonna go to our sites. And again inside our data model that we spec'd out, pages belong to a single site. So one page belongs to one site. Same thing for posts and all the way down.
So what we're going to do here is use the one to many field inside Directus. We're going to call this pages. The key is the name of the field inside the sites collection. We're gonna pick our related collection, which is gonna be pages, and then the foreign key will be the field that holds the site ID in the pages collection. So, you know, this might be site ID.
I prefer just calling it site just because of the way that you can grab that relational data in a single API call. Alright, so the next thing we want to do is make sure we set up our display templates. And in this case, you know, I could go in and pick the fields that we wanna display, but what I'm gonna do here, I'm gonna use the table layout instead. So I could show multiple columns here and kinda get like a a standard table view that I'm looking for. So we'll add the title of the page, the status of the page, and do we need any more than that?
Maybe not. Maybe we want permalink just because that could be dramatically different than the title. We will sort those by our sort order, and we're just gonna enable these other options to enable searching and to show a specific link. Okay. So now we've established a relationship to our pages.
What does that actually look like? Alright. Let's scroll down. I can already see that the UI is getting a little bit, messy as far as user experience. We'll tackle that in just a moment.
But here, now I can see my pages. I can create a new page directly from this site, or I can go in and add existing pages to that site. So I could say, okay. Hey. This site has a home page.
It has a privacy policy page. I can go ahead and add these other pages to this specific site. There we go. I can save and stay, and then I've got, you know, the ability to edit these pages. I can also navigate directly to the page from there.
So this is looking nice. One of the other things that I wanna do here though, because I care a lot about what my content editors are working with inside the CMS, we're gonna clean this up a bit. And I'm sure if you're watching this, you care as well. So there's an extension that I love to use quite a bit. It is called the group tabs group tabs interface.
I've already got this installed in the simple website CMS template. It is by mister Hannes Kuttner, which is a a member of our core team. I think he actually built this before he joined the core team, though. Great little extension. Not sure why my images aren't displaying here.
But basically, it gives me a nice tabbed interface. So what we're gonna do, we're gonna go to our sites data model, and I'm gonna look for the tab group option. Now this has got an option to actually fill the width of the page, so we'll take advantage of that, and I'm gonna save this. So this basically gives me a container where I can start dragging and dropping these other fields within, and we're gonna clean up this UI a bit. So let's see what that actually looks like.
Boom. Now I've got title, I've got URL, I've got tagline. Except, you know, maybe I want to nest all of these fields under one single tab, right, instead of having, like, 35 different tabs. So what I can do here, I'm gonna reach for another field type here called the raw group. This doesn't actually create any columns in the database or anything like that.
We're just gonna call this group branding, for instance. That's gonna be a key that we need to add, and then that gives us another container. And I could do things like this, where now I've got the title, I've got the URL, got the tagline, description, social links, and maybe we put pages up here ahead of those. So basically what I'm doing now is constructing the interface. So now we have pages.
We have all these fields that show up under group branding. That doesn't doesn't seem right to me. Right? That looks kinda funky. When somebody who's adding this, they're gonna say group branding.
What does that mean? So how can we control that? Right? The developer in me wants to say, hey. I need everything nicely name spaced and organized just for my own sanity, especially when I add more people to this.
But the designer, the UX person in me says, hey. This this doesn't make sense. So what we can do is go into edit that specific field, and under the field settings, there are the field name translations. This is one of the most underrated features in Directus, I think. So I could just give this a translation.
Right? And I pick the language. Of course, my user is set to the English language, but you could create translations for as many different languages as you have users. And Directus will display the correct one because when they log in, they can set up their default language on their profile. So I can work in English.
Alexander on our team, he can work in French. Some of our fine folks from Germany, they can work in German, and so forth. Whatever it whatever we need. Alright. So as soon as I edit that, nothing changes here.
But when I go back to the sites, now you can see that tab has a nice name that more aligns with what we're trying to achieve. Alright. So now that we have this relationship between pages, right, we keep moving on down the line. I'll go back to sites, and we're gonna add another one to many relationship for posts. That will be the related collection of posts, the foreign key, we're gonna use site.
We'll add a table for this. And let's say we want the title of the post, so we want the status of the post, and we want when this was published. You know, typically, blog posts are rolling through in chronological order. So now for our sort field, instead of, like, an actual sort field, we're probably gonna wanna do published at. And maybe we do that by descending order so that we show, the most recent posts first.
I can set how many pages or how many items per page that I wanna show in the table. And away we go, we can hit save. Great. So we'll add posts. Let's go back and hit one to many again.
We're gonna do forms. So we'll do related collection forms. We will do the foreign key of site. Keep with that same theme. For the forms, do we actually need the table view?
Yeah. Maybe we wanna add that. Right? So we've got title. We've got, the number of fields for the form.
Maybe we wanna show that information. The number of fields, what fields we have. Over the fields we go. Laughing all the way. How many submissions maybe?
Looks great. Cool. And now we'll do that. Now I know that we're gonna run into an issue with navigation because I'm the one who built this website CMS template and let me show you why. So when constructing this, the Symbol CMS template is made for a single website, not multi site, which we're building here.
So the problem is the primary key is set up for a single site. Basically this is just a string. Right? Because I wanted to be able to do something like this for our actual content where I could say, hey, item slash navigation, and instead of calling, like, a random UUID, I could just call main and I can get my main navigation. And again, using that fields parameter, I could go in and get all the underlying items for that navigation.
And I could go even further and do items dot children dot asterisk and get all of the children or even deeper. Right? Items.children.page. And then I can get the details of the actual page that we're linking to, like the permalink, so that I don't have to create brittle links that are gonna break as soon as soon as someone changes a permalink or a slug. Right?
But in my haste in doing that, I kind of limited us here as far as what we can do with this structure. Right? Because if this is called footer, right, I would venture to guess that a lot of the sites that we're working on are also gonna have a footer. So then we have to worry about creating unique names. So what we're gonna do here, I'm just gonna trash this entire navigation collection.
Boom. Away it goes. I could go into the actual SQL and update this, but for this purpose we'll just keep it nice and lightweight. We'll just create this collection again, And instead of using a string as the primary key, we're just going to use UUID. Perfect.
We'll give this a key. And let's make this and give it a key field. Yeah, just to be clever. Alright. We can even set this field to be indexed for database performance.
And maybe we want to slugify this thing as well so that it is friendly in URLs. The internal key for this navigation menu for this menu, I don't think we need to be super verbose here. I think there was an is active option there, so we'll just set that. Is this menu actually active for the site or not? And then I'm gonna go back and just like we did on sites, I'm gonna create a one to many relationship to our navigation items.
So the items are gonna be the individual links. The navigation is basically our entire menu. So navigation will be main menu or header or footer, and then within that we'll have the navigation items. Great. Alright.
For the foreign key is gonna be navigation. That's our parent collection. And you can always kinda see you can already see kind of the conventions that I follow where we have a singular and then a plural here for the the child relationship, just to try to keep things clean. Again, you could follow whatever convention that you want here. Totally up to you.
So then we'll have a title for those items. We'll show a link. We'll sort by the actual sort field that's an integer. And if I expand this a little bit, you know, we can go in and set up our display templates, which we'll wanna do just to make sure that, when we're working inside, like, a a layout, which is like the table or the kanban or whatever layout you prefer to edit this, that that information shows, the actual title instead of random UUIDs. Alright.
So, we're getting close there. Let's just add a menu icon for this and put this back up into our list. We'll put navigation items below that. Great. And now we can resume what we were doing.
Go back to sites. We'll do a one to many relationship here. This will be navigation. Could call it menus. That's the hardest part of doing any of these things is what to call the names.
But we'll just follow the same convention. Navigation. There's the site. Here's the menus. We're gonna show the key.
Maybe we wanna show the number of items there. Great. We're gonna sort by nothing. Great. Alright.
Now let's take a look at our progress. I'm kinda curious where we're at on the timer. We're about half an hour in. Still got a good way to go. But as far as what we're working with, and I can already see, right, there's our display templates that we need to set up.
So I'm seeing basically the UUID of all the pages, and that's not what I wanna see. Right? So I'm gonna just go to pages here. You can see I've got my interface, which is the actual form detail. That's where these things show.
The display shows up in that layout, so the the list of all the data. Right? We're gonna choose to show the related values, and I can add whatever fields here I want from pages. So maybe the title and the status of that page. Great.
We could do the same thing for our blog posts. We wanna show the title and the status. Maybe the same thing for the form. We just wanna show the title of the form. And for our navigation, maybe we want to show the key and the number of items.
Let's see how that looks. Right. Now we get something that's a little more manageable. We can see, hey, here's all the pages. I can bounce out to a specific page right from this view.
I can go into the actual site. There's the name for this site. We can see we've got pages. We've got posts. We've got forms.
We've got navigation. We've got branding. This is starting to look wonderful. Right? We'll probably need a header for this site.
Let's see if we can copy some of these existing items. That's great. Let's create a new one. We'll call this the footer. Add some of these other items that we did not copy, contact us, privacy policy, blog, etcetera.
Great. Make that active. We'll make the header active. Great. We hit save and stay.
We can see okay. Here's the items within each one of those. Perfect. Everything's looking nice. Great.
So now we have this site structure. Right? And if I were to go to my Directus URL, I go to items slash sites, I can get all the info for the site. Site b is looking pretty lonely in this case. We'll sort that in a moment.
But here, there's lots of ways that I could get access to the information for this specific site. Again, I could go through and use the fields params here to fetch all the pages for the site. So pages dot asterisk, and I could see here's the same content I was getting at the pages endpoint. Now I could also, like if I were to grab the ID of this specific site, right, I could go through and get all the pages. So, we can see here there's a a site field where we're storing an actual ID.
That's great. Let's go in and how do we get back to the actual admin? Admoine. Sausage fingers get me every single time. But let's go in and add a page to site b.
Right? So we have site b. We're gonna create a new page for site b. Site b page. Cool.
We'll just go ahead and pretend like this is a great page. We'll publish it. Cool. And now if I go back and I refresh, right, we're seeing this page for site b. So how can I filter this out?
I could use our filter params for that. So I could do, like, filter. We're gonna do the site, we're gonna use underscore equals, which is our syntax for that. And oh, I lost my fancy stuff. So we're gonna run that again.
Filter site. Just wrap these in brackets equals this. And now you can see I only see the items for the agency OS site, and I don't see that page that I just created. Now this is great for us in a multi site CMS setup. Right?
I can have all this content, I can query it by the individual site, but let's talk about the problem here. So the problem is how do we lock down this content? So I'm just gonna copy this. Right? I'm gonna pull this up in another window, an incognito window.
We're gonna discard the changes here for site b. And I'm gonna go in and add our first user. So the scene is set. We've got our sites. We want our users to be able to start editing the content.
Alright. So this is gonna be our agency OS content editor. Great. This will be agencyOS@example.com. We're gonna do a real secure password of password.
Alright. So I'm gonna create this new user. I could also invite users. I'm gonna give them the role of team member, which has already has a couple policies, attached to it. So if we just look at that, we see that this person can they'll have access to the direct to studio.
They'll have, edit capabilities for our specific content. Right? So we'll just create this new user, and then I should be able to log in as that user over here. Example.com. Password.
Badda bing badda boom. And now I can see all of these pages. Right? And you're probably already picking up on the problem. We could just make it more visible here.
If we drag site over, that's gonna aggravate me. So we're just gonna go ahead and fix that in the pages section. Alright. I can go to site. I wanna display the related values.
I wanna show the name of the site. In the mini to one, we're gonna show the name of the site. Great. Great. Beautiful.
I can unhide this field on the pages if I want to. I'll show you what that looks like in just a moment. But now if I just refresh over here, you can see the problem. Well, number one, I don't have access to see any of the sites, but I can see the content for site b. And I can edit this even though I am not the content editor for site b.
So let's just go in and fix these permissions. Looks like I've got a few that we need to fix. We'll just add site access. And we'll just set all of these up for all right now just to get back to, healthy baseline of this. We don't want folks to create sites.
We don't want them to delete sites. We're gonna leave that for the admin. And then I'll just hit save and stay. Alright. So now I'll be able to see sites up here.
And again, you can see the problem is that I could go in and I could change the title of this to Site B Sucks. Not cool. So how do we actually lock this down from being edited? Right? Directus gives us a great way to do that with custom permissions.
And this is a little complicated the first time that you encounter it, but once you understand what's happening, it is incredibly powerful. So here we are on our access policy for content editors. Right? Anybody with this policy attached to their user directly or to their user role will have edit access for any of these things that we've set up. And these are just the individual collections.
Right. And one way that Directus protects your data when you add a new collection, we don't give access rights to any of the those new collections by default. We like to keep things secure. But, the AgencyOS folks should not be able to see content for Site B. So what do we need to do?
Right? We need to establish a relationship between site and the user. So to do that, we're gonna go back to our sites, and in this case we're gonna use that many to many relationship that we already talked about. We're gonna create a new field inside the sites collection called users. The related collection will be direct us users.
We don't want to allow duplicates. We're gonna show a link. Great. We'll just go ahead and create this. Actually, I'm gonna delete that back up.
Forgot to explain what's actually going on. In that many to many relationship, basically it creates a junction table for us. So let's try again. We'll do sites, our users, direct us users is the related collection. And instead of hitting save here, I'm gonna open up this advanced mode, and now you can see what's going on underneath the hood.
Right? There's a junction collection that's being created inside our database. I'm gonna call this siteusers. By default this will get auto populated. You can go in and change it.
Right. So I'm gonna clean up this convention just a little bit. So we've got the junction table will be siteusers, the site ID will be stored under site, the directus user ID will be stored as user, and then one important piece here is to add this corresponding field to the directus users collection as well, because we're gonna need to use that in our permissions. So we'll add a sort field, and the relational triggers here just let us set up, actions based on what happens to this specific thing, right. If we deselect the site users or we delete the sites, we delete one of the directest users, we probably wanna keep this thing clean and delete the record inside the junction table.
Great. Alright. So what did we actually achieve here? So if I go into sites, we go to AgencyOS, I can now add that AgencyOS content editor to that specific site. Okay.
Great, Bryant. Why can he still edit all of the content in site b? That's where our access policies come into play. Right? We're running on twenty minutes here.
We still got plenty of time to set this up. What we're going to do now is restrict access to all of our content. So we've got all the relationships set up. We're ready to lock this down. What we're gonna do, we're gonna go into our sites.
Right? The first thing that we wanna lock down if you're not part of site b, you shouldn't be seeing site b. Right? So we'll go into our read access. This has full read access.
We're gonna use custom permissions to scope this down a bit. So we're gonna drill into our users, and this is our junction table, right? So the user, users dot users equals dollar sign underscore current user. So basically what we're saying is if the user is part of this site and they are then able to read information from that site. So why is that not showing up?
Oh, we probably didn't set up permissions for the junction table. There we go. Okay. We don't want anybody to be able to adjust that. Okay.
So now I can see all the users within a certain site for only sites that I am a part of. Right? So I can no longer see or search or find Site B for this example user that's logged in. Right? The AgencyOS Content Editor, they cannot see Site B.
Now if I go into site b and I give them access to site b and adding them as a user of site b, refresh over here, boom, site b shows up. I remove them. You can now not get access to site b. Right? One problem though, while I don't have access to the actual site, I can still see the pages for site b.
So I here's where a bit of the tedious part comes in, where I just go in and actually edit our permissions for all of these different items. Right? So we wanna be able to edit, create new pages, but when it comes to reading and updating pages, we want to make sure that those are scoped properly. So the site, we're gonna go back to that junction table. Right?
Site users, user equals current user. So because we have a relationship to the site from pages, we are now able to check all the users within the site and make sure that the ID for those includes the current user ID. And what I'm gonna do here, I'm gonna just copy this rule, and I'm gonna go through and paste this rule into the item's permissions item permissions. And I could go through and do that for other things as well, like our post collection. Where are you posts?
Right? Copy paste this rule. Again, we'll add the rule there. We don't want them to be able to delete pages that don't belong to their site. Same thing for posts.
And, you know, depending on your scenario, right, whether you're setting up multi site for different clients or different teams within a larger organization or, yeah, some combination thereof, you've got total control of this by creating different access policies, different roles. So the access policies I love, because I can get really granular about what what content I want to make available to read, update, create, etcetera. Alright. So for the sake of time, I'm not gonna go through and adjust all of these things, but if I just hit save and stay, now I should not be able to edit any content for site b or even see it unless I'm part of that site. And that applies for the API as well, right.
So if I go in and I do items slash pages, we make this pretty print, I don't see any of those pages. Right? And just to prove this to you, we've got a page here that belongs to site b. If I copy the ID of this, and remember I'm logged in as the content editor for site, AgencyOS over here, right, I'm gonna get a error that I do not have permission to access this specific page. So great.
That works really nicely, Bryant. Cool. How do we actually implement this on a, like, front end scenario? Right? How should we set this up?
And one pattern that I follow quite a bit is creating a bot user for my front end. Right? So now that we've got this data model set up, everything is flowing permission wise the way that we want it, right we would go through and create our front ends. So you could have your front ends all in Next. Js, Nuxt, whatever front end framework you prefer.
You can even have different front end frameworks. You're gonna wanna fetch that content and either generate your front end statically, dynamically, server side rendered, incremental static regeneration, all the different ways of rendering your content, you know, all available to you via the API. But one pattern that I like, I will go in. I'm just gonna create a new role. I'm gonna call it bot.
And this is just a role for our website front ends to access the API. Alright, so within roles we are going to add content policies. Right? So we don't want our front end to actually be able to edit the content, we just want it to read content. Potentially we might want to let it upload files or submit forms.
But for now, let's just keep this simple. We are going to let it read content. And one of the other roles that comes with Directus out of the box is the public role. So the public role controls what API data is available without authentication. Now usually I will take advantage of the public role, and you can see we have different policies here.
So I can let the general public read all the published content. That's okay. In this multi tenant scenario, you might want to lock that down. Right? So I'll just remove all access for the public role.
And if I get really crazy with this, go to Safari, we go to items, we go to pages, right, now I don't get access to any of the content. Right? I cannot have access to pages. But, if I am still logged in over here, we can get access to our pages. Right.
Right. And, again, we're only seeing the page data for agent c o s. Cool. So we've created this bot user or this bot role. Let's take a look at our read policies.
We'll just go ahead and fix all of these other collections that we would need to render on the front end. Probably the navigation. Great sites. Cool. And again, you can see some of these are already, kind of partial.
50% opacity here just highlighted where it says partial read access. Let's take a look at, like, pages for instance. I'm gonna save this really quickly though. So let's take a look at pages. Right?
The rule for pages is that it has to be published, and the published date has to be now or before now. Makes sense, right? But we wanna add one more stipulation to this that any of these bot users can only access the sites that they have access to. So again, we can use that same setup where we go into the Junction collection, we use the current user. And with these bot users, I would highly recommend just creating one bot per website.
So if you've got 50 websites, you create 50 bot users, that will be able to give you a token that will only, be able to access data for that specific site. And that way you don't have to keep passing something like site ID equals x y z in the parameters or every single call. Because that can get tedious, honestly. So here we're gonna adjust posts to do the same thing. We want the site, we want the underscore users user equal current user.
And again, this is just a dynamic variable. Directus will populate that user ID for you there. Did we do that for pages as well? Let's do it for pages. Where are you?
Sites. Tabs, users. Current user. Great. And if I want to, like, copy and paste these entire rules, these are just the standard filter rule syntax.
If you check out docs.directus.io, you can find all of these things. Right. Great. Okay. So now we've got that locked down.
Let's go in and create a new bot. Right? Let's say bot, site, site b bot. Crack myself up sometimes. And here, like, basically, we don't need an email and password.
These folks aren't gonna log in to the data studio. Right? This is just gonna be a user for our front end to access the data. So in this case, we're gonna add that bot role. Cool.
So I can know how many of these bots that we have. One other thing to note is that I don't even have to create a separate role. I could just give them read access, through the policies. So you've got a lot of flexibility there, and, you know, maybe you've got that one content editor who needs to do a little bit more than the others. Instead of having to create a separate role for them, just give them the policy that they need.
What I'm gonna do here is create a static token. This just allows us to access it and we're gonna add this bot to Site B. Alright. Cool. Great.
Save. Now we've got this user. And, cool. So now, what are we gonna do? I want to just demonstrate this.
I don't think we're gonna have enough time. We've got like ten minutes before the hour's up. We're probably not gonna be able to dive into anything front end for that'll be tremendously helpful. Right? But let's just pretend like we're doing this on the front end.
Documents. Great. We'll create a multi site collection here. I'm just using this HTTP client called Bruno. Usually, we do not talk about Bruno, but in this case, we are talking about Bruno.
So I will grab the URL here from our API, and I'm gonna pretend to be the site b front end here. So we're gonna try to fetch our pages for site b. If I just make this full screen, I should get an error. Right? We do not have permission to access this.
Rightfully so, we disabled all public access. But now if I go in and I add my token, right, now without having to add a param for site or or any of this mess. Right? I am only getting the content for site b because of the relationships and the permissions that I set up. So this bot user is a part of site b, and we can fix that to actually display this.
So we'll go into direct us users. We'll go to sites. We want to display the site name. We'll display the site name in our display templates. Great.
So now if we go back to bot for site b, alright, we can see the sites that they're available to. Now, if you're creating a lot of sites, you know, in this process of creating bots for each site are kind of cumbersome. Anytime you create a new site you could potentially run a flow that creates this bot user automatically, etcetera, so that you don't have to mess with this. It just depends on how many different sites that you're managing and what that setup kinda looks like. But here you can already see, let's go in and, you know, if I add the bot to the AgencyOS site, I save that.
And if I rerun this API call, you can see now I'm getting all of those pages for the AgencyOS site as well, which is not what we want in this case. But, that way we can lock down the data that each one of these folks have access to. And this could be an unlimited number of front ends. So I hope that is it for this one. That is creating a multi site CMS.
Right? Just to recap, we've got a sites collection that we created. We shifted all of our settings for the site into the actual sites collection themselves. Then we have relationships for pages, posts, etcetera, and we set up a bot user to access that for our front end. Right?
On the front end, we would then just set up an API call to the direct us API. Not gonna get into that. That is gonna be opening a can of worms with five minutes left, and I don't wanna disappoint myself. So with that, multisite CMS, we're calling that a wrap. That's a win for this episode of 100 apps, one hundred hours.
I hope you'll tune in to the next one, and make sure you check out our documentation for more insight on how to set some of these things up. I'll see you.