# QPOST / Tagmoa 사이트맵 운영 규칙

## 1. 목적

QPOST의 사이트맵은 **검색엔진에 실제로 색인되어도 되는 대표 URL만 제공하는 것**을 목적으로 한다.

사이트맵은 다음 세 가지 개념을 분리해서 처리한다.

```text
검색엔진 색인 여부
→ indexable

테넌트 콘텐츠 공개 여부
→ 게시판/게시글의 공개·발행·접근 상태

Tagmoa 플랫폼 운영 상태
→ 수집 / 활성 / 숨김 / 차단 / moderation
```

Tagmoa 플랫폼에 수집되었다는 이유만으로 검색엔진에 색인하지 않으며, 반대로 Tagmoa에 수집되지 않은 테넌트 콘텐츠도 정상적인 공개 콘텐츠라면 sitemap에 포함할 수 있다.

---

# 2. 기본 사이트맵 판단 흐름

사이트맵은 다음 순서로 판단한다.

```text
Tenant
  ↓
indexable = true?
  ↓
대표 canonical URL 결정
  ↓
실제 외부 공개 콘텐츠인가?
  ↓
발행된 콘텐츠인가?
  ↓
예약 발행 시간이 지났는가?
  ↓
비밀번호·접근 제한이 없는가?
  ↓
검색 차단 대상이 아닌가?
  ↓
sitemap 출력
```

하나라도 조건을 충족하지 않으면 해당 URL은 sitemap에서 제외한다.

---

# 3. 테넌트 검색엔진 색인 설정

테넌트별 검색엔진 색인 여부는 기존 `indexable` 설정을 사용한다.

새로운 `is_indexable` 필드를 만들지 않는다.

## indexable = true

검색엔진 색인을 허용한다.

```text
테넌트 대표 홈
→ sitemap 포함 가능

공개 게시판
→ sitemap 포함 가능

공개 게시글
→ sitemap 포함 가능

공개 페이지
→ sitemap 포함 가능
```

각 콘텐츠는 추가적인 공개/발행 조건을 만족해야 한다.

## indexable = false

테넌트는 정상적으로 사용할 수 있지만 검색엔진 색인 대상에서는 제외한다.

```text
사이트 접속
→ 가능

HTML robots
→ noindex,follow

tenant sitemap
→ 제외

platform sitemap
→ 해당 tenant 전체 제외
```

`test` 같은 테스트·개발·샘플 테넌트는 `indexable=false`를 사용한다.

특정 tenant key를 코드에 하드코딩하여 제외하지 않는다.

---

# 4. 테넌트 메인 페이지

테넌트 메인의 검색엔진 대표 URL은 항상 canonical 기준의 루트 URL이다.

Path tenant 예:

```text
https://tagmoa.com/sites/lifetip/
```

Subdomain tenant 예:

```text
https://lifetip.tagmoa.com/
```

Custom domain 예:

```text
https://example.com/
```

테넌트가 `indexable=true`이고 메인이 정상적인 공개 페이지라면 sitemap에 포함한다.

---

# 5. pages/home 처리

`pages` 게시판의 `home` 게시글은 **테넌트 메인을 구성하기 위한 내부 콘텐츠**로 취급한다.

예:

```text
/sites/lifetip/
→ 검색엔진 대표 메인 URL

/sites/lifetip/pages/home
→ 메인 렌더링용 내부 콘텐츠
```

따라서 sitemap 정책은 다음과 같다.

```text
/sites/lifetip/
→ sitemap 포함

/sites/lifetip/pages/home
→ sitemap 제외
```

`pages/home`의 존재 및 공개 상태를 이용해 테넌트 메인의 sitemap 포함 여부를 판단할 수 있지만, `pages/home` 자체를 별도의 sitemap URL로 등록하지 않는다.

외부에서 `/pages/home`에 접근할 수 있더라도 canonical은 테넌트 루트 `/`를 사용한다.

즉:

```text
pages/home
→ 내부 콘텐츠

tenant /
→ 검색엔진 대표 URL
```

---

# 6. 게시판 sitemap 규칙

일반 게시판은 실제 비로그인 사용자가 접근할 수 있는 공개 게시판만 sitemap에 포함한다.

다음과 같은 게시판은 제외한다.

- 비공개 게시판
- 관리자 전용 게시판
- 회원 등 특정 사용자만 접근할 수 있는 게시판
- 검색엔진에 노출해서는 안 되는 게시판
- 실제 접근할 수 없는 게시판

게시판 URL은 해당 테넌트의 canonical 대표 도메인을 사용한다.

---

# 7. 게시글 sitemap 규칙

일반 게시글은 다음 조건을 모두 만족해야 sitemap에 포함한다.

- 소속 tenant의 `indexable=true`
- 공개 게시글
- 발행된 게시글
- 예약 발행이라면 현재 시간이 `published_at` 이후
- 비밀번호 보호 글이 아님
- 외부 사용자가 정상적으로 접근 가능
- 삭제된 글이 아님
- 검색 차단 상태가 아님

예:

```text
공개 + 발행 완료
→ 포함

비공개
→ 제외

미래 예약 발행
→ 제외

비밀번호 보호
→ 제외

삭제
→ 제외
```

---

# 8. TagmoaPost와 tenant sitemap

Tagmoa 플랫폼의 콘텐츠 수집 여부와 테넌트 원본의 검색엔진 색인 여부는 원칙적으로 분리한다.

## TagmoaPost가 없는 경우

```text
원본 tenant 글 정상 공개
+
TagmoaPost 없음

→ tenant sitemap 포함 가능
```

즉 Tagmoa에 수집되지 않았다는 이유만으로 sitemap에서 제외하지 않는다.

## is_active = false

`TagmoaPost.is_active=false`만으로 원본 tenant 글을 sitemap에서 제외하지 않는다.

```text
원본 tenant 글 정상 공개
+
TagmoaPost.is_active=false

→ sitemap 포함 가능
```

플랫폼의 수집 활성 상태와 검색엔진 색인 여부를 동일한 개념으로 사용하지 않는다.

## blocked

현재 정책에서는 `blocked` 상태의 TagmoaPost는 sitemap에서 제외한다.

```text
TagmoaPost blocked
→ sitemap 제외
```

향후 `blocked`의 의미가 플랫폼 내 차단만을 의미하도록 변경된다면 검색엔진 차단 상태와 분리하는 것을 검토한다.

---

# 9. 대표 URL / canonical

동일 콘텐츠에 여러 접근 URL이 존재하더라도 sitemap에는 **canonical 대표 URL 하나만 등록**한다.

대표 URL은 기존 `SiteUrlResolver` 정책을 따른다.

```text
Path tenant
→ https://tagmoa.com/sites/{tenant}/...

Subdomain tenant
→ https://{tenant}.tagmoa.com/...

Custom domain
→ https://custom-domain.com/...
```

동일 콘텐츠에 대해 path/subdomain/custom domain URL을 동시에 sitemap에 등록하지 않는다.

원칙:

```text
sitemap <loc>
=
해당 페이지 canonical URL
```

---

# 10. 중복 콘텐츠 URL

동일하거나 실질적으로 동일한 콘텐츠가 여러 URL로 접근 가능한 경우 sitemap에는 대표 URL만 등록한다.

대표적인 사례:

```text
tenant /
+
pages/home

→ tenant / 만 sitemap 포함
```

향후 테넌트 메인이 특정 게시판 목록을 그대로 재사용하여 다음 두 URL이 실질적으로 동일해지는 경우는 별도 정책으로 판단한다.

```text
/sites/lifetip/
+
/sites/lifetip/blog
```

현재는 일반 게시판 URL을 유지할 수 있으나, 두 페이지가 완전히 동일한 콘텐츠가 되는 경우 canonical 및 sitemap 중복 여부를 다시 검토한다.

---

# 11. noindex와 sitemap

검색엔진에 색인하지 않는 페이지를 sitemap에 포함하지 않는다.

다음 상태가 발생해서는 안 된다.

```text
HTML
<meta name="robots" content="noindex,follow">

+

sitemap
<loc>해당 URL</loc>
```

특히:

```text
tenant indexable=false
→ HTML noindex,follow
→ tenant 전체 sitemap 제외
```

가 함께 동작해야 한다.

robots.txt에서 크롤링 자체를 차단하는 것과 `noindex`는 별개의 개념으로 관리한다.

---

# 12. sitemap cache

사이트맵은 파일 캐시를 사용할 수 있지만, sitemap 포함 여부에 영향을 주는 상태가 변경되면 기존 sitemap cache를 무효화한다.

원칙은:

```text
콘텐츠/설정 변경
→ sitemap cache invalidate
→ 다음 sitemap 요청 또는 기존 생성 과정에서 재생성
```

이며 모든 변경마다 sitemap을 즉시 강제 생성할 필요는 없다.

## cache 무효화 대상

### 테넌트 설정

- `indexable` 변경
- sitemap 포함 여부에 영향을 주는 사이트 설정 변경

### 테넌트 관리

- tenant 생성
- tenant 수정
- tenant 삭제
- 대표 도메인 변경
- canonical 대표 URL에 영향을 주는 도메인 설정 변경

### 게시판

- 생성
- 수정
- 삭제
- 공개/접근 상태 변경
- slug 등 URL 변경

### 게시글

- 생성
- 수정
- 삭제
- 공개 상태 변경
- 발행 상태 변경
- URL 또는 접근 가능성에 영향을 주는 변경

### 플랫폼

플랫폼 자체 게시판·게시글·사이트 설정 변경 시 platform sitemap cache를 무효화한다.

### moderation

검색엔진 sitemap 포함 여부에 영향을 주는 moderation/blocked 상태 변경 시 관련 sitemap cache를 무효화한다.

---

# 13. Path tenant와 platform sitemap

Path tenant는 다음 형태의 URL을 사용한다.

```text
https://tagmoa.com/sites/{tenant}/
```

`indexable=true`인 path tenant의 검색 가능한 URL은 platform sitemap에 포함할 수 있다.

```text
tagmoa.com/sitemap.xml
  ├─ 플랫폼 URL
  ├─ 플랫폼 공개 콘텐츠
  └─ indexable path tenants
       ├─ tenant /
       ├─ 공개 게시판
       └─ 공개 게시글
```

`indexable=false`인 path tenant는 platform sitemap에 어떠한 URL도 포함하지 않는다.

---

# 14. Subdomain / Custom Domain sitemap

Subdomain 또는 Custom Domain이 canonical 대표 도메인인 경우 해당 canonical domain을 기준으로 sitemap을 생성한다.

동일 tenant 콘텐츠를 platform path URL과 canonical domain 양쪽 sitemap에 중복해서 등록하지 않는다.

`SiteUrlResolver`가 판단한 대표 URL 정책을 우선한다.

---

# 15. lastmod

`lastmod`는 가능한 한 실제 콘텐츠 변경 시각을 사용한다.

게시글은 실제 게시글의 수정 시각을 기준으로 한다.

사이트맵을 새로 생성했다는 이유만으로 콘텐츠의 `lastmod`를 현재 시각으로 갱신하지 않는다.

게시판 목록의 `lastmod`를 하위 게시글 변경 시각까지 정확하게 반영하는 문제는 별도 개선 사항으로 관리할 수 있다.

---

# 16. priority / changefreq

`priority`와 `changefreq`는 sitemap 구조상 사용할 수 있지만 핵심적인 색인 판단 기준으로 사용하지 않는다.

중요도는 다음 순서로 본다.

```text
1. indexable 정확성
2. 실제 공개 여부
3. canonical 정확성
4. sitemap 중복 방지
5. lastmod 정확성
6. priority / changefreq
```

---

# 17. sitemap 구현 책임

현재 사이트맵 아키텍처를 기본 구조로 유지한다.

주요 역할은 다음과 같다.

```text
GenerateSiteSitemap
→ 공통 sitemap URL/콘텐츠 생성

GenerateTenantSitemap
→ tenant sitemap 생성

GeneratePlatformSitemap
→ platform + path tenant sitemap 생성

SiteUrlResolver
→ canonical 대표 URL 결정
```

현재 단계에서는 불필요한 SEO policy 계층이나 새로운 indexability 시스템을 추가하지 않는다.

공통 정책의 중복이 실제 유지보수 문제로 커질 경우 향후 별도의 정책 객체 도입을 검토한다.

---

# 18. 회귀 테스트 필수 조건

사이트맵 관련 변경 시 최소한 다음 정책이 깨지지 않는지 확인한다.

```text
indexable=true tenant
→ 정상 공개 URL sitemap 포함

indexable=false tenant
→ platform/tenant sitemap 제외

pages/home
→ sitemap 제외

tenant /
→ 대표 홈 URL로 포함

공개 게시글
→ 포함

비공개 게시글
→ 제외

미래 예약 게시글
→ 제외

비밀번호 보호 게시글
→ 제외

Tagmoa 미수집 공개 글
→ 포함

Tagmoa inactive 공개 글
→ 포함

Tagmoa blocked 글
→ 제외
```

가능하면 generator 결과뿐 아니라 **최종 생성 XML에서도 검증**한다.

---

# 19. 현재 확정된 핵심 정책

```text
[테넌트]

indexable=false
→ 사이트 사용 가능
→ noindex,follow
→ sitemap 전체 제외


[홈]

pages/home
→ 내부 렌더링 콘텐츠
→ 개별 sitemap 제외
→ canonical은 tenant /

tenant /
→ 검색엔진 대표 홈
→ sitemap 포함


[게시글]

공개 + 발행 + 접근 가능
→ sitemap 포함

비공개 / 미래 예약 / 비밀번호 / 삭제
→ sitemap 제외


[Tagmoa]

미수집
→ sitemap 영향 없음

is_active=false
→ sitemap 영향 없음

blocked
→ sitemap 제외


[URL]

canonical 대표 URL
→ sitemap <loc>

비대표/중복 URL
→ sitemap 제외


[캐시]

sitemap에 영향을 주는 상태 변경
→ 관련 tenant/platform sitemap cache 즉시 무효화
```

---

# 20. 향후 검토 사항

현재 P0 정책에는 포함하지 않으며 필요할 때 별도로 검토한다.

1. 테넌트 메인이 특정 게시판 목록과 완전히 동일할 경우 `/`와 `/{board}`의 sitemap 중복 정책
2. `blocked`를 플랫폼 차단과 검색엔진 차단으로 분리할 필요가 있는지
3. 게시판 목록 `lastmod`에 하위 게시글 변경 시각을 정확하게 반영할지
4. canonical / noindex / sitemap 판단 중복이 커질 경우 공통 SEO visibility policy 도입 여부

현재는 위 사항 때문에 기존 사이트맵 구조를 추가로 리팩터링하지 않는다.
