Strapi V4 search by slug instead ID

you’re a hero man

1 Like

Lol. It took me a while to figure it out too. A lot of other posts helped me as well.

This works in Strapi V4. Thanks :pray:

1 Like

Wow Paul, huge shoutout!!
This is exactly wht I needed but couldn’t figure out completely.
Thanks!

1 Like

Wow that was amazing, thank you so much Paul, it works flawlessly!

The only thing though, is that when I do:

http://localhost:1337/api/posts/how-to-use-a-slug-vs-an-id-field?populate=*

It populates certain fields (e.g. SEO) but not others (like for example author or the localizations object)

but when I do “http://localhost:1337/api/posts?populate=*” then yes, it populates everything correctly.

Do you know what could be happening? Thanks

Not quiet sure what is going on. Let me know if you figure it out before me. I will also try to figure it too. But it maybe due to populating nested fields.

Omg, finally, after so many hours of research (I’m new to this) I found that the problem was with the sanitizer, so if you remove it:

module.exports = createCoreController("api::post.post", ({ strapi }) => ({
  async findOne(ctx) {
    const { slug } = ctx.params;

    const query = {
      filters: { slug },
      ...ctx.query,
    };

    const post = await strapi.entityService.findMany("api::post.post", query);

    return this.transformResponse(post[0]);
  },
}));

It gives you all the fields on the response.

Do you know how can I use it without having to remove it completely?

Thank you,

1 Like

You are right, it completely slipped my mind. I need to look into this, but yes, certain things will not be returned after sanitizing the data. I know in the past, I created a custom sanitize function, but not sure if it was the way to go. I will dive into this so I can figure this out for myself too. I am assuming there must be options you can pass into transformResponse.

@piwi I could not find any references of transformResponse in the documentation. Lot’s of question around this topic. Both for REST and GraphQL.

Thank you very much Paul,

For the moment I managed to just populate the fields I wanted on the frontend:

http://localhost:1337/api/posts/whateverpost?populate=author, SEO, localizations

but if you find more about transformResponse I will be happy to learn it as well.

Thanks again,

1 Like

If anyone needs this using GraphQL (which you probably should be using), for me that works (assuming Apollo client is configured correctly):

import React from "react";
import { useParams } from "react-router-dom";
import { gql, useQuery } from "@apollo/client";

const POST = gql`
    query GetPost($slug: String!) {
        posts(filters: {slug: {eq: $slug}}) {
            data {
                attributes {
                    slug
                    title
                }
            }
        }
    }
`;

const Post = () => {
  const { slug } = useParams();
  const { loading, error, data } = useQuery(POST, {
    variables: { slug: slug },
  });

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;

  console.log(data);

  return (
    <div>
        <h2>{data.posts.data[0].attributes.title}</h2>
    </div>
  );
};

export default Post;

probably a good idea to have the slug set to unique in the data type.

1 Like

Hello Paul,

First of all thanks for your work and contribution, is working for me but…

I have a multilingual site and when i do the query in my front i only recieve one locale, not the others asociated to this post.

ideas?

Thanks

Interesting, I am going to look into that, will let you know when I find something out.

[Update] How to get Articles by ID

In my previous post on this topic that you can find above, I overrode the findOne route and findOne controller. Even though that worked, after discussing this with Derrick and Richard, I realized it is not the best solution.

Two main reasons.

You don’t want to override findOne core controllers. Instead, you want it to keep its intended functionality.
The same can be said about routes.

This will also make it easier in the future when migrating your application to another version of Strapi since the core controller logic stays the same.

Instead, we just want to create a new route and a brand new controller.

Here is how I refactored my app where I used my previous implementation.

Create Custom Route

Create routes.js file inside your routes folder with the following code.

module.exports = {
  routes: [
    {
      method: 'GET',
      path: '/post/find-by-slug/:slug', 
      handler: 'api::post.post.findBySlug',
    },
  ]
};

Notice the new path we will use when making an API call and the new handler that will point to our custom controller we are about to create called findBySlug.

Create Custom Controller

Inside the controller folder for your content-type file, add the following code.


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

module.exports = createCoreController('api::post.post', ( {strapi}) => ({
  async findBySlug(ctx) {
    const { slug } = ctx.params;

    const query = {
      filters: { slug },
      ...ctx.query,
    };

    const post = await strapi.entityService.findMany("api::post.post", query);
    const sanitizedEntity = await this.sanitizeOutput(post);

    return this.transformResponse(sanitizedEntity[0]);
  },
}));

Ensure you enable the permissions within Strapi Admin under Settings > Roles > Publick to allow us access to our newly created endpoint.

When we request /api/post/find-by-slug/:slug will trigger our newly created controller.

Now, you should be able to request from Insomnia or Postman to test it.

In my case, I make a GET request to http://localhost:1338/api/post/find-by-slug/first-post

And it works.

Here is my result

{
	"data": {
		"id": 1,
		"attributes": {
			"title": "First Post",
			"content": "# Hello from first post",
			"slug": "first-post",
			"createdAt": "2022-08-06T01:35:48.996Z",
			"updatedAt": "2022-08-06T01:35:50.718Z",
			"publishedAt": "2022-08-06T01:35:50.715Z"
		}
	},
	"meta": {}
}

Let me know if you have any additional questions.

1 Like

Could this not be added to core to be a flag on the FindOne permission per content type? (pull in UID field if it exists or ID as radio options if UID field exists)

There is a caveat I found very recently with that override method (or custom): We lose the meta object!! It is always returned as an empty object, except if we use the super.findOne() method.
Does someone know how to get it with the right values back when using a custom controller?

1 Like

For anyone using GraphQL, I found the easiest solution is to change the resolver and type definition.

// src/index.js
module.exports = {
  register({ strapi }) {
    const extensionService = strapi.plugin("graphql").service("extension");

    const extension = () => ({
      typeDefs: `
        type Query {
          page(slug: String!): PageEntityResponse
        }
      `,
      resolvers: {
        Query: {
          page: {
            resolve: async (parent, args, context) => {
              const { toEntityResponse } = strapi.service(
                "plugin::graphql.format"
              ).returnTypes;
              const data = await strapi.services["api::page.page"].find({
                filters: { slug: args.slug },
              });
              const response = toEntityResponse(data.results[0]);
              return response;
            },
          },
        },
      },
    });
    extensionService.use(extension);
  },
};

3 Likes

Great Point. entityService does not return the meta object and it is something that needs to be taken into consideration. There are ways to add the pagination but it is not included in the documentation.

You can checkout this example from core code. strapi/collection-type.js at main · strapi/strapi · GitHub

You can see in the find method they get pagination separately, and use entity service to get the data.

In the return object, results get returned as data and pagination gets returned as meta @DMehaffy I think I am correct.

My workaround, if I don’t want to implement pagination myself ( which i don’t ) is to use supper to extend the controller or you can call the service directly. Which will return both the data and meta information.

Hope this helps.

When I get a chance I will update the above example. Thank you for the feed back.

Here is an update if you would like to have access to pagination. But when looking for a single items I don’t think you need pagination.

But just wanted to show here another way of doing it since entityService does not return the meta data.

Extending Controller

'use strict';

/**
 * post controller
 */

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

module.exports = createCoreController('api::post.post', ( {strapi}) => ({
  async findBySlug(ctx) {
    const { slug } = ctx.params;

    ctx.query = {
      filters: { slug },
      ...ctx.query,
    };

    const { data, meta } = await super.find(ctx);
    return { data, meta };

  },
}));

Calling a Service


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

module.exports = createCoreController('api::post.post', ({ strapi }) => ({
  async findBySlug(ctx) {
    const { slug } = ctx.params;

    const query = {
      filters: { slug },
      ...ctx.query,
    };

    const entity = await strapi.service('api::post.post').find(query);
    const sanitizedEntity = await this.sanitizeOutput(entity, ctx);
    return this.transformResponse(sanitizedEntity);

  },
}));


This works perfectly well for querying using the slug, but the response is not returning any relational fields added in the content type when using ?populate=*.