Skip to content

Commit 6649c20

Browse files
authored
Merge pull request #9 from umsungjun/feat/add-og-tags
feat: implement og:url and og:type with Vitest test cases and demo update (#8)
2 parents b229929 + 33ed3f1 commit 6649c20

10 files changed

Lines changed: 105 additions & 37 deletions

File tree

README.ko.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ function MyPage() {
5959
ogTitle="My Page Title for Social Media"
6060
ogDescription="This is the description for social media."
6161
ogImage="https://example.com/image.jpg"
62+
ogUrl="https://example.com/page"
63+
ogType="website"
6264
/>
6365
<div>Your page content...</div>
6466
</>
@@ -77,14 +79,16 @@ function MyPage() {
7779

7880
### ReactHeadSafeProps
7981

80-
| Prop | Type | Description |
81-
| --------------- | -------- | -------------------------------------------------------- |
82-
| `title` | `string` | `document.title`에 설정될 페이지 제목 |
83-
| `description` | `string` | SEO를 위한 메타 설명 태그 콘텐츠 |
84-
| `keywords` | `string` | SEO를 위한 메타 키워드 태그 콘텐츠 |
85-
| `ogTitle` | `string` | 소셜 미디어 공유를 위한 Open Graph 제목 (og:title) |
86-
| `ogDescription` | `string` | 소셜 미디어 공유를 위한 Open Graph 설명 (og:description) |
87-
| `ogImage` | `string` | 소셜 미디어 공유를 위한 Open Graph 이미지 URL (og:image) |
82+
| Prop | Type | Description |
83+
| --------------- | -------- | --------------------------------------------------------------------------- |
84+
| `title` | `string` | `document.title`에 설정될 페이지 제목 |
85+
| `description` | `string` | SEO를 위한 메타 설명 태그 콘텐츠 |
86+
| `keywords` | `string` | SEO를 위한 메타 키워드 태그 콘텐츠 |
87+
| `ogTitle` | `string` | 소셜 미디어 공유를 위한 Open Graph 제목 (og:title) |
88+
| `ogDescription` | `string` | 소셜 미디어 공유를 위한 Open Graph 설명 (og:description) |
89+
| `ogImage` | `string` | 소셜 미디어 공유를 위한 Open Graph 이미지 URL (og:image) |
90+
| `ogUrl` | `string` | 소셜 미디어 공유를 위한 Open Graph URL (og:url) |
91+
| `ogType` | `string` | 소셜 미디어 공유를 위한 Open Graph 타입, 예: "website", "article" (og:type) |
8892

8993
## 로컬 개발
9094

README.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ function MyPage() {
5959
ogTitle="My Page Title for Social Media"
6060
ogDescription="This is the description for social media."
6161
ogImage="https://example.com/image.jpg"
62+
ogUrl="https://example.com/page"
63+
ogType="website"
6264
/>
6365
<div>Your page content...</div>
6466
</>
@@ -77,14 +79,16 @@ That's it! The component will automatically:
7779

7880
### ReactHeadSafeProps
7981

80-
| Prop | Type | Description |
81-
| --------------- | -------- | -------------------------------------------------------------------- |
82-
| `title` | `string` | The page title that will be set in the `document.title` |
83-
| `description` | `string` | The meta description tag content for SEO |
84-
| `keywords` | `string` | The meta keywords tag content for SEO |
85-
| `ogTitle` | `string` | The Open Graph title (og:title) for social media sharing |
86-
| `ogDescription` | `string` | The Open Graph description (og:description) for social media sharing |
87-
| `ogImage` | `string` | The Open Graph image URL (og:image) for social media sharing |
82+
| Prop | Type | Description |
83+
| --------------- | -------- | -------------------------------------------------------------------------------------------- |
84+
| `title` | `string` | The page title that will be set in the `document.title` |
85+
| `description` | `string` | The meta description tag content for SEO |
86+
| `keywords` | `string` | The meta keywords tag content for SEO |
87+
| `ogTitle` | `string` | The Open Graph title (og:title) for social media sharing |
88+
| `ogDescription` | `string` | The Open Graph description (og:description) for social media sharing |
89+
| `ogImage` | `string` | The Open Graph image URL (og:image) for social media sharing |
90+
| `ogUrl` | `string` | The canonical URL of your object that will be used as its permanent ID in the graph (og:url) |
91+
| `ogType` | `string` | The type of your object, e.g., "website", "article" (og:type) |
8892

8993
## Local Development
9094

examples/basic/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
content="(main) A CSR-only React head manager that prevents duplicate meta tags."
1818
/>
1919
<meta property="og:image" content="/logo.png" />
20+
<meta property="og:url" content="https://react-head-safe.vercel.app/" />
21+
<meta property="og:type" content="website" />
2022
</head>
2123
<body>
2224
<div id="root"></div>

examples/basic/src/pages/About.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export default function About() {
1010
ogTitle="About - React Head Safe Demo"
1111
ogDescription="Learn more about React Head Safe and how it solves the duplicate meta tag problem in CSR applications."
1212
ogImage={`${window.location.origin}/logo.png`}
13+
ogUrl={window.location.href}
14+
ogType="website"
1315
/>
1416

1517
<div className="page-container">

examples/basic/src/pages/Contact.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export default function Contact() {
1010
ogTitle="Contact - React Head Safe Demo"
1111
ogDescription="Get in touch with the React Head Safe team. Report issues, suggest features, or contribute to the project."
1212
ogImage={`${window.location.origin}/logo.png`}
13+
ogUrl={window.location.href}
14+
ogType="website"
1315
/>
1416

1517
<div className="page-container">

examples/basic/src/pages/Home.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export default function Home() {
1010
ogTitle="Home - React Head Safe Demo"
1111
ogDescription="Welcome to React Head Safe - A CSR-only React head manager that prevents duplicate meta tags."
1212
ogImage={`${window.location.origin}/logo.png`}
13+
ogUrl={window.location.href}
14+
ogType="website"
1315
/>
1416

1517
<div className="page-container">

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-head-safe",
3-
"version": "1.0.2",
3+
"version": "1.2.0",
44
"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.",
55
"author": "umsungjun",
66
"license": "MIT",

src/ReactHeadSafe.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { type ReactHeadSafeProps } from './types';
1414
* ogTitle="My Page Title for Social Media"
1515
* ogDescription="This is the description for social media."
1616
* ogImage="https://example.com/image.jpg"
17+
* ogUrl="https://example.com/page"
18+
* ogType="website"
1719
* />
1820
*/
1921
export const ReactHeadSafe: FC<ReactHeadSafeProps> = ({
@@ -23,6 +25,8 @@ export const ReactHeadSafe: FC<ReactHeadSafeProps> = ({
2325
ogTitle,
2426
ogDescription,
2527
ogImage,
28+
ogUrl,
29+
ogType,
2630
}) => {
2731
useLayoutEffect(() => {
2832
// Update title
@@ -52,7 +56,24 @@ export const ReactHeadSafe: FC<ReactHeadSafeProps> = ({
5256
if (ogImage !== undefined) {
5357
updateMetaTag('property', 'og:image', ogImage);
5458
}
55-
}, [title, description, keywords, ogTitle, ogDescription, ogImage]);
59+
60+
if (ogUrl !== undefined) {
61+
updateMetaTag('property', 'og:url', ogUrl);
62+
}
63+
64+
if (ogType !== undefined) {
65+
updateMetaTag('property', 'og:type', ogType);
66+
}
67+
}, [
68+
title,
69+
description,
70+
keywords,
71+
ogTitle,
72+
ogDescription,
73+
ogImage,
74+
ogUrl,
75+
ogType,
76+
]);
5677

5778
return null;
5879
};

src/test/ReactHeadSafe.test.tsx

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,22 @@ describe('ReactHeadSafe', () => {
126126
);
127127
});
128128

129+
it('should create og:url meta tag', () => {
130+
render(<ReactHeadSafe ogUrl="https://example.com/page" />);
131+
132+
const metaTag = document.querySelector('meta[property="og:url"]');
133+
expect(metaTag).toBeInTheDocument();
134+
expect(metaTag?.getAttribute('content')).toBe('https://example.com/page');
135+
});
136+
137+
it('should create og:type meta tag', () => {
138+
render(<ReactHeadSafe ogType="website" />);
139+
140+
const metaTag = document.querySelector('meta[property="og:type"]');
141+
expect(metaTag).toBeInTheDocument();
142+
expect(metaTag?.getAttribute('content')).toBe('website');
143+
});
144+
129145
it('should prevent duplicate og:title meta tags', () => {
130146
const { rerender } = render(<ReactHeadSafe ogTitle="First OG Title" />);
131147
rerender(<ReactHeadSafe ogTitle="Second OG Title" />);
@@ -160,6 +176,24 @@ describe('ReactHeadSafe', () => {
160176
'https://example.com/second.jpg'
161177
);
162178
});
179+
180+
it('should prevent duplicate og:url meta tags', () => {
181+
const { rerender } = render(<ReactHeadSafe ogUrl="https://site.com/1" />);
182+
rerender(<ReactHeadSafe ogUrl="https://site.com/2" />);
183+
184+
const metaTags = document.querySelectorAll('meta[property="og:url"]');
185+
expect(metaTags).toHaveLength(1);
186+
expect(metaTags[0].getAttribute('content')).toBe('https://site.com/2');
187+
});
188+
189+
it('should prevent duplicate og:type meta tags', () => {
190+
const { rerender } = render(<ReactHeadSafe ogType="website" />);
191+
rerender(<ReactHeadSafe ogType="website" />);
192+
193+
const metaTags = document.querySelectorAll('meta[property="og:type"]');
194+
expect(metaTags).toHaveLength(1);
195+
expect(metaTags[0].getAttribute('content')).toBe('website');
196+
});
163197
});
164198

165199
describe('multiple props', () => {
@@ -172,6 +206,8 @@ describe('ReactHeadSafe', () => {
172206
ogTitle="OG Title"
173207
ogDescription="OG Description"
174208
ogImage="https://example.com/image.jpg"
209+
ogUrl="https://example.com/page"
210+
ogType="website"
175211
/>
176212
);
177213

@@ -181,54 +217,45 @@ describe('ReactHeadSafe', () => {
181217
.querySelector('meta[name="description"]')
182218
?.getAttribute('content')
183219
).toBe('Test Description');
184-
expect(
185-
document.querySelector('meta[name="keywords"]')?.getAttribute('content')
186-
).toBe('test, keywords');
187220
expect(
188221
document
189222
.querySelector('meta[property="og:title"]')
190223
?.getAttribute('content')
191224
).toBe('OG Title');
192225
expect(
193226
document
194-
.querySelector('meta[property="og:description"]')
227+
.querySelector('meta[property="og:url"]')
195228
?.getAttribute('content')
196-
).toBe('OG Description');
229+
).toBe('https://example.com/page');
197230
expect(
198231
document
199-
.querySelector('meta[property="og:image"]')
232+
.querySelector('meta[property="og:type"]')
200233
?.getAttribute('content')
201-
).toBe('https://example.com/image.jpg');
234+
).toBe('website');
202235
});
203236

204237
it('should update only changed props', () => {
205238
const { rerender } = render(
206-
<ReactHeadSafe
207-
title="Initial Title"
208-
description="Initial Description"
209-
/>
239+
<ReactHeadSafe title="Initial Title" ogUrl="https://example.com/1" />
210240
);
211241

212242
expect(document.title).toBe('Initial Title');
213243
expect(
214244
document
215-
.querySelector('meta[name="description"]')
245+
.querySelector('meta[property="og:url"]')
216246
?.getAttribute('content')
217-
).toBe('Initial Description');
247+
).toBe('https://example.com/1');
218248

219249
rerender(
220-
<ReactHeadSafe
221-
title="Updated Title"
222-
description="Initial Description"
223-
/>
250+
<ReactHeadSafe title="Updated Title" ogUrl="https://example.com/1" />
224251
);
225252

226253
expect(document.title).toBe('Updated Title');
227254
expect(
228255
document
229-
.querySelector('meta[name="description"]')
256+
.querySelector('meta[property="og:url"]')
230257
?.getAttribute('content')
231-
).toBe('Initial Description');
258+
).toBe('https://example.com/1');
232259
});
233260
});
234261

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@ export interface ReactHeadSafeProps {
1111
ogDescription?: string;
1212
/** The Open Graph image URL (og:image) for social media sharing */
1313
ogImage?: string;
14+
/** The canonical URL of your object that will be used as its permanent ID in the graph (og:url) */
15+
ogUrl?: string;
16+
/** The type of your object, e.g., "website", "article" (og:type) */
17+
ogType?: string;
1418
}

0 commit comments

Comments
 (0)