Skip to content

TetraMeister/React-Blog-App

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

15 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Headless CMS Blog – desktop view

Headless CMS Blog

See the live version of Headless CMS Blog.

A React single-page application that pulls all of its content from Prismic – a headless CMS – through its Content Query API. The whole app is built around clean, UX-friendly URLs: articles live under date-based nested routes and can be filtered, sorted and paginated entirely through the URL's query string, so every view is shareable and bookmarkable.

Main features:

  • Content fetched from a Prismic headless CMS (no custom back-end needed)
  • Nested, date-aware routing – e.g. /articles/:year/:month/:uid
  • URL-driven filtering & sorting – state lives in searchParams, not local state
  • Category filtering powered by Prismic content relationships
  • Reusable client-side pagination delivered as a Higher-Order Component
  • Redux Toolkit store with thunks and request caching
  • Fully responsive, dark-themed UI built with styled-components + CSS custom properties

Headless CMS Blog – mobile view

Β 

πŸ’‘ Technologies

React Redux React Router styled-components Prismic JavaScript

Built on React 19 with Redux Toolkit 2, React Router 7 and styled-components 6, using @prismicio/client and @prismicio/react for the CMS layer.

Β 

πŸ’Ώ Installation

The project uses node and npm. With them installed, type into the terminal:

npm i
npm start

The app talks to a public Prismic repository (react-routing-blogg), so there's no API key or .env to set up – it works out of the box.

Β 

πŸ€” Solutions provided in the project

  • URL as the single source of truth (useFilters hook) – sorting, pagination size and active tags are read from and written to the URL's searchParams, so the current view can be shared or refreshed without losing state:
const [searchParams, setSearchParams] = useSearchParams();
const { year, month } = useParams();

const sorting = searchParams.get('sorting') || 'date-desc';
const pagination = searchParams.get('pagination') || 3;
const tags = searchParams.getAll('tags');

const setTags = (value) => {
  setSearchParams((prev) => {
    prev.delete('tags');
    value.forEach((tag) => prev.append('tags', tag));
    return prev;
  });
};

Β 

  • Nested routes that double as a date filter – the router exposes year/month segments, and the same useFilters hook reads them via useParams to narrow the article list down to a given period:
<Routes>
  <Route path="/articles/:year/:month/:uid" element={<Article />} />
  <Route path="/articles/:year/:month" element={<Articles />} />
  <Route path="/articles/:year" element={<Articles />} />
  <Route path={'/articles'} element={<Articles />} />
  <Route path={'/'} element={<Home />} />
  <Route path={'*'} element={<Error />} />
</Routes>
const nestedRouteFilterData = (data) => {
  if (!data) return null;
  if (!year && !month) return data;

  return data.filter((el) => {
    const date = new Date(el.data.timestamp);
    const matchYear = year ? date.getFullYear() === Number(year) : true;
    const matchMonth = month ? date.getMonth() + 1 === Number(month) : true;
    return matchYear && matchMonth;
  });
};
Route View
/ Home
/articles, /articles/:year, /articles/:year/:month Articles (filtered list)
/articles/:year/:month/:uid Article
* Error

Β 

  • Pagination as a Higher-Order Component – withPagination wraps any list component, injecting the sliced data and the prev/next controls. It reads the page size straight from the URL via useFilters, so list and pagination stay in sync:
const withPagination =
  (WrappedComponent) =>
  ({ data, ...props }) => {
    const { pagination } = useFilters();
    const [currPage, setCurrPage] = useState(1);

    const totalPages = Math.ceil(data?.length / pagination);
    const paginatedData = data?.slice(
      (currPage - 1) * pagination,
      currPage * pagination
    );

    return data ? (
      <StyledPaginatedArticleList>
        <WrappedComponent data={paginatedData} {...props} />
        {/* prev / next buttons + page indicator */}
      </StyledPaginatedArticleList>
    ) : (
      <Navigate to={'/error'} />
    );
  };

Β 

  • Redux Toolkit slices with cached thunks – data and categories each live in their own slice. The thunks bail out early when the data is already in the store, so navigating between views doesn't re-fetch. The post query also pulls related category names in one request via fetchLinks:
export const fetchPages = () => (dispatch, getState) => {
  if (getState().data.data) return;

  client
    .getAllByType('post', {
      fetchLinks: ['category.name'],
    })
    .then((data) => dispatch(setDataAction(data)))
    .catch((err) => dispatch(setErrorAction(err)))
    .finally(() => dispatch(setLoadingAction(false)));
};

Components never touch Redux directly – they go through small custom hooks that expose a clean { loading, data, error } shape:

const usePages = () => {
  const dispatch = useDispatch();

  const loading = useSelector((state) => state.data.loading);
  const data = useSelector((state) => state.data.data);
  const error = useSelector((state) => state.data.error);

  useEffect(() => {
    dispatch(fetchPages());
  }, [dispatch]);

  return { loading, data, error };
};

Β 

  • Headless CMS integration – a single Prismic client powers the whole app, and the CMS Rich Text is mapped to project components through a components options map, keeping styling fully under the app's control:
import * as prismic from '@prismicio/client';

const client = prismic.createClient('react-routing-blogg');

export default client;
const options = {
  heading1: ({ children }) => <h3 className="card__title">{children}</h3>,
  paragraph: ({ children }) => <p className="card__desc">{children}</p>,
};

<PrismicRichText field={article?.data.title} components={options} />
<PrismicImage field={article?.data.image} className="card__image" />

Β 

πŸ’­ Conclusions for future projects

A few directions I'd like to take this project further:

Embedded media (video) in articles:

The Rich Text already renders through a components map, which is the natural extension point. Adding an embed handler would let YouTube/Vimeo content render inline – right now the body only maps headings, paragraphs and images:

const options = {
  heading1: ({ children }) => <h1 className="article__title">{children}</h1>,
  paragraph: ({ children }) => <p className="article__desc">{children}</p>,
  // embed: ({ node }) => <VideoEmbed oembed={node.oembed} />,  // ← next step
};

A dedicated "About" / static-pages section:

Navigation is currently Home + Articles. An About page (and other evergreen content) would round the site out – either as its own Prismic Custom Type or as simple static routes, reusing the existing layout and styling.

Per-article metadata for SEO:

Bringing in React Helmet (as hinted in the task brief) would let every article set its own <title> and meta description – worth it for a content site that's meant to be shared and indexed.

Category landing pages:

Category data is already fetched and related to posts via fetchLinks. Surfacing it as dedicated /category/:name routes would make that relationship even more useful for readers browsing by topic.

Β 

πŸ™‹β€β™‚οΈ Feel free to contact me

Write sth nice ;) Find me on GitHub as TetraMeister.

Β 

πŸ‘ Special thanks

Thanks to my Mentor – Mateusz Bogolubow (devmentor.pl) – for providing me with this task and for the code review.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors