Medium.com sucks, here's how I improved it

Published: 2022-07-17 - Updated: 2022-08-03

Medium.com

You've probably at some point visited it when searching for something, and you may notice how utterly bloated it is and how slow it loads. I got the following measurements when loading it in, over a hundred network requests.

Of course it will keep sending requests, as it sends back "analytics" about the user every 50 seconds. These contain your browsers width/height along with how far you've scrolled and for how long you've been viewing the article.

It works fine without Javascript "kinda", so it's beyond me why they're sending so many wasteful requests. As they can could save tons of bandwith and potential load on the server. If you haven't visited the article before with Javascript, it will only show a preview. Also you're only able to read three articles before being prompted to create an account.

It's also behind Cloudflare, whom may or not block you if you're connecting over the Tor.

A simple proxy

I decided to write my own frontend to Medium.com that's more lightweight (only shows the article nothing else). It should also be able to bypass the paywalls/account prompts.

First thought was just to download the page on the given url and boom take the HTML and return it. Sadly, it wasn't that simple, as Medium.com only sends a small preview of the article.

But after looking at the requests the client sends, I found this massive request that gets the contents of the article in JSON format. Which I think is used to reassemble the preview orginally sent by the server, which is kinda a strange way of doing it.

fetch("https://medium.com/_/graphql", {
    "body": `[{\"operationName\":\"PostViewerEdgeContentQuery\",\"variables\":{\"postId\":\"7c9c2aee211\",\"postMeteringOptions\":{\"referrer\":\"\"}},\"query\":\"query PostViewerEdgeContentQuery($postId: ID!, $postMeteringOptions: PostMeteringOptions) {\\n  post(id: $postId) {\\n    ... on Post {\\n      id\\n      viewerEdge {\\n        id\\n        fullContent(postMeteringOptions: $postMeteringOptions) {\\n          isLockedPreviewOnly\\n          validatedShareKey\\n          bodyModel {\\n            ...PostBody_bodyModel\\n            __typename\\n          }\\n          __typename\\n        }\\n        __typename\\n      }\\n      __typename\\n    }\\n    __typename\\n  }\\n}\\n\\nfragment PostBody_bodyModel on RichText {\\n  sections {\\n    name\\n    startIndex\\n    textLayout\\n    imageLayout\\n    backgroundImage {\\n      id\\n      originalHeight\\n      originalWidth\\n      __typename\\n    }\\n    videoLayout\\n    backgroundVideo {\\n      videoId\\n      originalHeight\\n      originalWidth\\n      previewImageId\\n      __typename\\n    }\\n    __typename\\n  }\\n  paragraphs {\\n    id\\n    ...PostBodySection_paragraph\\n    __typename\\n  }\\n  ...normalizedBodyModel_richText\\n  __typename\\n}\\n\\nfragment PostBodySection_paragraph on Paragraph {\\n  name\\n  ...PostBodyParagraph_paragraph\\n  __typename\\n  id\\n}\\n\\nfragment PostBodyParagraph_paragraph on Paragraph {\\n  name\\n  type\\n  ...ImageParagraph_paragraph\\n  ...TextParagraph_paragraph\\n  ...IframeParagraph_paragraph\\n  ...MixtapeParagraph_paragraph\\n  __typename\\n  id\\n}\\n\\nfragment ImageParagraph_paragraph on Paragraph {\\n  href\\n  layout\\n  metadata {\\n    id\\n    originalHeight\\n    originalWidth\\n    focusPercentX\\n    focusPercentY\\n    alt\\n    __typename\\n  }\\n  ...Markups_paragraph\\n  ...ParagraphRefsMapContext_paragraph\\n  ...PostAnnotationsMarker_paragraph\\n  __typename\\n  id\\n}\\n\\nfragment Markups_paragraph on Paragraph {\\n  name\\n  text\\n  hasDropCap\\n  dropCapImage {\\n    ...MarkupNode_data_dropCapImage\\n    __typename\\n    id\\n  }\\n  markups {\\n    type\\n    start\\n    end\\n    href\\n    anchorType\\n    userId\\n    linkMetadata {\\n      httpStatus\\n      __typename\\n    }\\n    __typename\\n  }\\n  __typename\\n  id\\n}\\n\\nfragment MarkupNode_data_dropCapImage on ImageMetadata {\\n  ...DropCap_image\\n  __typename\\n  id\\n}\\n\\nfragment DropCap_image on ImageMetadata {\\n  id\\n  originalHeight\\n  originalWidth\\n  __typename\\n}\\n\\nfragment ParagraphRefsMapContext_paragraph on Paragraph {\\n  id\\n  name\\n  text\\n  __typename\\n}\\n\\nfragment PostAnnotationsMarker_paragraph on Paragraph {\\n  ...PostViewNoteCard_paragraph\\n  __typename\\n  id\\n}\\n\\nfragment PostViewNoteCard_paragraph on Paragraph {\\n  name\\n  __typename\\n  id\\n}\\n\\nfragment TextParagraph_paragraph on Paragraph {\\n  type\\n  hasDropCap\\n  ...Markups_paragraph\\n  ...ParagraphRefsMapContext_paragraph\\n  __typename\\n  id\\n}\\n\\nfragment IframeParagraph_paragraph on Paragraph {\\n  iframe {\\n    mediaResource {\\n      id\\n      iframeSrc\\n      iframeHeight\\n      iframeWidth\\n      title\\n      __typename\\n    }\\n    __typename\\n  }\\n  layout\\n  ...getEmbedlyCardUrlParams_paragraph\\n  ...Markups_paragraph\\n  __typename\\n  id\\n}\\n\\nfragment getEmbedlyCardUrlParams_paragraph on Paragraph {\\n  type\\n  iframe {\\n    mediaResource {\\n      iframeSrc\\n      __typename\\n    }\\n    __typename\\n  }\\n  __typename\\n  id\\n}\\n\\nfragment MixtapeParagraph_paragraph on Paragraph {\\n  type\\n  mixtapeMetadata {\\n    href\\n    mediaResource {\\n      mediumCatalog {\\n        id\\n        __typename\\n      }\\n      __typename\\n    }\\n    __typename\\n  }\\n  ...GenericMixtapeParagraph_paragraph\\n  __typename\\n  id\\n}\\n\\nfragment GenericMixtapeParagraph_paragraph on Paragraph {\\n  text\\n  mixtapeMetadata {\\n    href\\n    thumbnailImageId\\n    __typename\\n  }\\n  markups {\\n    start\\n    end\\n    type\\n    href\\n    __typename\\n  }\\n  __typename\\n  id\\n}\\n\\nfragment normalizedBodyModel_richText on RichText {\\n  paragraphs {\\n    markups {\\n      type\\n      __typename\\n    }\\n    ...getParagraphHighlights_paragraph\\n    ...getParagraphPrivateNotes_paragraph\\n    __typename\\n  }\\n  sections {\\n    startIndex\\n    ...getSectionEndIndex_section\\n    __typename\\n  }\\n  ...getParagraphStyles_richText\\n  ...getParagraphSpaces_richText\\n  __typename\\n}\\n\\nfragment getParagraphHighlights_paragraph on Paragraph {\\n  name\\n  __typename\\n  id\\n}\\n\\nfragment getParagraphPrivateNotes_paragraph on Paragraph {\\n  name\\n  __typename\\n  id\\n}\\n\\nfragment getSectionEndIndex_section on Section {\\n  startIndex\\n  __typename\\n}\\n\\nfragment getParagraphStyles_richText on RichText {\\n  paragraphs {\\n    text\\n    type\\n    __typename\\n  }\\n  sections {\\n    ...getSectionEndIndex_section\\n    __typename\\n  }\\n  __typename\\n}\\n\\nfragment getParagraphSpaces_richText on RichText {\\n  paragraphs {\\n    layout\\n    metadata {\\n      originalHeight\\n      originalWidth\\n      __typename\\n    }\\n    type\\n    ...paragraphExtendsImageGrid_paragraph\\n    __typename\\n  }\\n  ...getSeriesParagraphTopSpacings_richText\\n  ...getPostParagraphTopSpacings_richText\\n  __typename\\n}\\n\\nfragment paragraphExtendsImageGrid_paragraph on Paragraph {\\n  layout\\n  type\\n  __typename\\n  id\\n}\\n\\nfragment getSeriesParagraphTopSpacings_richText on RichText {\\n  paragraphs {\\n    id\\n    __typename\\n  }\\n  sections {\\n    startIndex\\n    __typename\\n  }\\n  __typename\\n}\\n\\nfragment getPostParagraphTopSpacings_richText on RichText {\\n  paragraphs {\\n    layout\\n    text\\n    __typename\\n  }\\n  sections {\\n    startIndex\\n    __typename\\n  }\\n  __typename\\n}\\n\"}]`,
    "method": "POST",
    "headers": {
        "content-type": "application/json",
        "graphql-operation": "PostViewerEdgeContentQuery"
    }
}

This returns something like this, an array of each paragraph in the article. The type dicates what HTML tag should be used for the text, such as P, H3, H4, IMG. But it can also contain their own custom tags, such as MIXTAPE_EMBED.

Then a paragraph may contain markups which detail how the line should be formatted, the type works similar to the thing above, so it could be EM, STRONG, A. start decides at which character position the tag should be begin at, and end does the opposite.

"paragraphs": [
{
    "id": "9dd4b3e86a27_3",
    "name": "bf60",
    "type": "P",
    "text": "In Start with Why, Simon Sinek (2011) explains how Martin Luther King Jr.",
    "markups": [
        {
            "type": "EM",
            "href": null,
            "start": 3,
            "end": 17,
            "__typename": "Markup"
        },
    ],
    "__typename": "Paragraph",
}

The example above will be parsed into the following HTML

<P>In<EM> Start with Why</EM>, Simon Sinek (2011) explains how Martin Luther King Jr.</P>

I decide to write the proxy in NodeJS, as its what am most familiar with when it comes to webservers. The source code and instructions on how to setup your own can be found on my git. There are a few I'd like to add, but I don't really have any need for personally such as:

Currently it simply returns the HTML of the article, nothing more. My instance can be found on medium.urof.net Tor.

Automatic redirection to the proxy

I've yet to find an extension that allows me to write custom rewrite, I currently do it manually by swapping out medium.com to medium.urof.net. This also works for articles that are served on custom domains, but still using the Medium.com backend.

If you were to find an extension to do it, you can use the following regex. URLS end with an 12 character hexadecimal string. Which is all you need to get the article. Therefore my proxy accepts any path that matches the following regex

/[0-9-a-f]{12}$/
Questions or comments? contact me!