@tryghost/algolia 0.2.10 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2013-2023 Ghost Foundation
3
+ Copyright (c) 2013-2025 Ghost Foundation
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,70 +1,73 @@
1
1
  # Algolia Ghost CLI
2
2
 
3
- CLI tool to initially index the full Ghost post content into an Algolia index.
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
- `npm install @tryghost/algolia --save`
9
+ Add the CLI to a project:
8
10
 
9
- or
11
+ ```sh
12
+ npm install @tryghost/algolia
13
+ ```
10
14
 
11
- `yarn add @tryghost/algolia`
15
+ or
12
16
 
17
+ ```sh
18
+ pnpm add @tryghost/algolia
19
+ ```
13
20
 
14
21
  ## Usage
15
22
 
16
- To use the CLI, install the dependencies with `yarn install` or `npm install`.
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
- Copy the existing `example.config.json` to e. g. `config.json` and replace the relevant values for Ghost and Algolia.
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
- To run the batch index, run
27
+ After installing the package in another project, run its binary through that project's package runner:
22
28
 
23
- ```bash
24
- yarn algolia index <pathToConfig> [options]
29
+ ```sh
30
+ npx algolia index config.json [options]
25
31
  ```
26
32
 
27
- ### Caveats
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
- ```bash
32
- yarn algolia index <pathToConfig> -s post-slug-to-exclude,and-another-post-slug-to-exclude
35
+ ```sh
36
+ cd packages/algolia
37
+ node bin/cli.js index config.json [options]
33
38
  ```
34
39
 
35
- ### Flags
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
- - `-s, --skip`, takes a comma separated list of post slugs that need to be **excluded** from the index (see [caveats](#caveats) above)
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
- - `-V, --verbose`, switches on verbose mode, but there's not much too see here (yet)
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
- ## Develop
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
- This is a mono repository, managed with [lerna](https://lernajs.io/).
49
-
50
- Follow the instructions for the top-level repo.
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
- ## Test
59
+ ## Development
61
60
 
62
- - `yarn lint` run just eslint
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
- # Copyright & License
71
+ ## Copyright & License
69
72
 
70
- Copyright (c) 2013-2023 Ghost Foundation - Released under the [MIT license](LICENSE).
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: 'Amount of posts we want to fetch from Ghost'
28
+ desc: 'Fetch one page containing 1 to 100 posts'
28
29
  });
29
30
  sywac.number('-p --page', {
30
- desc: 'Use page to navigate through posts when setting a limit'
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 params = {limit: 'all', include: 'tags,authors'};
73
+ const fetchOptions = {};
60
74
  const ghost = new GhostContentAPI({
61
75
  url: context.ghost.apiUrl,
62
76
  key: context.ghost.apiKey,
63
- version: 'canary'
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
- params.filter = `slug:-[${filterSlugs}]`;
83
+ fetchOptions.filter = `slug:-[${filterSlugs}]`;
70
84
  }
71
85
 
72
- if (argv.limit) {
73
- params.limit = argv.limit;
86
+ if (argv.limit !== undefined) {
87
+ fetchOptions.limit = argv.limit;
74
88
  }
75
89
 
76
- ui.log.info(`Fetching ${params.limit} posts from Ghost...`);
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
- params.page = argv.page;
94
+ fetchOptions.page = argv.page;
81
95
  }
82
96
 
83
- context.posts = await ghost.posts.browse(params);
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.2.10",
4
- "repository": "https://github.com/TryGhost/algolia/tree/master/packages/algolia",
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": "15.2.0"
29
- },
30
25
  "dependencies": {
31
- "@tryghost/algolia-fragmenter": "^0.2.6",
32
- "@tryghost/algolia-indexer": "^0.3.0",
33
- "@tryghost/content-api": "1.11.16",
34
- "@tryghost/errors": "1.2.26",
35
- "@tryghost/pretty-cli": "1.2.38",
36
- "fs-extra": "11.1.1"
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
- "gitHead": "0f0d8a416adb0f78fda0ec72f872146406d76eae"
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
+ }