Juejin Hot List Skill v1.4.0: One CLI Call, Verified API, No Login
juejin-skills Series ②: Turning "View Juejin Hot List" into an Agent Skill
This series ① I turned "Auto-publish articles to Juejin" into an Agent skill: juejin-skills ② This post: Adding "Juejin Hot List" to the skill package (v1.4.0) ③ Preview: F3 Juejin Pin/Boiling Point publishing
🔗 Repository: https://github.com/liuleihang/juejin-skills (MIT, Node ≥ 18, zero-dependency release scripts)
🔗 workbuddy link: https://www.workbuddy.cn/share/asset?shareCode=Z4Xnm1b8khzoZfBydIM9jb&sig=3BYkn4xsNc_4gDQn
When the previous post went out, I left a F1–F6 to-do list at the end, with F2 "Juejin Hot List" marked as 📋 Planning.
This post checks it off. In v1.4.0, when you say "look at today's Juejin frontend hot list" in an Agent, it returns ranking, title, author, heat, views/likes/collects/comments, and article links—all without requiring login.
1. Why do the hot list first
First, look at the complexity difference between the two features:
| Feature | F1 Article Publishing | F2 Juejin Hot List |
|---|---|---|
| Auth | Requires Cookie, login state expires | Public API, no login needed |
| Chain | Session check → tag cache → image upload → draft creation → second confirmation before publishing | One GET → field mapping → output |
| Risk | External write operation, accidental publishing is irreversible | Read-only, no side effects |
The hot list is the only capability on the whole to-do list that doesn't need login, and the only one without dangerous actions.
There's a judgment often overlooked: Does this requirement really need an Agent? No. A single HTTP GET, a field mapping, and a formatted output form a standard deterministic fixed workflow—no decision branches, no autonomous loops, no memory, and no stopping conditions. Wrapping such a task in an "autonomous Agent" only introduces uncertainty out of thin air.
So the implementation only added a CLI script scripts/hot.mjs and supplemented the trigger words and command description in SKILL.md. The Agent's role here is turning natural language into a command, not making decisions for you.
Anything that can be solved with one call shouldn't be packaged as an Agent's autonomous decision. A skill package isn't better because it's "smarter"; it's better when it's more predictable.
2. Don't hard-code the API from memory
Juejin has no official public API documentation. Endpoints and fields found online, including examples in various blogs, may already be outdated. The first step is not to write code, but to verify:
- Cross-compare multiple independent sources to find candidate endpoints;
- Send a request directly and read the returned structure field by field before deciding on the mapping;
- Write the verification result (including the date) into docs, not into your head.
What was ultimately confirmed:
GET https://api.juejin.cn/content_api/v1/content/article_rank
?category_id={id}&type=hot&aid=2608&spider=0
| Item | Measured result (2026-11-11) |
|---|---|
| Auth | No Cookie returns err_no=0, publicly readable |
| Response | {err_no:0, err_msg:"success", data:[...]}, 50 items in the overall ranking |
| Fields | author.name, content.{content_id,title}, content_counter.{hot_rank,view,like,collect,comment_count} |
| Link | https://juejin.cn/post/{content_id} |
| Categories | query_category_briefs returns 8 categories (Backend / Frontend / Android / iOS / AI / Dev tools / Code Life / Reading) |
| Overall ranking | category_id=1 is a convention value, not among the 8 categories |
| Category filtering | Verified working (frontend ID returns 20 frontend articles) |
Two places where assumptions are easy: author_counter counts are all 0, useless; the overall ranking 1 cannot be discovered from the category list, so it needs a separate alias mapping.
3. First delete the "Summary" from the spec
In the original SPEC, F2's output list was written like this:
Article list: title, author, likes / views, comments, article link, summary
When I actually tested the API: there's no summary field at all.
So the SPEC output columns were changed to the fields that actually exist, and the doc explicitly added "The API does not return a summary; don't promise or fabricate a summary to users."
This deserves its own section because it's the most typical class of error: writing "should have" as "the API has". Adding a nonexistent field to the spec makes downstream code fetch undefined; worse, if that spec is fed to an AI, it may invent a summary just to "fill in the field", and readers can't tell it's fabricated.
I handled type=new the same way: this request returned Internal Server Error. The parameter entry is kept, but the doc marks it "to verify, no capability promise" instead of vaguely saying "supports hot / latest rankings."
If you're not sure, say so; don't invent. Better to have one fewer capability than one more hallucination.
4. How hot.mjs is written
The script is split into small functions: parseArgs (argument and boundary validation), fetchCategories (fetch categories), resolveCategory (name → id), fetchHotList (request + field mapping), formatText (human-readable output).
The core field mapping looks like this:
const GENERAL_ID = '1'; // overall ranking convention value, not in query_category_briefs response
const POST_BASE = 'https://juejin.cn/post/';
// 2026-11-11 verified: public GET, no Cookie
// data[] = { author:{name}, content:{content_id,title},
// content_counter:{hot_rank,view,like,collect,comment_count} }
export async function fetchHotList({ categoryId, type, limit } = {}) {
const params = new URLSearchParams({
category_id: categoryId || GENERAL_ID,
type: type || 'hot',
aid: '2608',
spider: '0',
});
const r = await api(`${ENDPOINTS.articleRank}?${params}`);
if (!r || r.err_no !== 0) {
throw new Error(`Hot list fetch failed: err_no=${r && r.err_no} err_msg=${r && r.err_msg} (unofficial API, may change)`);
}
const list = Array.isArray(r.data) ? r.data : [];
return list.slice(0, limit || CFG.hot.maxLimit).map((x, i) => {
const c = x.content || {};
const cc = x.content_counter || {};
return {
rank: i + 1,
title: c.title || '(no title)',
author: (x.author && x.author.name) || '(unknown)',
articleId: String(c.content_id || ''),
url: c.content_id ? `${POST_BASE}${c.content_id}` : '',
hotRank: cc.hot_rank ?? 0,
view: cc.view ?? 0,
like: cc.like ?? 0,
collect: cc.collect ?? 0,
comment: cc.comment_count ?? 0,
};
});
}
Several design trade-offs:
- GET first, fallback to POST on failure: the category list endpoint is method-sensitive, so it reuses the dual safety already verified in
tags.mjs; - Two-level category name matching: exact match first, then fallback to contains match;
综合/全部/allare all normalized tocategory_id=1; --limitclamped to 1–50: the API returns at most 50 items per call, so instead of silently truncating, the boundary is written into parameter validation;- Text +
--jsondual output: text for humans, JSON for Agents and pipelines, the latter outputting{category, categoryId, type, fetchedAt, items}; isMainguard: whenexported for reuse by other scripts, importing it must not run the CLI;- No caching: the ranking is real-time. In two adjacent requests, the same article's heat went from
2862to2880and views from4738to4773—caching would give stale answers.
Running it looks like:
$ node scripts/hot.mjs --limit 3
Juejin Hot List · Overall · Hot · 3 items
1. Goodbye, programmers! (reposted)
XCaptain heat 2826 views 4662 likes 45 collects 18 comments 37
https://juejin.cn/post/7705987138957328427
2. AI writes pages like a manual, fixes bugs like a bandage? Write these two sentences into AGENTS.md
ikoala heat 891 views 1443 likes 22 collects 25 comments 5
https://juejin.cn/post/7705724831249432591
3. AI is eating away at our coding abilities
石小石Orz heat 765 views 1335 likes 12 collects 8 comments 7
https://juejin.cn/post/7705961716425490447
All 6 smoke tests (overall ranking / category filter / JSON pipeline / category list / invalid parameter rejection / import no side effects) passed, and SKILL.md also passed the platform's format validation.
5. A real pitfall: default config gets overwritten entirely
When adding the feature, I added a hot group to DEFAULTS in config.mjs:
hot: {
type: 'hot',
defaultLimit: 20,
maxLimit: 50,
},
Looks enough? Not quite. loadUserConfig() merges user config like this:
const merged = { ...DEFAULTS, ...raw };
merged.defaults = { ...DEFAULTS.defaults, ...(raw.defaults || {}) };
merged.imageUpload = { ...DEFAULTS.imageUpload, ...(raw.imageUpload || {}) };
merged.tagCache = { ...DEFAULTS.tagCache, ...(raw.tagCache || {}) };
Each nested group is separately shallow-merged. If I forget that line when adding hot, a user writing {"hot":{"defaultLimit":5}} in ~/.juejin/config.json will wipe out type and maxLimit entirely, turning them into undefined—two consequences: --limit's upper-bound validation becomes no-op (5 > undefined is always false), and when no count is explicitly passed, fetchHotList gets slice(0, undefined), returning an empty list.
Add this line to fix it:
merged.tagCache = { ...DEFAULTS.tagCache, ...(raw.tagCache || {}) };
+ merged.hot = { ...DEFAULTS.hot, ...(raw.hot || {}) };
The subtlety: the default path never tests this. Only when a user explicitly overrides a new group does it surface. So every time you add a nested group to DEFAULTS, you must check the merge logic synchronously—config structure's extension points are more worth adding to a checklist than a new field.
6. What you can ask now
| You say | It runs |
|---|---|
| "Look at the top 10 articles on the Juejin hot list today" | node scripts/hot.mjs --limit 10 |
| "What are the hottest articles in the frontend category" | node scripts/hot.mjs 前端 --limit 5 |
| "What categories does Juejin have" | node scripts/hot.mjs --categories |
| When you need structured data | node scripts/hot.mjs --json --limit 20 |
Output fields are fixed: rank, title, author, article ID, article link, heat value, views, likes, collects, comments. No summary—the API doesn't provide it, the script doesn't invent it.
7. Updated to-do list
| # | Feature | Status |
|---|---|---|
| F1 | Article publishing (including local image upload) | ✅ Series ① |
| F2 | Juejin hot list | ✅ Series ② (this post) |
| F3 | Juejin Pin/Boiling Point publishing | 📋 Next: short content + topic + image, reuse ImageX chain, preview confirmation before publishing |
| F4 | Juejin check-in | 📋 Planning |
| F5 | Article download (single) | 📋 Planning |
| F6 | Article batch download | 📋 Planning |
🔗 Repository: https://github.com/liuleihang/juejin-skills (MIT, Node ≥ 18, zero-dependency release scripts)
🔗 workbuddy link: https://www.workbuddy.cn/share/asset?shareCode=Z4Xnm1b8khzoZfBydIM9jb&sig=3BYkn4xsNc_4gDQn
This project is a community tool, unrelated to Juejin officially. The APIs mentioned are community-captured and verified via testing; unofficial APIs may change at any time; if
err_no != 0, report it as-is, don't blindly retry.