Build a RSS reader using Next.js and Airtable

In this project I’m going to create a Web Application using Next.js.

To goal is to build an app where people can view the latest posts from a list of blogs we selected.

Think of this like “I select a list of sources and I create a website to automatically aggregate the latests content from them”

This is the end result we’ll get to:

People can click the “Add a new blog” link and submit a new blog to us, the admins, though a form:

We’ll store the data using Airtable as our backend of choice, and we’ll use it to approve the blogs proposed by our users.

The list of blog posts will be fetched from the blog posts RSS/Atom feeds.

And here comes into play one feature of Next.js that will be at the core of our app: the home page and the form pages will be static pages, generated at build time, but at the same time our API will be handled by the server-side code.

We must generate the home page at build time, in particular, because otherwise we’d quickly hit the Airtable API limits, and also it would take a lot of time to keep parsing the RSS feeds, and we’d be causing a waste of resources.

After the app is deployed on Now we’ll be able to set up a deploy hook to rebuild the application every hour, for example.

Start with create-next-app

Go into the folder you use for your projects, and run npx create-next-app.

You’ll be asked a name, you can use project-next-blogposts

Wait until the app is ready:

Now run

cd project-next-blogposts

then run

npm run dev

to start the development environment. This will make the sample application ready on port 3000:

The homepage

The sample Next.js app that’s been created by create-next-app contains one page: pages/index.js.

Open this file, remove everything that’s returned by the component, and get it to a bare bones state:

import Head from 'next/head'

const Home = () => (
  <div></div>
)

export default Home

I’m now going to add some HTML I’ve taken from the Tailwind UI repository of UI components, so we have a good structure to start from:

import Head from 'next/head'

const Home = () => (
  <div>
    <Head>
      <title>Latest posts</title>
      <link rel='icon' href='/favicon.ico' />
      <link
        rel='stylesheet'
        href='https://cdn.jsdelivr.net/npm/@tailwindcss/ui@latest/dist/tailwind-ui.min.css'
      />
    </Head>

    <div>
      <header className='bg-white shadow'>
        <div className='max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8'>
          <div className='flex justify-between'>
            <h1 className='text-3xl font-bold leading-tight text-gray-900'>
              Latest posts
            </h1>
          </div>
        </div>
      </header>
      <main>
        <div className='max-w-7xl mx-auto py-6 sm:px-6 lg:px-8'>
          <div className='px-4 py-4 sm:px-0'>
            <div className='border-4 rounded-lg'>
              <div className='flex flex-col'>
                <div className='-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8'>
                  <div className='align-middle inline-block min-w-full shadow overflow-hidden sm:rounded-lg'>
                    <table className='min-w-full'>
                      <thead>
                        <tr>
                          <th className='px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider'>
                            Post
                          </th>
                          <th className='px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider'>
                            Date
                          </th>
                        </tr>
                      </thead>
                      <tbody className='bg-white'>
                      </tbody>
                    </table>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </main>
    </div>
  </div>
)

export default Home

I included the Head component from next/head, so we can add the Tailwind UI CSS file, and added some static HTML.

Here’s the result:

Fetch data from the RSS feed

Now we need to feed the component some data.

To start with, I’m going to use my own blog RSS feed, available at https://flaviocopes.com/index.xml.

Install the rss-parser npm library:

npm install rss-parser

and at the top of index.js add

import Parser from 'rss-parser'

Now create a new exported getStaticProps function from the component:

export async function getStaticProps(context) {

}

We use this function to export an object that contains a props property. This will be feeded to the component:

//...

const Home = (props) => (

)

export async function getStaticProps(context) {

  return {
    props: {
      //...
    }
  }

}

export default Home

In particular, we’ll be exporting a posts array that contains the lists of posts we want to display.

We use the parser library we installed to get the posts from https://flaviocopes.com/index.xml, like this:

export async function getStaticProps(context) {
  const parser = new Parser()
  
  const data = await parser.parseURL('https://flaviocopes.com/index.xml')

  const posts = []
  data.items.slice(0, 10).forEach((item) => {
    posts.push({
      title: item.title,
      link: item.link,
      date: item.isoDate,
      name: 'Flavio Copes'
    })
  })

  return {
    props: {
      posts
    }
  }
}

Now in the component JSX we iterate through the posts, in the table body, and we print their data:

<tbody className='bg-white'>
  {props.posts
    .sort((a, b) => new Date(b.date) - new Date(a.date))
  .map((value, index) => {
    return (
      <tr key={index}>
        <td className='px-6 py-4 whitespace-no-wrap border-b border-gray-200'>
          <div className='flex items-center'>
            <div className='ml-4'>
              <div className='text-sm leading-5 font-medium text-gray-900 underline'>
                <a href={value.link}>{value.title}</a>
              </div>
              <div className='text-sm leading-5 text-gray-500'>
                {value.name}
              </div>
            </div>
          </div>
        </td>
        <td className='px-6 py-4 whitespace-no-wrap border-b border-gray-200'>
          <div className='text-sm leading-5 text-gray-900'>
            {new Date(value.date).toDateString()}
          </div>
          <div className='text-sm leading-5 text-gray-500'></div>
        </td>
      </tr>
    )
  })}
</tbody>

Notice how I use the sort() function to order the posts by date. The feed should already return posts in the order we want, but this will be more useful as we’ll get data from multiple feeds later on.

Here is the result so far:

Thanks to Tailwind, this is automatically nicely formatted just by adding classes to the JSX.

This is a great starting point for the rest of our application.

We now need to get multiple feeds, so we can get data from multiple sources.

To do so, we’re going to use Airtable.

Create the Airtable database

Airtable is one of my favorite go-to solutions as a quick database for prototypes and small apps.

The reason I like it is that we don’t have to setup anything, and it has a nice API.

Create a new table, and add 6 fields:

  • name
  • email
  • blogurl
  • feedurl
  • notes
  • approved

All text fields, except approved which is a checkbox.

We’ll be able to manually approve blogs from the Airtable interface, simply by clicking the column.

We’re going to fill the Airtable table by letting our users submit new blogs, which will be added through a form.

Let’s build the form!

Create the form

Create a new page in Next.js, in pages/form.js:

export default function () {
  return (
    <div>
    </div>
  )
}

Try and visit http://localhost:3000/form, you’ll see an empty page.

Like we did before, let’s add some JSX to generate the HTML structure of the page:

import Head from 'next/head'

export default function () {
  return (
    <div>
      <Head>
        <title>Add new blog</title>
        <link rel='icon' href='/favicon.ico' />
        <link
          rel='stylesheet'
          href='https://cdn.jsdelivr.net/npm/@tailwindcss/ui@latest/dist/tailwind-ui.min.css'
        />
      </Head>
      <div>
        <header className='bg-white shadow'>
          <div className='max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8'>
            <div className='flex justify-between'>
              <h1 className='text-3xl font-bold leading-tight text-gray-900'>
                Add new blog
              </h1>
            </div>
          </div>
        </header>

        <main>
          <p className='text-center pb-5'></p>

          <div className='max-w-7xl mx-auto sm:px-6 lg:px-8'>
            <div>
              <div className='max-w-3xl mx-auto sm:px-6 lg:px-8'>
                <form
                  className='mt-5 md:mt-0 md:col-span-2'
                  action=''
                  method='POST'
                >
                  <div className='shadow sm:rounded-md sm:overflow-hidden'>
                    <div className='px-4 py-5 bg-white sm:p-6'>
                      <label className='block text-sm font-medium leading-5 text-gray-700'>
                        Blog name / owner name
                      </label>
                      <input
                        required
                        className='mb-5 mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5'
                      />
                      <label className='block text-sm font-medium leading-5 text-gray-700'>
                        Email address
                      </label>
                      <input
                        required
                        type='email'
                        className='mb-5 mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5'
                      />
                      <label className='block text-sm font-medium leading-5 text-gray-700'>
                        Blog URL
                      </label>
                      <input
                        type='url'
                        required
                        className='mb-5 mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5'
                        placeholder='https://www.example.com'
                      />
                      <label className='block text-sm font-medium leading-5 text-gray-700'>
                        RSS Feed URL
                      </label>
                      <input
                        type='url'
                        required
                        className='mb-5 mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5'
                        placeholder='https://www.example.com/feed'
                      />

                      <label
                        htmlFor='about'
                        className='block text-sm leading-5 font-medium text-gray-700'
                      >
                        Notes
                      </label>
                      <div className='rounded-md shadow-sm'>
                        <textarea
                          rows='3'
                          className='form-textarea mt-1 block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5'
                          placeholder='Anything you want to tell us!'
                        ></textarea>
                      </div>
                      <p className='mt-2 text-sm text-gray-500'>
                        Your submission will be approved before appearing on the
                        site
                      </p>
                    </div>
                    <div className='px-4 py-3 bg-gray-50 text-right sm:px-6'>
                      <span className='inline-flex rounded-md shadow-sm'>
                        <button
                          type='submit'
                          className='inline-flex justify-center py-2 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition duration-150 ease-in-out'
                        >
                          Save
                        </button>
                      </span>
                    </div>
                  </div>
                </form>
              </div>
            </div>
          </div>
        </main>
      </div>
    </div>
  )
}

I included the Head component from next/head, so we can add the Tailwind UI CSS file, and added some static HTML.

Here’s the result so far:

One thing we need to do now is to link the 2 pages, the posts list and the form.

In both pages, add

import Link from 'next/link'

at the top, then in index.js right after the h1 tag add:

<p>
  <Link href='/form'>
    <p className='underline cursor-pointer mt-2'>
      <a>Add a new blog</a>
    </p>
  </Link>
</p>

in form.js:

<p>
  <Link href='/'>
    <p className='underline cursor-pointer mt-2'>
      <a>Back</a>
    </p>
  </Link>
</p>

The form submit

Now let’s handle the form submit process.

We import useState from react

import { useState } from 'react'

Then we can define local state hooks for each form element:

const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [blogurl, setBlogurl] = useState('')
const [feedurl, setFeedurl] = useState('')
const [notes, setNotes] = useState('')

Now I use them for each input element, adding the value as the value attribute, and the update function in the onChange handler function, like this:

value={name}
onChange={(event) => setName(event.target.value)}

Full code:

<form
  className='mt-5 md:mt-0 md:col-span-2'
  action=''
  method='POST'
>
  <div className='shadow sm:rounded-md sm:overflow-hidden'>
    <div className='px-4 py-5 bg-white sm:p-6'>
      <label className='block text-sm font-medium leading-5 text-gray-700'>
        Blog name / owner name
      </label>
      <input
        required
        value={name}
        onChange={(event) => setName(event.target.value)}
        className='mb-5 mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5'
      />
      <label className='block text-sm font-medium leading-5 text-gray-700'>
        Email address
      </label>
      <input
        required
        type='email'
        value={email}
        onChange={(event) => setEmail(event.target.value)}
        className='mb-5 mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5'
      />
      <label className='block text-sm font-medium leading-5 text-gray-700'>
        Blog URL
      </label>
      <input
        type='url'
        required
        value={blogurl}
        onChange={(event) => setBlogurl(event.target.value)}
        className='mb-5 mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5'
        placeholder='https://www.example.com'
      />
      <label className='block text-sm font-medium leading-5 text-gray-700'>
        RSS Feed URL
      </label>
      <input
        type='url'
        required
        value={feedurl}
        onChange={(event) => setFeedurl(event.target.value)}
        className='mb-5 mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5'
        placeholder='https://www.example.com/feed'
      />

      <label
        htmlFor='about'
        className='block text-sm leading-5 font-medium text-gray-700'
      >
        Notes
      </label>
      <div className='rounded-md shadow-sm'>
        <textarea
          value={notes}
          onChange={(event) => setNotes(event.target.value)}
          rows='3'
          className='form-textarea mt-1 block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5'
          placeholder='Anything you want to tell us!'
        ></textarea>
      </div>
      <p className='mt-2 text-sm text-gray-500'>
        Your submission will be approved before appearing on the
        site
      </p>
    </div>
    <div className='px-4 py-3 bg-gray-50 text-right sm:px-6'>
      <span className='inline-flex rounded-md shadow-sm'>
        <button
          type='submit'
          className='inline-flex justify-center py-2 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition duration-150 ease-in-out'
        >
          Save
        </button>
      </span>
    </div>
  </div>
</form>

Handle the form submit

Now I add an onSubmit event handler to the form, which is assigned a function called handleSubmit:

<form
  className='mt-5 md:mt-0 md:col-span-2'
  action=''
  method='POST'
  onSubmit={handleSubmit}
>

here’s the function:

const handleSubmit = async (event) => {
  event.preventDefault()
  //...
}

Inside the function, we’re going to hit an API we’re soon going to define, that will answer to a POST request on the /api/blog URL:

const handleSubmit = async (event) => {
  event.preventDefault()

  try {
    const res = await fetch('/api/blog', {
      method: 'POST',
      body: JSON.stringify({ name, email, blogurl, feedurl, notes }),
      headers: { 'Content-Type': 'application/json' },
    })

    const json = await res.json()
}

While I’m here I’m also adding a bit of error checking and positive / negative feedback handling from the API. If the API contains a success property, we’re going to fire an alert() and redirect back to the homepage.

Add import Router from 'next/router' to use Router.push('/')

We’re going to use a response local state through hooks.

Define

const [response, setResponse] = useState('')

and we’ll update it when the response is back:

const handleSubmit = async (event) => {
  event.preventDefault()

  try {
    const res = await fetch('/api/blog', {
      method: 'POST',
      body: JSON.stringify({ name, email, blogurl, feedurl, notes }),
      headers: { 'Content-Type': 'application/json' },
    })

    const json = await res.json()

    if (json.success) {
      alert('Thank you for submitting your blog!')
      Router.push('/')
    } else {
      setResponse(json.message)
    }
  } catch (error) {
    setResponse('An error occured while submitting the form')
  }
}

Now in the JSX, I’m printing the response in the empty element we have right after the opening main tag:

<p className='text-center pb-5'></p>

Like this:

<p className='text-center pb-5'>{response}</p>

The POST /api/blog API endpoint

Let’s now create the API endpoint.

Create a file pages/api/blog.js, and add a default export that accepts a req and res objects:

export default (req, res) => {
 
}

Those are the familiar objects that you get in any Node.js server app. We’re going to first disable any request that’s not a POST request:

export default (req, res) => {
  if (!req.method === 'POST') {
    res.status(405).end() //Method Not Allowed
    return
  }
}

Then we extract the values we need from the request body:

  const { name, email, blogurl, feedurl, notes } = req.body

and we send them using the Airtable API.

On Airtable, in the base you just created, you can see the HELP menu. Click that, and then select API documentation:

This will give you ready-to-use examples to interact with the table.

The nice thing is that they automatically include the API key and the base ID you need to use.

We’re going to use this code:

const Airtable = require('airtable')
const base = new Airtable({ apiKey: process.env.APIKEY }).base(
  'appYOURBASEID'
)

base('Table 1').create(
  [{ fields: { name, email, blogurl, feedurl, notes } }],
  (err) => {
    if (err) {
      console.error(err)
      res.status(500).end()
      return
    }
  }
)

Make sure you run

npm install airtable

to install the Airtable official library that we require.

Note that I entered my base id in the code.

The Airtable API key, however, is listed as process.env.APIKEY because I’m going to pass it from the command line, so it’s not included in the code.

Stop the process in the terminal, and run

APIKEY=keyYOURAPIKEY npm run dev

You can find your API key in the Airtable API documentation

Finally, we return the response back to the user:

res.json({
  success: true
})

Here’s the full pages/api/blog.js file content:

export default (req, res) => {
  if (!req.method === 'POST') {
    res.status(405).end() //Method Not Allowed
    return
  }

  const { name, email, blogurl, feedurl, notes } = req.body

  const Airtable = require('airtable')
  const base = new Airtable({ apiKey: process.env.APIKEY }).base(
    'appYOURBASEID'
  )

  base('Table 1').create([{ fields: { name, email, blogurl, feedurl, notes } }], (err) => {
    if (err) {
      console.error(err)
      res.status(500).end()
      return
    }
  })

  res.json({
    success: true
  })
}

Now try filling the form: the data should be saved on Airtable.

Fetching blogs using the Airtable API

Now that we have the database set up, it’s time to use the Airtable API in the index.js page, to fetch the list of blogs approved.

Tip: add a few blogs, and approve them manually on Airtable by clicking the checkbox in the approved column.

The getStaticProps() function now exports the list of blog posts from my blog, because we hardcoded that address.

export async function getStaticProps(context) {
  const parser = new Parser()
  
  const data = await parser.parseURL('https://flaviocopes.com/index.xml')

  const posts = []
  data.items.slice(0, 10).forEach((item) => {
    posts.push({
      title: item.title,
      link: item.link,
      date: item.isoDate,
      name: 'Flavio Copes'
    })
  })

  return {
    props: {
      posts,
    }
  }
}

We’re going to refactor this code, retrieving the list of feed URLs.

Here’s how I retrieve the first 100 results from Airtable:

const Airtable = require('airtable')
const base = new Airtable({ apiKey: process.env.APIKEY }).base(
  'appYOURBASEID'
)

const records = await base('Table 1').select({
  view: 'Grid view',
}).firstPage()

We only get the first page of results and we limit to the first 100 blogs listed in Airtable, as that’s the maxiumum items we can get for each API call, but I think that to start with this is a good number.

Airtable will always serve the items from item #1 to item #100, so the new blogs that will be added, even if unapproved, will not affect our results. They will not “push down” existing and approved blogs.

Now we can get the data from the records:

const feeds = records.filter((record) => {
  if (record.get('approved') === true) return true
}).map((record) => {
  return {
    id: record.id,
    name: record.get('name'),
    blogurl: record.get('blogurl'),
    feedurl: record.get('feedurl'),
  }
})

and we can process each blog RSS feed in a loop:

const posts = []

for (const feed of feeds) {
  const data = await parser.parseURL(feed.feedurl)

  data.items.slice(0, 10).forEach((item) => {
    posts.push({
      title: item.title,
      link: item.link,
      date: item.isoDate,
      name: feed.name,
    })
  })
}

Awesome! This should be the result, a mix of blog posts ordered by date:


This content originally appeared on flaviocopes.com and was authored by flaviocopes.com

In this project I’m going to create a Web Application using Next.js.

To goal is to build an app where people can view the latest posts from a list of blogs we selected.

Think of this like “I select a list of sources and I create a website to automatically aggregate the latests content from them”

This is the end result we’ll get to:

People can click the “Add a new blog” link and submit a new blog to us, the admins, though a form:

We’ll store the data using Airtable as our backend of choice, and we’ll use it to approve the blogs proposed by our users.

The list of blog posts will be fetched from the blog posts RSS/Atom feeds.

And here comes into play one feature of Next.js that will be at the core of our app: the home page and the form pages will be static pages, generated at build time, but at the same time our API will be handled by the server-side code.

We must generate the home page at build time, in particular, because otherwise we’d quickly hit the Airtable API limits, and also it would take a lot of time to keep parsing the RSS feeds, and we’d be causing a waste of resources.

After the app is deployed on Now we’ll be able to set up a deploy hook to rebuild the application every hour, for example.

Start with create-next-app

Go into the folder you use for your projects, and run npx create-next-app.

You’ll be asked a name, you can use project-next-blogposts

Wait until the app is ready:

Now run

cd project-next-blogposts

then run

npm run dev

to start the development environment. This will make the sample application ready on port 3000:

The homepage

The sample Next.js app that’s been created by create-next-app contains one page: pages/index.js.

Open this file, remove everything that’s returned by the component, and get it to a bare bones state:

import Head from 'next/head'

const Home = () => (
  <div></div>
)

export default Home

I’m now going to add some HTML I’ve taken from the Tailwind UI repository of UI components, so we have a good structure to start from:

import Head from 'next/head'

const Home = () => (
  <div>
    <Head>
      <title>Latest posts</title>
      <link rel='icon' href='/favicon.ico' />
      <link
        rel='stylesheet'
        href='https://cdn.jsdelivr.net/npm/@tailwindcss/ui@latest/dist/tailwind-ui.min.css'
      />
    </Head>

    <div>
      <header className='bg-white shadow'>
        <div className='max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8'>
          <div className='flex justify-between'>
            <h1 className='text-3xl font-bold leading-tight text-gray-900'>
              Latest posts
            </h1>
          </div>
        </div>
      </header>
      <main>
        <div className='max-w-7xl mx-auto py-6 sm:px-6 lg:px-8'>
          <div className='px-4 py-4 sm:px-0'>
            <div className='border-4 rounded-lg'>
              <div className='flex flex-col'>
                <div className='-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8'>
                  <div className='align-middle inline-block min-w-full shadow overflow-hidden sm:rounded-lg'>
                    <table className='min-w-full'>
                      <thead>
                        <tr>
                          <th className='px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider'>
                            Post
                          </th>
                          <th className='px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider'>
                            Date
                          </th>
                        </tr>
                      </thead>
                      <tbody className='bg-white'>
                      </tbody>
                    </table>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </main>
    </div>
  </div>
)

export default Home

I included the Head component from next/head, so we can add the Tailwind UI CSS file, and added some static HTML.

Here’s the result:

Fetch data from the RSS feed

Now we need to feed the component some data.

To start with, I’m going to use my own blog RSS feed, available at https://flaviocopes.com/index.xml.

Install the rss-parser npm library:

npm install rss-parser

and at the top of index.js add

import Parser from 'rss-parser'

Now create a new exported getStaticProps function from the component:

export async function getStaticProps(context) {

}

We use this function to export an object that contains a props property. This will be feeded to the component:

//...

const Home = (props) => (

)

export async function getStaticProps(context) {

  return {
    props: {
      //...
    }
  }

}

export default Home

In particular, we’ll be exporting a posts array that contains the lists of posts we want to display.

We use the parser library we installed to get the posts from https://flaviocopes.com/index.xml, like this:

export async function getStaticProps(context) {
  const parser = new Parser()
  
  const data = await parser.parseURL('https://flaviocopes.com/index.xml')

  const posts = []
  data.items.slice(0, 10).forEach((item) => {
    posts.push({
      title: item.title,
      link: item.link,
      date: item.isoDate,
      name: 'Flavio Copes'
    })
  })

  return {
    props: {
      posts
    }
  }
}

Now in the component JSX we iterate through the posts, in the table body, and we print their data:

<tbody className='bg-white'>
  {props.posts
    .sort((a, b) => new Date(b.date) - new Date(a.date))
  .map((value, index) => {
    return (
      <tr key={index}>
        <td className='px-6 py-4 whitespace-no-wrap border-b border-gray-200'>
          <div className='flex items-center'>
            <div className='ml-4'>
              <div className='text-sm leading-5 font-medium text-gray-900 underline'>
                <a href={value.link}>{value.title}</a>
              </div>
              <div className='text-sm leading-5 text-gray-500'>
                {value.name}
              </div>
            </div>
          </div>
        </td>
        <td className='px-6 py-4 whitespace-no-wrap border-b border-gray-200'>
          <div className='text-sm leading-5 text-gray-900'>
            {new Date(value.date).toDateString()}
          </div>
          <div className='text-sm leading-5 text-gray-500'></div>
        </td>
      </tr>
    )
  })}
</tbody>

Notice how I use the sort() function to order the posts by date. The feed should already return posts in the order we want, but this will be more useful as we’ll get data from multiple feeds later on.

Here is the result so far:

Thanks to Tailwind, this is automatically nicely formatted just by adding classes to the JSX.

This is a great starting point for the rest of our application.

We now need to get multiple feeds, so we can get data from multiple sources.

To do so, we’re going to use Airtable.

Create the Airtable database

Airtable is one of my favorite go-to solutions as a quick database for prototypes and small apps.

The reason I like it is that we don’t have to setup anything, and it has a nice API.

Create a new table, and add 6 fields:

  • name
  • email
  • blogurl
  • feedurl
  • notes
  • approved

All text fields, except approved which is a checkbox.

We’ll be able to manually approve blogs from the Airtable interface, simply by clicking the column.

We’re going to fill the Airtable table by letting our users submit new blogs, which will be added through a form.

Let’s build the form!

Create the form

Create a new page in Next.js, in pages/form.js:

export default function () {
  return (
    <div>
    </div>
  )
}

Try and visit http://localhost:3000/form, you’ll see an empty page.

Like we did before, let’s add some JSX to generate the HTML structure of the page:

import Head from 'next/head'

export default function () {
  return (
    <div>
      <Head>
        <title>Add new blog</title>
        <link rel='icon' href='/favicon.ico' />
        <link
          rel='stylesheet'
          href='https://cdn.jsdelivr.net/npm/@tailwindcss/ui@latest/dist/tailwind-ui.min.css'
        />
      </Head>
      <div>
        <header className='bg-white shadow'>
          <div className='max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8'>
            <div className='flex justify-between'>
              <h1 className='text-3xl font-bold leading-tight text-gray-900'>
                Add new blog
              </h1>
            </div>
          </div>
        </header>

        <main>
          <p className='text-center pb-5'></p>

          <div className='max-w-7xl mx-auto sm:px-6 lg:px-8'>
            <div>
              <div className='max-w-3xl mx-auto sm:px-6 lg:px-8'>
                <form
                  className='mt-5 md:mt-0 md:col-span-2'
                  action=''
                  method='POST'
                >
                  <div className='shadow sm:rounded-md sm:overflow-hidden'>
                    <div className='px-4 py-5 bg-white sm:p-6'>
                      <label className='block text-sm font-medium leading-5 text-gray-700'>
                        Blog name / owner name
                      </label>
                      <input
                        required
                        className='mb-5 mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5'
                      />
                      <label className='block text-sm font-medium leading-5 text-gray-700'>
                        Email address
                      </label>
                      <input
                        required
                        type='email'
                        className='mb-5 mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5'
                      />
                      <label className='block text-sm font-medium leading-5 text-gray-700'>
                        Blog URL
                      </label>
                      <input
                        type='url'
                        required
                        className='mb-5 mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5'
                        placeholder='https://www.example.com'
                      />
                      <label className='block text-sm font-medium leading-5 text-gray-700'>
                        RSS Feed URL
                      </label>
                      <input
                        type='url'
                        required
                        className='mb-5 mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5'
                        placeholder='https://www.example.com/feed'
                      />

                      <label
                        htmlFor='about'
                        className='block text-sm leading-5 font-medium text-gray-700'
                      >
                        Notes
                      </label>
                      <div className='rounded-md shadow-sm'>
                        <textarea
                          rows='3'
                          className='form-textarea mt-1 block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5'
                          placeholder='Anything you want to tell us!'
                        ></textarea>
                      </div>
                      <p className='mt-2 text-sm text-gray-500'>
                        Your submission will be approved before appearing on the
                        site
                      </p>
                    </div>
                    <div className='px-4 py-3 bg-gray-50 text-right sm:px-6'>
                      <span className='inline-flex rounded-md shadow-sm'>
                        <button
                          type='submit'
                          className='inline-flex justify-center py-2 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition duration-150 ease-in-out'
                        >
                          Save
                        </button>
                      </span>
                    </div>
                  </div>
                </form>
              </div>
            </div>
          </div>
        </main>
      </div>
    </div>
  )
}

I included the Head component from next/head, so we can add the Tailwind UI CSS file, and added some static HTML.

Here’s the result so far:

One thing we need to do now is to link the 2 pages, the posts list and the form.

In both pages, add

import Link from 'next/link'

at the top, then in index.js right after the h1 tag add:

<p>
  <Link href='/form'>
    <p className='underline cursor-pointer mt-2'>
      <a>Add a new blog</a>
    </p>
  </Link>
</p>

in form.js:

<p>
  <Link href='/'>
    <p className='underline cursor-pointer mt-2'>
      <a>Back</a>
    </p>
  </Link>
</p>

The form submit

Now let’s handle the form submit process.

We import useState from react

import { useState } from 'react'

Then we can define local state hooks for each form element:

const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [blogurl, setBlogurl] = useState('')
const [feedurl, setFeedurl] = useState('')
const [notes, setNotes] = useState('')

Now I use them for each input element, adding the value as the value attribute, and the update function in the onChange handler function, like this:

value={name}
onChange={(event) => setName(event.target.value)}

Full code:

<form
  className='mt-5 md:mt-0 md:col-span-2'
  action=''
  method='POST'
>
  <div className='shadow sm:rounded-md sm:overflow-hidden'>
    <div className='px-4 py-5 bg-white sm:p-6'>
      <label className='block text-sm font-medium leading-5 text-gray-700'>
        Blog name / owner name
      </label>
      <input
        required
        value={name}
        onChange={(event) => setName(event.target.value)}
        className='mb-5 mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5'
      />
      <label className='block text-sm font-medium leading-5 text-gray-700'>
        Email address
      </label>
      <input
        required
        type='email'
        value={email}
        onChange={(event) => setEmail(event.target.value)}
        className='mb-5 mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5'
      />
      <label className='block text-sm font-medium leading-5 text-gray-700'>
        Blog URL
      </label>
      <input
        type='url'
        required
        value={blogurl}
        onChange={(event) => setBlogurl(event.target.value)}
        className='mb-5 mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5'
        placeholder='https://www.example.com'
      />
      <label className='block text-sm font-medium leading-5 text-gray-700'>
        RSS Feed URL
      </label>
      <input
        type='url'
        required
        value={feedurl}
        onChange={(event) => setFeedurl(event.target.value)}
        className='mb-5 mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5'
        placeholder='https://www.example.com/feed'
      />

      <label
        htmlFor='about'
        className='block text-sm leading-5 font-medium text-gray-700'
      >
        Notes
      </label>
      <div className='rounded-md shadow-sm'>
        <textarea
          value={notes}
          onChange={(event) => setNotes(event.target.value)}
          rows='3'
          className='form-textarea mt-1 block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5'
          placeholder='Anything you want to tell us!'
        ></textarea>
      </div>
      <p className='mt-2 text-sm text-gray-500'>
        Your submission will be approved before appearing on the
        site
      </p>
    </div>
    <div className='px-4 py-3 bg-gray-50 text-right sm:px-6'>
      <span className='inline-flex rounded-md shadow-sm'>
        <button
          type='submit'
          className='inline-flex justify-center py-2 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition duration-150 ease-in-out'
        >
          Save
        </button>
      </span>
    </div>
  </div>
</form>

Handle the form submit

Now I add an onSubmit event handler to the form, which is assigned a function called handleSubmit:

<form
  className='mt-5 md:mt-0 md:col-span-2'
  action=''
  method='POST'
  onSubmit={handleSubmit}
>

here’s the function:

const handleSubmit = async (event) => {
  event.preventDefault()
  //...
}

Inside the function, we’re going to hit an API we’re soon going to define, that will answer to a POST request on the /api/blog URL:

const handleSubmit = async (event) => {
  event.preventDefault()

  try {
    const res = await fetch('/api/blog', {
      method: 'POST',
      body: JSON.stringify({ name, email, blogurl, feedurl, notes }),
      headers: { 'Content-Type': 'application/json' },
    })

    const json = await res.json()
}

While I’m here I’m also adding a bit of error checking and positive / negative feedback handling from the API. If the API contains a success property, we’re going to fire an alert() and redirect back to the homepage.

Add import Router from 'next/router' to use Router.push('/')

We’re going to use a response local state through hooks.

Define

const [response, setResponse] = useState('')

and we’ll update it when the response is back:

const handleSubmit = async (event) => {
  event.preventDefault()

  try {
    const res = await fetch('/api/blog', {
      method: 'POST',
      body: JSON.stringify({ name, email, blogurl, feedurl, notes }),
      headers: { 'Content-Type': 'application/json' },
    })

    const json = await res.json()

    if (json.success) {
      alert('Thank you for submitting your blog!')
      Router.push('/')
    } else {
      setResponse(json.message)
    }
  } catch (error) {
    setResponse('An error occured while submitting the form')
  }
}

Now in the JSX, I’m printing the response in the empty element we have right after the opening main tag:

<p className='text-center pb-5'></p>

Like this:

<p className='text-center pb-5'>{response}</p>

The POST /api/blog API endpoint

Let’s now create the API endpoint.

Create a file pages/api/blog.js, and add a default export that accepts a req and res objects:

export default (req, res) => {
 
}

Those are the familiar objects that you get in any Node.js server app. We’re going to first disable any request that’s not a POST request:

export default (req, res) => {
  if (!req.method === 'POST') {
    res.status(405).end() //Method Not Allowed
    return
  }
}

Then we extract the values we need from the request body:

  const { name, email, blogurl, feedurl, notes } = req.body

and we send them using the Airtable API.

On Airtable, in the base you just created, you can see the HELP menu. Click that, and then select API documentation:

This will give you ready-to-use examples to interact with the table.

The nice thing is that they automatically include the API key and the base ID you need to use.

We’re going to use this code:

const Airtable = require('airtable')
const base = new Airtable({ apiKey: process.env.APIKEY }).base(
  'appYOURBASEID'
)

base('Table 1').create(
  [{ fields: { name, email, blogurl, feedurl, notes } }],
  (err) => {
    if (err) {
      console.error(err)
      res.status(500).end()
      return
    }
  }
)

Make sure you run

npm install airtable

to install the Airtable official library that we require.

Note that I entered my base id in the code.

The Airtable API key, however, is listed as process.env.APIKEY because I’m going to pass it from the command line, so it’s not included in the code.

Stop the process in the terminal, and run

APIKEY=keyYOURAPIKEY npm run dev

You can find your API key in the Airtable API documentation

Finally, we return the response back to the user:

res.json({
  success: true
})

Here’s the full pages/api/blog.js file content:

export default (req, res) => {
  if (!req.method === 'POST') {
    res.status(405).end() //Method Not Allowed
    return
  }

  const { name, email, blogurl, feedurl, notes } = req.body

  const Airtable = require('airtable')
  const base = new Airtable({ apiKey: process.env.APIKEY }).base(
    'appYOURBASEID'
  )

  base('Table 1').create([{ fields: { name, email, blogurl, feedurl, notes } }], (err) => {
    if (err) {
      console.error(err)
      res.status(500).end()
      return
    }
  })

  res.json({
    success: true
  })
}

Now try filling the form: the data should be saved on Airtable.

Fetching blogs using the Airtable API

Now that we have the database set up, it’s time to use the Airtable API in the index.js page, to fetch the list of blogs approved.

Tip: add a few blogs, and approve them manually on Airtable by clicking the checkbox in the approved column.

The getStaticProps() function now exports the list of blog posts from my blog, because we hardcoded that address.

export async function getStaticProps(context) {
  const parser = new Parser()
  
  const data = await parser.parseURL('https://flaviocopes.com/index.xml')

  const posts = []
  data.items.slice(0, 10).forEach((item) => {
    posts.push({
      title: item.title,
      link: item.link,
      date: item.isoDate,
      name: 'Flavio Copes'
    })
  })

  return {
    props: {
      posts,
    }
  }
}

We’re going to refactor this code, retrieving the list of feed URLs.

Here’s how I retrieve the first 100 results from Airtable:

const Airtable = require('airtable')
const base = new Airtable({ apiKey: process.env.APIKEY }).base(
  'appYOURBASEID'
)

const records = await base('Table 1').select({
  view: 'Grid view',
}).firstPage()

We only get the first page of results and we limit to the first 100 blogs listed in Airtable, as that’s the maxiumum items we can get for each API call, but I think that to start with this is a good number.

Airtable will always serve the items from item #1 to item #100, so the new blogs that will be added, even if unapproved, will not affect our results. They will not “push down” existing and approved blogs.

Now we can get the data from the records:

const feeds = records.filter((record) => {
  if (record.get('approved') === true) return true
}).map((record) => {
  return {
    id: record.id,
    name: record.get('name'),
    blogurl: record.get('blogurl'),
    feedurl: record.get('feedurl'),
  }
})

and we can process each blog RSS feed in a loop:

const posts = []

for (const feed of feeds) {
  const data = await parser.parseURL(feed.feedurl)

  data.items.slice(0, 10).forEach((item) => {
    posts.push({
      title: item.title,
      link: item.link,
      date: item.isoDate,
      name: feed.name,
    })
  })
}

Awesome! This should be the result, a mix of blog posts ordered by date:


This content originally appeared on flaviocopes.com and was authored by flaviocopes.com


Print Share Comment Cite Upload Translate Updates
APA

flaviocopes.com | Sciencx (2022-03-15T05:00:00+00:00) Build a RSS reader using Next.js and Airtable. Retrieved from https://www.scien.cx/2022/03/15/build-a-rss-reader-using-next-js-and-airtable/

MLA
" » Build a RSS reader using Next.js and Airtable." flaviocopes.com | Sciencx - Tuesday March 15, 2022, https://www.scien.cx/2022/03/15/build-a-rss-reader-using-next-js-and-airtable/
HARVARD
flaviocopes.com | Sciencx Tuesday March 15, 2022 » Build a RSS reader using Next.js and Airtable., viewed ,<https://www.scien.cx/2022/03/15/build-a-rss-reader-using-next-js-and-airtable/>
VANCOUVER
flaviocopes.com | Sciencx - » Build a RSS reader using Next.js and Airtable. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2022/03/15/build-a-rss-reader-using-next-js-and-airtable/
CHICAGO
" » Build a RSS reader using Next.js and Airtable." flaviocopes.com | Sciencx - Accessed . https://www.scien.cx/2022/03/15/build-a-rss-reader-using-next-js-and-airtable/
IEEE
" » Build a RSS reader using Next.js and Airtable." flaviocopes.com | Sciencx [Online]. Available: https://www.scien.cx/2022/03/15/build-a-rss-reader-using-next-js-and-airtable/. [Accessed: ]
rf:citation
» Build a RSS reader using Next.js and Airtable | flaviocopes.com | Sciencx | https://www.scien.cx/2022/03/15/build-a-rss-reader-using-next-js-and-airtable/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.