Strapi V4 populate dynamic zones media not populating
Asked Answered
S

10

12

Data fields appear to be populating ok by using the suggested patch below, however, media fields are not populating.

Tried the following with no luck -

* http://localhost:1337/api/pages?populate=*
* {{protocol}}://{{host}}:{{port}}/api/pages?populate[Content][populate]=images

Reference - https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest/populating-fields.html#component-dynamic-zones

Example below is from - https://forum.strapi.io/t/strapi-v4-populate-media-and-dynamiczones-from-components/12670/9

Also tried this plugin (no luck) - https://www.npmjs.com/package/strapi-plugin-populate-deep

Page example below with dynamic zone -

enter image description here

/helpers/populate.js (this works for all data except images) -

const { createCoreController } = require("@strapi/strapi/lib/factories");

function populateAttribute({ components }) {
  if (components) {
    const populate = components.reduce((currentValue, current) => {
      return { ...currentValue, [current.split(".").pop()]: { populate: "*" } };
    }, {});
    return { populate };
  }
  return { populate: "*" };
}

const getPopulateFromSchema = function (schema) {
  return Object.keys(schema.attributes).reduce((currentValue, current) => {
    const attribute = schema.attributes[current];
    if (!["dynamiczone", "component"].includes(attribute.type)) {
      return currentValue;
    }
    return {
      ...currentValue,
      [current]: populateAttribute(attribute),
    };
  }, {});
};

function createPopulatedController(uid, schema) {
  return createCoreController(uid, () => {
    console.log(schema.collectionName, JSON.stringify(getPopulateFromSchema(schema)));
    return {
      async find(ctx) {
        ctx.query = {
          ...ctx.query,
          populate: getPopulateFromSchema(schema),
        //   populate: '*',
        };
        return await super.find(ctx);
      },
      async findOne(ctx) {
        ctx.query = {
          ...ctx.query,
          populate: getPopulateFromSchema(schema),
          // populate: '*',
        };
        return await super.findOne(ctx);
      },
    };
  });
}

module.exports = createPopulatedController;

/src/api/page/controllers/pages.js -

"use strict";

const collectionType = 'page'

const schema = require(`../content-types/${collectionType}/schema.json`);
const createPopulatedController = require("../../../helpers/populate");

module.exports = createPopulatedController(`api::${collectionType}.${collectionType}`, schema);

Response below images don't come through -

{
  "data": [
    {
      "id": 1,
      "attributes": {
        "title": "testing home page",
        "slug": "/",
        "publish_at": null,
        "createdAt": "2022-04-12T12:08:32.002Z",
        "updatedAt": "2022-04-12T15:07:11.990Z",
        "publishedAt": "2022-04-12T12:42:55.682Z",
        "locale": "en",
        "seoComponent": null,
        "block": [
          {
            "id": 1,
            "__component": "image.image-copy-full",
            "heading": "Delivering something amazing",
            "subHeading": "test sadasdf",
            "ctaButton": "test",
            "miscValues": {
              "testing": "object field"
            },
            "actionUrl": null,
            "isInternal": true,
            "isVisible": true
          },
          {
            "id": 1,
            "__component": "image.image-copy-chip",
            "heading": "A platform",
            "subHeading": "Allowing full integration",
            "ctaButton": null,
            "miscValues": null,
            "actionUrl": null,
            "isInternal": true,
            "isVisible": false
          },
          {
            "id": 1,
            "__component": "image.image-copy",
            "heading": "To transform our world",
            "subHeading": "In order to reach that scale",
            "ctaButton": null,
            "miscValues": null,
            "actionUrl": null,
            "isInternal": true
          }
        ]
      }
    }
  ],
  "meta": {
    "pagination": {
      "page": 1,
      "pageSize": 25,
      "pageCount": 1,
      "total": 1
    }
  }
}

Schema sample for "ImageCopyFull" on screenshot -

{
  "collectionName": "components_image_image_copy_fulls",
  "info": {
    "displayName": "ImageCopyFull",
    "icon": "file-image",
    "description": ""
  },
  "options": {},
  "attributes": {
    "heading": {
      "type": "string"
    },
    "subHeading": {
      "type": "string"
    },
    "ctaButton": {
      "type": "string"
    },
    "miscValues": {
      "type": "json"
    },
    "actionUrl": {
      "type": "string"
    },
    "isInternal": {
      "type": "boolean",
      "default": true
    },
    "image": {
      "type": "media",
      "multiple": false,
      "required": true,
      "allowedTypes": [
        "images",
        "videos",
        "audios",
        "files"
      ]
    },
    "isVisible": {
      "type": "boolean",
      "default": false
    }
  }
}
Sharasharai answered 12/4, 2022 at 15:28 Comment(0)
H
18

Recommended

Highly recommend you to check out this YouTube video for the native solution.

Alternative Easy and quick solution (less performant)

You can make use of the Populate Deep plugin from the marketplace.

You simply have to install the package with:

yarn add strapi-plugin-populate-deep

Then use your populate parameter with the value of deep like so:

/api/articles/1?populate=deep
Homoeroticism answered 21/6, 2022 at 8:17 Comment(6)
Thank you @Rubek, I did try using "strapi-plugin-populate-deep" but no dice at the time.Sharasharai
Just an update. I have tried strapi-plugin-populate-deep on the latest v4 release and it works as expected. In my case it was "/populate=deep,3". It can go up to 10 deep.Sharasharai
The YouTube video was extremely informative, thank you. I was able to do my query without the need for strapi-plugin-populate-deep.Maturate
All though it is a good plugin, it can effect performance of your app. I stoped using it and handle populate natively within Strapi. Here is a good read to learn more strapi.io/blog/demystifying-strapi-s-populate-and-filteringVirginity
Hey, that is me and Derrick in the video. Lol. I just added another answer to this thread of how I use route middleware to define populate logic below. And although the populate plugin is good. We have noticed that it can lead to performance issues on big projects. So I no longer use it. And handle populate and filtering natively. Another video i did on populating things in Strapi youtu.be/RceLeh9D85o?feature=shared&t=2879Virginity
Why is this highly recommended and who highly recommended it?Benoite
V
5

I don't use the populate plugin and handle it natively in Strapi. Did you know that you can add your populate logic inside Strapi route middleware.

Here is a quick example.

Inside my Next JS project. I call the getPageBySlug() function

export default async function PageRoute({params}: Props) {
    const page = await getPageBySlug(params.slug, params.lang);
    if (page.data.length === 0) return null;
    const contentSections = page.data[0].attributes.contentSections;
    return contentSections.map((section: any, index: number) => sectionRenderer(section, index));
}

getPageBySlug() makes call to Strapi API. Notice how I am not passing any populate logic. This instead is handled in my Strapi apps route middleware.

import {fetchAPI} from "@/app/[lang]/utils/fetch-api";

export async function getPageBySlug(slug: string, lang: string) {
    const token = process.env.NEXT_PUBLIC_STRAPI_API_TOKEN;

    const path = `/pages`;
    const urlParamsObject = {filters: {slug}, locale: lang};
    const options = {headers: {Authorization: `Bearer ${token}`}};
    return await fetchAPI(path, urlParamsObject, options);

Here is the route middleware inside my Strapi app that handles the population. No need to use populate deep plugin, which will return everything.

Instead my middleware only returns the items my site requires.

"use strict";

/**
 * `page-populate-middleware` middleware
 */

const populate = {
  contentSections: {
    populate: {
      picture: {
        fields: ["url", "alternativeText", "caption", "width", "height"],
      },
      buttons: {
        populate: true,
      },
      feature: {
        populate: {
          fields: ["title", "description", "showLink", "newTab", "url", "text"],
          media: {
            fields: ["url", "alternativeText", "caption", "width", "height"],
          },
        },
      },
      testimonials: {
        populate: {
          picture: {
            fields: ["url", "alternativeText", "caption", "width", "height"],
          },
        },
      },
      plans: {
        populate: ["product_features"],
      },
      submitButton: {
        populate: true,
      },
    },
  },
  seo: {
    fields: ["metaTitle", "metaDescription"],
    populate: { shareImage: true },
  }
};

module.exports = (config, { strapi }) => {
  // Add your own logic here.
  return async (ctx, next) => {
    ctx.query = {
      populate,
      filters: { slug: ctx.query.filters.slug },
      locale: ctx.query.locale,
    };

    console.log("page-populate-middleware.js: ctx.query = ", ctx.query);

    await next();
  };
};

I am not saying you should not use the populate deep plugin, but know that there are possible performance impact.

Here are some resource to learn more on how to perform Strapi populate and filtering natively.

https://strapi.io/blog/demystifying-strapi-s-populate-and-filtering

Example project: https://github.com/strapi/nextjs-corporate-starter

If you have additional questions around Strapi feel free to ask here. But we also have open office hours mon - fri 12:30 pm CST on Strapi's discord.

Hope this help.

I also made a crash course video that covers all these topics. https://youtu.be/RceLeh9D85o?feature=shared

Paul

Virginity answered 29/9, 2023 at 17:24 Comment(0)
S
1

Found the answer under strapi github, shoutout to "Tomnovotny7", thank you man.

Copy the following code under your "page.js" -

const { isEmpty, merge } = require("lodash/fp");

const getModelPopulationAttributes = (model) => {
  if (model.uid === "plugin::upload.file") {
    const { related, ...attributes } = model.attributes;
    return attributes;
  }

  return model.attributes;
};

const getFullPopulateObject = (modelUid, maxDepth = 20) => {
  if (maxDepth <= 1) {
    return true;
  }
  if (modelUid === "admin::user") {
    return undefined;
  }

  const populate = {};
  const model = strapi.getModel(modelUid);
  for (const [key, value] of Object.entries(
    getModelPopulationAttributes(model)
  )) {
    if (value) {
      if (value.type === "component") {
        populate[key] = getFullPopulateObject(value.component, maxDepth - 1);
      } else if (value.type === "dynamiczone") {
        const dynamicPopulate = value.components.reduce((prev, cur) => {
          const curPopulate = getFullPopulateObject(cur, maxDepth - 1);
          return curPopulate === true ? prev : merge(prev, curPopulate);
        }, {});
        populate[key] = isEmpty(dynamicPopulate) ? true : dynamicPopulate;
      } else if (value.type === "relation") {
        const relationPopulate = getFullPopulateObject(
          value.target,
          maxDepth - 1
        );
        if (relationPopulate) {
          populate[key] = relationPopulate;
        }
      } else if (value.type === "media") {
        populate[key] = true;
      }
    }
  }
  return isEmpty(populate) ? true : { populate };
};

const modelUid = "api::page.page";

module.exports = createCoreController(modelUid, ({ strapi }) => ({
  async find(ctx) {
    const { query } = ctx;

    const { results, meta } = await strapi.service(modelUid).find({
      ...getFullPopulateObject(modelUid),
      ...query,
    });

    const sanitizedEntities = await this.sanitizeOutput(results, ctx);

    return {
      data: sanitizedEntities,
      meta,
    };
  },
}));
Sharasharai answered 13/4, 2022 at 22:54 Comment(1)
An explanation of what this code does or how it works would improve this answer.Maturate
S
1

This seem to work for me when getting dynamic components and components in Strapi v4, not sure if it is the "recommended way", 3 ways (1 and 2 are almost same)

1)

`${process.env.NEXT_PUBLIC_STRAPI_API}/pages?publicationState=live&populate[seo][populate]=%2A&populate[pageHeading][populate]=%2A&populate[socialMedia][populate]=%2A&populate[components][populate]=%2A&filters[slug]=${params.slug}`

Same as 1) except replace "%2A" with "*":

`${process.env.NEXT_PUBLIC_STRAPI_API}/pages?publicationState=live&populate[seo][populate]=*&populate[pageHeading][populate]=*&populate[socialMedia][populate]=*&populate[components][populate]=*&filters[slug]=${params.slug}`
  1. `${process.env.NEXT_PUBLIC_STRAPI_API}/pages?publicationState=live&filters[slug]=${params.slug}`
    

controller (src/api/page/controllers/page.js)

"use strict";

/**
 *  page controller
 */

const { createCoreController } = require("@strapi/strapi").factories;

module.exports = createCoreController("api::page.page", () => ({
  async find(ctx) {
    const populateList = [
      "seo",
      "pageHeading",
      "socialMedia",
      "components.image",
      "components.types",
    ];
    populateList.push(ctx.query.populate);
    ctx.query.populate = populateList.join(",");

    const content = await super.find(ctx);
    return content;
  },
}));

enter image description here

enter image description here

Steepen answered 26/9, 2022 at 19:33 Comment(0)
D
0

Nuxt Strapi Module with "populate on" syntax

After digging into the Documentation and a lot of testing I found a solution that worked for me. I was able populate resources in dynamic content.

const response = await find('posts', {
  populate: {
    project: true,
    blocks: {
      on: {
        'shared.media': { populate: '*' },
        'shared.quote': { populate: '*' },
      },
    },
  },
});

This results to:

.../api/posts?populate[project]=true&populate[blocks][on][shared.media][populate]=%2A&populate[blocks][on][shared.quote][populate]=%2A

Dwarfish answered 20/2, 2023 at 21:35 Comment(0)
H
0

This is an example of a response of the dynamic section (blocks) of a Content-Type:

"blocks": [
    {
        "id": 1,
        "__component": "shared.media",
        "file": {}

    },
    {
        "id": 1,
        "__component": "shared.slider",
        "files": {}

    },
    {
        "id": 1,
        "__component": "shared.rich-text",
        "body": "# Test"
    }
]

As you can see, in shared.media and shared.slider the attachments are in file and files respectively, which are not populated by default. This is the final request:

http://localhost:1337/api/articles?populate[0]=cover.attributes.&populate[1]=categories&populate[2]=blocks.file&populate[3]=blocks.files

You have to specify each field you want to populate, if you want to go deeper, just add more fields.

Hertzfeld answered 11/8, 2023 at 10:1 Comment(0)
H
0

you can do it in two times, for example : Call 1 : get all the components of the dynamic zone :

/api/pages/2?populate[component][populate]=*

you will get

...
{
  "id": 1,
  "__component": "component.multi-pins"
},
....

them you do the second call :

populate[component][on][component.multi-pins][populate]=*

Heavy answered 16/12, 2023 at 9:55 Comment(0)
S
0

I've been playing with an alternative approach recently which populates at the strapi level instead of the frontend, in particular for dynamic zones, for examples "Page". This is a similar approach to @atazmin (above using controllers) but instead, used as a default in the middleware.

Create a middleware on your content type with something like this -

"use strict";

/**
 * `page-populate-middleware` middleware
 */

const populate = {
    metadata: {
      populate: '*',
    },
    contentSections: {
        populate: {
          faqAccordionList: {
            populate: {
              categories: true,
            }
          },
          faqCards: {
            populate: '*',
            categories: {
              populate: '*',
            }
          },
          media: true,
          benefitList: {
            populate: '*',
          },
          bannerContent: {
            populate: '*',
          },
          button: {
            populate: '*',
          },
          attributes: {
            populate: '*',
          },
          testimonials: {
            populate: '*',
          },
          picture: {
            populate: '*',
          },
        }
    },
    page: {
      populate: {
        pages: true,
      }
    }
};

module.exports = (config, { strapi }) => {
  // Add your own logic here.
  return async (ctx, next) => {

    ctx.query = {
      populate,
      ...ctx.query
    };

    await next();
  };
};

On your content-type route add the middleware -

'use strict';

/**
 * page router.
 */

const { createCoreRouter } = require('@strapi/strapi').factories;

module.exports = createCoreRouter('api::page.page', {
    config: {
      find: {
        middlewares: ["api::page.page-populate-middleware"]
      },
      findOne: {
        middlewares: ["api::page.page-populate-middleware"]
      },
    }
});

Sharasharai answered 19/12, 2023 at 22:19 Comment(0)
C
-1

The same situation, but for GraphQL… Is there any elastic way to request dynamic zones via Graphql without explicitly defining each of them in a query? For example, I DON’T want to do something like this:

`query Homepage {
  components {
... on SliderComponent {
      image
      text
    }
    ... on ParagraphComponent {
      title
      description
    }
    // and so on...
  }
}`

Instead, somehow, I’d like to be able to get all of the dynamic zones without querying them separately. So ideally would be something like this:

query Homepage { components } and that would return all of the possible dynamic zone components with their nested fields.

NOTE: I know that the queries above are not correct, but this is just an idea of the query shape.

Convict answered 29/6, 2022 at 11:29 Comment(2)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Ethridge
By design, GraphQL is there to serve specific fields. For example "select *" isn't recommended or possible. You might be better off creating a stored procedure with a custom endpoint.Sharasharai

© 2022 - 2024 — McMap. All rights reserved.