RSS в 66 социальных сетей: Facebook, Instagram, X, LinkedIn, Telegram и другие Блог Партнерская программа Контакты
Войти Начать бесплатно
Updated: 2026-10-22
hapi RSS Feed: Route, the rss Package and Auto-Post

Short answer: To serve an RSS feed from a hapi application, add a GET route for /feed.xml, read the newest published rows from your database, build the document with the rss package, answer conditional requests with the h.entity helper that hapi provides, and return the XML with the application/rss+xml content type and a public cache policy. Compute the ETag from the data you query, not from the finished XML, test the route with curl and a validator, then give the public address to an auto-poster so every new item reaches your social channels.

The hapi framework is known for configuration over code, built-in validation and a plugin system that keeps large Node.js services tidy. Teams that use it run APIs, content back ends and internal tools, and sooner or later such a service publishes something that people want to follow: articles, release notes, offers, events or listings. A feed is the standard way to let readers, aggregators and auto-posters watch that content without an account or an API key. This guide builds a complete hapi RSS feed with a clean query, stable identifiers, correct dates and conditional requests, explains each decision and ends with the checks that keep the feed dependable once other systems rely on it.

When a hapi service needs a feed

A feed is worth adding whenever the service publishes things that people outside it want to follow:

  • articles, news or announcements stored in a database and served by the application;
  • release notes or a changelog for a product, a library or an internal tool;
  • listings, events, offers or openings added by staff or by users;
  • any content that should also appear on social networks without being copied by hand.

If your content already lives in a blog or a CMS, use the feed it provides instead; see how to find your WordPress feed link. If you use another Node.js framework, the same idea works in Express и Fastify; only the route definition changes.

Set up the project

Create a project and install hapi, the rss package and a database driver. The example uses SQLite through better-sqlite3 so that it runs anywhere, but the query and the loop stay the same with PostgreSQL or MySQL.

mkdir feedapp && cd feedapp
npm init -y
npm install @hapi/hapi rss better-sqlite3

Keep the public address of the site in configuration rather than reading it from the incoming request. Behind a reverse proxy, the request may show an internal host name or a plain http scheme, and a feed full of internal addresses is useless to every reader. The example reads the address from an environment variable with a safe default.

The complete route

The file below creates the server, defines one route and starts listening. The condition on the publication time matters: without it, an item scheduled for tomorrow would appear in the feed today, and an auto-poster would share it before it exists on the website. Publication times are stored as UTC timestamps in seconds.

'use strict';

const crypto = require('crypto');
const Hapi = require('@hapi/hapi');
const RSS = require('rss');
const Database = require('better-sqlite3');

const SITE_URL = process.env.SITE_URL || 'https://www.example.com';
const db = new Database('data/site.db', { readonly: true });
const latest = db.prepare(
  `SELECT id, title, slug, summary, published_at
     FROM posts
    WHERE status = 'published' AND published_at <= ?
    ORDER BY published_at DESC
    LIMIT 20`
);

const init = async () => {
  const server = Hapi.server({ port: 3000, host: '0.0.0.0' });

  server.route({
    method: 'GET',
    path: '/feed.xml',
    options: { cache: { privacy: 'public', expiresIn: 300000 } },
    handler: (request, h) => {
      const rows = latest.all(Math.floor(Date.now() / 1000));

      const etag = crypto.createHash('sha256')
        .update(JSON.stringify(rows))
        .digest('hex')
        .slice(0, 32);
      const notModified = h.entity({ etag });
      if (notModified) return notModified;

      const newest = rows.length ? rows[0].published_at : 0;
      const feed = new RSS({
        title: 'Example News',
        description: 'The latest posts from Example News',
        feed_url: SITE_URL + '/feed.xml',
        site_url: SITE_URL + '/',
        language: 'en',
        pubDate: new Date(newest * 1000),
      });

      for (const row of rows) {
        feed.item({
          title: row.title,
          description: row.summary,
          url: SITE_URL + '/' + row.slug + '/',
          guid: 'post-' + row.id,
          date: new Date(row.published_at * 1000),
        });
      }

      return h.response(feed.xml({ indent: true })).type('application/rss+xml');
    },
  });

  await server.start();
  console.log('Feed on', server.info.uri);
};

init();

Run it with node and open the route in a browser or with curl. You should see well-formed XML with at most twenty items, newest first. The rss package wraps text fields in CDATA sections, so titles and summaries containing an ampersand or an angle bracket stay valid, and it writes the guid element with isPermaLink set to false, which is what you want for an identifier that is not an address.

Why the ETag comes from the data

The rss package writes the current time into the last build date of every document it generates. That is harmless for a page that is rendered once, but it is a trap for a conditional request: if you hashed the finished XML, the hash would change every second and the ETag would never match. The route would never answer 304, and every poll would download the whole file. The example avoids the problem by hashing the rows returned by the query, which change only when the content changes, and by checking the entity before it builds any XML. That also saves the work of generating the document for requests that end in a 304. The same rule applies to any generator that stamps the current time into its output, in any language.

What h.entity does for you

The h.entity helper belongs to the response toolkit of hapi. It sets the ETag header, compares it with the If-None-Match header of the request and, when they match, returns a ready-made 304 response that the handler can return as it is. When they do not match, it returns nothing and the handler carries on to build the full document. The route option cache adds a Cache-Control header that allows shared caches to keep the response for five minutes. With both in place, a poll that finds nothing new costs the server one small query and almost no bandwidth.

Where each field comes from

Feed fieldSource in the exampleRule to keep
titleThe title columnPlain text, no markup
linkSite address plus slugAbsolute address that returns 200 directly
guidThe row id with a prefixNever changes, even if the slug changes
pubDateThe publication timestamp in UTCRFC 822 format, never in the future
descriptionThe summary columnOne or two sentences that make sense alone

The identifier is the most important decision. An auto-poster remembers which items it has already shared by their GUID. If the value changes when someone edits a slug or republishes an article, the same story is shared again. Using the database id with a fixed prefix avoids that. The problem and its fixes are described in why a feed auto-posts the same article twice.

Dates in the right format

RSS dates follow RFC 822, with English abbreviations for days and months. The rss package takes JavaScript Date objects and writes them in UTC with the GMT suffix, which every parser accepts, so you never format a date by hand. Make sure the value you pass is a real Date built from a UTC timestamp, and not a string from the database that the driver returned in local time. Do not use ISO 8601 in an RSS document; that format belongs to Atom feeds, and many RSS readers reject it. If a validator complains about dates, read the guide to RSS date format errors.

Caching and conditional requests in practice

Auto-posters, readers and search engines request a feed often, and almost always nothing has changed. The background to the two techniques used here is in the guide to ETags and conditional GET и the guide to feed caching. Choose the cache period with care. A long period delays every new post by the same amount, because shared caches keep serving the old list until it expires. A few minutes is a good balance for most sites. If the table grows large, add an index on the status and publication columns so that the query stays fast.

Running hapi in production

In production, run the service under a process manager and put a reverse proxy in front of it that terminates HTTPS. Keep the public address in your own setting rather than trusting forwarded headers, so that links in the feed are always correct. Check that the proxy does not strip the ETag and Cache-Control headers and that it does not add its own long cache that keeps old XML for too long. After every deployment, request the feed once to confirm that it still returns 200 and that the address is exactly the one you gave to the auto-poster. If the feed ever stops updating, this checklist shows what to inspect first.

Images and richer items

An auto-poster is more useful when each item carries a picture. The rss package accepts an enclosure object on each item with the address, the size in bytes and the type of the image, and you can fill it from an image column. Use an absolute https address and a file of reasonable size. Without an image, many networks show a plain text card. How tools read these elements is explained in the guide to enclosure tags. If you want the full article body in the feed instead of a summary, read the CDATA guide first.

Test the feed before you connect it

  1. Request the route with curl and look at the headers: the status should be 200, the content type should be application/rss+xml and an ETag should be present.
  2. Repeat the request with the ETag in an If-None-Match header and confirm that a 304 comes back.
  3. Paste the public address into the W3C Feed Validation Service and fix every error.
  4. Insert a test row and check that it appears at the top of the feed on the next request.
  5. Set a publication time in the future and check that the item stays hidden until then.

There is a longer checklist in the feed validation guide and a short curl routine in the guide to debugging a feed with curl.

Common mistakes to avoid

  • Hashing the finished XML. Generators that print the current time make the hash change on every request.
  • Future items. Filter by publication time so scheduled content stays hidden until its moment.
  • Unstable GUIDs. Use an identifier that never changes.
  • Dates as local strings. Build Date objects from UTC timestamps.
  • Addresses taken from the request. Build links from the configured public address.
  • Changing the feed address. Keep the route, or redirect the old one permanently.

How PostRSS works with a hapi feed

PostRSS has been publishing RSS and Atom feeds to social networks since 2014. Paste the address of your feed route, connect your accounts and choose how each post is built from the title, the description or the page title. PostRSS checks the feed as often as every minute and shares each new item, with its image and link, to the channels you choose among 66 supported networks, messengers, team chats and blogs, including LinkedIn, X, Bluesky, Mastodon and Telegram. Keyword filters, UTM parameters and posting windows let you tune each channel, and webhooks connect PostRSS to your own systems. See the PostRSS features page and the plan limits on the pricing page. More developer guides are under the Technical Guide tag.

Связанное чтение

The bottom line

The hapi framework needs only a route, a query and the rss package to serve a dependable RSS feed: select published items dated in the past, use a stable GUID, pass real Date objects, compute the ETag from the queried data, let h.entity answer conditional requests and send the result with the right content type and a public cache policy. Test the route with curl and a validator, keep the address unchanged and connect it to an auto-poster. After that, every item you publish can reach your social channels automatically.

FAQ

Does hapi have a built-in RSS generator?

No. Hapi has no feed support, but the rss package produces a valid RSS document from a few calls, and hapi returns it with the right content type and handles conditional requests through h.entity.

Why does my hapi feed never return 304?

Most often the ETag is computed from a body that changes between requests. Some generators write the current time into the document. Compute the ETag from the queried rows instead, and check it before building the XML.

How many items should the feed contain?

Between ten and twenty of the newest published items is enough. An auto-poster only needs recent items, and a smaller file is faster to download and parse.

How do I stop old posts from being shared again?

Give every item a stable identifier and use it as the GUID. Avoid editing the publication date of items that are already public, because some tools treat a changed date as a new item.

Can I use this feed with PostRSS?

Yes. PostRSS reads any valid RSS or Atom address that is reachable without a login. Paste the address, connect your channels and new items are shared as soon as they appear.

New guides, once a month

What changed in the networks, what broke, and how to fix it before it costs you reach.

We send a confirmation e-mail first. Unsubscribe any time.

Другие инструменты нашей команды

Создано Internet Solutions — командой PostRSS. Каждый продукт экономит ваше время по-своему.

ИИ-чат для сайтов Talkmio Ваш сайт отвечает посетителям круглосуточно на основе вашего контента и на их языке. Бесплатный план · без карты ИИ-ассистент Ask Mio Чат, код, дизайн, тексты и исследования. Mio выбирает лучшую модель для каждой задачи. Бесплатный план ИИ-автопилот для блога и соцсетей AI Blog Autopilot ИИ пишет SEO-статьи на 2 000–3 000 слов и публикует каждую в 60 соцсетях. Первые 3 статьи бесплатно Проверка состояния сайта Аудит ИИ сайта SEO, скорость, SSL, безопасность и настройки почты в одном отчёте — в порядке важности исправлений. Первый аудит бесплатно Глубокий SEO-аудит Аудит SEO ИИ сайта Полный SEO-обход по 7 направлениям, включая видимость в ИИ-поиске, с исправлениями по степени влияния. Первый аудит бесплатно RSS и товарные фиды Создатель RSS‑ленты Создавайте RSS из любой веб-страницы, а также товарные фиды для Google и Meta, которые обновляются сами. Бесплатный план Разработка сайтов и SEO Internet Solutions Сайты, интернет-магазины и индивидуальные системы — проектируем, создаём и поддерживаем сами. С 2011 года