# Extending with hooks

SleekRank fires filters and actions that let site code change the data a page
is built from and the HTML it produces, and add its own data source types,
mapping types and sitemap integrations. This page shows how they fit
together; [Hooks reference](/docs/rank/reference/hooks.md) lists every public
hook with its parameters, generated from the code.

## Changing the data

`sleekRank/data/item` filters the row a generated page is built from, after
it was found in the cache and before any mapping reads it.
`sleekRank/data/item/{slug}` then does the same for one page group, named by
its slug. Both run for public pages and the workspace preview. On a public
page they run while WordPress parses the request, before the main query, so
conditional tags such as `is_page()` do not answer in them yet.

```php
add_filter("sleekRank/data/item", function ($item, $pageGroup) {
    $item["name"] = strtoupper($item["name"]);
    return $item;
}, 10, 2);

add_filter("sleekRank/data/item/city-pages", function ($item) {
    $item["description"] = wp_trim_words($item["description"], 30);
    return $item;
});
```

`sleekRank/data/resolved` fires once the row is final, on
`template_redirect` before the base page loads, with the row and the page
group.

## Changing mapping values

Every value a tag, selector or meta mapping writes passes through
`sleekRank/replacement/value`, then through
`sleekRank/replacement/value/{type}` for its mapping type (`tag`, `selector`
or `meta`). The value is already escaped for where it goes. List mappings do
not pass through these filters.

```php
add_filter("sleekRank/replacement/value/meta", function ($value, $mapping, $data) {
    return $mapping["target"] === "description"
        ? wp_trim_words($value, 25)
        : $value;
}, 10, 3);
```

## Changing the page

Four filters receive the page's complete HTML:

- `sleekRank/output/before` and `sleekRank/output/after` run on every
  generated page and preview, before and after the mappings, with the page
  group and the row
- `sleekRank/replacements/before` and `sleekRank/replacements/after` run
  directly around the mappings, with the row and the mappings, and only when
  the group has at least one mapping

```php
add_filter("sleekRank/output/after", function ($html, $pageGroup, $data) {
    $banner = '<div class="generated-page-banner">' . esc_html($data["name"] ?? "") . "</div>";
    return preg_replace("/<body([^>]*)>/", '<body$1>' . $banner, $html, 1);
}, 10, 3);
```

On a public page, Rank points the canonical link and `og:url` at the
generated URL after these filters, whichever of WordPress, Yoast SEO, Rank
Math, The SEO Framework or All in One SEO printed them.
`sleekRank/output/canonical` filters that URL; return an empty string to keep
the base page's canonical:

```php
add_filter("sleekRank/output/canonical", function ($url, $pageGroup) {
    return "city-pages" === $pageGroup->getSlug() ? "" : $url;
}, 10, 2);
```

`sleek_rank_related_items()` lists a page's siblings with a title taken from
the first of `pageTitle`, `heroHeadline`, `title` and `name` the row has, or
its slug; `sleekRank/data/relatedItem` filters each listed item with its row,
so the title can come from any field.

The registration hooks below fire on `after_setup_theme`, so a plugin or
the theme's `functions.php` can add to them.

## Adding a data source type

`sleekRank/register/dataSources` passes the data source registry after the
seven built-in types are registered. Register a class that implements
`SleekRank\DataSourceInterface`; extending `SleekRank\Sources\BaseDataSource`
and implementing its `fetchRaw()` gives the new type Rank's cache, refresh
and lookups. The class receives the source's `config` array, with
`cacheDuration` added.

```php
add_action("sleekRank/register/dataSources", function ($registry) {
    $registry->register("airtable", MyAirtableDataSource::class);
});
```

`sleekRank/dataSource/registered` fires for every registered type. The
workspace editor offers the built-in types for new sources and keeps a source
of a custom type as it is, showing its type; its settings are configured
through WP-CLI or REST (see
[Data sources](/docs/rank/page-groups/data-sources.md#configuring-sources-from-wp-cli-or-rest)).
A mapping of a custom type is kept and shown the same way.

## Adding a mapping type

`sleekRank/register/replacements` passes the replacement engine after the four
built-in mapping types are registered. Register an object that implements
`SleekRank\ReplacementInterface`: its `supports()` decides which mappings it
handles, and `replace()` returns the changed HTML.

```php
add_action("sleekRank/register/replacements", function ($engine) {
    $engine->registerReplacement(new MyCountdownReplacement());
});
```

## Adding a sitemap integration

`sleekRank/register/sitemapIntegrations` passes the sitemap registry after the
built-in SEO plugin integrations and before the WordPress core fallback.
Register an object that implements `SleekRank\SitemapIntegrationInterface`;
the first registered integration whose `isActive()` is true is used. Build it
on `sleekRank()->getSitemapProvider()`, which supplies the entries.

```php
add_action("sleekRank/register/sitemapIntegrations", function ($registry) {
    $registry->register(
        new MySitemapIntegration(sleekRank()->getSitemapProvider())
    );
});
```

`sleekRank/sitemap/urlsPerSitemap` changes how many URLs one sitemap file
holds (see [Sitemap](/docs/rank/sitemap.md)).

## Lifecycle and build hooks

- `sleekRank/init` fires once Rank has booted, on `after_setup_theme`, with
  the instance `sleekRank()` returns
- `sleekRank/rewrites/registered` fires after the published groups' rewrite
  rules are added
- `sleekRank/activated` and `sleekRank/deactivated` fire when Rank is
  activated or deactivated on a site
- `sleekRank/runtime/allowRouteInventory` decides whether
  `sleek_rank_generated_route_inventory()` may list the generated URLs in a
  request other than WP-CLI, where `wp sleek rank routes` uses it
