As to your understanding of my use case for Components, for this use case, you are correct in your understanding. It is not a requirement that you use it in this way, that sort of depends on how you want to use Squidex Components in your own designs, if at all.
While I’m no stranger to web design and database architecture in general, I’m also very new to both Astro and Squidex as well. I’ve been learning as I go for sure. I’m using Astro in SSR mode.
If you want more clues as to how I designed the Squidex schemas:
- I created a Site schema which has for instance, siteName. There’s only one site, so I defined it as a single.
- I created a SEO component and included it on all pages and the site, and anywhere else I need a SEO component.
- I created schemas for my business data models, and components for parts of the business data.
- I created a Page as a multi. I created page components. Pages are for everything that isn’t a business data model but just a page on the site. In my case I have 3 pages: Home page, About page, and 404 page. Everything else is generated from business models.
- On the frontend I routed my URLs to particular Astro handlers for whatever base Schema would be rendering, based in part on a slug cache with references back to the underlying schema data.
To better understand why Components are what they are, perhaps we can peel back a layer from Squidex. Squidex is built as a .NET client to a MongoDB service. The underlying data we observe from Squidex is a projection of the MongoDB data. MongoDB is a noSQL database which, instead of storing relational data (like MySQL, Postgres, Oracle, MS SQL Server, SQLite, etc.) or key-value data (like Redis, etc.), it stores BSON documents. BSON is like JSON but with support for binary blobs of data in MongoDB. If we examine the MongoDB manual, we can see what these BSON documents look like.
So what a Component is, it’s like a defined reusable excerpt of the BSON data, as a complex type, for a portion of a whole MongoDB document, and for the purposes of creating an editor UI for the reusable excerpt of the BSON document. Embedding a Component is like creating an object structure within the object structure, like in this JSON:
{
"field": "value",
"component": {
"componentField": "value"
}
}
You can see how the introduction of the Components represent a portion of the overall document by inspecting the JSON document that Squidex projects for us. Notice how the data in the Component becomes an embedded part of the JSON output for the Page schema:
I don’t necessarily expect you will use Components in the exact same way I do. This was actually an approach I discovered in a YouTube video which was quite useful for me in understanding how people are using headless CMS in the wild. The video was about Strapi and I originally started out using Strapi and decided later I wanted to avoid the complexities of relational data.
Content Modeling - how to structure your data in Strapi | YouTube @ 12:48.
While this is for an entirely different headless CMS ecosystem, it is what I used as a design to replicate. The practical difference here between the system described in the video is that Components in Squidex are embedded portions of BSON into your overall MongoDB BSON document whereas Components in Strapi are different tables of the SQL database relational model, which introduces its own complexities. Also I liked that Squidex supports versioning for all single and multi schemas and that it supports custom workflows, which I can use as part of my automation processes.
I was able to replicate the use case from the Strapi video in Squidex. I will comment that building the frontend to adapt to the complexities of dynamic components is a lot of extra work on the frontend, with the effect being that you’ll have a UI to edit your site by adding and removing Components of a Page and the frontend can pick and choose which front-end components match up at that pivot point. There are perhaps other, maybe even better, ways to model your data depending upon the shape of things and how much data you’re modeling and who’s editing it and how frequently your page designs may need to adapt, etc. As developers we tend to create our own complexities, and this was one of the complex things I deliberately added to my design.
I also use a lot of Reference fields, which work similar to (but definitely entirely different from) relationships in a traditional RDBMS. Unlike a Components field, a Reference field is an ID reference to another Schema’s particular data whereas a Component represents an embedded portion of the Component Schema’s document. When you use references in Squidex or MongoDB, you’ll want to pay particular attention to which side you are putting your references on; as there are no two-way references as is the case in traditional relational data models.
The takeaway about Squidex Components is they represent an embeddable portion of the MongoDB BSON. There is some documentation about them on the Squidex docs as well, as you originally mentioned. Looking back at these docs, it should start to all make sense where Squidex Components fit into designing your schema models.
Schemas | Squidex
I have also contrived an example where within a component I have created a relation field. On the part of my Page where I can add components, I have added a Selected Brands List component.
Then I select the relations on the business schema.
You can see how this affects the JSON output from Squidex. You can see the embedded object within the object and the unique ID for the Brand reference.
Then when this renders on the front-end app, the component router knows what to do with it on the front-end.
Actually just kidding. It doesn’t know what to do until I build it.
Done!
case 'page-brands-list':
let brandsList = dynComponent as PageBrandList;
let brandsListDto = (await getBrandsByIds(brandsList.brands.join(',')));
let listedBrands = brandsDto!.items;
let listedBrandsHeading: { [key: string]: string } = {
'en-US': "Brands",
'es-US': "Marcas",
'fr-CA': "Marques",
};
return (
listedBrands.length > 0 && <h3 class="section">{listedBrandsHeading[locale]}</h3>
<ul class="link-card-grid brands" data-squidex-token={pageEditToken}>
{listedBrands.sort((a, b) => a.data!.brandName[locale].localeCompare(b.data!.brandName[locale])).map(brandDto => {
return (
<BrandCard
locale={locale}
editToken={brandDto.editToken}
brand={brandDto.data!}
/>
);
})}
</ul>
);
This demonstrates a Page Schema with a Page Brands List Component containing References to Brand, a business object.
So that’s kind of cool if you ask me. This demonstrates the way you can use both Components and Relations.
It’s important to think ahead and figure out which problem you are trying to solve.
Let me contrive another possibility. Say my business needs change and I can now sell brand slots on certain pages. I want to store each list of brand slots and then reference the list instead of create the list on each page. Then we refactor. I can leave everything in place while I do. I will create two new Schemas. One for the business model, brands-slots, and one for the page component, page-brands-slots so that I can pick which list we’re looking at on certain pages.
Save us a content for this campaign.
Then make the page component.
Add page-brand-slots to the page.content field.
Replace the Selected Brands List with the new page-brand-slots component.
Now we can see the reference to the brands slots, which has a list of brands.
Add all the glue and the feature to the front-end:
case 'page-brands-slots':
let brandsSlots = dynComponent as PageBrandSlots;
let brandsSlotsDto = (await getBrandSlotsByIds(brandsSlots.brandSlots.join(',')));
let brandsSlotsListDto = (await getBrandsByIds(brandsSlotsDto.items[0].data!.brands.iv.join(',')));
let listedBrandSlots = brandsSlotsListDto!.items;
let listedBrandSlotsHeading: { [key: string]: string } = {
'en-US': "Brands",
'es-US': "Marcas",
'fr-CA': "Marques",
};
return (
listedBrandSlots.length > 0 && <h3 class="section">{listedBrandSlotsHeading[locale]}</h3>
<ul class="link-card-grid brands" data-squidex-token={pageEditToken}>
{listedBrandSlots.sort((a, b) => a.data!.brandName[locale].localeCompare(b.data!.brandName[locale])).map(brandDto => {
return (
<BrandCard
locale={locale}
editToken={brandDto.editToken}
brand={brandDto.data!}
/>
);
})}
</ul>
);
Now we have the Page with a Component to a Referenced Brands List with a list of References to a Brand.
Think ahead about how you might want to use the data. But know there are many ways to combine the techniques.