Roll your own Instagram gallery in 5 mins
Tags: vue, javascript
Published: 2020-08-28 16:00:26

A client had asked me about showing Instagram pictures from their own account on their website. A bit similar to Twitter's own [timeline widget](https://developer.twitter.com/en/docs/twitter-for-websites/timelines/overview).

However, Instagram doesn't have a widget to display pictures in a feed. So I built one myself. This is a post about how you could do that for your website too. It took a bit of reverse-engineering but it all worked in the end and hopefully stays that way.🤞

Although this post talks about a single Instagram account, you could modify what I show here to pull images from other accounts too. I make no guarantees!

## The Facts

* Instagram has a [developer API platform](https://developers.facebook.com/docs/instagram).
* The method this post covers is only for publicly accessible accounts.
* If you need to show pictures from a private account, you must use the official Instagram API platform to register an app, get an API key, get it reviewed etc. The same applies if you want to use their platform to even show public images in a feed, unlike Twitter's timeline widget for public profiles.

## The Query Hash

Navigate to the Instagram account you want to show pictures from and open the Developer Tools and click on the **Network** tab. With the Network tab open, refresh the Instagram page you are viewing. Then click on the **XHR** filter. Here's what it looks like for me in the latest Firefox browser.

![Firefox Developer Tools](https://dev-to-uploads.s3.amazonaws.com/i/x1rs06ptml4oob63y5l1.png)
<figcaption>Network tab showing only XHRs</figcaption>

Instagram uses GraphQL to fetch the posts, where each post contains URLs to different sizes of the picture, stats (likes, comments etc.), author and much more. We need the query hash from the request so we can make the same API call ourselves.

You can find the query hash from the request made to the endpoint `https://www.instagram.com/graphql/query/`. Once again you can filter the requests using the textbox to find the specific request.

![Instagram GraphQL API](https://dev-to-uploads.s3.amazonaws.com/i/4qwzxz5o4oqp7znxg7si.png)
<figcaption>Instagram GraphQL API request</figcaption>

Here's the full URL from Developer Tools window:
`https://www.instagram.com/graphql/query/?query_hash=d4d88dc1500312af6f937f7b804c68c3&variables={"user_id":"249074882","include_chaining":false,"include_reel":false,"include_suggested_users":false,"include_logged_out_extras":true,"include_highlight_reels":true,"include_live_status":true}`

Save the value of the `query_hash` query-parameter for later. We'll plug that into our own request to the same API endpoint.

## The Vue Component

While the following is a Vue component, you could very easily do the same with any other front-end framework.

Declare some constants for our API calls.

```js
// These two URLs will be the same regardless of the account
// you want to show pictures from.
const IG_API_URL = "https://www.instagram.com/graphql/query/";
const IG_POST_URL = "https://www.instagram.com/p";

// The `query_hash` value from before.
const TIMELINE_QUERY_HASH = "d4d88dc1500312af6f937f7b804c68c3";
// The number of images to fetch per page.
const IMAGES_PER_PAGE = 5;
```

Make the API call

```js
async fetchData(after) {
  const variables = {"id":"249074882","first":IMAGES_PER_PAGE};
  const api = axios.default.create();
  const resp = await api.get(IG_API_URL, {
    params: {
      "query_hash": TIMELINE_QUERY_HASH,
      "variables": JSON.stringify(variables)
    }
  });
  return resp.data;
}
```

Transform the data for display. Feel free to modify this according to what you want to show.

```js
transformData(timeline) {
  if (!timeline.hasOwnProperty("edges")) {
    return;
  }
  const edges = timeline.edges;
  edges.forEach(edge => {
    if (!edge.hasOwnProperty("node") || edge.node["__typename"] !== "GraphImage") {
      return;
    }
    const node = edge.node;
    const thumbnails = node["thumbnail_resources"];
    const urls = thumbnails.filter(t => t["config_width"] === this.imgProps.w);
    let url;
    if (!urls || !urls.length) {
      url = thumbnails[0].src;
    } else {
      url = urls[0].src;
    }
    this.posts.push({
      url,
      href: `${IG_POST_URL}/${node["shortcode"]}`
    });
  });
 }
}
```

And that's it. With just those few lines, we have made an API call and retrieved a page of Instagram posts just like Instagram's own official site.

### Putting it all together

```vue
<template>
  <section class="instagram mb-10">
    <div class="d-flex flex-row flex-md-row flex-column flex-sm-column justify-center align-center">
      <v-card
        v-for="(post, index) in posts"
        :key="index"
        tile
        class="pa-2 ma-2"
      >
        <a
          :href="post.href"
          class="post"
          rel="noopener"
          target="_blank"
        >
          <img
            :src="post.url"
            width="100%"
            height="auto"
          >
        </a>
      </v-card>
    </div>
  </section>
</template>
<script>
import * as axios from "axios";

const IG_API_URL = "https://www.instagram.com/graphql/query/";
const IG_POST_URL = "https://www.instagram.com/p";
const TIMELINE_QUERY_HASH = "9dcf6e1a98bc7f6e92953d5a61027b98";
const IMAGES_PER_PAGE = 5;

export default {
    name: "Instagram",
    data() {
      return {
        posts: [],
        imgProps: {
          w: 320,
          h: 320
        }
      };
    },
    async created() {
      const resp = await this.fetchData();
      if (!resp.data.hasOwnProperty("user") || !resp.data.user.hasOwnProperty("edge_owner_to_timeline_media")) {
        return;
      }

      this.transformData(resp.data.user.edge_owner_to_timeline_media);
    },
    methods: {
      async fetchData() {
        const variables = {"id":"20214540375","first":IMAGES_PER_PAGE};
        const api = axios.default.create();
        const resp = await api.get(IG_API_URL, {
          params: {
            "query_hash": TIMELINE_QUERY_HASH,
            "variables": JSON.stringify(variables)
          }
        });
        return resp.data;
      },

      transformData(timeline) {
        if (!timeline.hasOwnProperty("edges")) {
          return;
        }
        const edges = timeline.edges;
        edges.forEach(edge => {
          if (!edge.hasOwnProperty("node") || edge.node["__typename"] !== "GraphImage") {
            return;
          }
          const node = edge.node;
          const thumbnails = node["thumbnail_resources"];
          const urls = thumbnails.filter(t => t["config_width"] === this.imgProps.w);
          let url;
          if (!urls || !urls.length) {
            url = thumbnails[0].src;
          } else {
            url = urls[0].src;
          }
          this.posts.push({
            url,
            href: `${IG_POST_URL}/${node["shortcode"]}`
          });
        });
      }
    }
}
</script>
<style lang="scss" scoped>

.instagram {
  overflow: hidden;
}

@media all and (max-width: 640px) {
  .instagram {
    .ig-container {
      .columns {
        display: flex;
        justify-content: center;
      }
    }
  }
}
</style>
```

That's it for now. If you like this post and want me to write about something else, please let me know! Until next time, see ya! 👋