# Instagram Gallery (Instagram Feed) Widget

> Renders an Instagram media gallery from a user's Instagram account using the Instagram Graph API. Supports grid / carousel / masonry / load-more layouts.

**Class file:** `includes/Elements/Instagram_Feed.php` (908 lines)
**Slug:** `instagram-gallery` (widget id `eael-instagram-gallery`) ⚠ class is `Instagram_Feed` but slug is `instagram-gallery`
**Public docs:** <https://essential-addons.com/elementor/docs/instagram-feed/>
**Pro-shared:** Pro-only. **External integration: Instagram Graph API v20.0**.

## Overview

Fetches a user's Instagram media via Instagram Graph API and renders as a gallery. Three-step fetch flow: get user ID from access token → get media ID list → fetch each media's details. Cached via transient keyed by access-token hash + cache TTL. Layout, columns, captions, hover effects all user-configurable.

## Pro vs Lite

Pro-only.

## File Map

| File | Role |
| --- | --- |
| `includes/Elements/Instagram_Feed.php` | Widget class — controls + render |
| `includes/Traits/Instagram_Feed.php` | Helper trait composed into `Bootstrap` — `get_instagram_data()`, caching, pagination |
| `src/css/view/instagram-gallery.scss` → `assets/front-end/css/view/instagram-gallery.min.css` | Grid/carousel styling |
| `src/js/view/instagram-gallery.js` → `assets/front-end/js/view/instagram-gallery.min.js` | Layout init + load-more handler |
| `assets/front-end/css/view/load-more.min.css` | Load-more pagination styling (Lite-owned — referenced via `EAEL_PLUGIN_PATH`) |
| `config.php` entry `'instagram-gallery'` | Mixed Lite + Pro CSS, self JS |

## Architecture

- **Three-call Graph API flow** (Instagram_Feed trait `:86-105`):
  1. `GET /me?fields=user_id,username&access_token={token}` — resolve token → user_id
  2. `GET /{user_id}/media?access_token={token}` — list media IDs
  3. `GET /{media_id}?fields=...&access_token={token}` per media — fetch each one's details
  This is **N+2 HTTP calls per cache refresh**, where N is the number of media displayed. Heavy.
- **Transient cache keyed by access-token hash + TTL** — `eael_instafeed_<md5(token + cache_limit)>` (trait `:57`). User-configurable cache duration via `eael_instafeed_data_cache_limit`.
- **Access token stored in widget settings** — `eael_instafeed_access_token` control. Lives in `_elementor_data` post meta in plaintext.
- **Pagination via `paging.next` URL** — Instagram returns a `paging.next` URL for the next page. Pro follows it for load-more (trait `:68`).
- **Mixed `EAEL_PLUGIN_PATH` + `EAEL_PRO_PLUGIN_PATH`** in `config.php` — Pro reuses Lite's `load-more.min.css` for the pagination UI. See [`_patterns.md`](_patterns.md).
- **Access token in URL query strings** — every Graph API call has `access_token={token}` as a URL parameter. Tokens leak to:
  - The site's own access log (HTTP request URL)
  - Browser DevTools if WP_DEBUG echoes the URL
  - Any HTTP proxy logging the URL

## Render Output

```html
<div class="eael-instafeed-gallery-wrap eael-instafeed-{layout}"
     data-id="{element-id}"
     data-layout="grid|masonry|carousel"
     data-columns="3">
  <div class="eael-instafeed-item">
    <a href="{permalink}" target="_blank">
      <img src="{media_url}" alt="{caption}" />
      [?] <div class="eael-instafeed-caption">{caption}</div>
    </a>
  </div>
  ...
  [?] <button class="eael-load-more-button">Load More</button>
</div>
```

## Controls Reference

| Control id | Tab → Section | Type | Purpose |
| --- | --- | --- | --- |
| `eael_instafeed_access_token` | Content → Settings | TEXT | Instagram Graph access token |
| `eael_instafeed_data_cache_limit` | Content → Settings | NUMBER | Cache TTL (hours) |
| Layout | Content → Layout | SELECT | Grid / masonry / carousel |
| Columns | Content → Layout | NUMBER | Grid columns (1–6) |
| Items per page | Content → Layout | NUMBER | Initial item count |
| Load more | Content → Layout | SWITCHER | Enable pagination |
| Show caption / hover overlay | Content → Display | SWITCHER | Visibility toggles |

## Conditional Dependencies

```text
load_more = 'yes'
  └── shows load-more button + uses pagination
  └── enqueues Lite's load-more CSS via config.php Lite-path entry

layout = 'carousel'
  └── shows carousel-specific controls (autoplay, arrows, dots)
```

## JavaScript Lifecycle

`src/js/view/instagram-gallery.js`:

```js
var InstaFeedHandler = function( $scope, $ ) {
    var $wrap = $scope.find( '.eael-instafeed-gallery-wrap' );
    var layout = $wrap.data( 'layout' );

    if ( layout === 'masonry' ) {
        // Init Isotope
    } else if ( layout === 'carousel' ) {
        // Init Swiper
    }

    // Load-more click handler — AJAX to a Lite endpoint that calls back into the trait's get_instagram_data
};
```

## Hooks & Filters

### Elementor hooks consumed

Standard widget render. No extension-style hooks.

### Pro / EA hooks emitted

None visible.

### AJAX endpoints

Load-more goes through Lite's standard `wp_ajax_eael_load_more` handler. Pro's contribution is in the trait's data getter. Verify the integration point when editing.

## Common Issues

| Symptom | Likely cause | Diagnose | Fix |
| --- | --- | --- | --- |
| "No items" / blank gallery | Access token expired (Instagram tokens expire every 60 days) | Check `wp_remote_get` response for 401 | User re-issues token at Instagram developer console |
| Items stale after Instagram post | Transient cache TTL not yet elapsed | Check cache key in DB | Reduce TTL or manually delete transient |
| Slow page render | N+2 API calls hit per refresh | Network: many `graph.instagram.com` requests | Increase cache TTL; reduce media count |
| Access token visible in page source | Token concatenated into image URLs by Instagram | View source — `access_token=...` in image URL | Document for users — this is Instagram's URL design, not a Pro bug |
| Load more fetches duplicates | `paging.next` URL not stripping already-shown IDs | Inspect AJAX response | Track shown IDs client-side; dedupe on append |

## Known Limitations

- **N+2 API calls** for N media items per cache refresh — high quota burn on free tier
- **Access token in URL** is Instagram-API design; cannot be passed via header. Mitigation: short cache TTL = more refreshes = more leaks
- **Token stored plaintext in post meta** — same risk as Mailchimp / Google Sheets
- **Token expiry not auto-handled** — refresh-token rotation not visible in Pro code. Tokens expire after ~60 days; user must manually re-issue
- **No fallback to stale cache on API failure** — if Instagram returns 500 / 4xx, the gallery renders empty
- **No retry / backoff** on Graph API errors
- **No timeouts** on `wp_remote_get` — uses WP default 5s. Slow Instagram response stalls render

## Cross-References

- Architecture: [`docs/architecture/external-api-integrations.md`](../architecture/external-api-integrations.md) — Instagram surface
- Shared patterns: [`_patterns.md`](_patterns.md) — External-API widget pattern
- Rule: [`.claude/rules/external-api-integrations.md`](../../.claude/rules/external-api-integrations.md)
