@tryghost/algolia 0.2.11 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +42 -39
- package/bin/cli.js +25 -11
- package/lib/fetch-posts.js +73 -0
- package/package.json +23 -22
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,70 +1,73 @@
|
|
|
1
1
|
# Algolia Ghost CLI
|
|
2
2
|
|
|
3
|
-
CLI
|
|
3
|
+
`@tryghost/algolia` is a CLI for initially indexing the full published content of a Ghost site in Algolia.
|
|
4
|
+
|
|
5
|
+
The CLI uses the Ghost 6 Content API. By default, it requests the [maximum 100 posts](https://docs.ghost.org/content-api/parameters#limit) at a time, follows Ghost's [`meta.pagination.next`](https://docs.ghost.org/content-api/pagination) value, and pauses briefly between pages until the full site has been fetched.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
Add the CLI to a project:
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
```sh
|
|
12
|
+
npm install @tryghost/algolia
|
|
13
|
+
```
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
or
|
|
12
16
|
|
|
17
|
+
```sh
|
|
18
|
+
pnpm add @tryghost/algolia
|
|
19
|
+
```
|
|
13
20
|
|
|
14
21
|
## Usage
|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
Copy [`example.config.json`](example.config.json) to a local file such as `config.json`, then set the Ghost Content API and Algolia credentials. `indexSettings` contains the current defaults and can be customized or removed.
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
`indexSettings` reflects the current default settings and can either be overwritten, or removed from the config file.
|
|
25
|
+
Configuration files contain secrets and should not be committed. Files matching `packages/algolia/config*.json` are ignored by this repository.
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
After installing the package in another project, run its binary through that project's package runner:
|
|
22
28
|
|
|
23
|
-
```
|
|
24
|
-
|
|
29
|
+
```sh
|
|
30
|
+
npx algolia index config.json [options]
|
|
25
31
|
```
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
The [Fragmenter](https://github.com/TryGhost/algolia/tree/master/packages/algolia-fragmenter) breaks down large HTML pieces into smaller chunks by its headings. Sometimes the fragment is still too big and Algolia will throw an error listing the post id that caused the large fragment. The post id can be used to get the post slug, which then can be excluded from the batch run like this:
|
|
33
|
+
From this repository, invoke the entrypoint directly:
|
|
30
34
|
|
|
31
|
-
```
|
|
32
|
-
|
|
35
|
+
```sh
|
|
36
|
+
cd packages/algolia
|
|
37
|
+
node bin/cli.js index config.json [options]
|
|
33
38
|
```
|
|
34
39
|
|
|
35
|
-
###
|
|
36
|
-
|
|
37
|
-
- `pathToConfig`, needs to be the relative (from this package) path to the config JSON file that contains the Ghost and Algolia API keys and settings (see [usage](#usage) above)
|
|
40
|
+
### Options
|
|
38
41
|
|
|
39
|
-
-
|
|
42
|
+
- `pathToConfig` is the path, relative to the current directory, to the JSON configuration file.
|
|
43
|
+
- `-s, --skip` excludes a comma-separated list of post slugs from the index.
|
|
44
|
+
- `-V, --verbose` enables verbose output.
|
|
45
|
+
- `-l, --limit` makes a single request for 1 to 100 posts instead of fetching every page.
|
|
46
|
+
- `-p, --page` selects the page for that single request and requires `--limit`.
|
|
47
|
+
- `-sjs, --skipjsonslugs` currently controls only a log message about `ignore_slugs`; it does not control exclusion. Configured `ignore_slugs` are always excluded after posts are fetched.
|
|
40
48
|
|
|
41
|
-
|
|
42
|
-
- `-l, --limit`, limit the amount of posts to receive. Default is 'all'
|
|
43
|
-
- `-p --page`, define the page to fetch posts from. To be used in combination with `limit`.
|
|
44
|
-
- `-sjs --skipjsonslugs`, uses a list of slugs in `config.json` to skip before they're uploaded. This method will request all data from Ghost and skip at the point it would normally upload to Algolia. If you're getting `414 Request-URI Too Large` errors using `-s`, this is the method to use.
|
|
49
|
+
### Large fragments
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
The [Fragmenter](../algolia-fragmenter/README.md) splits large HTML strings by heading. A fragment can still exceed Algolia's record-size limit; the resulting error includes the post ID. Resolve the ID to a slug and exclude it from the batch:
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
1. `git clone` this repo & `cd` into it as usual
|
|
52
|
-
2. Run `yarn` to install top-level dependencies.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
## Run
|
|
56
|
-
|
|
57
|
-
- `yarn dev`
|
|
53
|
+
```sh
|
|
54
|
+
npx algolia index config.json --skip post-slug,another-post-slug
|
|
55
|
+
```
|
|
58
56
|
|
|
57
|
+
If a long `--skip` filter causes a `414 Request-URI Too Large` response, put the slugs in the configuration file's `ignore_slugs` array. The CLI always applies that array after fetching posts; `--skipjsonslugs` is not required and currently affects logging only.
|
|
59
58
|
|
|
60
|
-
##
|
|
59
|
+
## Development
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
- `yarn test` run lint and tests
|
|
61
|
+
Install dependencies from the repository root with `pnpm install`. From the root, run this package's tests and ESLint checks with:
|
|
64
62
|
|
|
63
|
+
```sh
|
|
64
|
+
pnpm --filter @tryghost/algolia test
|
|
65
|
+
```
|
|
65
66
|
|
|
67
|
+
Run the full monorepo suite with `pnpm test`.
|
|
66
68
|
|
|
69
|
+
---
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
## Copyright & License
|
|
69
72
|
|
|
70
|
-
Copyright (c) 2013-
|
|
73
|
+
Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). Ghost and the Ghost Logo are trademarks of Ghost Foundation Ltd. Please see our [trademark policy](https://ghost.org/trademark/) for info on acceptable usage.
|
package/bin/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ const prettyCLI = require('@tryghost/pretty-cli');
|
|
|
3
3
|
const ui = require('@tryghost/pretty-cli').ui;
|
|
4
4
|
const fs = require('fs-extra');
|
|
5
5
|
const utils = require('../lib/utils');
|
|
6
|
+
const {fetchPosts} = require('../lib/fetch-posts');
|
|
6
7
|
const GhostContentAPI = require('@tryghost/content-api');
|
|
7
8
|
const transforms = require('@tryghost/algolia-fragmenter');
|
|
8
9
|
const IndexFactory = require('@tryghost/algolia-indexer');
|
|
@@ -24,15 +25,28 @@ prettyCLI.command({
|
|
|
24
25
|
desc: 'Comma separated list of post slugs to exclude from indexing'
|
|
25
26
|
});
|
|
26
27
|
sywac.number('-l --limit', {
|
|
27
|
-
desc: '
|
|
28
|
+
desc: 'Fetch one page containing 1 to 100 posts'
|
|
28
29
|
});
|
|
29
30
|
sywac.number('-p --page', {
|
|
30
|
-
desc: '
|
|
31
|
+
desc: 'Select a page; requires --limit'
|
|
31
32
|
});
|
|
32
33
|
sywac.array('-sjs --skipjsonslugs', {
|
|
33
34
|
defaultValue: false,
|
|
34
35
|
desc: 'Exclude post slugs from config JSON file'
|
|
35
36
|
});
|
|
37
|
+
sywac.check((argv, context) => {
|
|
38
|
+
if (argv.limit !== undefined && (!Number.isInteger(argv.limit) || argv.limit < 1 || argv.limit > 100)) {
|
|
39
|
+
context.cliMessage('--limit must be an integer from 1 to 100.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (argv.page !== undefined && argv.limit === undefined) {
|
|
43
|
+
context.cliMessage('--page requires --limit.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (argv.page !== undefined && (!Number.isInteger(argv.page) || argv.page < 1)) {
|
|
47
|
+
context.cliMessage('--page must be a positive integer.');
|
|
48
|
+
}
|
|
49
|
+
});
|
|
36
50
|
},
|
|
37
51
|
run: async (argv) => {
|
|
38
52
|
const mainTimer = Date.now();
|
|
@@ -56,31 +70,31 @@ prettyCLI.command({
|
|
|
56
70
|
// 2. Fetch all posts from the Ghost instance
|
|
57
71
|
try {
|
|
58
72
|
const timer = Date.now();
|
|
59
|
-
const
|
|
73
|
+
const fetchOptions = {};
|
|
60
74
|
const ghost = new GhostContentAPI({
|
|
61
75
|
url: context.ghost.apiUrl,
|
|
62
76
|
key: context.ghost.apiKey,
|
|
63
|
-
version: '
|
|
77
|
+
version: 'v6.0'
|
|
64
78
|
});
|
|
65
79
|
|
|
66
80
|
if (argv.skip && argv.skip.length > 0) {
|
|
67
81
|
const filterSlugs = argv.skip.join(',');
|
|
68
82
|
|
|
69
|
-
|
|
83
|
+
fetchOptions.filter = `slug:-[${filterSlugs}]`;
|
|
70
84
|
}
|
|
71
85
|
|
|
72
|
-
if (argv.limit) {
|
|
73
|
-
|
|
86
|
+
if (argv.limit !== undefined) {
|
|
87
|
+
fetchOptions.limit = argv.limit;
|
|
74
88
|
}
|
|
75
89
|
|
|
76
|
-
ui.log.info(`Fetching ${
|
|
90
|
+
ui.log.info(`Fetching ${argv.limit === undefined ? 'all' : argv.limit} posts from Ghost...`);
|
|
77
91
|
|
|
78
|
-
if (argv.page) {
|
|
92
|
+
if (argv.page !== undefined) {
|
|
79
93
|
ui.log.info(`...from page #${argv.page}.`);
|
|
80
|
-
|
|
94
|
+
fetchOptions.page = argv.page;
|
|
81
95
|
}
|
|
82
96
|
|
|
83
|
-
context.posts = await ghost.posts.browse(
|
|
97
|
+
context.posts = await fetchPosts(ghost.posts.browse.bind(ghost.posts), fetchOptions);
|
|
84
98
|
|
|
85
99
|
ui.log.info(`Done fetching posts in ${Date.now() - timer}ms.`);
|
|
86
100
|
} catch (error) {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const DEFAULT_PAGE_SIZE = 100;
|
|
2
|
+
const PAGE_DELAY_MS = 100;
|
|
3
|
+
|
|
4
|
+
const waitBetweenPages = () => {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
setTimeout(resolve, PAGE_DELAY_MS);
|
|
7
|
+
});
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const getNextPage = (posts) => {
|
|
11
|
+
if (!posts.meta || !posts.meta.pagination || typeof posts.meta.pagination !== 'object') {
|
|
12
|
+
throw TypeError('Ghost returned posts without pagination metadata.');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const nextPage = posts.meta.pagination.next;
|
|
16
|
+
|
|
17
|
+
if (nextPage !== null && (!Number.isInteger(nextPage) || nextPage < 1)) {
|
|
18
|
+
throw TypeError('Ghost returned an invalid next page.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return nextPage;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const validatePosts = (posts) => {
|
|
25
|
+
if (!Array.isArray(posts)) {
|
|
26
|
+
throw TypeError('Ghost returned posts in an invalid format.');
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
module.exports.fetchPosts = async (browsePosts, options = {}) => {
|
|
31
|
+
const hasExplicitLimit = options.limit !== undefined;
|
|
32
|
+
const params = {include: 'tags,authors'};
|
|
33
|
+
|
|
34
|
+
if (hasExplicitLimit) {
|
|
35
|
+
params.limit = options.limit;
|
|
36
|
+
if (options.page !== undefined) {
|
|
37
|
+
params.page = options.page;
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
params.limit = DEFAULT_PAGE_SIZE;
|
|
41
|
+
params.page = 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (options.filter !== undefined) {
|
|
45
|
+
params.filter = options.filter;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const firstPage = await browsePosts(params);
|
|
49
|
+
validatePosts(firstPage);
|
|
50
|
+
const posts = [...firstPage];
|
|
51
|
+
|
|
52
|
+
if (hasExplicitLimit) {
|
|
53
|
+
return posts;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let nextPage = getNextPage(firstPage);
|
|
57
|
+
const fetchedPages = new Set([1]);
|
|
58
|
+
|
|
59
|
+
while (nextPage !== null) {
|
|
60
|
+
if (fetchedPages.has(nextPage)) {
|
|
61
|
+
throw TypeError('Ghost returned a repeated next page.');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fetchedPages.add(nextPage);
|
|
65
|
+
await waitBetweenPages();
|
|
66
|
+
const page = await browsePosts({...params, page: nextPage});
|
|
67
|
+
validatePosts(page);
|
|
68
|
+
posts.push(...page);
|
|
69
|
+
nextPage = getNextPage(page);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return posts;
|
|
73
|
+
};
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tryghost/algolia",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"repository":
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/TryGhost/algolia.git",
|
|
7
|
+
"directory": "packages/algolia"
|
|
8
|
+
},
|
|
5
9
|
"author": "Ghost Foundation",
|
|
6
10
|
"license": "MIT",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=24"
|
|
13
|
+
},
|
|
7
14
|
"bin": {
|
|
8
15
|
"algolia": "./bin/cli.js"
|
|
9
16
|
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"dev": "algolia",
|
|
12
|
-
"algolia": "algolia",
|
|
13
|
-
"test": "NODE_ENV=testing mocha './test/**/*.test.js'",
|
|
14
|
-
"lint": "eslint . --ext .js --cache",
|
|
15
|
-
"posttest": "yarn lint"
|
|
16
|
-
},
|
|
17
17
|
"files": [
|
|
18
18
|
"bin",
|
|
19
19
|
"index.js",
|
|
@@ -22,18 +22,19 @@
|
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
25
|
-
"devDependencies": {
|
|
26
|
-
"mocha": "10.2.0",
|
|
27
|
-
"should": "13.2.3",
|
|
28
|
-
"sinon": "17.0.0"
|
|
29
|
-
},
|
|
30
25
|
"dependencies": {
|
|
31
|
-
"@tryghost/
|
|
32
|
-
"@tryghost/
|
|
33
|
-
"@tryghost/
|
|
34
|
-
"
|
|
35
|
-
"@tryghost/
|
|
36
|
-
"
|
|
26
|
+
"@tryghost/content-api": "1.12.10",
|
|
27
|
+
"@tryghost/errors": "1.3.13",
|
|
28
|
+
"@tryghost/pretty-cli": "1.2.52",
|
|
29
|
+
"fs-extra": "11.4.0",
|
|
30
|
+
"@tryghost/algolia-fragmenter": "^0.2.8",
|
|
31
|
+
"@tryghost/algolia-indexer": "^0.3.2"
|
|
37
32
|
},
|
|
38
|
-
"
|
|
39
|
-
|
|
33
|
+
"scripts": {
|
|
34
|
+
"dev": "algolia",
|
|
35
|
+
"algolia": "algolia",
|
|
36
|
+
"test": "NODE_ENV=testing vitest run --root .",
|
|
37
|
+
"lint": "eslint . --ext .js --cache",
|
|
38
|
+
"posttest": "pnpm lint"
|
|
39
|
+
}
|
|
40
|
+
}
|