diff --git a/website/docs/targeting/feature-flag-evaluation.mdx b/website/docs/targeting/feature-flag-evaluation.mdx
index e2be3077c..6f6b2d923 100644
--- a/website/docs/targeting/feature-flag-evaluation.mdx
+++ b/website/docs/targeting/feature-flag-evaluation.mdx
@@ -4,8 +4,6 @@ title: Feature Flag Evaluation
description: This document offers an in-depth explanation of how the ConfigCat SDK determines the value of a feature flag.
---
-# Feature Flag Evaluation
-
This document offers an in-depth explanation of how the SDK determines the value of a feature flag when executing the `GetValue` function. Understanding this process requires prior knowledge of [targeting concepts](../targeting-overview).
The feature flag's value is determined by:
diff --git a/website/docs/targeting/targeting-rule/targeting-rule-overview.mdx b/website/docs/targeting/targeting-rule/targeting-rule-overview.mdx
index be5143407..b8c378a30 100644
--- a/website/docs/targeting/targeting-rule/targeting-rule-overview.mdx
+++ b/website/docs/targeting/targeting-rule/targeting-rule-overview.mdx
@@ -1,11 +1,9 @@
---
id: targeting-rule-overview
-title: Targeting Rule Overview
+title: Targeting Rule
description: Targeting Rules allow you to set different feature flag values for specific users or groups of users in your application.
---
-# Targeting Rule
-
## What is a Targeting Rule?
_Targeting Rules_ allow you to set different feature flag values for specific users or groups of users in your application.
diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts
index 7c236577e..3e0cfc7af 100644
--- a/website/docusaurus.config.ts
+++ b/website/docusaurus.config.ts
@@ -217,7 +217,6 @@ const config: Config = {
// smartlookKey: '05d0e4ca90c61150955104a9d4b76ab16a0b2380',
// }
// ],
- require.resolve('./plugins/copy-page-button'),
],
themeConfig: {
image: '/img/docs-cover.png',
diff --git a/website/plugins/copy-page-button/client.js b/website/plugins/copy-page-button/client.js
deleted file mode 100644
index 6dca7b15c..000000000
--- a/website/plugins/copy-page-button/client.js
+++ /dev/null
@@ -1,138 +0,0 @@
-import React from 'react';
-import { createRoot } from 'react-dom/client';
-import CopyPageButton from './CopyPageButton';
-import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
-
-// Only run in browser
-if (ExecutionEnvironment.canUseDOM) {
- let root = null;
- let lastUrl = location.href;
- let recheckInterval = null;
-
- const getPluginOptions = () =>
- (typeof window !== 'undefined' && window.__COPY_PAGE_BUTTON_OPTIONS__) ||
- {};
-
- const cleanup = () => {
- const container = document.getElementById('copy-page-button-container');
- if (container) {
- if (root) {
- try {
- root.unmount();
- } catch (e) {}
- root = null;
- }
- container.remove();
- }
- if (recheckInterval) {
- clearInterval(recheckInterval);
- recheckInterval = null;
- }
- };
-
- // Inject button next to main
in docs header (preserve scroll to prevent mobile jump)
- const injectNextToHeading = () => {
- const header = document.querySelector('.theme-doc-markdown header');
- if (!header) return;
-
- const h1 = header.querySelector('h1');
- if (!h1) return;
-
- // Avoid duplicates
- if (header.querySelector('#copy-page-button-container')) return;
-
- // Save current scroll position (works for mobile and desktop)
- const scrollX = window.scrollX || window.pageXOffset || 0;
- const scrollY = window.scrollY || window.pageYOffset || 0;
-
- // Remove old container (if present) to avoid duplicates
- cleanup();
-
- const container = document.createElement('div');
- container.id = 'copy-page-button-container';
-
- const pluginOptions = getPluginOptions();
- const customStyles = pluginOptions.customStyles || {};
- const containerStyles = customStyles.container?.style || {};
- Object.assign(container.style, containerStyles);
-
- // Insert after the using insertAdjacentElement to avoid affecting focus
- h1.insertAdjacentElement('afterend', container);
-
- // Render React root into container
- if (root) {
- try {
- root.unmount();
- } catch (e) {}
- }
- root = createRoot(container);
-
- root.render(
- React.createElement(CopyPageButton, {
- customStyles: pluginOptions.customStyles,
- enabledActions: pluginOptions.enabledActions,
- }),
- );
- };
-
- const initializeButton = () => {
- setTimeout(() => {
- injectNextToHeading();
-
- // Re-check in case of hydration delays
- let attempts = 0;
- const maxAttempts = 30;
- recheckInterval = setInterval(() => {
- attempts++;
- const hasButton = document.getElementById('copy-page-button-container');
- const h1 = document.querySelector('.theme-doc-markdown header h1');
- if (h1 && !hasButton) injectNextToHeading();
- if (attempts > maxAttempts || hasButton) {
- clearInterval(recheckInterval);
- recheckInterval = null;
- }
- }, 300);
- }, 150);
- };
-
- const handleRouteChange = () => {
- cleanup();
- // Delay slightly to let Docusaurus render the new heading, then inject
- setTimeout(() => {
- injectNextToHeading();
- }, 250);
- };
-
- // --- Bootstrapping ---
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', initializeButton);
- } else {
- initializeButton();
- }
-
- // Handle SPA navigation
- window.addEventListener('popstate', handleRouteChange);
- if (typeof document !== 'undefined') {
- document.addEventListener('docusaurus-route-update', handleRouteChange);
- }
-
- // Detect pushState/replaceState
- const originalPushState = history.pushState;
- const originalReplaceState = history.replaceState;
- const checkUrlChange = () => {
- if (location.href !== lastUrl) {
- lastUrl = location.href;
- handleRouteChange();
- }
- };
-
- history.pushState = function (...args) {
- originalPushState.apply(this, args);
- setTimeout(checkUrlChange, 0);
- };
-
- history.replaceState = function (...args) {
- originalReplaceState.apply(this, args);
- setTimeout(checkUrlChange, 0);
- };
-}
diff --git a/website/plugins/copy-page-button/index.js b/website/plugins/copy-page-button/index.js
deleted file mode 100644
index a54c30727..000000000
--- a/website/plugins/copy-page-button/index.js
+++ /dev/null
@@ -1,34 +0,0 @@
-const path = require("path");
-
-module.exports = function copyPageButtonPlugin(context, options = {}) {
- const {
- customStyles = {},
- enabledActions = ['copy', 'view', 'chatgpt', 'claude'],
- ...otherOptions
- } = options;
-
- return {
- name: "copy-page-button-plugin",
-
- getClientModules() {
- return [path.resolve(__dirname, "./client.js")];
- },
-
- injectHtmlTags() {
- return {
- headTags: [
- {
- tagName: 'script',
- innerHTML: `
- window.__COPY_PAGE_BUTTON_OPTIONS__ = ${JSON.stringify({
- customStyles,
- enabledActions,
- ...otherOptions
- })};
- `
- }
- ]
- };
- },
- };
-};
diff --git a/website/plugins/copy-page-button/CopyPageButton.js b/website/src/components/CopyPageButton.tsx
similarity index 50%
rename from website/plugins/copy-page-button/CopyPageButton.js
rename to website/src/components/CopyPageButton.tsx
index 8cd292c36..f8873bf04 100644
--- a/website/plugins/copy-page-button/CopyPageButton.js
+++ b/website/src/components/CopyPageButton.tsx
@@ -1,9 +1,35 @@
-import React, { useState, useEffect, useRef } from 'react';
-import { createPortal } from 'react-dom';
-import styles from './styles.module.css';
+import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
+import styles from '../css/copy-page-button.module.scss';
+
+// --- TYPES ---
+
+type ActionId = 'copy' | 'view' | 'chatgpt' | 'claude';
+
+interface DropdownItem {
+ id: ActionId;
+ title: string;
+ description: string;
+ icon: JSX.Element;
+ action: () => void;
+}
+
+interface DropdownPosition {
+ top: number;
+ left: number;
+}
+
+// --- CONFIGS ---
+
+const CONFIG = {
+ MOBILE_BREAKPOINT: 767,
+ DROPDOWN_OFFSET: 8,
+ DROPDOWN_WIDTH: 300,
+ DEBUG: process.env.NODE_ENV === 'development',
+ MIN_CONTENT_LENGTH: 100,
+};
// Static selectors for content cleanup
-const SELECTORS_TO_REMOVE = [
+const DEFAULT_SELECTORS_TO_REMOVE = [
'.theme-edit-this-page',
'.theme-last-updated',
'.pagination-nav',
@@ -17,23 +43,117 @@ const SELECTORS_TO_REMOVE = [
'.line-number',
];
-export default function CopyPageButton({
- enabledActions = ['copy', 'view', 'chatgpt', 'claude'],
-}) {
+const DEFAULT_CONTENT_SELECTORS = [
+ 'main article',
+ 'main .markdown',
+ 'main',
+ 'article',
+ '.main-wrapper',
+ '[role="main"]',
+] as const;
+
+// --- UTILS ---
+
+const log = (...args: any[]) => {
+ if (CONFIG.DEBUG) {
+ console.log('[CopyPageButton]', ...args);
+ }
+};
+
+// Text cleaning
+const cleanSpecialChars = (text: string): string => {
+ return text
+ .replace(/[\u200B-\u200D\uFEFF]/g, '')
+ .replace(/\u00A0/g, ' ')
+ .replace(/[\u2018\u2019]/g, "'")
+ .replace(/[\u201C\u201D]/g, '"')
+ .replace(/​/g, '')
+ .replace(/\s+/g, ' ')
+ .trim();
+};
+
+// Sanitize content
+const sanitizeContent = (content: string): string => {
+ return content
+ .replace(/