2.2.1 β€’ Published 3 months ago

strapi-plugin-permalinks v2.2.1

Weekly downloads
-
License
MIT
Repository
github
Last release
3 months ago

Get Started

✨ Features

  • Use a custom field to manage a chain of URL paths to build a unique permalink.
  • Nested relationships for content types.
  • Parent/child relations can use different collection types.
  • Customize which content types should use permalinks.
  • Child relations automatically sync when parents change.
  • Include optional button to copy the permalink to your clipboard.
  • Supports localization with the i18n Strapi plugin.

πŸ’Ž Installation

yarn add strapi-plugin-permalinks@latest

Don't forget to restart or rebuild your Strapi app when installing a new plugin.

✏️ Custom Field

To get started, let's create a simple Page collection that uses a permalink field.

Use the "Custom" tab in the content type builder to add the permalink field to the model.

After adding a permalink field through the content type builder, there are additional targetField and targetRelation props that will need to be manually added to the permalink schema attribute.

NOTE: Strapi does not currently provide the necessary params to dynamically render a list containing the other field names as select menu options in the content type builder, which is why targetField and targetRelation need to be added manually for now. This will be updated in the future.

Schema for Page

// src/api/page/content-types/page/schema.json
{
  "kind": "collectionType",
  "collectionName": "pages",
  "info": {
    "singularName": "page",
    "pluralName": "pages",
    "displayName": "Page"
  },
  "options": {
    "draftAndPublish": true
  },
  "pluginOptions": {},
  "attributes": {
    "title": {
      "type": "string",
      "required": true
    },
    "slug": {
      "type": "customField",
      "customField": "plugin::permalinks.permalink",
      "required": true,
      "pluginOptions": {
        "permalinks": {
          "targetField": "title",
          "targetRelation": "parent"
        }
      }
    },
    "content": {
      "type": "richtext"
    },
    "parent": {
      "type": "relation",
      "relation": "oneToOne",
      "target": "api::page.page"
    }
  }
}

targetField

This is the same targetField prop used with uid field types. It should point to a string type field which will be used to make suggestions for the unique permalink value.

targetRelation

This prop should point to a oneToOne relation field which will be used as it's "parent" relation.

πŸ”§ Configuration

In addition to the permalink attribute itself, we also need to enable the plugin config.

propertytype (default)description
contentTypesarray ([])An array of objects describing related UIDs that use permalink fields.
contentTypes[].uidsarray ([])The UID values for related collection types.
contentTypes[].urlstringThe URL string template for a UID which can be used to create an absolute URL. See section about mapping data into the URLs for greater customization.
contentTypes[].copyboolean (true)Set to false to disable the "Copy permalink" button that appears in the edit view sidebar.
lowercaseboolean (true)If set to true, it will ensure the input value is always lowercased.

contentTypes

An array of objects describing related UIDs that use permalink fields.

Example

Let's add our Page content type to the plugin config, which will enable it in middlewares, lifecycles, etc. and also help keep related collections synced as data changes.

// config/plugins.js
'use strict';

module.exports = {
  permalinks: {
    config: {
      contentTypes: [
        {
          uids: ['api::page.page'],
        },
      ],
    },
  },
};

Example with mixed relations

In addition to our generic Page collection, let's say we have other collections representing different types of pages, such as HelpPages and ProductPages.

We will want to keep them all in sync with unique permalinks. Our config might look like the code below:

// config/plugins.js
'use strict';

module.exports = {
  permalinks: {
    config: {
      contentTypes: [
        {
          uids: [
            'api::page.page',
            'api::product-page.product-page',
          ],
        },
        {
          uids: ['api::help-page.help-page'],
        },
      ],
    },
  },
};

This informs the plugin which collections should avoid permalink conflicts with other collections. Order of uids does not matter here.

Here is a recap of what is achieved in this example:

  • Pages have parent Pages and will sync across Pages and ProductPages.
  • ProductPages have parent Pages and will sync across Pages and ProductPages.
  • HelpPages have parent HelpPages and will sync across HelpPages.

Example with absolute URLs

A permalink input is almost identical to a regular uid input in Strapi and therefore will not store a full URL string in the database. Instead we only save the unique path part of the URL.

In order to use the full permalink URL for the "Copy permalink" feature or to use the full permalink in API responses, you must configure the url option.

// config/plugins.js
'use strict';

module.exports = ({ env }) => ({
  permalinks: {
    config: {
      contentTypes: [
        {
          uids: [
            'api::page.page',
            'api::product-page.product-page',
          ],
          url: `${env('STRAPI_PERMALINKS_BASE_URL')}/{slug}`,
        },
        {
          uids: ['api::help-page.help-page'],
          url: `${env('STRAPI_PERMALINKS_BASE_URL')}/help/{slug}`,
        },
      ],
    },
  },
});

Example with localization

With i18n enabled for the slug field, you can insert the locale into the permalink value using the url config option.

Currently this does not keep the locale with the value in the database. In a future release, a change may be introduced where the locale value is optionally saved with the slug value in the database.

// config/plugins.js
'use strict';

module.exports = ({ env }) => ({
  permalinks: {
    config: {
      contentTypes: [
        // Include `locale` in the localized slug.
        {
          uids: [
            'api::page.page',
            'api::product-page.product-page',
          ],
          url: '{locale}/{slug}',
        },

        // Example with localized absolute URL and env vars.
        {
          uids: ['api::help-page.help-page'],
          url: `${env('STRAPI_PERMALINKS_BASE_URL')}/{locale}/help/{slug}`,
        },

        // Another example with a different locale position.
        {
          uids: ['api::example.example'],
          url: 'https://{locale}.example.com/{slug}',
        },
      ],
    },
  },
});

Notice how the HelpPage example has /help/ prepending it's slug value, which already makes it unique against Page and ProductPage.

Mapping values from entry data into permalink URLs

By using {curly_braces}, you can map values from the entry data into your permalink URLs to customize the URL however you like. Unmatched values will be replaced with an empty string.

Relation fields are not currently supported but that will change in a future update which will allow the url option to use something like {category.slug} to inject a category slug into the path.

{
  uids: ['api::product-page.product-page'],
  url: `${env('STRAPI_PERMALINKS_BASE_URL')}/{locale}/{slug}`,
}

Disable copy link button

The "Copy permalink" button located in the edit view sidebar can be disabled with the copy: false prop. This value is true by default.

{
  uids: ['api::page.page'],
  url: `${env('STRAPI_PERMALINKS_BASE_URL')}/{slug}`,
  copy: false,
}

lowercase

Defaults to true. It will ensure the permalink value is always lowercased.

NOTE: If you are setting this option to true when it was previously set to false, it will not automatically lowercase existing permalinks in the database. You will need to lowercase existing permalinks yourself, which can be easily done with a database migration script.

// config/plugins.js
'use strict';

module.exports = ({ env }) => ({
  permalinks: {
    config: {
      lowercase: false,
      contentTypes: [
        // etc.
      ],
    },
  },
});

Example migration script to lowercase existing permalinks

// database/migrations/100-lowercase-permalinks.js
'use strict';

module.exports = {
  async up(knex) {
    const rows = await knex('pages');

    const promisedUpdates = rows.map(row => {
      return knex(uid)
        .where({ id: row.id })
        .update({ slug: row.slug.toLowerCase() });
    });

    await Promise.all(promisedUpdates);
  },

  down() {},
};

πŸ“˜ User Guide

Assign a parent relation to an entity to automatically generate a URL path that includes the slugs of it's parent entities.

Syncing entities with children

All child entities will automatically have their permalink values updated when the permalink of their ancestor changes. This extends down to all descendants.

Deleting entities with children

Deleting an entity that has children will orphan those children. Strapi will automatically remove the parent relation from the child entities but no other changes to their data will occur.

If orphaned pages exist, you will see their slug value in the content manager list view as a red label instead of plain text.

Editing the orphaned page will display a warning and an error message on the permalink field. From here you can assign a new parent or no parent at all. Upon saving, any children of the entity will also update their target fields to reflect to new parent permalinks.

πŸ’© Troubleshooting

In general

Remember to rebuild your app after making changes to some config or other code.

yarn build
# OR
yarn develop

❀️ Support or Donate

If you are enjoying this plugin and feel extra appreciative, you can buy me a beer or 3 🍺🍺🍺.

🚧 Roadmap

  • Better support for localization and relation field data populating into the URL.
  • Config option to prevent using reserved slugs.
  • Config option to limit nesting depth.
  • Better conflict resolution for orphaned pages.
  • Better method for handling complicated localized URLs (currently requires plugin with custom hook and middleware).
  • Use same content-type builder options as uid field, then deprecate it in plugin config (not currently possible).
2.2.1

3 months ago

2.2.0

6 months ago

2.1.0

7 months ago

1.0.4

9 months ago

2.0.1

9 months ago

2.0.0

9 months ago

1.0.2

1 year ago

1.0.1

1 year ago

1.0.0

1 year ago

1.0.3

1 year ago

0.3.0

1 year ago

0.2.0

1 year ago

0.3.2

1 year ago

0.4.0

1 year ago

0.3.1

1 year ago

0.1.7

2 years ago

0.1.4

2 years ago

0.1.6

2 years ago

0.1.5

2 years ago

0.1.2

2 years ago

0.1.3

2 years ago

0.1.0

2 years ago

0.1.1

2 years ago

0.0.0

2 years ago