PageSourceSearch

https://www.infonova.com.br/wix-blog.js

js infonova.com.br collected 2026-10-02 22:26:57 UTC 27,627 bytes, 554 lines download raw bytes

1(function () {
2    "use strict";
3
4    const CLIENT_ID = "49cbb3ba-84fc-452f-a3d1-63112a8d8f76";
5    const TOKEN_URL = "https://www.wixapis.com/oauth2/token";
6    const POSTS_QUERY_URL = "https://www.wixapis.com/v3/posts/query";
7    const POST_BY_SLUG_URL = "https://www.wixapis.com/v3/posts/slugs/";
8    const CATEGORIES_URL = "https://www.wixapis.com/blog/v3/categories";
9    const PAGE_SIZE = 6;
10    const PAGE_WINDOW = 3; // números exibidos em torno da página atual (mínimo 3), além de primeira/última
11    const TOKEN_KEY = "infonova_wix_visitor_token_v1";
12    const fallback = (window.INFONOVA_BLOG_FALLBACK && window.INFONOVA_BLOG_FALLBACK.posts) || [];
13
14    const state = {
15        mode: "loading",
16        categories: [],
17        liveCategories: [],
18        page: 1,
19        total: 0,
20        hasNext: false,
21        categoryId: null,
22        categoryLabel: null,
23        search: ""
24    };
25
26    function escapeHtml(value) {
27        return String(value == null ? "" : value)
28            .replace(/&/g, "&")
29            .replace(/</g, "&lt;")
30            .replace(/>/g, "&gt;")
31            .replace(/"/g, "&quot;")
32            .replace(/'/g, "&#039;");
33    }
34
35    function safeUrl(value) {
36        const raw = String(value || "").trim();
37        if (!raw) return "";
38        if (/^(javascript|data):/i.test(raw)) return "";
39        return raw;
40    }
41
42    function formatDate(value) {
43        if (!value) return "";
44        const d = new Date(value);
45        if (Number.isNaN(d.getTime())) return "";
46        return new Intl.DateTimeFormat("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", timeZone: "America/Sao_Paulo" }).format(d);
47    }
48
49    function fallbackCategories() {
50        const labels = [];
51        fallback.forEach(post => (post.categories || []).forEach(label => {
52            if (label && !labels.includes(label)) labels.push(label);
53        }));
54        return labels.sort((a,b) => a.localeCompare(b, "pt-BR")).map(label => ({ id: "fallback:" + label, label }));
55    }
56
57    async function getVisitorToken() {
58        try {
59            const cached = JSON.parse(sessionStorage.getItem(TOKEN_KEY) || "null");
60            if (cached && cached.accessToken && cached.expiresAt > Date.now() + 60000) return cached.accessToken;
61        } catch (_) {}
62
63        const response = await fetch(TOKEN_URL, {
64            method: "POST",
65            headers: { "Content-Type": "application/json" },
66            body: JSON.stringify({ clientId: CLIENT_ID, grantType: "anonymous" })
67        });
68        const data = await response.json().catch(() => ({}));
69        if (!response.ok || !data.access_token) throw new Error("Não foi possível autenticar a leitura do Wix Blog.");
70        const tokenData = {
71            accessToken: data.access_token,
72            expiresAt: Date.now() + Math.max(60, Number(data.expires_in || 3600) - 60) * 1000
73        };
74        try { sessionStorage.setItem(TOKEN_KEY, JSON.stringify(tokenData)); } catch (_) {}
75        return tokenData.accessToken;
76    }
77
78    async function wixFetch(url, options) {
79        const token = await getVisitorToken();
80        const opts = Object.assign({}, options || {});
81        opts.headers = Object.assign({ "Content-Type": "application/json", "Authorization": token }, opts.headers || {});
82        const response = await fetch(url, opts);
83        const data = await response.json().catch(() => ({}));
84        if (!response.ok) {
85            const err = new Error("Wix Blog retornou HTTP " + response.status);
86            err.status = response.status;
87            err.data = data;
88            throw err;
89        }
90        return data;
91    }
92
93    async function getCategories() {
94        const data = await wixFetch(CATEGORIES_URL + "?fieldsets=URL", { method: "GET" });
95        return data.categories || [];
96    }
97
98    async function queryPosts(params) {
99        params = params || {};
100        const query = {};
101        const filter = {};
102        if (params.categoryId) filter.categoryIds = { "$hasSome": [params.categoryId] };
103        if (params.search) filter.title = { "$startsWith": params.search };
104        if (Object.keys(filter).length) query.filter = filter;
105        query.sort = [{ fieldName: "firstPublishedDate", order: "DESC" }];
106        // Paginação por offset: devolve pagingMetadata.total, permitindo numeração real de páginas.
107        query.paging = { limit: PAGE_SIZE, offset: Math.max(0, Number(params.offset) || 0) };
108        return wixFetch(POSTS_QUERY_URL, {
109            method: "POST",
110            body: JSON.stringify({ fieldsets: ["URL"], query })
111        });
112    }
113
114    async function getPostBySlug(slug) {
115        const query = new URLSearchParams();
116        query.append("fieldsets", "RICH_CONTENT");
117        query.append("fieldsets", "SEO");
118        query.append("fieldsets", "URL");
119        return wixFetch(POST_BY_SLUG_URL + encodeURIComponent(slug) + "?" + query.toString(), { method: "GET" });
120    }
121
122    function liveImage(post) {
123        return safeUrl(
124            post && post.media && post.media.wixMedia && post.media.wixMedia.image && post.media.wixMedia.image.url ||
125            post && post.heroImage && post.heroImage.url || ""
126        );
127    }
128
129    function categoryLabels(post) {
130        if (post && Array.isArray(post.categories) && post.categories.length) return post.categories;
131        const ids = (post && post.categoryIds) || [];
132        return ids.map(id => {
133            const found = state.liveCategories.find(c => c.id === id);
134            return found ? found.label : "";
135        }).filter(Boolean);
136    }
137
138    function postHref(slug) {
139        if ((slug || "") === "governanca-de-ti") return "/governanca-de-ti-insight.html";
140        return "/" + encodeURIComponent(slug || "") + ".html";
141    }
142
143    function cardHtml(post) {
144        const slug = post.slug || post.seoSlug || "";
145        const href = postHref(slug);
146        const cats = categoryLabels(post).slice(0, 2);
147        const image = post.image || liveImage(post) || "assets/images/blog/blog-list-1-1.jpg";
148        const title = post.title || "Insight Infonova";
149        const excerpt = post.excerpt || post.contentText || "";
150        const author = post.author || "Infonova";
151        const date = formatDate(post.firstPublishedDate || post.date);
152        return `
153            <article class="blog-list__single wix-blog-card">
154                <div class="blog-list__img wix-blog-card__image">
155                    <a href="${href}" aria-label="Abrir ${escapeHtml(title)}"><img src="${escapeHtml(image)}" alt="${escapeHtml(title)}" loading="lazy" decoding="async"></a>
156                    ${cats.length ? `<div class="blog-list__tags">${cats.map(c => `<span>${escapeHtml(c)}</span>`).join("")}</div>` : ""}
157                </div>
158                <div class="blog-list__content">
159                    <ul class="blog-list__meta list-unstyled">
160                        <li class="d-flex align-items-center gap-3">
161                            <a href="${href}"><span class="far fa-calendar-alt"></span>${escapeHtml(date)}</a>
162                            <span>•</span>
163                            <a href="${href}"><span class="far fa-user"></span>${escapeHtml(author)}</a>
164                        </li>
165                    </ul>
166                    <h3 class="blog-list__title"><a href="${href}">${escapeHtml(title)}</a></h3>
167                    <p class="blog-list__text">${escapeHtml(excerpt)}</p>
168                    <div class="blog-list__btn-box"><a href="${href}" class="thm-btn">Saiba Mais<span class="icon-right-arrow"></span></a></div>
169                </div>
170            </article>`;
171    }
172
173    function renderList(posts) {
174        const target = document.getElementById("wix-blog-list");
175        if (!target) return;
176        if (!posts || !posts.length) {
177            target.innerHTML = `<div class="wix-blog-empty"><h3>Nenhum insight encontrado.</h3><p>Tente outra busca ou categoria.</p></div>`;
178            return;
179        }
180        target.innerHTML = posts.map(cardHtml).join("");
181    }
182
183    /* -------------------------------------------------------------
184       Paginação
185       O CSS do template (blog.css) força `transform: rotate(180deg)`
186       no 6º <li>, partindo do pressuposto de que a lista sempre teria
187       exatamente 6 itens. Com paginação dinâmica isso girava um número
188       de página de cabeça para baixo. O bloco abaixo neutraliza a regra
189       e cuida do estado "disabled" e das reticências.
190    ------------------------------------------------------------- */
191    function ensurePaginationStyles() {
192        if (document.getElementById("wix-blog-pagination-style")) return;
193        const style = document.createElement("style");
194        style.id = "wix-blog-pagination-style";
195        style.textContent = `
196#wix-blog-pagination .pg-pagination li:nth-child(n) a,
197#wix-blog-pagination .pg-pagination li:nth-child(n) a:before,
198#wix-blog-pagination .pg-pagination li:nth-child(n) a:after { transform: none; }
199#wix-blog-pagination .pg-pagination li.disabled a { opacity: .35; pointer-events: none; cursor: default; }
200#wix-blog-pagination .pg-pagination li.gap { color: currentColor; opacity: .55; padding: 0 4px; line-height: 48px; font-weight: 500; user-select: none; }
201#wix-blog-pagination .pg-pagination li a[data-nav] span { display: inline-block; line-height: inherit; }
202@media (max-width: 767px) {
203  #wix-blog-pagination .pg-pagination { gap: 6px; }
204  #wix-blog-pagination .pg-pagination li a { height: 40px; width: 40px; line-height: 40px; font-size: 16px; }
205  #wix-blog-pagination .pg-pagination li.gap { line-height: 40px; padding: 0 2px; }
206}
207`;
208        document.head.appendChild(style);
209    }
210
211    // Janela de páginas: sempre tenta exibir pelo menos 3 números.
212    function pageWindow(current, totalPages) {
213        const size = Math.max(3, PAGE_WINDOW);
214        if (totalPages <= size) {
215            const all = [];
216            for (let i = 1; i <= totalPages; i++) all.push(i);
217            return all;
218        }
219        let start = Math.max(1, current - Math.floor(size / 2));
220        let end = start + size - 1;
221        if (end > totalPages) { end = totalPages; start = end - size + 1; }
222        const items = [];
223        if (start > 1) {
224            items.push(1);
225            if (start > 2) items.push("gap");
226        }
227        for (let i = start; i <= end; i++) items.push(i);
228        if (end < totalPages) {
229            if (end < totalPages - 1) items.push("gap");
230            items.push(totalPages);
231        }
232        return items;
233    }
234
235    function goToPage(page, totalPages) {
236        const target = Math.min(Math.max(1, page), Math.max(1, totalPages));
237        if (target === state.page) return;
238        state.page = target;
239        if (state.mode === "live") { loadLive().catch(() => loadFallback()); }
240        else { loadFallback(); }
241        scrollToList();
242    }
243
244    function renderPagination(meta) {
245        const target = document.getElementById("wix-blog-pagination");
246        if (!target) return;
247        ensurePaginationStyles();
248
249        const total = Number(meta && meta.total);
250        let totalPages;
251        if (Number.isFinite(total) && total > 0) {
252            totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
253        } else {
254            // Sem "total" na resposta: deduz o mínimo possível a partir de hasNext.
255            totalPages = state.page + ((meta && meta.hasNext) || state.hasNext ? 1 : 0);
256        }
257
258        if (totalPages <= 1) { target.innerHTML = ""; return; }
259        if (state.page > totalPages) state.page = totalPages;
260
261        const items = pageWindow(state.page, totalPages).map(item => {
262            if (item === "gap") return `<li class="gap" aria-hidden="true">…</li>`;
263            const isActive = item === state.page;
264            return `<li class="count ${isActive ? "active" : ""}"><a href="#" data-page="${item}" aria-label="Página ${item}"${isActive ? ' aria-current="page"' : ""}>${String(item).padStart(2, "0")}</a></li>`;
265        }).join("");
266
267        const canPrev = state.page > 1;
268        const canNext = state.page < totalPages;
269
270        target.innerHTML = `<ul class="pg-pagination list-unstyled" role="navigation" aria-label="Paginação dos insights">
271            <li class="prev ${canPrev ? "" : "disabled"}"><a href="#" data-nav="prev" aria-label="Página anterior"><span class="icon-left-arrow-1"></span></a></li>
272            ${items}
273            <li class="next ${canNext ? "" : "disabled"}"><a href="#" data-nav="next" aria-label="Próxima página"><span class="icon-right-arrow-1"></span></a></li>
274        </ul>`;
275
276        target.querySelectorAll("[data-page]").forEach(a => a.addEventListener("click", e => {
277            e.preventDefault();
278            goToPage(Number(a.dataset.page), totalPages);
279        }));
280        const prev = target.querySelector('[data-nav="prev"]');
281        const next = target.querySelector('[data-nav="next"]');
282        if (prev) prev.addEventListener("click", e => { e.preventDefault(); if (canPrev) goToPage(state.page - 1, totalPages); });
283        if (next) next.addEventListener("click", e => { e.preventDefault(); if (canNext) goToPage(state.page + 1, totalPages); });
284    }
285
286    function scrollToList() {
287        const el = document.querySelector(".blog-list");
288        if (el) window.scrollTo({ top: Math.max(0, el.getBoundingClientRect().top + window.scrollY - 120), behavior: "smooth" });
289    }
290
291    function renderCategories(categories) {
292        state.categories = categories || [];
293        const target = document.getElementById("wix-blog-categories");
294        if (!target) return;
295        const all = `<li class="${!state.categoryId && !state.categoryLabel ? "active" : ""}"><a href="#" data-category-all>Todos os insights</a></li>`;
296        target.innerHTML = all + state.categories.map(c => `<li class="${state.categoryId === c.id || state.categoryLabel === c.label ? "active" : ""}"><a href="#" data-category-id="${escapeHtml(c.id)}" data-category-label="${escapeHtml(c.label)}">${escapeHtml(c.label)}</a></li>`).join("");
297        const reset = target.querySelector("[data-category-all]");
298        if (reset) reset.addEventListener("click", e => { e.preventDefault(); state.categoryId = null; state.categoryLabel = null; resetPaging(); refreshList(); });
299        target.querySelectorAll("[data-category-id]").forEach(a => a.addEventListener("click", e => {
300            e.preventDefault();
301            if (state.mode === "live") { state.categoryId = a.dataset.categoryId; state.categoryLabel = a.dataset.categoryLabel; }
302            else { state.categoryId = null; state.categoryLabel = a.dataset.categoryLabel; }
303            resetPaging(); refreshList();
304        }));
305    }
306
307    function resetPaging() { state.page = 1; state.total = 0; state.hasNext = false; }
308
309    function filteredFallback() {
310        const q = state.search.trim().toLocaleLowerCase("pt-BR");
311        return fallback.filter(post => {
312            const catOk = !state.categoryLabel || (post.categories || []).includes(state.categoryLabel);
313            const hay = ((post.title || "") + " " + (post.excerpt || "")).toLocaleLowerCase("pt-BR");
314            const qOk = !q || hay.includes(q);
315            return catOk && qOk;
316        });
317    }
318
319    function loadFallback() {
320        state.mode = "fallback";
321        const all = filteredFallback();
322        const totalPages = Math.max(1, Math.ceil(all.length / PAGE_SIZE));
323        if (state.page > totalPages) state.page = totalPages;
324        const start = (state.page - 1) * PAGE_SIZE;
325        state.total = all.length;
326        state.hasNext = state.page < totalPages;
327        renderList(all.slice(start, start + PAGE_SIZE));
328        renderPagination({ total: all.length });
329        renderCategories(fallbackCategories());
330    }
331
332    async function loadLive() {
333        const data = await queryPosts({
334            offset: (state.page - 1) * PAGE_SIZE,
335            categoryId: state.categoryId,
336            search: state.search.trim()
337        });
338        const posts = data.posts || [];
339        const meta = data.pagingMetadata || {};
340        state.total = Number(meta.total) || state.total;
341        state.hasNext = !!meta.hasNext;
342        renderList(posts);
343        renderPagination(meta);
344        renderCategories(state.liveCategories);
345        return posts.length;
346    }
347
348    async function refreshList() {
349        if (state.mode === "live") {
350            try { await loadLive(); return; } catch (err) { console.warn("Wix Blog indisponível, usando fallback visual.", err); }
351        }
352        loadFallback();
353    }
354
355    function setupSearch() {
356        const form = document.getElementById("wix-blog-search-form");
357        const input = document.getElementById("wix-blog-search-input");
358        if (!form || !input) return;
359        form.addEventListener("submit", e => {
360            e.preventDefault();
361            state.search = input.value || "";
362            resetPaging();
363            refreshList();
364        });
365    }
366
367    async function initList() {
368        setupSearch();
369        try {
370            state.liveCategories = await getCategories();
371            const data = await queryPosts({ offset: 0 });
372            const livePosts = data.posts || [];
373            if (livePosts.length) {
374                state.mode = "live";
375                const meta = data.pagingMetadata || {};
376                state.total = Number(meta.total) || 0;
377                state.hasNext = !!meta.hasNext;
378                renderList(livePosts);
379                renderPagination(meta);
380                renderCategories(state.liveCategories);
381                return;
382            }
383        } catch (err) {
384            console.warn("Preview do Wix Blog: ainda sem publicações ao vivo; exibindo fallback visual.", err);
385        }
386        loadFallback();
387    }
388
389    function wixImageSrc(imageLike) {
390        if (!imageLike) return "";
391        if (imageLike.url) return safeUrl(imageLike.url);
392        if (imageLike.src && imageLike.src.url) return safeUrl(imageLike.src.url);
393        const id = imageLike.id || (imageLike.src && imageLike.src.id);
394        return id ? "https://static.wixstatic.com/media/" + encodeURIComponent(id).replace(/%7E/g, "~") : "";
395    }
396
397    function normalizeInternalLink(url) {
398        const raw = safeUrl(url);
399        if (!raw) return "";
400        try {
401            const parsed = new URL(raw, window.location.href);
402            const host = parsed.hostname.replace(/^www\./, "").toLowerCase();
403            if (host === "infonova.com.br") {
404                const parts = parsed.pathname.split("/").filter(
404Boolean);
405                const last = parts[parts.length - 1] || "";
406                if (last && !last.includes(".") && !["solucoes","cases","sobre","contato","metodologia"].includes(last)) return postHref(last);
407            }
408        } catch (_) {}
409        return raw;
410    }
411
412    function renderTextNode(node) {
413        let value = escapeHtml(node && node.textData && node.textData.text || "");
414        const decorations = node && node.textData && node.textData.decorations || [];
415        decorations.forEach(dec => {
416            const type = dec.type || "";
417            if (type === "BOLD") value = `<strong>${value}</strong>`;
418            else if (type === "ITALIC") value = `<em>${value}</em>`;
419            else if (type === "UNDERLINE") value = `<u>${value}</u>`;
420            else if (type === "LINK" && dec.linkData && dec.linkData.link) {
421                const link = dec.linkData.link;
422                const href = normalizeInternalLink(link.url || "");
423                if (href) value = `<a href="${escapeHtml(href)}"${link.target === "BLANK" ? ' target="_blank" rel="noopener"' : ""}>${value}</a>`;
424            }
425        });
426        return value;
427    }
428
429    function renderRicosNode(node) {
430        if (!node) return "";
431        const children = () => (node.nodes || []).map(renderRicosNode).join("");
432        switch (node.type) {
433            case "TEXT": return renderTextNode(node);
434            case "PARAGRAPH": return `<p>${children()}</p>`;
435            case "HEADING": {
436                const level = Math.min(6, Math.max(2, Number(node.headingData && node.headingData.level || 2)));
437                return `<h${level}>${children()}</h${level}>`;
438            }
439            case "BULLETED_LIST": return `<ul>${children()}</ul>`;
440            case "ORDERED_LIST": return `<ol>${children()}</ol>`;
441            case "LIST_ITEM": return `<li>${children()}</li>`;
442            case "BLOCKQUOTE": return `<blockquote>${children()}</blockquote>`;
443            case "CODE_BLOCK": return `<pre><code>${children()}</code></pre>`;
444            case "DIVIDER": return `<hr>`;
445            case "IMAGE": {
446                const data = node.imageData || {};
447                const src = wixImageSrc(data.image);
448                if (!src) return "";
449                const alt = data.altText || "";
450                const linked = data.link && data.link.url ? normalizeInternalLink(data.link.url) : "";
451                const img = `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" loading="lazy" decoding="async">`;
452                return linked ? `<p class="wix-blog-rich-image"><a href="${escapeHtml(linked)}">${img}</a></p>` : `<p class="wix-blog-rich-image">${img}</p>`;
453            }
454            case "BUTTON": {
455                const data = node.buttonData || {};
456                const href = data.link ? normalizeInternalLink(data.link.url || "") : "";
457                return href ? `<p><a class="thm-btn" href="${escapeHtml(href)}">${escapeHtml(data.text || "Saiba mais")}<span class="icon-right-arrow"></span></a></p>` : "";
458            }
459            case "TABLE": return `<div class="wix-blog-table-wrap"><table>${children()}</table></div>`;
460            case "TABLE_ROW": return `<tr>${children()}</tr>`;
461            case "TABLE_CELL": return `<td>${children()}</td>`;
462            case "CAPTION": return `<figcaption>${children()}</figcaption>`;
463            case "LAYOUT": case "LAYOUT_CELL": case "CARD": case "SMART_BLOCK": case "SMART_BLOCK_CELL": return children();
464            default: return children();
465        }
466    }
467
468    function renderRicos(richContent) {
469        if (!richContent || !Array.isArray(richContent.nodes)) return "";
470        return richContent.nodes.map(renderRicosNode).join("");
471    }
472
473    function findFallback(slug) { return fallback.find(p => p.slug === slug) || null; }
474
475    function setMeta(post) {
476        const title = post.title || "Insight Infonova";
477        document.title = title + " | Infonova";
478        const heading = document.getElementById("wix-blog-page-title");
479        if (heading) heading.textContent = title;
480        let desc = document.querySelector('meta[name="description"]');
481        if (!desc) { desc = document.createElement("meta"); desc.name = "description"; document.head.appendChild(desc); }
482        desc.content = String(post.excerpt || post.contentText || "").replace(/\s+/g, " ").slice(0, 160);
483
484        const slug = post.slug || post.seoSlug || slugFromLocation();
485        const canonicalUrl = slug ? "https://www.infonova.com.br" + postHref(slug) : "https://www.infonova.com.br/blog.html";
486        let canonical = document.querySelector('link[rel="canonical"]');
487        if (!canonical) { canonical = document.createElement("link"); canonical.rel = "canonical"; document.head.appendChild(canonical); }
488        canonical.href = canonicalUrl;
489
490        const setMetaProp = (property, content) => {
491            let el = document.querySelector('meta[property="' + property + '"]');
492            if (!el) { el = document.createElement("meta"); el.setAttribute("property", property); document.head.appendChild(el); }
493            el.setAttribute("content", content || "");
494        };
495        setMetaProp("og:type", "article");
496        setMetaProp("og:title", title);
497        setMetaProp("og:description", desc.content);
498        setMetaProp("og:url", canonicalUrl);
499        const image = post.image || liveImage(post);
500        if (image) setMetaProp("og:image", image);
501    }
502
503    async function renderArticle(post, isFallback) {
504        const target = document.getElementById("wix-blog-article");
505        if (!target) return;
506        setMeta(post);
507        let cats = categoryLabels(post);
508        if (!cats.length && isFallback) cats = post.categories || [];
509        const image = post.image || liveImage(post);
510        const date = formatDate(post.firstPublishedDate || post.date);
511        const author = post.author || "Infonova";
512        const body = isFallback ? (post.contentHtml || "") : renderRicos(post.richContent);
513        target.innerHTML = `
514            <div class="blog-details__single-content">
515                <ul class="blog-details__meta list-unstyled">
516                    <li><a href="/blog.html"><span class="far fa-calendar-alt"></span>${escapeHtml(date)}</a></li>
517                    ${cats.length ? `<li><a href="/blog.html"><span class="far fa-folder"></span>${escapeHtml(cats[0])}</a></li>` : ""}
518                </ul>
519            </div>
520            ${image ? `<div class="blog-details__img wix-blog-hero"><img src="${escapeHtml(image)}" alt="${escapeHtml(post.title || "")}" decoding="async"></div>` : ""}
521            <div class="wix-blog-rich-content">${body || `<p>${escapeHtml(post.excerpt || "Conteúdo indisponível.")}</p>`}</div>
522            ${cats.length ? `<ul class="blog-details__meta list-unstyled wix-blog-bottom-tags"><li class="tags d-flex flex-column flex-lg-row align-items-baseline gap-3"><a class="mb-3" href="/blog.html"><span class="far fa-tag"></span>Tags: </a><div class="d-flex align-items-center gap-2 flex-wrap">${cats.map(c => `<span><a href="/blog.html">${escapeHtml(c)}</a></span>`).join("")}</div></li></ul>` : ""}
523            <div class="blog-details__single-content autor d-flex align-items-center align-items-lg-start flex-lg-row flex-column gap-3 wix-blog-author"><div class="nome text-center text-lg-start"><h2>${escapeHtml(author)}</h2><p>Conteúdo publicado pela Infonova Tecnologia</p></div></div>`;
524    }
525
526    function slugFromLocation() {
527        const params = new URLSearchParams(window.location.search);
528        const q = params.get("slug");
529        if (q) return q;
530        const parts = window.location.pathname.split("/").filter(Boolean);
531        const last = parts[parts.length - 1] || "";
532        if (last === "governanca-de-ti-insight.html") return "governanca-de-ti";
533        if (last.endsWith(".html") && !["insight-interno.html", "insight.html", "blog.html"].includes(last)) return decodeURIComponent(last.slice(0, -5));
534        return last && !last.endsWith(".html") ? decodeURIComponent(last) : "";
535    }
536
537    async function initArticle() {
538        const slug = slugFromLocation() || (fallback[0] && fallback[0].slug) || "";
539        if (!slug) return;
540        try {
541            state.liveCategories = await getCategories();
542            const data = await getPostBySlug(slug);
543            if (data && data.post) { await renderArticle(data.post, false); return; }
544        } catch (err) {
545            console.warn("Artigo ainda não está publicado no Wix Blog; usando fallback visual quando disponível.", err);
546        }
547        const local = findFallback(slug);
548        if (local) { await renderArticle(local, true); return; }
549        const target = document.getElementById("wix-blog-article");
550        if (target) target.innerHTML = `<div class="wix-blog-empty"><h3>Insight ainda não disponível.</h3><p>
550Este artigo está em preparação no novo blog da Infonova.</p><p><a href="/blog.html" class="thm-btn">Voltar aos insights<span class="icon-right-arrow"></span></a></p></div>`;
551    }
552
553    window.InfonovaWixBlog = { initList, initArticle };
554})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.