ATX Instagram Feed
ATX Instagram Feed for WordPress by Neil VM — local media sync, theme APIs, blocks, and GitHub updates.
by ATX - Neil VM · github.com/siko001/atx-instagram-feed · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/siko001/atx-instagram-feed/archive/refs/heads/main.zipBy ATX - Neil VM · GitHub profile · Releases
A reusable WordPress integration for Instagram Professional accounts. Synchronization writes normalized media into a site-specific database table. Themes, plugins, REST clients and the two optional Gutenberg blocks read that local data; normal frontend requests never call the Instagram API.
Installation
Requires WordPress 6.5+, PHP 8.1+, JSON and PHP Sodium for connecting accounts. Install the complete dist/atx-instagram-feed.zip through Plugins → Add New → Upload Plugin, or place this directory in wp-content/plugins/atx-instagram-feed and run:
composer install --no-dev --optimize-autoloader
Activate ATX Instagram Feed, then open Instagram Feed → Connection & Settings. Source installations need Composer; release ZIPs include the production autoloader. Node and PHPUnit are development tools only. Runtime slider assets are bundled locally; Node is only needed when updating the pinned Swiper dependency.
Network activation installs each site's own table; account, filters, tokens and cleanup choices are site-specific. Large networks should activate site by site to avoid web-request time limits. PHP callers using switch_to_blog() read the selected site's table and settings.
Admin screens and access
The plugin supplies neutral, scoped admin styles and four separate screens: Connection, Media, Exclusions, and Logs & status. Administrators also see Advanced for existing prune and uninstall-policy tools. Britannia's frontend branding remains in its theme; the plugin's frontend CSS supplies only reusable layout and interaction essentials.
Editors (the edit_others_posts capability) can view the connection/status, queue a sync, hide/show or trash/restore selected cached posts and save media-type/hashtag/keyword exclusions. Authors and Subscribers cannot manage the feed. Connection, reconnection, disconnection, permanent deletion, account-wide cleanup, pruning and uninstall policy remain administrator-only (manage_options). Nonces and capability checks protect form actions and REST operations. Exclusion saves preserve uninstall policy, including when an unauthorized field is submitted.
Logs retain the newest 50 operational events locally, with fixed event codes and numeric diagnostics only. The history begins with this update; earlier success/failure timestamps remain in Status. INSTAGRAM_FEED_DEBUG controls additional PHP error-log output, not this bounded admin history. Tokens and raw provider payloads are never stored in the activity log.
Meta app setup and connection
Use Instagram API with Instagram Login, with a Business or Creator account. This plugin does not use Basic Display and does not require a Facebook Page. Only instagram_business_basic is requested. See the dated Meta verification record and the official Business Login documentation.
-
Configure Instagram API with Instagram Login in your Meta app dashboard. Use its Instagram App ID and Secret.
-
Set the credentials in server environment variables or
wp-config.php, before WordPress loads:define('INSTAGRAM_FEED_APP_ID', getenv('INSTAGRAM_FEED_APP_ID')); define('INSTAGRAM_FEED_APP_SECRET', getenv('INSTAGRAM_FEED_APP_SECRET'));Alternatively, expose those environment variables directly to the PHP process; defining constants is optional. Constants take precedence, even if empty. Never add real values to this repository or theme templates.
-
Register the exact HTTPS redirect URI shown by the settings page. It normally ends in
/wp-admin/admin-post.php, with no query string. Your domain, scheme and port must match. If upgrading from the callback with?action=instagram_feed_callback, update the registered URI and start a fresh connection. Local development needs a correctly configured HTTPS hostname that Meta accepts; a plain HTTP localhost site cannot complete this flow. -
Configure the app's required privacy policy, data deletion information, app domains and account roles in Meta. Accounts you own/manage can use eligible Standard Access; connecting other clients' accounts requires the relevant Advanced Access/App Review and verification.
-
As a WordPress administrator, click Connect Instagram and authorize the Professional account. Successful connection queues the first synchronization. Refresh the status page after cron runs.
Reconnect uses fresh consent and preserves the previous working account/token if the replacement exchange fails. Disconnect always opens a confirmation showing the account and affected record count. Choose Keep cached posts, Move cached posts to Trash, or Delete cached posts permanently. It deletes the local token and stops recurring/manual sync events; kept posts and the last account remain readable. Trashed posts stay out of public feeds even after reconnecting and syncing. Disconnect does not revoke access in Meta; remove the app in Instagram's permissions UI if revocation is required. Connecting a different account changes the public feed to that account without mixing previous accounts' records.
Live consent, refresh, revocation and account-specific media fields require testing with your own app/account. The automated suites use mocked provider responses and local WordPress storage, not live credentials.
Synchronization and retention
The default interval is one hour. Sync Now and POST /admin/sync queue work instead of blocking an admin request. WP-Cron must run for queued work to finish. On low-traffic sites, configure a server scheduler to run this command every few minutes:
wp cron event run --due-now --path=/path/to/wordpress
You can run a connected site's sync immediately from WP-CLI with wp cron event run instagram_feed_synchronize. Recurring events are created only when connected. Change the interval in seconds (clamped to 300–86400):
add_filter('instagram_feed_sync_interval', static fn (int $seconds): int => 1800);
The database lease prevents overlapping synchronization, reconnect and pruning. A separate token lease protects refresh. Long-lived tokens are refreshed during sync when fewer than seven days remain and they are at least 24 hours old. Expired/revoked tokens require reconnecting; a temporary refresh failure can continue with an unexpired token.
Upserts preserve manual visibility, Trash status and creation time. API failures never truncate the feed; successfully written records from a partial run remain available. A single run is bounded to 100 media pages and approximately 200 seconds of processing. Very large accounts may need a custom client/backfill strategy; this version restarts pagination on the next run and does not checkpoint historical backfills.
Media → Account / View filters the local library by account and Active posts or Trash. Use each row’s Trash/Restore/Delete action, or check posts and choose a bulk action. Administrators can also use Clean up this account to trash, restore or permanently delete all its cached records across every page. Every cleanup requires a separate confirmation identifying the account, action and affected count; changes to the selection/count require a fresh confirmation. Restore retains the previous Hide/Show choice and global filters still apply. Trash has no automatic expiry.
Permanent deletion removes the local records and their visibility/Trash choices; nothing is deleted from Instagram. It does not keep a “never import again” marker: a later sync of that account may import those posts again. Use Trash when posts must stay excluded through syncing. Saved block post selections are retained, but missing, trashed and other-account posts do not render. Reselect the client’s posts after changing accounts. Purge any page/CDN cache after cleanup to remove previously rendered content.
There is no automatic deletion for IDs missing from an API response. Tools: prune local history deliberately deletes records before a UTC date, including hidden/trashed records and their visibility choices; a later synchronization can import them again. Records with no publication date are not age-pruned.
Media metadata, captions, children and URLs are stored locally; image/video binaries remain on Instagram's CDN. Browser image/video requests are separate from the Instagram API. CDN URLs can expire during a prolonged sync outage, so this is not a permanent media archive. No remote fallback is attempted during page rendering.
Filtering and manual visibility
Global settings allow IMAGE, VIDEO (including reels) and CAROUSEL_ALBUM. Unchecking every type hides the entire public feed. The Media page includes hidden records, thumbnails, type, caption excerpt, date, permalink and paginated Hide/Show controls.
- Hashtags match exact, case-insensitive Unicode tags:
#vacancymatches#VACANCY, but not#vacancyMalta. Literal hyphenated rules such as#website-hideare supported. - Keywords are separate case-insensitive literal substrings. Enter phrases such as
we're hiringon separate lines or separate them with commas. - Display exclusions are evaluated when querying; synchronization keeps excluded records so changing a rule does not require another API fetch.
- Manual hiding always wins. Public callers and hooks cannot reveal globally excluded content. Posts with an empty or unavailable caption pass hashtag/keyword exclusions because no match is present; they do not satisfy hashtag/keyword inclusion rules. Filtering uses the stored caption only, so it cannot detect excluded text that Meta has not supplied.
Public PHP API
Call after plugins_loaded priority 10, normally from theme templates or block callbacks:
$posts = instagram_feed_get_media([
'limit' => 8,
'media_types' => ['IMAGE', 'CAROUSEL_ALBUM'],
]);
foreach ($posts as $post) {
// Themes own all markup. Escape at the output boundary.
if (!$post->permalink || !$post->mediaUrl || $post->mediaType !== 'IMAGE') {
continue;
}
printf(
'<a href="%s"><img src="%s" alt="%s" loading="lazy"></a>',
esc_url($post->permalink),
esc_url($post->mediaUrl),
esc_attr(wp_strip_all_tags($post->caption ?? 'Instagram photo'))
);
}
InstagramFeed\Feed::get($args) is equivalent. Both return list<InstagramFeed\Media\MediaItem>; no internal HTTP request to WordPress is involved. Invalid PHP query arguments throw InvalidArgumentException. Empty/disconnected-with-no-history feeds return []; local database failures are contained. Optional plugin absence should be handled by checking function_exists('instagram_feed_get_media') in themes.
Each immutable DTO exposes id, accountId, username, nullable caption, mediaType, nullable mediaProductType, nullable mediaUrl, thumbnailUrl, permalink, nullable UTC publishedAt (DateTimeImmutable), and children (an array of DTOs). Public results have isHidden=false and isTrashed=false; toPublicArray() omits that storage flag entirely. Videos normally use thumbnailUrl as a preview; album children are available to custom renderers. Missing optional fields stay null.
| Argument | Values/default |
|---|---|
limit |
Integer 1–100; default 12 |
offset, page |
Offset 0–100000; default 0. Page starts at 1 and computes offset from limit; explicit offset takes precedence. |
media_types |
List of IMAGE, VIDEO, CAROUSEL_ALBUM; empty means no additional restriction |
album_media_types |
List of IMAGE, VIDEO; only albums containing matching usable children qualify. Empty means no additional restriction. Returned DTOs retain all children. |
media_product_types |
Product values such as REELS or FEED, when supplied by Meta |
include_hashtags, exclude_hashtags |
Up to 50 tags, with or without # |
include_keywords, exclude_keywords |
Up to 50 strings/phrases, max 200 bytes per value |
date_before, date_after |
Exclusive UTC bounds; YYYY-MM-DD or ISO-8601 timestamp with timezone |
order |
DESC (default) or ASC |
orderby |
published_at (default) or local insertion id; not Instagram ID |
List arguments accept arrays or comma-separated strings; hashtag strings additionally allow whitespace separation. Lists use OR within an inclusion list and AND between different rule groups. Global restrictions and the original caller's restrictions remain enforced after query filters. Offset/limit count visible records. Unknown PHP arguments—including account/visibility bypasses—are rejected.
Themes can resolve a saved selection with InstagramFeed\Feed::find($instagramId).
This local-only lookup returns a MediaItem or null and enforces the same active-account,
manual hiding and global exclusion rules as collection queries. Album children are
available on the returned post’s children; store their IDs, not expiring CDN URLs.
Guard optional integrations with function_exists('instagram_feed_get_media') and
is_callable([InstagramFeed\Feed::class, 'find']), retaining uploaded-media fallbacks
in the theme. Theme-specific layouts and controls belong in the theme.
REST API
Public routes:
GET /wp-json/instagram-feed/v1/media?limit=12&media_types=IMAGE,VIDEO
GET /wp-json/instagram-feed/v1/media/INSTAGRAM_MEDIA_ID
Collection responses are JSON arrays. Item responses contain id, account_id, username, caption, media_type, media_product_type, media_url, thumbnail_url, permalink, timestamp and children. Hidden/excluded/missing IDs return 404. Invalid input returns 400; unexpected service failures return a generic 503. Query values use the PHP API rules. Unrecognized REST parameters do not become service arguments. Responses request cache revalidation; no total count is calculated.
Feed-management routes require edit_others_posts (Editors and Administrators by default) or manage_options, plus WordPress authentication:
POST /wp-json/instagram-feed/v1/admin/sync → 202 {"queued":true}
POST /wp-json/instagram-feed/v1/admin/media/ID/hide
POST /wp-json/instagram-feed/v1/admin/media/ID/show
Cookie-authenticated JavaScript must supply WordPress's X-WP-Nonce REST nonce; WordPress application passwords can authenticate external admin tools over HTTPS. A disconnected/unqueueable sync returns 409. Tokens, app secrets and authentication data never appear in plugin REST output. REST filters are trusted PHP extensions and must not insert secrets into public fields.
Gutenberg blocks
Insert Instagram Grid (instagram-feed/grid) or Instagram Carousel (instagram-feed/carousel). Both are dynamic blocks registered from block.json; editor previews use WordPress ServerSideRender and the same local feed service. Additional CSS classes and wide/full alignments use normal block supports.
| Shared attribute | Default / bounds |
|---|---|
source |
latest; also selected |
selectedPosts |
Ordered Instagram ID strings, up to 100; used only in selected mode |
postsToShow |
12; 1–100; latest mode only |
mediaTypes |
[]; further narrows globally allowed media. Supports IMAGE, VIDEO, CAROUSEL_ALBUM (all album media), CAROUSEL_IMAGE, CAROUSEL_VIDEO. |
gap |
16 pixels; 0–80 |
showCaption, captionLength |
false, 140; 0–1000 characters |
showIcon, openInNewTab |
true, true |
aspectRatio |
1/1; also 4/5, 3/4, 16/9, auto |
objectFit |
cover; also contain |
videoAutoplay, videoControls, videoPlayPause |
false, false, true; independent video settings, including album children |
albumMode |
inherit; also instagram, lightbox, inline |
Choose particular posts: select the block → Posts → Post source → Selected posts → Choose and order posts. Click thumbnails to select posts, then use the up/down buttons in Display order. Save the page to persist the selection. Each block has its own list; switching back to Latest posts retains the selection for later. An empty selection stays empty rather than falling back to latest posts. Missing, manually hidden, other-account and globally excluded posts never render. Block media-type filters also narrow selected posts. The picker pages through locally synced, globally visible posts, including posts without captions.
Grid uses columns=3, tabletColumns=2, mobileColumns=1. Carousel uses slidesPerView=3, tabletSlidesPerView=2, mobileSlidesPerView=1. Desktop/tablet values allow 1–6; mobile allows 1–4. CSS breakpoints are 600px and 960px.
Carousel also supports navigation=true, autoplay=false, autoplayInterval=6000 (3000–30000 milliseconds), and loop=false. Bundled Swiper 12.1.4 drives the carousel; Loop uses its rewind behavior, wrapping at the ends without duplicated slides. Keyboard arrows, Home/End, direction-aware controls, visible-range announcements and reduced-motion handling are included. Autoplay starts automatically when enabled, pauses during hover/focus or dragging, and resumes when interaction ends. No play/pause button or pagination is displayed. The Gutenberg preview supports the same arrow navigation and rebuilds when settings change. Without JavaScript the native horizontal scroll area remains usable. Videos play inline with a poster and a plugin-owned play/pause overlay. The Video playback panel independently controls muted looping autoplay, native browser controls (off by default), and the center play/pause button. Autoplay runs only for visible videos, respects reduced motion, and pauses offscreen or in hidden album panels. Muted looping videos do not hold up carousel autoplay. The overlay appears on pointer activity and fades after one second of inactivity; keyboard focus keeps it visible. Playback pauses when its carousel slide leaves the visible range. Manually played videos and open album viewers pause carousel autoplay.
Album behavior: choose the global default under Instagram Feed → Display settings, or override it in either block’s Presentation panel. Open on Instagram preserves the cover/link behavior; Open album in lightbox opens a keyboard-accessible modal; Inline album slider provides independent previous/next controls inside each post, with a 450 ms horizontal transition (instant for reduced motion). Album videos use the same playback controls, and stop when moving to another item or closing the viewer. These controls work in Gutenberg previews too. Hidden/excluded posts remain excluded in every mode.
Media filters: combine Standalone images with Images within albums for photos only, or Standalone videos / Reels with Videos within albums for videos only. Select an album option alone to omit standalone posts. Albums retain only the chosen media, in their original order, even when one item remains; albums without a match are skipped before counting the number of posts. The cover also uses the first matching item. Carousel albums (all media) includes every child and replaces the narrower album choices. With Open on Instagram, only the matching cover is shown locally and the link still opens the original full post; use Inline album slider or Open album in lightbox to browse all matching items locally. These filters work with Latest posts, Selected posts and Collections in both blocks, without changing synchronized media or global exclusions.
Block metadata registers shared CSS for WordPress’s block asset loading. Nonempty rendered carousels enqueue the local Swiper bundle and carousel adapter; grids do not request those scripts. Editor assets do not load on normal frontend requests. Styles use .instagram-feed, .instagram-feed--grid, .instagram-feed--carousel and predictable instagram-feed__* elements. Dynamic values use a short set of CSS custom properties; no !important rules are used.
Updating Swiper
In the source checkout:
npm ci
npm run vendor:swiper
npm test
To change versions, run npm install --save-dev --save-exact swiper@VERSION, then npm run vendor:swiper and npm test, and verify both desktop and mobile in a browser. Commit/distribute the updated lockfile and assets/vendor together. The vendor script retains the MIT license and exposes window.InstagramFeedSwiper, leaving a theme’s window.Swiper untouched. No CDN or theme asset build is required. The adapter and styles use isolated classes so theme .swiper initializers do not take over. Editor previews support carousel arrows, album navigation and video controls; test touch behavior on the page preview.
Britannia’s client-specific presentation lives in its theme’s resources/css/plugins/instagram-feed.css, registered with wp_enqueue_block_style in app/setup.php. Other sites keep the plugin’s neutral defaults.
Theme template overrides
Copy any desired template into your child or parent theme:
your-theme/instagram-feed/grid.php
your-theme/instagram-feed/carousel.php
your-theme/instagram-feed/partials/media-item.php
Child theme overrides take precedence. Grid/carousel templates receive $items (prepared presentation arrays), $options, escaped $wrapperAttributes, $trackId and $renderItem (renders a partial including item hooks). Partials receive $media plus imageUrl, videoUrl, permalink, alt, caption, showCaption, showIcon, target, rel, linkLabel, typeLabel, mediaType. Use the bundled templates as references and retain accessibility controls/data attributes when retaining the carousel script.
Names are restricted to relative lowercase PHP paths without dots/traversal. Resolution uses real paths confined to the plugin template directory or the active child/parent theme. instagram_feed_template_path cannot include arbitrary external PHP files. Templates are trusted code; escape URLs, attributes and caption text at output. The PHP API needs no template loader at all.
Hooks and integration boundaries
See every public action and filter, with signatures. For example:
add_filter('instagram_feed_should_display_media', static function (bool $display, InstagramFeed\Media\MediaItem $media): bool {
return $display && !str_contains(strtolower($media->caption ?? ''), '#custom-client-rule');
}, 10, 2);
InstagramClientInterface isolates remote requests; instagram_feed_api_client can inject another implementation before bootstrap. MediaRepositoryInterface separates persistence; FeedService applies global rules. REST controllers and block adapters call services directly. Architecture describes the composition and phases.
Configuration, storage and security
| Configuration | Purpose |
|---|---|
INSTAGRAM_FEED_APP_ID |
Instagram App ID; constant or environment |
INSTAGRAM_FEED_APP_SECRET |
Instagram App Secret; constant or environment |
INSTAGRAM_FEED_API_VERSION |
Validated version; default v25.0; constant or environment |
INSTAGRAM_FEED_DEBUG |
PHP boolean constant enabling additional PHP error-log output; default false |
API origins, OAuth endpoints and the sole required scope are centralized in ApiConfig; arbitrary authentication/API hosts and redirects are refused. The default version follows supported Instagram reference examples; see the verification record for version dates and optional-field differences.
{$wpdb->prefix}instagram_feed_media stores normalized records with a unique Instagram ID and account/date/type/visibility indexes. No raw API/authentication payload is stored. instagram_feed_schema_version is independent of the plugin version. Small options under instagram_feed_* hold settings, account, sync status, cache generation, encrypted token, leases and expiring OAuth state. Large/sensitive options are non-autoloaded. Upserts use prepared SQL and preserve existing captions/product types when Meta omits those optional fields.
OAuth state is random, user/session-bound, expiring and atomically consumed. The query-free admin-post callback only claims returns matching this plugin's pending state for the current administrator session; other callbacks are ignored. Tokens use Sodium authenticated encryption derived from WordPress salts and the site ID; base64 only transports the encrypted bytes. Rotating WordPress salts or moving ciphertext to another site requires reconnection. Provider responses are normalized and output is escaped. Plugin logs use controlled events/numeric diagnostics, never provider response text or credentials. Third-party HTTP debugging/logging plugins must redact token exchange URLs because Meta requires credentials in those server-to-server GET parameters.
Core feed queries are intentionally uncached; indexed SQL narrows candidates and caption rules are applied in batches without N+1 queries. instagram_feed_cache_generation changes after settings, visibility, account or sync writes for downstream cache keys. Themes/CDNs caching full pages must purge their own caches after exclusions change; otherwise an old rendered page can remain visible.
Deactivation only removes scheduled work. Delete plugin data when uninstalling defaults OFF. When enabled, WordPress uninstall removes that site's media table and plugin options. On multisite each site's setting controls its own deletion. Deleting the plugin while this setting is off deliberately retains the stored token/account/media for a later reinstall; disconnect first if you want to remove the token while retaining history.
Development and verification
composer install
composer check
node --check assets/editor.js
node --check assets/carousel.js
node --test tests/JavaScript/carousel.test.cjs
On an activated local WordPress test site:
wp eval 'require WP_PLUGIN_DIR . "/atx-instagram-feed/tests/Integration/wordpress-smoke.php";'
The integration script uses an isolated temporary table, request-scoped fixture filters and an HTTP guard. It removes its temporary table afterwards and does not connect an account. PHPUnit covers domain rules, normalization, repository SQL/upserts, visibility guarantees, OAuth state/encryption/exchanges, token refresh, locks, sync failure retention, REST validation and template safety. JavaScript tests cover carousel controls, looping, RTL, reduced motion and autoplay interruption. Validation and remaining acceptance checks record tested runtimes and limitations.
Build the complete installable ZIP with python3 tools/build-release.py. It stages only runtime files/docs, generates an optimized production Composer autoloader in the staging directory, and excludes tests, development dependencies, caches and local configuration.
GitHub releases and WordPress updates
Install atx-instagram-feed.zip from the latest GitHub release. GitHub's automatically generated source archives do not include the Composer autoloader. Existing installations without this updater need this ZIP installed once; subsequent releases appear through WordPress's normal plugin update system. The Plugins screen also provides Check for updates, including in Network Admin. WordPress controls whether automatic installation is enabled.
The public repository is siko001/atx-instagram-feed; updater settings live in config/github-updater.php. No GitHub token or Instagram credentials are required for downloading updates. Checks cache successful responses for six hours and failures for five minutes. A manual check bypasses that cache. Only stable releases with the exact installable ZIP asset are offered; development branches and prereleases are excluded.
The GitHub Actions release workflow runs PHP lint/tests on PHP 8.1 and 8.3, JavaScript tests, and a production package build before publishing:
- Push to
mainormaster: publish the next patch version, or the source header version if it is higher. The first release uses the source header version. - Push a stable
vX.Y.Ztag: publish that version. - Run Release WordPress Plugin manually with a stable
X.Y.Zversion for an explicit release.
The workflow stamps the version into the packaged plugin, creates a GitHub Release, and attaches atx-instagram-feed.zip. It uses the repository's built-in GITHUB_TOKEN with contents: write; no additional secrets are needed. Tags and existing releases are never overwritten. Use a new version for corrections. The source header is the minimum release version, while the installed release reads its version from the stamped ZIP.
For a local production build, run python3 tools/build-release.py or python3 tools/build-release.py --version 1.0.2. This leaves the working Composer dependencies and source version unchanged.
Troubleshooting
- No Connect button: check that PHP sees a numeric Instagram App ID and nonempty secret; constants override environment variables. Enable Sodium if the page reports it missing.
- Consent/state failure: use the exact HTTPS callback and the same logged-in administrator/browser session that started the flow. Start again after an expired or already consumed attempt.
- Token problem: reconnect after revocation, expiration, salt rotation or moving a site's encrypted token. Existing local records remain available.
- Sync queued indefinitely: inspect
wp cron event list, ensure cron runs, and configure a server scheduler if needed. A stale operation lease expires automatically. - Empty feed despite cached rows: inspect global media types, exclusions, missing captions, manual visibility, selected account and media URL availability. Blocks skip items with no usable visual; data APIs can still expose their metadata.
- Optional fields missing: Meta's shared documentation varies by login product. The client retries rejected optional fields with a smaller set and preserves cached caption/product values when omitted. Verify caption/product fields with the selected app/version.
- Debugging: enable
INSTAGRAM_FEED_DEBUGfor controlled event names and numeric HTTP/provider codes. Never paste tokens, app secrets or full authentication URLs into logs or support requests.
Licensed GPL-2.0-or-later; see LICENSE.
Video and album styling
Playback and album scripts live in the plugin and are loaded when needed. Themes can override partials/media-visual.php as well as partials/media-item.php. Use the --instagram-feed-video-control-* properties (size, border, radius, color, background) and --instagram-feed-album-control-* (background, color, radius) for controls; --instagram-feed-album-dialog-background and --instagram-feed-album-dialog-color style the viewer. Native browser video controls retain their platform appearance. Client branding belongs in theme styles.
Reusable collections
Use Instagram Feed → Collections to create named, ordered groups of up to 100 synced posts. Choose Collection as the post source in any grid or carousel block to reuse a group. Updating a collection updates every use on the next render; layout stays independent for each block. Posts may belong to multiple collections, and hidden/trashed posts and global exclusions still apply.
Integrations can list collections with InstagramFeed\Feed::collections() and retrieve posts with instagram_feed_get_collection_media($collectionId, $args) or InstagramFeed\Feed::collection($collectionId, $args). REST provides GET /wp-json/instagram-feed/v1/collections and GET /wp-json/instagram-feed/v1/collections/{id}/media. See collections documentation for authentication, editing endpoints, pagination, and storage limits.
Follow / link button
Both blocks offer a Follow / link button panel: enable the button, edit its text and destination, choose whether to show the Instagram icon, and choose a new tab. A blank URL uses the connected account’s Instagram profile. A missing profile or invalid URL hides the button. It is a normal outbound link; following the account happens on Instagram.
For theme integrations, InstagramFeed\Feed::followLink(['text' => 'Follow us', 'url' => '', 'showIcon' => true, 'newTab' => true]) returns text, url, showIcon, target, rel, and ariaLabel, or null when no valid destination is available. Escape these values when generating custom HTML. GET /wp-json/instagram-feed/v1/follow-link returns { "button": ... } for the connected profile (or null), containing only public link data. It never calls Meta or exposes tokens. Integrations can use their own label when consuming the REST response.
The plugin renders partials/follow-button.php with neutral .instagram-feed__follow and .instagram-feed__follow-button classes. Themes can override that partial for their own markup/animation; full grid/carousel template overrides should output the provided $followButtonHtml. CSS variables --instagram-feed-button-color, --instagram-feed-button-background, and --instagram-feed-button-radius customize the default styling.
Heading text, spacing and next-slide preview
Both blocks have a Heading and spacing panel. Enable it to edit left, centre and right text directly above the feed. Each text area has independent alignment; empty areas are omitted on the frontend. A native WordPress Spacer inner block controls the gap before the media. Disabling the heading preserves its text and spacing for reuse. Text inherits theme typography; themes can override .instagram-feed__heading and the --instagram-feed-heading-font, --instagram-feed-heading-size, --instagram-feed-heading-line-height and --instagram-feed-heading-gap variables.
Carousel → Layout → Show part of the next slide adds a configurable 10–60% preview beyond the configured columns. It adapts to desktop, tablet and mobile column counts. Short feeds fit without an unnecessary partial slide. Theme carousel template overrides should output $headingHtml before their media stage and copy the data-layout / data-peek attributes from the bundled template.
Admin visibility and local time
Media initially lists posts without global exclusions. Choose Excluded posts only or All posts, including excluded to manage matching records. Status labels identify the matching hashtag, caption phrase or media-type rule. Manual hiding, Trash and previous accounts remain separately labelled. Filters run before pagination.
Admin dates and the shared block/collection picker follow WordPress's timezone and date/time formats. Set the site timezone to Europe/Malta for Malta time with daylight-saving changes. Pruning interprets the chosen date at midnight in the site timezone; stored API timestamps and query bounds remain UTC.
Album autoplay in custom integrations
The plugin album runtime also accepts data-album-autoplay="1", data-album-loop="1" and data-album-interval="6000" on an inline album root. Previous/next buttons are optional. Autoplay pauses for hover, keyboard focus, hidden/offscreen content, manual video playback and reduced motion. Integrations opening their own lightbox can dispatch instagram-feed:album-suspend and instagram-feed:album-resume on the album root.