// Hyvor Talk Integration for Obsidian Publish
// Version: 2.0.1 - Paywall & Membership System (Fixed SPA navigation + login detection)
// Documentation: https://github.com/your-repo/obsidian-hyvor-talk
(function() {
'use strict';
// ============================================
// CONFIGURATION
// ============================================
const CONFIG = {
websiteId: '11990',
pageIdStrategy: 'url',
loadingMode: 'default',
containerSelector: '.published-container .markdown-preview-view',
insertPosition: 'end',
customSelector: null,
excludePaths: [],
includePaths: [],
showHeader: true,
customSettings: {},
sso: {
enabled: false,
},
debug: true,
// ============================================
// PAYWALL CONFIGURATION
// ============================================
paywall: {
enabled: true,
// Pages accessibles SANS abonnement (wildcards supportés)
publicPaths: [
'/',
'/index',
'/accueil',
'/mentions-legales',
'/a-propos',
'/tarifs',
'/contact'
],
// Textes de l'écran paywall
title: 'Contenu réservé aux membres',
subtitle: 'Rejoignez notre communauté professionnelle pour accéder à ce contenu.',
loginText: 'Déjà membre ?',
loginButton: 'Se connecter',
subscribeButton: 'Découvrir les offres',
// Style
blurContent: true,
blurAmount: '8px'
}
};
// ============================================
// UTILITY FUNCTIONS
// ============================================
function log(...args) {
if (CONFIG.debug) {
console.log('[Hyvor Talk]', ...args);
}
}
function error(...args) {
console.error('[Hyvor Talk]', ...args);
}
function matchPath(path, pattern) {
if (pattern === path) return true;
if (pattern.endsWith('/*')) {
const prefix = pattern.slice(0, -2);
return path.startsWith(prefix);
}
return false;
}
function normalizePath(path) {
try {
path = decodeURIComponent(path);
} catch (e) {}
return path.replace(/\+/g, ' ').replace(/\/+$/, '') || '/';
}
function isPublicPage() {
if (!CONFIG.paywall.enabled) return true;
const currentPath = normalizePath(window.location.pathname);
for (const pattern of CONFIG.paywall.publicPaths) {
if (matchPath(currentPath, normalizePath(pattern))) {
log('Page publique:', currentPath);
return true;
}
}
log('Page protégée:', currentPath);
return false;
}
function shouldShowComments() {
const currentPath = normalizePath(window.location.pathname);
if (CONFIG.excludePaths.length > 0) {
for (const pattern of CONFIG.excludePaths) {
if (matchPath(currentPath, normalizePath(pattern))) {
log('Page excluded by pattern:', pattern);
return false;
}
}
}
if (CONFIG.includePaths.length > 0) {
let matched = false;
for (const pattern of CONFIG.includePaths) {
if (matchPath(currentPath, normalizePath(pattern))) {
matched = true;
break;
}
}
if (!matched) {
log('Page not in include paths');
return false;
}
}
return true;
}
function getPageId() {
if (CONFIG.pageIdStrategy === 'url') {
const canonical = document.querySelector('link[rel="canonical"]');
return canonical ? canonical.href : window.location.href;
} else if (CONFIG.pageIdStrategy === 'custom') {
const metaPageId = document.querySelector('meta[name="hyvor-page-id"]');
if (metaPageId) {
return metaPageId.content;
}
return window.location.pathname;
}
return window.location.href;
}
// ============================================
// CSS STYLES
// ============================================
function injectStyles() {
if (document.getElementById('hyvor-talk-custom-css')) return;
const style = document.createElement('style');
style.id = 'hyvor-talk-custom-css';
style.textContent = `
/* ============================================
LAYOUT FIX - Minimal theme graph view
============================================ */
/* Ensure site-body is a proper flex row */
.published-container .site-body {
display: flex !important;
align-items: flex-start !important;
flex-wrap: nowrap !important;
}
/* Main content area must not overflow into graph column */
.published-container .site-body-center-column {
flex: 1 1 auto !important;
min-width: 0 !important;
overflow-x: hidden !important;
}
/* Right column (graph view) stays in its lane */
.published-container .site-body-right-column {
flex: 0 0 auto !important;
position: sticky !important;
top: 0 !important;
align-self: flex-start !important;
max-height: 100vh !important;
overflow-y: auto !important;
}
/* Wrapper respects Minimal/Sanctum theme width constraint and centers */
.published-container .markdown-preview-view .hyvor-talk-wrapper {
width: 100% !important;
max-width: var(--line-width, var(--file-line-width, 40rem)) !important;
margin: 3rem auto 0 auto !important;
padding: 2rem 0 0 0 !important;
box-sizing: border-box !important;
}
/* Basic constraints for Hyvor Talk comments */
hyvor-talk-comments {
display: block !important;
min-height: auto !important;
height: auto !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
/* ============================================
PAYWALL STYLES
============================================ */
/* Blur effect on protected content */
.hyvor-paywall-blur {
filter: blur(${CONFIG.paywall.blurAmount}) !important;
pointer-events: none !important;
user-select: none !important;
}
/* Paywall overlay container */
.hyvor-paywall-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
backdrop-filter: blur(4px);
}
/* Dark mode support */
.theme-dark .hyvor-paywall-overlay,
[data-theme="dark"] .hyvor-paywall-overlay {
background: rgba(30, 30, 30, 0.95);
}
/* Paywall card */
.hyvor-paywall-card {
background: var(--background-primary, #ffffff);
border-radius: 12px;
padding: 3rem 2.5rem;
max-width: 420px;
width: 90%;
text-align: center;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
border: 1px solid var(--background-modifier-border, #e0e0e0);
}
.theme-dark .hyvor-paywall-card,
[data-theme="dark"] .hyvor-paywall-card {
background: var(--background-primary, #1e1e1e);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
}
/* Paywall icon */
.hyvor-paywall-icon {
font-size: 3rem;
margin-bottom: 1.5rem;
}
/* Paywall title */
.hyvor-paywall-title {
font-size: 1.5rem;
font-weight: 600;
color: var(--text-normal, #333);
margin: 0 0 0.75rem 0;
}
/* Paywall subtitle */
.hyvor-paywall-subtitle {
font-size: 1rem;
color: var(--text-muted, #666);
margin: 0 0 2rem 0;
line-height: 1.5;
}
/* Paywall buttons container */
.hyvor-paywall-buttons {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
/* Primary button (subscribe) */
.hyvor-paywall-btn-primary {
background: var(--interactive-accent, #7c3aed);
color: white;
border: none;
padding: 0.875rem 1.5rem;
font-size: 1rem;
font-weight: 500;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.hyvor-paywall-btn-primary:hover {
opacity: 0.9;
transform: translateY(-1px);
}
/* Secondary button (login) */
.hyvor-paywall-btn-secondary {
background: transparent;
color: var(--text-muted, #666);
border: 1px solid var(--background-modifier-border, #e0e0e0);
padding: 0.75rem 1.5rem;
font-size: 0.9rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.hyvor-paywall-btn-secondary:hover {
background: var(--background-secondary, #f5f5f5);
}
/* Login text */
.hyvor-paywall-login-text {
font-size: 0.85rem;
color: var(--text-muted, #888);
margin-top: 0.5rem;
}
/* Memberships widget positioning */
hyvor-talk-memberships {
position: fixed;
bottom: 0;
right: 0;
z-index: 10000;
}
`;
document.head.appendChild(style);
}
// ============================================
// PAYWALL FUNCTIONS
// ============================================
function createPaywallOverlay() {
const overlay = document.createElement('div');
overlay.className = 'hyvor-paywall-overlay';
overlay.id = 'hyvor-paywall-overlay';
overlay.innerHTML = `
<div class="hyvor-paywall-card">
<div class="hyvor-paywall-icon">🔒</div>
<h2 class="hyvor-paywall-title">${CONFIG.paywall.title}</h2>
<p class="hyvor-paywall-subtitle">${CONFIG.paywall.subtitle}</p>
<div class="hyvor-paywall-buttons">
<button class="hyvor-paywall-btn-primary" id="hyvor-paywall-subscribe">
${CONFIG.paywall.subscribeButton}
</button>
<p class="hyvor-paywall-login-text">${CONFIG.paywall.loginText}</p>
<button class="hyvor-paywall-btn-secondary" id="hyvor-paywall-login">
${CONFIG.paywall.loginButton}
</button>
</div>
</div>
`;
return overlay;
}
function showPaywall() {
log('Showing paywall...');
// Add blur to content if enabled
if (CONFIG.paywall.blurContent) {
const content = document.querySelector('.markdown-preview-view');
if (content) {
content.classList.add('hyvor-paywall-blur');
}
}
// Create and show overlay
if (!document.getElementById('hyvor-paywall-overlay')) {
const overlay = createPaywallOverlay();
document.body.appendChild(overlay);
// Button handlers
const subscribeBtn = document.getElementById('hyvor-paywall-subscribe');
const loginBtn = document.getElementById('hyvor-paywall-login');
subscribeBtn.addEventListener('click', () => {
log('Opening membership modal...');
const memberships = document.querySelector('hyvor-talk-memberships');
if (memberships && memberships.api) {
memberships.api.modal.open();
}
});
loginBtn.addEventListener('click', () => {
log('Opening login modal...');
const memberships = document.querySelector('hyvor-talk-memberships');
if (memberships && memberships.api) {
memberships.api.modal.open();
}
});
}
}
function hidePaywall() {
log('Hiding paywall - user is a member');
// Remove blur
const content = document.querySelector('.markdown-preview-view');
if (content) {
content.classList.remove('hyvor-paywall-blur');
}
// Remove overlay
const overlay = document.getElementById('hyvor-paywall-overlay');
if (overlay) {
overlay.remove();
}
}
function checkMembershipStatus(membershipsElement) {
const user = membershipsElement.api.auth.user();
if (!user) {
log('User not logged in');
return { loggedIn: false, isMember: false };
}
log('User logged in:', user.name);
if (user.subscription && user.subscription.status === 'active') {
log('User has active subscription:', user.subscription.plan.name);
return { loggedIn: true, isMember: true, user, subscription: user.subscription };
}
log('User logged in but no active subscription');
return { loggedIn: true, isMember: false, user };
}
// ============================================
// SCRIPT LOADERS
// ============================================
function loadMembershipsScript() {
return new Promise((resolve, reject) => {
if (document.querySelector('script[src*="memberships.js"]')) {
log('Memberships script already loaded');
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://talk.hyvor.com/embed/memberships.js';
script.type = 'module';
script.async = true;
script.onload = () => {
log('Memberships script loaded');
resolve();
};
script.onerror = () => {
error('Failed to load Memberships script');
reject(new Error('Failed to load Memberships script'));
};
document.head.appendChild(script);
});
}
function loadHyvorScript() {
return new Promise((resolve, reject) => {
if (document.querySelector('script[src*="talk.hyvor.com/embed/embed.js"]')) {
log('Hyvor script already loaded');
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://talk.hyvor.com/embed/embed.js';
script.type = 'module';
script.async = true;
script.onload = () => {
log('Hyvor script loaded successfully');
resolve();
};
script.onerror = () => {
error('Failed to load Hyvor script');
reject(new Error('Failed to load Hyvor Talk script'));
};
document.head.appendChild(script);
});
}
function createMembershipsElement() {
if (document.querySelector('hyvor-talk-memberships')) {
return document.querySelector('hyvor-talk-memberships');
}
const memberships = document.createElement('hyvor-talk-memberships');
memberships.setAttribute('website-id', CONFIG.websiteId);
// French translations
memberships.setAttribute('t-button-subscribe', 'S\'abonner');
memberships.setAttribute('t-modal-title', 'Rejoindre la communauté');
memberships.setAttribute('t-modal-title-members', 'Vous êtes membre !');
memberships.setAttribute('t-login-to-subscribe', 'Se connecter pour s\'abonner');
memberships.setAttribute('t-manage-subscription', 'Gérer mon abonnement');
memberships.setAttribute('t-logout', 'Déconnexion');
document.body.appendChild(memberships);
return memberships;
}
function createCommentsElement() {
const comments = document.createElement('hyvor-talk-comments');
comments.setAttribute('website-id', CONFIG.websiteId);
comments.setAttribute('page-id', getPageId());
if (CONFIG.loadingMode) {
comments.setAttribute('loading', CONFIG.loadingMode);
}
if (CONFIG.customSettings && Object.keys(CONFIG.customSettings).length > 0) {
try {
comments.setAttribute('settings', JSON.stringify(CONFIG.customSettings));
} catch (e) {
error('Failed to apply custom settings:', e);
}
}
if (CONFIG.sso && CONFIG.sso.enabled) {
if (CONFIG.sso.userData && CONFIG.sso.hash) {
comments.setAttribute('sso-user', CONFIG.sso.userData);
comments.setAttribute('sso-hash', CONFIG.sso.hash);
}
}
return comments;
}
function insertComments() {
if (document.querySelector('.hyvor-talk-wrapper')) {
log('Comments already inserted, skipping...');
return false;
}
let container = document.querySelector(CONFIG.containerSelector);
if (!container) {
error('Container not found:', CONFIG.containerSelector);
return false;
}
log('Using stable container:', CONFIG.containerSelector);
const wrapper = document.createElement('div');
wrapper.className = 'hyvor-talk-wrapper';
wrapper.style.borderTop = '1px solid var(--background-modifier-border, #e0e0e0)';
wrapper.style.minHeight = 'auto';
wrapper.style.maxHeight = 'none';
if (CONFIG.showHeader) {
const header = document.createElement('h2');
header.textContent = 'Commentaires';
header.style.marginBottom = '1.5rem';
wrapper.appendChild(header);
}
const comments = createCommentsElement();
wrapper.appendChild(comments);
if (CONFIG.insertPosition === 'custom' && CONFIG.customSelector) {
const customTarget = document.querySelector(CONFIG.customSelector);
if (customTarget) {
customTarget.appendChild(wrapper);
} else {
container.appendChild(wrapper);
}
} else if (CONFIG.insertPosition === 'start') {
container.insertBefore(wrapper, container.firstChild);
} else {
container.appendChild(wrapper);
}
log('Comments inserted successfully');
return true;
}
// ============================================
// INITIALIZATION
// ============================================
function handlePageAccess(memberships) {
const status = checkMembershipStatus(memberships);
// Check if this is a public page
if (isPublicPage()) {
log('Public page - showing content');
hidePaywall();
if (shouldShowComments()) {
insertComments();
}
return;
}
// Protected page - check membership
if (status.isMember) {
log('Member access granted');
hidePaywall();
if (shouldShowComments()) {
insertComments();
}
} else {
log('Access denied - showing paywall');
showPaywall();
}
}
// Polling to detect login state changes (for existing members logging in)
let loginCheckInterval = null;
let lastLoginState = null;
function startLoginPolling(memberships) {
if (loginCheckInterval) return;
loginCheckInterval = setInterval(() => {
const user = memberships.api ? memberships.api.auth.user() : null;
const currentState = user ? `${user.id}-${user.subscription?.status || 'none'}` : 'logged-out';
if (lastLoginState !== null && lastLoginState !== currentState) {
log('Login state changed:', lastLoginState, '->', currentState);
handlePageAccess(memberships);
}
lastLoginState = currentState;
}, 1000);
}
function stopLoginPolling() {
if (loginCheckInterval) {
clearInterval(loginCheckInterval);
loginCheckInterval = null;
}
lastLoginState = null;
}
async function init() {
log('Initializing Hyvor Talk integration v2.0...');
// Validate configuration
if (!CONFIG.websiteId || CONFIG.websiteId === 'YOUR_WEBSITE_ID') {
error('Website ID not configured. Please set your Hyvor Talk Website ID.');
return;
}
// Inject styles
injectStyles();
try {
// Load scripts
await Promise.all([
loadHyvorScript(),
loadMembershipsScript()
]);
// Create or get existing memberships element
const memberships = createMembershipsElement();
// Check if API is already available (SPA navigation case)
if (memberships.api) {
log('Memberships API already available (SPA navigation)');
handlePageAccess(memberships);
startLoginPolling(memberships);
} else {
// First load - wait for loaded event
memberships.addEventListener('loaded', () => {
log('Memberships component loaded');
handlePageAccess(memberships);
startLoginPolling(memberships);
});
}
// Listen for subscription changes (new subscriptions)
memberships.addEventListener('subscription:success', (e) => {
log('Subscription successful!', e.detail);
hidePaywall();
if (shouldShowComments()) {
insertComments();
}
});
log('Hyvor Talk initialized successfully');
} catch (err) {
error('Initialization failed:', err);
}
}
// ============================================
// STARTUP
// ============================================
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Handle SPA navigation
let lastUrl = location.href;
let isInitializing = false;
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl && !isInitializing) {
lastUrl = url;
isInitializing = true;
log('Navigation detected, reinitializing...');
// Stop polling to prevent memory leaks
stopLoginPolling();
// Clean up
const wrapper = document.querySelector('.hyvor-talk-wrapper');
if (wrapper) wrapper.remove();
const overlay = document.getElementById('hyvor-paywall-overlay');
if (overlay) overlay.remove();
const content = document.querySelector('.markdown-preview-view');
if (content) content.classList.remove('hyvor-paywall-blur');
// Reinitialize
setTimeout(() => {
init();
isInitializing = false;
}, 200);
}
}).observe(document.body || document, { subtree: true, childList: true });
})();