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
12 changes: 12 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ React에서 document head 요소를 관리하기 위한 가볍고 CSR에 최적
- ✅ **경량** - React 외에 의존성 제로
- ✅ **CSR 최적화** - 동기적 DOM 업데이트를 위해 `useLayoutEffect` 사용
- ✅ **Open Graph 지원** - 소셜 미디어 메타 태그 기본 지원
- ✅ **Twitter Card 지원** - Open Graph 태그 설정 시 Twitter 태그 자동 생성
- ✅ **간단한 API** - props만 전달하면 되는 복잡하지 않은 설정

## 설치
Expand Down Expand Up @@ -73,6 +74,7 @@ function MyPage() {
- document title 설정
- meta description과 keywords 추가/업데이트
- 소셜 미디어용 Open Graph 태그 추가/업데이트
- Twitter Card 태그 추가/업데이트 (Open Graph 태그에서 자동 생성)
- 중복 태그 제거

## API 레퍼런스
Expand All @@ -90,6 +92,16 @@ function MyPage() {
| `ogUrl` | `string` | 소셜 미디어 공유를 위한 Open Graph URL (og:url) |
| `ogType` | `string` | 소셜 미디어 공유를 위한 Open Graph 타입, 예: "website", "article" (og:type) |

### Twitter Card 지원

Open Graph 태그를 설정하면 해당하는 Twitter Card 태그가 자동으로 생성됩니다:

| Open Graph Prop | 생성되는 Twitter 태그 |
| --------------- | --------------------- |
| `ogTitle` | `twitter:title` |
| `ogDescription` | `twitter:description` |
| `ogImage` | `twitter:image` + `twitter:card` (summary_large_image) |

## 로컬 개발

예제 애플리케이션으로 로컬 변경사항을 테스트하려면:
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ A lightweight, CSR-focused alternative for managing document head elements in Re
- ✅ **Lightweight** - Zero dependencies except React
- ✅ **CSR Optimized** - Uses `useLayoutEffect` for synchronous DOM updates
- ✅ **Open Graph Support** - Built-in support for social media meta tags
- ✅ **Twitter Card Support** - Automatically sets Twitter tags when Open Graph tags are provided
- ✅ **Simple API** - Just pass props, no complex configuration

## Installation
Expand Down Expand Up @@ -73,6 +74,7 @@ That's it! The component will automatically:
- Set the document title
- Add/update meta description and keywords
- Add/update Open Graph tags for social media
- Add/update Twitter Card tags (automatically generated from Open Graph tags)
- Remove any duplicate tags

## API Reference
Expand All @@ -90,6 +92,16 @@ That's it! The component will automatically:
| `ogUrl` | `string` | The canonical URL of your object that will be used as its permanent ID in the graph (og:url) |
| `ogType` | `string` | The type of your object, e.g., "website", "article" (og:type) |

### Twitter Card Support

When you set Open Graph tags, the corresponding Twitter Card tags are automatically generated:

| Open Graph Prop | Twitter Tag Generated |
| --------------- | --------------------- |
| `ogTitle` | `twitter:title` |
| `ogDescription` | `twitter:description` |
| `ogImage` | `twitter:image` + `twitter:card` (summary_large_image) |

## Local Development

To test your local changes with the example application:
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.2.0",
"version": "1.3.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
6 changes: 5 additions & 1 deletion src/ReactHeadSafe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,21 @@ export const ReactHeadSafe: FC<ReactHeadSafeProps> = ({
updateMetaTag('name', 'keywords', keywords);
}

// Update Open Graph tags
// Update Open Graph tags and Twitter tags
if (ogTitle !== undefined) {
updateMetaTag('property', 'og:title', ogTitle);
updateMetaTag('name', 'twitter:title', ogTitle);
}

if (ogDescription !== undefined) {
updateMetaTag('property', 'og:description', ogDescription);
updateMetaTag('name', 'twitter:description', ogDescription);
}

if (ogImage !== undefined) {
updateMetaTag('property', 'og:image', ogImage);
updateMetaTag('name', 'twitter:image', ogImage);
updateMetaTag('name', 'twitter:card', 'summary_large_image');
}

if (ogUrl !== undefined) {
Expand Down
137 changes: 137 additions & 0 deletions src/test/ReactHeadSafe.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,143 @@ describe('ReactHeadSafe', () => {
});
});

describe('Twitter tags', () => {
it('should create twitter:title meta tag when ogTitle is set', () => {
render(<ReactHeadSafe ogTitle="Twitter Test Title" />);

const metaTag = document.querySelector('meta[name="twitter:title"]');
expect(metaTag).toBeInTheDocument();
expect(metaTag?.getAttribute('content')).toBe('Twitter Test Title');
});

it('should create twitter:description meta tag when ogDescription is set', () => {
render(<ReactHeadSafe ogDescription="Twitter Test Description" />);

const metaTag = document.querySelector(
'meta[name="twitter:description"]'
);
expect(metaTag).toBeInTheDocument();
expect(metaTag?.getAttribute('content')).toBe('Twitter Test Description');
});

it('should create twitter:image meta tag when ogImage is set', () => {
render(<ReactHeadSafe ogImage="https://example.com/twitter-image.jpg" />);

const metaTag = document.querySelector('meta[name="twitter:image"]');
expect(metaTag).toBeInTheDocument();
expect(metaTag?.getAttribute('content')).toBe(
'https://example.com/twitter-image.jpg'
);
});

it('should create twitter:card meta tag with summary_large_image when ogImage is set', () => {
render(<ReactHeadSafe ogImage="https://example.com/image.jpg" />);

const metaTag = document.querySelector('meta[name="twitter:card"]');
expect(metaTag).toBeInTheDocument();
expect(metaTag?.getAttribute('content')).toBe('summary_large_image');
});

it('should prevent duplicate twitter:title meta tags', () => {
const { rerender } = render(
<ReactHeadSafe ogTitle="First Twitter Title" />
);
rerender(<ReactHeadSafe ogTitle="Second Twitter Title" />);

const metaTags = document.querySelectorAll('meta[name="twitter:title"]');
expect(metaTags).toHaveLength(1);
expect(metaTags[0].getAttribute('content')).toBe('Second Twitter Title');
});

it('should prevent duplicate twitter:description meta tags', () => {
const { rerender } = render(
<ReactHeadSafe ogDescription="First Twitter Description" />
);
rerender(<ReactHeadSafe ogDescription="Second Twitter Description" />);

const metaTags = document.querySelectorAll(
'meta[name="twitter:description"]'
);
expect(metaTags).toHaveLength(1);
expect(metaTags[0].getAttribute('content')).toBe(
'Second Twitter Description'
);
});

it('should prevent duplicate twitter:image meta tags', () => {
const { rerender } = render(
<ReactHeadSafe ogImage="https://example.com/first.jpg" />
);
rerender(<ReactHeadSafe ogImage="https://example.com/second.jpg" />);

const metaTags = document.querySelectorAll('meta[name="twitter:image"]');
expect(metaTags).toHaveLength(1);
expect(metaTags[0].getAttribute('content')).toBe(
'https://example.com/second.jpg'
);
});

it('should prevent duplicate twitter:card meta tags', () => {
const { rerender } = render(
<ReactHeadSafe ogImage="https://example.com/first.jpg" />
);
rerender(<ReactHeadSafe ogImage="https://example.com/second.jpg" />);

const metaTags = document.querySelectorAll('meta[name="twitter:card"]');
expect(metaTags).toHaveLength(1);
expect(metaTags[0].getAttribute('content')).toBe('summary_large_image');
});

it('should set both og and twitter tags together', () => {
render(
<ReactHeadSafe
ogTitle="Shared Title"
ogDescription="Shared Description"
ogImage="https://example.com/shared.jpg"
/>
);

// OG tags
expect(
document
.querySelector('meta[property="og:title"]')
?.getAttribute('content')
).toBe('Shared Title');
expect(
document
.querySelector('meta[property="og:description"]')
?.getAttribute('content')
).toBe('Shared Description');
expect(
document
.querySelector('meta[property="og:image"]')
?.getAttribute('content')
).toBe('https://example.com/shared.jpg');

// Twitter tags
expect(
document
.querySelector('meta[name="twitter:title"]')
?.getAttribute('content')
).toBe('Shared Title');
expect(
document
.querySelector('meta[name="twitter:description"]')
?.getAttribute('content')
).toBe('Shared Description');
expect(
document
.querySelector('meta[name="twitter:image"]')
?.getAttribute('content')
).toBe('https://example.com/shared.jpg');
expect(
document
.querySelector('meta[name="twitter:card"]')
?.getAttribute('content')
).toBe('summary_large_image');
});
});

describe('multiple props', () => {
it('should handle all props together', () => {
render(
Expand Down