Дата: Понедельник, 10.11.2025, 00:42 | Сообщение # 1 |
|
Написал: Узнаваемый
Автор темы
Мурчанн
не в сети
Сообщений: 162
Представляю новый JavaScript-парсер, который позволяет динамически выводить последние темы с форума прямо на главной странице или в любом другом блоке вашего сайта.Основные функции парсера 1. Автоматическая выборка последних тем 2. Скрипт подключается к указанной категории форума (FORUM_URL) и получает список последних тем. 3. Можно ограничить количество отображаемых тем (MAX_CARDS).Извлечение первого сообщения темы Для каждой темы парсер загружает первую публикацию. Извлекаются: 1. Автор поста 2. Дата публикации 3. Краткий текст сообщения (до 160 символов) 4. Изображение (если присутствует в первом посте)Кэширование Все данные кэшируются в localStorage браузера. 1. Время жизни кэша — 30 минут, чтобы снизить нагрузку на сервер и ускорить работу сайта. 2. Парсер обновляет данные в фоновом режиме, не перезагружая страницу, чтобы пользователи видели контент без мерцаний.Красивый дизайн карточек Автор темы отображается в обводке (border) с желтым акцентом. 1. Дата публикации оформлена с градиентным фоном, тенями и эффектами для лучшей визуальной читаемости. 2. Кнопка “Read more” ведет на тему полностью.Поддержка миниатюр изображений из поста или дефолтной картинки при их отсутствии. Безопасность и адаптивность 1. Все ссылки обрабатываются функцией toAbsolute(), чтобы корректно работать с относительными URL. 2. Текст сообщений безопасно экранируется с помощью escapeHtml() для предотвращения XSS.Скрипт работает полностью на стороне клиента, не требует серверного кода. Итог С помощью этого парсера можно красиво и компактно отображать последние темы форума, полностью контролировать дизайн и кэширование, а также минимизировать нагрузку на сервер.Важно: в скрипте нужно менять только адрес FORUM_URL на адрес вашего форума или сайта. Всё остальное работает без дополнительной настройки. Это идеальное решение для сайтов и блогов, которые хотят интегрировать свежий контент форума без использования серверных скриптов. Создаем глобальный блок и вставляем Парсер Код
<section class="sect flex-grow-1"> <div class="sect__header d-flex"> <h2 class="sect__title flex-grow-1"> Medical Discoveries Updates <img src="https://jordan.moy.su/forumimages/3droom.png" alt="3D Room Icon" style="height:24px; margin-left:-2px; vertical-align:middle;"> </h2> </div> <div class="dark-border"> <div id="f28_cards"></div> </div> </section> <style> /* Контейнер карточки */ #f28_cards { display: block; width: 100%; } /* Одна большая карточка */ .f28_card { display: flex; flex-direction: column; width: 100%; text-decoration: none; color: #fff; transition: transform 0.3s ease, opacity 0.3s ease; margin-bottom: 20px; border-radius: 12px; /* скругление */ overflow: hidden; box-shadow: 0 8px 25px rgba(0,0,0,0.3); /* тень */ background: rgba(15,15,20,0.8); /* полупрозрачный фон для объёма */ } /* Обёртка картинки с заголовком */ .f28_img_wrapper { position: relative; width: 100%; height: 240px; overflow: hidden; } .f28_img_wrapper img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.4s ease; } .f28_card:hover .f28_img_wrapper img { transform: scale(1.05); } /* Заголовок поверх картинки */ .f28_title { position: absolute; bottom: 12px; left: 12px; right: 12px; font-size: 2.2rem; font-weight: 700; color: #fff; text-shadow: 0 2px 6px rgba(0, 0, 0, 0.7); line-height: 1.25; } /* Оболочка описания */ .f28_desc_wrapper { background: rgba(0,0,0,0.45); padding: 12px 14px; overflow: hidden; border-top: 1px solid rgba(255,255,255,0.05); display: flex; flex-direction: column; gap: 6px; } /* Описание текста */ .f28_desc { font-size: 1rem; color: #ccc; line-height: 1.6em; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical; } /* Нижняя строка: автор и дата + кнопка */ .f28_meta { display: flex; justify-content: space-between; align-items: center; font-size: 0.85rem; color: #aaa; } .f28_button { background: #855b97; color: #fff; padding: 6px 12px; border-radius: 6px; text-decoration: none; font-weight: 600; transition: background 0.3s ease; } .f28_button:hover { background: #a170b5; } /* Нижняя строка: дата слева, кнопка справа */ .f28_footer { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; } .f28_date { font-size: 0.85rem; color: #aaa; } .f28_button { background: #855b97; color: #fff; padding: 6px 12px; border-radius: 6px; text-decoration: none; font-weight: 600; transition: background 0.3s ease; white-space: nowrap; } .f28_button:hover { background: #a170b5; } /* Адаптивность */ @media (max-width: 1024px) { .f28_card { flex: 0 0 calc(33.333% - 10px); } } @media (max-width: 768px) { .f28_card { flex: 0 0 calc(50% - 10px); } } @media (max-width: 500px) { .f28_card { flex: 0 0 100%; } } /* Автор сверху с желтой обводкой */ .f28_author { position: absolute; top: 10px; left: 12px; color: #fff; font-size: 1rem; font-weight: 600; text-shadow: 1px 1px 4px rgba(0,0,0,0.7); padding: 2px 6px; border: 2px solid yellow; border-radius: 4px; z-index: 2; background: rgba(0,0,0,0.3); } /* Дата снизу слева */ .f28_footer { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; } .f28_date { font-size: 0.85rem; color: #ccc; text-align: left; } </style> <script> (async function forum28Parser() { const container = document.querySelector('#f28_cards'); if (!container) return; const FORUM_URL = 'https://jordan.moy.su/forum/28'; const MAX_CARDS = 8; const CACHE_KEY = 'f28_cards_cache_v10'; const CACHE_TIME = 30 * 60 * 1000; // 30 минут const toAbsolute = (url) => url && !url.startsWith('http') ? 'https://jordan.moy.su' + (url.startsWith('/') ? url : '/' + url) : url; const escapeHtml = (text) => String(text||'').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); // Создание карточки const createCard = (data) => { const card = document.createElement('a'); card.className = 'f28_card'; card.href = data.href; card.target = '_self'; card.dataset.href = data.href; card.innerHTML = ` <div class="f28_img_wrapper"> <img src="${data.img || '/forumimages/Newsletter-3.png'}" alt="${escapeHtml(data.title)}"> <div class="f28_author" style="border:2px solid yellow; padding:2px 6px; display:inline-block; margin-bottom:4px;"> ${escapeHtml(data.author || 'Автор')} </div> <div class="f28_title">${escapeHtml(data.title || 'Без названия')}</div> </div> <div class="f28_desc_wrapper"> <div class="f28_desc">${escapeHtml(data.text || '')}</div> <div class="f28_footer" style="display:flex; justify-content:space-between; align-items:center;"> <span class="f28_date" style=" background: linear-gradient(135deg, #0fd700, #7e1280, #f714d9); color: #FFF; padding: 4px 8px; border-radius: 4px; font-weight: 700; font-size: 10px; text-shadow: 1px 1px 2px rgba(0,0,0,0.2); box-shadow: 0 2px 6px rgba(0,0,0,0.15); display: inline-block; transition: transform 0.2s, box-shadow 0.2s; "> ${escapeHtml(data.date || 'Дата')} </span> <a href="${data.href}" class="f28_button">Read more</a> </div> </div> `; return card; }; // Плавное обновление: только новые или измененные карточки const renderCards = (cards) => { const existingCards = Array.from(container.querySelectorAll('.f28_card')); const existingMap = Object.fromEntries(existingCards.map(c => [c.dataset.href, c])); cards.forEach(data => { const exist = existingMap[data.href]; if(exist){ // Обновляем текст, автора и дату только если изменились if(exist.querySelector('.f28_title').textContent !== data.title) exist.querySelector('.f28_title').textContent = data.title; if(exist.querySelector('.f28_desc').textContent !== data.text) exist.querySelector('.f28_desc').textContent = data.text; if(exist.querySelector('.f28_author').textContent !== data.author) exist.querySelector('.f28_author').textContent = data.author; if(exist.querySelector('.f28_date').textContent !== data.date) exist.querySelector('.f28_date').textContent = data.date; } else { // Новая карточка container.appendChild(createCard(data)); } }); }; const fetchHTML = async (url) => (await fetch(url, { credentials: 'same-origin' })).text(); const parseThreadsList = (html) => { const doc = new DOMParser().parseFromString(html, 'text/html'); const arr = []; const items = doc.querySelectorAll('.threadNametd .threadLink'); for (let i=0; i<items.length && arr.length<MAX_CARDS; i++) { const a = items[i]; let href = a.getAttribute('href')||''; if(href && !href.startsWith('http')) href = toAbsolute(href); arr.push({ href, title: a.textContent.trim() }); } return arr; }; const parseFirstPost = (html) => { const doc = new DOMParser().parseFromString(html,'text/html'); let text='', img='', author='Автор', date='Дата'; const post = doc.querySelector('.post_content, .post_body, .post, .ipsType_richText'); if(post){ text = post.textContent.trim().substring(0,160); const imgEl = post.querySelector('img'); if(imgEl) img = toAbsolute(imgEl.getAttribute('src')||''); } const authorEl = doc.querySelector('a.postUser'); if(authorEl) author = authorEl.textContent.trim(); const postTdTop = Array.from(doc.querySelectorAll('td.postTdTop')).find(td => td.textContent.includes('Дата:')); if(postTdTop){ const dateMatch = postTdTop.innerHTML.match(/Дата:\s*([^|<]+)/); if(dateMatch) date = dateMatch[1].trim(); } return { text, img, author, date }; }; // Основная функция const update = async () => { try { const cached = localStorage.getItem(CACHE_KEY); if(cached){ const data = JSON.parse(cached); if(Date.now()-data.time<CACHE_TIME){ renderCards(data.cards); return; } } const listHtml = await fetchHTML(FORUM_URL); const threads = parseThreadsList(listHtml); const cards = (await Promise.all( threads.map(t => fetchHTML(t.href).then(html => ({...t, ...parseFirstPost(html)})).catch(() => t)) )).filter(Boolean); localStorage.setItem(CACHE_KEY, JSON.stringify({ time:Date.now(), cards })); renderCards(cards); } catch(err){ console.error('Ошибка парсинга форума:', err); } }; // Первичная загрузка update(); // Периодическое обновление каждые 30 минут setInterval(update, CACHE_TIME); })(); </script>
Если вы заметите какие-либо проблемы , не переживайте, ничего сразу не бывает идеальным. Будем совершенствовать версии парсера шаг за шагом. Ваши идеи и предложения всегда приветствуются они помогут сделать инструмент ещё удобнее и функциональнее для всех, кто работает с форумом.
Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.