{"id":20603351,"url":"https://github.com/metalsmith/permalinks","last_synced_at":"2025-05-16T15:09:38.688Z","repository":{"id":13896512,"uuid":"16594961","full_name":"metalsmith/permalinks","owner":"metalsmith","description":"A Metalsmith plugin for permalinks.","archived":false,"fork":false,"pushed_at":"2025-03-19T10:06:31.000Z","size":926,"stargazers_count":62,"open_issues_count":0,"forks_count":67,"subscribers_count":37,"default_branch":"main","last_synced_at":"2025-05-14T09:42:46.007Z","etag":null,"topics":["metalsmith","metalsmith-plugin","permalink","static-site-generator"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/metalsmith.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2014-02-06T21:43:25.000Z","updated_at":"2025-01-05T21:44:01.000Z","dependencies_parsed_at":"2023-12-13T00:31:27.567Z","dependency_job_id":"3580c5b9-69b0-4097-a7d2-e86069e30b4b","html_url":"https://github.com/metalsmith/permalinks","commit_stats":{"total_commits":181,"total_committers":25,"mean_commits":7.24,"dds":0.5414364640883977,"last_synced_commit":"7db2dee36be211f9de6bee805b9af2201bc7ff01"},"previous_names":["segmentio/metalsmith-permalinks"],"tags_count":27,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metalsmith%2Fpermalinks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metalsmith%2Fpermalinks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metalsmith%2Fpermalinks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metalsmith%2Fpermalinks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/metalsmith","download_url":"https://codeload.github.com/metalsmith/permalinks/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254553958,"owners_count":22090417,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["metalsmith","metalsmith-plugin","permalink","static-site-generator"],"created_at":"2024-11-16T09:17:01.546Z","updated_at":"2025-05-16T15:09:33.679Z","avatar_url":"https://github.com/metalsmith.png","language":"JavaScript","readme":"# @metalsmith/permalinks\n\nA Metalsmith plugin that applies a custom permalink pattern to files, and renames them so that they're nested properly for static sites (converting `about.html` into `about/index.html`).\n\n[![metalsmith: core plugin][metalsmith-badge]][metalsmith-url]\n[![npm: version][npm-badge]][npm-url]\n[![ci: build][ci-badge]][ci-url]\n[![code coverage][codecov-badge]][codecov-url]\n[![license: MIT][license-badge]][license-url]\n\n## Installation\n\nNPM:\n\n```bash\nnpm install @metalsmith/permalinks\n```\n\nYarn:\n\n```bash\nyarn add @metalsmith/permalinks\n```\n\n## Usage\n\nBy default `@metalsmith/permalinks` moves all HTML source files at `:dirname?/:basename` to the build as `:dirname/:basename/index.html` and adds [a customizable `permalink` property](#customizing-permalinks) to te file metadata. You can tweak [which files to `match`](#matching-files), set [fixed permalinks](#fixed-permalinks), use a permalink `pattern` with `:placeholder`'s that will be read from the file's metadata, and finetune how that metadata and the final permalink are formatted as a string through the `directoryIndex`, `slug`, `date` and `trailingSlash` options.\n\nFixed permalinks or permalink patterns can be defined in file front-matter, or for a set of files through plugin options. Permalink patterns defined in file front-matter take precedence over plugin options.\n\n```js\nimport { dirname } from 'path'\nimport { fileURLToPath } from 'url'\nimport Metalsmith from 'metalsmith'\nimport permalinks from '@metalsmith/permalinks'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n// defaults\nMetalsmith(__dirname).use(permalinks())\n\n// explicit defaults\nMetalsmith(__dirname).use(\n  permalinks({\n    // files to target\n    match: '**/*.html',\n    // permalink pattern with placeholders\n    pattern: ':dirname?/:basename',\n    // how to format Date values when substituting pattern parts\n    date: {\n      format: 'YYYY/MM/DD',\n      locale: 'en-US' // only relevant if you use textual date part formats\n    },\n    // how to postprocess a resolved permalink in a URL (and filesystem)-friendly way\n    slug: {\n      lowercase: true,\n      remove: /[\u003c\u003e:\"\\'|?*]|[^\\\\w\\\\s$_+~.()!\\\\-@\\\\/]+/g,\n      extend: { ':': '-', '|': '-', '/': '-', '\u003c': '', '\u003e': '' }\n    },\n    trailingSlash: false,\n    directoryIndex: 'index.html',\n    // throw an error when 2 files have the same target permalink\n    duplicates: 'error',\n    // additional linksets\n    linksets: []\n  })\n)\n```\n\nEvery `permalinks()` instantiation supports the following options:\n\n- directoryIndex - traditionally `index.html`, but servers could be configured with alternatives. See [Overriding the default index.html file](#overriding-the-default-indexhtml-file)\n- trailingSlash - whether to add a trailing `/` so that the permalink becomes `blog/post/` instead of `blog/post`. Useful to avoid redirects on servers which do not have a built-in rewrite module enabled.\n- duplicates - what to do when 2 files have the same target destination. See [Ensure files have unique URI's](#ensure-files-have-unique-uris)\n- linksets, see [Defining linksets](#defining-linksets)\n\nPlaceholder substitution will mostly `toString` the value. For example, when you have an `:array` placeholder and a file with front-matter `array: ['one','two']`, it will substitute into `'onetwo'`, but you can refer to the n\u003csup\u003eth\u003c/sup\u003e value with a dot-delimited keypath (eg `:array.0`).\n\nA boolean `false` will result in an error unless the placeholder is optional (it would then be an empty string = omitted), **but a boolean `true` will see the placeholder substituted with its key name** (eg `:placeholder` for a file with front-matter `placeholder: true` will become `placeholder`).\n\n### Matching files\n\nThe `match` option can be 1 or multiple glob patterns, or an object with key-value pairs that will be matched on _either... or..._ basis.\n\n```js\n// only match non-root html files\nmetalsmith.use(permalinks({ match: '*/**/*.html' }))\n\n// match templates so you can use the permalink property in @metalsmith/layouts later\nmetalsmith.use(permalinks({ match: '**/*.hbs' }))\n\n// match files that are either primary:false or have id:1\nmetalsmith.use(permalinks({ match: { primary: false, id: 1 } }))\n```\n\nIf a `match` object property targets an array in file metadata, it will be matched if the array contains the value in the `match` object.\n\n### Defining linksets\n\nWhereas the default `match` option globally defines which files are permalinked, additional `linksets` can be defined with their own `match`, `pattern`, `date` and `slug` options.\n\n```js\nmetalsmith.use(\n  permalinks({\n    // original options act as the keys of a `default` linkset,\n    pattern: ':dirname?/:basename',\n    date: 'YYYY',\n\n    // each linkset defines a match, and any other desired option\n    linksets: [\n      {\n        match: { collection: 'blogposts' },\n        pattern: 'blog/:date/:title',\n        date: 'MM-DD-YYYY'\n      },\n      {\n        match: { collection: 'pages' },\n        pattern: 'pages/:title'\n      }\n    ]\n  })\n)\n```\n\nEvery matched file is only permalinked once, even if it is matched by multiple linksets. The linksets defined in `linksets` take precedence over the default match, and the first linkset in `linksets` takes precedence over the next. In the example above, a file which has `collection: ['pages','blogposts']` would be permalinked to `blog/:date/:title`.\n\n### Fixed permalinks\n\nYou can declare a fixed permalink in file front-matter:\n\n```yml\n---\n# src/topic_metalsmith.html\npermalink: topics/static-site/metalsmith\n---\n```\n\n`@metalsmith/permalinks` will move the source file `topic_metalsmith.html` to the build path `topics/static-site/metalsmith/index.html` and add `permalink: 'topics/static-site/metalsmith'` to the file metadata.\nSetting an explicit front-matter permalink overrides any other `match` that also matched the file from plugin options.\n\n_Typical use case: SEO-sensitive links that should be preserved, even if you moved or renamed the file or updated its front-matter._\n\n### Computed permalinks\n\nFile permalinks can be computed from other (own) file metadata properties.\n\n```yml\n---\n# src/topic_metalsmith.html\ntopic: static-site\nsubtopic: metalsmith\npermalink: topics/:topic/:subtopic\n---\n```\n\nJust like the previous example, this will also move the source file `topic_metalsmith.html` to the build path `topics/static-site/metalsmith/index.html` and add `permalink: 'topics/static-site/metalsmith'` to the file metadata.\n\nPlaceholders can also refer to a keypath within the front-matter `permalink` or plugin option linkset, e.g. `permalink: blog/:postData.html.slug`.\n\n### Skipping permalinks\n\nAn otherwise linkset-matched file can be excluded from permalinking by setting `permalink: false` in its front-matter:\n\n```yaml\n---\ntitle: error\npermalink: false\n---\n```\n\nExplicitly disabling a permalink in front-matter overrides any other pattern that also matched the file from plugin options.\n\n_Typical use case: hosting static sites on third-party providers with specific conventions, e.g. on AWS S3 there must be a top level `error.html` file and not an `error/index.html` file._\n\n### Customizing permalinks\n\nThe `pattern` can contain a reference to **any piece of metadata associated with the file** by using the `:PROPERTY` syntax for placeholders.\nBy default, all files get a `:dirname?/:basename` (+ directoryIndex = `/index.html`) pattern, i.e. the original filepath `blog/post1.html` becomes `blog/post1/index.html`. The `dirname` and `basename` values are automatically made available by @metalsmith/permalinks for the purpose of generating the permalink.\n\nIf you want to tweak how the characters in the permalink are transformed (for example to handle unicode \u0026 non-ascii characters),see [slug options](#slug-options).\n\nThe `pattern` can also be set as such:\n\n```js\nmetalsmith.use(\n  permalinks({\n    // original options act as the keys of a `default` linkset,\n    pattern: ':title',\n    date: 'YYYY',\n\n    // each linkset defines a match, and any other desired option\n    linksets: [\n      {\n        match: { collection: 'blogposts' },\n        pattern: 'blog/:date/:title',\n        date: 'MM-DD-YYYY'\n      },\n      {\n        match: { collection: 'pages' },\n        pattern: 'pages/:title'\n      }\n    ]\n  })\n)\n```\n\n#### Optional permalink pattern parts\n\nThe permalink example in [Computed permalinks](#computed-permalinks) would result in an error if `subtopic` or `topic` were not defined. To allow this add a question mark to the placeholder like `:topic/:subtopic?`. If the property is not defined in a file's metadata, it will be replaced with an empty string `''`. For example the pattern `:category?/:title` applied to a source directory with 2 files:\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\n\u003cpre\u003e\u003ccode\u003e---\ntitle: With category\ncategory: category1\n---\u003c/pre\u003e\u003c/code\u003e\n    \u003c/td\u003e\n    \u003ctd\u003e\n\u003cpre\u003e\u003ccode\u003e---\ntitle: No category\n---\u003c/pre\u003e\u003c/code\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\nwould generate the file tree:\n\n```\nbuild\n├── category1/with-category/index.html\n└── no-category/index.html\n```\n\n#### Date formatting\n\nBy default any date will be converted to a `YYYY/MM/DD` format when using in a permalink pattern, but you can change the conversion by passing a `date` option:\n\n```js\nmetalsmith.use(\n  permalinks({\n    pattern: ':date/:title',\n    date: 'YYYY'\n  })\n)\n```\n\nStarting from v3 `@metalsmith/permalinks` no longer uses moment.js. A subset of date-formatting tokens relevant to site URI's are made available that are largely compatible with those defined at [moment.js](https://momentjs.com/docs/#/displaying/format/):\n\n| Token | Description                 | Examples                   |\n| ----- | --------------------------- | -------------------------- |\n| D     | Date numeric                | 1 2 ... 30 31              |\n| DD    | Date numeric zero-padded    | 01 02 ... 30 31            |\n| d     | Day of week numeric         | 0 1 ... 5                  |\n| dd    | Day of week 2-letter (\\*)   | Su Mo ... Sa               |\n| ddd   | Day of week short (\\*)      | Sun Mon ... Sat            |\n| dddd  | Day of week long (\\*)       | Sunday Monday ... Saturday |\n| M     | Month numeric               | 1 2 ... 11 12              |\n| MM    | Month numeric zero-padded   | 01 02 ... 11 12            |\n| MMM   | Month short (\\*)            | Jan, Feb                   |\n| MMMM  | Month full (\\*)             | January, February          |\n| Q     | Quarter                     | 1 2 3 4                    |\n| YY    | Year 2 last digits          | 70, 24                     |\n| YYYY  | Year full                   | 1970, 2024                 |\n| W     | Week of year                | 1 2 ... 51 52              |\n| WW    | Week of year zero-padded    | 01 02 ... 51 52            |\n| x     | Unix milliseconds timestamp | 1697401520387              |\n| X     | Unix timestamp              | 1697401520                 |\n\nTokens marked with (\\*) use the [Node.js Intl API](https://nodejs.org/api/intl.html) which is not available by default in every Node.js distribution.  \nThe `date` option can be a string of date-formatting tokens and will default to `en-US` for the locale, or an object in the format `{ format: 'YYYY', locale: 'en-US' }`. However, if your Node.js distribution does not have support for the Intl API, or the locale you specified is missing, the build will throw an error.\n\nIf you need more customization you can also pass a date formatting function:\n\n```js\nmetalsmith.use(\n  permalinks({\n    pattern: ':date',\n    // will result in sun/jan/01/2024/index.html for date 2024-01-01\n    date(value) {\n      return value.toDateString().toLowerCase().replace(/\\W/g, '/')\n    }\n  })\n)\n```\n\n#### Slug options\n\nYou can finetune how a pattern is processed by providing custom [slug](https://developer.mozilla.org/en-US/docs/Glossary/Slug) options.\nBy default [slugify](https://www.npmjs.com/package/slugify) is used and patterns will be lowercased.\n\nYou can pass custom [slug options](https://www.npmjs.com/package/slugify#options):\n\n```js\nmetalsmith.use(\n  permalinks({\n    slug: {\n      replacement: '_',\n      lower: false\n    }\n  })\n)\n```\n\nThe following makes everything snake-case but allows `'` to be converted to `-`\n\n```js\nmetalsmith.use(\n  permalinks({\n    slug: {\n      remove: /[^a-z0-9- ]+/gi,\n      lower: true,\n      extend: {\n        \"'\": '-'\n      }\n    }\n  })\n)\n```\n\n#### Custom 'slug' function\n\nIf the result is not to your liking, you can replace the slug function altogether.\nFor now only the js version of syntax is supported and tested.\n\n```js\nimport { slugify } from 'transliteration'\n\nmetalsmith.use(\n  permalinks({\n    pattern: ':title',\n    slug: slugify\n  })\n)\n```\n\nThere are plenty of other options on npm for transliteration and slugs. \u003chttps://www.npmjs.com/browse/keyword/transliteration\u003e.\n\n### Overriding the default `index.html` file\n\nUse `directoryIndex` to define a custom index file.\n\n```js\nmetalsmith.use(\n  permalinks({\n    directoryIndex: 'alt.html'\n  })\n)\n```\n\nUse an empty `directoryIndex` to create _extensionless_ files that can be accompanied by a matching Content-Type Response header with a server like Apache or Nginx, so you could call `https://mysite.com/api/plugins` supposing you have files at `src/api/plugins.json`\n\n```js\nmetalsmith.use(\n  permalinks({\n    match: '**/*.json',\n    directoryIndex: ''\n  })\n)\n```\n\n### Ensure files have unique URIs\n\nNormally you should take care to make sure your source files do not permalink to the same target.  \nWhen URI clashes occur nevertheless, the build will halt with an error stating the target file conflict.\n\n```js\nmetalsmith.use(\n  permalinks({\n    duplicates: 'error'\n  })\n)\n```\n\nThere are 3 other possible values for the `duplicates` option: `index` will add an `-\u003cindex\u003e` suffix to other files with the same target URI, `overwrite` will silently overwrite previous files with the same target URI.\n\nThe third possibility is to provide your own function to handle duplicates, with the signature:\n\n```js\nfunction paginateDupes(targetPath, files, filename, options) =\u003e {\n  let target,\n    counter = 0,\n    postfix = ''\n  while (files[target]) {\n    postfix = `/${++counter}`\n    target = path.join(`${targetPath}${postfix}`, options.indexFile)\n  }\n  return target\n}\n```\n\nReturn an error in the custom duplicates handler to halt the build.  \nThe example above is a variant of the `index` value, where 2 files targeting the URI `gallery` will be written to `gallery/1/index.html` and `gallery/2/index.html`.\n\n_Note_: The `duplicates` option combines the `unique` and `duplicatesFail` options of version \u003c 2.4.1. Specifically, `duplicatesFail:true` maps to `duplicates:'error'`, `unique:true` maps to `duplicates:'index'`, and `unique:false` or `duplicatesFail:false` map to `duplicates:'overwrite'`.\n\n### Maintaining relative links\n\nPreviously this plugin had a `relative: true` option that allowed one to transform a file structure such as:\n\n```txt\n|_ posts\n    |_ hello-world.html\n    |_ post-image.png\n```\n\ninto\n\n```txt\n|_ posts\n    |_ hello-world\n    |   |_ index.html\n    |   |_ post-image.png\n    |_ post-image.png\n```\n\nThis allowed users to reference post-image.png as `\u003cimg src=\"./post-image.png\"\u003e`, but also duplicated the asset and resulted in other unexpected side-effects.\nOur advice is to keep your media in an `assets` or similar folder that does not undergo path transforms, and reference it with a root-relative URI (eg `/assets/hello-world/post-image.png`). If this is not an option for you, the better way to structure your source folder is to have the source path of the referencing HTML file equal to its destination permalink:\n\n```txt\n|_ posts\n    |_ hello-world\n        |_ index.html\n        |_ post-image.png\n```\n\n### Migrating from v2 to v3\n\nThe v2 -\u003e v3 update had a lot of breaking changes, but the good news is v3 can do all that v2 did, just a bit differently, and more. v3 is less forgiving (and more predictable) in that it will throw errors when a (required) `:placeholder` resolves to an undefined value.\n\nPreviously permalinks would omit that pattern part so that a `my-post.html` with pattern `:category/:title` and `title: My post` but without a defined category would be output to `blog/my-post/index.html`. To preserve this behavior, make the failing `:placeholder?` optional by adding a question mark.\n\nThe `indexFile` option has been renamed to `directoryIndex`.\nThe options `duplicatesFail` and `unique` have been condensed into `duplicates`, see also [Ensure files have unique URI's](#ensure-files-have-unique-uris).\n\n### Debug\n\nTo enable debug logs, set the `DEBUG` environment variable to `@metalsmith/permalinks`:\n\n```js\nmetalsmith.env('DEBUG', '@metalsmith/permalinks*')\n```\n\nAlternatively you can set `DEBUG` to `@metalsmith/*` to debug all Metalsmith core plugins.\n\n### CLI usage\n\nTo use this plugin with the Metalsmith CLI, add `@metalsmith/permalinks` to the `plugins` key in your `metalsmith.json` file:\n\n```json\n{\n  \"plugins\": [\n    {\n      \"permalinks\": {\n        \"pattern\": \":title\"\n      }\n    }\n  ]\n}\n```\n\n## License\n\n[MIT](LICENSE)\n\n[npm-badge]: https://img.shields.io/npm/v/@metalsmith/permalinks.svg\n[npm-url]: https://www.npmjs.com/package/@metalsmith/permalinks\n[ci-badge]: https://github.com/metalsmith/permalinks/actions/workflows/test.yml/badge.svg\n[ci-url]: https://github.com/metalsmith/permalinks/actions/workflows/test.yml\n[metalsmith-badge]: https://img.shields.io/badge/metalsmith-plugin-green.svg?longCache=true\n[metalsmith-url]: https://metalsmith.io/\n[codecov-badge]: https://img.shields.io/coveralls/github/metalsmith/permalinks\n[codecov-url]: https://coveralls.io/github/metalsmith/permalinks\n[license-badge]: https://img.shields.io/github/license/metalsmith/permalinks\n[license-url]: LICENSE\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmetalsmith%2Fpermalinks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmetalsmith%2Fpermalinks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmetalsmith%2Fpermalinks/lists"}