Skip to content

feat: add support for nested relation url building#9

Open
michintosh wants to merge 11 commits into
freshcodes:mainfrom
michintosh:main
Open

feat: add support for nested relation url building#9
michintosh wants to merge 11 commits into
freshcodes:mainfrom
michintosh:main

Conversation

@michintosh

Copy link
Copy Markdown

This feature was added out of necessity for some of my client's projects.

We have some structure where an entity is connected to another entity which serves as a taxonomy (eg.: Article and Article Category). So the url is rendered in this way: /articles/tech/latest-tech-stack-2026

This PR creates the possibility of adding as many related entities under the config.relations specs (as long as they exists and have the final field name populated, so for the upper example the config will result in:

config:{
    'api::article.article': {
          urlTemplate:
            '/articles/{article_category.slug}/{slug}',
        },
}

The process is valid even for multiple nested relations, so if you have like /articles/tech/trends/latest-tech-stack-2026 with the respective entities:

  • Article:
    • slug
    • article_category (relation many-to-one with Article Category)
  • Article Category:
    • slug
    • articles (relation one-to-may with Article)
    • article_category_group (relation many-to-one with Article Category Group)
  • Article Category Group:
    • slug
    • article_categories (relation one-to-many with Article Category)

you can specify in the config:

config:{
    'api::article.article': {
          urlTemplate:
           '/articles/{article_category.article_category_group.slug}/{article_category.slug}/{slug}',
        },
}

The most important edits were made in the generateUrl and processUrlTemplate of the service

@michintosh

Copy link
Copy Markdown
Author

in the meantime other 2 features were added:

Regenerate URLs

If you make some changes to the config, like the structure of the URL, this will not be seen until the next update/creation of said entity.

Now the plugin supports in the list view of the configured entities a "Regenerate URLs" button that will update all of the "published" version (if draft/publish is enabled) without the need of getting to each entity manually

Localizations

The plugin now supports the localization of the url config, this is better explained in the localization part of the new docs:

This plugin supports also the i18n native plugin offered by Strapi. The related entities will automatically pick the correct entities based on the locale provided by the admin panel.
You may also specify a custom translations map with the key of the ISO-code provided by Strapi if you want to have different routes based on the locale (eg.: in "en" locale you may want /articles/slug-of-the-article and in "it" you may want /articoli/slug-of-the-article).
To do so you can extend the configuration like this:

 config: {
      translationsMap: {
        en: {
          articles: "articles",
        },
        it: {
          articles: "articoli",
        },
      },
      relations: {
        "api::article.article": {
          urlTemplate:
            "/[articles]/{article_category.article_category_group.slug}/{article_category.slug}/{slug}",
        },
      }
 }

Also from the admin panel, if the entity is localized, from the select option of the link custom-field the entities will be filtered with the current locale we're editing

@brandonaaron

Copy link
Copy Markdown
Member

I’ve been thinking about the localized-template part of this PR and wanted to get your thoughts on a possible config tweak. What if the config shape looked like this:

relations: {
  'api::page.page': {
    urlTemplate: '/pages/{slug}',
  },
  'api::article.article': {
    urlTemplate: {
      en: '/articles/{slug}',
      it: '/articoli/{slug}',
    },
  },
}

I like this approach because it does not introduce a secondary template pattern. It does trade some repetition in the url structure instead of repetition in what string is getting translated.

This would change the PluginConfig type to:

export interface PluginConfig {
  relations: Partial<
    Record<
      UID.ContentType,
      {
        urlTemplate: string | Record<string, string>
      }
    >
  >
}

In this approach we'd take the following path to look up the url template:

  1. If urlTemplate is a string, use it directly.
  2. If urlTemplate is an object and the current locale exists in it, use that locale’s template.
  3. Otherwise, if Strapi i18n is available, look up Strapi’s default locale and use that template if present.
  4. Otherwise, use the first configured template entry.

What do you think?

@michintosh

Copy link
Copy Markdown
Author

Yeah! I think that this is a more clean approach, I will try to change the code in order to reflect this pattern.
Thanks for the suggestion <3

@michintosh

Copy link
Copy Markdown
Author

I've changed as you suggest it, also took some time for improving the batch update, since it was updating ALL of the entities even tough you may be located in the list view of one single entity in particular. Also added the correct support for draft/publish lifecycle inside the batch update logic, lmk!

@brandonaaron

Copy link
Copy Markdown
Member

Just dropping a note... I didn't find the time today to keep looking over this. I really want to dig into the regeneration and nested populations. The regeneration in particular feels heavy and potentially long running. Makes me curious if there is a different approach to solving that scenario... but I don't know what that'd be either... 🤔

@michintosh

Copy link
Copy Markdown
Author

Np! Yeah, it can turn into an heavy and long-running process, but I did not come up with some other ideas to make this from the admin panel. It's a bit of an "edge" case, considering that rarely someone will change the template of the url, but still it could happen, and in that case one user should be able to "update" everything in order to reflect the changes...

@brandonaaron

Copy link
Copy Markdown
Member

Does it make sense to make the regenerate a developer action/node script since the develper is really the one that has to change the template anyways? I suppose in some environments that might mean downloading everything, running the change, and then pushing everything back up.

@michintosh

Copy link
Copy Markdown
Author

Hi! Sry, been busy during the holidays. Yeah you're right, but I thought that it could be useful to give a "native" developer options to support this type of migration. If a developer needs to add some custom logic to this kind of script it can always be available, but for me could be a nice add-on. Lmk!

@brandonaaron

Copy link
Copy Markdown
Member

@michintosh I wanted to propose that we make the regen a bin as part of the plugin itself. Then the dev could potentially run something like:

npx link-field-regenerate \
  --relation api::article.article \
  --locale en \
  --dry-run

I like this approach because it potentially gives us the option to dry run it and it limits the exposure of long running processes/errors in a UI driven action. I think the logic for generating the link still exists as a strapi service in the plugin so it is reused both in the bin/script and the CMS.

I admit I haven't fully baked this idea but it seems doable and maybe overall a better scenario for large collections in particular. Am I making sense? What do you think about this?

@michintosh

Copy link
Copy Markdown
Author

hi @brandonaaron ! Thx for the response, yeah I think that is a good compromise, my only doubt is the usage of the "npx" command, since it always require a running version of the Strapi installation (with .env and so on), so I don't think that enabling it from outside with npx could work, but I've never published a npx command, so I could be mistaken.

Maybe the script could live under a generic "scripts" folder and the command like you say coulb be smth like:

node scripts/regenerate-urls.js --relationKey api::article.article --locale it

what do you think?

@brandonaaron

Copy link
Copy Markdown
Member

Ahh yeah I was thinking the dev would have a fully functional environment locally with a full export of the data. Then run the regenerator and push that back to the strapi instance. Sounds like you're thinking it might be easier/better to use the strapi api? Although, I think running the API we might still want a functional environment locally with the strapi client npm package installed. Maybe it's an option of the script to use an api key/api url instead of doing the export flow. As for the npx comman path I think we'd just export a bin in this package and npx would use it within the strapi project... but this is an area I haven't explored in a published package yet either.

I think in the package.json we'd have something like this:

  "bin": {
    "link-field-regenerate": "./bin/link-field-regenerate.js"
  }

Then I think devs could run the following within their project that had this strapi plugin installed.

npx link-field-regenerate --relation api::article.article

or

pnpm exec link-field-regenerate --relation api::article.article

@michintosh

Copy link
Copy Markdown
Author

Hi, sry didn't have much time these past days to look into it. For the moment if you prefer we can "hide" this feature on the UI side, so that we do not have that problem.

Let me know! thx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants