xiaohongshu.com

get-trending-content

Installation

Adds this website's skill for your agents

browse skills add xiaohongshu.com/get-trending-content-lrb8ax
Summary

Return the current batch of trending Xiaohongshu (RedNote) feed cards — each card's 1-line displayTitle, type, video duration, like count, author, and canonical note URL. Filter to short videos for the short-video use case.

FIG. 01
FIG. 02
FIG. 03
FIG. 04
SKILL.md
234 lines

Xiaohongshu Trending Short-Video Titles

Purpose

Return the current batch of trending Xiaohongshu (RedNote, 小红书) feed cards from xiaohongshu.com/explore, with each card's 1-line title (displayTitle), content type (video vs image post), video duration in seconds, like count, author handle, note id, and canonical reader URL. Filter noteCard.type === "video" for the short-video subset. The same shape works for the algorithmic recommend feed (default) and for any of the ten category channels (fashion, food, beauty, fitness, etc.). Read-only — never likes, comments, follows, or posts.

When to Use

  • A short-video pipeline that needs fresh trending titles + durations + like counts to seed downstream summarization, captioning, or trend-spotting.
  • Daily / hourly polling of XHS hot content for marketing or content-research dashboards.
  • A "what's hot on Xiaohongshu right now in {fashion|food|fitness|...}" agent query.
  • Anywhere you would otherwise scrape the rendered explore page — the SSR __INITIAL_STATE__ blob beats a snapshot loop by ~50× and never needs the signed XHS API.

Workflow

The Xiaohongshu explore page is server-side rendered: every response embeds a full window.__INITIAL_STATE__ = {...} JSON dump whose state.feed.feeds[] array contains the 22–27 trending notes the homepage would have rendered. You do not need a real browser. One HTTP GET via Browserbase Fetch with a residential proxy returns the same data as opening the page in Chrome.

1. Fetch the explore page through a residential proxy

bb fetch "https://www.xiaohongshu.com/explore" \
  --allow-redirects --proxies \
  --output /tmp/xhs-explore.html

--proxies is mandatory. From a datacenter IP, Xiaohongshu returns a 10 KB anti-bot stub with no __INITIAL_STATE__. With residential, the response is ~580 KB and contains the full SSR feed. Status code is 200 in both cases — check the response body size or the presence of window.__INITIAL_STATE__, not the status, to detect the wall.

To scope to a category, append ?channel_id=<id>. The channel enum is published in the SSR payload itself (state.feed.channels.categories); copy below:

channel_idCategory (中文)English label
homefeed_recommend (default — no param)推荐Algorithmic recommend (mixed)
homefeed.fashion_v3穿搭Fashion
homefeed.food_v3美食Food
homefeed.cosmetics_v3彩妆Beauty / Makeup
homefeed.movie_and_tv_v3影视Movies & TV
homefeed.career_v3职场Career
homefeed.love_v3情感Relationships
homefeed.household_product_v3家居Home
homefeed.gaming_v3游戏Gaming
homefeed.travel_v3旅行Travel
homefeed.fitness_v3健身Fitness

There are also two legacy/unlisted channels not in state.feed.channels.categories but still served: homefeed.video_feed and homefeed.video_v2_feed. They are not strict video filters — they return mixed content that simply leans heavier on video. Don't rely on them as a server-side video filter; filter client-side.

2. Extract __INITIAL_STATE__ and decode the feed

import fs from "node:fs";
const html = fs.readFileSync("/tmp/xhs-explore.html", "utf8");
const m = html.match(/window\.__INITIAL_STATE__\s*=\s*(\{[\s\S]*?\})\s*<\/script>/);
// XHS emits `undefined` literals — replace before JSON.parse
const state = JSON.parse(m[1].replace(/:\s*undefined/g, ":null"));
const feeds = state.feed.feeds;

feeds is an array of { id, modelType, noteCard, xsecToken, trackId, ... }. Each noteCard has the fields you actually want:

FieldDescription
noteCard.displayTitleThe 1-line title shown on the card. Can be very short (1–2 chars) or contain emoji — never strip non-ASCII.
noteCard.type"video" or "normal" (image post). Filter === "video" for the short-video subset.
noteCard.video.capa.durationVideo length in seconds (integer). Present only when type === "video". Range observed: 7 s – 560 s.
noteCard.interactInfo.likedCountPre-formatted Chinese number string: "5844", "2.9万" (29k), "10万+" (100k+). Don't try to parse to an int — keep the string.
noteCard.user.nickName / user.nicknameAuthor display name. Both casings appear; prefer nickName, fall back to nickname.
noteCard.user.userIdAuthor's user id.
f.idNote id (24-char hex).
f.xsecTokenPer-request access token. Required to build the canonical URL — without it the reader page returns "笔记不见了" / 404.

3. Construct the canonical reader URL

https://www.xiaohongshu.com/explore/{f.id}?xsec_token={f.xsecToken}&xsec_source=pc_feed

xsec_source=pc_feed is the source tag matching what the SSR page would set when you click through. Any other value (e.g. pc_search) will also work but the source attribution differs.

4. Filter and return

For the short-video use case:

const shortVideos = feeds
  .filter(f => f.noteCard?.type === "video")
  .map(f => ({
    title: f.noteCard.displayTitle,
    duration_sec: f.noteCard.video.capa.duration,
    likes: f.noteCard.interactInfo.likedCount,
    author: f.noteCard.user.nickName || f.noteCard.user.nickname,
    note_id: f.id,
    url: `https://www.xiaohongshu.com/explore/${f.id}?xsec_token=${f.xsecToken}&xsec_source=pc_feed`,
  }));

A single request yields ~6–19 short videos depending on the channel (fitness_v3 is heaviest at ~70% video; fashion_v3 is lightest at ~25%). To collect more than one batch, re-fetch — every fresh GET returns a different batch (0% overlap between two consecutive default-channel fetches in the converged test). The feed is freshly resampled per request, not paginated by cursor.

5. Pagination beyond the SSR batch — only if you really need it

The XHS internal endpoint https://edith.xiaohongshu.com/api/sns/web/v1/homefeed accepts {cursor_score, num, category, refresh_type} POST bodies but is gated by an X-S HMAC signature and X-T timestamp computed in obfuscated browser JS. The signature rotates per build and there is no maintained open-source library that tracks it reliably. Don't try to forge the signature. If you need more than the SSR batch, the honest path is to drive a real browser (Browserbase remote, --advanced-stealth --proxies), navigate to /explore, scroll, and re-extract window.__INITIAL_STATE__.feed.feeds after each scroll-triggered append.

Browser fallback (only for pagination)

# Mandatory: residential proxy + advanced stealth, both flags.
SID=$(bb sessions create --keep-alive --proxies --advanced-stealth | jq -r '.id')
browse --connect "$SID" open "https://www.xiaohongshu.com/explore"
browse --connect "$SID" wait selector ".note-item"
# Read the first batch from the rendered page
browse --connect "$SID" get html body > page-1.html
# Trigger more loads
for i in 1 2 3 4; do
  browse --connect "$SID" scroll 600 400 0 1500
  browse --connect "$SID" wait timeout 1500
done
browse --connect "$SID" get html body > page-deep.html
# Same regex on the rendered DOM yields the deeper appended cards.

Deduplicate by f.id across batches.

Site-Specific Gotchas

  • --proxies is required, full stop. Datacenter IPs receive a 10,352-byte HTML stub with no __INITIAL_STATE__, no feeds[], and only a formula-runtime JS bootstrap. Residential IPs receive ~579 KB with the full SSR payload. Both responses return HTTP 200, so don't gate on status code — gate on response body size (>100 KB) or on html.includes("__INITIAL_STATE__").
  • The page emits undefined literals inside the JSON state, which JSON.parse rejects. Always run raw.replace(/:\s*undefined/g, ":null") (or equivalent) before parsing. The OpenSearch CSP-friendly value is null.
  • xsec_token is per-request and required for canonical URLs. Reusing a token from yesterday's fetch will 404 the reader page. Either keep the token alongside the note id, or re-fetch immediately before navigating.
  • displayTitle may be 1–2 characters or pure emoji ("练", "📷课代表上线"). Don't strip non-ASCII, don't drop short titles — those are real cards, often the most viral.
  • likedCount is a pre-formatted Chinese display string, not an integer: "5844", "2.9万" (29,000), "10万+" (>100k). Decode formula: "X.Y万" = X.Y × 10000, "X万+" = >X × 10000. Keep the raw string in your output for fidelity; parse to a numeric only at sort/threshold time.
  • The legacy channels homefeed.video_feed and homefeed.video_v2_feed are NOT strict video filters, despite the name. They return mixed content (15 video + 12 image in one observed run for video_feed; 7 video + 15 image for video_v2_feed). Always filter noteCard.type === "video" client-side regardless of channel.
  • The feed is fresh-per-request, not cursor-paginated. Two consecutive GETs of /explore returned 23 and 27 cards with zero overlap by f.id. To collect a wider sample, loop the GET (with a short delay) and dedupe by id — don't try to find a ?page=2 param. None exist.
  • Don't try to forge the X-S/X-T signature for the /api/sns/web/v1/homefeed POST endpoint. The hash is computed by minified, regularly-rotated browser JS and there is no maintained signature lib. Confirmed dead-end during testing — only reachable from a real browser context after the page boots.
  • Note ids are 24-char hex (e.g. 640c6b480000000027011016). The first 8 hex chars encode the post creation timestamp (Unix epoch in seconds) — parseInt(id.slice(0,8), 16) gives the creation time, useful for filtering "trending in the last 24h".
  • CJK only. Almost all titles are Simplified Chinese; if your downstream pipeline expects English, plan to translate after extraction. The site has an English newsroom subdomain (www.xiaohongshu.com/en/newsroom/) but no English explore feed.
  • No auth required for explore. Don't waste time threading cookies or login state for this skill — the SSR feed is available anonymously. Cookies only matter if you need personalized recommendations.

Expected Output

A JSON object with success, the channel queried, the timestamp of capture, and an array of short-video items. Each item carries the title, type, duration, like count, author, and a clickable canonical URL.

{
  "success": true,
  "channel_id": "homefeed_recommend",
  "fetched_at": "2026-05-18T21:19:14Z",
  "total_cards": 23,
  "short_videos": [
    {
      "title": "救命🆘这里有一只芭蕾小熊哇~",
      "type": "video",
      "duration_sec": 19,
      "likes": "3.4万",
      "author": "饭一碗",
      "note_id": "640c6b480000000027011016",
      "url": "https://www.xiaohongshu.com/explore/640c6b480000000027011016?xsec_token=ABDqV1xAR3SEoDdoB40_yD04rQV8-A8MpNSgFQSdePSyc=&xsec_source=pc_feed"
    },
    {
      "title": "姐妹试这个中分丸子新扎法技巧 巨好看!",
      "type": "video",
      "duration_sec": 81,
      "likes": "4.8万",
      "author": "漫妮Fiona",
      "note_id": "63e9d57e0000000014024b16",
      "url": "https://www.xiaohongshu.com/explore/63e9d57e0000000014024b16?xsec_token=...&xsec_source=pc_feed"
    },
    {
      "title": "练",
      "type": "video",
      "duration_sec": 75,
      "likes": "10万+",
      "author": "白敬亭",
      "note_id": "6481cf3b000000001300a7a6",
      "url": "https://www.xiaohongshu.com/explore/6481cf3b000000001300a7a6?xsec_token=...&xsec_source=pc_feed"
    }
  ]
}

Failure shape when the anti-bot wall fires (datacenter IP, no --proxies):

{
  "success": false,
  "reason": "antibot_stub",
  "detail": "Response body is 10352 bytes with no window.__INITIAL_STATE__ block — residential proxy was not applied. Re-issue with `bb fetch --proxies`."
}

Empty-channel shape (none of the canonical channels return empty in practice, but be defensive):

{
  "success": true,
  "channel_id": "homefeed.gaming_v3",
  "fetched_at": "2026-05-18T21:19:14Z",
  "total_cards": 22,
  "short_videos": []
}