Список пользователей

1
Админ
Постов: 162
2
Элита
Постов: 45
3
VIP
Постов: 35
4
Проверенные
Постов: 31
5
Проверенные
Постов: 30
6
Пользователи
Постов: 27
7
VIP
Постов: 26
8
Пользователи
Постов: 24

  • Страница 1 из 1
  • 1
Парсер последних тем форума стильный вывод постов UCOZ V 1.0
Дата: Понедельник, 10.11.2025, 00:42 | Сообщение # 1 | | Написал: Узнаваемый
Автор темы
Мурчанн не в сети
        Сообщений:162
         Регистрация:20.10.2016

Представляю новый 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>




Если вы заметите какие-либо проблемы , не переживайте, ничего сразу не бывает идеальным. Будем совершенствовать версии парсера шаг за шагом. Ваши идеи и предложения всегда приветствуются они помогут сделать инструмент ещё удобнее и функциональнее для всех, кто работает с форумом.

Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
  • Страница 1 из 1
  • 1
Поиск: