Repository: nodejs/nodejs.dev
Pull request #2951 (2022-10-20 08:10:55)

Feature/components/pagination
## Description
Pagination implemented and connected to the blog page.

Component preview:
![Component preview](https://user-images.githubusercontent.com/16764625/197334240-0340c09c-f27d-4fbe-a155-643052219293.png)
Dark theme preview with hover state:
<img width="462" alt="Screen Shot 2022-10-22 at 20 15 51" src="https://user-images.githubusercontent.com/16764625/197354018-6b970863-7228-4281-8bc6-127c78dc7207.png">


## Related Issues

Related to #2531

### Check List

- [x] I have read the [Contributing Guidelines](https://github.com/nodejs/nodejs.dev/blob/main/CONTRIBUTING.md) and made commit messages that follow the guideline.
- [x] I have run `npm run lint:js -- --fix` and/or `npm run lint:md -- --fix` for my JavaScript and/or Markdown changes.
  - This is important as most of the cases your code changes might not be correctly linted
- [x] I have run `npm run test` to check if all tests are passing, and/or `npm run test -- -u` to update snapshots if I created and/or updated React Components.
- [x] I have checked that the build works locally and that `npm run build` work fine.
- [x] I've covered new added functionality with unit tests if necessary.


github-actions[bot]: Please find a preview at: https://staging.nodejs.dev/2951/
github-actions[bot]: Please find a preview at: https://staging.nodejs.dev/2951/
codecov-commenter: # [Codecov](https://codecov.io/gh/nodejs/nodejs.dev/pull/2951?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
Base: **66.02**% // Head: **68.09**% // Increases project coverage by **`+2.07%`** :tada:
> Coverage data is based on head [(`a48dcde`)](https://codecov.io/gh/nodejs/nodejs.dev/pull/2951?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) compared to base [(`fd57b87`)](https://codecov.io/gh/nodejs/nodejs.dev/commit/fd57b871c2b1ad2f6fb812ed066dec074d76db22?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> Patch has no changes to coverable lines.

> :exclamation: Current head a48dcde differs from pull request most recent head 4e5c137. Consider uploading reports for the commit 4e5c137 to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #2951      +/-   ##
==========================================
+ Coverage   66.02%   68.09%   +2.07%     
==========================================
  Files         118      141      +23     
  Lines        1351     1633     +282     
  Branches      342      389      +47     
==========================================
+ Hits          892     1112     +220     
- Misses        422      479      +57     
- Partials       37       42       +5     
```


| [Impacted Files](https://codecov.io/gh/nodejs/nodejs.dev/pull/2951?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [src/\_\_fixtures\_\_/hooks.tsx](https://codecov.io/gh/nodejs/nodejs.dev/pull/2951/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL19fZml4dHVyZXNfXy9ob29rcy50c3g=) | `100.00% <ø> (ø)` | |
| [src/\_\_fixtures\_\_/page.tsx](https://codecov.io/gh/nodejs/nodejs.dev/pull/2951/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL19fZml4dHVyZXNfXy9wYWdlLnRzeA==) | `94.11% <ø> (-5.89%)` | :arrow_down: |
| [...components/ApiComponents/Components/ApiHeading.tsx](https://codecov.io/gh/nodejs/nodejs.dev/pull/2951/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2NvbXBvbmVudHMvQXBpQ29tcG9uZW50cy9Db21wb25lbnRzL0FwaUhlYWRpbmcudHN4) | `100.00% <ø> (+100.00%)` | :arrow_up: |
| [...rc/components/ApiComponents/Components/ApiLink.tsx](https://codecov.io/gh/nodejs/nodejs.dev/pull/2951/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2NvbXBvbmVudHMvQXBpQ29tcG9uZW50cy9Db21wb25lbnRzL0FwaUxpbmsudHN4) | `100.00% <ø> (+100.00%)` | :arrow_up: |
| [...onents/ApiComponents/Components/JsonLink/index.tsx](https://codecov.io/gh/nodejs/nodejs.dev/pull/2951/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2NvbXBvbmVudHMvQXBpQ29tcG9uZW50cy9Db21wb25lbnRzL0pzb25MaW5rL2luZGV4LnRzeA==) | `100.00% <ø> (ø)` | |
| [...ents/ApiComponents/Components/SourceLink/index.tsx](https://codecov.io/gh/nodejs/nodejs.dev/pull/2951/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2NvbXBvbmVudHMvQXBpQ29tcG9uZW50cy9Db21wb25lbnRzL1NvdXJjZUxpbmsvaW5kZXgudHN4) | `50.00% <ø> (+50.00%)` | :arrow_up: |
| [...ApiComponents/Components/VersionSelector/index.tsx](https://codecov.io/gh/nodejs/nodejs.dev/pull/2951/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2NvbXBvbmVudHMvQXBpQ29tcG9uZW50cy9Db21wb25lbnRzL1ZlcnNpb25TZWxlY3Rvci9pbmRleC50c3g=) | `100.00% <ø> (+100.00%)` | :arrow_up: |
| [src/components/ApiComponents/index.tsx](https://codecov.io/gh/nodejs/nodejs.dev/pull/2951/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2NvbXBvbmVudHMvQXBpQ29tcG9uZW50cy9pbmRleC50c3g=) | `100.00% <ø> (+100.00%)` | :arrow_up: |
| [src/components/ArticleComponents/Alert/index.tsx](https://codecov.io/gh/nodejs/nodejs.dev/pull/2951/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2NvbXBvbmVudHMvQXJ0aWNsZUNvbXBvbmVudHMvQWxlcnQvaW5kZXgudHN4) | `100.00% <ø> (ø)` | |
| [src/templates/blog.tsx](https://codecov.io/gh/nodejs/nodejs.dev/pull/2951/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3RlbXBsYXRlcy9ibG9nLnRzeA==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| ... and [181 more](https://codecov.io/gh/nodejs/nodejs.dev/pull/2951/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/nodejs/nodejs.dev/pull/2951?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).

shlommo: @ovflowd ci error fixed
shlommo: > The idea is good, but still there a few things to be ironed out 😅
> 
> But still I truly appreciate and thank you for your contribution!!
> 
> Note, again, It'd be great if you could attach a screenshot :D


@ovflowd, Thank you for your review 🌈 . I got you 👍  
shlommo: > The idea is good, but still there a few things to be ironed out 😅
> 
> But still I truly appreciate and thank you for your contribution!!
> 
> Note, again, It'd be great if you could attach a screenshot :D

I've attached one ) but I'm going to change this component and attach a new screenshot 
shlommo: @ovflowd, i've attached preview on the description

Patch:
diff --git a/gatsby-node.js b/gatsby-node.js
index ddd08d9e..36f4c3eb 100644
--- a/gatsby-node.js
+++ b/gatsby-node.js
@@ -15,6 +15,7 @@ const createBlogPages = require('./util-node/createBlogPages');
 const createLearnPages = require('./util-node/createLearnPages');
 const createApiPages = require('./util-node/createApiPages');
 const generateRedirects = require('./util-node/generateRedirects');
+const getPaginationPath = require('./util-node/getPaginationPath');
 const redirects = require('./redirects');
 const nodeLocales = require('./locales');
 const { learnPath, apiPath, blogPath } = require('./pathPrefixes');
@@ -168,15 +169,11 @@ exports.createPages = async ({ graphql, actions }) => {
       total: paginatedPosts.length,
     });
 
-    const getBlogPagePath = index => {
-      const categoryPath = node.name ? `${blogPath}${node.name}/` : blogPath;
-
-      return index === 0 ? categoryPath : `${categoryPath}page/${index + 1}/`;
-    };
+    const getBlogPagePath = getPaginationPath(blogPath, node.name);
 
     paginatedPosts.forEach((currentPagePosts, index) => {
       createPage({
-        path: getBlogPagePath(index),
+        path: getBlogPagePath(index + 1),
         component: blogTemplate,
         context: {
           category: node.name ? node : null,
diff --git a/src/components/BlogComponents/Pagination/PaginationNextPage.tsx b/src/components/BlogComponents/Pagination/PaginationNextPage.tsx
new file mode 100644
index 00000000..cd511db8
--- /dev/null
+++ b/src/components/BlogComponents/Pagination/PaginationNextPage.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import classnames from 'classnames';
+import { LocalizedLink as Link } from 'gatsby-theme-i18n';
+import { FormattedMessage } from 'react-intl';
+import styles from './index.module.scss';
+
+interface Props {
+  className: string;
+  currentPage: number;
+  hrefBuilder: (n: number) => string;
+  onPageChange: (n: number) => void;
+  pageCount: number;
+}
+
+const PaginationNextPage = ({
+  className,
+  currentPage,
+  hrefBuilder,
+  onPageChange,
+  pageCount,
+}: Props) => {
+  const componentClassName = classnames(className, styles.next);
+  const content = <FormattedMessage id="components.pagination.next" />;
+  const disabled = currentPage === pageCount;
+
+  if (disabled) {
+    return (
+      <span aria-disabled="true" className={componentClassName}>
+        {content}
+      </span>
+    );
+  }
+
+  const nextPage = currentPage + 1;
+
+  return (
+    <Link
+      aria-label="Next Page"
+      onClick={onPageChange(nextPage)}
+      className={componentClassName}
+      rel="next"
+      to={hrefBuilder(nextPage)}
+    >
+      {content}
+    </Link>
+  );
+};
+
+export default PaginationNextPage;
diff --git a/src/components/BlogComponents/Pagination/PaginationPage.tsx b/src/components/BlogComponents/Pagination/PaginationPage.tsx
new file mode 100644
index 00000000..a375019f
--- /dev/null
+++ b/src/components/BlogComponents/Pagination/PaginationPage.tsx
@@ -0,0 +1,42 @@
+import React, { MouseEvent } from 'react';
+import classnames from 'classnames';
+import { LocalizedLink as Link } from 'gatsby-theme-i18n';
+import { PageModel } from '../../../types/pagination';
+
+interface Props {
+  className: string;
+  page: PageModel;
+  hrefBuilder: (n: number) => string;
+  onPageChange?: (e: MouseEvent) => void;
+}
+
+const PaginationPage = ({
+  className,
+  page,
+  hrefBuilder,
+  onPageChange,
+}: Props) => {
+  const content = String(page.num);
+  const componentClassName = classnames(className, page?.className ?? '');
+
+  if (page.selected) {
+    return (
+      <em aria-current="page" className={componentClassName}>
+        {content}
+      </em>
+    );
+  }
+
+  return (
+    <Link
+      aria-label={`Page ${page.num}`}
+      onClick={onPageChange}
+      className={componentClassName}
+      to={hrefBuilder(page.num)}
+    >
+      {content}
+    </Link>
+  );
+};
+
+export default PaginationPage;
diff --git a/src/components/BlogComponents/Pagination/PaginationPreviousPage.tsx b/src/components/BlogComponents/Pagination/PaginationPreviousPage.tsx
new file mode 100644
index 00000000..4d5b611e
--- /dev/null
+++ b/src/components/BlogComponents/Pagination/PaginationPreviousPage.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import classnames from 'classnames';
+import { LocalizedLink as Link } from 'gatsby-theme-i18n';
+import { FormattedMessage } from 'react-intl';
+import styles from './index.module.scss';
+
+interface Props {
+  className: string;
+  currentPage: number;
+  hrefBuilder: (n: number) => string;
+  onPageChange: (n: number) => void;
+}
+
+const PaginationPreviousPage = ({
+  className,
+  currentPage,
+  hrefBuilder,
+  onPageChange,
+}: Props) => {
+  const componentClassName = classnames(className, styles.prev);
+  const content = <FormattedMessage id="components.pagination.previous" />;
+  const disabled = currentPage === 1;
+
+  if (disabled) {
+    return (
+      <span aria-disabled="true" className={componentClassName}>
+        {content}
+      </span>
+    );
+  }
+
+  const previousPage = currentPage - 1;
+
+  return (
+    <Link
+      aria-label="Previous Page"
+      onClick={onPageChange(previousPage)}
+      className={componentClassName}
+      rel="prev"
+      to={hrefBuilder(previousPage)}
+    >
+      {content}
+    </Link>
+  );
+};
+
+export default PaginationPreviousPage;
diff --git a/src/components/BlogComponents/Pagination/index.module.scss b/src/components/BlogComponents/Pagination/index.module.scss
new file mode 100644
index 00000000..f126a351
--- /dev/null
+++ b/src/components/BlogComponents/Pagination/index.module.scss
@@ -0,0 +1,70 @@
+.pagination {
+  display: flex;
+  flex-wrap: wrap;
+  list-style: none;
+  margin: 0;
+  padding: 0;
+
+  .pagination-page {
+    align-items: center;
+    background-color: var(--brand0);
+    color: var(--black9);
+    display: inline-flex;
+    font-size: var(--font-size-caption);
+    font-style: normal;
+    justify-content: center;
+    line-height: var(--line-height-caption);
+    padding: var(--space-07) var(--space-13);
+    text-align: center;
+    text-decoration: none;
+    transition: background-color 0.2s ease-in-out;
+    user-select: none;
+    white-space: nowrap;
+
+    &:hover,
+    &:focus {
+      background-color: var(--brand10);
+      outline: 0;
+      text-decoration: none;
+      transition-duration: 0.1s;
+    }
+
+    &.prev,
+    &.next {
+      background-color: var(--black2);
+      color: var(--black9);
+    }
+
+    &[aria-current],
+    &[aria-current]:hover {
+      background-color: var(--color-brand-primary);
+      color: var(--black0);
+    }
+
+    &.prev {
+      border-radius: 4px 0 0 4px;
+    }
+
+    &.next {
+      border-radius: 0 4px 4px 0;
+    }
+
+    &[aria-disabled],
+    &[aria-disabled]:hover {
+      background-color: var(--black1);
+      color: var(--black9);
+      cursor: not-allowed;
+    }
+  }
+}
+
+:global(.dark) {
+  .pagination-page {
+    color: var(--black0);
+
+    &:hover,
+    &:focus {
+      color: var(--black9);
+    }
+  }
+}
diff --git a/src/components/BlogComponents/Pagination/index.tsx b/src/components/BlogComponents/Pagination/index.tsx
new file mode 100644
index 00000000..3878dcc9
--- /dev/null
+++ b/src/components/BlogComponents/Pagination/index.tsx
@@ -0,0 +1,72 @@
+import React, { MouseEvent, useCallback } from 'react';
+import { generatePageList } from './util/generatePageList';
+import { PageType } from '../../../types/pagination';
+import PaginationPreviousPage from './PaginationPreviousPage';
+import PaginationNextPage from './PaginationNextPage';
+import PaginationPage from './PaginationPage';
+import styles from './index.module.scss';
+
+interface Props {
+  currentPage: number;
+  hrefBuilder?: (n: number) => string;
+  pageCount: number;
+  onPageChange?: (e: MouseEvent, n: number) => void;
+  showPages?: boolean;
+  wrapperClassName?: string;
+}
+
+const defaultHrefBuilder = (pageNum: number): string => `#${pageNum}`;
+
+const Pagination = ({
+  pageCount,
+  currentPage,
+  onPageChange,
+  hrefBuilder = defaultHrefBuilder,
+  showPages = true,
+  wrapperClassName,
+}: Props) => {
+  const pageChange = useCallback(
+    (n: number) => (e: MouseEvent) => onPageChange && onPageChange(e, n),
+    [onPageChange]
+  );
+
+  const pageList = generatePageList(pageCount, currentPage, !!showPages);
+
+  const pageElements = pageList.map(page => (
+    <li key={`${page.type}${page.num}`}>
+      <PaginationPage
+        className={styles.paginationPage}
+        hrefBuilder={hrefBuilder}
+        page={page}
+        onPageChange={pageChange(page.num)}
+      />
+    </li>
+  ));
+
+  return (
+    <nav aria-label="Pagination" className={wrapperClassName}>
+      <ul className={styles.pagination}>
+        <li key={PageType.PREV}>
+          <PaginationPreviousPage
+            className={styles.paginationPage}
+            currentPage={currentPage}
+            hrefBuilder={hrefBuilder}
+            onPageChange={pageChange}
+          />
+        </li>
+        {pageElements}
+        <li key={PageType.NEXT}>
+          <PaginationNextPage
+            className={styles.paginationPage}
+            currentPage={currentPage}
+            hrefBuilder={hrefBuilder}
+            onPageChange={pageChange}
+            pageCount={pageCount}
+          />
+        </li>
+      </ul>
+    </nav>
+  );
+};
+
+export default Pagination;
diff --git a/src/components/BlogComponents/Pagination/util/generatePageList.ts b/src/components/BlogComponents/Pagination/util/generatePageList.ts
new file mode 100644
index 00000000..e8033cd4
--- /dev/null
+++ b/src/components/BlogComponents/Pagination/util/generatePageList.ts
@@ -0,0 +1,21 @@
+import { PageModel, PageType } from '../../../../types/pagination';
+
+export function generatePageList(
+  pageCount: number,
+  currentPage: number,
+  showPages: boolean
+): PageModel[] {
+  const pages: PageModel[] = [];
+  if (showPages) {
+    for (let i = 1; i <= pageCount; i += 1) {
+      const selected = i === currentPage;
+      pages.push({
+        type: PageType.NUM,
+        num: i,
+        selected,
+      });
+    }
+  }
+
+  return pages;
+}
diff --git a/src/components/BlogComponents/index.ts b/src/components/BlogComponents/index.ts
index 38330e67..52be4e56 100644
--- a/src/components/BlogComponents/index.ts
+++ b/src/components/BlogComponents/index.ts
@@ -1,2 +1,3 @@
 export { default as BlogAuthorsList } from './BlogAuthorsList';
 export { default as BlogCard } from './BlogCard';
+export { default as Pagination } from './Pagination';
diff --git a/src/styles/templates/blog.module.scss b/src/styles/templates/blog.module.scss
index e536033b..0000260f 100644
--- a/src/styles/templates/blog.module.scss
+++ b/src/styles/templates/blog.module.scss
@@ -36,4 +36,8 @@
       grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
     }
   }
+
+  .blog-pagination-wrapper {
+    padding: var(--space-24) var(--space-24) var(--space-32) 0;
+  }
 }
diff --git a/src/styles/tokens.scss b/src/styles/tokens.scss
index 82fc614b..792ac769 100644
--- a/src/styles/tokens.scss
+++ b/src/styles/tokens.scss
@@ -131,8 +131,10 @@ body {
   --space-01: 0.1rem;
   --space-02: 0.2rem;
   --space-04: 0.4rem;
+  --space-07: 0.7rem;
   --space-08: 0.8rem;
   --space-12: 1.2rem;
+  --space-13: 1.3rem;
   --space-14: 1.4rem;
   --space-16: 1.6rem;
   --space-20: 2rem;
diff --git a/src/templates/blog.tsx b/src/templates/blog.tsx
index 25c981ff..5c5ec784 100644
--- a/src/templates/blog.tsx
+++ b/src/templates/blog.tsx
@@ -1,10 +1,11 @@
 import React, { useMemo } from 'react';
 import { injectIntl, WrappedComponentProps } from 'react-intl';
 import DefaultLayout from '../layouts/default';
-import { BlogComponents, ArticleComponents } from '../components';
+import { ArticleComponents, BlogComponents } from '../components';
 import BlogNavigation from '../navigations/blog';
 import { blogPath } from '../../pathPrefixes';
 import { BlogCategory, BlogTemplateContext } from '../types';
+import getPaginationPath from '../../util-node/getPaginationPath';
 import styles from '../styles/templates/blog.module.scss';
 
 const blogHomeSection = {
@@ -23,7 +24,7 @@ interface Props {
 }
 
 const BlogTemplate = ({
-  pageContext: { posts, category, categories },
+  pageContext: { category, categories, pagination, posts },
   intl,
 }: Props & WrappedComponentProps): JSX.Element => {
   const currentCategory = useMemo(() => {
@@ -42,6 +43,8 @@ const BlogTemplate = ({
     };
   }, [category, intl]);
 
+  const shouldShowPagination = pagination.total > 1;
+
   return (
     <DefaultLayout title="Blogs at Nodejs">
       <main className="grid-container">
@@ -64,6 +67,14 @@ const BlogTemplate = ({
               />
             ))}
           </div>
+          {shouldShowPagination && (
+            <BlogComponents.Pagination
+              currentPage={pagination.current}
+              hrefBuilder={getPaginationPath(blogPath, category?.name)}
+              pageCount={pagination.total}
+              wrapperClassName={styles.blogPaginationWrapper}
+            />
+          )}
         </div>
       </main>
     </DefaultLayout>
diff --git a/src/types/markdown.ts b/src/types/markdown.ts
index 1b37faca..e038c8dc 100644
--- a/src/types/markdown.ts
+++ b/src/types/markdown.ts
@@ -12,3 +12,8 @@ export interface PaginationInfo {
   slug: string;
   title: string;
 }
+
+export interface PagePaginationInfo {
+  current: number;
+  total: number;
+}
diff --git a/src/types/pages/blog.ts b/src/types/pages/blog.ts
index 2ad319dd..8328e586 100644
--- a/src/types/pages/blog.ts
+++ b/src/types/pages/blog.ts
@@ -1,7 +1,9 @@
+import { PagePaginationInfo } from '../markdown';
 import { BlogCategory, BlogPost } from '../blog';
 
 export interface BlogTemplateContext {
-  posts: BlogPost[];
   categories: BlogCategory[];
   category: BlogCategory['node'];
+  pagination: PagePaginationInfo;
+  posts: BlogPost[];
 }
diff --git a/src/types/pagination.ts b/src/types/pagination.ts
new file mode 100644
index 00000000..6c6b854d
--- /dev/null
+++ b/src/types/pagination.ts
@@ -0,0 +1,14 @@
+// eslint-disable-next-line no-shadow
+export enum PageType {
+  NEXT = 'NEXT',
+  NUM = 'NUM',
+  PREV = 'PREV',
+}
+
+export interface PageModel {
+  className?: string;
+  type: PageType;
+  num: number;
+  disabled?: boolean;
+  selected?: boolean;
+}
diff --git a/util-node/getPaginationPath.js b/util-node/getPaginationPath.js
new file mode 100644
index 00000000..211286bc
--- /dev/null
+++ b/util-node/getPaginationPath.js
@@ -0,0 +1,9 @@
+function getPaginationPath(pagePath, nodeName) {
+  return page => {
+    const nodePath = nodeName ? `${pagePath}${nodeName}/` : pagePath;
+
+    return page === 1 ? nodePath : `${nodePath}page/${page}/`;
+  };
+}
+
+module.exports = getPaginationPath;


Test patch:
diff --git a/src/components/BlogComponents/Pagination/__tests__/Pagination.test.tsx b/src/components/BlogComponents/Pagination/__tests__/Pagination.test.tsx
new file mode 100644
index 00000000..65fdcdcc
--- /dev/null
+++ b/src/components/BlogComponents/Pagination/__tests__/Pagination.test.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import Pagination from '..';
+
+describe('Pagination component', () => {
+  it('renders correctly', () => {
+    const { container } = render(<Pagination currentPage={1} pageCount={10} />);
+    expect(container).toMatchSnapshot();
+  });
+
+  it('renders with only next and prev link', () => {
+    const { container } = render(
+      <Pagination currentPage={1} pageCount={10} showPages={false} />
+    );
+    expect(container).toMatchSnapshot();
+  });
+
+  it('renders with disabled next link', () => {
+    const { container } = render(
+      <Pagination currentPage={10} pageCount={10} />
+    );
+    expect(container).toMatchSnapshot();
+  });
+
+  it('renders with wrapperClassName', () => {
+    const { container } = render(
+      <Pagination currentPage={1} pageCount={10} wrapperClassName="wrapper" />
+    );
+    expect(container).toMatchSnapshot();
+  });
+
+  it('handles onPageChange', async () => {
+    const onClick = jest.fn();
+    render(
+      <Pagination
+        currentPage={1}
+        pageCount={10}
+        wrapperClassName="wrapper"
+        onPageChange={onClick}
+      />
+    );
+    const linkItem: Element = screen.getAllByRole('link')[0] as Element;
+    await userEvent.click(linkItem);
+    expect(onClick).toHaveBeenCalled();
+  });
+
+  it('handles hrefBuilder', () => {
+    const hrefBuilder = jest.fn((page: number) => `/page/${page}`);
+    const { container } = render(
+      <Pagination
+        currentPage={2}
+        hrefBuilder={hrefBuilder}
+        pageCount={10}
+        wrapperClassName="wrapper"
+      />
+    );
+
+    expect(hrefBuilder).toHaveBeenCalled();
+    expect(container).toMatchSnapshot();
+  });
+});
diff --git a/src/components/BlogComponents/Pagination/__tests__/__snapshots__/Pagination.test.tsx.snap b/src/components/BlogComponents/Pagination/__tests__/__snapshots__/Pagination.test.tsx.snap
new file mode 100644
index 00000000..3bc50561
--- /dev/null
+++ b/src/components/BlogComponents/Pagination/__tests__/__snapshots__/Pagination.test.tsx.snap
@@ -0,0 +1,516 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Pagination component handles hrefBuilder 1`] = `
+<div>
+  <nav
+    aria-label="Pagination"
+    class="wrapper"
+  >
+    <ul
+      class="pagination"
+    >
+      <li>
+        <a
+          aria-label="Previous Page"
+          class="paginationPage prev"
+          href="/page/1"
+          rel="prev"
+        >
+          Prev
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 1"
+          class="paginationPage"
+          href="/page/1"
+        >
+          1
+        </a>
+      </li>
+      <li>
+        <em
+          aria-current="page"
+          class="paginationPage"
+        >
+          2
+        </em>
+      </li>
+      <li>
+        <a
+          aria-label="Page 3"
+          class="paginationPage"
+          href="/page/3"
+        >
+          3
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 4"
+          class="paginationPage"
+          href="/page/4"
+        >
+          4
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 5"
+          class="paginationPage"
+          href="/page/5"
+        >
+          5
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 6"
+          class="paginationPage"
+          href="/page/6"
+        >
+          6
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 7"
+          class="paginationPage"
+          href="/page/7"
+        >
+          7
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 8"
+          class="paginationPage"
+          href="/page/8"
+        >
+          8
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 9"
+          class="paginationPage"
+          href="/page/9"
+        >
+          9
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 10"
+          class="paginationPage"
+          href="/page/10"
+        >
+          10
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Next Page"
+          class="paginationPage next"
+          href="/page/3"
+          rel="next"
+        >
+          Next
+        </a>
+      </li>
+    </ul>
+  </nav>
+</div>
+`;
+
+exports[`Pagination component renders correctly 1`] = `
+<div>
+  <nav
+    aria-label="Pagination"
+  >
+    <ul
+      class="pagination"
+    >
+      <li>
+        <span
+          aria-disabled="true"
+          class="paginationPage prev"
+        >
+          Prev
+        </span>
+      </li>
+      <li>
+        <em
+          aria-current="page"
+          class="paginationPage"
+        >
+          1
+        </em>
+      </li>
+      <li>
+        <a
+          aria-label="Page 2"
+          class="paginationPage"
+          href="#2"
+        >
+          2
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 3"
+          class="paginationPage"
+          href="#3"
+        >
+          3
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 4"
+          class="paginationPage"
+          href="#4"
+        >
+          4
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 5"
+          class="paginationPage"
+          href="#5"
+        >
+          5
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 6"
+          class="paginationPage"
+          href="#6"
+        >
+          6
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 7"
+          class="paginationPage"
+          href="#7"
+        >
+          7
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 8"
+          class="paginationPage"
+          href="#8"
+        >
+          8
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 9"
+          class="paginationPage"
+          href="#9"
+        >
+          9
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 10"
+          class="paginationPage"
+          href="#10"
+        >
+          10
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Next Page"
+          class="paginationPage next"
+          href="#2"
+          rel="next"
+        >
+          Next
+        </a>
+      </li>
+    </ul>
+  </nav>
+</div>
+`;
+
+exports[`Pagination component renders with disabled next link 1`] = `
+<div>
+  <nav
+    aria-label="Pagination"
+  >
+    <ul
+      class="pagination"
+    >
+      <li>
+        <a
+          aria-label="Previous Page"
+          class="paginationPage prev"
+          href="#9"
+          rel="prev"
+        >
+          Prev
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 1"
+          class="paginationPage"
+          href="#1"
+        >
+          1
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 2"
+          class="paginationPage"
+          href="#2"
+        >
+          2
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 3"
+          class="paginationPage"
+          href="#3"
+        >
+          3
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 4"
+          class="paginationPage"
+          href="#4"
+        >
+          4
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 5"
+          class="paginationPage"
+          href="#5"
+        >
+          5
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 6"
+          class="paginationPage"
+          href="#6"
+        >
+          6
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 7"
+          class="paginationPage"
+          href="#7"
+        >
+          7
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 8"
+          class="paginationPage"
+          href="#8"
+        >
+          8
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 9"
+          class="paginationPage"
+          href="#9"
+        >
+          9
+        </a>
+      </li>
+      <li>
+        <em
+          aria-current="page"
+          class="paginationPage"
+        >
+          10
+        </em>
+      </li>
+      <li>
+        <span
+          aria-disabled="true"
+          class="paginationPage next"
+        >
+          Next
+        </span>
+      </li>
+    </ul>
+  </nav>
+</div>
+`;
+
+exports[`Pagination component renders with only next and prev link 1`] = `
+<div>
+  <nav
+    aria-label="Pagination"
+  >
+    <ul
+      class="pagination"
+    >
+      <li>
+        <span
+          aria-disabled="true"
+          class="paginationPage prev"
+        >
+          Prev
+        </span>
+      </li>
+      <li>
+        <a
+          aria-label="Next Page"
+          class="paginationPage next"
+          href="#2"
+          rel="next"
+        >
+          Next
+        </a>
+      </li>
+    </ul>
+  </nav>
+</div>
+`;
+
+exports[`Pagination component renders with wrapperClassName 1`] = `
+<div>
+  <nav
+    aria-label="Pagination"
+    class="wrapper"
+  >
+    <ul
+      class="pagination"
+    >
+      <li>
+        <span
+          aria-disabled="true"
+          class="paginationPage prev"
+        >
+          Prev
+        </span>
+      </li>
+      <li>
+        <em
+          aria-current="page"
+          class="paginationPage"
+        >
+          1
+        </em>
+      </li>
+      <li>
+        <a
+          aria-label="Page 2"
+          class="paginationPage"
+          href="#2"
+        >
+          2
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 3"
+          class="paginationPage"
+          href="#3"
+        >
+          3
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 4"
+          class="paginationPage"
+          href="#4"
+        >
+          4
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 5"
+          class="paginationPage"
+          href="#5"
+        >
+          5
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 6"
+          class="paginationPage"
+          href="#6"
+        >
+          6
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 7"
+          class="paginationPage"
+          href="#7"
+        >
+          7
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 8"
+          class="paginationPage"
+          href="#8"
+        >
+          8
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 9"
+          class="paginationPage"
+          href="#9"
+        >
+          9
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Page 10"
+          class="paginationPage"
+          href="#10"
+        >
+          10
+        </a>
+      </li>
+      <li>
+        <a
+          aria-label="Next Page"
+          class="paginationPage next"
+          href="#2"
+          rel="next"
+        >
+          Next
+        </a>
+      </li>
+    </ul>
+  </nav>
+</div>
+`;
diff --git a/util-node/__tests__/getPaginationPath.test.ts b/util-node/__tests__/getPaginationPath.test.ts
new file mode 100644
index 00000000..ec908021
--- /dev/null
+++ b/util-node/__tests__/getPaginationPath.test.ts
@@ -0,0 +1,21 @@
+import getPaginationPath from '../getPaginationPath';
+
+describe('getPaginationPath', () => {
+  it('returns correct path', () => {
+    const path = getPaginationPath('/blog/', 'nodName')(2);
+
+    expect(path).toBe('/blog/nodName/page/2/');
+  });
+
+  it('returns only only page path', () => {
+    const path = getPaginationPath('/blog/')(2);
+
+    expect(path).toBe('/blog/page/2/');
+  });
+
+  it('returns path without pagination', () => {
+    const path = getPaginationPath('/blog/', 'nodName')(1);
+
+    expect(path).toBe('/blog/nodName/');
+  });
+});