Collect a small set of relevant, publicly addressable TikTok videos or photo posts for a topic. Use direct hashtag, search, or For You feed URLs, accumulate lazy-loaded results, exclude cards visibly marked restricted or unavailable, and return canonical links with observed visible metadata. Treat surfaced ordering as TikTok's available ordering; do not claim global popularity or infer relevance from hidden state or audiovisual interpretation.
Use Cases
Use when the caller wants public TikTok short-form content for a hashtag, keyword topic, or the For You feed, including requests to browse visible content without interacting with posts. Inputs may be {hashtag}, {query}, {topic}, and optional {limit}. A saved browser profile or stealth mode may be supplied by the caller as session configuration; it does not change the navigation or extraction recipe.
Automation Flow
- Build the shortest direct destination:
- Hashtag:
https://www.tiktok.com/tag/{encodeURIComponent(hashtag)} - Keyword search:
https://www.tiktok.com/search?q={encodeURIComponent(query)} - For You feed:
https://www.tiktok.com/foryouSkip the homepage, search-box interaction, and post navigation unless the caller explicitly requests inspection of an individual post. Dismiss only a blocking consent, login, or modal overlay.
- Hashtag:
- On a hashtag or search page, wait about 3–4 seconds for client-rendered content. For a For You request, use the feed scroll container
#column-list-containerwhen present. TikTok recommendation cards normally usearticle[data-e2e="recommend-list-item-container"]; some rendered For You layouts expose cards assection[id^="media-card-"], so retain that fallback. Accumulate visible links across bounded scrolling rather than reading only the initial or final DOM. - Run this evaluator once, replacing
limitwith the requested limit. It preserves visible metadata, deduplicates numeric post IDs, filters explicit restriction markers, and handles both observed For You card structures. It collects a bounded surplus (target) rather than stopping atlimit, because topic relevance is judged in step 4 on the returned cards: on a For You or search page the first{limit}posts can all be off-topic, and stopping there would leave nothing to select from:
(async()=>{
const limit=8,target=Math.min(limit*3,limit+40),container=document.querySelector('#column-list-container'),videos=new Map();
const restricted=/\b(?:age[ -]?restricted|restricted content|under review|private (?:video|account|post)|(?:video|photo|post|content|this account) (?:is )?(?:unavailable|not available)|currently unavailable|no longer available)\b/i;
const clean=s=>(s||'').replace(/\s+/g,' ').trim();
const visible=e=>{if(!e)return false;const r=e.getBoundingClientRect(),s=getComputedStyle(e);return r.width>0&&r.height>0&&r.bottom>0&&r.top<innerHeight&&s.display!=='none'&&s.visibility!=='hidden'&&s.opacity!=='0'};
const isFeed=/\/foryou\/?$/i.test(location.pathname);
const countIn=s=>{const m=clean(s).match(/\d[\d.,]*\s*[KMBkmb]?/);return m?m[0].trim():null};
const metric=(card,e2e,labelSel)=>countIn(card.querySelector('[data-e2e="'+e2e+'"],[data-e2e$="'+e2e+'"]')?.innerText)||countIn(card.querySelector(labelSel)?.getAttribute('aria-label'))||null;
const collect=()=>{
const cards=isFeed
? [...document.querySelectorAll('article[data-e2e="recommend-list-item-container"],section[id^="media-card-"]')].filter(visible)
: [...document.querySelectorAll('a[href*="/video/"],a[href*="/photo/"]')].filter(visible).map(a=>a.closest('[data-e2e="challenge-item"],article,li,section')||a.parentElement?.parentElement||a.parentElement);
for(const card of cards){
const text=clean(card.innerText||card.textContent);
for(const a of [...card.querySelectorAll('a[href*="/video/"],a[href*="/photo/"]')].filter(visible)){
const href=a.href||a.getAttribute('href')||'',m=href.match(/\/(?:video|photo)\/(\d+)/);if(!m)continue;
const metadata=clean([a.getAttribute('aria-label')||'',a.getAttribute('title')||'',a.querySelector('img')?.alt||''].join(' '));
if(restricted.test(text+' '+metadata))continue;
const author=[...card.querySelectorAll('a[href^="/@"],a[href*="tiktok.com/@"]')].find(visible),v=card.querySelector('video'),id=m[1];
videos.set(id,{id,url:href.split('?')[0].split('#')[0],type:href.includes('/photo/')?'photo':'video',creator:clean(author?.innerText||author?.getAttribute('href')||'')||null,creatorUrl:author?.href?.split('?')[0]||null,caption:clean(card.querySelector('[data-e2e="challenge-item-desc"]')?.innerText||a.querySelector('img')?.alt||a.getAttribute('title')||''),cardText:text.slice(0,1200),linkText:clean(a.innerText||a.textContent).slice(0,160),ariaLabel:a.getAttribute('aria-label')||null,likes:metric(card,'like-count','[aria-label*="Like video" i]'),comments:metric(card,'comment-count','[aria-label*="comment" i]'),shares:metric(card,'share-count','[aria-label*="Share video" i]'),hashtags:[...card.querySelectorAll('a[href*="/tag/"]')].map(x=>clean(x.innerText||x.textContent)).filter(Boolean).slice(0,10),visibleImageAlts:[...card.querySelectorAll('img[alt]')].map(x=>clean(x.alt)).filter(Boolean).slice(0,5),video:v?{duration:Number.isFinite(v.duration)?Math.round(v.duration*10)/10:null,currentTime:Number.isFinite(v.currentTime)?Math.round(v.currentTime*10)/10:null,paused:v.paused,width:v.videoWidth||null,height:v.videoHeight||null}:null});
}
}
};
let unchanged=0,lastCount=0,lastTop=-1;
for(let i=0;i<30&&videos.size<target&&unchanged<3;i++){
collect();
if(isFeed&&container)container.scrollTop=Math.min(container.scrollTop+Math.max(650,container.clientHeight*.8),container.scrollHeight);else window.scrollTo(0,document.documentElement.scrollHeight);
await new Promise(r=>setTimeout(r,1200));collect();
const top=isFeed&&container?container.scrollTop:window.scrollY,count=videos.size;unchanged=count===lastCount&&top===lastTop?unchanged+1:0;lastCount=count;lastTop=top;
}
collect();
return JSON.stringify({sourceUrl:location.href.split('?')[0].split('#')[0],hashtag:location.pathname.match(/^\/tag\/([^/]+)/)?.[1]||null,query:new URL(location.href).searchParams.get('q'),requested:limit,collected:Math.min(videos.size,target),count:Math.min(videos.size,target),containerFound:!!container,videos:[...videos.values()].slice(0,target),filter:'excluded cards visibly marked restricted, private, unavailable, age-restricted, under review, or content unavailable'});
})()Select up to
{limit}results whose visible caption, card text, creator context, link text, hashtag, or image alt is relevant to{topic}. For a For You request, relevance must be based on visible text or metadata supplied by the caller; do not infer it from hidden state or audiovisual interpretation. Preserve raw evidence and state that relevance is a visible-text or metadata signal.Tag and search cards often render no like or view count at all, so whether a post is actually viral cannot be settled from this page. To confirm view counts for the selected posts, use step 3 of Analyze TikTok Post Performance by Views on each returned
url.If no qualifying public links are exposed, return an empty
videosarray and record the observed limitation. Do not reconstruct URLs from usernames, captions, wrapper IDs, or other non-URL content.
Possible Friction Points
- TikTok topic pages are directly addressable through
/tag/{hashtag}and/search?q={query}; the For You feed is directly addressable at/foryou. - Hashtags containing emoji or non-ASCII characters must be encoded with
encodeURIComponent. - Search, hashtag, and For You results lazy-load and may virtualize cards. Accumulate canonical
/video/{numeric-id}and/photo/{numeric-id}links during scrolling. - The For You feed commonly uses the scrollable
#column-list-containerand recommendation cards markedarticle[data-e2e="recommend-list-item-container"]. Some visible layouts additionally exposesection[id^="media-card-"]; use it as a fallback when recommendation articles are absent. - Cards may expose
/video/{numeric-id}or/photo/{numeric-id}links. Preserve the complete observed canonical URL after removing query strings and fragments. - Restriction filtering is evidence-based. Exclude only cards whose visible card text, title, image alt, or accessibility label contains an explicit restriction marker. Match status phrasing such as "age-restricted", "private video", or "content unavailable" rather than bare words: a public post captioned "private jet" or "unavailable this weekend" is otherwise dropped as restricted.
- Engagement counts are read from TikTok's
data-e2eattributes (like-count,comment-count,share-count) first, with the Englisharia-labelkept only as a fallback. MatchingLike videoorShare videoalone returns null on every non-English UI. A count is parsed out of whichever source is found, so a label carrying no digits yieldsnullrather than the action name itself. Absent metrics remain null and must not be treated as zero. - The first surfaced cards are not guaranteed to be globally popular or representative. Report surfaced ordering and visible evidence only.
- Consent, login prompts, regional restrictions, bot checks, or incomplete rendering can reduce discovery. Report the limitation rather than bypassing it.
- Do not infer relevance from unavailable video content, hidden comments, hidden application state, or guessed URLs. Do not fabricate creator handles, IDs, or metrics.