or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

index.md
tile.json

index.mddocs/

gatsby-source-medium

gatsby-source-medium is a Gatsby source plugin that enables developers to pull data from Medium's unofficial JSON endpoint into their Gatsby applications. It fetches the most recent 10 posts from a specified Medium user or publication and transforms this data into GraphQL nodes that can be queried in Gatsby sites.

Package Information

  • Package Name: gatsby-source-medium
  • Package Type: npm
  • Language: JavaScript
  • Installation: npm install gatsby-source-medium

Core Imports

The plugin exports Gatsby Node API functions that are automatically called by Gatsby during the build process. No direct imports are needed in your application code.

Dependencies used internally:

const axios = require('axios'); // For HTTP requests to Medium API

Basic Usage

Add the plugin to your gatsby-config.js:

// gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-medium`,
      options: {
        username: `@medium-username` // or publication slug
      }
    }
  ]
}

Query Medium data in your Gatsby components:

import React from "react"
import { graphql } from "gatsby"

export const query = graphql`
  query {
    allMediumPost(sort: { fields: [createdAt], order: DESC }) {
      edges {
        node {
          title
          virtuals {
            subtitle
            readingTime
          }
          author {
            name
          }
        }
      }
    }
  }
`

Capabilities

Plugin Configuration

Configure the plugin with Medium username or publication information.

interface PluginOptions {
  /** Medium username (with @ prefix) or publication slug */
  username: string;
  /** Optional limit for number of posts (default: 100, but Medium API typically returns max 10) */
  limit?: number;
}

Source Nodes Creation

Automatically creates GraphQL nodes from Medium data during Gatsby's build process.

/**
 * Gatsby Node API function that fetches Medium data and creates GraphQL nodes
 * Called automatically by Gatsby during build
 */
async function sourceNodes(
  { actions, createNodeId, createContentDigest },
  { username, limit }
): Promise<void>;

Implementation details:

  • Fetches data from https://medium.com/{username}/?format=json&limit={limit} using axios
  • Creates MediumPost, MediumUser, and MediumCollection GraphQL nodes
  • Establishes relationships between posts and authors via author___NODE and posts___NODE links
  • Converts timestamp numbers to YYYY-MM-DD date format
  • Default limit is 100, but Medium API typically returns max 10 most recent posts
  • Removes Medium's JSON prefix ])}while(1);</x> before parsing

Schema Customization

Defines comprehensive GraphQL schema types for Medium data structures.

/**
 * Gatsby Node API function that defines custom GraphQL schema types
 * Called automatically by Gatsby during build
 */
async function createSchemaCustomization({ actions }): Promise<void>;

Schema types created:

  • MediumPost: Article/post data with content, metadata, and author relationships
  • MediumUser: Author information with links to their posts
  • MediumCollection: Publication/collection data with branding and configuration

Internal Helper Functions

These functions are used internally by the plugin and are not part of the public API:

/**
 * Fetches data from Medium's unofficial JSON endpoint using axios
 * Internal function used by sourceNodes
 * @param username - Medium username or publication slug
 * @param limit - Maximum number of posts to fetch (default: 100)
 * @returns Promise resolving to axios response with data property
 */
function fetch(username: string, limit: number = 100): Promise<{ data: string }>;

/**
 * Removes Medium's JSON prefix '])}while(1);</x>' from API response
 * Internal function used by sourceNodes
 * @param payload - Raw response string from Medium API
 * @returns Cleaned JSON string
 */
function strip(payload: string): string;

/**
 * Recursively converts timestamp numbers to YYYY-MM-DD date format
 * Internal function used by sourceNodes
 * @param nextObj - Object to process for timestamp conversion
 * @param prevObj - Parent object (used internally for recursion)
 * @param prevKey - Parent key (used internally for recursion)
 */
function convertTimestamps(nextObj: any, prevObj?: any, prevKey?: string): void;

GraphQL Queries

Query Medium data using standard Gatsby GraphQL queries.

# Get all Medium posts
query AllMediumPosts {
  allMediumPost(sort: { fields: [createdAt], order: DESC }) {
    edges {
      node {
        id
        title
        slug
        createdAt
        virtuals {
          subtitle
          readingTime
          wordCount
          previewImage {
            imageId
          }
        }
        author {
          name
          username
        }
      }
    }
  }
}

# Get specific Medium post by ID
query MediumPostById($id: String!) {
  mediumPost(id: { eq: $id }) {
    title
    createdAt
    virtuals {
      subtitle
      readingTime
      tags {
        name
        slug
      }
    }
    author {
      name
      bio
    }
  }
}

# Get all Medium users with their posts
query AllMediumUsers {
  allMediumUser {
    edges {
      node {
        name
        username
        bio
        posts {
          title
          slug
          createdAt
        }
      }
    }
  }
}

Types

MediumPost

interface MediumPost {
  /** Unique post identifier */
  id: string;
  /** Medium's internal post ID */
  medium_id: string;
  /** Post title */
  title: string;
  /** URL slug for the post */
  slug: string;
  /** Post creation timestamp */
  createdAt: string;
  /** Last update timestamp */
  updatedAt: string;
  /** First publication timestamp */
  firstPublishedAt: string;
  /** Latest publication timestamp */
  latestPublishedAt: string;
  /** Post content and metadata */
  content: MediumPostContent;
  /** Post statistics and computed values */
  virtuals: MediumPostVirtuals;
  /** Connected author information */
  author: MediumUser;
  /** Whether post allows responses */
  allowResponses: boolean;
  /** Post visibility level */
  visibility: number;
}

MediumPostVirtuals

interface MediumPostVirtuals {
  /** Post subtitle/description */
  subtitle: string;
  /** Estimated reading time in minutes */
  readingTime: number;
  /** Total word count */
  wordCount: number;
  /** Number of images in post */
  imageCount: number;
  /** Preview/header image information */
  previewImage: {
    imageId: string;
    width: number;
    height: number;
  };
  /** Post tags */
  tags: Array<{
    name: string;
    slug: string;
    postCount: number;
  }>;
  /** Number of recommendations/claps */
  totalClapCount: number;
  /** Number of responses */
  responsesCreatedCount: number;
}

MediumUser

interface MediumUser {
  /** Unique user identifier */
  id: string;
  /** Medium's internal user ID */
  userId: string;
  /** User's display name */
  name: string;
  /** Medium username */
  username: string;
  /** User biography */
  bio: string;
  /** Profile image ID */
  imageId: string;
  /** Account creation timestamp */
  createdAt: string;
  /** Twitter username if connected */
  twitterScreenName: string;
  /** Array of user's posts */
  posts: MediumPost[];
}

MediumCollection

interface MediumCollection {
  /** Unique collection identifier */
  id: string;
  /** Medium's internal collection ID */
  medium_id: string;
  /** Collection name */
  name: string;
  /** URL slug */
  slug: string;
  /** Collection description */
  description: string;
  /** Short description for collection */
  shortDescription: string;
  /** Collection tags */
  tags: string[];
  /** Collection creator ID */
  creatorId: string;
  /** Subscriber count */
  subscriberCount: number;
  /** Collection tagline */
  tagline: string;
  /** Collection type */
  type: string;
  /** Collection image configuration */
  image: MediumCollectionImage;
  /** Collection metadata with follower info */
  metadata: MediumCollectionMetadata;
  /** Collection virtual/computed properties */
  virtuals: MediumCollectionVirtuals;
  /** Collection logo configuration */
  logo: MediumCollectionLogo;
  /** Twitter username if linked */
  twitterUsername: string;
  /** Facebook page name if linked */
  facebookPageName: string;
  /** Public email address */
  publicEmail: string;
  /** Custom domain if configured */
  domain: string;
  /** Collection sections configuration */
  sections: MediumCollectionSections[];
  /** Theme tint color */
  tintColor: string;
  /** Whether to use light text */
  lightText: boolean;
  /** Favicon configuration */
  favicon: MediumCollectionFavicon;
  /** Color palette settings */
  colorPalette: MediumCollectionColorPalette;
  /** Navigation items */
  navItems: MediumCollectionNavItems[];
  /** Color behavior setting */
  colorBehavior: number;
  /** Instant Articles state */
  instantArticlesState: number;
  /** Accelerated Mobile Pages state */
  acceleratedMobilePagesState: number;
  /** AMP logo configuration */
  ampLogo: MediumCollectionAmpLogo;
  /** Collection header configuration */
  header: MediumCollectionHeader;
  /** Date when paid for domain */
  paidForDomainAt: string;
}

interface MediumCollectionImage {
  imageId: string;
  filter: string;
  backgroundSize: string;
  originalWidth: number;
  originalHeight: number;
  strategy: string;
  height: number;
  width: number;
}

interface MediumCollectionMetadata {
  followerCount: number;
  activeAt: string;
}

interface MediumCollectionVirtuals {
  permissions: MediumCollectionVirtualsPermissions;
  isSubscribed: boolean;
  isEnrolledInHightower: boolean;
  isEligibleForHightower: boolean;
  isSubscribedToCollectionEmails: boolean;
  isMuted: boolean;
  canToggleEmail: boolean;
}

interface MediumCollectionVirtualsPermissions {
  canPublish: boolean;
  canPublishAll: boolean;
  canRepublish: boolean;
  canRemove: boolean;
  canManageAll: boolean;
  canSubmit: boolean;
  canEditPosts: boolean;
  canAddWriters: boolean;
  canViewStats: boolean;
  canSendNewsletter: boolean;
  canViewLockedPosts: boolean;
  canViewCloaked: boolean;
  canEditOwnPosts: boolean;
  canBeAssignedAuthor: boolean;
  canEnrollInHightower: boolean;
  canLockPostsForMediumMembers: boolean;
  canLockOwnPostsForMediumMembers: boolean;
  canViewNewsletterV2Stats: boolean;
  canCreateNewsletterV3: boolean;
}

interface MediumCollectionLogo {
  imageId: string;
  filter: string;
  backgroundSize: string;
  originalWidth: number;
  originalHeight: number;
  strategy: string;
  height: number;
  width: number;
}

interface MediumCollectionSections {
  type: number;
  collectionHeaderMetadata: MediumCollectionSectionsCollectionHeaderMetadata;
  postListMetadata: MediumCollectionSectionsPostListMetadata;
}

interface MediumCollectionSectionsCollectionHeaderMetadata {
  title: string;
  description: string;
  backgroundImage: MediumCollectionSectionsCollectionHeaderMetadataBackgroundImage;
  logoImage: MediumCollectionSectionsCollectionHeaderMetadataLogoImage;
  alignment: number;
  layout: number;
}

interface MediumCollectionSectionsCollectionHeaderMetadataBackgroundImage {
  id: string;
  originalWidth: number;
  originalHeight: number;
  focusPercentX: number;
  focusPercentY: number;
}

interface MediumCollectionSectionsCollectionHeaderMetadataLogoImage {
  id: string;
  originalWidth: number;
  originalHeight: number;
  alt: string;
}

interface MediumCollectionSectionsPostListMetadata {
  source: number;
  layout: number;
  number: number;
}

interface MediumCollectionFavicon {
  imageId: string;
  filter: string;
  backgroundSize: string;
  originalWidth: number;
  originalHeight: number;
  strategy: string;
  height: number;
  width: number;
}

interface MediumCollectionColorPalette {
  defaultBackgroundSpectrum: MediumCollectionColorPaletteDefaultBackgroundSpectrum;
  tintBackgroundSpectrum: MediumCollectionColorPaletteTintBackgroundSpectrum;
  highlightSpectrum: MediumCollectionColorPaletteHighlightSpectrum;
}

interface MediumCollectionColorPaletteDefaultBackgroundSpectrum {
  colorPoints: MediumCollectionColorPaletteDefaultBackgroundSpectrumColorPoints[];
  backgroundColor: string;
}

interface MediumCollectionColorPaletteDefaultBackgroundSpectrumColorPoints {
  color: string;
  point: number;
}

interface MediumCollectionColorPaletteTintBackgroundSpectrum {
  colorPoints: MediumCollectionColorPaletteTintBackgroundSpectrumColorPoints[];
  backgroundColor: string;
}

interface MediumCollectionColorPaletteTintBackgroundSpectrumColorPoints {
  color: string;
  point: number;
}

interface MediumCollectionColorPaletteHighlightSpectrum {
  colorPoints: MediumCollectionColorPaletteHighlightSpectrumColorPoints[];
  backgroundColor: string;
}

interface MediumCollectionColorPaletteHighlightSpectrumColorPoints {
  color: string;
  point: number;
}

interface MediumCollectionNavItems {
  type: number;
  title: string;
  url: string;
  topicId: string;
  source: string;
}

interface MediumCollectionAmpLogo {
  imageId: string;
  filter: string;
  backgroundSize: string;
  originalWidth: number;
  originalHeight: number;
  strategy: string;
  height: number;
  width: number;
}

interface MediumCollectionHeader {
  title: string;
  description: string;
  backgroundImage: MediumCollectionHeaderBackgroundImage;
  logoImage: MediumCollectionHeaderLogoImage;
  alignment: number;
  layout: number;
}

interface MediumCollectionHeaderBackgroundImage {
  id: string;
  originalWidth: number;
  originalHeight: number;
  focusPercentX: number;
  focusPercentY: number;
}

interface MediumCollectionHeaderLogoImage {
  id: string;
  originalWidth: number;
  originalHeight: number;
  alt: string;
}

Error Handling

The plugin handles various error scenarios:

  • Network errors: Failed requests to Medium's API will cause the build to exit with error code 1
  • Invalid username: Non-existent usernames or publications will result in empty data sets
  • API rate limiting: Medium may rate limit requests, causing temporary failures
  • Malformed JSON: Invalid responses from Medium's API are caught and logged

Error handling pattern:

try {
  // Fetch and process Medium data
} catch (error) {
  console.error(error);
  process.exit(1);
}

Limitations

  • Post limit: Due to Medium API limitations, only the 10 most recent posts are available
  • Content preview: The JSON endpoint provides only post previews, not full article content
  • Unofficial API: Uses Medium's unofficial JSON endpoint which may change without notice
  • No authentication: Cannot access private or member-only content

For full article content, consider using gatsby-source-rss as an alternative.