← Back to Blog

Building an Instagram Feed Module in Drupal 10

drupalphpdrupal-modulesinstagramtutorial

Building an Instagram Feed Module in Drupal 10

So, you want to show your Instagram posts on your Drupal site? Yeah, I've had this request a bunch of times from clients. They want to display their Instagram feed right on their website without paying for some expensive third-party service.

Here's how I built my own module.

What You'll Need

  • Drupal 10 up and running
  • Some PHP knowledge (not too much, I'll walk you through it)
  • An Instagram account (to get the API token)

Getting the Instagram Token

Before we write any code, we need to get access to the Instagram API:

  1. Go to developers.facebook.com
  2. Create a new app
  3. Add "Instagram Basic Display" to your app
  4. Add yourself as a Test User
  5. Generate a long-lived access token (they expire, so this is important)

Save that token somewhere - you'll need it later.

Set Up the Module Structure

Create a folder in your Drupal installation:

modules/custom/instagram_feed/
├── instagram_feed.info.yml
├── instagram_feed.module
├── instagram_feed.routing.yml
├── src/
│   └── Controller/
│       └── InstagramFeedController.php
├── templates/
│   └── instagram-feed.html.twig
└── instagram_feed.libraries.yml

Step 1: The Info File

# instagram_feed.info.yml
name: Instagram Feed
description: Displays an Instagram feed on your website
core_version_requirement: ^10
package: Custom
version: 1.0.0
dependencies:
  - drupal:block

Step 2: The Module File

// instagram_feed.module
<?php

use Drupal\Core\Routing\RouteMatchInterface;

function instagram_feed_help($route_name, RouteMatchInterface $route_match) {
  if ($route_name == 'help.page.instagram_feed') {
    return '<p>Display Instagram feed on your website.</p>';
  }
}

function instagram_feed_theme() {
  return [
    'instagram_feed' => [
      'template' => 'instagram-feed',
      'variables' => [
        'items' => [],
        'access_token' => '',
      ],
    ],
  ];
}

Step 3: Routing

# instagram_feed.routing.yml
instagram_feed.admin_settings:
  path: '/admin/config/instagram-feed'
  defaults:
    _title: 'Instagram Feed Settings'
    _form: '\Drupal\instagram_feed\Form\InstagramFeedSettingsForm'
  requirements:
    _permission: 'administer site configuration'

Step 4: The Controller

This is where the magic happens - fetching the Instagram posts:

// src/Controller/InstagramFeedController.php
<?php

namespace Drupal\instagram_feed\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Config\ConfigFactoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use GuzzleHttp\Client;

class InstagramFeedController extends ControllerBase {

  protected $httpClient;
  protected $configFactory;

  public function __construct(Client $http_client, ConfigFactoryInterface $config_factory) {
    $this->httpClient = $http_client;
    $this->configFactory = $config_factory;
  }

  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('http_client'),
      $container->get('config.factory')
    );
  }

  public function getFeed() {
    $config = $this->configFactory->get('instagram_feed.settings');
    $access_token = $config->get('access_token');
    $limit = $config->get('limit') ?? 9;

    $items = [];

    if ($access_token) {
      try {
        $url = "https://graph.instagram.com/me/media?fields=id,caption,media_type,media_url,thumbnail_url,permalink&access_token={$access_token}&limit={$limit}";
        
        $response = $this->httpClient->get($url);
        $data = json_decode($response->getBody()->getContents(), TRUE);

        if (isset($data['data'])) {
          foreach ($data['data'] as $item) {
            $items[] = [
              'id' => $item['id'],
              'url' => $item['media_type'] === 'VIDEO' 
                ? ($item['thumbnail_url'] ?? $item['media_url']) 
                : $item['media_url'],
              'link' => $item['permalink'],
              'caption' => $item['caption'] ?? '',
              'type' => $item['media_type'],
            ];
          }
        }
      }
      catch (\Exception $e) {
        \Drupal::logger('instagram_feed')->error('Failed to fetch Instagram feed: @message', [
          '@message' => $e->getMessage(),
        ]);
      }
    }

    return [
      '#theme' => 'instagram_feed',
      '#items' => $items,
      '#cache' => [
        'tags' => ['instagram_feed_cache'],
        'max-age' => 3600,
      ],
    ];
  }
}

Step 5: Settings Form

Let admins enter their token without touching code:

// src/Form/InstagramFeedSettingsForm.php
<?php

namespace Drupal\instagram_feed\Form;

use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;

class InstagramFeedSettingsForm extends ConfigFormBase {

  protected function getEditableConfigNames() {
    return ['instagram_feed.settings'];
  }

  public function getFormId() {
    return 'instagram_feed_settings_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
    $config = $config = $this->config('instagram_feed.settings');

    $form['access_token'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Instagram Access Token'),
      '#description' => $this->t('Enter your Instagram Basic Display API access token.'),
      '#default_value' => $config->get('access_token'),
      '#required' => TRUE,
    ];

    $form['limit'] = [
      '#type' => 'number',
      '#title' => $this->t('Number of items'),
      '#default_value' => $config->get('limit') ?? 9,
      '#min' => 1,
      '#max' => 25,
    ];

    return parent::buildForm($form, $form_state);
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->config('instagram_feed.settings')
      ->set('access_token', $form_state->getValue('access_token'))
      ->set('limit', $form_state->getValue('limit'))
      ->save();

    parent::submitForm($form, $form_state);
  }
}

Step 6: The Template

{# templates/instagram-feed.html.twig #}
<div class="instagram-feed">
  {% if items %}
    <div class="instagram-feed-grid">
      {% for item in items %}
        <a href="{{ item.link }}" target="_blank" class="instagram-feed-item">
          <img src="{{ item.url }}" alt="{{ item.caption }}" loading="lazy" />
        </a>
      {% endfor %}
    </div>
  {% else %}
    <p>No Instagram posts to display.</p>
  {% endif %}
</div>

<style>
.instagram-feed-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.instagram-feed-item img {
  width: 100%;
  height: auto;
  display: block;
}
</style>

Step 7: Enable and Configure

  1. Enable it:

    drush en instagram_feed
    
  2. Go to /admin/config/instagram-feed

  3. Paste your access token

  4. Save

  5. Add the block to a region or embed in a template

Why Build Your Own?

I know what you're thinking - "why not just use a module from Drupal.org?"

Here's the thing:

  • Most free modules have limitations
  • Paid modules are expensive
  • Building your own means you control everything
  • It's actually not that hard once you see the code

Wrapping Up

That's it! You've got a working Instagram feed module. You can style it however you want, add caching, build a better UI - whatever the client needs.

The Instagram API changes sometimes, so you might need to update the endpoint URLs eventually. But the basic structure should work for a while.


Need help building custom Drupal modules? That's what I do. Let's talk.