Estate planning and wealth management experts, Honey Group, were frustrated as their previous system couldn’t keep up with increased data volumes resulting in CRM integration issues and an overall lack of efficiency that impacted productivity. MaxContact’s solution has provided them with the scalability and agility they need to succeed now, and in the future.
Honey Group replaced a fragmented contact centre setup with a single, integrated MaxContact platform to support rapid growth and deliver sharper commercial performance.
The new solution eliminated CRM friction, improved data flow and introduced omnichannel capabilities that connect every customer touchpoint.
It also brough conversation analytics driven quality assurance into daily use, giving managers visibility of every call, every conversation and every opportunity to improve.
The result: clearer operational oversight, more efficient coaching and measurable improvements in conversion and process efficiency.
(function () {
const READ_WPM = 220; // average words per minute
const $readTime = document.getElementById('read-time');
const $progress = document.getElementById('progress-bar');
// Get all rich text elements from case_studies-sections div
function getAllRichTexts() {
const caseStudySection = document.querySelector('.case_studies-sections');
if (!caseStudySection) return [];
return Array.from(caseStudySection.querySelectorAll('.text-rich-text'));
}
const $articles = getAllRichTexts();
if (!$articles.length) return;
// --- Reading time ---
function computeReadingTime() {
let totalText = '';
// Collect text from all rich text elements
$articles.forEach($article => {
const text = ($article.innerText || $article.textContent || '').trim();
totalText += text + ' ';
});
const words = totalText.length ? totalText.split(/\s+/).length : 0;
const minutes = Math.max(1, Math.ceil(words / READ_WPM));
if ($readTime) $readTime.textContent = `${minutes} MIN READ`;
}
// --- Scroll progress ---
let startY = 0;
let endY = 0;
function clamp01(x) { return Math.max(0, Math.min(1, x)); }
function measure() {
// Calculate bounds based on all articles
let minTop = Infinity;
let maxBottom = -Infinity;
$articles.forEach($article => {
const rect = $article.getBoundingClientRect();
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const docTopToArticleTop = rect.top + scrollY;
const articleHeight = $article.offsetHeight;
minTop = Math.min(minTop, docTopToArticleTop);
maxBottom = Math.max(maxBottom, docTopToArticleTop + articleHeight);
});
const viewportH = window.innerHeight || document.documentElement.clientHeight;
startY = minTop - 16;
endY = maxBottom - viewportH + 16;
if (endY < startY) endY = startY + 1;
}
function onScroll() {
if (!$progress) return;
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const t = clamp01((scrollY - startY) / (endY - startY));
$progress.style.width = (t * 100).toFixed(2) + '%';
}
function watchImages() {
$articles.forEach($article => {
const images = $article.querySelectorAll('img');
images.forEach(img => {
if (!img.complete) {
img.addEventListener('load', () => { measure(); onScroll(); }, { once: true });
img.addEventListener('error', () => { measure(); onScroll(); }, { once: true });
}
});
});
}
computeReadingTime();
measure();
watchImages();
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', () => { measure(); onScroll(); });
// Watch for changes in all articles
const mo = new MutationObserver(() => { computeReadingTime(); measure(); onScroll(); });
$articles.forEach($article => {
mo.observe($article, { childList: true, subtree: true, characterData: true });
});
})();
(() => {
const rich = document.querySelector('#rich-text');
const toc = document.querySelector('#toc');
if (!rich || !toc) return;
// Only H2s inside the Rich Text
const headings = [...rich.querySelectorAll('h2')];
if (!headings.length) { toc.style.display = 'none'; return; }
// Slugify + ensure unique IDs (handles accents like šđčćž)
const slugCounts = {};
const slugify = (str) => {
const base = (str || '')
.trim()
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '') // remove diacritics
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
const n = (slugCounts[base] = (slugCounts[base] || 0) + 1);
return n > 1 ? `${base}-${n}` : base || `section-${n}`;
};
// Build anchors directly inside #toc
toc.innerHTML = '';
headings.forEach((h, idx) => {
if (!h.id) h.id = slugify(h.textContent || `section-${idx+1}`);
const a = document.createElement('a');
a.href = `#${h.id}`;
a.className = 'content_link';
a.dataset.target = h.id;
a.setAttribute('aria-label', h.textContent || `Section ${idx+1}`);
const p = document.createElement('p');
p.className = 'text-size-small';
p.textContent = h.textContent || `Section ${idx+1}`;
a.appendChild(p);
toc.appendChild(a);
});
// Offset for fixed navs - with extra spacing for visibility
const getOffset = () => {
const nav = document.querySelector('.navbar, .w-nav, [data-nav]');
const navHeight = nav ? nav.getBoundingClientRect().height : 0;
// Add 30px buffer to ensure heading is clearly visible below fixed navbar
return navHeight + 30;
};
toc.addEventListener('click', (e) => {
const link = e.target.closest('a.content_link[href^="#"]');
if (!link) return;
e.preventDefault();
e.stopPropagation(); // Stop other event listeners
const id = link.getAttribute('href').slice(1);
const target = document.getElementById(id);
if (!target) return;
const targetTop = target.getBoundingClientRect().top + window.scrollY;
const finalY = targetTop - 150;
// Use only smooth scroll
window.scrollTo({ top: finalY, behavior: 'smooth' });
history.replaceState(null, '', `#${id}`);
});
})();
Honey Group is a UK-based specialist in estate planning, later-life and wealth management. Its contact centre team books appointments for a nationwide network of advisers and handles thousands of customer interactions each month.
As the organisation scaled, the business needed to:
- Replace an ageing dialler that could not keep up with lead volumes
- Integrate seamlessly with their CRM to remove manual data handling
- Gain stronger reporting and insight across all teams
- Introduce new communication channels such as webchat and email
- Improve call quality, compliance and coaching visibility
The project covered dialler replacement, CRM/API integration, omnichannel rollout, automated reporting and conversation analytics for quality assurance, all within one MaxContact platform.
(function () {
const READ_WPM = 220; // average words per minute
const $readTime = document.getElementById('read-time');
const $progress = document.getElementById('progress-bar');
// Get all rich text elements from case_studies-sections div
function getAllRichTexts() {
const caseStudySection = document.querySelector('.case_studies-sections');
if (!caseStudySection) return [];
return Array.from(caseStudySection.querySelectorAll('.text-rich-text'));
}
const $articles = getAllRichTexts();
if (!$articles.length) return;
// --- Reading time ---
function computeReadingTime() {
let totalText = '';
// Collect text from all rich text elements
$articles.forEach($article => {
const text = ($article.innerText || $article.textContent || '').trim();
totalText += text + ' ';
});
const words = totalText.length ? totalText.split(/\s+/).length : 0;
const minutes = Math.max(1, Math.ceil(words / READ_WPM));
if ($readTime) $readTime.textContent = `${minutes} MIN READ`;
}
// --- Scroll progress ---
let startY = 0;
let endY = 0;
function clamp01(x) { return Math.max(0, Math.min(1, x)); }
function measure() {
// Calculate bounds based on all articles
let minTop = Infinity;
let maxBottom = -Infinity;
$articles.forEach($article => {
const rect = $article.getBoundingClientRect();
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const docTopToArticleTop = rect.top + scrollY;
const articleHeight = $article.offsetHeight;
minTop = Math.min(minTop, docTopToArticleTop);
maxBottom = Math.max(maxBottom, docTopToArticleTop + articleHeight);
});
const viewportH = window.innerHeight || document.documentElement.clientHeight;
startY = minTop - 16;
endY = maxBottom - viewportH + 16;
if (endY < startY) endY = startY + 1;
}
function onScroll() {
if (!$progress) return;
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const t = clamp01((scrollY - startY) / (endY - startY));
$progress.style.width = (t * 100).toFixed(2) + '%';
}
function watchImages() {
$articles.forEach($article => {
const images = $article.querySelectorAll('img');
images.forEach(img => {
if (!img.complete) {
img.addEventListener('load', () => { measure(); onScroll(); }, { once: true });
img.addEventListener('error', () => { measure(); onScroll(); }, { once: true });
}
});
});
}
computeReadingTime();
measure();
watchImages();
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', () => { measure(); onScroll(); });
// Watch for changes in all articles
const mo = new MutationObserver(() => { computeReadingTime(); measure(); onScroll(); });
$articles.forEach($article => {
mo.observe($article, { childList: true, subtree: true, characterData: true });
});
})();
(() => {
const rich = document.querySelector('#rich-text');
const toc = document.querySelector('#toc');
if (!rich || !toc) return;
// Only H2s inside the Rich Text
const headings = [...rich.querySelectorAll('h2')];
if (!headings.length) { toc.style.display = 'none'; return; }
// Slugify + ensure unique IDs (handles accents like šđčćž)
const slugCounts = {};
const slugify = (str) => {
const base = (str || '')
.trim()
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '') // remove diacritics
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
const n = (slugCounts[base] = (slugCounts[base] || 0) + 1);
return n > 1 ? `${base}-${n}` : base || `section-${n}`;
};
// Build anchors directly inside #toc
toc.innerHTML = '';
headings.forEach((h, idx) => {
if (!h.id) h.id = slugify(h.textContent || `section-${idx+1}`);
const a = document.createElement('a');
a.href = `#${h.id}`;
a.className = 'content_link';
a.dataset.target = h.id;
a.setAttribute('aria-label', h.textContent || `Section ${idx+1}`);
const p = document.createElement('p');
p.className = 'text-size-small';
p.textContent = h.textContent || `Section ${idx+1}`;
a.appendChild(p);
toc.appendChild(a);
});
// Offset for fixed navs - with extra spacing for visibility
const getOffset = () => {
const nav = document.querySelector('.navbar, .w-nav, [data-nav]');
const navHeight = nav ? nav.getBoundingClientRect().height : 0;
// Add 30px buffer to ensure heading is clearly visible below fixed navbar
return navHeight + 30;
};
toc.addEventListener('click', (e) => {
const link = e.target.closest('a.content_link[href^="#"]');
if (!link) return;
e.preventDefault();
e.stopPropagation(); // Stop other event listeners
const id = link.getAttribute('href').slice(1);
const target = document.getElementById(id);
if (!target) return;
const targetTop = target.getBoundingClientRect().top + window.scrollY;
const finalY = targetTop - 150;
// Use only smooth scroll
window.scrollTo({ top: finalY, behavior: 'smooth' });
history.replaceState(null, '', `#${id}`);
});
})();
Growth was outpacing the tools in place, and Honey Group’s previous systems created more friction than flow.
- Data issues: large lead files and manual imports slowed down daily operations
- Limited CRM connectivity: broken integrations caused duplication and delays
- Manual reporting: leaders relied on spreadsheets and ad hoc updates
- Restricted quality assurance coverage: the team could listen to only a small percentage of calls, missing key compliance and coaching opportunities
These pain points limited efficiency and made it difficult for managers to gain a clear, consistent view of performance.
(function () {
const READ_WPM = 220; // average words per minute
const $readTime = document.getElementById('read-time');
const $progress = document.getElementById('progress-bar');
// Get all rich text elements from case_studies-sections div
function getAllRichTexts() {
const caseStudySection = document.querySelector('.case_studies-sections');
if (!caseStudySection) return [];
return Array.from(caseStudySection.querySelectorAll('.text-rich-text'));
}
const $articles = getAllRichTexts();
if (!$articles.length) return;
// --- Reading time ---
function computeReadingTime() {
let totalText = '';
// Collect text from all rich text elements
$articles.forEach($article => {
const text = ($article.innerText || $article.textContent || '').trim();
totalText += text + ' ';
});
const words = totalText.length ? totalText.split(/\s+/).length : 0;
const minutes = Math.max(1, Math.ceil(words / READ_WPM));
if ($readTime) $readTime.textContent = `${minutes} MIN READ`;
}
// --- Scroll progress ---
let startY = 0;
let endY = 0;
function clamp01(x) { return Math.max(0, Math.min(1, x)); }
function measure() {
// Calculate bounds based on all articles
let minTop = Infinity;
let maxBottom = -Infinity;
$articles.forEach($article => {
const rect = $article.getBoundingClientRect();
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const docTopToArticleTop = rect.top + scrollY;
const articleHeight = $article.offsetHeight;
minTop = Math.min(minTop, docTopToArticleTop);
maxBottom = Math.max(maxBottom, docTopToArticleTop + articleHeight);
});
const viewportH = window.innerHeight || document.documentElement.clientHeight;
startY = minTop - 16;
endY = maxBottom - viewportH + 16;
if (endY < startY) endY = startY + 1;
}
function onScroll() {
if (!$progress) return;
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const t = clamp01((scrollY - startY) / (endY - startY));
$progress.style.width = (t * 100).toFixed(2) + '%';
}
function watchImages() {
$articles.forEach($article => {
const images = $article.querySelectorAll('img');
images.forEach(img => {
if (!img.complete) {
img.addEventListener('load', () => { measure(); onScroll(); }, { once: true });
img.addEventListener('error', () => { measure(); onScroll(); }, { once: true });
}
});
});
}
computeReadingTime();
measure();
watchImages();
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', () => { measure(); onScroll(); });
// Watch for changes in all articles
const mo = new MutationObserver(() => { computeReadingTime(); measure(); onScroll(); });
$articles.forEach($article => {
mo.observe($article, { childList: true, subtree: true, characterData: true });
});
})();
(() => {
const rich = document.querySelector('#rich-text');
const toc = document.querySelector('#toc');
if (!rich || !toc) return;
// Only H2s inside the Rich Text
const headings = [...rich.querySelectorAll('h2')];
if (!headings.length) { toc.style.display = 'none'; return; }
// Slugify + ensure unique IDs (handles accents like šđčćž)
const slugCounts = {};
const slugify = (str) => {
const base = (str || '')
.trim()
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '') // remove diacritics
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
const n = (slugCounts[base] = (slugCounts[base] || 0) + 1);
return n > 1 ? `${base}-${n}` : base || `section-${n}`;
};
// Build anchors directly inside #toc
toc.innerHTML = '';
headings.forEach((h, idx) => {
if (!h.id) h.id = slugify(h.textContent || `section-${idx+1}`);
const a = document.createElement('a');
a.href = `#${h.id}`;
a.className = 'content_link';
a.dataset.target = h.id;
a.setAttribute('aria-label', h.textContent || `Section ${idx+1}`);
const p = document.createElement('p');
p.className = 'text-size-small';
p.textContent = h.textContent || `Section ${idx+1}`;
a.appendChild(p);
toc.appendChild(a);
});
// Offset for fixed navs - with extra spacing for visibility
const getOffset = () => {
const nav = document.querySelector('.navbar, .w-nav, [data-nav]');
const navHeight = nav ? nav.getBoundingClientRect().height : 0;
// Add 30px buffer to ensure heading is clearly visible below fixed navbar
return navHeight + 30;
};
toc.addEventListener('click', (e) => {
const link = e.target.closest('a.content_link[href^="#"]');
if (!link) return;
e.preventDefault();
e.stopPropagation(); // Stop other event listeners
const id = link.getAttribute('href').slice(1);
const target = document.getElementById(id);
if (!target) return;
const targetTop = target.getBoundingClientRect().top + window.scrollY;
const finalY = targetTop - 150;
// Use only smooth scroll
window.scrollTo({ top: finalY, behavior: 'smooth' });
history.replaceState(null, '', `#${id}`);
});
})();
Honey Group adopted MaxContact to bring everything into one connected platform.
The new setup included:
- Integrated dialler and CRM/API connection to ensure data flows automatically and accurately between systems
- Omnichannel capability blending voice, webchat and email for a smoother customer experience
- Automated daily reports so managers start the day with up-to-date dashboards instead of manual data pulls
- Speech analytics and automated conversation review that transcribes and summarises 100% of calls to detect topics, sentiment and potential compliance risks
Implementation was guided by MaxContact’s professional services team, who supported the setup, built custom reports and trained staff to make full use of the new platform.
“MaxContact’s support team was fantastic. They helped us build the custom reports we needed.”
Karl Burke, Contact Centre Manager
(function () {
const READ_WPM = 220; // average words per minute
const $readTime = document.getElementById('read-time');
const $progress = document.getElementById('progress-bar');
// Get all rich text elements from case_studies-sections div
function getAllRichTexts() {
const caseStudySection = document.querySelector('.case_studies-sections');
if (!caseStudySection) return [];
return Array.from(caseStudySection.querySelectorAll('.text-rich-text'));
}
const $articles = getAllRichTexts();
if (!$articles.length) return;
// --- Reading time ---
function computeReadingTime() {
let totalText = '';
// Collect text from all rich text elements
$articles.forEach($article => {
const text = ($article.innerText || $article.textContent || '').trim();
totalText += text + ' ';
});
const words = totalText.length ? totalText.split(/\s+/).length : 0;
const minutes = Math.max(1, Math.ceil(words / READ_WPM));
if ($readTime) $readTime.textContent = `${minutes} MIN READ`;
}
// --- Scroll progress ---
let startY = 0;
let endY = 0;
function clamp01(x) { return Math.max(0, Math.min(1, x)); }
function measure() {
// Calculate bounds based on all articles
let minTop = Infinity;
let maxBottom = -Infinity;
$articles.forEach($article => {
const rect = $article.getBoundingClientRect();
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const docTopToArticleTop = rect.top + scrollY;
const articleHeight = $article.offsetHeight;
minTop = Math.min(minTop, docTopToArticleTop);
maxBottom = Math.max(maxBottom, docTopToArticleTop + articleHeight);
});
const viewportH = window.innerHeight || document.documentElement.clientHeight;
startY = minTop - 16;
endY = maxBottom - viewportH + 16;
if (endY < startY) endY = startY + 1;
}
function onScroll() {
if (!$progress) return;
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const t = clamp01((scrollY - startY) / (endY - startY));
$progress.style.width = (t * 100).toFixed(2) + '%';
}
function watchImages() {
$articles.forEach($article => {
const images = $article.querySelectorAll('img');
images.forEach(img => {
if (!img.complete) {
img.addEventListener('load', () => { measure(); onScroll(); }, { once: true });
img.addEventListener('error', () => { measure(); onScroll(); }, { once: true });
}
});
});
}
computeReadingTime();
measure();
watchImages();
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', () => { measure(); onScroll(); });
// Watch for changes in all articles
const mo = new MutationObserver(() => { computeReadingTime(); measure(); onScroll(); });
$articles.forEach($article => {
mo.observe($article, { childList: true, subtree: true, characterData: true });
});
})();
(() => {
const rich = document.querySelector('#rich-text');
const toc = document.querySelector('#toc');
if (!rich || !toc) return;
// Only H2s inside the Rich Text
const headings = [...rich.querySelectorAll('h2')];
if (!headings.length) { toc.style.display = 'none'; return; }
// Slugify + ensure unique IDs (handles accents like šđčćž)
const slugCounts = {};
const slugify = (str) => {
const base = (str || '')
.trim()
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '') // remove diacritics
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
const n = (slugCounts[base] = (slugCounts[base] || 0) + 1);
return n > 1 ? `${base}-${n}` : base || `section-${n}`;
};
// Build anchors directly inside #toc
toc.innerHTML = '';
headings.forEach((h, idx) => {
if (!h.id) h.id = slugify(h.textContent || `section-${idx+1}`);
const a = document.createElement('a');
a.href = `#${h.id}`;
a.className = 'content_link';
a.dataset.target = h.id;
a.setAttribute('aria-label', h.textContent || `Section ${idx+1}`);
const p = document.createElement('p');
p.className = 'text-size-small';
p.textContent = h.textContent || `Section ${idx+1}`;
a.appendChild(p);
toc.appendChild(a);
});
// Offset for fixed navs - with extra spacing for visibility
const getOffset = () => {
const nav = document.querySelector('.navbar, .w-nav, [data-nav]');
const navHeight = nav ? nav.getBoundingClientRect().height : 0;
// Add 30px buffer to ensure heading is clearly visible below fixed navbar
return navHeight + 30;
};
toc.addEventListener('click', (e) => {
const link = e.target.closest('a.content_link[href^="#"]');
if (!link) return;
e.preventDefault();
e.stopPropagation(); // Stop other event listeners
const id = link.getAttribute('href').slice(1);
const target = document.getElementById(id);
if (!target) return;
const targetTop = target.getBoundingClientRect().top + window.scrollY;
const finalY = targetTop - 150;
// Use only smooth scroll
window.scrollTo({ top: finalY, behavior: 'smooth' });
history.replaceState(null, '', `#${id}`);
});
})();
The benefits were immediate and measurable.
Greater visibility and quality assurance efficiency
Managers can now review and analyse far more conversations without expanding the QA team. Automatic transcription and topic tagging surface key insights quickly.
Improved conversion and coaching
Sales and operations teams use conversation insights to identify what works best and replicate high-performing behaviours, helping to improve conversion from roughly one in sixteen calls to one in fifteen.
Stronger operational control
With automated reporting and accurate CRM data, Honey Group’s leadership can spot trends and act on issues before they escalate.
Future plans
Honey Group intends to deepen CRM integration, expand omnichannel use and explore real-time analytics to further enhance efficiency and customer experience.
(function () {
const READ_WPM = 220; // average words per minute
const $readTime = document.getElementById('read-time');
const $progress = document.getElementById('progress-bar');
// Get all rich text elements from case_studies-sections div
function getAllRichTexts() {
const caseStudySection = document.querySelector('.case_studies-sections');
if (!caseStudySection) return [];
return Array.from(caseStudySection.querySelectorAll('.text-rich-text'));
}
const $articles = getAllRichTexts();
if (!$articles.length) return;
// --- Reading time ---
function computeReadingTime() {
let totalText = '';
// Collect text from all rich text elements
$articles.forEach($article => {
const text = ($article.innerText || $article.textContent || '').trim();
totalText += text + ' ';
});
const words = totalText.length ? totalText.split(/\s+/).length : 0;
const minutes = Math.max(1, Math.ceil(words / READ_WPM));
if ($readTime) $readTime.textContent = `${minutes} MIN READ`;
}
// --- Scroll progress ---
let startY = 0;
let endY = 0;
function clamp01(x) { return Math.max(0, Math.min(1, x)); }
function measure() {
// Calculate bounds based on all articles
let minTop = Infinity;
let maxBottom = -Infinity;
$articles.forEach($article => {
const rect = $article.getBoundingClientRect();
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const docTopToArticleTop = rect.top + scrollY;
const articleHeight = $article.offsetHeight;
minTop = Math.min(minTop, docTopToArticleTop);
maxBottom = Math.max(maxBottom, docTopToArticleTop + articleHeight);
});
const viewportH = window.innerHeight || document.documentElement.clientHeight;
startY = minTop - 16;
endY = maxBottom - viewportH + 16;
if (endY < startY) endY = startY + 1;
}
function onScroll() {
if (!$progress) return;
const scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
const t = clamp01((scrollY - startY) / (endY - startY));
$progress.style.width = (t * 100).toFixed(2) + '%';
}
function watchImages() {
$articles.forEach($article => {
const images = $article.querySelectorAll('img');
images.forEach(img => {
if (!img.complete) {
img.addEventListener('load', () => { measure(); onScroll(); }, { once: true });
img.addEventListener('error', () => { measure(); onScroll(); }, { once: true });
}
});
});
}
computeReadingTime();
measure();
watchImages();
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', () => { measure(); onScroll(); });
// Watch for changes in all articles
const mo = new MutationObserver(() => { computeReadingTime(); measure(); onScroll(); });
$articles.forEach($article => {
mo.observe($article, { childList: true, subtree: true, characterData: true });
});
})();
(() => {
const rich = document.querySelector('#rich-text');
const toc = document.querySelector('#toc');
if (!rich || !toc) return;
// Only H2s inside the Rich Text
const headings = [...rich.querySelectorAll('h2')];
if (!headings.length) { toc.style.display = 'none'; return; }
// Slugify + ensure unique IDs (handles accents like šđčćž)
const slugCounts = {};
const slugify = (str) => {
const base = (str || '')
.trim()
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '') // remove diacritics
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
const n = (slugCounts[base] = (slugCounts[base] || 0) + 1);
return n > 1 ? `${base}-${n}` : base || `section-${n}`;
};
// Build anchors directly inside #toc
toc.innerHTML = '';
headings.forEach((h, idx) => {
if (!h.id) h.id = slugify(h.textContent || `section-${idx+1}`);
const a = document.createElement('a');
a.href = `#${h.id}`;
a.className = 'content_link';
a.dataset.target = h.id;
a.setAttribute('aria-label', h.textContent || `Section ${idx+1}`);
const p = document.createElement('p');
p.className = 'text-size-small';
p.textContent = h.textContent || `Section ${idx+1}`;
a.appendChild(p);
toc.appendChild(a);
});
// Offset for fixed navs - with extra spacing for visibility
const getOffset = () => {
const nav = document.querySelector('.navbar, .w-nav, [data-nav]');
const navHeight = nav ? nav.getBoundingClientRect().height : 0;
// Add 30px buffer to ensure heading is clearly visible below fixed navbar
return navHeight + 30;
};
toc.addEventListener('click', (e) => {
const link = e.target.closest('a.content_link[href^="#"]');
if (!link) return;
e.preventDefault();
e.stopPropagation(); // Stop other event listeners
const id = link.getAttribute('href').slice(1);
const target = document.getElementById(id);
if (!target) return;
const targetTop = target.getBoundingClientRect().top + window.scrollY;
const finalY = targetTop - 150;
// Use only smooth scroll
window.scrollTo({ top: finalY, behavior: 'smooth' });
history.replaceState(null, '', `#${id}`);
});
})();