Building a page feed component for your blog in Makeswift
July 24, 2024
Page feeds are a useful component to engage your site’s visitors with previews of relevant content that matches their interests. This guide will walk you through building a basic page feed component for a blog site in Makeswift that you can easily drag-and-drop into any of your Makeswift pages.
Setting up your host
To start, we’re going to need a custom Makeswift host running version `0.19.1` (or later) of the runtime. If you already have a custom host integrated with Makeswift and are running the correct version of the runtime, feel free to skip this step.
The easiest way to set up a custom host is with our CLI:
npx makeswift@latest init
Follow the steps in the CLI to create a new Next.js app integrated with Makeswift. For more information on getting started with this, check out our Quickstart documentation.
Creating the content
We can now start creating pages in the Makeswift builder. For this example, we’ll create a sample engineering blog site with the following pages:
- A home page - this will serve as the entry point to our blog and eventually display our page feed component. We’ll set the path for this page as /home
- Our blog posts - these pages will hold our actual blog content. Each of these pages should have a path starting with /blog/
so that they can be retrieved by our feed later. In the page sidebar, fill out the title, description, and social image (along with other useful metadata) for each blog post.
Creating a custom API route
In v0.19.0
of the runtime, we shipped a new getPages
method on the Makeswift client that can retrieve detailed information on pages you’ve created in Makeswift. However, the client is not intended for use directly in a browser context, so we’re going to create an API route in our host that will return the pages retrieved from getPages
.
In our custom host site, let’s create an API route under app/api/blog-feed/route.ts
:
import { NextRequest } from 'next/server' import { client } from '@/lib/makeswift/client' export async function GET(req: NextRequest) { const searchParams = req.nextUrl.searchParams const limit = Number(searchParams.get('limit') ?? 4) const after = searchParams.get('after') ?? undefined const pagesResult = await client.getPages({ limit, after, pathPrefix: '/blog/', sortBy: 'createdAt', sortDirection: 'desc' }) return Response.json(pagesResult) }
We’ve kept the interface for our custom endpoint very limited: the only query parameters accepted by this endpoint are for pagination, after
and limit
. For the remaining getPages
options, we pass values to only retrieve Makeswift pages that have a path starting with /blog/
, sorted by newest published pages first.
Creating the Feed Component
We can now start creating our page feed component. Our page feed component will show a list of page card links, along with a “Load More” button that will retrieve more pages from our endpoint. We’re going to use SWR’s infinite pagination to fetch our pages, but in your own implementation, feel free to use any data fetching tools you’d like.
Let’s start by installing swr
in our host site:
npm i swr
Now, let’s create the boilerplate for our PageFeed
component that will handle the retrieval of the pages:
// components/PageFeed/PageFeed.tsx 'use client' import { Ref, forwardRef } from 'react' import { MakeswiftPage } from '@makeswift/runtime/dist/types/next' import clsx from 'clsx' import useSWRInfinite from 'swr/infinite' type BlogFeedResult = { data: MakeswiftPage[] hasMore: boolean } async function fetcher(url: string): Promise<BlogFeedResult> { const res = await fetch(url) return await res.json() } function getKeyFactory(limit: number): string | null { return (pageIndex: number, previousPageData: BlogFeedResult | null) => { if (previousPageData != null && !previousPageData.hasMore) return null const params = new URLSearchParams() params.set('limit', limit.toString()) const lastPageId = previousPageData?.data.at(-1)?.id if (pageIndex > 0 && lastPageId) params.set('after', lastPageId) return `/api/blog-feed?${params.toString()}` } } export const PageFeed = forwardRef(function PageFeed( { className, pagesPerLoad = 5, showLoadMore = true }: Props, ref: Ref<HTMLDivElement> ) { const getKey = getKeyFactory(pagesPerLoad) const { data, size, setSize, isLoading, isValidating } = useSWRInfinite(getKey, fetcher) const pages = data?.flatMap(page => page.data) ?? [] const hasMorePages = data?.at(-1)?.hasMore ?? true const loading = isLoading || isValidating return ( <>{/* TODO */}</> ) }) export default PageFeed
Note that we dynamically create a getKey
function for retrieving the SWR key based on the `pagesPerLoad
` prop. We’ve made pagesPerLoad
a prop so we can control this value from the Makeswift builder.
The data
result from useSWRInfinite
is an array of API responses, which is why we need to flatMap
the retrieved pages into a single array. This is why we also evaluate hasMorePages
based on the data contained in the last API response.
Now, let’s write some markup to render the retrieved pages, some skeleton loaders, and a “Load More” button that will manually retrieve more pages when clicked:
'use client' import Image from 'next/image' import Link from 'next/link' import { Ref, forwardRef } from 'react' import { MakeswiftPage } from '@makeswift/runtime/dist/types/next' import clsx from 'clsx' import useSWRInfinite from 'swr/infinite' type BlogFeedResult = { data: MakeswiftPage[] hasMore: boolean } async function fetcher(url: string): Promise<BlogFeedResult> { const res = await fetch(url) return await res.json() } function getKeyFactory(limit: number): string | null { return (pageIndex: number, previousPageData: BlogFeedResult | null) => { if (previousPageData != null && !previousPageData.hasMore) return null const params = new URLSearchParams() params.set('limit', limit.toString()) const lastPageId = previousPageData?.data.at(-1)?.id if (pageIndex > 0 && lastPageId) params.set('after', lastPageId) return `/api/blog-feed?${params.toString()}` } } type Props = { className?: string pagesPerLoad?: number showLoadMore?: boolean } export const PageFeed = forwardRef(function PageFeed( { className, pagesPerLoad = 4, showLoadMore = true }: Props, ref: Ref<HTMLDivElement> ) { const getKey = getKeyFactory(pagesPerLoad) const { data, size, setSize, isLoading, isValidating } = useSWRInfinite(getKey, fetcher) const pages = data?.flatMap(page => page.data) ?? [] const hasMorePages = data?.at(-1)?.hasMore ?? true const loading = isLoading || isValidating return ( <div ref={ref} className={clsx('flex flex-col items-center gap-3 py-6', className)}> {/* Skeleton loaders */} {pages.length === 0 && loading && Array.from(Array(pagesPerLoad)).map((_, i) => ( <div key={i} className={clsx( 'h-[172px] w-full max-w-4xl animate-pulse rounded-md', 'bg-gradient-to-br from-slate-200 via-slate-100 to-slate-200' )} /> ))} {pages.map(page => { return ( <Link href={page.path} key={page.id} className={clsx( 'flex h-[172px] max-w-4xl cursor-pointer rounded-md border border-gray-200 bg-white shadow-sm', 'transition duration-200 hover:scale-[1.02] hover:shadow-md' )} > {page.socialImageUrl && ( <Image src={page.socialImageUrl} width={148} height={148} alt="" priority className="w-auto rounded-l-md object-cover" /> )} <div className="border-l border-gray-200 p-4"> <h2 className="mb-2 text-xl font-bold">{page.title}</h2> <p className="mb-5 text-gray-600">{page.description}</p> <p className="text-blue-500 hover:underline">Read more</p> </div> </Link> ) })} {hasMorePages && showLoadMore && ( <button className={clsx( 'mt-5 rounded-md bg-violet-500 px-4 py-3 text-white', 'transition duration-200 hover:bg-violet-600', 'disabled:cursor-not-allowed disabled:bg-violet-400' )} onClick={() => setSize(size + 1)} disabled={loading} > {loading ? 'Loading...' : 'Load more'} </button> )} </div> ) }) export default PageFeed
All our “Load More” button has to do is increase the number of paginated requests to make, and SWR will handle the rest! Note that we’ve added a className
prop to add styles to the outermost div
of the page feed, and a showLoadMore
prop to selectively hide the “Load More” button. We may hide the button in case we’re using our page feed at the bottom of an existing blog post, and don’t want the component to appear too long.
Registering the Component in Makeswift
Now, all we have to do is register our component in Makeswift. Create a new components/PageFeed/PageFeed.makeswift.ts
file, and use the Checkbox
, Number
, and Style
prop to match the prop interface of the component:
import { lazy } from 'react' import { Checkbox, Number, Style } from '@makeswift/runtime/controls' import { runtime } from '@/lib/makeswift/runtime' runtime.registerComponent( lazy(() => import('./PageFeed')), { type: 'PageFeed', label: 'Custom / Page Feed', props: { className: Style(), pagesPerLoad: Number({ label: 'Pages per load', defaultValue: 4, max: 20 }), showLoadMore: Checkbox({ label: 'Show Load More', defaultValue: true }), }, } )
Now, let’s add this component registration to our Makeswift imports, under lib/makeswift/component.ts
:
import "@/components/Accordions/Accordions.makeswift"; import "@/components/Marquee/Marquee.makeswift"; +import "@/components/PageFeed/PageFeed.makeswift"; import "@/components/Tabs/Tabs.makeswift";
With our host running, we can now test our component in the Makeswift builder:
And that’s it!
We can now drag and drop this component into any of our other pages to display a feed of the blog pages, and we can use the panel on the right to control the prop values visually.
The client getPages
method includes many other options and returns page data not demonstrated in this post.
To see full details and more examples for getPages
, check our method documentation.