| Server IP : 138.197.107.151 / Your IP : 216.73.217.7 Web Server : Apache/2.4.58 (Ubuntu) System : Linux BloxBy-Builder 6.8.0-71-generic #71-Ubuntu SMP PREEMPT_DYNAMIC Tue Jul 22 16:52:38 UTC 2025 x86_64 User : wpbetasites_mrakzqskir ( 1022) PHP Version : 8.3.6 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/bsd-crawler-parser/viewer/templates/ |
Upload File : |
{% extends "base.html" %}
{% from "_filters.html" import filter_bar with context %}
{% block title %}URL trend{% endblock %}
{% block body %}
<div class="card">
{{ filter_bar(detailed=True, all_sites=all_sites, platforms_by_group=platforms_by_group,
site=site, platform=platform, url_query=url_query) }}
<p class="muted" style="margin:12px 0 0; font-size:12px;">
Daily AI crawler hits on
{% if url_query %}URLs containing <code>{{ url_query }}</code>{% else %}any URL{% endif %}
{%- if site %} on {{ site }}{% endif %}
{%- if platform %} by {{ platform }}{% endif %}.
Type a URL fragment (e.g. <code>/dog-bite/</code>) to watch one page or section;
leave it empty for the whole picture. Optimised a page recently? Set the range
to straddle the change and see whether AI traffic picked up.
</p>
</div>
{% if not days %}
<div class="card"><div class="empty">
No data in that range. Data runs {{ dates[-1] }} to {{ dates[0] }}.
</div></div>
{% else %}
{% if missing %}
<div class="note" style="margin-bottom:16px;">
{{ missing|length }} day{{ '' if missing|length == 1 else 's' }} in this range
({{ missing|join(', ') }}) ha{{ 's' if missing|length == 1 else 've' }} no
trend data yet (parsed before trend tracking existed). Shown as 0.
</div>
{% endif %}
<div class="card">
<h2>
{{ total|commafy }} matching hit{{ '' if total == 1 else 's' }}
across {{ days|length }} day{{ '' if days|length == 1 else 's' }}
{%- if not include_search %} · AI only{% endif %}
{%- if content_only %} · content pages{% endif %}
</h2>
{% if total == 0 %}
<div class="empty">No hits match these filters in this range.</div>
{% else %}
<div id="trendchart" style="position:relative;"></div>
{% endif %}
</div>
{% if total %}
<div class="card">
<div class="tablebar">
<h2>Hits per day</h2>
<span class="count">{{ days|length }} days</span>
</div>
<table style="max-width:640px;">
<thead><tr>
<th>Date</th><th class="num">Hits</th><th style="width:220px"></th>
</tr></thead>
<tbody>
{% for day, hits in per_day %}
<tr class="{{ 'zero' if not hits }}">
<td style="font-variant-numeric:tabular-nums;">{{ day }}</td>
<td class="num">{{ hits|commafy }}</td>
<td><div class="bar" style="width:{{ (hits / peak * 100)|round(1) if peak else 0 }}%"></div></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card">
<div class="tablebar">
<h2>Top matching URLs</h2>
<span class="count">across the whole range</span>
</div>
<table>
<thead><tr><th>URL</th><th class="num">Hits</th><th></th></tr></thead>
<tbody>
{% for url, hits in top_urls %}
<tr>
<td class="url">{{ url }}</td>
<td class="num">{{ hits|commafy }}</td>
<td><a href="{{ url_for('detail', **keep(q=url)) }}">View hits <span class="arrow">↗</span></a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card">
<h2>By AI platform (matching hits)</h2>
<table style="max-width:640px;">
<thead><tr><th>Bot</th><th class="num">Hits</th><th class="num">Share</th></tr></thead>
<tbody>
{% for plat, hits in platform_rows %}
<tr>
<td><span class="rowline"><strong>{{ plat }}</strong>
<i class="info" data-tip="{{ plat }} — {{ bot_info(plat) }}">i</i></span></td>
<td class="num">{{ hits|commafy }}</td>
<td class="num muted">{{ '%.1f'|format(hits / total * 100) }}%</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
(function () {
/* Line chart, drawn to the card's real pixel width so text never scales.
Specs follow the house dataviz rules: 2px line with round joins, a 10%
area wash, hairline solid gridlines, an 8px end-dot with a 2px surface
ring, one direct label at the endpoint, and a crosshair + tooltip that
snaps to the nearest day (nobody can aim at a 2px line). Single series,
so no legend: the card's heading says what is plotted. */
var DATA = {{ per_day|tojson }};
var mount = document.getElementById('trendchart');
if (!mount || !DATA.length) return;
var INK = '#16191d', MUTED = '#667085', LINE = '#e3e6ea',
SERIES = '#0077aa', SURFACE = '#ffffff';
var H = 280, PAD = {top: 18, right: 56, bottom: 28, left: 52};
var NS = 'http://www.w3.org/2000/svg';
function el(name, attrs, parent) {
var n = document.createElementNS(NS, name);
for (var k in attrs) n.setAttribute(k, attrs[k]);
if (parent) parent.appendChild(n);
return n;
}
function fmt(n) { return n.toLocaleString('en-US'); }
/* Gridline step from the 1/2/5 ladder, sized for ~4 lines; the axis tops out
at the first step at or above the max, so the data always fills most of
the plot instead of floating in the bottom half of a too-tall scale. */
function gridStep(m) {
if (m <= 4) return 1;
var raw = m / 4;
var pow = Math.pow(10, Math.floor(Math.log10(raw)));
var ladder = [1, 2, 5, 10];
for (var i = 0; i < ladder.length; i++) {
if (ladder[i] * pow >= raw) return ladder[i] * pow;
}
return 10 * pow;
}
var tip = document.createElement('div');
tip.style.cssText = 'position:absolute; display:none; pointer-events:none;' +
'background:#16191d; color:#fff; padding:7px 10px; border-radius:7px;' +
'font-size:12px; line-height:1.4; white-space:nowrap; z-index:10;' +
'box-shadow:0 6px 18px rgba(0,0,0,.22);';
var tipVal = document.createElement('div');
tipVal.style.cssText = 'font-weight:700; font-size:13px;';
var tipDay = document.createElement('div');
tipDay.style.cssText = 'color:#c6cbd2;';
tip.appendChild(tipVal); tip.appendChild(tipDay);
mount.appendChild(tip);
function draw() {
var old = mount.querySelector('svg');
if (old) old.remove();
var W = Math.max(320, mount.clientWidth);
var svg = el('svg', {width: W, height: H, role: 'img',
'aria-label': 'Matching hits per day'}, mount);
var innerW = W - PAD.left - PAD.right, innerH = H - PAD.top - PAD.bottom;
var peak = Math.max.apply(null, DATA.map(function (d) { return d[1]; }));
var step = gridStep(peak);
var max = Math.max(step, Math.ceil(peak / step) * step);
var x = function (i) {
return PAD.left + (DATA.length === 1 ? innerW / 2 : i * innerW / (DATA.length - 1));
};
var y = function (v) { return PAD.top + innerH * (1 - v / max); };
/* Gridlines + y ticks at each step: hairline, recessive. */
for (var val = 0; val <= max; val += step) {
var gy = y(val);
el('line', {x1: PAD.left, x2: W - PAD.right, y1: gy, y2: gy,
stroke: LINE, 'stroke-width': 1}, svg);
el('text', {x: PAD.left - 8, y: gy + 4, 'text-anchor': 'end', fill: MUTED,
'font-size': 11}, svg).textContent = fmt(Math.round(val));
}
/* X ticks: about six, always including the first and last day. */
var step = Math.max(1, Math.ceil((DATA.length - 1) / 5));
for (var i = 0; i < DATA.length; i += step) {
var last = i + step >= DATA.length;
var ti = last ? DATA.length - 1 : i;
el('text', {x: x(ti), y: H - 8, 'text-anchor': 'middle', fill: MUTED,
'font-size': 11}, svg).textContent = DATA[ti][0].slice(5);
if (last) break;
}
var pts = DATA.map(function (d, i) { return [x(i), y(d[1])]; });
if (DATA.length > 1) {
var path = pts.map(function (p, i) {
return (i ? 'L' : 'M') + p[0].toFixed(1) + ' ' + p[1].toFixed(1);
}).join('');
el('path', {d: path + 'L' + pts[pts.length - 1][0].toFixed(1) + ' ' + (PAD.top + innerH) +
'L' + pts[0][0].toFixed(1) + ' ' + (PAD.top + innerH) + 'Z',
fill: SERIES, opacity: 0.1}, svg);
el('path', {d: path, fill: 'none', stroke: SERIES, 'stroke-width': 2,
'stroke-linejoin': 'round', 'stroke-linecap': 'round'}, svg);
}
/* Endpoint: ringed dot + the one direct label. */
var endI = DATA.length - 1;
el('circle', {cx: pts[endI][0], cy: pts[endI][1], r: 6, fill: SURFACE}, svg);
el('circle', {cx: pts[endI][0], cy: pts[endI][1], r: 4, fill: SERIES}, svg);
el('text', {x: pts[endI][0] + 10, y: pts[endI][1] + 4, fill: INK,
'font-size': 12, 'font-weight': 700}, svg).textContent = fmt(DATA[endI][1]);
/* Crosshair + hover dot, hidden until the pointer arrives. */
var hair = el('line', {y1: PAD.top, y2: PAD.top + innerH, stroke: '#98a2b3',
'stroke-width': 1, visibility: 'hidden'}, svg);
var hoverRing = el('circle', {r: 6, fill: SURFACE, visibility: 'hidden'}, svg);
var hoverDot = el('circle', {r: 4, fill: SERIES, visibility: 'hidden'}, svg);
function showAt(i) {
var px = pts[i][0], py = pts[i][1];
hair.setAttribute('x1', px); hair.setAttribute('x2', px);
hair.setAttribute('visibility', 'visible');
hoverRing.setAttribute('cx', px); hoverRing.setAttribute('cy', py);
hoverRing.setAttribute('visibility', 'visible');
hoverDot.setAttribute('cx', px); hoverDot.setAttribute('cy', py);
hoverDot.setAttribute('visibility', 'visible');
tipVal.textContent = fmt(DATA[i][1]) + ' hit' + (DATA[i][1] === 1 ? '' : 's');
tipDay.textContent = DATA[i][0];
tip.style.display = 'block';
var left = px + 14;
if (left + tip.offsetWidth > W - 4) left = px - tip.offsetWidth - 14;
tip.style.left = left + 'px';
tip.style.top = Math.max(0, py - tip.offsetHeight - 6) + 'px';
}
function hide() {
hair.setAttribute('visibility', 'hidden');
hoverRing.setAttribute('visibility', 'hidden');
hoverDot.setAttribute('visibility', 'hidden');
tip.style.display = 'none';
}
/* The whole plot is the hit target; the crosshair snaps to the nearest day. */
var overlay = el('rect', {x: PAD.left, y: PAD.top, width: innerW, height: innerH,
fill: 'transparent'}, svg);
overlay.addEventListener('pointermove', function (ev) {
var box = svg.getBoundingClientRect();
var mx = ev.clientX - box.left;
var i = DATA.length === 1 ? 0 :
Math.round((mx - PAD.left) / (innerW / (DATA.length - 1)));
showAt(Math.min(DATA.length - 1, Math.max(0, i)));
});
overlay.addEventListener('pointerleave', hide);
}
draw();
var timer = null;
window.addEventListener('resize', function () {
clearTimeout(timer); timer = setTimeout(draw, 120);
});
})();
</script>
{% endif %}
{% endif %}
{% endblock %}