Skip to content

Commit 4bac90a

Browse files
authored
Merge pull request #16 from umsungjun/feat/add-canonical-url-support
Feat/add canonical url support
2 parents 5227a37 + 720a15f commit 4bac90a

9 files changed

Lines changed: 80 additions & 1 deletion

File tree

README.ko.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ function MyPage() {
6262
ogImage="https://example.com/image.jpg"
6363
ogUrl="https://example.com/page"
6464
ogType="website"
65+
canonicalUrl="https://example.com/page"
6566
/>
6667
<div>Your page content...</div>
6768
</>
@@ -75,6 +76,7 @@ function MyPage() {
7576
- meta description과 keywords 추가/업데이트
7677
- 소셜 미디어용 Open Graph 태그 추가/업데이트
7778
- Twitter Card 태그 추가/업데이트 (Open Graph 태그에서 자동 생성)
79+
- canonical URL 링크 태그 추가/업데이트
7880
- 중복 태그 제거
7981

8082
## API 레퍼런스
@@ -91,6 +93,7 @@ function MyPage() {
9193
| `ogImage` | `string` | 소셜 미디어 공유를 위한 Open Graph 이미지 URL (og:image) |
9294
| `ogUrl` | `string` | 소셜 미디어 공유를 위한 Open Graph URL (og:url) |
9395
| `ogType` | `string` | 소셜 미디어 공유를 위한 Open Graph 타입, 예: "website", "article" (og:type) |
96+
| `canonicalUrl` | `string` | SEO를 위한 페이지의 대표 URL (`<link rel="canonical">`) |
9497

9598
### Twitter Card 지원
9699

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ function MyPage() {
6262
ogImage="https://example.com/image.jpg"
6363
ogUrl="https://example.com/page"
6464
ogType="website"
65+
canonicalUrl="https://example.com/page"
6566
/>
6667
<div>Your page content...</div>
6768
</>
@@ -75,6 +76,7 @@ That's it! The component will automatically:
7576
- Add/update meta description and keywords
7677
- Add/update Open Graph tags for social media
7778
- Add/update Twitter Card tags (automatically generated from Open Graph tags)
79+
- Add/update the canonical URL link tag
7880
- Remove any duplicate tags
7981

8082
## API Reference
@@ -91,6 +93,7 @@ That's it! The component will automatically:
9193
| `ogImage` | `string` | The Open Graph image URL (og:image) for social media sharing |
9294
| `ogUrl` | `string` | The canonical URL of your object that will be used as its permanent ID in the graph (og:url) |
9395
| `ogType` | `string` | The type of your object, e.g., "website", "article" (og:type) |
96+
| `canonicalUrl` | `string` | The canonical URL of the page for SEO (`<link rel="canonical">`) |
9497

9598
### Twitter Card Support
9699

examples/basic/src/pages/About.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default function About() {
1212
ogImage={`${window.location.origin}/logo.png`}
1313
ogUrl={window.location.href}
1414
ogType="website"
15+
canonicalUrl={window.location.href}
1516
/>
1617

1718
<div className="page-container">
@@ -66,6 +67,7 @@ function MyPage() {
6667
ogImage="https://example.com/image.jpg"
6768
ogUrl="https://example.com/page"
6869
ogType="website"
70+
canonicalUrl="https://example.com/page"
6971
/>
7072
7173
<div>Your page content here</div>

examples/basic/src/pages/Contact.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default function Contact() {
1212
ogImage={`${window.location.origin}/logo.png`}
1313
ogUrl={window.location.href}
1414
ogType="website"
15+
canonicalUrl={window.location.href}
1516
/>
1617

1718
<div className="page-container">

examples/basic/src/pages/Home.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default function Home() {
1212
ogImage={`${window.location.origin}/logo.png`}
1313
ogUrl={window.location.href}
1414
ogType="website"
15+
canonicalUrl={window.location.href}
1516
/>
1617

1718
<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.3.0",
3+
"version": "1.4.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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { type ReactHeadSafeProps } from './types';
1616
* ogImage="https://example.com/image.jpg"
1717
* ogUrl="https://example.com/page"
1818
* ogType="website"
19+
* canonicalUrl="https://example.com/page"
1920
* />
2021
*/
2122
export const ReactHeadSafe: FC<ReactHeadSafeProps> = ({
@@ -27,6 +28,7 @@ export const ReactHeadSafe: FC<ReactHeadSafeProps> = ({
2728
ogImage,
2829
ogUrl,
2930
ogType,
31+
canonicalUrl,
3032
}) => {
3133
useLayoutEffect(() => {
3234
// Update title
@@ -68,6 +70,11 @@ export const ReactHeadSafe: FC<ReactHeadSafeProps> = ({
6870
if (ogType !== undefined) {
6971
updateMetaTag('property', 'og:type', ogType);
7072
}
73+
74+
// Update canonical URL
75+
if (canonicalUrl !== undefined) {
76+
updateLinkTag('canonical', canonicalUrl);
77+
}
7178
}, [
7279
title,
7380
description,
@@ -77,11 +84,28 @@ export const ReactHeadSafe: FC<ReactHeadSafeProps> = ({
7784
ogImage,
7885
ogUrl,
7986
ogType,
87+
canonicalUrl,
8088
]);
8189

8290
return null;
8391
};
8492

93+
/**
94+
* Updates or creates a link tag in the document head.
95+
* Removes existing tag with the same rel to prevent duplicates.
96+
*/
97+
function updateLinkTag(rel: string, href: string): void {
98+
const existingTag = document.querySelector(`link[rel="${rel}"]`);
99+
if (existingTag) {
100+
existingTag.remove();
101+
}
102+
103+
const linkTag = document.createElement('link');
104+
linkTag.setAttribute('rel', rel);
105+
linkTag.setAttribute('href', href);
106+
document.head.appendChild(linkTag);
107+
}
108+
85109
/**
86110
* Updates or creates a meta tag in the document head.
87111
* Removes existing tag with the same identifier to prevent duplicates.

src/test/ReactHeadSafe.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,43 @@ describe('ReactHeadSafe', () => {
333333
});
334334
});
335335

336+
describe('canonical URL', () => {
337+
it('should create link rel="canonical" tag', () => {
338+
render(<ReactHeadSafe canonicalUrl="https://example.com/page" />);
339+
340+
const linkTag = document.querySelector('link[rel="canonical"]');
341+
expect(linkTag).toBeInTheDocument();
342+
expect(linkTag?.getAttribute('href')).toBe('https://example.com/page');
343+
});
344+
345+
it('should update canonical URL when prop changes', () => {
346+
const { rerender } = render(
347+
<ReactHeadSafe canonicalUrl="https://example.com/page-1" />
348+
);
349+
350+
let linkTag = document.querySelector('link[rel="canonical"]');
351+
expect(linkTag?.getAttribute('href')).toBe('https://example.com/page-1');
352+
353+
rerender(<ReactHeadSafe canonicalUrl="https://example.com/page-2" />);
354+
355+
linkTag = document.querySelector('link[rel="canonical"]');
356+
expect(linkTag?.getAttribute('href')).toBe('https://example.com/page-2');
357+
});
358+
359+
it('should prevent duplicate canonical link tags', () => {
360+
const { rerender } = render(
361+
<ReactHeadSafe canonicalUrl="https://example.com/first" />
362+
);
363+
rerender(<ReactHeadSafe canonicalUrl="https://example.com/second" />);
364+
365+
const linkTags = document.querySelectorAll('link[rel="canonical"]');
366+
expect(linkTags).toHaveLength(1);
367+
expect(linkTags[0].getAttribute('href')).toBe(
368+
'https://example.com/second'
369+
);
370+
});
371+
});
372+
336373
describe('multiple props', () => {
337374
it('should handle all props together', () => {
338375
render(
@@ -345,6 +382,7 @@ describe('ReactHeadSafe', () => {
345382
ogImage="https://example.com/image.jpg"
346383
ogUrl="https://example.com/page"
347384
ogType="website"
385+
canonicalUrl="https://example.com/page"
348386
/>
349387
);
350388

@@ -369,6 +407,11 @@ describe('ReactHeadSafe', () => {
369407
.querySelector('meta[property="og:type"]')
370408
?.getAttribute('content')
371409
).toBe('website');
410+
expect(
411+
document
412+
.querySelector('link[rel="canonical"]')
413+
?.getAttribute('href')
414+
).toBe('https://example.com/page');
372415
});
373416

374417
it('should update only changed props', () => {

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ export interface ReactHeadSafeProps {
1515
ogUrl?: string;
1616
/** The type of your object, e.g., "website", "article" (og:type) */
1717
ogType?: string;
18+
/** The canonical URL of the page for SEO (link rel="canonical") */
19+
canonicalUrl?: string;
1820
}

0 commit comments

Comments
 (0)