Curated Instagram Wall
A self-hosted, curated Instagram-style grid for WordPress. Images live in the media library; no API, no oEmbed, no hotlinking.
by Kushagra Bindal · github.com/kushagra-bindal/curated-instagram-wall · 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/kushagra-bindal/curated-instagram-wall/archive/refs/heads/main.zipReadme
Curated Instagram Wall
A self-hosted WordPress plugin that shows a curated Instagram-style feed as a clean profile grid.
The core principle
Every image is uploaded into the WordPress media library and served from the same server as the site. There is no Instagram API, no access token, no oEmbed, no embedded iframe and no hotlinked CDN URL anywhere. The expiring-URL problem that turns other Instagram feeds into black tiles simply cannot happen: all content is local, so it always loads.
The grid mimics an Instagram profile grid: 3:4 portrait media (Instagram's current grid ratio), tight gaps, a centred play triangle on reels, a stacked-squares badge on carousels, and no like/comment/share UI at all. Above the grid sits a small avatar + name; below it, a centred Load More + Follow on Instagram button pair.
Screenshots
| Grid | Lightbox |
|---|---|
![]() |
![]() |
| Card editor | Settings |
|---|---|
![]() |
![]() |
Installation
- Download the latest release zip, or clone this repository into
wp-content/plugins/curated-instagram-wall. - Plugins → Add New → Upload Plugin, choose the zip, Install Now, Activate.
- A new Instagram Wall menu appears in the sidebar.
Requires WordPress 6.0+ and PHP 7.4+.
Adding a card
Go to Instagram Wall → Add New. The same steps are shown on that screen. Only the fields for the chosen media type appear.
- Title: internal label only, e.g. "Summer workshop 2026 – reel". Visitors never see it.
- Instagram post URL: on Instagram open the post → Share → Copy link, paste it in. Required; it powers the "View on Instagram" links.
- Media type: the upload fields switch automatically.
- Photo: upload one image.
- Carousel: upload all the slides, in order (drag the thumbnails to reorder; the first slide is the card image). The card becomes a swipeable slider.
- Reel: upload a thumbnail image (screenshot the reel cover; required), and optionally the MP4 video. With a video the reel plays in a lightbox on the page; without one, clicking opens Instagram.
- Caption: plain text, shown Instagram-style with a "more" toggle.
- Order (Page Attributes): lower numbers appear first. Then Publish.
Publishing is blocked (saved as draft, with a notice saying exactly what is missing) until the card has its link and its picture(s). Incomplete cards are flagged in the list table and never render on the front end.
What happens when a visitor clicks a card
| Card | Click result |
|---|---|
| Photo | Lightbox with the large image + "View on Instagram" link |
| Carousel | Swipeable on the card itself (arrows/dots/touch); click opens a lightbox with the full swipeable set + link |
| Reel with MP4 | Opens a centred lightbox and starts playing immediately (native controls + link) |
| Reel without MP4 | Opens the Instagram post in a new tab |
Who can add cards
Cards use the standard post capabilities, so Authors, Editors and Contributors manage them with the accounts they already have. Only the Settings page is Administrator-only (manage_options).
Settings
Instagram Wall → Settings (Administrators only):
| Section | Setting | Default | Notes |
|---|---|---|---|
| Grid layout | Desktop / Tablet / Mobile columns | 5 / 3 / 2 | Breakpoints at 1024px and 600px; values 1–12 |
| Grid layout | Captions | Hidden | Or Below the picture (with a "more" toggle), or On hover (overlay, always visible on touch) |
| Profile | Show header | On | Avatar + name above the grid; skipped entirely when nothing is filled in |
| Profile | Avatar | – | Any media library image; square works best |
| Profile | Display name | – | Bold text next to the avatar; falls back to @handle |
| Profile | Instagram handle | – | Without the @ |
| Profile | Profile URL | – | Target of the Follow on Instagram button; leave empty to hide it |
| Load more | Posts shown at first | 10 | Tiles visible before the first click (1–100) |
| Load more | Posts per click | 10 | Tiles revealed per click (1–100) |
Shortcode
Add the shortcode to any page (Shortcode block in the block editor):
[ciw_wall]
[ig_feed] is registered as an alias and behaves identically:
[ig_feed]
Attributes
Anything in the shortcode beats the Settings page:
[ciw_wall desktop="4" tablet="2" mobile="1" limit="12" caption="hover" header="no" initial="8" batch="4"]
| Attribute | Values | Meaning |
|---|---|---|
desktop / tablet / mobile |
1–12 | Columns at each screen size |
limit |
integer | Maximum number of cards; 0 or omitted shows all |
caption |
below, hover, off |
Caption mode (also accepts yes/no) |
header |
yes, no |
Show the profile header |
initial |
1–100 | Cards visible before Load More |
batch |
1–100 | Cards revealed per Load More click |
The same attributes work on [ig_feed]. Cards are ordered by the Order field (ascending), then newest first.
How it works (for developers)
- Prefixes:
ciw_/CIW_. Text domain:curated-instagram-wall. - Post type
ciw_post:capability_type => 'post',map_meta_cap => true,public => false,show_ui => true. Classic editor on purpose (show_in_rest => false): the publish-blocking validation and its notices need the synchronouspost.phpsave flow. - Meta keys:
_ciw_caption,_ciw_url,_ciw_media_type(photo/reel/carousel),_ciw_image_id(photo image / reel thumbnail),_ciw_gallery_ids(ordered array of carousel slide attachment IDs),_ciw_video_id. All registered viaregister_post_meta()with sanitize and auth callbacks. - Option:
ciw_settings(a single array, sanitised as a whole byCIW_Settings::sanitize()), registered in theciw_settings_groupsettings group. Settings page slug:ciw-settings. - Per-type renderability (
ciw_card_is_renderable()): a validinstagram.comURL and the required local media: photo needs an image, reel needs a thumbnail image, carousel needs at least one slide. This one function gates both publishing and front-end output. - Publish validator:
CIW_Meta::validate()runs onsave_post_ciw_postat priority 20, after the meta box save at priority 10, on every save path (form, quick edit, programmatic). Apublishorfuturecard that is not renderable is demoted todraft, and aciw_blockedquery arg drives the admin notice. - Attachment validation: every attachment ID is checked (
get_post_type() === 'attachment',wp_attachment_is_image()for images) on save and on read. - Asset loading: front-end CSS/JS (handle
ciw-feed) are registered onwp_enqueue_scriptsand enqueued only when the shortcode actually renders. Admin CSS/JS (handleciw-admin) load only on the plugin's own screens; the media modal and jQuery UI sortable are enqueued only on the editor and settings screens, never on the list table. - JavaScript: the front end is vanilla JS with no jQuery. One small slider engine powers both card carousels and the lightbox carousel (arrows, dots, pointer-event touch swipe with drag-vs-click detection). Admin JS is vanilla too, with jQuery UI sortable used only for drag-reordering slides; add/remove still work without it. Localised strings arrive via
ciwL10nandciwAdminL10n. - Progressive enhancement: photo/reel media are real
<a>tags to the Instagram post; carousel slides are each a plain link and only the first slide shows without JS; Load More extras are hidden only once JS adds.ciw-feed--js; conditional admin fields render their initial state server-side. - Styling hooks: BEM-style
ciw-*classes; column counts via CSS custom properties written inline on the grid (--ciw-cols-desktop,--ciw-cols-tablet,--ciw-cols-mobile); media locked to 3:4 viaaspect-ratio: 3/4with a padding-bottom fallback. - Images: lazy-loaded (
loading="lazy"); cards usemedium_large, the lightbox useslarge. - No external requests: the plugin never calls Instagram or any other remote service, on the front end or in the admin.
- Deactivation leaves all content and settings untouched. Uninstall keeps data by default; see below.
Uninstall and data removal
Deleting the plugin keeps every card and setting unless you opt in to removal. To remove them, do one of:
// wp-config.php
define( 'CIW_UNINSTALL_REMOVE_DATA', true );
wp option update ciw_uninstall_remove_data 1
With either in place, uninstalling deletes the ciw_settings option, the flag itself, and every ciw_post post with its meta. Media library files are never deleted.
Known limitations
- The curated feed is queried with
posts_per_page => -1and then filtered in PHP (renderability check, thenlimit). This suits tens to low hundreds of cards, which is the intended use; it is not built for thousands. - Classic editor only, by design (see above). The card editor does not support the block editor.
- One profile per site: a single avatar, name, handle and profile URL in settings.
- No dedicated block or widget; use the shortcode.
Roadmap
Ideas under consideration, not commitments:
- A block that wraps the shortcode with a live preview.
- Paged or AJAX loading for very large feeds.
- Bulk creation of cards from existing media library images.
Contributing
Coding standards are enforced with PHPCS (WordPress-Extra + WordPress-Docs):
composer install
composer lint
License
GPL-2.0-or-later. See LICENSE.
Originally built for a school website; published here with permission as a generic plugin.



