Gatsby source plugin for building websites using Medium as a data source
npx @tessl/cli install tessl/npm-gatsby-source-medium@5.15.0gatsby-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.
npm install gatsby-source-mediumThe 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 APIAdd 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
}
}
}
}
}
`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;
}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:
https://medium.com/{username}/?format=json&limit={limit} using axiosMediumPost, MediumUser, and MediumCollection GraphQL nodesauthor___NODE and posts___NODE links])}while(1);</x> before parsingDefines 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 relationshipsMediumUser: Author information with links to their postsMediumCollection: Publication/collection data with branding and configurationThese 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;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
}
}
}
}
}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;
}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;
}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[];
}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;
}The plugin handles various error scenarios:
Error handling pattern:
try {
// Fetch and process Medium data
} catch (error) {
console.error(error);
process.exit(1);
}For full article content, consider using gatsby-source-rss as an alternative.