From 86f63f52b8f809bd45083b2595ee8e5ae4028097 Mon Sep 17 00:00:00 2001 From: umsungjun Date: Fri, 30 Jan 2026 09:43:15 +0900 Subject: [PATCH 1/3] feat: implement og:url and og:type with Vitest test cases and demo update (#8)) - Add ogUrl and ogType props to ReactHeadSafe component - Implement logic to manage og:url and og:type tags with duplicate prevention - Add Vitest test cases to verify tag creation and update logic - Update README.md and README.ko.md with the new API documentation - Update the demo project to showcase practical usage of the new OG tags --- README.ko.md | 20 +++++---- README.md | 20 +++++---- examples/basic/index.html | 2 + examples/basic/src/pages/About.tsx | 2 + examples/basic/src/pages/Contact.tsx | 2 + examples/basic/src/pages/Home.tsx | 2 + src/ReactHeadSafe.tsx | 23 +++++++++- src/test/ReactHeadSafe.test.tsx | 65 ++++++++++++++++++++-------- src/types.ts | 4 ++ 9 files changed, 104 insertions(+), 36 deletions(-) diff --git a/README.ko.md b/README.ko.md index 3baf642..95483fe 100644 --- a/README.ko.md +++ b/README.ko.md @@ -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" />
Your page content...
@@ -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) | ## 로컬 개발 diff --git a/README.md b/README.md index 6fc55d8..c565ae3 100644 --- a/README.md +++ b/README.md @@ -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" />
Your page content...
@@ -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 diff --git a/examples/basic/index.html b/examples/basic/index.html index d5949b3..8644889 100644 --- a/examples/basic/index.html +++ b/examples/basic/index.html @@ -17,6 +17,8 @@ content="(main) A CSR-only React head manager that prevents duplicate meta tags." /> + +
diff --git a/examples/basic/src/pages/About.tsx b/examples/basic/src/pages/About.tsx index a0f999d..e707ca4 100644 --- a/examples/basic/src/pages/About.tsx +++ b/examples/basic/src/pages/About.tsx @@ -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" />
diff --git a/examples/basic/src/pages/Contact.tsx b/examples/basic/src/pages/Contact.tsx index d146cd8..e8481c3 100644 --- a/examples/basic/src/pages/Contact.tsx +++ b/examples/basic/src/pages/Contact.tsx @@ -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" />
diff --git a/examples/basic/src/pages/Home.tsx b/examples/basic/src/pages/Home.tsx index e6d8906..fe1e9d9 100644 --- a/examples/basic/src/pages/Home.tsx +++ b/examples/basic/src/pages/Home.tsx @@ -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" />
diff --git a/src/ReactHeadSafe.tsx b/src/ReactHeadSafe.tsx index d3b0933..ab58d20 100644 --- a/src/ReactHeadSafe.tsx +++ b/src/ReactHeadSafe.tsx @@ -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 = ({ @@ -23,6 +25,8 @@ export const ReactHeadSafe: FC = ({ ogTitle, ogDescription, ogImage, + ogUrl, + ogType, }) => { useLayoutEffect(() => { // Update title @@ -52,7 +56,24 @@ export const ReactHeadSafe: FC = ({ 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; }; diff --git a/src/test/ReactHeadSafe.test.tsx b/src/test/ReactHeadSafe.test.tsx index 3459a81..99508d4 100644 --- a/src/test/ReactHeadSafe.test.tsx +++ b/src/test/ReactHeadSafe.test.tsx @@ -126,6 +126,22 @@ describe('ReactHeadSafe', () => { ); }); + it('should create og:url meta tag', () => { + render(); + + 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(); + + 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(); rerender(); @@ -160,6 +176,24 @@ describe('ReactHeadSafe', () => { 'https://example.com/second.jpg' ); }); + + it('should prevent duplicate og:url meta tags', () => { + const { rerender } = render(); + rerender(); + + 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(); + rerender(); + + const metaTags = document.querySelectorAll('meta[property="og:type"]'); + expect(metaTags).toHaveLength(1); + expect(metaTags[0].getAttribute('content')).toBe('website'); + }); }); describe('multiple props', () => { @@ -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" /> ); @@ -181,9 +217,6 @@ 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"]') @@ -191,44 +224,38 @@ describe('ReactHeadSafe', () => { ).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( - + ); 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( - + ); 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'); }); }); diff --git a/src/types.ts b/src/types.ts index 0da63d2..f8ddfc6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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; } From ae3d69a7474b6faa08ac40e5c186487a0e072970 Mon Sep 17 00:00:00 2001 From: umsungjun Date: Fri, 30 Jan 2026 09:44:02 +0900 Subject: [PATCH 2/3] 1.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 38adf38..0d282cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-head-safe", - "version": "1.0.2", + "version": "1.1.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", From 33ed3f104b10a59464e3ff30fd7eec645986e1cc Mon Sep 17 00:00:00 2001 From: umsungjun Date: Fri, 30 Jan 2026 09:45:46 +0900 Subject: [PATCH 3/3] 1.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0d282cf..89702cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-head-safe", - "version": "1.1.0", + "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",