# SDAweb Social Galleri Feed — Developer Hooks

This plugin exposes 21 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 an
issue at 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 have existed in the plugin for several versions, but
are first formally documented as a public contract in **5.3.0**.

---

## Filters

### Rendering & output

#### `sdawsoga_shortcode_attributes`

Filter the sanitized shortcode attributes immediately after `shortcode_atts()`
and `sanitize_attributes()`, before the gallery is rendered. This is the
broadest hook for changing how the plugin behaves site-wide.

**Arguments:**
- `array $atts` — The fully sanitized shortcode attribute array (feed_id,
  account, limit, columns, layout, aspect_ratio, hashtag, etc.).

**Returns:** The (possibly modified) `$atts` array.

**Example — force masonry layout for posts in the "design" category:**

```php
add_filter( 'sdawsoga_shortcode_attributes', function( $atts ) {
    if ( has_category( 'design' ) ) {
        $atts['layout'] = 'masonry';
    }
    return $atts;
} );
```

---

#### `sdawsoga_gallery_template_path`

Filter the template file path before the gallery template is included. The
plugin enforces a `realpath()` allowlist (plugin dir + theme dirs) on the
filtered value, so you can override safely from your theme but cannot escape
to arbitrary filesystem locations.

**Arguments:**
- `string $template_path` — Absolute path to the default template.
- `array  $atts` — Sanitized shortcode attributes.

**Returns:** A path string. Must resolve inside the plugin dir, the active
stylesheet dir, or the parent theme dir, otherwise the filter is ignored
and an error notice is rendered.

**Example — load `gallery-template.php` from your child theme if it exists:**

```php
add_filter( 'sdawsoga_gallery_template_path', function( $template_path, $atts ) {
    $theme_template = get_stylesheet_directory() . '/sdawsoga/gallery-template.php';
    if ( file_exists( $theme_template ) ) {
        return $theme_template;
    }
    return $template_path;
}, 10, 2 );
```

---

#### `sdawsoga_gallery_output`

Filter the final HTML string output by `render_shortcode()` after the
template has been included and `ob_get_clean()` has captured the buffer.
This is your last chance to modify the output before WordPress prints it.

**Arguments:**
- `string $output` — The full rendered HTML.
- `array  $atts` — Sanitized shortcode attributes.

**Returns:** A string of HTML.

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

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

---

#### `sdawsoga_thumbnail_url`

Filter the thumbnail URL for a single Instagram media item. Lets you swap
to a CDN, a local sideload, or an image proxy.

**Arguments:**
- `string $url` — The thumbnail URL the plugin was about to use, already
  passed through `esc_url()`.
- `array  $item` — The full Instagram media item (id, caption, media_type,
  media_url, thumbnail_url, timestamp, permalink, like_count, comments_count,
  children, etc.).

**Returns:** A URL string. Always escape if you build a new value.

**Example — route Instagram CDN images through your own image proxy:**

```php
add_filter( 'sdawsoga_thumbnail_url', function( $url ) {
    return str_replace( 'scontent.cdninstagram.com', 'images.example.com/ig', $url );
} );
```

---

#### `sdawsoga_permalink`

Filter the Instagram permalink URL for a single media item. Useful for
internal redirects, click tracking, or affiliate routing.

**Arguments:**
- `string $permalink` — The Instagram permalink, already passed through
  `esc_url()`. Defaults to `'#'` if the item has no permalink.
- `array  $item` — The full Instagram media item.

**Returns:** A URL string.

---

#### `sdawsoga_formatted_timestamp`

Filter the human-readable "X ago" string used for post timestamps.

**Arguments:**
- `string $formatted` — The default formatted string (e.g. "2 hours ago").
- `string $timestamp` — The original ISO 8601 timestamp from Instagram.
- `int    $time` — The Unix timestamp (`strtotime( $timestamp )`).

**Returns:** A string.

**Example — show absolute date instead of relative time:**

```php
add_filter( 'sdawsoga_formatted_timestamp', function( $formatted, $timestamp, $time ) {
    return wp_date( get_option( 'date_format' ), $time );
}, 10, 3 );
```

---

#### `sdawsoga_media_type_label`

Filter the accessible label for a media type (used in `aria-label` and
similar contexts). Defaults are `Image`, `Video`, `Carousel`, `Media`.

**Arguments:**
- `string $label` — The default translated label.
- `string $media_type` — The Instagram media type (`IMAGE`, `VIDEO`,
  `CAROUSEL_ALBUM`).

**Returns:** A string.

---

#### `sdawsoga_caption_before_process`

Filter the raw caption text *before* hashtag/mention linking and
`nl2br()`/`wp_kses_post()` processing. Use this to clean or modify the
source text before the plugin transforms it.

**Arguments:**
- `string $caption` — The raw caption with horizontal whitespace already
  collapsed but newlines preserved.

**Returns:** A string of plain text (HTML will be escaped later).

---

#### `sdawsoga_caption_after_process`

Filter the caption *after* all processing: hashtag/mention linking, `nl2br()`,
and `wp_kses_post()`. This is the final shape before the caption is rendered.

**Arguments:**
- `string $caption` — The fully processed caption HTML.

**Returns:** A string of HTML. Be careful — anything you add will NOT pass
through `wp_kses_post()` again.

**Example — convert YouTube links in captions to embeds:**

```php
add_filter( 'sdawsoga_caption_after_process', function( $caption ) {
    return preg_replace_callback(
        '#https?://(?:www\.)?youtube\.com/watch\?v=([a-zA-Z0-9_-]{11})#',
        function( $m ) {
            return '<a class="my-yt-link" href="' . esc_url( $m[0] ) . '">▶ Watch on YouTube</a>';
        },
        $caption
    );
} );
```

---

#### `sdawsoga_is_carousel`

Filter the carousel detection result for an item. By default an item is a
carousel if `$item['children']['data']` has more than one element.

**Arguments:**
- `bool  $is_carousel` — The default detection result.
- `array $item` — The Instagram media item.

**Returns:** Bool.

---

#### `sdawsoga_error_message_html`

Filter the HTML used for inline error/warning/info notices.

**Arguments:**
- `string $html` — The default `<div class="sdawsoga-notice ..." role="alert">`.
- `string $message` — The plain-text error message.
- `string $type` — One of `error`, `warning`, `success`, `info`.

**Returns:** A string of HTML.

---

### Post filtering

#### `sdawsoga_filtered_posts`

Filter the result of the by-type filter (`?type=image`, `?type=video`,
`?type=carousel`).

**Arguments:**
- `array  $filtered` — Posts after type filtering.
- `array  $posts` — The original unfiltered posts.
- `string $type` — The requested type (`all`, `image`, `video`, `carousel`).

**Returns:** Array of post arrays.

---

#### `sdawsoga_hashtag_filtered_posts`

Filter the result of the include-by-hashtag filter.

**Arguments:**
- `array  $filtered` — Posts whose captions match the hashtag.
- `array  $posts` — The original unfiltered posts.
- `string $hashtag` — The hashtag (without leading `#`).

**Returns:** Array.

---

#### `sdawsoga_exclude_hashtags_filtered_posts`

Filter the result of the exclude-hashtag filter.

**Arguments:**
- `array  $filtered` — Posts whose captions do NOT match any of the
  excluded hashtags.
- `array  $posts` — The original unfiltered posts.
- `string $exclude_hashtags` — Comma-separated hashtag list.

**Returns:** Array.

---

### API & cached data

#### `sdawsoga_cached_feed_data`

Filter cached feed data when serving it to the REST endpoint or AJAX
handler. Lets you modify the cached data before returning it to the
frontend, without invalidating the cache.

**Arguments:**
- `array  $cached_data` — The cached Instagram data structure.
- `string $slug` — The account slug whose data is being served.

**Returns:** Array.

**Example — append a "fetched at" timestamp without re-fetching:**

```php
add_filter( 'sdawsoga_cached_feed_data', function( $data, $slug ) {
    $meta = get_transient( 'sdawsoga_feed_meta_' . $slug );
    if ( is_array( $meta ) && isset( $meta['fetched_at'] ) ) {
        $data['_fetched_at'] = $meta['fetched_at'];
    }
    return $data;
}, 10, 2 );
```

---

#### `sdawsoga_fresh_feed_data`

Filter freshly-fetched API data **before** it is written to the cache. The
filtered value is what gets stored, so you can use this to permanently
augment, scrub, or normalize the cache contents.

**Arguments:**
- `array  $data` — The fresh Instagram data straight from the Graph API.
- `string $slug` — The account slug.

**Returns:** Array.

> **Tip:** This filter fires from four different code paths (REST endpoint,
> legacy AJAX, manual cache clear, scheduled cron refresh). The filtered
> value is always cached, so be deterministic — random or time-sensitive
> modifications belong in `sdawsoga_cached_feed_data` instead.

---

#### `sdawsoga_after_api_fetch`

Filter the full API response after `fetch_instagram_data()` completes,
including any pagination merge. This fires once per fetch operation, between
the API call and `sdawsoga_fresh_feed_data`. Use this for response
normalization that should run before caching.

**Arguments:**
- `array  $media_response` — The full Graph API response with merged
  pagination data.
- `string $account_slug` — The account slug.

**Returns:** Array.

> **Note:** Despite the "after" in the name, this is a **filter**, not an
> action — it must return its first argument.

---

#### `sdawsoga_stories_data`

Filter the Instagram Stories array after the Stories API has been fetched.
Fires once per Stories cron tick (15 minutes by default).

**Arguments:**
- `array  $stories` — Array of story items from the `{ig-id}/stories`
  endpoint.
- `string $slug` — The account slug.

**Returns:** Array of story items.

---

### SEO

#### `sdawsoga_schema_markup`

Filter the schema.org JSON-LD array before it is JSON-encoded and printed.
Use this to add custom structured data fields or override the defaults.

**Arguments:**
- `array $schema` — The default schema array (`@context`, `@type`,
  `author`).
- `array $data` — The Instagram data the schema was built from.

**Returns:** Array. Will be passed to `wp_json_encode()` with
`JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE`.

---

## Actions

### Lifecycle

#### `sdawsoga_before_api_fetch`

Fires at the start of `fetch_instagram_data()`, before any Graph API
request is made. Useful for logging, analytics, or pre-fetch hooks.

**Arguments:**
- `string $token` — The **decrypted plaintext** access token. Treat with
  care — never log it, never send it to external services.
- `string $account_slug` — The account slug being fetched (empty string
  for the legacy single-account path).

**Example — log the start of every fetch (without the token):**

```php
add_action( 'sdawsoga_before_api_fetch', function( $token, $slug ) {
    error_log( "[SDAweb Social] starting fetch for account: " . ( $slug ?: 'legacy' ) );
}, 10, 2 );
```

---

#### `sdawsoga_cache_cleared`

Fires after `clear_cache()` has finished deleting the feed, meta, error,
and rate-limit transients for an account. Useful for busting downstream
caches (page cache plugins, CDN purges, etc.) or audit logging.

**Arguments:**
- `string $account_slug` — The account whose cache was cleared (empty
  string for the legacy single-account path).

**Example — purge a page cache when the feed is refreshed:**

```php
add_action( 'sdawsoga_cache_cleared', function( $slug ) {
    if ( function_exists( 'wp_cache_flush' ) ) {
        wp_cache_flush();
    }
} );
```

---

## Patterns and conventions

- **Hook names use the `sdawsoga_` 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 request
  (like `sdawsoga_thumbnail_url`, which is called once per item per render).
  For background work, use `wp_schedule_single_event()` from inside your
  callback.
- **Treat the access token in `sdawsoga_before_api_fetch` as a secret.**
  It is the decrypted plaintext token. Don't log it, don't send it to
  third-party services, don't put it in error messages.
- **All hooks listed here are stable within major versions.** A 5.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.

## 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.
