# BlueBook Feed Sync — Developer Hooks

This plugin exposes 9 WordPress action and filter hooks so other developers
can extend or customize behavior **without forking the plugin**. All hooks
listed here are considered part of the public API: their names and signatures
will not be changed within a major version, and any deprecation will go
through the standard `_deprecated_hook()` cycle.

If you find yourself needing a hook that isn't listed here, please open a
thread in the wordpress.org support forum and describe what you're trying to
do. Adding a new hook is usually a one-line change for us and saves you from
forking — that's a win for everyone.

All hooks listed here are introduced in **3.10.0**.

---

## Filters

### Graph API & data

#### `bbfsync_field_sets`

Filter the Graph API field sets used by the progressive degradation fetch
strategy. The plugin tries each set in order, starting with the richest one
(includes shares, likes, comments, attachments, author info) and falling
back to simpler sets when Facebook returns field/permission errors.

**Arguments:**
- `array  $field_sets` — Array of field strings, ordered most-detailed-first.
  Each string is a comma-separated Graph API field expression.
- `string $page_id` — The Facebook Page ID being fetched.

**Returns:** Array of field strings.

**Example — request a custom field on top of the default set:**

```php
add_filter( 'bbfsync_field_sets', function( $field_sets ) {
    // Append a custom field to the richest variant only.
    $field_sets[0] .= ',privacy';
    return $field_sets;
} );
```

---

#### `bbfsync_posts_after_fetch`

Filter the result array (posts + next_cursor) immediately after a successful
Graph API fetch and **before** it is written to cache. The filtered value is
what gets cached, so be deterministic — random or time-sensitive
modifications belong in the render-time hooks (`bbfsync_post_data` /
`bbfsync_post_html`) instead.

**Arguments:**
- `array  $result` — `['posts' => array, 'next_cursor' => string]`
- `string $page_id` — The Facebook Page ID this result is for.

**Returns:** Array with the same shape.

**Example — exclude posts older than 90 days from the cached result:**

```php
add_filter( 'bbfsync_posts_after_fetch', function( $result ) {
    $cutoff = time() - 90 * DAY_IN_SECONDS;
    $result['posts'] = array_values( array_filter( $result['posts'], function( $post ) use ( $cutoff ) {
        $created = isset( $post['created_time'] ) ? strtotime( $post['created_time'] ) : 0;
        return $created >= $cutoff;
    } ) );
    return $result;
} );
```

---

#### `bbfsync_cache_duration`

Filter the cache duration for a feed fetch result, in seconds. The plugin
enforces a minimum of 900 seconds (15 minutes) **before** this filter runs,
but the filter can override that minimum if needed.

**Arguments:**
- `int    $cache_duration` — Duration in seconds.
- `string $page_id` — Facebook Page ID.

**Returns:** Integer (seconds).

**Example — cache for 1 hour during business hours, 6 hours overnight:**

```php
add_filter( 'bbfsync_cache_duration', function( $duration ) {
    $hour = (int) wp_date( 'G' );
    return ( $hour >= 9 && $hour < 18 ) ? HOUR_IN_SECONDS : 6 * HOUR_IN_SECONDS;
} );
```

---

#### `bbfsync_og_fetch_budget_per_render`

Filter the maximum number of cold-cache OG image fetches the renderer is
allowed to perform per render. Caps the worst-case page render time when
many shared-without-media posts hit OG recovery on the same render. Cached
results (including cached failures) are always honoured for free and do not
consume the budget.

**Arguments:**
- `int $budget` — Default `2`.

**Returns:** Integer (≥ 0). Zero disables OG fetches; cached results still
serve.

**Example — never block render on OG fetches; rely on cache fill from a
warm-up cron:**

```php
add_filter( 'bbfsync_og_fetch_budget_per_render', '__return_zero' );
```

---

#### `bbfsync_share_keywords`

Filter the lowercase substrings used to detect shared posts when the Graph
API response does not include `status_type` (older or restricted token
permissions). Each entry is matched against the post's `story` text via
`mb_strpos`. Locale-fragile by design — Graph API serves the share story in
the Page's primary language, so add substrings for the languages your
audience writes in.

**Arguments:**
- `string[] $keywords` — Lowercase substrings. Default: EN/NO/FR/PT/DE/IT.
- `string   $page_id`  — Facebook Page ID being rendered (may be empty).

**Returns:** Array of strings.

**Example — add Spanish, Dutch, Swedish, Danish:**

```php
add_filter( 'bbfsync_share_keywords', function( $keywords ) {
    return array_merge( $keywords, array(
        'compartió',  // es
        'deelde',     // nl
        'delade',     // sv
        'delte',      // da (already in default — duplicates are harmless)
    ) );
} );
```

---

### Rendering

#### `bbfsync_post_data`

Filter the raw post data **before any rendering happens**, at the top of
each iteration in `render_posts()`. The filtered array is what the rest of
the iteration reads from, so any modification you make here propagates
through every subsequent field extraction (message, attachments, author,
etc.).

**Arguments:**
- `array $post` — The Facebook Graph API post data.
- `array $settings` — The plugin settings array (including `_feed_id`).

**Returns:** A post array with the same shape.

**Example — fall back to the page name when a post has no `from.name`:**

```php
add_filter( 'bbfsync_post_data', function( $post, $settings ) {
    if ( empty( $post['from']['name'] ) ) {
        $post['from']['name'] = get_bloginfo( 'name' );
    }
    return $post;
}, 10, 2 );
```

---

#### `bbfsync_post_html`

Filter the rendered HTML for a single post card — everything between the
opening and closing `.bbfsync-post` divs. Fires once per post, on every
render path including the AJAX Load More flow.

Use this to wrap, augment, or replace per-post markup. **Anything you return
is concatenated into the feed without further escaping**, so escape your own
additions.

**Arguments:**
- `string $post_html` — The HTML for this post card.
- `array  $post` — The Graph API post data (already filtered through
  `bbfsync_post_data` if any handlers are registered for that filter).
- `array  $settings` — The plugin settings array.

**Returns:** A string of HTML.

**Example — add a "FEATURED" badge to posts containing a specific hashtag:**

```php
add_filter( 'bbfsync_post_html', function( $post_html, $post ) {
    $message = isset( $post['message'] ) ? $post['message'] : '';
    if ( stripos( $message, '#featured' ) !== false ) {
        $badge = '<span class="my-featured-badge">' . esc_html__( 'Featured', 'my-theme' ) . '</span>';
        // Insert just inside the post div.
        $post_html = preg_replace( '/(<div class="bbfsync-post[^"]*"[^>]*>)/', '$1' . $badge, $post_html, 1 );
    }
    return $post_html;
}, 10, 2 );
```

---

#### `bbfsync_feed_html`

Filter the final feed HTML output by `render()` before it is returned. This
is the **last chance** to modify the rendered HTML before it reaches the page.

**Note:** This filter does NOT fire on AJAX-loaded "Load More" batches —
those go through `render_posts()` directly. If you need a filter that runs
in both code paths, use `bbfsync_post_html`.

**Arguments:**
- `string $output` — The full feed HTML.
- `array  $settings` — The plugin settings array (including `_feed_id`).

**Returns:** A string of HTML.

**Example — wrap every feed in a responsive container:**

```php
add_filter( 'bbfsync_feed_html', function( $output ) {
    return '<div class="my-theme-feed-wrap">' . $output . '</div>';
} );
```

---

## Actions

### Lifecycle

#### `bbfsync_before_api_fetch`

Fires immediately before any Graph API call is made for a feed fetch. Useful
for logging, analytics, or pre-fetch hooks. The cursor is empty for the
first page of a feed and contains the after-cursor for paginated calls.

**Arguments:**
- `string $page_id` — Facebook Page ID being fetched.
- `string $after` — Pagination cursor (empty for first page).

**Example — log every fetch attempt:**

```php
add_action( 'bbfsync_before_api_fetch', function( $page_id, $after ) {
    $page = $after ? 'cursor=' . substr( $after, 0, 12 ) . '...' : 'first page';
    error_log( "[BlueBook] fetching {$page_id} ({$page})" );
}, 10, 2 );
```

---

#### `bbfsync_after_api_fetch`

Fires after a successful Graph API fetch has been cached. Receives the page
ID, the result array, and the count of posts stored in this batch. Useful
for cache busting, logging, Slack notifications, or any side-effect that
should happen on a successful refresh. Does **not** fire on failed fetches.

**Arguments:**
- `string $page_id` — Facebook Page ID that was refreshed.
- `array  $result` — `['posts' => array, 'next_cursor' => string]`
- `int    $stored` — Number of posts stored in this result.

**Example — bust your page cache when the feed refreshes:**

```php
add_action( 'bbfsync_after_api_fetch', function( $page_id, $result, $stored ) {
    if ( $stored > 0 && function_exists( 'wp_cache_flush' ) ) {
        wp_cache_flush();
    }
}, 10, 3 );
```

**Example — Slack notification when new posts arrive:**

```php
add_action( 'bbfsync_after_api_fetch', function( $page_id, $result, $stored ) {
    if ( $stored > 0 ) {
        wp_remote_post( 'https://hooks.slack.com/services/...', array(
            'body' => wp_json_encode( array(
                'text' => "BlueBook refreshed page {$page_id}: {$stored} posts in this batch.",
            ) ),
        ) );
    }
}, 10, 3 );
```

---

#### `bbfsync_cache_flushed`

Fires after `BBFSYNC_Cache::flush()` has finished deleting both the
transient cache and the permanent backup options. Useful for busting
downstream caches (page cache plugins, CDN purges, etc.) or audit logging.

**Arguments:** None.

**Example — purge a CDN edge cache when the plugin's feed is reset:**

```php
add_action( 'bbfsync_cache_flushed', function() {
    // Trigger your CDN purge endpoint here.
    error_log( '[BlueBook] cache flushed at ' . current_time( 'mysql' ) );
} );
```

---

## Patterns and conventions

- **Hook names use the `bbfsync_` prefix** consistently with the rest of the
  plugin's namespace.
- **Filters return their first argument**, modified or not. Returning
  `null` or omitting `return` will break the plugin.
- **Actions don't return anything.** Don't `return false` from an action
  callback expecting it to cancel the operation — it won't.
- **Don't throw exceptions from hooks.** Other code (including the plugin
  itself) will not catch them and your site will white-screen.
- **Don't do heavy synchronous work** in hooks that fire on every render
  (like `bbfsync_post_data` and `bbfsync_post_html`, which run once per
  post per render). For background work, schedule a one-off
  `wp_schedule_single_event()` from inside your callback instead.
- **Anything you return from `bbfsync_post_html` or `bbfsync_feed_html`
  bypasses `wp_kses_post()`** — escape your own additions with
  `esc_html()`, `esc_attr()`, `esc_url()`, or `wp_kses()` as appropriate.
- **All hooks listed here are stable within major versions.** A 3.x release
  will not remove or rename a documented hook. New hooks may be added in
  minor versions; deprecations go through `_deprecated_hook()` with at
  least one minor version of overlap.

## Two render paths to be aware of

The plugin renders feeds in two different code paths:

1. **Initial render (full feed):** `BBFSYNC_Renderer::render()` is called
   from the `[bbfsync_feed]` shortcode and the Gutenberg block. Builds the
   complete `<div class="bbfsync-feed">` wrapper, header, like box, posts,
   load-more button, and lightbox.

2. **AJAX Load More (just the new post batch):** `BBFSYNC_Renderer::render_posts()`
   is called directly from the `bbfsync_load_more` AJAX handler. Returns
   only the per-post HTML — no wrapper, no header, no lightbox markup.

This affects which filters fire:

| Filter | Initial render | AJAX Load More |
|---|---|---|
| `bbfsync_post_data` | ✓ | ✓ |
| `bbfsync_post_html` | ✓ | ✓ |
| `bbfsync_feed_html` | ✓ | ✗ |

If you need a modification that must apply consistently across both paths,
use `bbfsync_post_data` or `bbfsync_post_html`. Use `bbfsync_feed_html` only
for changes that genuinely belong on the outer wrapper.

## Requesting a new hook

If you want to customize something the existing hooks don't cover, please
open a thread in the wordpress.org support forum and describe what you're
trying to do. Adding a new hook is usually a one-line change for us, and
publishing it as part of the public API gives the change permanence.
