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/package.json b/package.json
index 38adf38..89702cc 100644
--- a/package.json
+++ b/package.json
@@ -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",
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;
}