Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 18 additions & 15 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,21 +115,24 @@ function MyPage() {

### ReactHeadSafeProps

| Prop | Type | Description |
| ---------------- | -------- | ------------------------------------------------------------------------ |
| `title` | `string` | `document.title`에 설정될 페이지 제목 |
| `description` | `string` | SEO를 위한 메타 설명 태그 콘텐츠 |
| `keywords` | `string` | SEO를 위한 메타 키워드 태그 콘텐츠 |
| `ogTitle` | `string` | 소셜 미디어 공유를 위한 Open Graph 제목 (og:title) |
| `ogDescription` | `string` | 소셜 미디어 공유를 위한 Open Graph 설명 (og:description) |
| `ogImage` | `string` | 소셜 미디어 공유를 위한 Open Graph 이미지 URL (og:image) |
| `ogUrl` | `string` | 소셜 미디어 공유를 위한 Open Graph URL (og:url) |
| `ogType` | `OgType` | Open Graph 타입 (og:type). 표준 12개 값 자동완성 + 임의 string 모두 허용 |
| `canonicalUrl` | `string` | SEO를 위한 페이지의 대표 URL (`<link rel="canonical">`) |
| `ogSiteName` | `string` | 소셜 미디어 공유를 위한 사이트 이름 (og:site_name) |
| `ogLocale` | `string` | 콘텐츠의 언어/지역 코드 (og:locale), 예: `"ko_KR"`, `"en_US"` |
| `twitterSite` | `string` | 웹사이트의 Twitter 계정 (twitter:site), 예: `"@mysite"` |
| `twitterCreator` | `string` | 콘텐츠 작성자의 Twitter 계정 (twitter:creator), 예: `"@author"` |
| Prop | Type | Description |
| ---------------- | -------- | -------------------------------------------------------------------------------------------- |
| `title` | `string` | `document.title`에 설정될 페이지 제목 |
| `description` | `string` | SEO를 위한 메타 설명 태그 콘텐츠 |
| `keywords` | `string` | SEO를 위한 메타 키워드 태그 콘텐츠 |
| `ogTitle` | `string` | 소셜 미디어 공유를 위한 Open Graph 제목 (og:title) |
| `ogDescription` | `string` | 소셜 미디어 공유를 위한 Open Graph 설명 (og:description) |
| `ogImage` | `string` | 소셜 미디어 공유를 위한 Open Graph 이미지 URL (og:image) |
| `ogUrl` | `string` | 소셜 미디어 공유를 위한 Open Graph URL (og:url) |
| `ogType` | `OgType` | Open Graph 타입 (og:type). 표준 12개 값 자동완성 + 임의 string 모두 허용 |
| `canonicalUrl` | `string` | SEO를 위한 페이지의 대표 URL (`<link rel="canonical">`) |
| `ogSiteName` | `string` | 소셜 미디어 공유를 위한 사이트 이름 (og:site_name) |
| `ogLocale` | `string` | 콘텐츠의 언어/지역 코드 (og:locale), 예: `"ko_KR"`, `"en_US"` |
| `twitterSite` | `string` | 웹사이트의 Twitter 계정 (twitter:site), 예: `"@mysite"` |
| `twitterCreator` | `string` | 콘텐츠 작성자의 Twitter 계정 (twitter:creator), 예: `"@author"` |
| `robots` | `string` | 크롤러 색인을 제어하는 robots 메타 태그 콘텐츠, 예: `"noindex,follow"`, `"noindex,nofollow"` |

> **주의:** JS로 주입된 `robots="noindex"`는 JavaScript를 렌더링하는 크롤러(예: Googlebot, 단 지연 있음)만 인식합니다. JS를 실행하지 않는 크롤러(상당수의 봇, 소셜 미디어 프리뷰 봇 등)는 이 태그를 무시하므로 페이지가 여전히 색인될 수 있습니다. 반드시 확실하게 색인에서 제외해야 하는 페이지라면, 서버 측 `X-Robots-Tag: noindex` 헤더를 사용하세요.

### Twitter Card 지원

Expand Down
33 changes: 18 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,21 +118,24 @@ That's it! The component will automatically:

### ReactHeadSafeProps

| Prop | Type | Description |
| ---------------- | -------- | ------------------------------------------------------------------------------------------------ |
| `title` | `string` | The page title that will be set in the `document.title` |
| `description` | `string` | The meta description tag content for SEO |
| `keywords` | `string` | The meta keywords tag content for SEO |
| `ogTitle` | `string` | The Open Graph title (og:title) for social media sharing |
| `ogDescription` | `string` | The Open Graph description (og:description) for social media sharing |
| `ogImage` | `string` | The Open Graph image URL (og:image) for social media sharing |
| `ogUrl` | `string` | The canonical URL of your object that will be used as its permanent ID in the graph (og:url) |
| `ogType` | `OgType` | The type of your object (og:type). Autocompletes 12 standard OG values; also accepts any string. |
| `canonicalUrl` | `string` | The canonical URL of the page for SEO (`<link rel="canonical">`) |
| `ogSiteName` | `string` | The site name for social media sharing (og:site_name) |
| `ogLocale` | `string` | The locale of the content (og:locale), e.g. `"en_US"`, `"ko_KR"` |
| `twitterSite` | `string` | The Twitter @username of the website (twitter:site), e.g. `"@mysite"` |
| `twitterCreator` | `string` | The Twitter @username of the content author (twitter:creator), e.g. `"@author"` |
| Prop | Type | Description |
| ---------------- | -------- | ------------------------------------------------------------------------------------------------------- |
| `title` | `string` | The page title that will be set in the `document.title` |
| `description` | `string` | The meta description tag content for SEO |
| `keywords` | `string` | The meta keywords tag content for SEO |
| `ogTitle` | `string` | The Open Graph title (og:title) for social media sharing |
| `ogDescription` | `string` | The Open Graph description (og:description) for social media sharing |
| `ogImage` | `string` | The Open Graph image URL (og:image) for social media sharing |
| `ogUrl` | `string` | The canonical URL of your object that will be used as its permanent ID in the graph (og:url) |
| `ogType` | `OgType` | The type of your object (og:type). Autocompletes 12 standard OG values; also accepts any string. |
| `canonicalUrl` | `string` | The canonical URL of the page for SEO (`<link rel="canonical">`) |
| `ogSiteName` | `string` | The site name for social media sharing (og:site_name) |
| `ogLocale` | `string` | The locale of the content (og:locale), e.g. `"en_US"`, `"ko_KR"` |
| `twitterSite` | `string` | The Twitter @username of the website (twitter:site), e.g. `"@mysite"` |
| `twitterCreator` | `string` | The Twitter @username of the content author (twitter:creator), e.g. `"@author"` |
| `robots` | `string` | The robots meta tag content controlling crawler indexing, e.g. `"noindex,follow"`, `"noindex,nofollow"` |

> **Warning:** A JS-injected `robots="noindex"` is only honored by crawlers that render JavaScript (e.g. Googlebot, with delay). Non-JS crawlers (many bots, social preview crawlers) ignore it, so the page may still be indexed. For pages that must be reliably excluded from indexing, use a server-side `X-Robots-Tag: noindex` header instead.

### Twitter Card Support

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-head-safe",
"version": "1.9.0",
"version": "1.10.0",
"description": "A lightweight React head manager for CSR apps. Safely manage document title, meta tags, Open Graph, and SEO metadata without duplicates. TypeScript support included.",
"author": "umsungjun",
"license": "MIT",
Expand Down
8 changes: 8 additions & 0 deletions src/ReactHeadSafe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const ReactHeadSafe: FC<ReactHeadSafeProps> = ({
ogLocale,
twitterSite,
twitterCreator,
robots,
}) => {
useLayoutEffect(() => {
// Track selectors for meta/link tags inserted in this effect run.
Expand Down Expand Up @@ -125,6 +126,12 @@ export const ReactHeadSafe: FC<ReactHeadSafeProps> = ({
insertedSelectors.push('meta[name="twitter:creator"]');
}

// Update robots meta tag (controls crawler indexing, e.g. "noindex,follow")
if (robots !== undefined) {
updateMetaTag('name', 'robots', robots);
insertedSelectors.push('meta[name="robots"]');
}

// Cleanup: remove every tag this effect inserted.
// - On deps change: runs before the next effect, clearing stale tags
// (handles prop → undefined transitions).
Expand All @@ -148,6 +155,7 @@ export const ReactHeadSafe: FC<ReactHeadSafeProps> = ({
ogLocale,
twitterSite,
twitterCreator,
robots,
]);

return null;
Expand Down
45 changes: 45 additions & 0 deletions src/test/ReactHeadSafe.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,51 @@ describe('ReactHeadSafe', () => {
});
});

describe('robots meta tag', () => {
it('should create robots meta tag', () => {
render(<ReactHeadSafe robots="noindex,follow" />);

const metaTag = document.querySelector('meta[name="robots"]');
expect(metaTag).toBeInTheDocument();
expect(metaTag?.getAttribute('content')).toBe('noindex,follow');
});

it('should update robots meta tag when prop changes', () => {
const { rerender } = render(<ReactHeadSafe robots="noindex,follow" />);
rerender(<ReactHeadSafe robots="noindex,nofollow" />);

const metaTag = document.querySelector('meta[name="robots"]');
expect(metaTag?.getAttribute('content')).toBe('noindex,nofollow');
});

it('should prevent duplicate robots meta tags', () => {
const { rerender } = render(<ReactHeadSafe robots="noindex,follow" />);
rerender(<ReactHeadSafe robots="noindex,nofollow" />);

const metaTags = document.querySelectorAll('meta[name="robots"]');
expect(metaTags).toHaveLength(1);
expect(metaTags[0].getAttribute('content')).toBe('noindex,nofollow');
});

it('should remove robots meta tag on unmount', () => {
const { unmount } = render(<ReactHeadSafe robots="noindex,follow" />);
unmount();

expect(
document.querySelector('meta[name="robots"]')
).not.toBeInTheDocument();
});

it('should remove robots meta tag when prop becomes undefined', () => {
const { rerender } = render(<ReactHeadSafe robots="noindex,follow" />);
rerender(<ReactHeadSafe />);

expect(
document.querySelector('meta[name="robots"]')
).not.toBeInTheDocument();
});
});

describe('cleanup on unmount', () => {
it('should remove all inserted meta and link tags on unmount', () => {
const { unmount } = render(
Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,12 @@ export interface ReactHeadSafeProps {
twitterSite?: string;
/** The Twitter @username of the content author (twitter:creator), e.g. "@author" */
twitterCreator?: string;
/**
* The robots meta tag content controlling crawler indexing, e.g. "noindex,follow", "noindex,nofollow".
*
* Note: a JS-injected `noindex` is only honored by crawlers that render JavaScript
* (e.g. Googlebot, with delay). Non-JS crawlers ignore it. For pages that must be
* reliably excluded, use a server-side `X-Robots-Tag: noindex` header instead.
*/
robots?: string;
}
Loading