/s/stay 가 "스테이머뭄" 구글 검색에서 통째로 빠졌다. out/s/ 에 목업·백업(stay2 · stay3 · stay.old)이 운영본과 제목·본문이 같은 채(단어 87%) 각자 자기를 canonical 로 가리키고 있었고, 사이트맵·/s 목록이 디렉토리를 훑어 이들을 전부 구글에 제출했다 — 같은 글 여러 벌 중 구글이 하나만 고른다. 목업 셋은 운영 볼륨에서 noindex 로 바꿨고(2026-09-11), 이 커밋은 그런 페이지가 다시 제출되지 않게 한다. - seo/directory.ts: readBakedNoindex — 구운 HTML 의 robots 메타를 읽는다. 슬러그 이름(.old)으로 거르지 않는다 — 페이지 자신의 선언이 유일한 출처다 - prerender.ts writeRootMachineFiles: noindex 페이지를 사이트맵·/s 목록·루트 llms.txt 에서 뺀다. 남겨 두면 서치콘솔이 "제출된 URL 에 noindex" 를 계속 띄운다 vitest directory.test.ts 8 passed · tsc·eslint 통과 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CEQ9auj65yJKk2MnWbtRqU
64 lines
2.7 KiB
TypeScript
64 lines
2.7 KiB
TypeScript
/**
|
|
* 구운 HTML → 사이트맵·목록 값.
|
|
*
|
|
* ★ 이 테스트가 지키는 것은 **커플링 하나**다. 사이트맵의 lastmod 는 페이지가 head 에
|
|
* 선언한 dateModified 와 같은 값이어야 한다 — 구글이 lastmod 를 신뢰하는 조건이
|
|
* "페이지의 실제 수정과 대조해 맞을 것" 이기 때문이다. head.ts 가 태그 모양을 바꾸면
|
|
* readBakedLastmod 는 조용히 undefined 를 내고 사이트맵은 mtime 으로 되돌아간다 —
|
|
* 빌드는 통과하고 화면도 멀쩡한, 눈으로 못 찾는 종류의 회귀다. 그래서 붙인다.
|
|
*/
|
|
import {describe, expect, it} from 'vitest';
|
|
|
|
import {MOONLIGHT_STAY_PAYLOAD} from '../fixtures/moonlight-stay';
|
|
import {readBakedLastmod, readBakedNoindex, readBakedTitle} from './directory';
|
|
import {renderHead} from './head';
|
|
import {homeMeta} from './meta';
|
|
|
|
/** 실제 프리렌더와 같은 경로로 head 를 굽는다(prerender.ts prerenderSite). */
|
|
function bakedHead(): string {
|
|
const payload = MOONLIGHT_STAY_PAYLOAD;
|
|
return renderHead({payload, meta: homeMeta(payload)});
|
|
}
|
|
|
|
describe('readBakedLastmod', () => {
|
|
it('head 가 선언한 dateModified 를 그대로 돌려준다', () => {
|
|
expect(readBakedLastmod(bakedHead())).toBe(MOONLIGHT_STAY_PAYLOAD.site.updatedAt);
|
|
});
|
|
|
|
it('메타가 없으면 undefined — 호출부가 mtime 으로 떨어진다', () => {
|
|
expect(readBakedLastmod('<html><head><title>x</title></head></html>')).toBeUndefined();
|
|
});
|
|
|
|
it('날짜로 못 읽는 값은 버린다 — 틀린 lastmod 는 없는 것보다 나쁘다', () => {
|
|
expect(readBakedLastmod('<meta name="dateModified" content="어제" />')).toBeUndefined();
|
|
expect(readBakedLastmod('<meta name="dateModified" content="" />')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('readBakedTitle', () => {
|
|
it('구운 HTML 의 title 을 꺼낸다', () => {
|
|
expect(readBakedTitle(bakedHead())).toBe(homeMeta(MOONLIGHT_STAY_PAYLOAD).title);
|
|
});
|
|
|
|
it('title 이 없으면 빈 문자열 — 호출부가 슬러그로 떨어진다', () => {
|
|
expect(readBakedTitle('<html><head></head></html>')).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('readBakedNoindex', () => {
|
|
it('발행본 head 는 index 다 — 사이트맵에 남는다', () => {
|
|
expect(readBakedNoindex(bakedHead())).toBe(false);
|
|
});
|
|
|
|
it('noindex 를 선언한 목업은 걸러진다', () => {
|
|
const mockup =
|
|
'<meta name="robots" content="noindex, follow, max-snippet:-1, max-image-preview:large" />';
|
|
expect(readBakedNoindex(mockup)).toBe(true);
|
|
expect(readBakedNoindex('<meta name="robots" content="none" />')).toBe(true);
|
|
});
|
|
|
|
it('robots 메타가 없으면 색인 대상으로 친다', () => {
|
|
expect(readBakedNoindex('<html><head><title>x</title></head></html>')).toBe(false);
|
|
});
|
|
});
|