{"id":13910407,"url":"https://github.com/MoritzLost/TextformatterPageTitleLinks","last_synced_at":"2025-07-18T09:31:22.227Z","repository":{"id":113117255,"uuid":"156600709","full_name":"MoritzLost/TextformatterPageTitleLinks","owner":"MoritzLost","description":"A Textformatter module for Processwire that will automatically add links to titles of other pages in a text.","archived":false,"fork":false,"pushed_at":"2020-09-04T11:06:04.000Z","size":76,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-11-25T19:39:52.008Z","etag":null,"topics":["processwire","processwire-modules","textformatter"],"latest_commit_sha":null,"homepage":null,"language":"PHP","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/MoritzLost.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"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":"2018-11-07T19:55:42.000Z","updated_at":"2023-03-27T17:43:14.000Z","dependencies_parsed_at":"2024-03-30T15:15:41.546Z","dependency_job_id":null,"html_url":"https://github.com/MoritzLost/TextformatterPageTitleLinks","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/MoritzLost/TextformatterPageTitleLinks","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MoritzLost%2FTextformatterPageTitleLinks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MoritzLost%2FTextformatterPageTitleLinks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MoritzLost%2FTextformatterPageTitleLinks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MoritzLost%2FTextformatterPageTitleLinks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MoritzLost","download_url":"https://codeload.github.com/MoritzLost/TextformatterPageTitleLinks/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MoritzLost%2FTextformatterPageTitleLinks/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265733862,"owners_count":23819426,"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":["processwire","processwire-modules","textformatter"],"created_at":"2024-08-07T00:01:21.249Z","updated_at":"2025-07-18T09:31:21.949Z","avatar_url":"https://github.com/MoritzLost.png","language":"PHP","funding_links":[],"categories":["PHP"],"sub_categories":[],"readme":"# TextformatterPageTitleLinks\n\nThis is a Textformatter module for the [ProcessWire CMF](https://processwire.com/) that replaces any title of pages on your site with links to that page.\n\n## Table of Contents\n\n- [Description](#description)\n- [Features](#features)\n    - [Caveats](#caveats)\n- [Installation](#installation)\n- [Configuration](#configuration)\n    - [All settings](#all-settings)\n- [Usage](#Usage)\n    - [Automatic usage](#automatic-usage)\n    - [Manual Usage](#manual-usage)\n        - [Options for manual usage](#options-for-manual-usage)\n- [Example project: Glossary terms with inline definitions](#example-project-glossary-terms-with-inline-definitions)\n- [Hooks](#hooks)\n- [Changelog](#changelog)\n\n## Description\n\nTextformatters are a type of ProcessWire that allow you to perform some automated formatting on any text field output when you access the field through the API. This module has a simple purpose: Whenever your text includes the title of another page on your site, the formatter automatically adds a link (an anchor tag) to that page. This is useful for quickly connecting multiple pages, and for SEO purposes. You can limit this functionality by template, so only pages using the specified templates will get automatically linked.\n\n## Features\n\n- Allows you to limit the automatic links by template.\n- Only includes published \u0026 visible pages by default, with an option to include hidden pages.\n- Automatically excludes the current page, with an option to change that behaviour.\n- Allows you to configure the minimum title length for linked pages.\n- Doesn't overwrite existing links, and detects most edge cases (titles inside other tag's attributes, titles inside existing links et c.).\n- Supports multi-language sites. Titles will only be linked if a title in the current language is set.\n- Can add configurable attributes to all automatically created links. This includes the ability to use page fields as replacement patterns for attributes. For example, you can create CSS classes that include the name of the template of the linked page.\n- Extensive options and hooks to change the generated markup completely. Need `\u003cmark\u003e` elements with a `title` attribute based on a page field instead of a link? No problem. See the example project below.\n- Prefer oldest or newest page in the case of duplicate titles.\n- Queries the database directly for improved performance.\n- Has options to switch between case sensitive and case insensitive modes, and force case sensitive behaviour even for case insensitive database collations.\n- Allows you to overwrite the module configuration via the API to call the module with different settings for different requirements on the same site.\n\n### Caveats\n\n- This module uses regex, so if it's used on a site with many pages, it will have a significant performance impact! Make sure to cache the results.\n- Since it's regex, it can never detect all edges cases with heavily nested HTML elements. Don't use this on a field with lots of custom HTML structures.\n\n## Installation\n\nThis module is available in the [modules directory here](https://modules.processwire.com/modules/textformatter-page-title-links/). You can download it through the backend using the classname `TextformatterPageTitleLinks`. You can also manually clone or download the repository into your `site/modules` directory and install it through the backend.\n\n## Configuration\n\nThe module is configurable through the ProcessWire backend. After installation, you first have to set the templates you wish to be automatically linked. By default, no templates are selected, so this module will do nothing!\n\nOn the module configuration page, you can set the options that will be used when using the module as a Textformatter through the field configuration. If you call the module manually in your template code, you can overwrite those options, see the section on manual usage below.\n\n### All settings\n\n- **Templates to search for matching titles:** Select all the templates whose pages you want to link automatically.\n- **Minimum length for linkable titles:** You can set the minimum length for linked pages. Pages with shorter titles than the defined minimum will not be linked.\n- **Include the current page:** By default, the module will not create self-referential links (links to the page the field is on), but you can include the current page with this option.\n- **Include hidden pages:** Check this if you want to link to hidden pages as well.\n- **Additional attributes for the HTML tag:** You can set any number of attributes (for example, custom classes) you want to add to all automatically generated links through the settings as well. You can also use replacement patterns that get passed to [$page-\u003egetText()](https://processwire.com/api/ref/page/get-text/). The `href` attribute is added automatically. Check out the examples on the module configuration page to get started.\n- **Change the HTML tag of the link element:** Use this to wrap titles in something other than `\u003ca\u003e` tags. Possible values include `span`, `mark`, `em`, `strong` or the name of any other inline HTML element.\n- **Disable automatic href attribute:** By default, the module automatically includes an `href` attribute in the generated link. But if you want to markup your titles with a different element, this may not be what you want, so you can disable it here.\n- **Disable automatic visibility check:** By default, the module will not generate a link to a page if it isn't viewable (using [\\$page-\u003eviewable()](https://processwire.com/api/ref/page-permissions/viewable/). Use this option to disable this check.\n- **Use case insensitive mode:** Add the `i` flag to the regular expressions, so that titles are matched on a case insensitive basis.\n- **Force case sensitive database query for title retrieval:** The database query groups by title, so if there are multiple pages with a similar title, only one is returned. For case insensitive database collations (`_ci` suffix), this means pages whose titles only differ by their casing or use of diacritics, only one row will be returned. Check this option to group by binary representation instead, this way you get all variations from the database.\n- **Preference for duplicate page titles:** If you have multiple pages with the same name, you can tell the module to prefer to link to the oldest or the newest page.\n\n## Usage\n\nYou can use the module in two ways: automatic and manual.\n\n### Automatic usage\n\nProcessWire textformatters can be added to any text(-area) field you want on the details tab of the field settings. This way, the textformatter will automatically be applied whenever you output that field in your template.\n\nIf you apply multiple formatters, mind the order. For example, if you apply the HTML entity encoder or a formatter that strips HTML tags, apply those *before* this one.\n\n### Manual usage\n\nAlternatively, you can manually use the formatter in your code (for example, if you only want it to apply to a field in certain places). Manual usage allows you to overwrite any options set in the module configuration on a per call basis.\n\nFirst, obtain an instance of the formatter through the ProcessWire API. Then, use one of the following methods:\n\n- Use `TextformatterPageTitleLinks::format` if you simply want to call the module with the default options set in the module configuration.\n- Use `TextformatterPageTitleLinks::formatWithOptions` if you want to call the module with custom options. See below for accepted options.\n\n```php\n// get the formatter\n$formatter = wire('modules')-\u003eget('TextformatterPageTitleLinks');\n// we'll apply it to the 'body' field\n$body = $page-\u003ebody;\n// call the formatter\n$formatter-\u003eformat($body);\necho $body;\n```\n\nBoth method accepts it's parameter by reference, so passing in `$page-\u003ebody` directly **won't work**, since you can't indirectly modify overloaded properties. Also, make sure not to echo the return value of the function; since it modifies the parameter it accepts by reference, it doesn't return anything.\n\n```php\n$formatter = wire('modules')-\u003eget('TextformatterPageTitleLinks');\n// this will cause a warning, and it won't work\n$formatter-\u003eformat($page-\u003ebody);\n\n// this will echo nothing, since the format method returns nothing\n$body = $page-\u003ebody;\necho $formatter-\u003eformat($body);\n```\n\nWith the `formatWithOptions` method, you can pass an associative array with options that will overwrite the defaults from the module configuration. You can also pass in a different page to be used as the 'current' page as the third argument.\n\n```php\n$formatter = wire('modules')-\u003eget('TextformatterPageTitleLinks');\n$body = $page-\u003ebody;\n$formatter-\u003eformatWithOptions(\n    $body,\n    [\n        // custom options go here\n    ],\n    $page // this is optional\n);\necho $body;\n```\n\n#### Options for manual usage\n\nThe following table lists all the available options with their respective labels in the module configuration, the array key for the options array with which you can overwrite it, as well as the option type and allowed values. The module will use the global module configuration for any options you leave out. For explanations of what each option does, check the module configuration page and the section on configuration above.\n\n\u003ctable\u003e\n    \u003cthead\u003e\n        \u003ctr\u003e\n            \u003cth\u003eOption name\u003c/th\u003e\n            \u003cth\u003eArray key\u003c/th\u003e\n            \u003cth\u003eType\u003c/th\u003e\n            \u003cth\u003eAllowed values\u003c/th\u003e\n        \u003c/tr\u003e\n    \u003c/thead\u003e\n    \u003ctbody\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eTemplates to search for matching titles\u003c/td\u003e\n            \u003ctd\u003e\u003ccode\u003eauto_link_templates\u003c/code\u003e\u003c/td\u003e\n            \u003ctd\u003earray\u003c/td\u003e\n            \u003ctd\u003eArray of template IDs, names or Template objects.\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eMinimum length for linkable titles\u003c/td\u003e\n            \u003ctd\u003e\u003ccode\u003eminimum_length\u003c/code\u003e\u003c/td\u003e\n            \u003ctd\u003eint\u003c/td\u003e\n            \u003ctd\u003eAny positive integer\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eInclude the current page?\u003c/td\u003e\n            \u003ctd\u003e\u003ccode\u003einclude_current_page\u003c/code\u003e\u003c/td\u003e\n            \u003ctd\u003ebool\u003c/td\u003e\n            \u003ctd\u003etrue | false\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eInclude hidden pages?\u003c/td\u003e\n            \u003ctd\u003e\u003ccode\u003einclude_hidden_pages\u003c/code\u003e\u003c/td\u003e\n            \u003ctd\u003ebool\u003c/td\u003e\n            \u003ctd\u003etrue | false\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eAdditional attributes for the HTML tag\u003c/td\u003e\n            \u003ctd\u003e\u003ccode\u003eadd_attributes\u003c/code\u003e\u003c/td\u003e\n            \u003ctd\u003estring\u003c/td\u003e\n            \u003ctd\u003e\n                A multi-line string, each line containing either a standalone attribute, or an attribute name and value seperated by an equals sign. E.g.: \u003cbr\u003e\n                \u003ccode\u003etitle=Jump to {title} \u003cbr\u003e class=autolink autolink-{template}\u003c/code\u003e\n            \u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eChange the HTML tag of the link element\u003c/td\u003e\n            \u003ctd\u003e\u003ccode\u003ehtml_tag\u003c/code\u003e\u003c/td\u003e\n            \u003ctd\u003estring\u003c/td\u003e\n            \u003ctd\u003eName of any inline HTML element, like \u003ccode\u003ea\u003c/code\u003e, \u003ccode\u003espan\u003c/code\u003e or \u003ccode\u003emark\u003c/code\u003e.\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eDisable automatic href attribute?\u003c/td\u003e\n            \u003ctd\u003e\u003ccode\u003edisable_href\u003c/code\u003e\u003c/td\u003e\n            \u003ctd\u003ebool\u003c/td\u003e\n            \u003ctd\u003etrue | false\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eDisable automatic visibility check?\u003c/td\u003e\n            \u003ctd\u003e\u003ccode\u003edisable_viewable_check\u003c/code\u003e\u003c/td\u003e\n            \u003ctd\u003ebool\u003c/td\u003e\n            \u003ctd\u003etrue | false\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eUse case insensitive mode?\u003c/td\u003e\n            \u003ctd\u003e\u003ccode\u003ecase_insensitive_match\u003c/code\u003e\u003c/td\u003e\n            \u003ctd\u003ebool\u003c/td\u003e\n            \u003ctd\u003etrue | false\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eForce case sensitive database query for title retrieval?\u003c/td\u003e\n            \u003ctd\u003e\u003ccode\u003eforce_case_sensitive_query\u003c/code\u003e\u003c/td\u003e\n            \u003ctd\u003ebool\u003c/td\u003e\n            \u003ctd\u003etrue | false\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003ePreference for duplicate page titles\u003c/td\u003e\n            \u003ctd\u003e\u003ccode\u003esame_title_order\u003c/code\u003e\u003c/td\u003e\n            \u003ctd\u003estring\u003c/td\u003e\n            \u003ctd\u003e\"MIN\" (older pages first) | \"MAX\" (newer pages first)\u003c/td\u003e\n        \u003c/tr\u003e\n    \u003c/tbody\u003e\n\u003c/table\u003e\n\n## Example project: Glossary terms with inline definitions\n\nThis example will walk you through create a glossary template and automatically marking up glossary terms within your text fields. For example, if our glossary contains the term `ProcessWire`, this term will be automatically marked up with a `\u003cmark\u003e` tag and a `title` wherever it appears in your body text:\n\n    // input\n    Our website is built with ProcessWire!\n\n    // output\n    Our website is built with \u003cmark title=\"ProcessWire is a Content Management Framework\"\u003eProcessWire\u003c/mark\u003e\n\nOf course, the markup and what attributes to use are up to you! Follow there steps to get started:\n\n1. Create a new template `glossary-term`. Then create a plain textarea field `description` and add it to the template.\n2. Create a couple of `glossary-term` pages for testing. For the example above, create a page *ProcessWire* and fill out it's description however you like!\n3. Now install this module. Go to the module configuration and select the `glossary-term` template under `Template to search for matching titles`.\n4. Change the following settings to achieve the intended result:\n    1. Under *Additional attributes for the HTML tag*, enter the following line: `title={description}`\n    2. Enter `mark` under *Change the HTML tag of the link element*.\n    3. Check the checkbox *Disable automatic href attribute*.\n    4. Check the checkbox *Disable automatic visibility check*.\n5. This module is a [textformatter](https://modules.processwire.com/categories/textformatter/), so you need to add it to a field in order for it to have any effect. In a standard installation, your pages will have a textarea field `body`, so you can use that for testing. Go to *Setup -\u003e Fields -\u003e body*, select the *Details* tab and add *Automatically link page titles* as a textformatter.\n6. Now create a new page and enter some dummy text into it's `body` field, including the term *ProcessWire*.\n7. Open the page and check the output. You should see the term `ProcessWire` marked up with as described above!\n\nNow you can continue to expand on this feature, here are a couple of suggestions:\n\n1. With the markup in place, you could use a JavaScript library like [Popper](https://popper.js.org/) to display the titles in a nice popup instead of the default browser display of the `title` attribute.\n2. By default, title matching is case-sensitive, so if your glossary term has the title *ProcessWire*, other capitalizations like *Processwire*, *processwire* or *PROCESSWIRE* won't match. You can change that through the module configuration by checking *Use case insensitive mode*.\n3. Maybe you want to have an individual page for each glossary term and link to it. In this case, start by creating the PHP template `glossary-term.php` in your templates directory. Then go to the module settings and reset the HTML tag from `mark` to `a`. Also, uncheck the two checkboxes that disable the href attribute and the visibility checks, respectively.\n\n## Hooks\n\nThe module uses two hookable methods for markup generation. You can use hooks to modify the markup for titles.\n\n### TextformatterPageTitleLinks::buildTitleMarkup\n\n- `@param string $title`     The title to use as the link text.\n- `@param Page $page`        The page to use for the link, replacements and access checks.\n- `@param array $options`    The module options.\n- `@return string`\n\nThis hook is called to create the markup around a title that the module wants to link. Hook after it to add or modify the markup, or before it to modify the options (see [Options for manual usage](#options-for-manual-usage)).\n\n```php\nwire()-\u003eaddHookAfter(\n    // add an inline edit link for logged-in users to glossary term\n    'TextformatterPageTitleLinks::buildTitleMarkup',\n    function (HookEvent $e) {\n        $title = $e-\u003earguments(0);\n        $page = $e-\u003earguments(1);\n        $options = $e-\u003earguments(2);\n\n        $linkMarkup = $e-\u003ereturn;\n        if ($page-\u003eeditable()) {\n            $editLink = sprintf(\n                '\u003ca href=\"%s\"\u003e%s\u003c/a\u003e',\n                $page-\u003eeditUrl(),\n                'Edit this glossary term'\n            );\n            $e-\u003ereturn = $linkMarkup . \" ({$editLink})\";\n        }\n    }\n);\n```\n\n### TextformatterPageTitleLinks::buildAttributesString\n\n- `@param array $attributes`     The attributes as a associative array containing attribute =\u003e value pairs.\n- `@param ?Page $page`           The page to use for replacements.\n- `@return string`\n\nTakes an associative array of attribute -\u003e value pairs and generates a HTML attribute string. Hook before this to add any additional dynamic attributes to the markup for titles.\n\n```php\nwire()-\u003eaddHookBefore(\n    // add an inline edit link for logged-in users to glossary term\n    'TextformatterPageTitleLinks::buildAttributesString',\n    function (HookEvent $e) {\n        $attributes = $e-\u003earguments(0);\n        $page = $e-\u003earguments(1);\n\n        $attributes['data-template'] = $page-\u003etemplate-\u003ename;\n        $e-\u003earguments(0, $attributes);\n    }\n);\n```\n\n## Changelog\n\nSince version 3.0.0, the changelog is maintained within the repository in [CHANGELOG.md](CHANGELOG.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMoritzLost%2FTextformatterPageTitleLinks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FMoritzLost%2FTextformatterPageTitleLinks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMoritzLost%2FTextformatterPageTitleLinks/lists"}