diff --git a/next.config.js b/next.config.js
index 2fe5e5325..fb78be929 100644
--- a/next.config.js
+++ b/next.config.js
@@ -59,6 +59,10 @@ module.exports = {
source: '/recentes/rss',
destination: '/api/v1/contents/rss',
},
+ {
+ source: '/api/v1/analytics',
+ destination: `${process.env.UMAMI_ENDPOINT}/api/send`,
+ },
];
},
headers() {
diff --git a/pages/_app.public.js b/pages/_app.public.js
index 0e825634a..254ddf2c2 100644
--- a/pages/_app.public.js
+++ b/pages/_app.public.js
@@ -1,9 +1,8 @@
-import { Analytics } from '@vercel/analytics/react';
import { RevalidateProvider } from 'next-swr';
import { SWRConfig } from 'swr';
import { ThemeProvider, Turnstile } from '@/TabNewsUI';
-import { DefaultHead, UserProvider } from 'pages/interface';
+import { Analytics, DefaultHead, UserProvider } from 'pages/interface';
async function SWRFetcher(resource, init) {
const response = await fetch(resource, init);
@@ -30,15 +29,7 @@ function MyApp({ Component, pageProps }) {
- {
- const { pathname } = new URL(event.url);
- if (['/', '/publicar'].includes(pathname)) {
- return null;
- }
- return event;
- }}
- />
+
);
diff --git a/pages/interface/components/Analytics/index.js b/pages/interface/components/Analytics/index.js
new file mode 100644
index 000000000..302fdb3c6
--- /dev/null
+++ b/pages/interface/components/Analytics/index.js
@@ -0,0 +1,26 @@
+import { Analytics as VercelAnalytics } from '@vercel/analytics/react';
+import Script from 'next/script';
+
+export default function Analytics() {
+ return (
+ <>
+
+ {
+ const { pathname } = new URL(event.url);
+ if (['/', '/publicar'].includes(pathname)) {
+ return null;
+ }
+ return event;
+ }}
+ />
+ >
+ );
+}
diff --git a/pages/interface/index.js b/pages/interface/index.js
index 40a282a2d..9ff6b5050 100644
--- a/pages/interface/index.js
+++ b/pages/interface/index.js
@@ -1,3 +1,4 @@
+export { default as Analytics } from './components/Analytics';
export { DefaultHead, default as Head } from './components/Head';
export { default as useCollapse } from './hooks/useCollapse';
export { default as useMediaQuery } from './hooks/useMediaQuery';
diff --git a/public/analytics.js b/public/analytics.js
new file mode 100644
index 000000000..ad512b877
--- /dev/null
+++ b/public/analytics.js
@@ -0,0 +1,276 @@
+// Original code from:
+// https://github.com/umami-software/umami/blob/bce70c1034c6668255261c26093a35900b869566/src/tracker/index.js
+// With a modification by @aprendendofelipe to filter tracked paths
+
+((window) => {
+ const {
+ screen: { width, height },
+ navigator: { language },
+ location,
+ document,
+ history,
+ } = window;
+ const { hostname, href, origin } = location;
+ const { currentScript, referrer } = document;
+ const localStorage = href.startsWith('data:') ? undefined : window.localStorage;
+
+ if (!currentScript) return;
+
+ const _data = 'data-';
+ const _false = 'false';
+ const _true = 'true';
+ const attr = currentScript.getAttribute.bind(currentScript);
+ const website = attr(_data + 'website-id');
+ const hostUrl = attr(_data + 'host-url');
+ const tag = attr(_data + 'tag');
+ const autoTrack = attr(_data + 'auto-track') !== _false;
+ const excludeSearch = attr(_data + 'exclude-search') === _true;
+ const pathMatcher = attr(_data + 'path-matcher');
+ const pathRegex = pathMatcher ? new RegExp(pathMatcher) : undefined;
+ const domain = attr(_data + 'domains') || '';
+ const domains = domain.split(',').map((n) => n.trim());
+ const host = hostUrl || currentScript.src.split('/').slice(0, -1).join('/');
+ const endpoint = `${host.replace(/\/$/, '')}/api/v1/analytics`;
+ const screen = `${width}x${height}`;
+ const eventRegex = /data-umami-event-([\w-_]+)/;
+ const eventNameAttribute = _data + 'umami-event';
+ const delayDuration = 300;
+
+ /* Helper functions */
+
+ const encode = (str) => {
+ if (!str) {
+ return undefined;
+ }
+
+ try {
+ const result = decodeURI(str);
+
+ if (result !== str) {
+ return result;
+ }
+ } catch (e) {
+ return str;
+ }
+
+ return encodeURI(str);
+ };
+
+ const parseURL = (url) => {
+ try {
+ // use location.origin as the base to handle cases where the url is a relative path
+ const { pathname, search, hash } = new URL(url, location.href);
+ url = pathname + search + hash;
+ } catch (e) {
+ /* empty */
+ }
+ return excludeSearch ? url.split('?')[0] : url;
+ };
+
+ const getPayload = () => ({
+ website,
+ hostname,
+ screen,
+ language,
+ title: encode(title),
+ url: encode(currentUrl),
+ referrer: encode(currentRef),
+ tag: tag ? tag : undefined,
+ });
+
+ /* Event handlers */
+
+ const handlePush = (state, title, url) => {
+ if (!url) return;
+
+ currentRef = currentUrl;
+ currentUrl = parseURL(url.toString());
+
+ if (currentUrl !== currentRef) {
+ setTimeout(track, delayDuration);
+ }
+ };
+
+ const handlePathChanges = () => {
+ const hook = (_this, method, callback) => {
+ const orig = _this[method];
+
+ return (...args) => {
+ callback.apply(null, args);
+
+ return orig.apply(_this, args);
+ };
+ };
+
+ history.pushState = hook(history, 'pushState', handlePush);
+ history.replaceState = hook(history, 'replaceState', handlePush);
+ };
+
+ const handleTitleChanges = () => {
+ const observer = new MutationObserver(([entry]) => {
+ title = entry && entry.target ? entry.target.text : undefined;
+ });
+
+ const node = document.querySelector('head > title');
+
+ if (node) {
+ observer.observe(node, {
+ subtree: true,
+ characterData: true,
+ childList: true,
+ });
+ }
+ };
+
+ const handleClicks = () => {
+ document.addEventListener(
+ 'click',
+ async (e) => {
+ const isSpecialTag = (tagName) => ['BUTTON', 'A'].includes(tagName);
+
+ const trackElement = async (el) => {
+ const attr = el.getAttribute.bind(el);
+ const eventName = attr(eventNameAttribute);
+
+ if (eventName) {
+ const eventData = {};
+
+ el.getAttributeNames().forEach((name) => {
+ const match = name.match(eventRegex);
+
+ if (match) {
+ eventData[match[1]] = attr(name);
+ }
+ });
+
+ return track(eventName, eventData);
+ }
+ };
+
+ const findParentTag = (rootElem, maxSearchDepth) => {
+ let currentElement = rootElem;
+ for (let i = 0; i < maxSearchDepth; i++) {
+ if (isSpecialTag(currentElement.tagName)) {
+ return currentElement;
+ }
+ currentElement = currentElement.parentElement;
+ if (!currentElement) {
+ return null;
+ }
+ }
+ };
+
+ const el = e.target;
+ const parentElement = isSpecialTag(el.tagName) ? el : findParentTag(el, 10);
+
+ if (parentElement) {
+ const { href, target } = parentElement;
+ const eventName = parentElement.getAttribute(eventNameAttribute);
+
+ if (eventName) {
+ if (parentElement.tagName === 'A') {
+ const external =
+ target === '_blank' || e.ctrlKey || e.shiftKey || e.metaKey || (e.button && e.button === 1);
+
+ if (eventName && href) {
+ if (!external) {
+ e.preventDefault();
+ }
+ return trackElement(parentElement).then(() => {
+ if (!external) location.href = href;
+ });
+ }
+ } else if (parentElement.tagName === 'BUTTON') {
+ return trackElement(parentElement);
+ }
+ }
+ } else {
+ return trackElement(el);
+ }
+ },
+ true,
+ );
+ };
+
+ /* Tracking functions */
+
+ const trackingDisabled = () =>
+ !website || (localStorage && localStorage.getItem('umami.disabled')) || (domain && !domains.includes(hostname));
+
+ const send = async (payload, type = 'event') => {
+ if (trackingDisabled()) return;
+
+ if (pathRegex && !pathRegex.test(payload.url)) return;
+
+ const headers = {
+ 'Content-Type': 'application/json',
+ };
+
+ if (typeof cache !== 'undefined') {
+ headers['x-umami-cache'] = cache;
+ }
+
+ try {
+ const res = await fetch(endpoint, {
+ method: 'POST',
+ body: JSON.stringify({ type, payload }),
+ headers,
+ });
+ const text = await res.text();
+
+ return (cache = text);
+ } catch (e) {
+ /* empty */
+ }
+ };
+
+ const init = () => {
+ if (!initialized) {
+ track();
+ handlePathChanges();
+ handleTitleChanges();
+ handleClicks();
+ initialized = true;
+ }
+ };
+
+ const track = (obj, data) => {
+ if (typeof obj === 'string') {
+ return send({
+ ...getPayload(),
+ name: obj,
+ data: typeof data === 'object' ? data : undefined,
+ });
+ } else if (typeof obj === 'object') {
+ return send(obj);
+ } else if (typeof obj === 'function') {
+ return send(obj(getPayload()));
+ }
+ return send(getPayload());
+ };
+
+ const identify = (data) => send({ ...getPayload(), data }, 'identify');
+
+ /* Start */
+
+ if (!window.umami) {
+ window.umami = {
+ track,
+ identify,
+ };
+ }
+
+ let currentUrl = parseURL(href);
+ let currentRef = referrer.startsWith(origin) ? '' : referrer;
+ let title = document.title;
+ let cache;
+ let initialized;
+
+ if (autoTrack && !trackingDisabled()) {
+ if (document.readyState === 'complete') {
+ init();
+ } else {
+ document.addEventListener('readystatechange', init, true);
+ }
+ }
+})(window);