import { test } from 'node:test'
import assert from 'node:assert/strict'
import { delta, fmtDuration, fmtNum, dayLabel, periodLabel, compareFrom, contactRate, refName, weekdayStats, summarize, chartModel } from '../public/analyse-view.js'
// Testdaten: eine vollstaendige Statistik-Antwort mit 30 Tagen (Form wie analytics/src/stats.js)
const isoDay = (from, n) => new Date(Date.UTC(+from.slice(0, 4), +from.slice(5, 7) - 1, +from.slice(8, 10) + n)).toISOString().slice(0, 10)
const days = (from, n, pv) => Array.from({ length: n }, (_, i) => ({ day: isoDay(from, i), pv: typeof pv === 'function' ? pv(i, isoDay(from, i)) : pv }))
const t = { pv: 6930, visitors: 2410, contacts: 188, consentRate: 0.61, engaged: 1800, avgSec: 108, avgScroll: 64 }
const prev = {
from: '2026-08-06', to: '2026-09-04', complete: true,
totals: { pv: 6080, visitors: 2200, contacts: 143, consentRate: 0.6, engaged: 1500, avgSec: 114, avgScroll: 60 },
series: days('2026-08-06', 30, 203),
contacts: [{ key: 'tel', count: 80 }, { key: 'mail', count: 40 }, { key: 'wa', count: 23 }],
}
const stats = (over = {}) => ({
range: 30, from: '2026-09-05', to: '2026-10-04', firstDay: '2026-07-01',
today: { day: '2026-10-05', pv: 42, visitors: 20, contacts: 1 },
totals: t,
series: days('2026-09-05', 30, 231),
pages: [{ key: '/', count: 3200 }, { key: '/preise', count: 1310 }, { key: '/kontakt', count: 900 }],
refs: [{ key: 'google.com', count: 880 }, { key: '', count: 700 }, { key: 'instagram.com', count: 400 }, { key: 'l.facebook.com', count: 290 }],
countries: [{ key: 'DE', count: 6000 }],
devices: [{ key: 'm', count: 4590 }, { key: 'd', count: 2200 }, { key: 't', count: 140 }],
contacts: [{ key: 'tel', count: 96 }, { key: 'mail', count: 52 }, { key: 'wa', count: 30 }, { key: 'out', count: 10 }],
banner: { shown: 3950, accept: 2410, reject: 1540 },
previous: prev,
...over,
})
test('delta: Prozent ab 100, sonst absolut, flat, Sekunden, null', () => {
assert.deepEqual(delta(6930, 6080), { dir: 'up', text: '▲ 14 %', aria: 'plus 14 Prozent gegenüber dem Zeitraum davor' })
assert.equal(delta(188, 143).text, '▲ 31 %') // beide ≥ 100 → Prozent
assert.equal(delta(45, 33).text, '▲ 12')
assert.equal(delta(30, 33).text, '▼ 3')
assert.deepEqual(delta(7, 7), { dir: 'flat', text: '± 0', aria: 'unverändert gegenüber dem Zeitraum davor' })
assert.equal(delta(1004, 1000).text, '± 0 %') // |0,4 %| < 1
assert.equal(delta(1004, 1000).dir, 'flat')
assert.equal(delta(108, 114, { kind: 'sec' }).text, '▼ 6 s')
assert.equal(delta(200, 135, { kind: 'sec' }).text, '▲ 1:05')
assert.equal(delta(null, 3), null)
assert.equal(delta(3, undefined), null)
assert.equal(delta('3', 4), null)
})
test('delta: Richtung und aria in Wörtern', () => {
assert.deepEqual(delta(5000, 6000), { dir: 'down', text: '▼ 17 %', aria: 'minus 17 Prozent gegenüber dem Zeitraum davor' })
assert.equal(delta(45, 33).aria, 'plus 12 gegenüber dem Zeitraum davor')
assert.equal(delta(30, 33).aria, 'minus 3 gegenüber dem Zeitraum davor')
assert.equal(delta(108, 114, { kind: 'sec' }).aria, 'minus 6 Sekunden gegenüber dem Zeitraum davor')
assert.equal(delta(200, 135, { kind: 'sec' }).aria, 'plus 1 Minute 5 Sekunden gegenüber dem Zeitraum davor')
assert.equal(delta(240, 120, { kind: 'sec' }).aria, 'plus 2 Minuten gegenüber dem Zeitraum davor')
})
test('fmtDuration, dayLabel, periodLabel, fmtNum', () => {
assert.equal(fmtDuration(108), '1:48'); assert.equal(fmtDuration(7), '0:07'); assert.equal(fmtDuration(null), '–')
assert.equal(fmtDuration(NaN), '–'); assert.equal(fmtDuration(-3), '–'); assert.equal(fmtDuration(undefined), '–')
assert.equal(dayLabel('2026-09-19'), 'Sa., 19.09.')
assert.equal(dayLabel('2026-09-20'), 'So., 20.09.')
assert.equal(periodLabel('2026-09-05', '2026-10-04'), '5. Sep. – 4. Okt.')
assert.equal(periodLabel('2026-03-01', '2026-06-30'), '1. März – 30. Juni')
assert.equal(periodLabel('', ''), '')
assert.equal(periodLabel('2026-09-05', ''), '')
assert.equal(fmtNum(6930), '6.930')
assert.equal(fmtNum(0), '0')
assert.equal(fmtNum(1234567), '1.234.567')
})
test('compareFrom: ok, Datum bei neuer Website, nichts ohne previous', () => {
assert.deepEqual(compareFrom(stats()), { ok: true, from: '' })
assert.deepEqual(compareFrom(stats({ firstDay: '2026-09-20', previous: { ...prev, complete: false } })), { ok: false, from: '19.11.' }) // range 30: +60 Tage (2 × range, der erste Tag mit vollem Vorzeitraum)
assert.deepEqual(compareFrom(stats({ range: 7, firstDay: '2026-09-28', previous: { ...prev, complete: false } })), { ok: false, from: '12.10.' }) // range 7: +14 Tage
assert.deepEqual(compareFrom(stats({ firstDay: '', previous: { ...prev, complete: false } })), { ok: false, from: '' })
assert.deepEqual(compareFrom({ totals: { pv: 50 }, series: [] }), { ok: false, from: '' })
assert.equal(compareFrom(stats({ previous: { ...prev, totals: { ...prev.totals, pv: 19 } } })).ok, false)
assert.equal(compareFrom(stats({ previous: { ...prev, totals: { ...prev.totals, pv: 20 } } })).ok, true)
})
test('contactRate und refName', () => {
assert.equal(contactRate(188, 2410), 7.8); assert.equal(contactRate(5, 29), null)
assert.equal(contactRate(0, 30), 0); assert.equal(contactRate(null, 100), null)
assert.equal(refName('www.google.de'), 'Google'); assert.equal(refName('l.facebook.com'), 'Facebook'); assert.equal(refName(''), 'Direkt oder unbekannt'); assert.equal(refName('blog.example'), 'blog.example')
assert.equal(refName('google.com'), 'Google'); assert.equal(refName('google.co.uk'), 'Google')
assert.equal(refName('www.bing.com'), 'Bing'); assert.equal(refName('l.instagram.com'), 'Instagram')
assert.equal(refName('m.facebook.com'), 'Facebook'); assert.equal(refName('facebook.com'), 'Facebook')
assert.equal(refName('duckduckgo.com'), 'DuckDuckGo'); assert.equal(refName('www.ecosia.org'), 'Ecosia')
assert.equal(refName('notgoogle.com'), 'notgoogle.com'); assert.equal(refName('google.evil.de'), 'google.evil.de')
assert.equal(refName('Google.DE'), 'Google')
})
test('weekdayStats: stärkster Wochentag, Spitzentag, < 7 Tage null', () => {
// 05.09.2026 ist ein Samstag: 5 Samstage mit 20, alle anderen 10 → Schnitt 11,67; Samstag 20 ≥ 1,2 × 11,67, aber < 1,8 × 11,67 (kein Spitzentag)
const sat = (i, day) => (new Date(day + 'T00:00:00Z').getUTCDay() === 6 ? 20 : 10)
const w = weekdayStats(days('2026-09-05', 30, sat), '')
assert.equal(w.best.weekday, 6); assert.equal(w.best.name, 'Samstag'); assert.equal(w.best.avg, 20)
assert.ok(Math.abs(w.avg - 350 / 30) < 1e-9)
assert.equal(w.peak, null)
// ein Tag mit dem 3-Fachen des Schnitts → Spitzentag
const spike = (i, day) => (day === '2026-09-19' ? 60 : 10)
const p = weekdayStats(days('2026-09-05', 30, spike), '')
assert.deepEqual(p.peak, { day: '2026-09-19', pv: 60 })
assert.equal(p.best.name, 'Samstag')
// gleichmäßig: weder stärkster Tag noch Spitzentag
assert.deepEqual(weekdayStats(days('2026-09-05', 30, 10), ''), { best: null, avg: 10, peak: null })
// weniger als 7 Tage → null; firstDay schneidet Tage davor ab
assert.equal(weekdayStats(days('2026-09-05', 6, 10), ''), null)
assert.notEqual(weekdayStats(days('2026-09-05', 30, 10), '2026-09-28'), null) // 28.09.–04.10. = genau 7 Tage
})
test('weekdayStats: firstDay zählt nur Tage ab firstDay', () => {
assert.equal(weekdayStats(days('2026-09-05', 30, 10), '2026-09-29'), null) // nur 6 Tage (29.09.–04.10.)
const w = weekdayStats(days('2026-09-05', 30, (i, day) => (day < '2026-09-20' ? 1000 : 10)), '2026-09-20')
assert.equal(w.avg, 10) // die Tage davor (1000) bleiben außen vor
assert.equal(weekdayStats(null, ''), null)
})
test('summarize: Wachstum mit Prozent', () => {
const s = summarize(stats())
assert.equal(s.lead, 'Deine Website wurde in den letzten 30 Tagen 6.930-mal aufgerufen – 14 % mehr als in den 30 Tagen davor.')
assert.equal(s.items[0].icon, 'contact')
assert.equal(s.items[0].html, '178-mal hat dich jemand direkt kontaktiert, meist per Telefon (96). Das sind 35 mehr als zuvor.') // Telefon + E-Mail + WhatsApp: 96 + 52 + 30 gegen 80 + 40 + 23
assert.ok(s.items.length <= 4)
})
test('summarize: Rückgang, etwa gleich, kleine Zahlen absolut', () => {
const lead = (totalsPv, prevPv) => summarize(stats({ totals: { ...t, pv: totalsPv }, previous: { ...prev, totals: { ...prev.totals, pv: prevPv } } })).lead
assert.equal(lead(6930, 7615), 'Deine Website wurde in den letzten 30 Tagen 6.930-mal aufgerufen – 9 % weniger als in den 30 Tagen davor.')
assert.equal(lead(6930, 6800), 'Deine Website wurde in den letzten 30 Tagen 6.930-mal aufgerufen – etwa so oft wie in den 30 Tagen davor.')
assert.equal(lead(80, 68), 'Deine Website wurde in den letzten 30 Tagen 80-mal aufgerufen – 12 Aufrufe mehr als in den 30 Tagen davor.')
assert.equal(lead(60, 75), 'Deine Website wurde in den letzten 30 Tagen 60-mal aufgerufen – 15 Aufrufe weniger als in den 30 Tagen davor.')
assert.equal(lead(84, 83), 'Deine Website wurde in den letzten 30 Tagen 84-mal aufgerufen – etwa so oft wie in den 30 Tagen davor.') // unter 5 % gilt auch bei kleinen Zahlen
assert.match(lead(32, 31), /aufgerufen – etwa so oft wie in den 30 Tagen davor\.$/)
assert.equal(lead(40, 30), 'Deine Website wurde in den letzten 30 Tagen 40-mal aufgerufen – 10 Aufrufe mehr als in den 30 Tagen davor.')
assert.equal(lead(31, 20), 'Deine Website wurde in den letzten 30 Tagen 31-mal aufgerufen – 11 Aufrufe mehr als in den 30 Tagen davor.')
assert.equal(lead(80, 80), 'Deine Website wurde in den letzten 30 Tagen 80-mal aufgerufen – etwa so oft wie in den 30 Tagen davor.')
assert.equal(lead(7200, 6857), 'Deine Website wurde in den letzten 30 Tagen 7.200-mal aufgerufen – 5 % mehr als in den 30 Tagen davor.') // genau 5 % gilt noch als Veränderung
})
test('summarize: ohne Vergleich und neu gestartet', () => {
// previous fehlt: '…in den letzten 30 Tagen X-mal aufgerufen.'
const { previous, ...ohne } = stats()
assert.equal(summarize(ohne).lead, 'Deine Website wurde in den letzten 30 Tagen 6.930-mal aufgerufen.')
// Vorzeitraum unvollständig, firstDay vor dem Zeitraum: kein Vergleich, aber kein „Seit dem Start“
assert.equal(summarize(stats({ firstDay: '2026-08-20', previous: { ...prev, complete: false } })).lead, 'Deine Website wurde in den letzten 30 Tagen 6.930-mal aufgerufen.')
// Vorzeitraum zu klein: kein Vergleich
assert.equal(summarize(stats({ previous: { ...prev, totals: { ...prev.totals, pv: 12 } } })).lead, 'Deine Website wurde in den letzten 30 Tagen 6.930-mal aufgerufen.')
// firstDay im Zeitraum (complete false): 'Seit dem Start der Analyse am 20.09. wurde deine Website X-mal aufgerufen.'
const neu = summarize(stats({ firstDay: '2026-09-20', previous: { ...prev, complete: false } }))
assert.equal(neu.lead, 'Seit dem Start der Analyse am 20.09. wurde deine Website 6.930-mal aufgerufen.')
// auch das Startdatum wird maskiert, falls der Collector je Unsinn liefert
assert.ok(!summarize(stats({ firstDay: 'x', previous: { ...prev, complete: false } })).lead.includes(''))
// ohne Vergleich kein „Das sind … mehr als zuvor“
assert.equal(neu.items[0].html, '178-mal hat dich jemand direkt kontaktiert, meist per Telefon (96).')
// 7 Tage: „7 Tagen“
assert.match(summarize(stats({ range: 7 })).lead, /in den letzten 7 Tagen .* als in den 7 Tagen davor\.$/)
})
test('summarize: zu wenig Daten → null; keine Kontakte → Hinweis nur ab 30 Besuchern', () => {
assert.equal(summarize(stats({ totals: { ...t, pv: 29 } })), null)
assert.equal(summarize(null), null)
assert.notEqual(summarize(stats({ totals: { ...t, pv: 30 } })), null)
// contacts 0, visitors 40: 'Von den Besuchern mit Zustimmung hat noch niemand über die Website angerufen oder geschrieben.'
const keine = summarize(stats({ totals: { ...t, contacts: 0, visitors: 40 }, contacts: [] }))
assert.deepEqual(keine.items[0], { icon: 'contact', html: 'Von den Besuchern mit Zustimmung hat noch niemand über die Website angerufen oder geschrieben.' })
const wenig = summarize(stats({ totals: { ...t, contacts: 0, visitors: 29 }, contacts: [] }))
assert.notEqual(wenig.items[0].icon, 'contact')
})
test('summarize: Kontakte – Vergleichssatz und häufigste Art, Links nach außen zählen nicht mit', () => {
const tel = (n) => ({ key: 'tel', count: n })
const html = (kinds, before) => summarize(stats({
totals: { ...t, contacts: kinds.reduce((a, k) => a + k.count, 0) },
contacts: kinds,
previous: { ...prev, contacts: before, totals: { ...prev.totals, contacts: before.reduce((a, k) => a + k.count, 0) } },
})).items[0].html
assert.equal(html([tel(96)], [tel(108)]), '96-mal hat dich jemand direkt kontaktiert, meist per Telefon (96). Das sind 12 weniger als zuvor.')
assert.equal(html([{ key: 'mail', count: 100 }], [{ key: 'mail', count: 100 }]), '100-mal hat dich jemand direkt kontaktiert, meist per E-Mail (100). Genauso oft wie zuvor.')
assert.equal(html([{ key: 'wa', count: 5 }], []), '5-mal hat dich jemand direkt kontaktiert, meist per WhatsApp (5).') // Vorwert 0: kein Vergleichssatz
assert.equal(html([tel(5)], [tel(2)]), '5-mal hat dich jemand direkt kontaktiert, meist per Telefon (5). Das sind 3 mehr als zuvor.')
// Links nach außen sind Klicks auf andere Seiten, kein Kontakt: weder in der Zahl noch als häufigste Art, auch nicht im Vorzeitraum
assert.equal(html([{ key: 'out', count: 300 }, tel(40)], [{ key: 'out', count: 900 }, tel(40)]), '40-mal hat dich jemand direkt kontaktiert, meist per Telefon (40). Genauso oft wie zuvor.')
assert.equal(html([{ key: 'out', count: 300 }, tel(40)], [{ key: 'out', count: 900 }]), '40-mal hat dich jemand direkt kontaktiert, meist per Telefon (40).')
// nur Links nach außen (Besucher ≥ 30): wie „keine Kontakte“
const nurOut = summarize(stats({ totals: { ...t, contacts: 300, visitors: 400 }, contacts: [{ key: 'out', count: 300 }] }))
assert.deepEqual(nurOut.items[0], { icon: 'contact', html: 'Von den Besuchern mit Zustimmung hat noch niemand über die Website angerufen oder geschrieben.' })
// Collector ohne Kontaktliste: kein Satz, der etwas Falsches behauptet
const { contacts, ...ohneListe } = stats({ totals: { ...t, contacts: 0, visitors: 400 } })
assert.notEqual(summarize(ohneListe).items[0].icon, 'contact')
})
test('summarize: Herkunft, Gerät, Seite und Reihenfolge, höchstens 4', () => {
// refs google.com 880 von 2270 → 'Die meisten, die über einen Link kamen, kommen von Google (880).'
const s = summarize(stats())
const byIcon = Object.fromEntries(s.items.map((i) => [i.icon, i.html]))
assert.equal(byIcon.source, 'Die meisten, die über einen Link kamen, kommen von Google (880).')
// devices m 4590/6930 → '66 % der Aufrufe kommen vom Handy.'
assert.equal(byIcon.device, '66 % der Aufrufe kommen vom Handy.')
// pages / dann /preise → 'Am meisten angesehen: die Startseite, danach /preise (1.310).'
assert.equal(byIcon.page, 'Am meisten angesehen: die Startseite, danach /preise (1.310).')
assert.deepEqual(s.items.map((i) => i.icon), ['contact', 'source', 'device', 'page'])
// Computer, Seite ohne Startseite
const pc = summarize(stats({ devices: [{ key: 'd', count: 600 }, { key: 'm', count: 300 }, { key: 't', count: 100 }], pages: [{ key: '/preise', count: 1310 }, { key: '/', count: 900 }] }))
assert.equal(pc.items.find((i) => i.icon === 'device').html, '60 % der Aufrufe kommen vom Computer.')
assert.equal(pc.items.find((i) => i.icon === 'page').html, 'Am meisten angesehen: /preise (1.310 Aufrufe).')
// nur Startseite
assert.equal(summarize(stats({ pages: [{ key: '/', count: 50 }] })).items.find((i) => i.icon === 'page').html, 'Am meisten angesehen: die Startseite (50 Aufrufe).')
// Gerät unter 55 %, Herkunft unter 20 %: beide Hinweise entfallen
const flach = summarize(stats({ devices: [{ key: 'm', count: 50 }, { key: 'd', count: 50 }], refs: [{ key: 'google.com', count: 10 }, { key: '', count: 90 }] }))
assert.ok(!flach.items.some((i) => i.icon === 'device' || i.icon === 'source'))
// Direkt (leerer Host) zählt nie als Quelle, auch wenn er vorne steht
const direkt = summarize(stats({ refs: [{ key: '', count: 900 }, { key: 'google.com', count: 300 }] }))
assert.equal(direkt.items.find((i) => i.icon === 'source').html, 'Die meisten, die über einen Link kamen, kommen von Google (300).')
// Samstage 2× → Wochentag-Hinweis; alle 5 Bedingungen wahr → Seite fällt weg
const sat = (i, day) => (new Date(day + 'T00:00:00Z').getUTCDay() === 6 ? 20 : 10)
const voll = summarize(stats({ series: days('2026-09-05', 30, sat) }))
assert.deepEqual(voll.items.map((i) => i.icon), ['contact', 'day', 'source', 'device'])
assert.equal(voll.items[1].html, 'Samstag ist im Schnitt dein stärkster Tag.')
})
test('summarize: Wochentag und Spitzentag in einem Hinweis', () => {
// Samstage 20, Spitzentag Sa., 19.09. mit 371, sonst 10: beides trifft zu
const both = (i, day) => (day === '2026-09-19' ? 371 : new Date(day + 'T00:00:00Z').getUTCDay() === 6 ? 40 : 10)
const s = summarize(stats({ series: days('2026-09-05', 30, both) }))
assert.equal(s.items.find((i) => i.icon === 'day').html, 'Samstag ist im Schnitt dein stärkster Tag. Spitzentag war Sa., 19.09. mit 371 Aufrufen.')
// nur Spitzentag am Mittwoch 23.09.: die anderen Mittwoche (0) gleichen den Wochentagsschnitt aus (alle Wochentage Ø 25)
const spike = (i, day) => (day === '2026-09-23' ? 100 : new Date(day + 'T00:00:00Z').getUTCDay() === 3 ? 0 : 25)
const p = summarize(stats({ series: days('2026-09-05', 30, spike) }))
assert.equal(p.items.find((i) => i.icon === 'day').html, 'Spitzentag war Mi., 23.09. mit 100 Aufrufen.')
// weniger als 70 Aufrufe oder weniger als 7 Tage: kein Wochentag-Hinweis
assert.ok(!summarize(stats({ totals: { ...t, pv: 69 }, series: days('2026-09-05', 30, spike) })).items.some((i) => i.icon === 'day'))
assert.ok(!summarize(stats({ firstDay: '2026-09-30', series: days('2026-09-05', 30, spike) })).items.some((i) => i.icon === 'day'))
})
test('summarize: Wochentag-Hinweis zählt alle Tage der Reihe (alle sind abgeschlossen), auch in der 7-Tage-Ansicht', () => {
const isSat = (day) => new Date(day + 'T00:00:00Z').getUTCDay() === 6
const dayHtml = (s) => s.items.find((i) => i.icon === 'day')?.html
// Reihe 04.09.–03.10.2026 (Ende = gestern, heute steht getrennt in `today`); der letzte Tag, Samstag 03.10., zählt mit
const last = '2026-10-03'
const base = { from: '2026-09-04', to: last, today: { day: '2026-10-04', pv: 5, visitors: 3, contacts: 0 } }
assert.equal(dayHtml(summarize(stats({ ...base, series: days('2026-09-04', 30, (i, day) => (isSat(day) ? 280 : 200)) }))), 'Samstag ist im Schnitt dein stärkster Tag.')
// Spitzentag am letzten Tag der Reihe wird erkannt
const spike = days('2026-09-04', 30, (i, day) => (day === last ? 400 : 100))
assert.equal(dayHtml(summarize(stats({ ...base, series: spike }))), 'Samstag ist im Schnitt dein stärkster Tag. Spitzentag war Sa., 03.10. mit 400 Aufrufen.')
// 7-Tage-Ansicht: genau 7 abgeschlossene Tage reichen für den Hinweis (28.09.–04.10., Sa. 03.10.)
const week = days('2026-09-28', 7, (i, day) => (isSat(day) ? 500 : 100))
assert.equal(dayHtml(summarize(stats({ range: 7, from: '2026-09-28', to: '2026-10-04', series: week }))), 'Samstag ist im Schnitt dein stärkster Tag. Spitzentag war Sa., 03.10. mit 500 Aufrufen.')
// weniger als 7 Tage (neue Website): weiter kein Hinweis
assert.equal(dayHtml(summarize(stats({ range: 7, from: '2026-09-28', to: '2026-10-04', firstDay: '2026-09-29', series: week }))), undefined)
// alter Collector ohne `today`: derselbe Hinweis, kein Absturz
const { today, ...alt } = stats({ ...base, series: days('2026-09-04', 30, (i, day) => (isSat(day) ? 280 : 200)) })
assert.equal(dayHtml(summarize(alt)), 'Samstag ist im Schnitt dein stärkster Tag.')
})
test('summarize: Pfade und Hosts werden maskiert', () => {
const s = summarize(stats({ pages: [{ key: '/
', count: 900 }], refs: [{ key: 'a"b.de', count: 900 }] }))
assert.ok(!JSON.stringify(s).includes('
i.icon === 'source').html, 'Die meisten, die über einen Link kamen, kommen von a"b.de (900).')
assert.equal(s.items.find((i) => i.icon === 'page').html, 'Am meisten angesehen: /<img src=x> (900 Aufrufe).')
// auch an zweiter Stelle hinter der Startseite
const z = summarize(stats({ pages: [{ key: '/', count: 900 }, { key: '/