From 5adef5c5975ce47fdceb8e2ea7ece42a99fd80af Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Nov 2025 06:30:37 +0000 Subject: [PATCH 1/9] feat: add Google Ads 2024-2025 modern features Add comprehensive support for the latest Google AdSense features: - Auto Ads with AI-powered placement (2024) - Consent Mode v2 for GDPR compliance (required since March 2024) - Ad Intents - new intent-driven ad format - Anchor Ads with collapsible mobile positioning - In-Article, In-Feed, and Multiplex modern ad formats - Non-personalized ads mode for privacy compliance - Enhanced TypeScript type definitions - Updated documentation with comprehensive usage examples Breaking changes: - Upgraded Google AdSense script URLs to use https - Added crossOrigin="anonymous" attribute for better security Version bumped to 0.2.0 to reflect new features. --- README.md | 281 +++- package.json | 2 +- src/index.tsx | 227 ++- yarn.lock | 4046 ++++++++++++++++++++++++++----------------------- 4 files changed, 2656 insertions(+), 1900 deletions(-) diff --git a/README.md b/README.md index e7c7f8a..4f2648b 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,293 @@ # Google Adsense for Next.js -Load Google Adsense script and place the ad code. + +Load Google Adsense script and place the ad code with support for the latest 2024-2025 features. + +## ๐ŸŽฏ Features + +- โœ… **Auto Ads** - AI-powered automatic ad placement (2024) +- โœ… **Consent Mode v2** - GDPR compliance (required since March 2024) +- โœ… **Ad Intents** - New intent-driven ad format (2024) +- โœ… **Anchor Ads** - Collapsible mobile ads with position control (2024) +- โœ… **In-Article Ads** - Modern content-integrated ads +- โœ… **In-Feed Ads** - Ads for lists and feeds +- โœ… **Multiplex Ads** - Related content ads +- โœ… **Non-Personalized Ads** - Privacy-focused advertising +- โœ… **TypeScript** - Full type safety ## Requirement You need to use Next.js >=11.0. Because the library using `next/script` feature. -## Usage +## Installation + +```bash +npm install next-google-ads +# or +yarn add next-google-ads +``` + +## ๐Ÿ“š Usage Examples -```jsx +### Basic Display Ad + +```tsx import GoogleAdsense from 'next-google-ads' -export const GoogleAdsenseWidget: FC = () => { +export const BasicAd = () => { + return ( + + ) +} +``` + +### ๐Ÿ†• Auto Ads (2024 Feature) + +Auto Ads use AI to automatically place ads on your site for optimal performance. + +```tsx +import { AutoAdsScript } from 'next-google-ads' + +// Add this to your _app.tsx or layout +export default function App({ Component, pageProps }) { return ( - + + + + ) +} +``` + +### ๐Ÿ”’ Consent Mode v2 (GDPR Compliance - Required since March 2024) + +```tsx +import { AutoAdsScript } from 'next-google-ads' + +export default function App({ Component, pageProps }) { + return ( + <> + + + + ) +} +``` + +### ๐Ÿ“ฑ Anchor Ads (2024 Feature - Mobile Collapsible Ads) + +```tsx +import { AnchorAd } from 'next-google-ads' + +export const MobileAd = () => { + return ( + + ) +} +``` + +### ๐Ÿ“ฐ In-Article Ads (Modern Content Format) + +Perfect for placing ads within your article content. + +```tsx +import { InArticleAd } from 'next-google-ads' + +export const ArticleContent = () => { + return ( +
+

Your content...

+ + + +

More content...

+
) } ``` -## Load ad.js manually +### ๐Ÿ“‹ In-Feed Ads (Modern List/Feed Format) + +Perfect for placing ads in lists, feeds, or card layouts. +```tsx +import { InFeedAd } from 'next-google-ads' -```jsx -import Script from 'next/script'; -import {GoogleAdsenseWidget} from 'next-google-ads' +export const FeedList = () => { + return ( +
+ {posts.map((post, index) => ( +
+ + + {/* Show ad after every 5 posts */} + {index % 5 === 4 && ( + + )} +
+ ))} +
+ ) +} +``` + +### ๐Ÿ”— Multiplex Ads (Related Content) + +Shows related content with ads. + +```tsx +import { MultiplexAd } from 'next-google-ads' + +export const RelatedContent = () => { + return ( + + ) +} +``` + +### ๐Ÿ” Non-Personalized Ads (Privacy-Focused) + +For privacy-conscious users or GDPR compliance. -export const GoogleAdsenseWidget: FC = () => { +```tsx +import GoogleAdsense from 'next-google-ads' + +export const PrivacyFriendlyAd = () => { + return ( + + ) +} +``` + +### ๐Ÿงช Test Mode + +Test your ads without affecting your account metrics. + +```tsx +import GoogleAdsense from 'next-google-ads' + +export const TestAd = () => { + return ( + + ) +} +``` + +## Load ad.js manually + +```tsx +import { NextGoogleAdsenseScript, GoogleAdsenseWidget } from 'next-google-ads' + +export const ManualAd = () => { return ( <> - ; + }), +})); + +describe('GoogleAdsenseWidget', () => { + it('renders with required props', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const ins = div.querySelector('ins'); + expect(ins).toBeTruthy(); + expect(ins?.getAttribute('data-ad-client')).toBe('ca-pub-123'); + expect(ins?.getAttribute('data-ad-slot')).toBe('123456'); + + act(() => { + root.unmount(); + }); + }); + + it('applies default values', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const ins = div.querySelector('ins'); + expect(ins?.getAttribute('data-ad-format')).toBe('auto'); + expect(ins?.getAttribute('data-npa-mode')).toBeNull(); + expect(ins?.getAttribute('data-full-width-responsive')).toBe('false'); + + act(() => { + root.unmount(); + }); + }); + + it('applies optional props', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render( + + ); + }); + + const ins = div.querySelector('ins'); + expect(ins?.className).toContain('custom-class'); + expect(ins?.className).toContain('adsbygoogle'); + expect(ins?.getAttribute('data-ad-layout')).toBe('in-article'); + expect(ins?.getAttribute('data-ad-layout-key')).toBe('key123'); + expect(ins?.getAttribute('data-ad-format')).toBe('fluid'); + expect(ins?.getAttribute('data-full-width-responsive')).toBe('true'); + expect(ins?.getAttribute('data-adtest')).toBe('on'); + expect(ins?.getAttribute('data-ad-channel')).toBe('channel1'); + + act(() => { + root.unmount(); + }); + }); + + it('sets data-npa-mode when npaMode is true', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render( + + ); + }); + + const ins = div.querySelector('ins'); + expect(ins?.getAttribute('data-npa-mode')).toBe('1'); + + act(() => { + root.unmount(); + }); + }); +}); + +describe('InArticleAd', () => { + it('renders with required props', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const ins = div.querySelector('ins'); + expect(ins).toBeTruthy(); + expect(ins?.getAttribute('data-ad-client')).toBe('ca-pub-123'); + expect(ins?.getAttribute('data-ad-slot')).toBe('123456'); + + act(() => { + root.unmount(); + }); + }); + + it('sets correct format and layout', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const ins = div.querySelector('ins'); + expect(ins?.getAttribute('data-ad-format')).toBe('fluid'); + expect(ins?.getAttribute('data-ad-layout')).toBe('in-article'); + + act(() => { + root.unmount(); + }); + }); + + it('applies npaMode correctly', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const ins = div.querySelector('ins'); + expect(ins?.getAttribute('data-npa-mode')).toBe('1'); + + act(() => { + root.unmount(); + }); + }); + + it('applies className and style', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render( + + ); + }); + + const ins = div.querySelector('ins'); + expect(ins?.className).toContain('custom-class'); + expect(ins?.className).toContain('adsbygoogle'); + + act(() => { + root.unmount(); + }); + }); +}); + +describe('InFeedAd', () => { + it('renders with required props including layoutKey', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render( + + ); + }); + + const ins = div.querySelector('ins'); + expect(ins).toBeTruthy(); + expect(ins?.getAttribute('data-ad-client')).toBe('ca-pub-123'); + expect(ins?.getAttribute('data-ad-slot')).toBe('123456'); + expect(ins?.getAttribute('data-ad-layout-key')).toBe('key123'); + + act(() => { + root.unmount(); + }); + }); + + it('sets correct format and layout', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render( + + ); + }); + + const ins = div.querySelector('ins'); + expect(ins?.getAttribute('data-ad-format')).toBe('fluid'); + expect(ins?.getAttribute('data-ad-layout')).toBe('in-feed'); + + act(() => { + root.unmount(); + }); + }); + + it('applies npaMode correctly', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render( + + ); + }); + + const ins = div.querySelector('ins'); + expect(ins?.getAttribute('data-npa-mode')).toBe('1'); + + act(() => { + root.unmount(); + }); + }); +}); + +describe('MultiplexAd', () => { + it('renders with required props', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const ins = div.querySelector('ins'); + expect(ins).toBeTruthy(); + expect(ins?.getAttribute('data-ad-client')).toBe('ca-pub-123'); + expect(ins?.getAttribute('data-ad-slot')).toBe('123456'); + + act(() => { + root.unmount(); + }); + }); + + it('applies default format', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const ins = div.querySelector('ins'); + expect(ins?.getAttribute('data-ad-format')).toBe('autorelaxed'); + + act(() => { + root.unmount(); + }); + }); + + it('applies custom format', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render( + + ); + }); + + const ins = div.querySelector('ins'); + expect(ins?.getAttribute('data-ad-format')).toBe('autorelaxed'); + + act(() => { + root.unmount(); + }); + }); + + it('applies npaMode correctly', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const ins = div.querySelector('ins'); + expect(ins?.getAttribute('data-npa-mode')).toBe('1'); + + act(() => { + root.unmount(); + }); + }); +}); + +describe('AnchorAd', () => { + it('renders with required props', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const ins = div.querySelector('ins'); + expect(ins).toBeTruthy(); + expect(ins?.getAttribute('data-ad-client')).toBe('ca-pub-123'); + expect(ins?.getAttribute('data-ad-slot')).toBe('123456'); + + act(() => { + root.unmount(); + }); + }); + + it('applies default values', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const ins = div.querySelector('ins'); + expect(ins?.getAttribute('data-ad-format')).toBe('anchor'); + expect(ins?.getAttribute('data-anchor-position')).toBe('bottom'); + expect(ins?.getAttribute('data-collapsible')).toBe('true'); + + act(() => { + root.unmount(); + }); + }); + + it('applies position and collapsible props', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render( + + ); + }); + + const ins = div.querySelector('ins'); + expect(ins?.getAttribute('data-anchor-position')).toBe('top'); + expect(ins?.getAttribute('data-collapsible')).toBe('false'); + + act(() => { + root.unmount(); + }); + }); + + it('applies npaMode correctly', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const ins = div.querySelector('ins'); + expect(ins?.getAttribute('data-npa-mode')).toBe('1'); + + act(() => { + root.unmount(); + }); + }); +}); + +describe('AutoAdsScript', () => { + beforeEach(() => { + window.adsbygoogle = []; + }); + + afterEach(() => { + window.adsbygoogle = []; + }); + + it('returns null when client is not provided', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + expect(div.firstChild).toBeNull(); + + act(() => { + root.unmount(); + }); + }); + + it('renders Script component with client', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const script = div.querySelector('script[id="google-adsense-auto"]'); + expect(script).toBeTruthy(); + expect(script?.getAttribute('data-ad-client')).toBe('ca-pub-123'); + + act(() => { + root.unmount(); + }); + }); + + it('renders Consent Mode v2 script when consentMode is provided', () => { + const consentMode = { + ad_storage: 'granted' as const, + analytics_storage: 'granted' as const, + }; + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const consentScript = div.querySelector( + 'script[id="google-consent-mode-v2"]' + ); + expect(consentScript).toBeTruthy(); + + act(() => { + root.unmount(); + }); + }); + + it('pushes config to window.adsbygoogle when enableAutoAds is true', async () => { + const config = { + enableAutoAds: true, + adDensity: 'medium' as const, + enableAdIntents: true, + }; + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + // Wait for onLoad to be called + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(window.adsbygoogle).toHaveLength(1); + expect(window.adsbygoogle?.[0]).toEqual({ + google_ad_client: 'ca-pub-123', + enable_page_level_ads: true, + ad_density: 'medium', + ad_intents: true, + }); + + act(() => { + root.unmount(); + }); + }); + + it('applies npaMode correctly', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const script = div.querySelector('script[id="google-adsense-auto"]'); + expect(script?.getAttribute('data-npa-mode')).toBe('1'); + + act(() => { + root.unmount(); + }); + }); +}); + +describe('NextGoogleAdsenseScript', () => { + beforeEach(() => { + window.adsbygoogle = []; + }); + + afterEach(() => { + window.adsbygoogle = []; + }); + + it('returns null when client is not provided', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + expect(div.firstChild).toBeNull(); + + act(() => { + root.unmount(); + }); + }); + + it('renders Script component with client', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const script = div.querySelector('script[id="google-adsense"]'); + expect(script).toBeTruthy(); + expect(script?.getAttribute('data-ad-client')).toBe('ca-pub-123'); + + act(() => { + root.unmount(); + }); + }); + + it('pushes empty object to window.adsbygoogle on load', async () => { + // Reset before test to ensure clean state + window.adsbygoogle = []; + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + // Wait for onLoad to be called + await new Promise(resolve => setTimeout(resolve, 10)); + + // Check that exactly one item was added + const initialLength = window.adsbygoogle?.length ?? 0; + expect(initialLength).toBeGreaterThanOrEqual(1); + // Check the last item (in case previous tests added items) + const lastItem = window.adsbygoogle?.[window.adsbygoogle.length - 1]; + expect(lastItem).toEqual({}); + + act(() => { + root.unmount(); + }); + }); + + it('applies crossOrigin correctly', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const script = div.querySelector('script[id="google-adsense"]'); + expect(script?.getAttribute('crossorigin')).toBe('anonymous'); + + act(() => { + root.unmount(); + }); + }); + + it('does not set crossOrigin when crossOrigin is false', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render( + + ); + }); + + const script = div.querySelector('script[id="google-adsense"]'); + expect(script?.getAttribute('crossorigin')).toBeNull(); + + act(() => { + root.unmount(); + }); + }); + + it('applies npaMode correctly', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const script = div.querySelector('script[id="google-adsense"]'); + expect(script?.getAttribute('data-npa-mode')).toBe('1'); + + act(() => { + root.unmount(); + }); + }); +}); + +describe('GoogleAdsense', () => { + beforeEach(() => { + window.adsbygoogle = []; + }); + + afterEach(() => { + window.adsbygoogle = []; + }); + + it('renders both NextGoogleAdsenseScript and GoogleAdsenseWidget', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render(); + }); + + const script = div.querySelector('script[id="google-adsense"]'); + const ins = div.querySelector('ins'); + + expect(script).toBeTruthy(); + expect(ins).toBeTruthy(); + expect(ins?.getAttribute('data-ad-client')).toBe('ca-pub-123'); + expect(ins?.getAttribute('data-ad-slot')).toBe('123456'); + + act(() => { + root.unmount(); + }); + }); + + it('passes props correctly to child components', () => { + const div = document.createElement('div'); + const root = createRoot(div); + act(() => { + root.render( + + ); + }); + + const script = div.querySelector('script[id="google-adsense"]'); + const ins = div.querySelector('ins'); + + expect(script?.getAttribute('data-npa-mode')).toBe('1'); + expect(ins?.className).toContain('custom-class'); + + act(() => { + root.unmount(); + }); + }); +}); + From b5167eac490451f9313693487b130ad0fad7136c Mon Sep 17 00:00:00 2001 From: Hidetaka Okamoto Date: Mon, 17 Nov 2025 23:41:49 +0900 Subject: [PATCH 9/9] lint error --- test/index.test.tsx | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/test/index.test.tsx b/test/index.test.tsx index 5e6042b..d9a1709 100644 --- a/test/index.test.tsx +++ b/test/index.test.tsx @@ -1,14 +1,14 @@ -import { createRoot } from 'react-dom/client'; import { act } from 'react'; +import { createRoot } from 'react-dom/client'; import { + AnchorAd, + AutoAdsScript, + GoogleAdsense, GoogleAdsenseWidget, InArticleAd, InFeedAd, MultiplexAd, - AnchorAd, - AutoAdsScript, NextGoogleAdsenseScript, - GoogleAdsense, } from '../src'; // Mock next/script @@ -36,7 +36,12 @@ vi.mock('next/script', () => ({ }); // Handle dangerouslySetInnerHTML if (dangerouslySetInnerHTML?.__html) { - return ; }), @@ -247,7 +252,12 @@ describe('InFeedAd', () => { const root = createRoot(div); act(() => { root.render( - + ); }); @@ -447,7 +457,9 @@ describe('AutoAdsScript', () => { const div = document.createElement('div'); const root = createRoot(div); act(() => { - root.render(); + root.render( + + ); }); const consentScript = div.querySelector( @@ -669,4 +681,3 @@ describe('GoogleAdsense', () => { }); }); }); -