🖱️ Hover-to-Preview System
Shows a preview card when user hovers over content, with option to generate link.
html
<script>
(function() {
'use strict';
// Configuration
const CONFIG = {
hoverDelay: 800, // ms before showing preview
targetSelector: 'h1, h2.entry-title, .post-title',
previewPosition: 'below' // 'below', 'above', 'side'
};
let hoverTimeout = null;
let previewCard = null;
let currentTarget = null;
// Preview card builder
const PreviewCard = {
create(metadata, backlinkURL) {
const card = document.createElement('div');
card.className = 'aepiot-hover-preview';
card.setAttribute('role', 'tooltip');
Object.assign(card.style, {
position: 'absolute',
zIndex: '10000',
width: '320px',
padding: '20px',
background: 'white',
border: '2px solid #e5e7eb',
borderRadius: '12px',
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
fontSize: '14px',
lineHeight: '1.6',
animation: 'fadeIn 0.3s ease-out'
});
card.innerHTML = ` <style>
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.aepiot-hover-preview-btn {
transition: all 0.2s ease;
}
.aepiot-hover-preview-btn:hover {
transform: translateY(-2px);
}
</style>
<div style="margin-bottom: 12px;">
<div style="font-weight: 700; color: #111827; margin-bottom: 4px; font-size: 16px;">
📎 aéPiot Backlink
</div>
<div style="color: #6b7280; font-size: 13px;">
Share this content with a semantic backlink
</div>
</div>
<div style="margin-bottom: 16px; padding: 12px; background: #f9fafb; border-radius: 8px;">
<div style="font-size: 13px; color: #374151; margin-bottom: 6px;">
<strong>Title:</strong> ${this.truncate(metadata.title, 50)}
</div>
<div style="font-size: 12px; color: #6b7280;">
${this.truncate(metadata.description, 80)}
</div>
</div>
<div style="display: flex; gap: 8px;">
<a href="${backlinkURL}"
target="_blank"
rel="noopener noreferrer"
class="aepiot-hover-preview-btn"
style="flex: 1; padding: 10px 16px; background: #6366f1; color: white;
text-align: center; text-decoration: none; border-radius: 8px;
font-weight: 600; font-size: 13px;">
Open Link
</a>
<button onclick="navigator.clipboard.writeText('${backlinkURL.replace(/'/g, "\\'")}');
this.textContent='✅ Copied';
setTimeout(() => this.textContent='📋 Copy', 2000);"
class="aepiot-hover-preview-btn"
style="padding: 10px 16px; background: #f3f4f6; color: #374151;
border: 1px solid #d1d5db; border-radius: 8px;
font-weight: 600; font-size: 13px; cursor: pointer;">
📋 Copy
</button>
</div>
<div style="margin-top: 12px; text-align: center;">
<button onclick="this.closest('.aepiot-hover-preview').remove();"
style="background: none; border: none; color: #9ca3af;
font-size: 12px; cursor: pointer; text-decoration: underline;">
Close
</button>
</div>
`;
return card;
},
truncate(text, length) {
if (!text) return '';
if (text.length <= length) return text;
return text.substring(0, length - 3) + '...';
},
position(card, target) {
const rect = target.getBoundingClientRect();
const cardHeight = 280; // approximate
if (CONFIG.previewPosition === 'above') {
card.style.top = (rect.top + window.scrollY - cardHeight - 10) + 'px';
card.style.left = (rect.left + window.scrollX) + 'px';
} else if (CONFIG.previewPosition === 'side') {
card.style.top = (rect.top + window.scrollY) + 'px';
card.style.left = (rect.right + window.scrollX + 15) + 'px';
} else {
// Default: below
card.style.top = (rect.bottom + window.scrollY + 10) + 'px';
card.style.left = (rect.left + window.scrollX) + 'px';
}
}
};
// Metadata extraction
function extractMetadata() {
return {
title: document.title || 'Untitled',
description: document.querySelector('meta[name="description"]')?.content ||
document.querySelector('p')?.textContent?.substring(0, 160) ||
'Content',
url: window.location.href
};
}
// Generate backlink URL
function generateBacklinkURL(metadata) {
const params = new URLSearchParams({
title: metadata.title,
description: metadata.description,
link: metadata.url
});
return `https://aepiot.com/backlink.html?${params.toString()}`;
}
// Show preview
function showPreview(target) {
// Remove existing preview
if (previewCard) {
previewCard.remove();
}
const metadata = extractMetadata();
const backlinkURL = generateBacklinkURL(metadata);
previewCard = PreviewCard.create(metadata, backlinkURL);
PreviewCard.position(previewCard, target);
document.body.appendChild(previewCard);
currentTarget = target;
// Auto-hide when clicking outside
setTimeout(() => {
document.addEventListener('click', hidePreview);
}, 100);
}
// Hide preview
function hidePreview(event) {
if (previewCard &&
!previewCard.contains(event?.target) &&
currentTarget !== event?.target) {
previewCard.remove();
previewCard = null;
document.removeEventListener('click', hidePreview);
}
}
// Initialize hover listeners
function initialize() {
const targets = document.querySelectorAll(CONFIG.targetSelector);
targets.forEach(target => {
target.style.cursor = 'help';
target.title = 'Hover to generate aéPiot backlink';
target.addEventListener('mouseenter', () => {
hoverTimeout = setTimeout(() => {
showPreview(target);
}, CONFIG.hoverDelay);
});
target.addEventListener('mouseleave', () => {
clearTimeout(hoverTimeout);
});
});
console.log(`[aéPiot] Hover preview enabled on ${targets.length} elements`);
}
// Start when ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
})();
</script>Features:
- 🖱️ Hover-triggered preview cards
- ⏱️ Configurable delay
- 📋 Quick copy functionality
- 🎯 Smart positioning
- 🎨 Modern, clean design
- ♿ Accessible tooltips
Continue to Part 3: Modular & Advanced Patterns →
Alternative aéPiot Working Scripts Guide - Part 3: Modular & Advanced Patterns
Web Components, Observers, and Dynamic Content Handling
🎨 Custom Web Component Implementation
Create a reusable <aepiot-link> web component for maximum flexibility.
html
<script>
(function() {
'use strict';
class AePiotLinkElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._backlinkURL = null;
}
// Observed attributes
static get observedAttributes() {
return ['title', 'description', 'url', 'style-variant', 'auto-generate'];
}
// Lifecycle: Connected to DOM
connectedCallback() {
this.render();
if (this.getAttribute('auto-generate') === 'true') {
this.generateBacklink();
}
}
// Lifecycle: Attribute changed
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
// Generate backlink URL
generateBacklink() {
const title = this.getAttribute('title') || this.extractTitle();
const description = this.getAttribute('description') || this.extractDescription();
const url = this.getAttribute('url') || window.location.href;
const params = new URLSearchParams({
title: this.sanitize(title, 200),
description: this.sanitize(description, 500),
link: url
});
this._backlinkURL = `https://aepiot.com/backlink.html?${params.toString()}`;
this.render();
// Dispatch custom event
this.dispatchEvent(new CustomEvent('backlink-generated', {
detail: { url: this._backlinkURL },
bubbles: true,
composed: true
}));
}
// Extract title from page
extractTitle() {
return document.title ||
document.querySelector('h1')?.textContent?.trim() ||
'Page';
}
// Extract description from page
extractDescription() {
return document.querySelector('meta[name="description"]')?.content ||
document.querySelector('p')?.textContent?.trim().substring(0, 160) ||
'Content';
}
// Sanitize text
sanitize(text, maxLength) {
if (!text) return '';
text = text.replace(/\s+/g, ' ').trim();
if (text.length > maxLength) {
text = text.substring(0, maxLength - 3) + '...';
}
return text;
}
// Render component
render() {
const variant = this.getAttribute('style-variant') || 'default';
const styles = this.getStyles(variant);
this.shadowRoot.innerHTML = ` <style>
:host {
display: block;
margin: 20px 0;
}
.container {
${styles.container}
}
.button {
${styles.button}
transition: all 0.3s ease;
}
.button:hover {
${styles.buttonHover}
}
.link {
${styles.link}
}
.generated {
${styles.generated}
}
@media (max-width: 768px) {
.container {
padding: 16px;
}
.button {
font-size: 14px;
padding: 10px 20px;
}
}
</style>
${this._backlinkURL ? this.renderGenerated() : this.renderButton()}
`;
this.attachEventListeners();
}
// Render button state
renderButton() {
return `
<div class="container">
<button class="button" id="generate-btn">
🔗 Generate aéPiot Backlink
</button>
</div>
`;
}
// Render generated state
renderGenerated() {
return `
<div class="container generated">
<div style="margin-bottom: 12px; font-weight: 600; color: #059669;">
✅ Backlink Generated
</div>
<a href="${this._backlinkURL}"
class="link"
target="_blank"
rel="noopener noreferrer">
${this.truncate(this._backlinkURL, 60)}
</a>
<div style="margin-top: 12px;">
<button class="button" id="copy-btn" style="font-size: 14px;">
📋 Copy to Clipboard
</button>
<button class="button" id="regenerate-btn"
style="font-size: 14px; margin-left: 8px; background: #6b7280;">
🔄 Regenerate
</button>
</div>
</div>
`;
}
// Attach event listeners
attachEventListeners() {
const generateBtn = this.shadowRoot.getElementById('generate-btn');
const copyBtn = this.shadowRoot.getElementById('copy-btn');
const regenerateBtn = this.shadowRoot.getElementById('regenerate-btn');
if (generateBtn) {
generateBtn.addEventListener('click', () => this.generateBacklink());
}
if (copyBtn) {
copyBtn.addEventListener('click', () => this.copyToClipboard());
}
if (regenerateBtn) {
regenerateBtn.addEventListener('click', () => {
this._backlinkURL = null;
this.generateBacklink();
});
}
}
// Copy to clipboard
async copyToClipboard() {
try {
await navigator.clipboard.writeText(this._backlinkURL);
const copyBtn = this.shadowRoot.getElementById('copy-btn');
if (copyBtn) {
copyBtn.textContent = '✅ Copied!';
setTimeout(() => {
copyBtn.textContent = '📋 Copy to Clipboard';
}, 2000);
}
} catch (err) {
console.error('Failed to copy:', err);
alert('Failed to copy to clipboard');
}
}
// Truncate text
truncate(text, length) {
if (text.length <= length) return text;
return text.substring(0, length - 3) + '...';
}
// Get styles based on variant
getStyles(variant) {
const variants = {
default: {
container: `
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
text-align: center;
`,
button: `
padding: 12px 24px;
background: white;
color: #667eea;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 16px;
cursor: pointer;
`,
buttonHover: `
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
`,
link: `
display: inline-block;
color: white;
text-decoration: underline;
word-break: break-all;
`,
generated: `
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
`
},
minimal: {
container: `
padding: 16px;
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
`,
button: `
padding: 10px 20px;
background: #6366f1;
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
font-size: 14px;
cursor: pointer;
`,
buttonHover: `
background: #4f46e5;
`,
link: `
display: inline-block;
color: #2563eb;
text-decoration: underline;
word-break: break-all;
`,
generated: `
background: #f0fdf4;
border-color: #86efac;
color: #166534;
`
},
dark: {
container: `
padding: 20px;
background: #1f2937;
border-radius: 12px;
color: white;
`,
button: `
padding: 12px 24px;
background: #6366f1;
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 16px;
cursor: pointer;
`,
buttonHover: `
background: #4f46e5;
transform: translateY(-2px);
`,
link: `
display: inline-block;
color: #93c5fd;
text-decoration: underline;
word-break: break-all;
`,
generated: `
background: #064e3b;
color: #d1fae5;
`
}
};
return variants[variant] || variants.default;
}
}
// Register custom element
if (!customElements.get('aepiot-link')) {
customElements.define('aepiot-link', AePiotLinkElement);
console.log('[aéPiot] Custom element <aepiot-link> registered');
}
})();
</script>
<!-- Usage Examples -->
<!-- Basic usage - auto-generate -->
<aepiot-link auto-generate="true"></aepiot-link>
<!-- With custom data -->
<aepiot-link
title="My Custom Title"
description="Custom description for this link"
url="https://example.com/page"
style-variant="minimal">
</aepiot-link>
<!-- Dark theme -->
<aepiot-link style-variant="dark"></aepiot-link>
<!-- Listen to events -->
<script>
document.addEventListener('backlink-generated', (e) => {
console.log('Backlink created:', e.detail.url);
// Send to analytics, etc.
});
</script>Features:
- 🧩 Reusable Web Component
- 🎨 Multiple style variants
- 🔄 Reactive to attribute changes
- 📢 Custom events for integration
- 🌓 Light/Dark theme support
- 📱 Responsive design
- ♿ Shadow DOM encapsulation
👀 Mutation Observer for Dynamic Content
Automatically generates backlinks when new content is added to the page (SPA compatibility).
html
<script>
(function() {
'use strict';
const CONFIG = {
targetSelector: 'article, .post, .entry',
observeSubtree: true,
debounceDelay: 1000,
maxBacklinks: 5
};
let processedElements = new WeakSet();
let debounceTimer = null;
let backlinkCount = 0;
// Backlink generator
const BacklinkGenerator = {
create(element) {
const metadata = this.extractFromElement(element);
const url = this.buildURL(metadata);
return this.createUI(url, metadata);
},
extractFromElement(element) {
return {
title: this.getElementTitle(element),
description: this.getElementDescription(element),
url: this.getElementURL(element)
};
},
getElementTitle(element) {
// Try various selectors
const titleElement = element.querySelector('h1, h2, h3, .title, .post-title, .entry-title');
if (titleElement) {
return titleElement.textContent.trim();
}
// Fallback to first text
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
const text = node.textContent.trim();
if (text.length > 10) {
return text.substring(0, 100);
}
}
return document.title || 'Content';
},
getElementDescription(element) {
const paragraph = element.querySelector('p, .excerpt, .summary');
if (paragraph) {
return paragraph.textContent.trim().substring(0, 160);
}
return element.textContent.trim().substring(0, 160);
},
getElementURL(element) {
// Try to find a link within the element
const link = element.querySelector('a[href^="http"]');
if (link) {
return link.href;
}
// Check for data attributes
const urlAttr = element.getAttribute('data-url') ||
element.getAttribute('data-permalink');
if (urlAttr) {
return urlAttr;
}
return window.location.href;
},
buildURL(metadata) {
const params = new URLSearchParams({
title: metadata.title,
description: metadata.description,
link: metadata.url
});
return `https://aepiot.com/backlink.html?${params.toString()}`;
},
createUI(url, metadata) {
const container = document.createElement('div');
container.className = 'aepiot-dynamic-link';
container.setAttribute('data-aepiot-generated', 'true');
Object.assign(container.style, {
margin: '16px 0',
padding: '12px 16px',
background: '#f0f9ff',
border: '1px solid #bae6fd',
borderLeft: '4px solid #0284c7',
borderRadius: '6px',
fontSize: '14px',
display: 'flex',
alignItems: 'center',
gap: '12px'
});
container.innerHTML = ` <div style="flex-shrink: 0; font-size: 20px;">🔗</div>
<div style="flex-grow: 1;">
<div style="font-weight: 600; color: #0c4a6e; margin-bottom: 4px;">
aéPiot Backlink
</div>
<a href="${url}"
target="_blank"
rel="noopener noreferrer"
style="color: #0284c7; text-decoration: none; font-size: 13px;">
View semantic backlink →
</a>
</div>
`;
return container;
}
};
// Process new elements
function processNewElements(elements) {
if (backlinkCount >= CONFIG.maxBacklinks) {
console.log('[aéPiot] Max backlinks reached, stopping observer');
observer.disconnect();
return;
}
elements.forEach(element => {
// Skip if already processed
if (processedElements.has(element)) {
return;
}
// Skip if already has backlink
if (element.querySelector('[data-aepiot-generated]')) {
return;
}
// Skip if too small (likely not main content)
const textContent = element.textContent.trim();
if (textContent.length < 100) {
return;
}
// Mark as processed
processedElements.add(element);
// Generate and insert backlink
try {
const backlinkElement = BacklinkGenerator.create(element);
element.appendChild(backlinkElement);
backlinkCount++;
console.log(`[aéPiot] Generated dynamic backlink ${backlinkCount}/${CONFIG.maxBacklinks}`);
// Analytics
if (typeof gtag !== 'undefined') {
gtag('event', 'aepiot_dynamic_generated', {
'event_category': 'automation',
'value': backlinkCount
});
}
} catch (error) {
console.error('[aéPiot] Failed to generate backlink:', error);
}
});
}
// Debounced mutation handler
function handleMutations(mutations) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const newElements = [];
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if node matches target
if (node.matches(CONFIG.targetSelector)) {
newElements.push(node);
}
// Check children
const children = node.querySelectorAll(CONFIG.targetSelector);
newElements.push(...Array.from(children));
}
});
});
if (newElements.length > 0) {
processNewElements(newElements);
}
}, CONFIG.debounceDelay);
}
// Initialize observer
const observer = new MutationObserver(handleMutations);
function initialize() {
// Process existing elements
const existingElements = document.querySelectorAll(CONFIG.targetSelector);
processNewElements(Array.from(existingElements));
// Start observing
observer.observe(document.body, {
childList: true,
subtree: CONFIG.observeSubtree
});
console.log('[aéPiot] Mutation observer started for:', CONFIG.targetSelector);
}
// Start when ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
observer.disconnect();
});
})();
</script>Features:
- 👁️ Monitors DOM changes automatically
- 🔄 Perfect for SPAs (Single Page Apps)
- ⚡ Debounced for performance
- 🎯 Smart content detection
- 📊 Backlink limit protection
- 🧹 Memory-efficient with WeakSet