188 lines
16 KiB
JavaScript
188 lines
16 KiB
JavaScript
import {test} from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {seo,clientSeo,headDocument} from './helpers/seo.mjs';
|
|
|
|
const config={seoInfo:{title:'全站标题',description:'全站描述',keywords:['全站词'],webSiteName:'91剧场'},config:{foreverUrlAddr:'https://drama.example.net/entry/?channel=1'}};
|
|
const environment=seo.seoEnvironment(config);
|
|
const page=(path,content={},search='')=>seo.buildPageHead(path,search,environment,content);
|
|
|
|
test('path-based search metadata retains keywords and page numbers without becoming indexable',()=>{
|
|
const head=page('/search/'+encodeURIComponent('都市 / 爱情')+'/page/2/',{totalCount:60,pageSize:25});
|
|
assert.ok(head.title.includes('都市 / 爱情 · 搜索结果'));
|
|
assert.ok(head.title.includes('第2页'));
|
|
assert.equal(head.robots,'noindex,follow');
|
|
assert.equal(head.canonical,undefined);assert.equal(head.jsonLd,undefined);
|
|
});
|
|
|
|
test('normalizes legacy/string SEO values and uses the permanent site origin',()=>{
|
|
assert.deepEqual(seo.normalizeSeo(JSON.stringify({webTitle:' 标题 ',webDescription:' 简介 ',keyworld:'都市,甜宠;都市',webSiteName:'91'})),{title:'标题',description:'简介',keywords:['都市','甜宠'],siteName:'91'});
|
|
assert.equal(environment.origin,'https://drama.example.net');
|
|
for(const value of ['',null,'""','{bad',[],12])assert.equal(seo.normalizeSeo(value).title,'');
|
|
for(const value of ['javascript:alert(1)','https://user:secret@host.test','localhost:3000','https://example.com/path','https://www.xx.xxx/'])assert.equal(seo.siteOrigin(value),'',value);
|
|
assert.equal(seo.seoEnvironment({config:{foreverUrlAddr:'https://example.com'},newAddressInfo:{foreverUrlAddr:'drama.example.org/entry'}}).origin,'https://drama.example.org');
|
|
assert.equal(seo.seoEnvironment({}).origin,'');
|
|
});
|
|
|
|
test('home uses global SEO, independent route fields survive, and copied homepage fields fall back to page-specific content',()=>{
|
|
const home=page('/');assert.equal(home.title,'全站标题 - 91剧场');assert.equal(home.description,'全站描述');assert.equal(home.siteName,'91剧场');
|
|
const copied=page('/shorts/',{seoInfo:config.seoInfo});assert.equal(copied.title,'短剧 - 91剧场');assert.notEqual(copied.description,'全站描述');
|
|
const own=page('/topics/7/',{title:'专题名称',seoInfo:{title:'独立专题TDK',description:'独立描述',keywords:'专题词'}});
|
|
assert.equal(own.title,'独立专题TDK - 91剧场');assert.equal(own.description,'独立描述');assert.deepEqual(own.keywords,['专题词']);
|
|
const detail=page('/detail/9/',seo.dramaSeoContent({id:9,title:'都市故事',desc:'故事简介',tagNames:['都市'],seoInfo:{title:'剧集独立TDK'}}));
|
|
assert.equal(detail.title,'剧集独立TDK - 91剧场');assert.equal(detail.description,'故事简介');assert.ok(detail.keywords.includes('都市'));
|
|
const episode=page('/play/9/90/',seo.dramaSeoContent({id:9,title:'都市故事',desc:'故事简介'},{currentEpisode:3}));
|
|
assert.equal(episode.title,'都市故事 - 第3集 | 91剧场');assert.ok(episode.description.includes('第3集'));assert.equal(episode.type,'video.episode');
|
|
});
|
|
|
|
test('navigation TDK has priority on category routes and does not replace drama content',()=>{
|
|
const nav=seo.seoEnvironment(config,[{urlValue:'/playlet',seoInfo:{title:'短剧分类TDK',keyworld:['分类词'],description:'分类描述'}},{urlValue:'/topics/7/',seoInfo:{title:'独立专题分类TDK'}}]);
|
|
assert.equal(seo.buildPageHead('/shorts/page/2/','',nav,{seoInfo:{title:'列表TDK'},totalCount:72}).title,'短剧分类TDK - 第2页');
|
|
const detail=seo.buildPageHead('/detail/9/','',nav,{title:'剧名',seoInfo:{title:'剧集TDK',description:'剧集描述'}});
|
|
assert.equal(detail.title,'剧集TDK - 91剧场');assert.equal(detail.description,'剧集描述');
|
|
assert.equal(seo.buildPageHead('/topics/7/','',nav).title,'独立专题分类TDK');
|
|
const withoutOwnTDK=seo.buildPageHead('/detail/9/','',nav,seo.dramaSeoContent({id:9,title:'具体剧名',desc:'具体简介'}));
|
|
assert.equal(withoutOwnTDK.title,'具体剧名 - 91剧场');assert.equal(withoutOwnTDK.description,'具体简介');
|
|
const ranking=seo.seoEnvironment(config,[{urlValue:'/rank/',seoInfo:{title:'排行榜配置标题'}}]);
|
|
assert.equal(seo.buildPageHead('/rank/new/','',ranking).title,'排行榜配置标题 - 新剧榜');
|
|
assert.equal(seo.buildPageHead('/rank/follow/','',ranking).title,'排行榜配置标题 - 追剧榜');
|
|
assert.equal(page('/rank/').canonical,page('/rank/hot/').canonical);
|
|
const home=seo.buildPageHead('/','',seo.seoEnvironment(config,[{urlValue:'https://external.test/',seoInfo:{title:'外站'}}]));assert.equal(home.title,'全站标题 - 91剧场');
|
|
});
|
|
|
|
test('configuration failures and missing permanent origins retain the last valid SEO environment',()=>{
|
|
const previous=seo.resolveSeoEnvironment(config,[{urlValue:'/shorts/',seoInfo:{title:'短剧分类'}}]);
|
|
for(const broken of [{},{seoInfo:{webSiteName:'错误品牌'}},{config:{foreverUrlAddr:'javascript:bad'}},{config:{foreverUrlAddr:'http://localhost:3000'}}]){
|
|
assert.equal(seo.resolveSeoEnvironment(broken,undefined,previous),previous);
|
|
const head=seo.buildPageHead('/shorts/page/2/','',seo.resolveSeoEnvironment(broken,undefined,previous),{totalCount:72});
|
|
assert.equal(head.canonical,'https://drama.example.net/shorts/page/2/');assert.equal(head.siteName,'91剧场');
|
|
assert.equal(head.jsonLd['@graph'].find(item=>item['@type']==='Organization').name,'91剧场');
|
|
}
|
|
const partial=seo.resolveSeoEnvironment({config:config.config},undefined,previous);
|
|
assert.equal(partial.site.siteName,'91剧场');assert.equal(partial.site.title,'全站标题');assert.deepEqual(partial.categories,previous.categories);
|
|
const updated=seo.resolveSeoEnvironment({config:{foreverUrlAddr:'https://new.drama.test/path/'},seoInfo:{webSiteName:'新站名'}},[],previous);
|
|
assert.equal(updated.origin,'https://new.drama.test');assert.equal(updated.site.siteName,'新站名');assert.deepEqual(updated.categories,[]);
|
|
});
|
|
|
|
test('cold starts never guess a canonical and the browser preserves the server identity',()=>{
|
|
const cold=seo.resolveSeoEnvironment({},[]);
|
|
assert.equal(cold.origin,'');assert.equal(cold.site.siteName,'黄果短剧');
|
|
const head=seo.buildPageHead('/shorts/','',cold,{totalCount:72});
|
|
assert.equal(head.canonical,undefined);assert.equal(head.jsonLd,undefined);assert.equal(head.next,undefined);
|
|
assert.equal(seo.clientSeoEnvironment(environment,{config:{foreverUrlAddr:'https://other.drama.test'},seoInfo:{webSiteName:'其他品牌'}}),environment);
|
|
assert.equal(seo.clientSeoEnvironment(environment,{}),environment);
|
|
const recovered=seo.clientSeoEnvironment(cold,config);
|
|
assert.equal(recovered.origin,environment.origin);assert.equal(recovered.site.siteName,environment.site.siteName);
|
|
const entity=page('/detail/9/',{title:'作品',seoInfo:{webSiteName:'作品接口的其他品牌'}});
|
|
assert.equal(entity.siteName,'91剧场');assert.ok(entity.title.endsWith(' - 91剧场'));
|
|
assert.equal(entity.jsonLd['@graph'].find(item=>item['@type']==='Organization').name,entity.siteName);
|
|
});
|
|
|
|
test('canonical is query-free, every pagination page identifies itself, and first/middle/last page links are bounded',()=>{
|
|
const first=page('/shorts',{totalCount:72,pageSize:24},'?channel=test#fragment');
|
|
assert.equal(first.canonical,'https://drama.example.net/shorts/');assert.equal(first.previous,undefined);assert.equal(first.next,'https://drama.example.net/shorts/page/2/?channel=test');
|
|
const middle=page('/shorts/page/2/',{totalCount:72,pageSize:24});
|
|
assert.equal(middle.canonical,'https://drama.example.net/shorts/page/2/');assert.equal(middle.previous,'https://drama.example.net/shorts/');assert.equal(middle.next,'https://drama.example.net/shorts/page/3/');
|
|
assert.match(middle.title,/第2页/);assert.match(middle.description,/第2页/);assert.ok(middle.keywords.includes('第2页'));
|
|
const last=page('/shorts/page/3/',{totalCount:72,pageSize:24});assert.equal(last.next,undefined);assert.equal(last.previous,'https://drama.example.net/shorts/page/2/');
|
|
assert.equal(page('/shorts/page/1/',{totalCount:72}).canonical,'https://drama.example.net/shorts/');
|
|
const overflow=page('/shorts/page/4/',{totalCount:72,pageSize:24});assert.equal(overflow.canonical,undefined);assert.match(overflow.robots,/noindex/);assert.equal(overflow.previous,undefined);
|
|
for(const totalCount of [0,1,24]){const head=page('/shorts/',{totalCount});assert.equal(head.previous,undefined);assert.equal(head.next,undefined);}
|
|
for(const totalCount of [-1,NaN,'72']){const head=page('/shorts/',{totalCount});assert.equal(head.next,undefined);}
|
|
});
|
|
|
|
test('private/tools/search/error pages do not inherit public canonical or indexability',()=>{
|
|
for(const path of ['/search/爱情/','/navigation/','/home-road/','/apps/','/unknown-page/']){const head=page(path);assert.match(head.robots,/noindex,follow/);assert.equal(head.canonical,undefined,path);}
|
|
for(const path of ['/member/center/','/member/likes/','/info/deposit/','/info/feedback/']){const head=page(path);assert.equal(head.robots,'noindex,follow',path);assert.equal(head.canonical,undefined,path);assert.equal(seo.pageResponseStatus(path,{}),404,path);}
|
|
assert.match(page('/shorts/',{},'?sort=2').robots,/noindex/);assert.equal(page('/shorts/',{},'?sort=2').canonical,undefined);
|
|
for(const content of [{failed:true},{missing:true},{unavailable:true}])assert.equal(page('/detail/9/',content).canonical,undefined);
|
|
for(const path of ['/info/about-us/','/info/privacy-policy/','/rank/hot/','/rank/new/','/rank/follow/'])assert.ok(page(path).canonical,path);
|
|
});
|
|
|
|
test('random shorts metadata matches the selected feed and remains excluded from indexing',()=>{
|
|
const head=page('/shorts/page/2/',{totalCount:72},'?sort=4');
|
|
assert.ok(head.title.includes('随机推荐'));assert.ok(head.description.includes('随机推荐'));assert.ok(head.title.includes('第2页'));
|
|
assert.equal(head.robots,'noindex,follow');assert.equal(head.canonical,undefined);
|
|
});
|
|
|
|
test('client navigation updates SSR head once and clears old canonical/OG URL/pagination after private or failed routes',()=>{
|
|
const doc=headDocument();
|
|
const first=page('/shorts/page/2/',{totalCount:72});clientSeo.applyPageHead(first,doc);
|
|
const duplicate=doc.createElement('link');duplicate.setAttribute('rel','canonical');doc.head.appendChild(duplicate);
|
|
const last=page('/rank/new/page/3/',{totalCount:72});clientSeo.applyPageHead(last,doc);
|
|
assert.equal(doc.head.querySelectorAll('link[rel="canonical"]').length,1);
|
|
assert.equal(doc.title,last.title);assert.equal(doc.value('meta[property="og:site_name"]'),'91剧场');
|
|
assert.equal(doc.value('link[rel="canonical"]'),last.canonical);assert.equal(doc.value('meta[property="og:url"]'),last.canonical);
|
|
assert.equal(doc.value('link[rel="next"]'),undefined);assert.equal(doc.value('link[rel="prev"]'),last.previous);
|
|
const privatePage=page('/unknown-page/');clientSeo.applyPageHead(privatePage,doc);
|
|
assert.equal(doc.value('link[rel="canonical"]'),undefined);assert.equal(doc.value('meta[property="og:url"]'),undefined);assert.equal(doc.value('link[rel="prev"]'),undefined);
|
|
assert.equal(doc.value('meta[name="robots"]'),'noindex,follow');assert.equal(doc.value('meta[name="googlebot"]'),'noindex,follow');
|
|
assert.equal(doc.value('meta[name="twitter:title"]'),privatePage.title);
|
|
});
|
|
|
|
|
|
test('episode metadata uses chapter SEO before drama SEO and keeps the real episode number',()=>{
|
|
const data=seo.dramaSeoContent({id:9,title:'都市故事',seoInfo:{title:'剧集标题',description:'剧集描述'}},{id:90,currentEpisode:3,seoInfo:{title:'章节标题',description:'章节描述',keyworld:'章节词'}});
|
|
const head=page('/play/9/90/',data);
|
|
assert.ok(head.title.includes('章节标题'));assert.equal(head.description,'章节描述 - 第3集 | 91剧场');assert.ok(head.keywords.includes('章节词'));assert.equal(data.episode,3);
|
|
});
|
|
|
|
test('topic aliases and numeric ID variants share a primary canonical without changing catalog pagination',()=>{
|
|
for(const [path,expected] of [
|
|
['/theme/007/','/theme/7/'],['/theme/007/page/2/','/theme/7/page/2/'],
|
|
['/topics/007/page/1/','/topics/7/'],['/tag/009/','/tag/9/'],['/detail/009/','/detail/9/'],
|
|
]){
|
|
const head=page(path,{totalCount:72},'?utm_source=test');
|
|
assert.equal(head.canonical,environment.origin+expected,path);assert.ok(head.robots.startsWith('index,'),path);
|
|
}
|
|
const alias=page('/theme/007/page/2/',{totalCount:72});
|
|
assert.equal(alias.previous,environment.origin+'/theme/7/');assert.equal(alias.next,environment.origin+'/theme/7/page/3/');
|
|
assert.equal(page('/shorts/page/2/',{totalCount:72}).canonical,environment.origin+'/shorts/page/2/');
|
|
assert.equal(page('/search/007/').canonical,undefined);
|
|
});
|
|
|
|
test('empty public lists are noindex while preserving successful and error statuses; detail/play pages are not paginated',()=>{
|
|
for(const path of ['/shorts/','/topics/','/topics/7/','/theme/7/','/tag/9/','/rank/hot/']){
|
|
const content={totalCount:0},head=page(path,content);
|
|
assert.equal(head.robots,'noindex,follow',path);assert.equal(head.canonical,undefined);assert.equal(head.jsonLd,undefined);
|
|
assert.equal(seo.pageResponseStatus(path,content),200);assert.equal(seo.pageResponseStatus(path+'page/2/',content),404);
|
|
assert.equal(seo.pageResponseStatus(path,{...content,failed:true}),500);
|
|
assert.equal(seo.pageResponseStatus(path,{...content,missing:true}),404);
|
|
}
|
|
const drama=seo.dramaSeoContent({id:9,title:'作品名',desc:'作品简介'});
|
|
const playable=seo.dramaSeoContent({id:9,title:'作品名'},{id:90,currentEpisode:3,videoUrl:'https://media.example.net/3.mp4'});
|
|
for(const [path,data] of [['/detail/9/',drama],['/play/9/90/',playable]]){
|
|
assert.ok(page(path,{...data,totalCount:0,pageSize:10}).robots.startsWith('index,'),path);
|
|
const content={...data,totalCount:35,pageSize:10};
|
|
assert.equal(seo.pageResponseStatus(path+'page/2/',content),404,'detail and play pages no longer paginate comments');
|
|
}
|
|
});
|
|
|
|
test('more snapshots need visible collection information and actual items before indexing',()=>{
|
|
const route={group:'page',key:'config',value:'31'};
|
|
const data={name:'都市精选',desc:'都市题材作品精选合集',list:[{id:9}],totalCount:1};
|
|
const complete=seo.listSeoContent(data,route);
|
|
assert.ok(page('/config/0031/',complete).robots.startsWith('index,'));
|
|
assert.equal(page('/config/0031/',complete).canonical,environment.origin+'/config/31/');
|
|
for(const patch of [{name:''},{desc:''},{list:[]},{list:[{id:-1}]},{desc:'<script>bad()</script>'}]){
|
|
const content=seo.listSeoContent({...data,...patch},route),head=page('/config/31/',content);
|
|
assert.equal(head.robots,'noindex,follow');assert.equal(head.jsonLd,undefined);assert.equal(seo.pageResponseStatus('/config/31/',content),200);
|
|
}
|
|
const topics=seo.listSeoContent({name:'主题精选',desc:'主题合集介绍',type:3,themeList:[{id:7}],totalCount:1},route);
|
|
assert.deepEqual(topics.items,[{path:'/theme/7/'}]);assert.ok(page('/config/31/',topics).canonical);
|
|
});
|
|
|
|
test('episode indexing requires an actual authorized source and episode identity, not metadata or a shared preview',()=>{
|
|
const data={id:9,title:'作品名',desc:'作品简介',preview:'https://media.example.net/shared-preview.mp4'};
|
|
const chapter={id:90,currentEpisode:3,videoUrl:'https://media.example.net/3.mp4'};
|
|
for(const patch of [{videoUrl:''},{videoUrl:'javascript:bad'},{currentEpisode:0},{currentEpisode:undefined},{canWatch:false,needUnlock:true},{canWatch:false,needVip:true}]){
|
|
const content=seo.dramaSeoContent(data,{...chapter,...patch,seoInfo:{title:'单集标题',description:'单集描述'}});
|
|
const head=page('/play/9/90/',content);
|
|
assert.equal(head.robots,'noindex,follow');assert.equal(head.jsonLd,undefined);assert.equal(seo.pageResponseStatus('/play/9/90/',content),200);
|
|
if(patch.needUnlock||patch.needVip)assert.equal(content.entity.video.source,undefined);
|
|
}
|
|
for(const patch of [{},{payType:1,canWatch:true},{payType:2,canWatch:true}])assert.ok(page('/play/9/90/',seo.dramaSeoContent(data,{...chapter,...patch})).canonical);
|
|
const doc=headDocument();clientSeo.applyPageHead(page('/play/9/90/',seo.dramaSeoContent(data,chapter)),doc);
|
|
clientSeo.applyPageHead(page('/play/9/90/',seo.dramaSeoContent(data,{...chapter,needUnlock:true})),doc);
|
|
assert.equal(doc.value('meta[name="robots"]'),'noindex,follow');assert.equal(doc.value('link[rel="canonical"]'),undefined);
|
|
});
|