modiimedia / contentful-hugo

A CLI tool that pulls data from Contentful and turns it into markdown files for Hugo and other static site generators. It also includes an express server that can be used for local development and content previews

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.