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
20 changes: 12 additions & 8 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ function MyPage() {
ogTitle="My Page Title for Social Media"
ogDescription="This is the description for social media."
ogImage="https://example.com/image.jpg"
ogUrl="https://example.com/page"
ogType="website"
/>
<div>Your page content...</div>
</>
Expand All @@ -77,14 +79,16 @@ 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) |
| 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` | `string` | 소셜 미디어 공유를 위한 Open Graph 타입, 예: "website", "article" (og:type) |

## 로컬 개발

Expand Down
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ function MyPage() {
ogTitle="My Page Title for Social Media"
ogDescription="This is the description for social media."
ogImage="https://example.com/image.jpg"
ogUrl="https://example.com/page"
ogType="website"
/>
<div>Your page content...</div>
</>
Expand All @@ -77,14 +79,16 @@ 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 |
| 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` | `string` | The type of your object, e.g., "website", "article" (og:type) |

## Local Development

Expand Down
2 changes: 2 additions & 0 deletions examples/basic/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
content="(main) A CSR-only React head manager that prevents duplicate meta tags."
/>
<meta property="og:image" content="/logo.png" />
<meta property="og:url" content="https://react-head-safe.vercel.app/" />
<meta property="og:type" content="website" />
</head>
<body>
<div id="root"></div>
Expand Down
2 changes: 2 additions & 0 deletions examples/basic/src/pages/About.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export default function About() {
ogTitle="About - React Head Safe Demo"
ogDescription="Learn more about React Head Safe and how it solves the duplicate meta tag problem in CSR applications."
ogImage={`${window.location.origin}/logo.png`}
ogUrl={window.location.href}
ogType="website"
/>

<div className="page-container">
Expand Down
2 changes: 2 additions & 0 deletions examples/basic/src/pages/Contact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export default function Contact() {
ogTitle="Contact - React Head Safe Demo"
ogDescription="Get in touch with the React Head Safe team. Report issues, suggest features, or contribute to the project."
ogImage={`${window.location.origin}/logo.png`}
ogUrl={window.location.href}
ogType="website"
/>

<div className="page-container">
Expand Down
2 changes: 2 additions & 0 deletions examples/basic/src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export default function Home() {
ogTitle="Home - React Head Safe Demo"
ogDescription="Welcome to React Head Safe - A CSR-only React head manager that prevents duplicate meta tags."
ogImage={`${window.location.origin}/logo.png`}
ogUrl={window.location.href}
ogType="website"
/>

<div className="page-container">
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.0.2",
"version": "1.2.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
23 changes: 22 additions & 1 deletion src/ReactHeadSafe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { type ReactHeadSafeProps } from './types';
* ogTitle="My Page Title for Social Media"
* ogDescription="This is the description for social media."
* ogImage="https://example.com/image.jpg"
* ogUrl="https://example.com/page"
* ogType="website"
* />
*/
export const ReactHeadSafe: FC<ReactHeadSafeProps> = ({
Expand All @@ -23,6 +25,8 @@ export const ReactHeadSafe: FC<ReactHeadSafeProps> = ({
ogTitle,
ogDescription,
ogImage,
ogUrl,
ogType,
}) => {
useLayoutEffect(() => {
// Update title
Expand Down Expand Up @@ -52,7 +56,24 @@ export const ReactHeadSafe: FC<ReactHeadSafeProps> = ({
if (ogImage !== undefined) {
updateMetaTag('property', 'og:image', ogImage);
}
}, [title, description, keywords, ogTitle, ogDescription, ogImage]);

if (ogUrl !== undefined) {
updateMetaTag('property', 'og:url', ogUrl);
}

if (ogType !== undefined) {
updateMetaTag('property', 'og:type', ogType);
}
}, [
title,
description,
keywords,
ogTitle,
ogDescription,
ogImage,
ogUrl,
ogType,
]);

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

it('should create og:url meta tag', () => {
render(<ReactHeadSafe ogUrl="https://example.com/page" />);

const metaTag = document.querySelector('meta[property="og:url"]');
expect(metaTag).toBeInTheDocument();
expect(metaTag?.getAttribute('content')).toBe('https://example.com/page');
});

it('should create og:type meta tag', () => {
render(<ReactHeadSafe ogType="website" />);

const metaTag = document.querySelector('meta[property="og:type"]');
expect(metaTag).toBeInTheDocument();
expect(metaTag?.getAttribute('content')).toBe('website');
});

it('should prevent duplicate og:title meta tags', () => {
const { rerender } = render(<ReactHeadSafe ogTitle="First OG Title" />);
rerender(<ReactHeadSafe ogTitle="Second OG Title" />);
Expand Down Expand Up @@ -160,6 +176,24 @@ describe('ReactHeadSafe', () => {
'https://example.com/second.jpg'
);
});

it('should prevent duplicate og:url meta tags', () => {
const { rerender } = render(<ReactHeadSafe ogUrl="https://site.com/1" />);
rerender(<ReactHeadSafe ogUrl="https://site.com/2" />);

const metaTags = document.querySelectorAll('meta[property="og:url"]');
expect(metaTags).toHaveLength(1);
expect(metaTags[0].getAttribute('content')).toBe('https://site.com/2');
});

it('should prevent duplicate og:type meta tags', () => {
const { rerender } = render(<ReactHeadSafe ogType="website" />);
rerender(<ReactHeadSafe ogType="website" />);

const metaTags = document.querySelectorAll('meta[property="og:type"]');
expect(metaTags).toHaveLength(1);
expect(metaTags[0].getAttribute('content')).toBe('website');
});
});

describe('multiple props', () => {
Expand All @@ -172,6 +206,8 @@ describe('ReactHeadSafe', () => {
ogTitle="OG Title"
ogDescription="OG Description"
ogImage="https://example.com/image.jpg"
ogUrl="https://example.com/page"
ogType="website"
/>
);

Expand All @@ -181,54 +217,45 @@ describe('ReactHeadSafe', () => {
.querySelector('meta[name="description"]')
?.getAttribute('content')
).toBe('Test Description');
expect(
document.querySelector('meta[name="keywords"]')?.getAttribute('content')
).toBe('test, keywords');
expect(
document
.querySelector('meta[property="og:title"]')
?.getAttribute('content')
).toBe('OG Title');
expect(
document
.querySelector('meta[property="og:description"]')
.querySelector('meta[property="og:url"]')
?.getAttribute('content')
).toBe('OG Description');
).toBe('https://example.com/page');
expect(
document
.querySelector('meta[property="og:image"]')
.querySelector('meta[property="og:type"]')
?.getAttribute('content')
).toBe('https://example.com/image.jpg');
).toBe('website');
});

it('should update only changed props', () => {
const { rerender } = render(
<ReactHeadSafe
title="Initial Title"
description="Initial Description"
/>
<ReactHeadSafe title="Initial Title" ogUrl="https://example.com/1" />
);

expect(document.title).toBe('Initial Title');
expect(
document
.querySelector('meta[name="description"]')
.querySelector('meta[property="og:url"]')
?.getAttribute('content')
).toBe('Initial Description');
).toBe('https://example.com/1');

rerender(
<ReactHeadSafe
title="Updated Title"
description="Initial Description"
/>
<ReactHeadSafe title="Updated Title" ogUrl="https://example.com/1" />
);

expect(document.title).toBe('Updated Title');
expect(
document
.querySelector('meta[name="description"]')
.querySelector('meta[property="og:url"]')
?.getAttribute('content')
).toBe('Initial Description');
).toBe('https://example.com/1');
});
});

Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ export interface ReactHeadSafeProps {
ogDescription?: string;
/** The Open Graph image URL (og:image) for social media sharing */
ogImage?: string;
/** The canonical URL of your object that will be used as its permanent ID in the graph (og:url) */
ogUrl?: string;
/** The type of your object, e.g., "website", "article" (og:type) */
ogType?: string;
}