Medium API 爬取資料與突破 Cloudflare 防護|完整 GraphQL 操作教學
針對 Medium 使用者擔心文章無備份、Cloudflare 阻擋爬蟲的痛點,詳解如何透過 Medium API 與 GraphQL 取得追蹤人數、文章列表及內容,並示範用 Cloudflare Worker 成功繞過防護,實現自動備份與資料同步。
基於 SEO 考量,本文標題與描述經 AI 調整,原始版本請參考內文。
文章目錄
Medium API 資料爬蟲與 Cloudflare 攻防的心路歷程
使用 Medium Graphql Private API 爬取資料、備份文章及 Cloudflare 驗證阻擋突破方式。
背景
自 2018 年開始經營 Medium 撰寫文章,大約在 2022 年時累積的文章數量已超過 50+ 篇,接近 6+ 萬字加上約 3 GB 的文章圖片 (直至 2025 已破 120+ 篇/10+萬字/6 GB+圖片); 沒有備份! 沒有備份! 沒有備份! ,因為我一直以來都直接用 Medium 寫文章(編輯器真的好用), 寫完就直接儲存發佈,本地沒有任何備份。
那時候有一股不安感油然而生,如果哪一天 Medium 倒了我的文章不就不見了?(而且那幾年 Medium 狀況一直都很不好,沒有賺錢) 或是我的帳號突然被停權心血不就都付之一炬了?
於是在那年開始研究如何爬取 Medium 資料、尤其是文章內容跟如何下載備份。
Medium HTML ❌
結構非常複雜、易改變、爬取緩慢,直接起瀏覽器爬內容是下下策。
Medium Official API ❌
Warning The Medium API is no longer supported. We do not recommend using it.
Medium 曾經提供過官方的 API 接口,但是應該在推出沒多久就停用了;推測應該是跟產品目標不同,Medium 是私域流量應該要盡量把內容留在站內,而不是開 API 讓外部有機會串接拿到資料。
Medium Unofficial API 🤔
也因為官方不提供 API,有民間大神自行開發對外的 API 接口讓有需求的使用者串接;但其中原理我推測也是打 Medium Private API,只是過了它那一層的封裝橋接,幫你把請求轉換成 Private API 取得原始資料。
收費也不便宜,約每 2,500次請求 $USD 5 美元 ;這個服務做蠻久的,但既然最終都是打 Medium Private API,那我自己嗅探自己打就好了。
Medium Private API ✅
何謂 Private API? 就是這個 API 接口 並非給外部使用、沒有文件、可能隨時停用、可能有法律風險 ;等於 直接去看他網站 / App 的行為找到呼叫的對應 API ,自己寫程式重用這個 API 撈資料到自己的服務上。
https://medium.com/_/graphql
經過網路請求嗅探,我們可以發現所有的 Medium API 都是使用 https://medium.com/_/graphql 這個接口,只要我們能在自己的程式上呼叫並取得相同的資料,這條路就能達成。
Medium Private API — 嗅探 Medium Graphql (取得追蹤人數為例)
我的第一個應用是想爬取我的追蹤人數顯示在我的入口網站。
因此我必須爬取 Medium 上我的帳號的追蹤人數。
- 我們先打開瀏覽器的「開發者模式」(Chrome -> 右鍵 -> 檢查) 切換到「Network」頁籤,勾選「Preserve log」、搜尋匡輸入「
graphql」 - 進入 Medium 帳號追蹤者頁面:「
https://medium.com/@YOUR_USER_NAME/followers」 只能用這個格式的 Follower 網址嗅探,https://YOUR_USER_NAME.medium.com/followers這種格式的沒有呼叫取得 Followers API。
- 一個一個查看左方「graphql」請求,在「Response」資料中搜尋「
followerCount」關鍵字,找到有這個關鍵字且username同網頁的使用者名稱 & 追蹤數字跟網頁顯示相同,找到這個目標請求。 - 找到目標的 API 後,切換到 Payload -> View Source -> 複製以下全部內容
現在我們已經有取得 Medium 追蹤者的 API & Payload 了,用 Postman 測試看看!
Medium 取得追蹤人數 Endpoint — Request & Response
Endpoint:
1
POST https://medium.com/_/graphql
Authorization: No authorization required (不用帶 Token or set cookies)
Payload:
1
2
3
4
5
6
7
8
9
10
[
{
"operationName": "UserFollowers",
"variables": {
"id": null,
"username": "zhgchgli"
},
"query": "query UserFollowers($username: ID, $id: ID, $paging: PagingOptions) {\n userResult(username: $username, id: $id) {\n __typename\n ... on User {\n id\n followersUserConnection(paging: $paging) {\n pagingInfo {\n next {\n from\n limit\n __typename\n }\n __typename\n }\n users {\n ...FollowList_publisher\n __typename\n }\n __typename\n }\n ...UserCanonicalizer_user\n ...FollowersHeader_publisher\n ...NoFollows_publisher\n __typename\n }\n }\n}\n\nfragment collectionUrl_collection on Collection {\n id\n domain\n slug\n __typename\n}\n\nfragment CollectionAvatar_collection on Collection {\n name\n avatar {\n id\n __typename\n }\n ...collectionUrl_collection\n __typename\n id\n}\n\nfragment SignInOptions_collection on Collection {\n id\n name\n __typename\n}\n\nfragment SignUpOptions_collection on Collection {\n id\n name\n __typename\n}\n\nfragment SusiModal_collection on Collection {\n name\n ...SignInOptions_collection\n ...SignUpOptions_collection\n __typename\n id\n}\n\nfragment PublicationFollowButton_collection on Collection {\n id\n slug\n name\n ...SusiModal_collection\n __typename\n}\n\nfragment PublicationFollowRow_collection on Collection {\n __typename\n id\n name\n description\n ...CollectionAvatar_collection\n ...PublicationFollowButton_collection\n}\n\nfragment userUrl_user on User {\n __typename\n id\n customDomainState {\n live {\n domain\n __typename\n }\n __typename\n }\n hasSubdomain\n username\n}\n\nfragment UserAvatar_user on User {\n __typename\n id\n imageId\n membership {\n tier\n __typename\n id\n }\n name\n username\n ...userUrl_user\n}\n\nfragment isUserVerifiedBookAuthor_user on User {\n verifications {\n isBookAuthor\n __typename\n }\n __typename\n id\n}\n\nfragment SignInOptions_user on User {\n id\n name\n imageId\n __typename\n}\n\nfragment SignUpOptions_user on User {\n id\n name\n imageId\n __typename\n}\n\nfragment SusiModal_user on User {\n ...SignInOptions_user\n ...SignUpOptions_user\n __typename\n id\n}\n\nfragment useNewsletterV3Subscription_newsletterV3 on NewsletterV3 {\n id\n type\n slug\n name\n collection {\n slug\n __typename\n id\n }\n user {\n id\n name\n username\n newsletterV3 {\n id\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment useNewsletterV3Subscription_user on User {\n id\n username\n newsletterV3 {\n ...useNewsletterV3Subscription_newsletterV3\n __typename\n id\n }\n __typename\n}\n\nfragment useAuthorFollowSubscribeButton_user on User {\n id\n name\n ...useNewsletterV3Subscription_user\n __typename\n}\n\nfragment useAuthorFollowSubscribeButton_newsletterV3 on NewsletterV3 {\n id\n name\n ...useNewsletterV3Subscription_newsletterV3\n __typename\n}\n\nfragment AuthorFollowSubscribeButton_user on User {\n id\n name\n imageId\n ...SusiModal_user\n ...useAuthorFollowSubscribeButton_user\n newsletterV3 {\n id\n ...useAuthorFollowSubscribeButton_newsletterV3\n __typename\n }\n __typename\n}\n\nfragment UserFollowRow_user on User {\n __typename\n id\n name\n bio\n ...UserAvatar_user\n ...isUserVerifiedBookAuthor_user\n ...AuthorFollowSubscribeButton_user\n}\n\nfragment FollowsHeader_publisher on Publisher {\n __typename\n id\n name\n ... on Collection {\n ...collectionUrl_collection\n __typename\n id\n }\n ... on User {\n ...userUrl_user\n __typename\n id\n }\n}\n\nfragment FollowList_publisher on Publisher {\n id\n ... on Collection {\n ...PublicationFollowRow_collection\n __typename\n id\n }\n ... on User {\n ...UserFollowRow_user\n __typename\n id\n }\n __typename\n}\n\nfragment UserCanonicalizer_user on User {\n id\n username\n hasSubdomain\n customDomainState {\n live {\n domain\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment FollowersHeader_publisher on Publisher {\n ...FollowsHeader_publisher\n ... on Collection {\n subscriberCount\n __typename\n id\n }\n ... on User {\n socialStats {\n followerCount\n __typename\n }\n __typename\n id\n }\n __typename\n}\n\nfragment NoFollows_publisher on Publisher {\n id\n name\n __typename\n}\n"
}
]
username : 換成你要查詢的使用者名稱。
Postman :
POST https://medium.com/_/graphql- Body -> raw -> JSON
Response:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[
{
"data": {
"userResult": {
"__typename": "User",
"id": "8854784154b8",
"followersUserConnection": {
"pagingInfo": {
"next": {
"from": "1140babdfef7",
"limit": 8,
"__typename": "PageParams"
},
"__typename": "Paging"
},
"users": [
///.. 略
],
"__typename": "UserConnection"
},
"username": "zhgchgli",
"hasSubdomain": true,
"customDomainState": {
"live": {
"domain": "zhgchgli.medium.com",
"__typename": "CustomDomain"
},
"__typename": "CustomDomainState"
},
"name": "ZhgChgLi",
"socialStats": {
"followerCount": 1050,
"__typename": "SocialStats"
}
}
}
}
]
關鍵資訊: response[0]["data"]["userResult"]["socialStats"]["followerCount"] 就是目標追蹤人數了!
Ruby Demo Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def load_medium_followers(username)
begin
url = "https://medium.com/_/graphql"
uri = URI.parse(url)
payload = [
{
"operationName": "UserFollowers",
"variables": {
"id": nil,
"username": username,
"paging": nil
},
"query": "query UserFollowers($username: ID, $id: ID, $paging: PagingOptions) {\n userResult(username: $username, id: $id) {\n __typename\n ... on User {\n id\n followersUserConnection(paging: $paging) {\n pagingInfo {\n next {\n from\n limit\n __typename\n }\n __typename\n }\n users {\n ...FollowList_publisher\n __typename\n }\n __typename\n }\n ...UserCanonicalizer_user\n ...FollowersHeader_publisher\n ...NoFollows_publisher\n __typename\n }\n }\n}\n\nfragment collectionUrl_collection on Collection {\n id\n domain\n slug\n __typename\n}\n\nfragment CollectionAvatar_collection on Collection {\n name\n avatar {\n id\n __typename\n }\n ...collectionUrl_collection\n __typename\n id\n}\n\nfragment SignInOptions_collection on Collection {\n id\n name\n __typename\n}\n\nfragment SignUpOptions_collection on Collection {\n id\n name\n __typename\n}\n\nfragment SusiModal_collection on Collection {\n name\n ...SignInOptions_collection\n ...SignUpOptions_collection\n __typename\n id\n}\n\nfragment PublicationFollowButton_collection on Collection {\n id\n slug\n name\n ...SusiModal_collection\n __typename\n}\n\nfragment PublicationFollowRow_collection on Collection {\n __typename\n id\n name\n description\n ...CollectionAvatar_collection\n ...PublicationFollowButton_collection\n}\n\nfragment userUrl_user on User {\n __typename\n id\n customDomainState {\n live {\n domain\n __typename\n }\n __typename\n }\n hasSubdomain\n username\n}\n\nfragment UserAvatar_user on User {\n __typename\n id\n imageId\n membership {\n tier\n __typename\n id\n }\n name\n username\n ...userUrl_user\n}\n\nfragment isUserVerifiedBookAuthor_user on User {\n verifications {\n isBookAuthor\n __typename\n }\n __typename\n id\n}\n\nfragment SignInOptions_user on User {\n id\n name\n imageId\n __typename\n}\n\nfragment SignUpOptions_user on User {\n id\n name\n imageId\n __typename\n}\n\nfragment SusiModal_user on User {\n ...SignInOptions_user\n ...SignUpOptions_user\n __typename\n id\n}\n\nfragment useNewsletterV3Subscription_newsletterV3 on NewsletterV3 {\n id\n type\n slug\n name\n collection {\n slug\n __typename\n id\n }\n user {\n id\n name\n username\n newsletterV3 {\n id\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment useNewsletterV3Subscription_user on User {\n id\n username\n newsletterV3 {\n ...useNewsletterV3Subscription_newsletterV3\n __typename\n id\n }\n __typename\n}\n\nfragment useAuthorFollowSubscribeButton_user on User {\n id\n name\n ...useNewsletterV3Subscription_user\n __typename\n}\n\nfragment useAuthorFollowSubscribeButton_newsletterV3 on NewsletterV3 {\n id\n name\n ...useNewsletterV3Subscription_newsletterV3\n __typename\n}\n\nfragment AuthorFollowSubscribeButton_user on User {\n id\n name\n imageId\n ...SusiModal_user\n ...useAuthorFollowSubscribeButton_user\n newsletterV3 {\n id\n ...useAuthorFollowSubscribeButton_newsletterV3\n __typename\n }\n __typename\n}\n\nfragment UserFollowRow_user on User {\n __typename\n id\n name\n bio\n ...UserAvatar_user\n ...isUserVerifiedBookAuthor_user\n ...AuthorFollowSubscribeButton_user\n}\n\nfragment FollowsHeader_publisher on Publisher {\n __typename\n id\n name\n ... on Collection {\n ...collectionUrl_collection\n __typename\n id\n }\n ... on User {\n ...userUrl_user\n __typename\n id\n }\n}\n\nfragment FollowList_publisher on Publisher {\n id\n ... on Collection {\n ...PublicationFollowRow_collection\n __typename\n id\n }\n ... on User {\n ...UserFollowRow_user\n __typename\n id\n }\n __typename\n}\n\nfragment UserCanonicalizer_user on User {\n id\n username\n hasSubdomain\n customDomainState {\n live {\n domain\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment FollowersHeader_publisher on Publisher {\n ...FollowsHeader_publisher\n ... on Collection {\n subscriberCount\n __typename\n id\n }\n ... on User {\n socialStats {\n followerCount\n __typename\n }\n __typename\n id\n }\n __typename\n}\n\nfragment NoFollows_publisher on Publisher {\n id\n name\n __typename\n}\n"
}
];
headers = {
"User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0",
"Content-Type" => "application/json"
}
https = Net::HTTP.new(uri.host, uri.port)
https.read_timeout = 30
https.open_timeout = 10
https.use_ssl = true
# --- TLS / Certificate verification setup ---
# Some OpenSSL builds/configs enable CRL checking, which can fail with:
# "certificate verify failed (unable to get certificate CRL)".
# Net::HTTP/OpenSSL does not automatically fetch CRLs, so we use a default
# cert store and clear CRL-related flags to avoid hard failures while still
# verifying the peer certificate.
https.verify_mode = OpenSSL::SSL::VERIFY_PEER
store = OpenSSL::X509::Store.new
store.set_default_paths
# Ensure no CRL-check flags are enabled by default
store.flags = 0
https.cert_store = store
# Allow overriding CA bundle paths via environment variables if needed.
if ENV['SSL_CERT_FILE'] && !ENV['SSL_CERT_FILE'].empty?
https.ca_file = ENV['SSL_CERT_FILE']
end
if ENV['SSL_CERT_DIR'] && !ENV['SSL_CERT_DIR'].empty?
https.ca_path = ENV['SSL_CERT_DIR']
end
# (Optional) timeouts to avoid hanging on network issues
https.open_timeout = 10
https.read_timeout = 30
# --- end TLS setup ---
req = Net::HTTP::Post.new(uri.request_uri, headers)
req.body = JSON.dump(payload)
res = https.request(req)
json = JSON.parse(res.body)
count = json&.dig(0, "data", "userResult", "socialStats", "followerCount")
count ? count.to_i : 0
return "1K+" unless count.to_i > 0
return "#{count.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}+"
rescue => e
"1K+"
end
end
Medium Private API — 取得帳號的所有文章
用相同的方式我們也可以取得目標帳號的文章列表。
Graphql Query Body:
1
2
3
4
5
6
7
8
9
10
11
12
[
{
"operationName": "UserProfileQuery",
"variables": {
"homepagePostsFrom": null,
"includeDistributedResponses": true,
"id": "8854784154b8",
"homepagePostsLimit": 10
},
"query": "query UserProfileQuery($id: ID, $username: ID, $homepagePostsLimit: PaginationLimit, $homepagePostsFrom: String = null, $includeDistributedResponses: Boolean = true) {\n userResult(id: $id, username: $username) {\n __typename\n ... on User {\n id\n name\n viewerIsUser\n viewerEdge {\n id\n isFollowing\n __typename\n }\n homePostsPublished: homepagePostsConnection(paging: {limit: 1}) {\n posts {\n id\n __typename\n }\n __typename\n }\n ...UserCanonicalizer_user\n ...UserProfileScreen_user\n ...EntityDrivenSubscriptionLandingPageScreen_writer\n ...useShouldShowEntityDrivenSubscription_creator\n __typename\n }\n }\n}\n\nfragment UserCanonicalizer_user on User {\n id\n username\n hasSubdomain\n customDomainState {\n live {\n domain\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment UserProfileScreen_user on User {\n __typename\n id\n viewerIsUser\n ...PublisherHeader_publisher\n ...PublisherHomePosts_publisher\n ...UserSubdomainFlow_user\n ...UserProfileMetadata_user\n ...SuspendedBannerLoader_user\n ...ExpandablePost_user\n ...useAnalytics_user\n}\n\nfragment PublisherHeader_publisher on Publisher {\n id\n ...PublisherHeaderBackground_publisher\n ...PublisherHeaderNameplate_publisher\n ...PublisherHeaderActions_publisher\n ...PublisherHeaderNav_publisher\n __typename\n}\n\nfragment PublisherHeaderBackground_publisher on Publisher {\n __typename\n id\n customStyleSheet {\n ...PublisherHeaderBackground_customStyleSheet\n __typename\n id\n }\n ... on Collection {\n colorPalette {\n tintBackgroundSpectrum {\n backgroundColor\n __typename\n }\n __typename\n }\n isAuroraVisible\n legacyHeaderBackgroundImage {\n id\n originalWidth\n focusPercentX\n focusPercentY\n __typename\n }\n ...collectionTintBackgroundTheme_collection\n __typename\n id\n }\n ...publisherUrl_publisher\n}\n\nfragment PublisherHeaderBackground_customStyleSheet on CustomStyleSheet {\n id\n global {\n colorPalette {\n background {\n rgb\n __typename\n }\n __typename\n }\n __typename\n }\n header {\n headerScale\n backgroundImageDisplayMode\n backgroundImageVerticalAlignment\n backgroundColorDisplayMode\n backgroundColor {\n alpha\n rgb\n ...getHexFromColorValue_colorValue\n ...getOpaqueHexFromColorValue_colorValue\n __typename\n }\n secondaryBackgroundColor {\n ...getHexFromColorValue_colorValue\n __typename\n }\n postBackgroundColor {\n ...getHexFromColorValue_colorValue\n __typename\n }\n backgroundImage {\n ...MetaHeaderBackground_imageMetadata\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment getHexFromColorValue_colorValue on ColorValue {\n rgb\n alpha\n __typename\n}\n\nfragment getOpaqueHexFromColorValue_colorValue on ColorValue {\n rgb\n __typename\n}\n\nfragment MetaHeaderBackground_imageMetadata on ImageMetadata {\n id\n originalWidth\n __typename\n}\n\nfragment collectionTintBackgroundTheme_collection on Collection {\n colorPalette {\n ...collectionTintBackgroundTheme_colorPalette\n __typename\n }\n customStyleSheet {\n id\n ...collectionTintBackgroundTheme_customStyleSheet\n __typename\n }\n __typename\n id\n}\n\nfragment collectionTintBackgroundTheme_colorPalette on ColorPalette {\n ...customTintBackgroundTheme_colorPalette\n __typename\n}\n\nfragment customTintBackgroundTheme_colorPalette on ColorPalette {\n tintBackgroundSpectrum {\n ...ThemeUtil_colorSpectrum\n __typename\n }\n __typename\n}\n\nfragment ThemeUtil_colorSpectrum on ColorSpectrum {\n backgroundColor\n ...ThemeUtilInterpolateHelpers_colorSpectrum\n __typename\n}\n\nfragment ThemeUtilInterpolateHelpers_colorSpectrum on ColorSpectrum {\n colorPoints {\n ...ThemeUtil_colorPoint\n __typename\n }\n __typename\n}\n\nfragment ThemeUtil_colorPoint on ColorPoint {\n color\n point\n __typename\n}\n\nfragment collectionTintBackgroundTheme_customStyleSheet on CustomStyleSheet {\n id\n ...customTintBackgroundTheme_customStyleSheet\n __typename\n}\n\nfragment customTintBackgroundTheme_customStyleSheet on CustomStyleSheet {\n id\n global {\n colorPalette {\n primary {\n colorPalette {\n ...customTintBackgroundTheme_colorPalette\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment publisherUrl_publisher on Publisher {\n id\n __typename\n ... on Collection {\n ...collectionUrl_collection\n __typename\n id\n }\n ... on User {\n ...userUrl_user\n __typename\n id\n }\n}\n\nfragment collectionUrl_collection on Collection {\n id\n domain\n slug\n __typename\n}\n\nfragment userUrl_user on User {\n __typename\n id\n customDomainState {\n live {\n domain\n __typename\n }\n __typename\n }\n hasSubdomain\n username\n}\n\nfragment PublisherHeaderNameplate_publisher on Publisher {\n ...PublisherAvatar_publisher\n ...PublisherHeaderLogo_publisher\n ...PublisherFollowerCount_publisher\n __typename\n}\n\nfragment PublisherAvatar_publisher on Publisher {\n __typename\n ... on Collection {\n id\n ...CollectionAvatar_collection\n __typename\n }\n ... on User {\n id\n ...UserAvatar_user\n __typename\n }\n}\n\nfragment CollectionAvatar_collection on Collection {\n name\n avatar {\n id\n __typename\n }\n ...collectionUrl_collection\n __typename\n id\n}\n\nfragment UserAvatar_user on User {\n __typename\n id\n imageId\n mediumMemberAt\n name\n username\n ...userUrl_user\n}\n\nfragment PublisherHeaderLogo_publisher on Publisher {\n __typename\n id\n customStyleSheet {\n id\n header {\n logoImage {\n id\n originalHeight\n originalWidth\n __typename\n }\n appNameColor {\n ...getHexFromColorValue_colorValue\n __typename\n }\n appNameTreatment\n __typename\n }\n __typename\n }\n name\n ... on Collection {\n isAuroraVisible\n logo {\n id\n originalHeight\n originalWidth\n __typename\n }\n __typename\n id\n }\n ...CustomHeaderTooltip_publisher\n ...publisherUrl_publisher\n}\n\nfragment CustomHeaderTooltip_publisher on Publisher {\n __typename\n id\n customStyleSheet {\n id\n header {\n appNameTreatment\n nameTreatment\n __typename\n }\n __typename\n }\n ... on Collection {\n isAuroraVisible\n slug\n __typename\n id\n }\n}\n\nfragment PublisherFollowerCount_publisher on Publisher {\n __typename\n id\n ... on Collection {\n slug\n subscriberCount\n __typename\n id\n }\n ... on User {\n socialStats {\n followerCount\n __typename\n }\n username\n __typename\n id\n }\n}\n\nfragment PublisherHeaderActions_publisher on Publisher {\n __typename\n ...MetaHeaderPubMenu_publisher\n ... on Collection {\n ...CollectionFollowButton_collection\n __typename\n id\n }\n ... on User {\n ...FollowAndSubscribeButtons_user\n __typename\n id\n }\n}\n\nfragment MetaHeaderPubMenu_publisher on Publisher {\n __typename\n ... on Collection {\n ...MetaHeaderPubMenu_publisher_collection\n __typename\n id\n }\n ... on User {\n ...MetaHeaderPubMenu_publisher_user\n __typename\n id\n }\n}\n\nfragment MetaHeaderPubMenu_publisher_collection on Collection {\n id\n slug\n name\n domain\n newsletterV3 {\n slug\n __typename\n id\n }\n ...MutePopoverOptions_collection\n __typename\n}\n\nfragment MutePopoverOptions_collection on Collection {\n id\n __typename\n}\n\nfragment MetaHeaderPubMenu_publisher_user on User {\n id\n username\n ...MutePopoverOptions_creator\n __typename\n}\n\nfragment MutePopoverOptions_creator on User {\n id\n __typename\n}\n\nfragment CollectionFollowButton_collection on Collection {\n __typename\n id\n name\n slug\n ...collectionUrl_collection\n ...SusiClickable_collection\n}\n\nfragment SusiClickable_collection on Collection {\n ...SusiContainer_collection\n __typename\n id\n}\n\nfragment SusiContainer_collection on Collection {\n name\n ...SignInOptions_collection\n ...SignUpOptions_collection\n __typename\n id\n}\n\nfragment SignInOptions_collection on Collection {\n id\n name\n __typename\n}\n\nfragment SignUpOptions_collection on Collection {\n id\n name\n __typename\n}\n\nfragment FollowAndSubscribeButtons_user on User {\n ...UserFollowButton_user\n ...UserSubscribeButton_user\n __typename\n id\n}\n\nfragment UserFollowButton_user on User {\n ...UserFollowButtonSignedIn_user\n ...UserFollowButtonSignedOut_user\n __typename\n id\n}\n\nfragment UserFollowButtonSignedIn_user on User {\n id\n __typename\n}\n\nfragment UserFollowButtonSignedOut_user on User {\n id\n ...SusiClickable_user\n __typename\n}\n\nfragment SusiClickable_user on User {\n ...SusiContainer_user\n __typename\n id\n}\n\nfragment SusiContainer_user on User {\n ...SignInOptions_user\n ...SignUpOptions_user\n __typename\n id\n}\n\nfragment SignInOptions_user on User {\n id\n name\n __typename\n}\n\nfragment SignUpOptions_user on User {\n id\n name\n __typename\n}\n\nfragment UserSubscribeButton_user on User {\n id\n isPartnerProgramEnrolled\n name\n viewerEdge {\n id\n isFollowing\n isUser\n __typename\n }\n viewerIsUser\n newsletterV3 {\n id\n ...useNewsletterV3Subscription_newsletterV3\n __typename\n }\n ...useNewsletterV3Subscription_user\n ...MembershipUpsellModal_user\n __typename\n}\n\nfragment useNewsletterV3Subscription_newsletterV3 on NewsletterV3 {\n id\n type\n slug\n name\n collection {\n slug\n __typename\n id\n }\n user {\n id\n name\n username\n newsletterV3 {\n id\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment useNewsletterV3Subscription_user on User {\n id\n username\n newsletterV3 {\n ...useNewsletterV3Subscription_newsletterV3\n __typename\n id\n }\n __typename\n}\n\nfragment MembershipUpsellModal_user on User {\n id\n name\n imageId\n postSubscribeMembershipUpsellShownAt\n newsletterV3 {\n id\n __typename\n }\n __typename\n}\n\nfragment PublisherHeaderNav_publisher on Publisher {\n __typename\n id\n customStyleSheet {\n navigation {\n navItems {\n name\n ...PublisherHeaderNavLink_headerNavigationItem\n __typename\n }\n __typename\n }\n __typename\n id\n }\n ...PublisherHeaderNavLink_publisher\n ... on Collection {\n domain\n isAuroraVisible\n slug\n navItems {\n tagSlug\n title\n url\n __typename\n }\n __typename\n id\n }\n ... on User {\n customDomainState {\n live {\n domain\n __typename\n }\n __typename\n }\n hasSubdomain\n username\n about\n homePostsPublished: homepagePostsConnection(paging: {limit: 1}) {\n posts {\n id\n __typename\n }\n __typename\n }\n __typename\n id\n }\n}\n\nfragment PublisherHeaderNavLink_headerNavigationItem on HeaderNavigationItem {\n href\n name\n tags {\n id\n normalizedTagSlug\n __typename\n }\n type\n __typename\n}\n\nfragment PublisherHeaderNavLink_publisher on Publisher {\n __typename\n id\n ... on Collection {\n slug\n __typename\n id\n }\n}\n\nfragment PublisherHomePosts_publisher on Publisher {\n __typename\n id\n homepagePostsConnection(\n paging: {limit: $homepagePostsLimit, from: $homepagePostsFrom}\n includeDistributedResponses: $includeDistributedResponses\n ) {\n posts {\n ...PublisherHomePosts_post\n __typename\n }\n pagingInfo {\n next {\n from\n limit\n __typename\n }\n __typename\n }\n __typename\n }\n ...CardByline_publisher\n ...NewsletterV3Promo_publisher\n ...PublisherHomePosts_user\n}\n\nfragment PublisherHomePosts_post on Post {\n id\n collection {\n id\n name\n ...collectionUrl_collection\n __typename\n }\n ...ExpandablePost_post\n __typename\n}\n\nfragment ExpandablePost_post on Post {\n id\n creator {\n ...ExpandablePost_user\n __typename\n id\n }\n collection {\n ...CardByline_collection\n __typename\n id\n }\n ...InteractivePostBody_postPreview\n firstPublishedAt\n isLocked\n isSeries\n isShortform\n latestPublishedAt\n inResponseToCatalogResult {\n __typename\n }\n mediumUrl\n postResponses {\n count\n __typename\n }\n previewImage {\n id\n focusPercentX\n focusPercentY\n __typename\n }\n readingTime\n sequence {\n slug\n __typename\n }\n title\n uniqueSlug\n visibility\n ...CardByline_post\n ...ExpandablePostFooter_post\n ...InResponseToEntityPreview_post\n ...PostScrollTracker_post\n ...ReadMore_post\n ...HighDensityPreview_post\n __typename\n}\n\nfragment ExpandablePost_user on User {\n __typename\n name\n username\n ...CardByline_user\n id\n}\n\nfragment CardByline_user on User {\n __typename\n id\n name\n username\n mediumMemberAt\n socialStats {\n followerCount\n __typename\n }\n ...userUrl_user\n ...UserMentionTooltip_user\n}\n\nfragment UserMentionTooltip_user on User {\n id\n name\n username\n bio\n imageId\n mediumMemberAt\n ...UserAvatar_user\n ...UserFollowButton_user\n __typename\n}\n\nfragment CardByline_collection on Collection {\n __typename\n id\n name\n ...collectionUrl_collection\n}\n\nfragment InteractivePostBody_postPreview on Post {\n extendedPreviewContent(\n truncationConfig: {previewParagraphsWordCountThreshold: 400, minimumWordLengthForTruncation: 150, truncateAtEndOfSentence: true, showFullImageCaptions: true, shortformPreviewParagraphsWordCountThreshold: 30, shortformMinimumWordLengthForTruncation: 30}\n ) {\n bodyModel {\n ...PostBody_bodyModel\n __typename\n }\n isFullContent\n __typename\n }\n __typename\n id\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\nfragment CardByline_post on Post {\n ...DraftStatus_post\n __typename\n id\n}\n\nfragment DraftStatus_post on Post {\n id\n pendingCollection {\n id\n creator {\n id\n __typename\n }\n ...BoldCollectionName_collection\n __typename\n }\n statusForCollection\n creator {\n id\n __typename\n }\n isPublished\n __typename\n}\n\nfragment BoldCollectionName_collection on Collection {\n id\n name\n __typename\n}\n\nfragment ExpandablePostFooter_post on Post {\n id\n allowResponses\n postResponses {\n count\n __typename\n }\n isLimitedState\n ...ExpandablePostCardOverflowButton_post\n ...BookmarkButton_post\n ...PostFooterSocialPopover_post\n ...MultiVote_post\n ...OverflowMenuButtonWithNegativeSignal_post\n __typename\n}\n\nfragment ExpandablePostCardOverflowButton_post on Post {\n creator {\n id\n __typename\n }\n ...ExpandablePostCardEditorWriterButton_post\n ...ExpandablePostCardReaderButton_post\n __typename\n id\n}\n\nfragment ExpandablePostCardEditorWriterButton_post on Post {\n id\n collection {\n id\n name\n slug\n __typename\n }\n allowResponses\n clapCount\n visibility\n mediumUrl\n responseDistribution\n ...useIsPinnedInContext_post\n ...CopyFriendLinkMenuItem_post\n ...NewsletterV3EmailToSubscribersMenuItem_post\n ...OverflowMenuItemUndoClaps_post\n __typename\n}\n\nfragment useIsPinnedInContext_post on Post {\n id\n collection {\n id\n __typename\n }\n pendingCollection {\n id\n __typename\n }\n pinnedAt\n pinnedByCreatorAt\n __typename\n}\n\nfragment CopyFriendLinkMenuItem_post on Post {\n id\n __typename\n}\n\nfragment NewsletterV3EmailToSubscribersMenuItem_post on Post {\n id\n creator {\n id\n newsletterV3 {\n id\n subscribersCount\n __typename\n }\n __typename\n }\n isNewsletter\n isAuthorNewsletter\n __typename\n}\n\nfragment OverflowMenuItemUndoClaps_post on Post {\n id\n clapCount\n ...ClapMutation_post\n __typename\n}\n\nfragment ClapMutation_post on Post {\n __typename\n id\n clapCount\n ...MultiVoteCount_post\n}\n\nfragment MultiVoteCount_post on Post {\n id\n ...PostVotersNetwork_post\n __typename\n}\n\nfragment PostVotersNetwork_post on Post {\n id\n voterCount\n recommenders {\n name\n __typename\n }\n __typename\n}\n\nfragment ExpandablePostCardReaderButton_post on Post {\n id\n collection {\n id\n __typename\n }\n creator {\n id\n __typename\n }\n clapCount\n ...ClapMutation_post\n __typename\n}\n\nfragment BookmarkButton_post on Post {\n visibility\n ...SusiClickable_post\n ...AddToCatalogBookmarkButton_post\n __typename\n id\n}\n\nfragment SusiClickable_post on Post {\n id\n mediumUrl\n ...SusiContainer_post\n __typename\n}\n\nfragment SusiContainer_post on Post {\n id\n __typename\n}\n\nfragment AddToCatalogBookmarkButton_post on Post {\n ...AddToCatalogBase_post\n __typename\n id\n}\n\nfragment AddToCatalogBase_post on Post {\n id\n __typename\n}\n\nfragment PostFooterSocialPopover_post on Post {\n id\n mediumUrl\n title\n ...SharePostButton_post\n __typename\n}\n\nfragment SharePostButton_post on Post {\n id\n __typename\n}\n\nfragment MultiVote_post on Post {\n id\n clapCount\n creator {\n id\n ...SusiClickable_user\n __typename\n }\n isPublished\n ...SusiClickable_post\n collection {\n id\n slug\n __typename\n }\n isLimitedState\n ...MultiVoteCount_post\n __typename\n}\n\nfragment OverflowMenuButtonWithNegativeSignal_post on Post {\n id\n ...OverflowMenuWithNegativeSignal_post\n ...CreatorActionOverflowPopover_post\n __typename\n}\n\nfragment OverflowMenuWithNegativeSignal_post on Post {\n id\n creator {\n id\n __typename\n }\n collection {\n id\n __typename\n }\n ...OverflowMenuItemUndoClaps_post\n __typename\n}\n\nfragment CreatorActionOverflowPopover_post on Post {\n allowResponses\n id\n statusForCollection\n isLocked\n isPublished\n clapCount\n mediumUrl\n pinnedAt\n pinnedByCreatorAt\n curationEligibleAt\n mediumUrl\n responseDistribution\n visibility\n inResponseToPostResult {\n __typename\n }\n inResponseToCatalogResult {\n __typename\n }\n pendingCollection {\n id\n name\n creator {\n id\n __typename\n }\n avatar {\n id\n __typename\n }\n domain\n slug\n __typename\n }\n creator {\n id\n ...MutePopoverOptions_creator\n ...auroraHooks_publisher\n __typename\n }\n collection {\n id\n name\n creator {\n id\n __typename\n }\n avatar {\n id\n __typename\n }\n domain\n slug\n ...MutePopoverOptions_collection\n ...auroraHooks_publisher\n __typename\n }\n ...useIsPinnedInContext_post\n ...NewsletterV3EmailToSubscribersMenuItem_post\n ...OverflowMenuItemUndoClaps_post\n __typename\n}\n\nfragment auroraHooks_publisher on Publisher {\n __typename\n ... on Collection {\n isAuroraEligible\n isAuroraVisible\n viewerEdge {\n id\n isEditor\n __typename\n }\n __typename\n id\n }\n ... on User {\n isAuroraVisible\n __typename\n id\n }\n}\n\nfragment InResponseToEntityPreview_post on Post {\n id\n inResponseToEntityType\n __typename\n}\n\nfragment PostScrollTracker_post on Post {\n id\n collection {\n id\n __typename\n }\n sequence {\n sequenceId\n __typename\n }\n __typename\n}\n\nfragment ReadMore_post on Post {\n mediumUrl\n readingTime\n ...usePostUrl_post\n __typename\n id\n}\n\nfragment usePostUrl_post on Post {\n id\n creator {\n ...userUrl_user\n __typename\n id\n }\n collection {\n id\n domain\n slug\n __typename\n }\n isSeries\n mediumUrl\n sequence {\n slug\n __typename\n }\n uniqueSlug\n __typename\n}\n\nfragment HighDensityPreview_post on Post {\n id\n title\n previewImage {\n id\n focusPercentX\n focusPercentY\n __typename\n }\n extendedPreviewContent(\n truncationConfig: {previewParagraphsWordCountThreshold: 400, minimumWordLengthForTruncation: 150, truncateAtEndOfSentence: true, showFullImageCaptions: true, shortformPreviewParagraphsWordCountThreshold: 30, shortformMinimumWordLengthForTruncation: 30}\n ) {\n subtitle\n __typename\n }\n ...HighDensityFooter_post\n __typename\n}\n\nfragment HighDensityFooter_post on Post {\n id\n readingTime\n tags {\n ...TopicPill_tag\n __typename\n }\n ...BookmarkButton_post\n ...ExpandablePostCardOverflowButton_post\n ...OverflowMenuButtonWithNegativeSignal_post\n __typename\n}\n\nfragment TopicPill_tag on Tag {\n __typename\n id\n displayTitle\n}\n\nfragment CardByline_publisher on Publisher {\n __typename\n ... on User {\n id\n ...CardByline_user\n __typename\n }\n ... on Collection {\n id\n ...CardByline_collection\n __typename\n }\n}\n\nfragment NewsletterV3Promo_publisher on Publisher {\n __typename\n ... on User {\n ...NewsletterV3Promo_publisher_User\n __typename\n id\n }\n ... on Collection {\n ...NewsletterV3Promo_publisher_Collection\n __typename\n id\n }\n}\n\nfragment NewsletterV3Promo_publisher_User on User {\n id\n username\n name\n viewerIsUser\n newsletterV3 {\n id\n ...NewsletterV3Promo_newsletterV3\n __typename\n }\n __typename\n}\n\nfragment NewsletterV3Promo_newsletterV3 on NewsletterV3 {\n slug\n name\n description\n promoHeadline\n promoBody\n ...NewsletterV3AmpButton_newsletterV3\n ...NewsletterV3SubscribeButton_newsletterV3\n ...NewsletterV3SubscribeByEmail_newsletterV3\n __typename\n id\n}\n\nfragment NewsletterV3AmpButton_newsletterV3 on NewsletterV3 {\n id\n collection {\n ...collectionDefaultBackgroundTheme_collection\n __typename\n id\n }\n __typename\n}\n\nfragment collectionDefaultBackgroundTheme_collection on Collection {\n colorPalette {\n ...collectionDefaultBackgroundTheme_colorPalette\n __typename\n }\n customStyleSheet {\n id\n ...collectionDefaultBackgroundTheme_customStyleSheet\n __typename\n }\n __typename\n id\n}\n\nfragment collectionDefaultBackgroundTheme_colorPalette on ColorPalette {\n ...customDefaultBackgroundTheme_colorPalette\n __typename\n}\n\nfragment customDefaultBackgroundTheme_colorPalette on ColorPalette {\n highlightSpectrum {\n ...ThemeUtil_colorSpectrum\n __typename\n }\n defaultBackgroundSpectrum {\n ...ThemeUtil_colorSpectrum\n __typename\n }\n tintBackgroundSpectrum {\n ...ThemeUtil_colorSpectrum\n __typename\n }\n __typename\n}\n\nfragment collectionDefaultBackgroundTheme_customStyleSheet on CustomStyleSheet {\n id\n ...customDefaultBackgroundTheme_customStyleSheet\n __typename\n}\n\nfragment customDefaultBackgroundTheme_customStyleSheet on CustomStyleSheet {\n id\n global {\n colorPalette {\n primary {\n colorPalette {\n ...customDefaultBackgroundTheme_colorPalette\n __typename\n }\n __typename\n }\n background {\n colorPalette {\n ...customDefaultBackgroundTheme_colorPalette\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment NewsletterV3SubscribeButton_newsletterV3 on NewsletterV3 {\n id\n name\n slug\n type\n user {\n id\n name\n username\n __typename\n }\n collection {\n slug\n ...SusiClickable_collection\n ...collectionDefaultBackgroundTheme_collection\n __typename\n id\n }\n ...SusiClickable_newsletterV3\n ...useNewsletterV3Subscription_newsletterV3\n __typename\n}\n\nfragment SusiClickable_newsletterV3 on NewsletterV3 {\n ...SusiContainer_newsletterV3\n __typename\n id\n}\n\nfragment SusiContainer_newsletterV3 on NewsletterV3 {\n ...SignInOptions_newsletterV3\n ...SignUpOptions_newsletterV3\n __typename\n id\n}\n\nfragment SignInOptions_newsletterV3 on NewsletterV3 {\n id\n name\n __typename\n}\n\nfragment SignUpOptions_newsletterV3 on NewsletterV3 {\n id\n name\n __typename\n}\n\nfragment NewsletterV3SubscribeByEmail_newsletterV3 on NewsletterV3 {\n id\n slug\n type\n user {\n id\n name\n username\n __typename\n }\n collection {\n ...collectionDefaultBackgroundTheme_collection\n ...collectionUrl_collection\n __typename\n id\n }\n __typename\n}\n\nfragment NewsletterV3Promo_publisher_Collection on Collection {\n id\n slug\n domain\n name\n newsletterV3 {\n id\n ...NewsletterV3Promo_newsletterV3\n __typename\n }\n __typename\n}\n\nfragment PublisherHomePosts_user on User {\n id\n ...useShowAuthorNewsletterV3Promo_user\n __typename\n}\n\nfragment useShowAuthorNewsletterV3Promo_user on User {\n id\n username\n newsletterV3 {\n id\n showPromo\n slug\n __typename\n }\n __typename\n}\n\nfragment UserSubdomainFlow_user on User {\n id\n hasCompletedProfile\n name\n bio\n imageId\n ...UserCompleteProfileDialog_user\n ...UserSubdomainOnboardingDialog_user\n __typename\n}\n\nfragment UserCompleteProfileDialog_user on User {\n id\n name\n bio\n imageId\n hasCompletedProfile\n __typename\n}\n\nfragment UserSubdomainOnboardingDialog_user on User {\n id\n customDomainState {\n pending {\n status\n __typename\n }\n live {\n status\n __typename\n }\n __typename\n }\n username\n __typename\n}\n\nfragment UserProfileMetadata_user on User {\n id\n username\n name\n bio\n socialStats {\n followerCount\n followingCount\n __typename\n }\n ...userUrl_user\n ...UserProfileMetadataHelmet_user\n __typename\n}\n\nfragment UserProfileMetadataHelmet_user on User {\n username\n name\n imageId\n twitterScreenName\n navItems {\n title\n __typename\n }\n __typename\n id\n}\n\nfragment SuspendedBannerLoader_user on User {\n id\n isSuspended\n __typename\n}\n\nfragment useAnalytics_user on User {\n id\n imageId\n name\n username\n __typename\n}\n\nfragment EntityDrivenSubscriptionLandingPageScreen_writer on User {\n name\n imageId\n id\n username\n isPartnerProgramEnrolled\n customStyleSheet {\n ...CustomThemeProvider_customStyleSheet\n ...CustomBackgroundWrapper_customStyleSheet\n ...MetaHeader_customStyleSheet\n __typename\n id\n }\n ...MetaHeader_publisher\n ...userUrl_user\n __typename\n}\n\nfragment CustomThemeProvider_customStyleSheet on CustomStyleSheet {\n id\n ...customDefaultBackgroundTheme_customStyleSheet\n ...customStyleSheetFontTheme_customStyleSheet\n __typename\n}\n\nfragment customStyleSheetFontTheme_customStyleSheet on CustomStyleSheet {\n id\n global {\n fonts {\n font1 {\n name\n __typename\n }\n font2 {\n name\n __typename\n }\n font3 {\n name\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment CustomBackgroundWrapper_customStyleSheet on CustomStyleSheet {\n id\n global {\n colorPalette {\n background {\n ...getHexFromColorValue_colorValue\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment MetaHeader_customStyleSheet on CustomStyleSheet {\n id\n header {\n headerScale\n horizontalAlignment\n __typename\n }\n ...MetaHeaderBackground_customStyleSheet\n ...MetaHeaderEngagement_customStyleSheet\n ...MetaHeaderLogo_customStyleSheet\n ...MetaHeaderNavVertical_customStyleSheet\n ...MetaHeaderTagline_customStyleSheet\n ...MetaHeaderThemeProvider_customStyleSheet\n __typename\n}\n\nfragment MetaHeaderBackground_customStyleSheet on CustomStyleSheet {\n id\n header {\n headerScale\n backgroundImageDisplayMode\n backgroundImageVerticalAlignment\n backgroundColorDisplayMode\n backgroundColor {\n ...getHexFromColorValue_colorValue\n ...getOpaqueHexFromColorValue_colorValue\n __typename\n }\n secondaryBackgroundColor {\n ...getHexFromColorValue_colorValue\n __typename\n }\n postBackgroundColor {\n ...getHexFromColorValue_colorValue\n __typename\n }\n backgroundImage {\n ...MetaHeaderBackground_imageMetadata\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment MetaHeaderEngagement_customStyleSheet on CustomStyleSheet {\n ...MetaHeaderNav_customStyleSheet\n __typename\n id\n}\n\nfragment MetaHeaderNav_customStyleSheet on CustomStyleSheet {\n id\n navigation {\n navItems {\n ...MetaHeaderNav_headerNavigationItem\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment MetaHeaderNav_headerNavigationItem on HeaderNavigationItem {\n name\n tagSlugs\n ...MetaHeaderNavLink_headerNavigationItem\n __typename\n}\n\nfragment MetaHeaderNavLink_headerNavigationItem on HeaderNavigationItem {\n name\n ...getNavItemHref_headerNavigationItem\n __typename\n}\n\nfragment getNavItemHref_headerNavigationItem on HeaderNavigationItem {\n href\n type\n tags {\n id\n normalizedTagSlug\n __typename\n }\n __typename\n}\n\nfragment MetaHeaderLogo_customStyleSheet on CustomStyleSheet {\n id\n header {\n nameColor {\n ...getHexFromColorValue_colorValue\n __typename\n }\n nameTreatment\n postNameTreatment\n logoImage {\n ...MetaHeaderLogo_imageMetadata\n __typename\n }\n logoScale\n __typename\n }\n __typename\n}\n\nfragment MetaHeaderLogo_imageMetadata on ImageMetadata {\n id\n originalWidth\n originalHeight\n ...PublisherLogo_image\n __typename\n}\n\nfragment PublisherLogo_image on ImageMetadata {\n id\n originalHeight\n originalWidth\n __typename\n}\n\nfragment MetaHeaderNavVertical_customStyleSheet on CustomStyleSheet {\n id\n navigation {\n navItems {\n ...MetaHeaderNavLink_headerNavigationItem\n __typename\n }\n __typename\n }\n ...MetaHeaderNav_customStyleSheet\n __typename\n}\n\nfragment MetaHeaderTagline_customStyleSheet on CustomStyleSheet {\n id\n header {\n taglineColor {\n ...getHexFromColorValue_colorValue\n __typename\n }\n taglineTreatment\n __typename\n }\n __typename\n}\n\nfragment MetaHeaderThemeProvider_customStyleSheet on CustomStyleSheet {\n id\n ...useMetaHeaderTheme_customStyleSheet\n __typename\n}\n\nfragment useMetaHeaderTheme_customStyleSheet on CustomStyleSheet {\n ...customDefaultBackgroundTheme_customStyleSheet\n global {\n colorPalette {\n primary {\n colorPalette {\n tintBackgroundSpectrum {\n ...ThemeUtil_colorSpectrum\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n header {\n backgroundColor {\n colorPalette {\n tintBackgroundSpectrum {\n ...ThemeUtil_colorSpectrum\n __typename\n }\n __typename\n }\n __typename\n }\n postBackgroundColor {\n colorPalette {\n tintBackgroundSpectrum {\n ...ThemeUtil_colorSpectrum\n __typename\n }\n __typename\n }\n __typename\n }\n backgroundImage {\n id\n __typename\n }\n __typename\n }\n __typename\n id\n}\n\nfragment MetaHeader_publisher on Publisher {\n __typename\n name\n ...MetaHeaderEngagement_publisher\n ...MetaHeaderLogo_publisher\n ...MetaHeaderNavVertical_publisher\n ...MetaHeaderTagline_publisher\n ...MetaHeaderThemeProvider_publisher\n ...MetaHeaderActions_publisher\n ...MetaHeaderTop_publisher\n ...MetaHeaderNavLink_publisher\n ... on Collection {\n id\n favicon {\n id\n __typename\n }\n tagline\n ...CollectionNavigationContextProvider_collection\n __typename\n }\n ... on User {\n id\n bio\n ...UserProfileCatalogsLink_publisher\n __typename\n }\n}\n\nfragment MetaHeaderEngagement_publisher on Publisher {\n __typename\n ...MetaHeaderNav_publisher\n ...PublisherAboutLink_publisher\n ...PublisherFollowButton_publisher\n ...PublisherFollowerCount_publisher\n ... on Collection {\n creator {\n id\n __typename\n }\n customStyleSheet {\n id\n ...CustomThemeProvider_customStyleSheet\n __typename\n }\n __typename\n id\n }\n ... on User {\n ...UserProfileCatalogsLink_publisher\n ...UserSubscribeButton_user\n customStyleSheet {\n id\n ...CustomThemeProvider_customStyleSheet\n __typename\n }\n __typename\n id\n }\n}\n\nfragment MetaHeaderNav_publisher on Publisher {\n id\n ...MetaHeaderNavLink_publisher\n __typename\n}\n\nfragment MetaHeaderNavLink_publisher on Publisher {\n id\n ...getNavItemHref_publisher\n __typename\n}\n\nfragment getNavItemHref_publisher on Publisher {\n id\n ...publisherUrl_publisher\n __typename\n}\n\nfragment PublisherAboutLink_publisher on Publisher {\n __typename\n id\n ... on Collection {\n slug\n __typename\n id\n }\n ... on User {\n ...userUrl_user\n __typename\n id\n }\n}\n\nfragment PublisherFollowButton_publisher on Publisher {\n __typename\n ... on Collection {\n ...CollectionFollowButton_collection\n __typename\n id\n }\n ... on User {\n ...UserFollowButton_user\n __typename\n id\n }\n}\n\nfragment UserProfileCatalogsLink_publisher on Publisher {\n __typename\n id\n ... on User {\n ...userUrl_user\n homePostsPublished: homepagePostsConnection(paging: {limit: 1}) {\n posts {\n id\n __typename\n }\n __typename\n }\n __typename\n id\n }\n}\n\nfragment MetaHeaderLogo_publisher on Publisher {\n __typename\n id\n name\n ... on Collection {\n logo {\n ...MetaHeaderLogo_imageMetadata\n ...PublisherLogo_image\n __typename\n id\n }\n __typename\n id\n }\n ...auroraHooks_publisher\n}\n\nfragment MetaHeaderNavVertical_publisher on Publisher {\n id\n ...PublisherAboutLink_publisher\n ...MetaHeaderNav_publisher\n ...MetaHeaderNavLink_publisher\n __typename\n}\n\nfragment MetaHeaderTagline_publisher on Publisher {\n __typename\n ... on Collection {\n tagline\n __typename\n id\n }\n ... on User {\n bio\n __typename\n id\n }\n}\n\nfragment MetaHeaderThemeProvider_publisher on Publisher {\n __typename\n customStyleSheet {\n ...MetaHeaderThemeProvider_customStyleSheet\n __typename\n id\n }\n ... on Collection {\n colorPalette {\n ...customDefaultBackgroundTheme_colorPalette\n __typename\n }\n __typename\n id\n }\n}\n\nfragment MetaHeaderActions_publisher on Publisher {\n __typename\n ...MetaHeaderPubMenu_publisher\n ...SearchWidget_publisher\n ... on Collection {\n id\n creator {\n id\n __typename\n }\n customStyleSheet {\n navigation {\n navItems {\n name\n __typename\n }\n __typename\n }\n __typename\n id\n }\n ...CollectionAvatar_collection\n ...CollectionMetabarActionsPopover_collection\n ...MetaHeaderActions_collection_common\n __typename\n }\n ... on User {\n id\n ...UserAvatar_user\n __typename\n }\n}\n\nfragment SearchWidget_publisher on Publisher {\n __typename\n ... on Collection {\n id\n slug\n name\n domain\n __typename\n }\n ... on User {\n id\n name\n __typename\n }\n ...algoliaSearch_publisher\n}\n\nfragment algoliaSearch_publisher on Publisher {\n __typename\n id\n}\n\nfragment CollectionMetabarActionsPopover_collection on Collection {\n id\n slug\n isAuroraEligible\n isAuroraVisible\n newsletterV3 {\n id\n slug\n __typename\n }\n ...collectionUrl_collection\n __typename\n}\n\nfragment MetaHeaderActions_collection_common on Collection {\n creator {\n id\n __typename\n }\n __typename\n id\n}\n\nfragment MetaHeaderTop_publisher on Publisher {\n __typename\n ... on Collection {\n slug\n ...CollectionMetabarActionsPopover_collection\n ...CollectionAvatar_collection\n ...MetaHeaderTop_collection\n __typename\n id\n }\n ... on User {\n username\n id\n __typename\n }\n}\n\nfragment MetaHeaderTop_collection on Collection {\n id\n creator {\n id\n __typename\n }\n __typename\n}\n\nfragment CollectionNavigationContextProvider_collection on Collection {\n id\n domain\n slug\n isAuroraVisible\n __typename\n}\n\nfragment useShouldShowEntityDrivenSubscription_creator on User {\n id\n __typename\n}\n"
}
]
id: 帳號對應的 User Id,上面那個取得追蹤者的 Endpoint 有給homepagePostsFrom: 分頁參數,可參考回應中的from帶入取得所有分頁資料
Postman :
Response:
在 response[0]["data"]["userResult"]["homepagePostsConnection"]["posts"] 可以取得文章列表資訊。
Medium Private API — 取得文章內容
再來就是最關鍵的,爬取文章原始內容。
Graphql Query Body:
1
2
3
4
5
6
7
8
9
[
{
"operationName": "PostViewerEdgeContentQuery",
"variables": {
"postId": "c008a9e8ceca"
},
"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 ...CodeBlockParagraph_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 codeBlockMetadata {\n mode\n lang\n __typename\n }\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 CodeBlockParagraph_paragraph on Paragraph {\n codeBlockMetadata {\n lang\n mode\n __typename\n }\n __typename\n id\n}\n\nfragment normalizedBodyModel_richText on RichText {\n paragraphs {\n markups {\n type\n __typename\n }\n codeBlockMetadata {\n lang\n mode\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 id\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 codeBlockMetadata {\n lang\n mode\n __typename\n }\n __typename\n }\n sections {\n startIndex\n __typename\n }\n __typename\n}\n"
}
]
posdId: 文章 ID,上面取得文章列表的 API 有給。
Postman :
Response:
文章 Source 如上圖:
response[0]["data"]["post"]["viewerEdge"]["fullContent"]["bodyModel"]["paragraphs"]:整篇文章段落,組合起來就是全篇文章["markups"]: 段落的文字樣式,例如粗體、超連結…
至於如何把這個 JSON 描述格式轉換成 Markdown 可以直接參考或使用我之前開發的開源工具 — ZMediumToMarkdown
- 如果要爬取自己的付費文章需要帶上 sid, uid 登入權杖資訊 (參考下文)
Medium x Cloudflare 攻防
本篇文章的第二個主題,就是 Cloudflare,大約在 2025 下半年開始,Medium 的 Cloudflare 防護設定幾乎到了最高等級,所有來自雲服務的請求都會被阻擋(我用 Google Apps Script / GitHub Actions 全都會被擋),導致爬取資料失敗。
阻擋訊息:403
1
<!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">....
這讓我非常苦惱,因為我的鏡像站 ( https://zhgchg.li ) 已經穩定運作了好幾年,只要 Medium 有新文章發佈,那邊就會定時自動執行腳本同步過去 (透過 Private API Graphql), 如果雲端服務會被阻擋,我就只能手動在本地電腦執行腳本 。
Header Cookies 多加上 sid, uid ❌
最一開始我在 Header Cookies 多加上 sid, uid 等於是 Medium 登入身份,可以順利通過,但過了幾個月之後連這個方法也行不通了。
uid: 你的使用者 IDsid: 你的 access token (務必保密)
Cloudflare Worker 橋接請求 ✅
亂試一通後找不到方法,突發奇想用 Cloudflare 的魔法打敗魔法,用 Cloudflare,沒想到就成功了!不會被 Cloudflare Bot 防護阻擋。
Demo Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const path = url.pathname;
if (path == "/graphql") {
let body;
try {
body = await request.json();
} catch {
return new Response("Invalid JSON body", { status: 400 });
}
let apiURL = "https://medium.com/_/graphql";
const apiResponse = await fetch(apiURL, {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0",
"Cookie": request.headers.get("cookie")
},
body: JSON.stringify(body),
});
const json = await apiResponse.json();
return new Response(JSON.stringify(json), {
status: apiResponse.status,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
});
}
return new Response("Not Found", { status: 404 });
},
};
Request:
從 https://medium.com/_/graphql 改成你部署的 Cloudflare Worker URL 即可。
總結
以上就是我跟 Medium API 搏鬥這麼多年的心路歷程,目前所有的文章 (Markdown 檔)及圖片都有備份到 https://zhgchg.li 包含所有附加圖片檔案,GitHub Repo 有一份、電腦硬碟也有一份,非常安全。
還是希望 Medium 能長長久久!本文內容只供實驗參考,作者不負任何使用責任。
有任何問題及指教歡迎 與我聯絡 。
本文首次發表於 Medium (點此查看原始版本),由 ZMediumToMarkdown 提供自動轉換與同步技術。









