Resolving deeper components
initrode opened this issue · comments
I've set up a content model based on a Page type comprised of blocks within those pages:
Page
|
+ - - heroBannerBlock
+ - - featureBlock
+ - - pageContentBlock
Each page has subcomponents
Is there a way to do a full resolution of the nested components contained within all those block types without having to iterate through each type and extracting the fields manually (and extremely painstakingly) - using an override
loop only lets me retrieve component.fields
at a depth of 1 - for example, my featureBlock
contains nested cards
components - they just resolve to the top level id
field only
Example result
type: "cardBlock"
component:
sectionTitle: "Card Section"
sectionLink: "card-section"
cards:
- sys:
type: "Link"
linkType: "Entry"
id: "2SjfeKW1NtXrpOcCZFrZaw"
Thanks
Hey @mjp80 yeah there isn't a way to do that right now. Although the prospect of auto resolving the fields of nested entries is interesting. I'd have to think about what that would look like (or even if it's something we would want). Maybe if the config provided a way to specify how many levels deep you'd like to go that could be cool.
There are also some considerations to keep in mind with server mode, such as how to go about detecting when a parent entry needs updating after a deeply nested subcomponent has been updated.
How To Solve This Issue With Existing Tools
Concerning dealing with nested components and subcomponents, I've actually done multiple projects with a similar setup to yours. I personally think the best way to handle this is breaking up your nested entries into partials. You will still have to fetch the fields with .Site.GetPage
but now you're doing it in isolation, which makes things way easier to maintain and extend imho.
I'll go ahead and share some examples from a recent project, and try to provide comments to explain what's happening. Hopefully it will be helpful to you. The core concepts revolves around using dict to pass parameters down to partials and sub-partials similarly to how you would when passing props in Vue or React.
Page Markdown File
Here's an example of what a pages markdown file would look like. It's pretty similar to yours except I used the term "modules" instead of "blocks"
---
# content/[entryId].md
# rest of fields
modules:
- id: "43EosPU4d7BcrW7HqMRyoR"
contentType: "moduleFeatureCardBlock"
- id: "6djx9a68Pt6BiP1UHPN4j9"
contentType: "moduleImagesAndContent"
- id: "79wzwe7AeyxivOUtrLKLu4"
contentType: "moduleImagesAndContent"
- id: "6fkC9KJQvRG2rUG1IRr0SF"
contentType: "moduleHeadingSeparator"
- id: "64toU1a15YmvzFxK5YkyVd"
contentType: "testimonySlider"
- id: "3LNTHMrX4eP5sHMUPcItmG"
contentType: "moduleContent"
---
Page Single Template:
In the page single we loop through the modules field and just send the values to a generic module partial. Dict is used to send the $.Site
context and the module context (id
, contentType
). This is important because nested partials don't always have access to $.Site
which we need in order to run $.Site.GetPage
// layouts/page/single.html
{{- range .Params.modules -}}
// nested partials often don't have access to $.Site
// so dict is used to pass down the $.Site context and the local context
// which is being named "module" in this case
{{- partial "modules/module" (dict "site" $.Site "module" . ) -}}
{{- end -}}
Generic Module Partial
This partial just a list of if-statements to decide which partial to render depending on the .contentType
. You could include this logic directly in the page single template if you'd like. However we ended up doing this because our homepage template also used modules and we didn't want to copy and paste the same conditional logic.
// layouts/partials/modules/module.html
// contentType is now accessible at .module.contentType
// and id is now accessible at .module.id
{{ $type := .module.contentType }}
{{ $id := .module.id }}
<section class="md:my-28 my-16">
<div class="container">
// render a partial depending on the contentType and use "dict" to pass the site context and the entryId
{{ if eq $type "testimonySlider" }}
{{ partial "modules/testimony-slider" (dict "site" .site "id" $id) }}
{{ end }}
{{ if eq $type "moduleFeatureCardBlock" }}
{{ partial "modules/feature-card-block" (dict "site" .site "id" $id) }}
{{ end }}
// continue as needed
</div>
</section>
Component Partial
Since we've passed down $.Site
as .site
we can easily get the related fields inside this partial
// layouts/partials/modules/feature-card-block.html
// $.Site.GetPage is now accessible at .site.GetPage
// the entryId is accessible at .id
{{ $module := .site.GetPage (print "_moduleFeatureCardBlock/" .id) }}
// declare $site as variable here so we can pass it to a subcomponent
{{ $site := .site }}
{{ with $module }}
// page fields can now be accessed like normal
<div
class="{{ if eq .Params.style "boxed" }}module-box{{ end }}"
>
<div class="flex lg:flex-row flex-col items-center">
<div class="lg:w-1/3">
<div class="content leading-snug lg:pr-3 lg:mb-0 mb-5">
{{ .Content }}
</div>
</div>
<div class="flex items-center lg:w-2/3 h-full">
<div class="flex lg:pl-3 flex-grow w-full h-full">
<div
class="flex-grow flex flex-wrap -m-3 h-full items-stretch"
>
{{ $isEven := modBool (len .Params.featureCards) 2 }}
// featureCards is array of entry references so we do the same thing here as before
{{ range $index, $el := .Params.featureCards }}
{{ partial "modules/feature-card" (dict "id" .id "site" $site "isEven" $isEven) }}
{{ end }}
</div>
</div>
</div>
</div>
</div>
{{ end }}
Subcomponent Partial
Site.GetPage still works because we've been passing it down with "dict". In this example you'll see that I also go another layer deep and it's still no issue.
// layouts/partials/modules/feature-card.html
{{ $isEven := .isEven }}
{{ $site := .site }}
// this still works because we've passed it down with "dict"
{{ with .site.GetPage (print "_moduleFeatureCard/" .id) }}
{{ $link := .Params.link }}
{{ $linkUrl := "" }}
{{ if $link }}
{{ with $site.GetPage "_navLink/" $link.id }}
// going three layers deep still isn't an issue because we continue to pass $.Site around
{{ $linkUrl = partial "header/nav-link-resolver" (dict "Link" . "Site" $site) }}
{{ end }}
{{ end }}
<div
class="{{ if $isEven }}md:w-1/2{{ else }}md:w-1/3{{ end }} w-full p-3 relative {{ if $linkUrl }}transform scale-100 hover:scale-105 transition-transform{{ end }}"
>
{{ if ne $linkUrl "" }}
<a href="{{ $linkUrl }}" class="text-gray-900 hoverable">
{{ end }}
<div class="relative h-full">
<div
class="relative z-10 p-4 border-2 border-gray-900 rounded h-full"
>
{{ with .Params.icon }}
<div class="text-primary-600 mb-6 text-2xl">
<i class="{{ . }}"></i>
</div>
{{ end }}
<div>
{{ with .Params.heading }}
<p>
<strong>{{ . }}</strong>
</p>
{{ end }}
{{ with .Params.content }}
<p>
{{ . }}
</p>
{{ end }}
</div>
</div>
<div
class="absolute rounded bg-secondary-400 popover-background h-full"
></div>
</div>
{{ if ne $linkUrl "" }}
</a>
{{ end }}
</div>
{{ end }}
Anyway that's how I've generally approached this issue. It's not the ideal setup but I do find isolating everything into partials like this makes things feel organized. Hopefully you find it useful, and let me know if you have any other questions.
Wow, that's a great writeup. The full component resolution isn't really required with this approach (which is the same pattern I've been using in 11ty and others) Nice to see it here for Hugo. Thanks for taking the time, really appreciate it.
@mjp80
Hmmm so apparently I've been doing in a way that is more complicated than necessary. I just looked over the docs and there's a global site
method. That can be accessed anywhere even if there's no Page
object.
https://gohugo.io/variables/site/#get-the-site-object-from-a-partial
So you can just do site.GetPage
and don't have to pass it down with dict
. Learn something new every day I suppose.
I've added additional documentation about retrieving linked entry data within partials here: https://github.com/ModiiMedia/contentful-hugo#entries
I'm going to close this issue for now. I think with Hugo's global site
method a complete auto-resolution of linked entries isn't necessary and could prove to create quite a few challenges for getting updates in server mode.
However, if someone can make a good argument for implementing this I'm willing to open it up again.