├── .babelrc
├── .flowconfig
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── config.yml
│ └── feature_request.md
└── dependabot.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __tests__
├── __snapshots__
│ └── index.js.snap
├── index.js
└── utils.js
├── dependabot.yml
├── docs
└── examples.md
├── package.json
├── src
├── components.js
├── index.js
└── utils.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env"],
3 | "plugins": [
4 | "babel-plugin-transform-react-jsx",
5 | "transform-flow-strip-types",
6 | "add-module-exports"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 |
3 | [include]
4 |
5 | [libs]
6 |
7 | [lints]
8 |
9 | [options]
10 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectError
11 |
12 | [strict]
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Additional context**
27 | Add any other context about the problem here.
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Support request
4 | url: https://stackoverflow.com/questions/tagged/gatsby
5 | about: Please ask and answer support requests on stack overflow
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | vrsion: 2
2 | allow:
3 | - package-ecosystem: "npm"
4 | directory: "/"
5 | allow:
6 | - dependency-type: "production"
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | lib/
3 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog][keep a changelog] and this project adheres to [Semantic Versioning][semantic versioning].
6 |
7 | ## [Unreleased]
8 |
9 | ---
10 |
11 | ## [0.3.8] - 2021-04-01
12 |
13 | - Bump lodash to 4.17.21 for security fixes
14 |
15 | ## [0.3.7] - 2021-04-01
16 |
17 | ### Fixed
18 |
19 | - Change `peerDependencies` to `>=` for Gatsby 3 support
20 |
21 | ---
22 |
23 |
24 |
25 | [keep a changelog]: https://keepachangelog.com/
26 | [semantic versioning]: https://semver.org/
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Gatsby Central
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Awesome Pagination for Gatsby
2 | ---
3 |
4 | A sensible approach to pagination for Gatsby sites.
5 |
6 | Please post questions on StackOverflow, only bug reports are accepted via GitHub.
7 |
8 | ## Contents
9 |
10 | * [QuickStart](#quick-start)
11 | * [Introduction](#introduction)
12 | * [Philosophy](#philosophy)
13 | * [Notes](#notes)
14 |
15 | ## Quick start
16 |
17 | ```
18 | yarn add gatsby-awesome-pagination
19 | ```
20 |
21 | Open `gatsby-node.js` and import:
22 |
23 | ```javascript
24 | import { paginate } from 'gatsby-awesome-pagination';
25 | ```
26 |
27 | Then, use `paginate()` like so:
28 |
29 | ```javascript
30 | exports.createPages = ({ actions, graphql }) => {
31 | const { createPage } = actions;
32 |
33 | // Fetch your items (blog posts, categories, etc).
34 | const blogPosts = doSomeMagic();
35 |
36 | // Create your paginated pages
37 | paginate({
38 | createPage, // The Gatsby `createPage` function
39 | items: blogPosts, // An array of objects
40 | itemsPerPage: 10, // How many items you want per page
41 | pathPrefix: '/blog', // Creates pages like `/blog`, `/blog/2`, etc
42 | component: path.resolve('...'), // Just like `createPage()`
43 | })
44 | }
45 | ```
46 |
47 | Now in your page query you can use the pagination context like so:
48 |
49 | ```javascript
50 | export const pageQuery = graphql`
51 | query ($skip: Int!, $limit: Int!) {
52 | allMarkdownRemark(
53 | sort: { fields: [frontmatter___date], order: DESC }
54 | skip: $skip // This was added by the plugin
55 | limit: $limit // This was added by the plugin
56 | ) {
57 | ...
58 | }
59 | }
60 | `
61 | ```
62 |
63 | Then inside your component, you can link to next / previous pages, and so on:
64 |
65 | ```javascript
66 | const BlogIndex = (props) => {
67 | return (
68 |
69 | {data.allMarkdownRemark.edges.map(edge =>
)}
70 |
71 | {/* previousPageLink and nextPageLink were added by the plugin */ }
72 | Previous
73 | Next
74 |
75 |
76 | )
77 | }
78 | ```
79 |
80 | For a more detailed example, see [docs/examples.md](https://github.com/GatsbyCentral/gatsby-awesome-pagination/blob/master/docs/examples.md)
81 |
82 | ## Introduction
83 |
84 | Love Gatsby, wanna paginate. Sweet, that's exactly what this package is for.
85 |
86 | We differ from other pagination options as follows:
87 |
88 | * Don't abuse `context` to pass data into components
89 | * Pass only pagination context via `context`
90 | * Provide helpers for next / previous links
91 |
92 | There are 2 types of pagination. You have 80 blog posts and you want to show
93 | them 15 at a time on pages like `/blog`, `/blog/2`, `/blog/3`, etc. You do this
94 | with `paginate()`. Then on each blog post, you want to link to the previous and
95 | next blog posts. You do this with `createPagePerItem()`.
96 |
97 | ## Philosophy
98 |
99 | Why did we create this plugin? We felt that the other Gatsby pagination plugins
100 | were using an approach that goes against the principles of GraphQL. One of the
101 | advantages of GraphQL is to be able to decide what data you need right where you
102 | use that data. That's how Gatsby works with page queries.
103 |
104 | By putting all the data into `context`, the other pagination plugins break this.
105 | Now you need to decide what data you require for each page inside
106 | `gatsby-node.js` and not inside your page query.
107 |
108 | We also felt that there were some helpers missing. Generating links to the next
109 | and previous pages.
110 |
111 | This plugin aims to make it easy to paginate in Gatsby **properly**. No
112 | compromises.
113 |
114 | ## API
115 |
116 | Both `paginate()` and `createPagePerItem()` take a single argument, an object.
117 | They share the following keys (* = required):
118 |
119 | * `createPage`* - The `createPage` function from `exports.createPages`
120 | * `component`* - The value you would pass to `createPage()` as `component` [Gatsby docs here](https://www.gatsbyjs.org/docs/bound-action-creators/#createPage)
121 | * `items`* - An array of objects, the items you want to paginate over
122 |
123 | ### `paginate()`
124 |
125 | In addition to the arguments above, `paginate()` also supports:
126 |
127 | * `itemsPerPage`* - An integer, how many items should be displayed on each page
128 | * `itemsPerFirstPage` - An integer, how many items should be displayed on the **first** page
129 | * `pathPrefix`* - A (nonempty) string or string returning function, the path (eg `/blog`) to which `/2`, `/3`, etc will be added
130 | * `context` - A base context object which is extended with the pagination context values
131 |
132 | Example:
133 |
134 | ```javascript
135 | paginate({
136 | createPage: boundActionCreators.createPage,
137 | component: path.resolve('./src/templates/blog-index.js'),
138 | items: blogPosts,
139 | itemsPerPage: 15,
140 | itemsPerFirstPage: 3,
141 | pathPrefix: '/blog'
142 | })
143 | ```
144 |
145 | Each page's `context` automatically receives the following values:
146 |
147 | * `pageNumber` - The page number (starting from 0)
148 | * `humanPageNumber` - The page number (starting from 1) for human consumption
149 | * `skip` - The $skip you can use in a GraphQL query
150 | * `limit` - The $limit you can use in a GraphQL query
151 | * `numberOfPages` - The total number of pages
152 | * `previousPagePath` - The path to the previous page or `undefined`
153 | * `nextPagePath` - The path to the next page or `undefined`
154 |
155 | #### pathPrefix()
156 |
157 | For more advanced use cases, you can supply a function to `pathPrefix`. This
158 | function will receive a single object as its only argument, that object will
159 | contain `pageNumber` and `numberOfPages`, both integers.
160 |
161 | A simple example implementation could be:
162 |
163 | ```javascript
164 | const pathPrefix = ({ pageNumber, numberOfPages }) =>
165 | pageNumber === 0 ? '/blog' : '/blog/page'
166 | ```
167 |
168 | This example produces pages like `/blog`, `/blog/page/2`, `/blog/page/3`, etc.
169 |
170 | ### `createPagePerItem()`
171 |
172 | WARNING: This API is under active development and will probably change. USE WITH
173 | CAUTION.
174 |
175 | In addition to the arguments above, `createPagePerItem()` also accepts:
176 |
177 | * `itemToPath`* - A function that takes one object from `items` and returns the
178 | `path` for this `item`
179 | * `itemToId`* - A function that takes one object from `items` and returns the
180 | item's ID
181 |
182 | **NOTE**: Both `itemToPath` and `itemToId` also accept a string with the path to
183 | the value, for example `node.frontmatter.permalink` or `node.id`.
184 |
185 | **NOTE**: If an individual `item` has a property called `context`, and that
186 | property is an object, then it's own properties will be added to the page's
187 | `context` for that item.
188 |
189 | Example:
190 |
191 | ```javascript
192 | createPagePerItem({
193 | createPage: boundActionCreators.createPage,
194 | component: path.resolve('./src/templates/blog-post.js'),
195 | items: blogPosts,
196 | itemToPath: 'node.frontmatter.permalink',
197 | itemToId: 'node.id'
198 | })
199 | ```
200 |
201 | Each page's `context` automatically receives the following values:
202 |
203 | * `previousPagePath` - The path to the previous page or `undefined`
204 | * `previousItem` - A copy of the previous element from `items`
205 | * `previousPageId` - The ID of the previous page
206 | * `nextPagePath` - The path to the next page or `undefined`
207 | * `nextItem` - A copy of the next element from `items`
208 | * `nextPageId` - The ID of the next page
209 |
210 | ## Notes
211 |
212 | ### Flow
213 |
214 | This plugin is written using [flow](https://flow.org/). There are some
215 | limitations when using flow and [lodash](https://lodash.com/). Specifically
216 | [this issue](https://github.com/facebook/flow/issues/34). In many cases we use
217 | `$FlowExpectError` and explicitly define the type of something to workaround. A
218 | more elegant solution does not currently seem to exist. Any input on improving
219 | the typing is greatly appreciated in the plugin's issues.
220 |
221 | ### Contributors
222 |
223 | Thanks to the following for their contributions:
224 |
225 | * https://github.com/Pyrax
226 | * https://github.com/JesseSingleton
227 | * https://github.com/silvenon
228 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/index.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`createPagePerItem() Calls createPage() with the correct params 1`] = `
4 | [MockFunction] {
5 | "calls": Array [
6 | Array [
7 | Object {
8 | "component": "/path/to/some/template.js",
9 | "context": Object {
10 | "nextItem": Object {
11 | "id": 1,
12 | "path": "/blog/post-2",
13 | "title": "Post number 1",
14 | },
15 | "nextPageId": 1,
16 | "nextPagePath": "/blog/post-2",
17 | "pageId": 0,
18 | "previousItem": undefined,
19 | "previousPageId": "",
20 | "previousPagePath": "",
21 | },
22 | "path": "/blog/post-1",
23 | },
24 | ],
25 | Array [
26 | Object {
27 | "component": "/path/to/some/template.js",
28 | "context": Object {
29 | "nextItem": Object {
30 | "id": 2,
31 | "path": "/blog/post-3",
32 | "title": "Post number 2",
33 | },
34 | "nextPageId": 2,
35 | "nextPagePath": "/blog/post-3",
36 | "pageId": 1,
37 | "previousItem": Object {
38 | "id": 0,
39 | "path": "/blog/post-1",
40 | "title": "Post number 0",
41 | },
42 | "previousPageId": "",
43 | "previousPagePath": "/blog/post-1",
44 | },
45 | "path": "/blog/post-2",
46 | },
47 | ],
48 | Array [
49 | Object {
50 | "component": "/path/to/some/template.js",
51 | "context": Object {
52 | "nextItem": Object {
53 | "id": 3,
54 | "path": "/blog/post-4",
55 | "title": "Post number 3",
56 | },
57 | "nextPageId": 3,
58 | "nextPagePath": "/blog/post-4",
59 | "pageId": 2,
60 | "previousItem": Object {
61 | "id": 1,
62 | "path": "/blog/post-2",
63 | "title": "Post number 1",
64 | },
65 | "previousPageId": 1,
66 | "previousPagePath": "/blog/post-2",
67 | },
68 | "path": "/blog/post-3",
69 | },
70 | ],
71 | Array [
72 | Object {
73 | "component": "/path/to/some/template.js",
74 | "context": Object {
75 | "nextItem": Object {
76 | "id": 4,
77 | "path": "/blog/post-5",
78 | "title": "Post number 4",
79 | },
80 | "nextPageId": 4,
81 | "nextPagePath": "/blog/post-5",
82 | "pageId": 3,
83 | "previousItem": Object {
84 | "id": 2,
85 | "path": "/blog/post-3",
86 | "title": "Post number 2",
87 | },
88 | "previousPageId": 2,
89 | "previousPagePath": "/blog/post-3",
90 | },
91 | "path": "/blog/post-4",
92 | },
93 | ],
94 | Array [
95 | Object {
96 | "component": "/path/to/some/template.js",
97 | "context": Object {
98 | "nextItem": Object {
99 | "id": 5,
100 | "path": "/blog/post-6",
101 | "title": "Post number 5",
102 | },
103 | "nextPageId": 5,
104 | "nextPagePath": "/blog/post-6",
105 | "pageId": 4,
106 | "previousItem": Object {
107 | "id": 3,
108 | "path": "/blog/post-4",
109 | "title": "Post number 3",
110 | },
111 | "previousPageId": 3,
112 | "previousPagePath": "/blog/post-4",
113 | },
114 | "path": "/blog/post-5",
115 | },
116 | ],
117 | Array [
118 | Object {
119 | "component": "/path/to/some/template.js",
120 | "context": Object {
121 | "nextItem": Object {
122 | "id": 6,
123 | "path": "/blog/post-7",
124 | "title": "Post number 6",
125 | },
126 | "nextPageId": 6,
127 | "nextPagePath": "/blog/post-7",
128 | "pageId": 5,
129 | "previousItem": Object {
130 | "id": 4,
131 | "path": "/blog/post-5",
132 | "title": "Post number 4",
133 | },
134 | "previousPageId": 4,
135 | "previousPagePath": "/blog/post-5",
136 | },
137 | "path": "/blog/post-6",
138 | },
139 | ],
140 | Array [
141 | Object {
142 | "component": "/path/to/some/template.js",
143 | "context": Object {
144 | "nextItem": Object {
145 | "id": 7,
146 | "path": "/blog/post-8",
147 | "title": "Post number 7",
148 | },
149 | "nextPageId": 7,
150 | "nextPagePath": "/blog/post-8",
151 | "pageId": 6,
152 | "previousItem": Object {
153 | "id": 5,
154 | "path": "/blog/post-6",
155 | "title": "Post number 5",
156 | },
157 | "previousPageId": 5,
158 | "previousPagePath": "/blog/post-6",
159 | },
160 | "path": "/blog/post-7",
161 | },
162 | ],
163 | Array [
164 | Object {
165 | "component": "/path/to/some/template.js",
166 | "context": Object {
167 | "nextItem": Object {
168 | "id": 8,
169 | "path": "/blog/post-9",
170 | "title": "Post number 8",
171 | },
172 | "nextPageId": 8,
173 | "nextPagePath": "/blog/post-9",
174 | "pageId": 7,
175 | "previousItem": Object {
176 | "id": 6,
177 | "path": "/blog/post-7",
178 | "title": "Post number 6",
179 | },
180 | "previousPageId": 6,
181 | "previousPagePath": "/blog/post-7",
182 | },
183 | "path": "/blog/post-8",
184 | },
185 | ],
186 | Array [
187 | Object {
188 | "component": "/path/to/some/template.js",
189 | "context": Object {
190 | "nextItem": Object {
191 | "id": 9,
192 | "path": "/blog/post-10",
193 | "title": "Post number 9",
194 | },
195 | "nextPageId": 9,
196 | "nextPagePath": "/blog/post-10",
197 | "pageId": 8,
198 | "previousItem": Object {
199 | "id": 7,
200 | "path": "/blog/post-8",
201 | "title": "Post number 7",
202 | },
203 | "previousPageId": 7,
204 | "previousPagePath": "/blog/post-8",
205 | },
206 | "path": "/blog/post-9",
207 | },
208 | ],
209 | Array [
210 | Object {
211 | "component": "/path/to/some/template.js",
212 | "context": Object {
213 | "nextItem": Object {
214 | "id": 10,
215 | "path": "/blog/post-11",
216 | "title": "Post number 10",
217 | },
218 | "nextPageId": 10,
219 | "nextPagePath": "/blog/post-11",
220 | "pageId": 9,
221 | "previousItem": Object {
222 | "id": 8,
223 | "path": "/blog/post-9",
224 | "title": "Post number 8",
225 | },
226 | "previousPageId": 8,
227 | "previousPagePath": "/blog/post-9",
228 | },
229 | "path": "/blog/post-10",
230 | },
231 | ],
232 | Array [
233 | Object {
234 | "component": "/path/to/some/template.js",
235 | "context": Object {
236 | "nextItem": Object {
237 | "id": 11,
238 | "path": "/blog/post-12",
239 | "title": "Post number 11",
240 | },
241 | "nextPageId": 11,
242 | "nextPagePath": "/blog/post-12",
243 | "pageId": 10,
244 | "previousItem": Object {
245 | "id": 9,
246 | "path": "/blog/post-10",
247 | "title": "Post number 9",
248 | },
249 | "previousPageId": 9,
250 | "previousPagePath": "/blog/post-10",
251 | },
252 | "path": "/blog/post-11",
253 | },
254 | ],
255 | Array [
256 | Object {
257 | "component": "/path/to/some/template.js",
258 | "context": Object {
259 | "nextItem": Object {
260 | "id": 12,
261 | "path": "/blog/post-13",
262 | "title": "Post number 12",
263 | },
264 | "nextPageId": 12,
265 | "nextPagePath": "/blog/post-13",
266 | "pageId": 11,
267 | "previousItem": Object {
268 | "id": 10,
269 | "path": "/blog/post-11",
270 | "title": "Post number 10",
271 | },
272 | "previousPageId": 10,
273 | "previousPagePath": "/blog/post-11",
274 | },
275 | "path": "/blog/post-12",
276 | },
277 | ],
278 | Array [
279 | Object {
280 | "component": "/path/to/some/template.js",
281 | "context": Object {
282 | "nextItem": Object {
283 | "id": 13,
284 | "path": "/blog/post-14",
285 | "title": "Post number 13",
286 | },
287 | "nextPageId": 13,
288 | "nextPagePath": "/blog/post-14",
289 | "pageId": 12,
290 | "previousItem": Object {
291 | "id": 11,
292 | "path": "/blog/post-12",
293 | "title": "Post number 11",
294 | },
295 | "previousPageId": 11,
296 | "previousPagePath": "/blog/post-12",
297 | },
298 | "path": "/blog/post-13",
299 | },
300 | ],
301 | Array [
302 | Object {
303 | "component": "/path/to/some/template.js",
304 | "context": Object {
305 | "nextItem": Object {
306 | "id": 14,
307 | "path": "/blog/post-15",
308 | "title": "Post number 14",
309 | },
310 | "nextPageId": 14,
311 | "nextPagePath": "/blog/post-15",
312 | "pageId": 13,
313 | "previousItem": Object {
314 | "id": 12,
315 | "path": "/blog/post-13",
316 | "title": "Post number 12",
317 | },
318 | "previousPageId": 12,
319 | "previousPagePath": "/blog/post-13",
320 | },
321 | "path": "/blog/post-14",
322 | },
323 | ],
324 | Array [
325 | Object {
326 | "component": "/path/to/some/template.js",
327 | "context": Object {
328 | "nextItem": Object {
329 | "id": 15,
330 | "path": "/blog/post-16",
331 | "title": "Post number 15",
332 | },
333 | "nextPageId": 15,
334 | "nextPagePath": "/blog/post-16",
335 | "pageId": 14,
336 | "previousItem": Object {
337 | "id": 13,
338 | "path": "/blog/post-14",
339 | "title": "Post number 13",
340 | },
341 | "previousPageId": 13,
342 | "previousPagePath": "/blog/post-14",
343 | },
344 | "path": "/blog/post-15",
345 | },
346 | ],
347 | Array [
348 | Object {
349 | "component": "/path/to/some/template.js",
350 | "context": Object {
351 | "nextItem": Object {
352 | "id": 16,
353 | "path": "/blog/post-17",
354 | "title": "Post number 16",
355 | },
356 | "nextPageId": 16,
357 | "nextPagePath": "/blog/post-17",
358 | "pageId": 15,
359 | "previousItem": Object {
360 | "id": 14,
361 | "path": "/blog/post-15",
362 | "title": "Post number 14",
363 | },
364 | "previousPageId": 14,
365 | "previousPagePath": "/blog/post-15",
366 | },
367 | "path": "/blog/post-16",
368 | },
369 | ],
370 | Array [
371 | Object {
372 | "component": "/path/to/some/template.js",
373 | "context": Object {
374 | "nextItem": Object {
375 | "id": 17,
376 | "path": "/blog/post-18",
377 | "title": "Post number 17",
378 | },
379 | "nextPageId": 17,
380 | "nextPagePath": "/blog/post-18",
381 | "pageId": 16,
382 | "previousItem": Object {
383 | "id": 15,
384 | "path": "/blog/post-16",
385 | "title": "Post number 15",
386 | },
387 | "previousPageId": 15,
388 | "previousPagePath": "/blog/post-16",
389 | },
390 | "path": "/blog/post-17",
391 | },
392 | ],
393 | Array [
394 | Object {
395 | "component": "/path/to/some/template.js",
396 | "context": Object {
397 | "nextItem": Object {
398 | "id": 18,
399 | "path": "/blog/post-19",
400 | "title": "Post number 18",
401 | },
402 | "nextPageId": 18,
403 | "nextPagePath": "/blog/post-19",
404 | "pageId": 17,
405 | "previousItem": Object {
406 | "id": 16,
407 | "path": "/blog/post-17",
408 | "title": "Post number 16",
409 | },
410 | "previousPageId": 16,
411 | "previousPagePath": "/blog/post-17",
412 | },
413 | "path": "/blog/post-18",
414 | },
415 | ],
416 | Array [
417 | Object {
418 | "component": "/path/to/some/template.js",
419 | "context": Object {
420 | "nextItem": Object {
421 | "id": 19,
422 | "path": "/blog/post-20",
423 | "title": "Post number 19",
424 | },
425 | "nextPageId": 19,
426 | "nextPagePath": "/blog/post-20",
427 | "pageId": 18,
428 | "previousItem": Object {
429 | "id": 17,
430 | "path": "/blog/post-18",
431 | "title": "Post number 17",
432 | },
433 | "previousPageId": 17,
434 | "previousPagePath": "/blog/post-18",
435 | },
436 | "path": "/blog/post-19",
437 | },
438 | ],
439 | Array [
440 | Object {
441 | "component": "/path/to/some/template.js",
442 | "context": Object {
443 | "nextItem": Object {
444 | "id": 20,
445 | "path": "/blog/post-21",
446 | "title": "Post number 20",
447 | },
448 | "nextPageId": 20,
449 | "nextPagePath": "/blog/post-21",
450 | "pageId": 19,
451 | "previousItem": Object {
452 | "id": 18,
453 | "path": "/blog/post-19",
454 | "title": "Post number 18",
455 | },
456 | "previousPageId": 18,
457 | "previousPagePath": "/blog/post-19",
458 | },
459 | "path": "/blog/post-20",
460 | },
461 | ],
462 | Array [
463 | Object {
464 | "component": "/path/to/some/template.js",
465 | "context": Object {
466 | "nextItem": Object {
467 | "id": 21,
468 | "path": "/blog/post-22",
469 | "title": "Post number 21",
470 | },
471 | "nextPageId": 21,
472 | "nextPagePath": "/blog/post-22",
473 | "pageId": 20,
474 | "previousItem": Object {
475 | "id": 19,
476 | "path": "/blog/post-20",
477 | "title": "Post number 19",
478 | },
479 | "previousPageId": 19,
480 | "previousPagePath": "/blog/post-20",
481 | },
482 | "path": "/blog/post-21",
483 | },
484 | ],
485 | Array [
486 | Object {
487 | "component": "/path/to/some/template.js",
488 | "context": Object {
489 | "nextItem": Object {
490 | "id": 22,
491 | "path": "/blog/post-23",
492 | "title": "Post number 22",
493 | },
494 | "nextPageId": 22,
495 | "nextPagePath": "/blog/post-23",
496 | "pageId": 21,
497 | "previousItem": Object {
498 | "id": 20,
499 | "path": "/blog/post-21",
500 | "title": "Post number 20",
501 | },
502 | "previousPageId": 20,
503 | "previousPagePath": "/blog/post-21",
504 | },
505 | "path": "/blog/post-22",
506 | },
507 | ],
508 | Array [
509 | Object {
510 | "component": "/path/to/some/template.js",
511 | "context": Object {
512 | "nextItem": Object {
513 | "id": 23,
514 | "path": "/blog/post-24",
515 | "title": "Post number 23",
516 | },
517 | "nextPageId": 23,
518 | "nextPagePath": "/blog/post-24",
519 | "pageId": 22,
520 | "previousItem": Object {
521 | "id": 21,
522 | "path": "/blog/post-22",
523 | "title": "Post number 21",
524 | },
525 | "previousPageId": 21,
526 | "previousPagePath": "/blog/post-22",
527 | },
528 | "path": "/blog/post-23",
529 | },
530 | ],
531 | Array [
532 | Object {
533 | "component": "/path/to/some/template.js",
534 | "context": Object {
535 | "nextItem": Object {
536 | "id": 24,
537 | "path": "/blog/post-25",
538 | "title": "Post number 24",
539 | },
540 | "nextPageId": 24,
541 | "nextPagePath": "/blog/post-25",
542 | "pageId": 23,
543 | "previousItem": Object {
544 | "id": 22,
545 | "path": "/blog/post-23",
546 | "title": "Post number 22",
547 | },
548 | "previousPageId": 22,
549 | "previousPagePath": "/blog/post-23",
550 | },
551 | "path": "/blog/post-24",
552 | },
553 | ],
554 | Array [
555 | Object {
556 | "component": "/path/to/some/template.js",
557 | "context": Object {
558 | "nextItem": Object {
559 | "id": 25,
560 | "path": "/blog/post-26",
561 | "title": "Post number 25",
562 | },
563 | "nextPageId": 25,
564 | "nextPagePath": "/blog/post-26",
565 | "pageId": 24,
566 | "previousItem": Object {
567 | "id": 23,
568 | "path": "/blog/post-24",
569 | "title": "Post number 23",
570 | },
571 | "previousPageId": 23,
572 | "previousPagePath": "/blog/post-24",
573 | },
574 | "path": "/blog/post-25",
575 | },
576 | ],
577 | Array [
578 | Object {
579 | "component": "/path/to/some/template.js",
580 | "context": Object {
581 | "nextItem": Object {
582 | "id": 26,
583 | "path": "/blog/post-27",
584 | "title": "Post number 26",
585 | },
586 | "nextPageId": 26,
587 | "nextPagePath": "/blog/post-27",
588 | "pageId": 25,
589 | "previousItem": Object {
590 | "id": 24,
591 | "path": "/blog/post-25",
592 | "title": "Post number 24",
593 | },
594 | "previousPageId": 24,
595 | "previousPagePath": "/blog/post-25",
596 | },
597 | "path": "/blog/post-26",
598 | },
599 | ],
600 | Array [
601 | Object {
602 | "component": "/path/to/some/template.js",
603 | "context": Object {
604 | "nextItem": Object {
605 | "id": 27,
606 | "path": "/blog/post-28",
607 | "title": "Post number 27",
608 | },
609 | "nextPageId": 27,
610 | "nextPagePath": "/blog/post-28",
611 | "pageId": 26,
612 | "previousItem": Object {
613 | "id": 25,
614 | "path": "/blog/post-26",
615 | "title": "Post number 25",
616 | },
617 | "previousPageId": 25,
618 | "previousPagePath": "/blog/post-26",
619 | },
620 | "path": "/blog/post-27",
621 | },
622 | ],
623 | Array [
624 | Object {
625 | "component": "/path/to/some/template.js",
626 | "context": Object {
627 | "nextItem": Object {
628 | "id": 28,
629 | "path": "/blog/post-29",
630 | "title": "Post number 28",
631 | },
632 | "nextPageId": 28,
633 | "nextPagePath": "/blog/post-29",
634 | "pageId": 27,
635 | "previousItem": Object {
636 | "id": 26,
637 | "path": "/blog/post-27",
638 | "title": "Post number 26",
639 | },
640 | "previousPageId": 26,
641 | "previousPagePath": "/blog/post-27",
642 | },
643 | "path": "/blog/post-28",
644 | },
645 | ],
646 | Array [
647 | Object {
648 | "component": "/path/to/some/template.js",
649 | "context": Object {
650 | "nextItem": Object {
651 | "id": 29,
652 | "path": "/blog/post-30",
653 | "title": "Post number 29",
654 | },
655 | "nextPageId": 29,
656 | "nextPagePath": "/blog/post-30",
657 | "pageId": 28,
658 | "previousItem": Object {
659 | "id": 27,
660 | "path": "/blog/post-28",
661 | "title": "Post number 27",
662 | },
663 | "previousPageId": 27,
664 | "previousPagePath": "/blog/post-28",
665 | },
666 | "path": "/blog/post-29",
667 | },
668 | ],
669 | Array [
670 | Object {
671 | "component": "/path/to/some/template.js",
672 | "context": Object {
673 | "nextItem": Object {
674 | "id": 30,
675 | "path": "/blog/post-31",
676 | "title": "Post number 30",
677 | },
678 | "nextPageId": 30,
679 | "nextPagePath": "/blog/post-31",
680 | "pageId": 29,
681 | "previousItem": Object {
682 | "id": 28,
683 | "path": "/blog/post-29",
684 | "title": "Post number 28",
685 | },
686 | "previousPageId": 28,
687 | "previousPagePath": "/blog/post-29",
688 | },
689 | "path": "/blog/post-30",
690 | },
691 | ],
692 | Array [
693 | Object {
694 | "component": "/path/to/some/template.js",
695 | "context": Object {
696 | "nextItem": Object {
697 | "id": 31,
698 | "path": "/blog/post-32",
699 | "title": "Post number 31",
700 | },
701 | "nextPageId": 31,
702 | "nextPagePath": "/blog/post-32",
703 | "pageId": 30,
704 | "previousItem": Object {
705 | "id": 29,
706 | "path": "/blog/post-30",
707 | "title": "Post number 29",
708 | },
709 | "previousPageId": 29,
710 | "previousPagePath": "/blog/post-30",
711 | },
712 | "path": "/blog/post-31",
713 | },
714 | ],
715 | Array [
716 | Object {
717 | "component": "/path/to/some/template.js",
718 | "context": Object {
719 | "nextItem": Object {
720 | "id": 32,
721 | "path": "/blog/post-33",
722 | "title": "Post number 32",
723 | },
724 | "nextPageId": 32,
725 | "nextPagePath": "/blog/post-33",
726 | "pageId": 31,
727 | "previousItem": Object {
728 | "id": 30,
729 | "path": "/blog/post-31",
730 | "title": "Post number 30",
731 | },
732 | "previousPageId": 30,
733 | "previousPagePath": "/blog/post-31",
734 | },
735 | "path": "/blog/post-32",
736 | },
737 | ],
738 | Array [
739 | Object {
740 | "component": "/path/to/some/template.js",
741 | "context": Object {
742 | "nextItem": Object {
743 | "id": 33,
744 | "path": "/blog/post-34",
745 | "title": "Post number 33",
746 | },
747 | "nextPageId": 33,
748 | "nextPagePath": "/blog/post-34",
749 | "pageId": 32,
750 | "previousItem": Object {
751 | "id": 31,
752 | "path": "/blog/post-32",
753 | "title": "Post number 31",
754 | },
755 | "previousPageId": 31,
756 | "previousPagePath": "/blog/post-32",
757 | },
758 | "path": "/blog/post-33",
759 | },
760 | ],
761 | Array [
762 | Object {
763 | "component": "/path/to/some/template.js",
764 | "context": Object {
765 | "nextItem": Object {
766 | "id": 34,
767 | "path": "/blog/post-35",
768 | "title": "Post number 34",
769 | },
770 | "nextPageId": 34,
771 | "nextPagePath": "/blog/post-35",
772 | "pageId": 33,
773 | "previousItem": Object {
774 | "id": 32,
775 | "path": "/blog/post-33",
776 | "title": "Post number 32",
777 | },
778 | "previousPageId": 32,
779 | "previousPagePath": "/blog/post-33",
780 | },
781 | "path": "/blog/post-34",
782 | },
783 | ],
784 | Array [
785 | Object {
786 | "component": "/path/to/some/template.js",
787 | "context": Object {
788 | "nextItem": Object {
789 | "id": 35,
790 | "path": "/blog/post-36",
791 | "title": "Post number 35",
792 | },
793 | "nextPageId": 35,
794 | "nextPagePath": "/blog/post-36",
795 | "pageId": 34,
796 | "previousItem": Object {
797 | "id": 33,
798 | "path": "/blog/post-34",
799 | "title": "Post number 33",
800 | },
801 | "previousPageId": 33,
802 | "previousPagePath": "/blog/post-34",
803 | },
804 | "path": "/blog/post-35",
805 | },
806 | ],
807 | Array [
808 | Object {
809 | "component": "/path/to/some/template.js",
810 | "context": Object {
811 | "nextItem": Object {
812 | "id": 36,
813 | "path": "/blog/post-37",
814 | "title": "Post number 36",
815 | },
816 | "nextPageId": 36,
817 | "nextPagePath": "/blog/post-37",
818 | "pageId": 35,
819 | "previousItem": Object {
820 | "id": 34,
821 | "path": "/blog/post-35",
822 | "title": "Post number 34",
823 | },
824 | "previousPageId": 34,
825 | "previousPagePath": "/blog/post-35",
826 | },
827 | "path": "/blog/post-36",
828 | },
829 | ],
830 | Array [
831 | Object {
832 | "component": "/path/to/some/template.js",
833 | "context": Object {
834 | "nextItem": Object {
835 | "id": 37,
836 | "path": "/blog/post-38",
837 | "title": "Post number 37",
838 | },
839 | "nextPageId": 37,
840 | "nextPagePath": "/blog/post-38",
841 | "pageId": 36,
842 | "previousItem": Object {
843 | "id": 35,
844 | "path": "/blog/post-36",
845 | "title": "Post number 35",
846 | },
847 | "previousPageId": 35,
848 | "previousPagePath": "/blog/post-36",
849 | },
850 | "path": "/blog/post-37",
851 | },
852 | ],
853 | Array [
854 | Object {
855 | "component": "/path/to/some/template.js",
856 | "context": Object {
857 | "nextItem": Object {
858 | "id": 38,
859 | "path": "/blog/post-39",
860 | "title": "Post number 38",
861 | },
862 | "nextPageId": 38,
863 | "nextPagePath": "/blog/post-39",
864 | "pageId": 37,
865 | "previousItem": Object {
866 | "id": 36,
867 | "path": "/blog/post-37",
868 | "title": "Post number 36",
869 | },
870 | "previousPageId": 36,
871 | "previousPagePath": "/blog/post-37",
872 | },
873 | "path": "/blog/post-38",
874 | },
875 | ],
876 | Array [
877 | Object {
878 | "component": "/path/to/some/template.js",
879 | "context": Object {
880 | "nextItem": Object {
881 | "id": 39,
882 | "path": "/blog/post-40",
883 | "title": "Post number 39",
884 | },
885 | "nextPageId": 39,
886 | "nextPagePath": "/blog/post-40",
887 | "pageId": 38,
888 | "previousItem": Object {
889 | "id": 37,
890 | "path": "/blog/post-38",
891 | "title": "Post number 37",
892 | },
893 | "previousPageId": 37,
894 | "previousPagePath": "/blog/post-38",
895 | },
896 | "path": "/blog/post-39",
897 | },
898 | ],
899 | Array [
900 | Object {
901 | "component": "/path/to/some/template.js",
902 | "context": Object {
903 | "nextItem": Object {
904 | "id": 40,
905 | "path": "/blog/post-41",
906 | "title": "Post number 40",
907 | },
908 | "nextPageId": 40,
909 | "nextPagePath": "/blog/post-41",
910 | "pageId": 39,
911 | "previousItem": Object {
912 | "id": 38,
913 | "path": "/blog/post-39",
914 | "title": "Post number 38",
915 | },
916 | "previousPageId": 38,
917 | "previousPagePath": "/blog/post-39",
918 | },
919 | "path": "/blog/post-40",
920 | },
921 | ],
922 | Array [
923 | Object {
924 | "component": "/path/to/some/template.js",
925 | "context": Object {
926 | "nextItem": Object {
927 | "id": 41,
928 | "path": "/blog/post-42",
929 | "title": "Post number 41",
930 | },
931 | "nextPageId": 41,
932 | "nextPagePath": "/blog/post-42",
933 | "pageId": 40,
934 | "previousItem": Object {
935 | "id": 39,
936 | "path": "/blog/post-40",
937 | "title": "Post number 39",
938 | },
939 | "previousPageId": 39,
940 | "previousPagePath": "/blog/post-40",
941 | },
942 | "path": "/blog/post-41",
943 | },
944 | ],
945 | Array [
946 | Object {
947 | "component": "/path/to/some/template.js",
948 | "context": Object {
949 | "nextItem": Object {
950 | "id": 42,
951 | "path": "/blog/post-43",
952 | "title": "Post number 42",
953 | },
954 | "nextPageId": 42,
955 | "nextPagePath": "/blog/post-43",
956 | "pageId": 41,
957 | "previousItem": Object {
958 | "id": 40,
959 | "path": "/blog/post-41",
960 | "title": "Post number 40",
961 | },
962 | "previousPageId": 40,
963 | "previousPagePath": "/blog/post-41",
964 | },
965 | "path": "/blog/post-42",
966 | },
967 | ],
968 | Array [
969 | Object {
970 | "component": "/path/to/some/template.js",
971 | "context": Object {
972 | "nextItem": undefined,
973 | "nextPageId": "",
974 | "nextPagePath": "",
975 | "pageId": 42,
976 | "previousItem": Object {
977 | "id": 41,
978 | "path": "/blog/post-42",
979 | "title": "Post number 41",
980 | },
981 | "previousPageId": 41,
982 | "previousPagePath": "/blog/post-42",
983 | },
984 | "path": "/blog/post-43",
985 | },
986 | ],
987 | ],
988 | "results": Array [
989 | Object {
990 | "isThrow": false,
991 | "value": undefined,
992 | },
993 | Object {
994 | "isThrow": false,
995 | "value": undefined,
996 | },
997 | Object {
998 | "isThrow": false,
999 | "value": undefined,
1000 | },
1001 | Object {
1002 | "isThrow": false,
1003 | "value": undefined,
1004 | },
1005 | Object {
1006 | "isThrow": false,
1007 | "value": undefined,
1008 | },
1009 | Object {
1010 | "isThrow": false,
1011 | "value": undefined,
1012 | },
1013 | Object {
1014 | "isThrow": false,
1015 | "value": undefined,
1016 | },
1017 | Object {
1018 | "isThrow": false,
1019 | "value": undefined,
1020 | },
1021 | Object {
1022 | "isThrow": false,
1023 | "value": undefined,
1024 | },
1025 | Object {
1026 | "isThrow": false,
1027 | "value": undefined,
1028 | },
1029 | Object {
1030 | "isThrow": false,
1031 | "value": undefined,
1032 | },
1033 | Object {
1034 | "isThrow": false,
1035 | "value": undefined,
1036 | },
1037 | Object {
1038 | "isThrow": false,
1039 | "value": undefined,
1040 | },
1041 | Object {
1042 | "isThrow": false,
1043 | "value": undefined,
1044 | },
1045 | Object {
1046 | "isThrow": false,
1047 | "value": undefined,
1048 | },
1049 | Object {
1050 | "isThrow": false,
1051 | "value": undefined,
1052 | },
1053 | Object {
1054 | "isThrow": false,
1055 | "value": undefined,
1056 | },
1057 | Object {
1058 | "isThrow": false,
1059 | "value": undefined,
1060 | },
1061 | Object {
1062 | "isThrow": false,
1063 | "value": undefined,
1064 | },
1065 | Object {
1066 | "isThrow": false,
1067 | "value": undefined,
1068 | },
1069 | Object {
1070 | "isThrow": false,
1071 | "value": undefined,
1072 | },
1073 | Object {
1074 | "isThrow": false,
1075 | "value": undefined,
1076 | },
1077 | Object {
1078 | "isThrow": false,
1079 | "value": undefined,
1080 | },
1081 | Object {
1082 | "isThrow": false,
1083 | "value": undefined,
1084 | },
1085 | Object {
1086 | "isThrow": false,
1087 | "value": undefined,
1088 | },
1089 | Object {
1090 | "isThrow": false,
1091 | "value": undefined,
1092 | },
1093 | Object {
1094 | "isThrow": false,
1095 | "value": undefined,
1096 | },
1097 | Object {
1098 | "isThrow": false,
1099 | "value": undefined,
1100 | },
1101 | Object {
1102 | "isThrow": false,
1103 | "value": undefined,
1104 | },
1105 | Object {
1106 | "isThrow": false,
1107 | "value": undefined,
1108 | },
1109 | Object {
1110 | "isThrow": false,
1111 | "value": undefined,
1112 | },
1113 | Object {
1114 | "isThrow": false,
1115 | "value": undefined,
1116 | },
1117 | Object {
1118 | "isThrow": false,
1119 | "value": undefined,
1120 | },
1121 | Object {
1122 | "isThrow": false,
1123 | "value": undefined,
1124 | },
1125 | Object {
1126 | "isThrow": false,
1127 | "value": undefined,
1128 | },
1129 | Object {
1130 | "isThrow": false,
1131 | "value": undefined,
1132 | },
1133 | Object {
1134 | "isThrow": false,
1135 | "value": undefined,
1136 | },
1137 | Object {
1138 | "isThrow": false,
1139 | "value": undefined,
1140 | },
1141 | Object {
1142 | "isThrow": false,
1143 | "value": undefined,
1144 | },
1145 | Object {
1146 | "isThrow": false,
1147 | "value": undefined,
1148 | },
1149 | Object {
1150 | "isThrow": false,
1151 | "value": undefined,
1152 | },
1153 | Object {
1154 | "isThrow": false,
1155 | "value": undefined,
1156 | },
1157 | Object {
1158 | "isThrow": false,
1159 | "value": undefined,
1160 | },
1161 | ],
1162 | }
1163 | `;
1164 |
1165 | exports[`paginate() allows fine-tuning pathPrefix with a function 1`] = `
1166 | [MockFunction] {
1167 | "calls": Array [
1168 | Array [
1169 | Object {
1170 | "component": "path/to/blog",
1171 | "context": Object {
1172 | "humanPageNumber": 1,
1173 | "limit": 3,
1174 | "nextPagePath": "/blog/page/2",
1175 | "numberOfPages": 4,
1176 | "pageNumber": 0,
1177 | "previousPagePath": "",
1178 | "skip": 0,
1179 | },
1180 | "path": "/blog",
1181 | },
1182 | ],
1183 | Array [
1184 | Object {
1185 | "component": "path/to/blog",
1186 | "context": Object {
1187 | "humanPageNumber": 2,
1188 | "limit": 3,
1189 | "nextPagePath": "/blog/page/3",
1190 | "numberOfPages": 4,
1191 | "pageNumber": 1,
1192 | "previousPagePath": "/blog",
1193 | "skip": 3,
1194 | },
1195 | "path": "/blog/page/2",
1196 | },
1197 | ],
1198 | Array [
1199 | Object {
1200 | "component": "path/to/blog",
1201 | "context": Object {
1202 | "humanPageNumber": 3,
1203 | "limit": 3,
1204 | "nextPagePath": "/blog/page/4",
1205 | "numberOfPages": 4,
1206 | "pageNumber": 2,
1207 | "previousPagePath": "/blog/page/2",
1208 | "skip": 6,
1209 | },
1210 | "path": "/blog/page/3",
1211 | },
1212 | ],
1213 | Array [
1214 | Object {
1215 | "component": "path/to/blog",
1216 | "context": Object {
1217 | "humanPageNumber": 4,
1218 | "limit": 3,
1219 | "nextPagePath": "",
1220 | "numberOfPages": 4,
1221 | "pageNumber": 3,
1222 | "previousPagePath": "/blog/page/3",
1223 | "skip": 9,
1224 | },
1225 | "path": "/blog/page/4",
1226 | },
1227 | ],
1228 | ],
1229 | "results": Array [
1230 | Object {
1231 | "isThrow": false,
1232 | "value": undefined,
1233 | },
1234 | Object {
1235 | "isThrow": false,
1236 | "value": undefined,
1237 | },
1238 | Object {
1239 | "isThrow": false,
1240 | "value": undefined,
1241 | },
1242 | Object {
1243 | "isThrow": false,
1244 | "value": undefined,
1245 | },
1246 | ],
1247 | }
1248 | `;
1249 |
1250 | exports[`paginate() creates pages and forwards context 1`] = `
1251 | [MockFunction] {
1252 | "calls": Array [
1253 | Array [
1254 | Object {
1255 | "component": "path/to/blog",
1256 | "context": Object {
1257 | "additionalContext": true,
1258 | "humanPageNumber": 1,
1259 | "limit": 5,
1260 | "nextPagePath": "/blog/2",
1261 | "numberOfPages": 4,
1262 | "pageNumber": 0,
1263 | "previousPagePath": "",
1264 | "skip": 0,
1265 | },
1266 | "path": "/blog",
1267 | },
1268 | ],
1269 | Array [
1270 | Object {
1271 | "component": "path/to/blog",
1272 | "context": Object {
1273 | "additionalContext": true,
1274 | "humanPageNumber": 2,
1275 | "limit": 3,
1276 | "nextPagePath": "/blog/3",
1277 | "numberOfPages": 4,
1278 | "pageNumber": 1,
1279 | "previousPagePath": "/blog",
1280 | "skip": 5,
1281 | },
1282 | "path": "/blog/2",
1283 | },
1284 | ],
1285 | Array [
1286 | Object {
1287 | "component": "path/to/blog",
1288 | "context": Object {
1289 | "additionalContext": true,
1290 | "humanPageNumber": 3,
1291 | "limit": 3,
1292 | "nextPagePath": "/blog/4",
1293 | "numberOfPages": 4,
1294 | "pageNumber": 2,
1295 | "previousPagePath": "/blog/2",
1296 | "skip": 8,
1297 | },
1298 | "path": "/blog/3",
1299 | },
1300 | ],
1301 | Array [
1302 | Object {
1303 | "component": "path/to/blog",
1304 | "context": Object {
1305 | "additionalContext": true,
1306 | "humanPageNumber": 4,
1307 | "limit": 3,
1308 | "nextPagePath": "",
1309 | "numberOfPages": 4,
1310 | "pageNumber": 3,
1311 | "previousPagePath": "/blog/3",
1312 | "skip": 11,
1313 | },
1314 | "path": "/blog/4",
1315 | },
1316 | ],
1317 | ],
1318 | "results": Array [
1319 | Object {
1320 | "isThrow": false,
1321 | "value": undefined,
1322 | },
1323 | Object {
1324 | "isThrow": false,
1325 | "value": undefined,
1326 | },
1327 | Object {
1328 | "isThrow": false,
1329 | "value": undefined,
1330 | },
1331 | Object {
1332 | "isThrow": false,
1333 | "value": undefined,
1334 | },
1335 | ],
1336 | }
1337 | `;
1338 |
--------------------------------------------------------------------------------
/__tests__/index.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 | import fp from "lodash/fp";
3 | import { paginate, createPagePerItem } from "../src";
4 |
5 | describe("paginate()", () => {
6 | it("creates pages and forwards context", () => {
7 | const createPage = jest.fn();
8 | paginate({
9 | createPage,
10 | items: Array(12).fill(),
11 | itemsPerPage: 3,
12 | itemsPerFirstPage: 5,
13 | pathPrefix: "/blog",
14 | component: "path/to/blog",
15 | context: { additionalContext: true }
16 | });
17 | expect(createPage).toMatchSnapshot();
18 | });
19 |
20 | it("allows fine-tuning pathPrefix with a function", () => {
21 | const createPage = jest.fn();
22 | paginate({
23 | createPage,
24 | items: Array(12).fill(),
25 | itemsPerPage: 3,
26 | pathPrefix: ({ pageNumber }) =>
27 | pageNumber === 0 ? "/blog" : "/blog/page",
28 | component: "path/to/blog"
29 | });
30 | expect(createPage).toMatchSnapshot();
31 | });
32 | });
33 |
34 | const blogPosts = _.map(Array(43), (item, index) => ({
35 | id: index,
36 | title: `Post number ${index.toString()}`,
37 | path: `/blog/post-${index + 1}`
38 | }));
39 |
40 | describe("createPagePerItem()", () => {
41 | it("Calls createPage() once per item", () => {
42 | const createPage = jest.fn();
43 |
44 | createPagePerItem({
45 | createPage,
46 | items: fp.cloneDeep(blogPosts),
47 | itemToPath: "path",
48 | itemToId: "id",
49 | component: "/path/to/some/template.js"
50 | });
51 |
52 | expect(createPage).toHaveBeenCalledTimes(blogPosts.length);
53 | });
54 |
55 | it("Calls createPage() with the correct params", () => {
56 | const createPage = jest.fn();
57 |
58 | createPagePerItem({
59 | createPage,
60 | items: fp.cloneDeep(blogPosts),
61 | itemToPath: "path",
62 | itemToId: "id",
63 | component: "/path/to/some/template.js"
64 | });
65 |
66 | expect(createPage).toMatchSnapshot();
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/__tests__/utils.js:
--------------------------------------------------------------------------------
1 | import {
2 | getPreviousItem,
3 | getNextItem,
4 | paginatedPath,
5 | calculateSkip
6 | } from "../src/utils";
7 |
8 | describe("utils", () => {
9 | describe("getPreviousItem()", () => {
10 | it("Returns the second item", () => {
11 | const items = [{ name: "one" }, { name: "two" }, { name: "three" }];
12 | expect(getPreviousItem(items, 2)).toEqual(items[1]);
13 | });
14 |
15 | it("Returns undefined for index 0", () => {
16 | const items = [{ name: "one" }, { name: "two" }, { name: "three" }];
17 | expect(getPreviousItem(items, 0)).toBeUndefined();
18 | });
19 | });
20 |
21 | describe("getNextItem()", () => {
22 | it("Returns the third item", () => {
23 | const items = [{ name: "one" }, { name: "two" }, { name: "three" }];
24 | expect(getNextItem(items, 1)).toEqual(items[2]);
25 | });
26 |
27 | it("Returns undefined for index 2", () => {
28 | const items = [{ name: "one" }, { name: "two" }, { name: "three" }];
29 | expect(getNextItem(items, 2)).toBeUndefined();
30 | });
31 | });
32 |
33 | describe("paginatedPath()", () => {
34 | it("Returns only one slash for the index page", () => {
35 | expect(paginatedPath("/", 0, 3)).toEqual("/");
36 | });
37 |
38 | it("Returns /2 for the second index page", () => {
39 | expect(paginatedPath("/", 1, 3)).toEqual("/2");
40 | });
41 |
42 | it("Passes pageNumber and numberOfPages to pathPrefix()", () => {
43 | const pathPrefix = jest.fn(() => "/foo");
44 | expect(paginatedPath(pathPrefix, 0, 3)).toEqual("/foo");
45 | expect(pathPrefix).toHaveBeenCalledWith({
46 | pageNumber: 0,
47 | numberOfPages: 3
48 | });
49 | });
50 |
51 | it("Returns empty string for the last + 1 page", () => {
52 | expect(paginatedPath("/foo", 2, 2)).toEqual("");
53 | });
54 |
55 | it("Returns empty string for page -1", () => {
56 | expect(paginatedPath("/foo", -1, 2)).toEqual("");
57 | });
58 | });
59 |
60 | describe("calculateSkip()", () => {
61 | it("Returns zero for the first page", () => {
62 | expect(calculateSkip(0, 3, 10)).toEqual(0);
63 | });
64 |
65 | it("Returns firstPageCount for the second page", () => {
66 | expect(calculateSkip(1, 3, 10)).toEqual(3);
67 | });
68 |
69 | it("Returns firstPageCount + itemsPerPage for the third page", () => {
70 | expect(calculateSkip(2, 3, 10)).toEqual(13);
71 | });
72 |
73 | it("Returns firstPageCount + (3 x itemsPerPage) for the fifth page", () => {
74 | expect(calculateSkip(4, 3, 10)).toEqual(33);
75 | });
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | allow:
8 | - dependency-type: "production"
9 |
--------------------------------------------------------------------------------
/docs/examples.md:
--------------------------------------------------------------------------------
1 | ## Examples
2 |
3 | ## `gatsby-node.js`
4 |
5 | ```javascript
6 | import { paginate, createPagePerItem } from "gatsby-awesome-pagination";
7 | ```
8 |
9 | ```javascript
10 | exports.createPages = ({ graphql, boundActionCreators }) => {
11 | const { createPage } = boundActionCreators;
12 |
13 | // We return a promise immediately
14 | return new Promise((resolve, reject) => {
15 | // Start by creating all the blog pages
16 | const blogPost = path.resolve("./src/templates/blog-post.js");
17 | const blogIndex = path.resolve("./src/templates/blog-index.js");
18 | resolve(
19 | graphql(
20 | `
21 | {
22 | allMarkdownRemark(
23 | sort: { fields: [frontmatter___date], order: DESC }
24 | ) {
25 | edges {
26 | node {
27 | id
28 | frontmatter {
29 | permalink
30 | }
31 | }
32 | }
33 | }
34 | }
35 | `
36 | ).then(result => {
37 | if (result.errors) {
38 | console.log(result.errors);
39 | reject(result.errors);
40 | }
41 |
42 | // Get an array of posts from the query result
43 | const blogPosts = _.get(result, "data.allMarkdownRemark.edges");
44 |
45 | // Create the blog index pages like `/blog`, `/blog/2`, `/blog/3`, etc.
46 | // The first page will have 3 items and each following page will have 10
47 | // blog posts and a link to the next and previous pages.
48 | paginate({
49 | createPage,
50 | items: blogPosts,
51 | component: blogIndex,
52 | itemsPerPage: 10,
53 | itemsPerFirstPage: 3,
54 | pathPrefix: "/blog"
55 | });
56 |
57 | // Create one page per blog post, with a link to the previous and next
58 | // blog posts.
59 | createPagePerItem({
60 | createPage,
61 | items: blogPosts,
62 | component: blogPost,
63 | itemToPath: "node.frontmatter.permalink",
64 | itemToId: "node.id"
65 | });
66 | })
67 | );
68 | });
69 | };
70 | ```
71 |
72 | ## Blog index component
73 |
74 | Retrieve posts via the `$skip` and `$limit` parameters like so:
75 |
76 | ```javascript
77 | export const pageQuery = graphql`
78 | query($skip: Int!, $limit: Int!) {
79 | posts: allMarkdownRemark(
80 | sort: { fields: [frontmatter___date], order: DESC }
81 | skip: $skip
82 | limit: $limit
83 | ) {
84 | edges {
85 | node {
86 | excerpt(pruneLength: 280)
87 | fields {
88 | permalink
89 | }
90 | frontmatter {
91 | date(formatString: "DD MMMM, YYYY")
92 | title
93 | }
94 | }
95 | }
96 | }
97 | }
98 | `;
99 | ```
100 |
101 | Render posts like so:
102 |
103 | ```javascript
104 | const BlogIndex = props => {
105 | const { pageContext } = props;
106 | const { previousPagePath, nextPagePath } = pageContext;
107 |
108 | return (
109 |
110 | {props.data.posts.edges.map(edge =>
)}
111 |
112 | {previousPagePath ? Previous : null}
113 | {nextPagePath ? Next : null}
114 |
115 |
116 | );
117 | };
118 | ```
119 |
120 | ## Blog post component
121 |
122 | First, you can retrieve any data you need like so:
123 |
124 | ```javascript
125 | export const pageQuery = graphql`
126 | query($pageId: String!) {
127 | post: markdownRemark(id: { eq: $pageId }) {
128 | html
129 | frontmatter {
130 | title
131 | }
132 | }
133 | }
134 | `;
135 | ```
136 |
137 | Then inside your component you can render links to the previous and next posts.
138 |
139 | ```javascript
140 | const BlogPost = props => {
141 | const { pageContext, data } = props;
142 | const { previousPagePath, nextPagePath, previousItem, nextItem } = pageContext;
143 | const { post } = data;
144 |
145 | return (
146 |
147 |
{post.frontmatter.title}
148 |
149 |
150 | {previousPagePath ? (
151 |
152 | {previousItem.node.frontmatter.title}
153 |
154 | ) : null}
155 | {nextPagePath ? (
156 |
157 | {nextItem.node.frontmatter.title}
158 |
159 | ) : null}
160 |
161 |
162 | );
163 | };
164 | ```
165 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby-awesome-pagination",
3 | "version": "0.3.8",
4 | "description": "An opinionated, more awesome approach to pagination in Gatsby",
5 | "files": [
6 | "lib"
7 | ],
8 | "main": "lib/index.js",
9 | "browser": "lib/components.js",
10 | "scripts": {
11 | "prepack": "npm run build",
12 | "build": "npm run prepack:babel && npm run prepack:flow",
13 | "prepack:babel": "babel src/ -d lib",
14 | "prepack:flow": "flow-copy-source src lib",
15 | "test": "jest"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/GatsbyCentral/gatsby-plugin-awesome-pagination.git"
20 | },
21 | "bugs": {
22 | "url": "https://github.com/GatsbyCentral/gatsby-plugin-awesome-pagination/issues"
23 | },
24 | "homepage": "https://github.com/GatsbyCentral/gatsby-plugin-awesome-pagination#readme",
25 | "keywords": [
26 | "gatsby",
27 | "gatsby-plugin",
28 | "pagination"
29 | ],
30 | "author": "Callum Macdonald",
31 | "license": "MIT",
32 | "devDependencies": {
33 | "babel-cli": "^6.26.0",
34 | "babel-jest": "^23.6.0",
35 | "babel-plugin-add-module-exports": "^0.2.1",
36 | "babel-plugin-transform-flow-strip-types": "^6.22.0",
37 | "babel-plugin-transform-react-jsx": "^6.24.1",
38 | "babel-preset-env": "^1.7.0",
39 | "flow-bin": "^0.72.0",
40 | "flow-copy-source": "^1.3.0",
41 | "jest": "^23.6.0"
42 | },
43 | "dependencies": {
44 | "lodash": "^4.17.21"
45 | },
46 | "peerDependencies": {
47 | "gatsby": ">=2.0.0",
48 | "react": ">=16.0.0"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Link } from "gatsby";
3 |
4 | const linkOrText = (
5 | label: string,
6 | link: string,
7 | activeStyle: {},
8 | inactiveStyle: {}
9 | ): React.Node =>
10 | link ? (
11 |
12 | {label}
13 |
14 | ) : (
15 | {label}
16 | );
17 |
18 | type PaginationLinksProps = {
19 | previousLabel?: string,
20 | nextLabel?: string,
21 | pageLabel?: string,
22 | separator?: string,
23 | pageContext: {
24 | pageNumber: number,
25 | humanPageNumber: number,
26 | skip: number,
27 | limit: number,
28 | numberOfPages: number,
29 | previousPagePath?: string,
30 | nextPagePath?: string
31 | }
32 | };
33 |
34 | export const PaginationLinks = ({
35 | previousLabel = "← previous",
36 | nextLabel = "next →",
37 | pageLabel = "Page: %d",
38 | separator = " - ",
39 | activeStyle = {},
40 | inactiveStyle = { textDecorationLine: "line-through", color: "grey" },
41 | pageContext: {
42 | pageNumber,
43 | humanPageNumber,
44 | skip,
45 | limit,
46 | numberOfPages,
47 | previousPagePath,
48 | nextPagePath
49 | }
50 | }: PaginationLinksProps): React.Node => {
51 | return (
52 |
53 | {linkOrText(previousLabel, previousPagePath, activeStyle, inactiveStyle)}
54 | {separator}
55 | {pageLabel.replace("%d", humanPageNumber)}
56 | {separator}
57 | {linkOrText(nextLabel, nextPagePath)}
58 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import isString from "lodash/fp/isString";
4 | import get from "lodash/fp/get";
5 | import times from "lodash/fp/times";
6 | import cloneDeep from "lodash/fp/cloneDeep";
7 | import isInteger from "lodash/fp/isInteger";
8 |
9 | import {
10 | paginatedPath,
11 | getPreviousItem,
12 | getNextItem,
13 | calculateSkip
14 | } from "./utils";
15 |
16 | import type { PathPrefix } from "./utils";
17 |
18 | type CreatePage = ({}) => void;
19 |
20 | type PaginateOpts = {
21 | createPage: CreatePage,
22 | items: {}[],
23 | itemsPerPage: number,
24 | itemsPerFirstPage?: number,
25 | pathPrefix: PathPrefix,
26 | component: string,
27 | context?: {}
28 | };
29 | export const paginate = (opts: PaginateOpts): void => {
30 | const {
31 | createPage,
32 | items,
33 | itemsPerPage,
34 | itemsPerFirstPage,
35 | pathPrefix,
36 | component,
37 | context
38 | } = opts;
39 |
40 | // How many items do we have in total? We use `items.length` here. In fact, we
41 | // could just accept an integer in the API as the actual contents of `items`
42 | // is never used.
43 | const totalItems = items.length;
44 | // If `itemsPerFirstPage` is specified, use that value for the first page,
45 | // otherwise use `itemsPerPage`.
46 | // $FlowExpectError
47 | const firstPageCount: number = isInteger(itemsPerFirstPage)
48 | ? itemsPerFirstPage
49 | : itemsPerPage;
50 |
51 | // How many page should we have?
52 | const numberOfPages =
53 | // If there are less than `firstPageCount` items, we'll only have 1 page
54 | totalItems <= firstPageCount
55 | ? 1
56 | : Math.ceil((totalItems - firstPageCount) / itemsPerPage) + 1;
57 |
58 | // Iterate as many times as we need pages
59 | times((pageNumber: number) => {
60 | // Create the path for this page
61 | const path = paginatedPath(pathPrefix, pageNumber, numberOfPages);
62 |
63 | // Calculate the path for the previous and next pages
64 | const previousPagePath = paginatedPath(
65 | pathPrefix,
66 | pageNumber - 1,
67 | numberOfPages
68 | );
69 | const nextPagePath = paginatedPath(
70 | pathPrefix,
71 | pageNumber + 1,
72 | numberOfPages
73 | );
74 |
75 | // Call `createPage()` for this paginated page
76 | createPage({
77 | path,
78 | component,
79 | // Clone the passed `context` and extend our new pagination context values
80 | // on top of it.
81 | context: Object.assign({}, cloneDeep(context), {
82 | pageNumber,
83 | humanPageNumber: pageNumber + 1,
84 | skip: calculateSkip(pageNumber, firstPageCount, itemsPerPage),
85 | limit: pageNumber === 0 ? firstPageCount : itemsPerPage,
86 | numberOfPages,
87 | previousPagePath,
88 | nextPagePath
89 | })
90 | });
91 | })(numberOfPages);
92 | };
93 |
94 | type CreatePagePerItemOpts = {
95 | createPage: CreatePage,
96 | items: {}[],
97 | itemToPath: string | (({}) => string),
98 | itemToId: string | (({}) => string),
99 | component: string
100 | };
101 | export const createPagePerItem = (opts: CreatePagePerItemOpts): void => {
102 | const { createPage, items, itemToPath, itemToId, component } = opts;
103 |
104 | // $FlowExpectError
105 | const getPath: ({}) => string = isString(itemToPath)
106 | ? get(itemToPath)
107 | : itemToPath;
108 | // $FlowExpectError
109 | const getId: ({}) => string = isString(itemToId) ? get(itemToId) : itemToId;
110 |
111 | // We cannot use `forEach()` here because in the FP version of lodash, the
112 | // iteratee is capped to a single argument, the item itself. We cannot get the
113 | // item's index in the array. So instead we use `times()` and provide the
114 | // length of the array.
115 | times((index: number) => {
116 | const item = items[index];
117 | const path = getPath(item);
118 | const id = getId(item);
119 |
120 | // NOTE: If there is no previous / next item, we set an empty string as the
121 | // value for the next and previous path and ID. Gatsby ignores context
122 | // values which are undefined, so we need these to exist.
123 | const previousItem = getPreviousItem(items, index);
124 | const previousPath = getPath(previousItem) || "";
125 | const nextItem = getNextItem(items, index);
126 | const nextPath = getPath(nextItem) || "";
127 |
128 | // Does the item have a `context` field?
129 | const itemContext = get("context")(item) || {};
130 | const context = Object.assign({}, itemContext, {
131 | pageId: id,
132 | previousPagePath: previousPath,
133 | previousItem: previousItem,
134 | previousPageId: getId(previousItem) || "",
135 | nextPagePath: nextPath,
136 | nextItem: nextItem,
137 | nextPageId: getId(nextItem) || ""
138 | });
139 |
140 | // Call `createPage()` for this item
141 | createPage({
142 | path,
143 | component,
144 | context
145 | });
146 | })(items.length);
147 | };
148 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import get from "lodash/fp/get";
3 |
4 | type ReturnedItem = {} | void;
5 |
6 | export const getPreviousItem = (items: {}[], index: number): ReturnedItem => {
7 | // If this is the first (or -1) element, there is no previous, so return
8 | // undefined
9 | if (index <= 0) {
10 | return;
11 | }
12 |
13 | // Get the previous element
14 | return get(`[${index - 1}]`, items);
15 | };
16 |
17 | export const getNextItem = (items: {}[], index: number): ReturnedItem => {
18 | // If this is the last element (or later), there is no previous, so return
19 | // undefined
20 | if (index >= items.length - 1) {
21 | return;
22 | }
23 |
24 | // Get the previous element
25 | return get(`[${index + 1}]`, items);
26 | };
27 |
28 | type PathPrefixFunction = ({
29 | pageNumber: number,
30 | numberOfPages: number
31 | }) => string;
32 |
33 | export type PathPrefix = string | PathPrefixFunction;
34 |
35 | export const paginatedPath = (
36 | pathPrefix: PathPrefix,
37 | pageNumber: number,
38 | numberOfPages: number
39 | ): string => {
40 | // If this page is less than zero (-1 for example), then it it does not
41 | // exist, return an empty string.
42 | if (pageNumber < 0) {
43 | return "";
44 | }
45 |
46 | // If this page number (which is zero indexed) plus one is more than the total
47 | // number of pages, then this page does not exist, so return an empty string.
48 | if (pageNumber + 1 > numberOfPages) {
49 | return "";
50 | }
51 |
52 | // Calculate a path prefix either by calling `pathPrefix()` if it's a function
53 | // or simply using `pathPrefix` if it is a string.
54 | const prefix =
55 | typeof pathPrefix === "function"
56 | ? pathPrefix({ pageNumber, numberOfPages })
57 | : pathPrefix;
58 |
59 | // If this is page 0, return only the pathPrefix
60 | if (pageNumber === 0) {
61 | return prefix;
62 | }
63 |
64 | // Otherwise, add a slash and the number + 1. We add 1 because `pageNumber` is
65 | // zero indexed, but for human consuption, we want 1 indexed numbers.
66 | return `${prefix !== "/" ? prefix : ""}/${pageNumber + 1}`;
67 | // NOTE: If `pathPrefix` is a single slash (the index page) then we do not
68 | // want to output two slashes, so we omit it.
69 | };
70 |
71 | export const calculateSkip = (
72 | pageNumber: number,
73 | firstPageCount: number,
74 | itemsPerPage: number
75 | ): number => {
76 | if (pageNumber === 0) {
77 | return 0;
78 | } else if (pageNumber === 1) {
79 | return firstPageCount;
80 | } else {
81 | return firstPageCount + itemsPerPage * (pageNumber - 1);
82 | }
83 | };
84 |
--------------------------------------------------------------------------------