@rr0/cms 0.3.65 → 0.3.67
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/dist/CMSGenerator.d.ts +6 -0
- package/dist/CMSGenerator.js +21 -1
- package/dist/DataContentVisitor.d.ts +9 -0
- package/dist/DataContentVisitor.js +28 -0
- package/dist/DataContentVisitor.test.js +26 -0
- package/dist/search/SearchComponent.mjs +23 -13
- package/dist/search/SearchIndexStep.d.ts +5 -1
- package/dist/search/SearchIndexStep.js +8 -3
- package/dist/search/SearchVisitor.d.ts +5 -1
- package/dist/search/SearchVisitor.js +7 -2
- package/package.json +1 -1
package/dist/CMSGenerator.d.ts
CHANGED
|
@@ -130,6 +130,12 @@ export declare class CMSGenerator implements CMSContext {
|
|
|
130
130
|
timeElementFactory: TimeElementFactory;
|
|
131
131
|
timeReplacer: TimeReplacer;
|
|
132
132
|
};
|
|
133
|
+
/**
|
|
134
|
+
* The other names each people's page can be searched by — their pseudonyms and their surnames, the latter said as
|
|
135
|
+
* "<surname> <lastName>" so that a surname alone doesn't leave the reader guessing whose it is. Keyed by page URL,
|
|
136
|
+
* the people's own page only: their other pages (a CV, say) are not that person's name.
|
|
137
|
+
*/
|
|
138
|
+
protected peopleNames(peopleService: PeopleService): Promise<Map<string, string[]>>;
|
|
133
139
|
protected setupPeople(context: RR0ContextImpl, peopleRenderer: PeopleHtmlRenderer, copies: string[]): Promise<{
|
|
134
140
|
peopleService: PeopleService;
|
|
135
141
|
peopleSteps: import("./index.js").PeopleDirectoryStep[];
|
package/dist/CMSGenerator.js
CHANGED
|
@@ -243,7 +243,8 @@ export class CMSGenerator {
|
|
|
243
243
|
}
|
|
244
244
|
const reindex = args.reindex;
|
|
245
245
|
if (reindex === null || reindex === void 0 ? void 0 : reindex.includes("search")) {
|
|
246
|
-
|
|
246
|
+
const peopleNames = await this.peopleNames(peopleService);
|
|
247
|
+
ssg.add(new SearchIndexStep("search/index.json", searchVisitor, outDir, (context, fileName) => timeService.setContextFromFile(context, fileName), url => peopleNames.get(url) || []));
|
|
247
248
|
}
|
|
248
249
|
if (reindex === null || reindex === void 0 ? void 0 : reindex.includes("sources")) {
|
|
249
250
|
ssg.add(new SourceIndexStep(this.options.sourceRegistryFileName, sourceFactory));
|
|
@@ -288,6 +289,25 @@ export class CMSGenerator {
|
|
|
288
289
|
const timeReplacer = new TimeReplacer(timeElementFactory);
|
|
289
290
|
return { timeFiles, timeElementFactory, timeReplacer };
|
|
290
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* The other names each people's page can be searched by — their pseudonyms and their surnames, the latter said as
|
|
294
|
+
* "<surname> <lastName>" so that a surname alone doesn't leave the reader guessing whose it is. Keyed by page URL,
|
|
295
|
+
* the people's own page only: their other pages (a CV, say) are not that person's name.
|
|
296
|
+
*/
|
|
297
|
+
async peopleNames(peopleService) {
|
|
298
|
+
const namesByUrl = new Map();
|
|
299
|
+
for (const people of await peopleService.getAll()) {
|
|
300
|
+
const lastName = people.lastName;
|
|
301
|
+
const names = [
|
|
302
|
+
...people.pseudonyms,
|
|
303
|
+
...(lastName ? people.surnames.map(surname => `${surname} ${lastName}`) : [])
|
|
304
|
+
];
|
|
305
|
+
if (names.length > 0 && people.dirName) {
|
|
306
|
+
namesByUrl.set(`${people.dirName}/index.html`, names);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return namesByUrl;
|
|
310
|
+
}
|
|
291
311
|
async setupPeople(context, peopleRenderer, copies) {
|
|
292
312
|
const peopleFiles = await this.peopleFactory.getFiles();
|
|
293
313
|
const peopleService = new PeopleService(this.dataService, this.peopleFactory, { files: peopleFiles, rootDir: "people" });
|
|
@@ -14,6 +14,15 @@ export declare class DataContentVisitor implements ContentVisitor {
|
|
|
14
14
|
visit(context: HtmlRR0Context): Promise<void>;
|
|
15
15
|
protected process(context: HtmlRR0Context, data: RR0Data): Promise<void>;
|
|
16
16
|
protected processEvent(context: HtmlRR0Context, event: RR0Event, data: RR0Data): Promise<void>;
|
|
17
|
+
/** Where the UFO@home player that replays a case's sightings is served from. */
|
|
18
|
+
static readonly SIGHTING_PLAYER_SCRIPT = "https://ufoathome.org/lib/rr0-sighting.mjs";
|
|
19
|
+
/**
|
|
20
|
+
* A sighting event of a case points at a witness's recording (its `url`, a UFO@home
|
|
21
|
+
* `sighting.json`): the page shows them in the UFO@home player, reading the case.json itself so
|
|
22
|
+
* that every sighting of the case is offered, not only this one. Inserted at the end of the page's
|
|
23
|
+
* contents, once, and only if the page does not already embed a player where its author put one.
|
|
24
|
+
*/
|
|
25
|
+
protected processSighting(context: HtmlRR0Context, data: RR0Data): void;
|
|
17
26
|
protected timeParagraph(context: HtmlRR0Context, event: RR0Event): {
|
|
18
27
|
eventP: HTMLParagraphElement;
|
|
19
28
|
timeEl: HTMLElement;
|
|
@@ -46,12 +46,38 @@ export class DataContentVisitor {
|
|
|
46
46
|
case "death":
|
|
47
47
|
await this.processDeath(context, event, data);
|
|
48
48
|
break;
|
|
49
|
+
case "sighting":
|
|
50
|
+
this.processSighting(context, data);
|
|
51
|
+
break;
|
|
49
52
|
default:
|
|
50
53
|
const { eventP, timeEl } = this.timeParagraph(context, event);
|
|
51
54
|
await this.eventRenderer.render(context, event, eventP);
|
|
52
55
|
context.file.document.append(eventP);
|
|
53
56
|
}
|
|
54
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* A sighting event of a case points at a witness's recording (its `url`, a UFO@home
|
|
60
|
+
* `sighting.json`): the page shows them in the UFO@home player, reading the case.json itself so
|
|
61
|
+
* that every sighting of the case is offered, not only this one. Inserted at the end of the page's
|
|
62
|
+
* contents, once, and only if the page does not already embed a player where its author put one.
|
|
63
|
+
*/
|
|
64
|
+
processSighting(context, data) {
|
|
65
|
+
const doc = context.file.document;
|
|
66
|
+
if (data.type !== "case" || doc.querySelector("rr0-sighting")) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const parentEl = doc.querySelector(".contents") || doc.body;
|
|
70
|
+
if (!parentEl) {
|
|
71
|
+
context.warn("no .contents to insert the sighting player in", context.file.name);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const scriptEl = doc.createElement("script");
|
|
75
|
+
scriptEl.type = "module";
|
|
76
|
+
scriptEl.src = DataContentVisitor.SIGHTING_PLAYER_SCRIPT;
|
|
77
|
+
const playerEl = doc.createElement("rr0-sighting");
|
|
78
|
+
playerEl.setAttribute("src", "case.json");
|
|
79
|
+
parentEl.append(scriptEl, playerEl);
|
|
80
|
+
}
|
|
55
81
|
timeParagraph(context, event) {
|
|
56
82
|
const container = context.file.document.createElement("p");
|
|
57
83
|
const eventContext = context.clone();
|
|
@@ -155,3 +181,5 @@ export class DataContentVisitor {
|
|
|
155
181
|
}
|
|
156
182
|
}
|
|
157
183
|
}
|
|
184
|
+
/** Where the UFO@home player that replays a case's sightings is served from. */
|
|
185
|
+
DataContentVisitor.SIGHTING_PLAYER_SCRIPT = "https://ufoathome.org/lib/rr0-sighting.mjs";
|
|
@@ -8,6 +8,9 @@ class TestDataContentVisitor extends DataContentVisitor {
|
|
|
8
8
|
renderImage(context, event) {
|
|
9
9
|
return this.processImage(context, event);
|
|
10
10
|
}
|
|
11
|
+
renderEvent(context, event, data) {
|
|
12
|
+
return this.processEvent(context, event, data);
|
|
13
|
+
}
|
|
11
14
|
}
|
|
12
15
|
describe("DataContentVisitor", () => {
|
|
13
16
|
const portraitEvent = {
|
|
@@ -31,4 +34,27 @@ describe("DataContentVisitor", () => {
|
|
|
31
34
|
await visitor.renderImage(context, portraitEvent);
|
|
32
35
|
expect(context.file.document.querySelectorAll(".contents img[src=\"portrait.jpg\"]").length).toBe(1);
|
|
33
36
|
});
|
|
37
|
+
const sightingEvent = { type: "event", eventType: "sighting", url: "sighting.json" };
|
|
38
|
+
const aCase = { type: "case", events: [sightingEvent] };
|
|
39
|
+
test("inserts the UFO@home player on the case, for a case with a sighting and no player", async () => {
|
|
40
|
+
const context = cmsTestUtil.newHtmlContext("people/v/VertongenJeanLuc/index.html", "<div class=\"contents\"><p>Le dossier</p></div>");
|
|
41
|
+
const visitor = new TestDataContentVisitor();
|
|
42
|
+
await visitor.renderEvent(context, sightingEvent, aCase);
|
|
43
|
+
await visitor.renderEvent(context, sightingEvent, aCase);
|
|
44
|
+
const players = context.file.document.querySelectorAll(".contents > rr0-sighting");
|
|
45
|
+
expect(players.length).toBe(1);
|
|
46
|
+
expect(players[0].getAttribute("src")).toBe("case.json");
|
|
47
|
+
const script = context.file.document.querySelector(".contents > script");
|
|
48
|
+
expect(script === null || script === void 0 ? void 0 : script.getAttribute("src")).toBe(DataContentVisitor.SIGHTING_PLAYER_SCRIPT);
|
|
49
|
+
expect(script === null || script === void 0 ? void 0 : script.getAttribute("type")).toBe("module");
|
|
50
|
+
// Nothing else: no dated paragraph per sighting, which a sighting without a time used to fail the build on.
|
|
51
|
+
expect(context.file.document.querySelectorAll(".contents > p").length).toBe(1);
|
|
52
|
+
});
|
|
53
|
+
test("leaves a player the page's author placed alone", async () => {
|
|
54
|
+
const context = cmsTestUtil.newHtmlContext("people/v/VertongenJeanLuc/index.html", "<div class=\"contents\"><p>Le dossier</p><rr0-sighting src=\"case.json\" show-witness-map></rr0-sighting></div>");
|
|
55
|
+
const visitor = new TestDataContentVisitor();
|
|
56
|
+
await visitor.renderEvent(context, sightingEvent, aCase);
|
|
57
|
+
expect(context.file.document.querySelectorAll("rr0-sighting").length).toBe(1);
|
|
58
|
+
expect(context.file.document.querySelectorAll("script").length).toBe(0);
|
|
59
|
+
});
|
|
34
60
|
});
|
|
@@ -9,7 +9,7 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
|
|
|
9
9
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
10
10
|
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
11
11
|
};
|
|
12
|
-
var _SearchComponent_instances, _SearchComponent_siteIndex, _SearchComponent_loading, _SearchComponent_maxResultCount, _SearchComponent_siteSearchChange, _SearchComponent_siteSearchLoad, _SearchComponent_setDataList;
|
|
12
|
+
var _SearchComponent_instances, _SearchComponent_siteIndex, _SearchComponent_entries, _SearchComponent_loading, _SearchComponent_maxResultCount, _SearchComponent_siteSearchChange, _SearchComponent_siteSearchLoad, _SearchComponent_setDataList;
|
|
13
13
|
const template = document.createElement("template");
|
|
14
14
|
const html = `
|
|
15
15
|
<input id="search-input" list="values" type="search" part="input">
|
|
@@ -32,6 +32,13 @@ export class SearchComponent extends HTMLElement {
|
|
|
32
32
|
* @type SearchIndex
|
|
33
33
|
*/
|
|
34
34
|
_SearchComponent_siteIndex.set(this, void 0);
|
|
35
|
+
/**
|
|
36
|
+
* One searchable entry per name: a page's title, plus each of the other names it is known by. A datalist filters on
|
|
37
|
+
* its options' own text, so a surname or a pseudonym is only reachable when it IS an option.
|
|
38
|
+
*
|
|
39
|
+
* @type {{label: string, url: string}[]}
|
|
40
|
+
*/
|
|
41
|
+
_SearchComponent_entries.set(this, []);
|
|
35
42
|
_SearchComponent_loading.set(this, false
|
|
36
43
|
/**
|
|
37
44
|
* @readonly
|
|
@@ -69,19 +76,17 @@ export class SearchComponent extends HTMLElement {
|
|
|
69
76
|
this.placeholder = "Recherche";
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
|
-
_SearchComponent_siteIndex = new WeakMap(), _SearchComponent_loading = new WeakMap(), _SearchComponent_maxResultCount = new WeakMap(), _SearchComponent_instances = new WeakSet(), _SearchComponent_siteSearchChange = function _SearchComponent_siteSearchChange(e) {
|
|
73
|
-
var _a;
|
|
79
|
+
_SearchComponent_siteIndex = new WeakMap(), _SearchComponent_entries = new WeakMap(), _SearchComponent_loading = new WeakMap(), _SearchComponent_maxResultCount = new WeakMap(), _SearchComponent_instances = new WeakSet(), _SearchComponent_siteSearchChange = function _SearchComponent_siteSearchChange(e) {
|
|
74
80
|
const value = e.target.value.trim();
|
|
75
|
-
const
|
|
81
|
+
const entries = __classPrivateFieldGet(this, _SearchComponent_entries, "f");
|
|
76
82
|
if (e.inputType === "insertReplacementText" || e.inputType == null) {
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
window.location.href = "/" + page.url;
|
|
83
|
+
const entry = entries.find(entry => entry.label === value);
|
|
84
|
+
if (entry) {
|
|
85
|
+
window.location.href = "/" + entry.url;
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
const lowValue = value.toLowerCase();
|
|
84
|
-
const dataList =
|
|
89
|
+
const dataList = entries.filter(entry => entry.label.toLowerCase().indexOf(lowValue) >= 0);
|
|
85
90
|
__classPrivateFieldGet(this, _SearchComponent_instances, "m", _SearchComponent_setDataList).call(this, dataList.length <= __classPrivateFieldGet(this, _SearchComponent_maxResultCount, "f") ? dataList : []);
|
|
86
91
|
}, _SearchComponent_siteSearchLoad = function _SearchComponent_siteSearchLoad() {
|
|
87
92
|
if (!__classPrivateFieldGet(this, _SearchComponent_siteIndex, "f") && !__classPrivateFieldGet(this, _SearchComponent_loading, "f")) {
|
|
@@ -89,22 +94,27 @@ _SearchComponent_siteIndex = new WeakMap(), _SearchComponent_loading = new WeakM
|
|
|
89
94
|
fetch("/search/index.json").then(async (response) => {
|
|
90
95
|
if (response.ok) {
|
|
91
96
|
__classPrivateFieldSet(this, _SearchComponent_siteIndex, await response.json(), "f");
|
|
97
|
+
const entries = [];
|
|
92
98
|
for (const page of __classPrivateFieldGet(this, _SearchComponent_siteIndex, "f").pages) {
|
|
93
99
|
const timeStr = page.time;
|
|
94
100
|
const title = page.title;
|
|
95
101
|
page.title += (timeStr && timeStr !== title.toLowerCase() ? ` (${timeStr})` : "");
|
|
102
|
+
entries.push({ label: page.title, url: page.url });
|
|
103
|
+
for (const name of page.names || []) {
|
|
104
|
+
entries.push({ label: name, url: page.url });
|
|
105
|
+
}
|
|
96
106
|
}
|
|
107
|
+
__classPrivateFieldSet(this, _SearchComponent_entries, entries, "f");
|
|
97
108
|
}
|
|
98
109
|
__classPrivateFieldSet(this, _SearchComponent_loading, false, "f");
|
|
99
110
|
});
|
|
100
111
|
}
|
|
101
|
-
}, _SearchComponent_setDataList = function _SearchComponent_setDataList(
|
|
112
|
+
}, _SearchComponent_setDataList = function _SearchComponent_setDataList(entries) {
|
|
102
113
|
const datalist = this.shadow.getElementById("values");
|
|
103
114
|
datalist.innerHTML = "";
|
|
104
|
-
for (let i = 0; i <
|
|
105
|
-
const page = pages[i];
|
|
115
|
+
for (let i = 0; i < entries.length; i++) {
|
|
106
116
|
const option = document.createElement("option");
|
|
107
|
-
option.value =
|
|
117
|
+
option.value = entries[i].label;
|
|
108
118
|
datalist.append(option);
|
|
109
119
|
}
|
|
110
120
|
};
|
|
@@ -9,12 +9,16 @@ export declare class SearchIndexStep implements SsgStep {
|
|
|
9
9
|
protected searchCommand: SearchVisitor;
|
|
10
10
|
protected contentRoot?: string;
|
|
11
11
|
protected prepareContext?: (context: HtmlRR0Context, fileName: string) => void;
|
|
12
|
+
protected alternateNames?: (url: string) => string[];
|
|
12
13
|
protected encoding: BufferEncoding;
|
|
13
14
|
/**
|
|
14
15
|
* @param fileName The index file path
|
|
15
16
|
* @param searchCommand The command that collected the pages info.
|
|
17
|
+
* @param contentRoot
|
|
18
|
+
* @param prepareContext
|
|
19
|
+
* @param alternateNames Other names a page can be searched by, such as a people's surnames and pseudonyms.
|
|
16
20
|
*/
|
|
17
|
-
constructor(fileName: string, searchCommand: SearchVisitor, contentRoot?: string, prepareContext?: (context: HtmlRR0Context, fileName: string) => void);
|
|
21
|
+
constructor(fileName: string, searchCommand: SearchVisitor, contentRoot?: string, prepareContext?: (context: HtmlRR0Context, fileName: string) => void, alternateNames?: (url: string) => string[]);
|
|
18
22
|
/**
|
|
19
23
|
* Write the search index file.
|
|
20
24
|
*
|
|
@@ -10,12 +10,16 @@ export class SearchIndexStep {
|
|
|
10
10
|
/**
|
|
11
11
|
* @param fileName The index file path
|
|
12
12
|
* @param searchCommand The command that collected the pages info.
|
|
13
|
+
* @param contentRoot
|
|
14
|
+
* @param prepareContext
|
|
15
|
+
* @param alternateNames Other names a page can be searched by, such as a people's surnames and pseudonyms.
|
|
13
16
|
*/
|
|
14
|
-
constructor(fileName, searchCommand, contentRoot, prepareContext) {
|
|
17
|
+
constructor(fileName, searchCommand, contentRoot, prepareContext, alternateNames) {
|
|
15
18
|
this.fileName = fileName;
|
|
16
19
|
this.searchCommand = searchCommand;
|
|
17
20
|
this.contentRoot = contentRoot;
|
|
18
21
|
this.prepareContext = prepareContext;
|
|
22
|
+
this.alternateNames = alternateNames;
|
|
19
23
|
this.encoding = "utf8";
|
|
20
24
|
}
|
|
21
25
|
/**
|
|
@@ -24,7 +28,7 @@ export class SearchIndexStep {
|
|
|
24
28
|
* @param context
|
|
25
29
|
*/
|
|
26
30
|
async execute(context) {
|
|
27
|
-
var _a;
|
|
31
|
+
var _a, _b;
|
|
28
32
|
const newIndex = this.searchCommand.index;
|
|
29
33
|
let existingIndex;
|
|
30
34
|
if (this.contentRoot) {
|
|
@@ -45,7 +49,8 @@ export class SearchIndexStep {
|
|
|
45
49
|
const titleMatch = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
|
|
46
50
|
const outputTitle = titleMatch ? this.decodeTitle(titleMatch[1]) : "";
|
|
47
51
|
const title = this.readSourceTitle(sourceFile) || outputTitle;
|
|
48
|
-
const
|
|
52
|
+
const pageUrl = url.split(path.sep).join("/");
|
|
53
|
+
const pageInfo = this.searchCommand.pageInfoFrom(title, pageUrl, pageContext, (_b = this.alternateNames) === null || _b === void 0 ? void 0 : _b.call(this, pageUrl));
|
|
49
54
|
if (pageInfo) {
|
|
50
55
|
const resultTitle = SearchVisitor.resultTitle(pageInfo);
|
|
51
56
|
const titleIndexed = pagesByResultTitle.get(resultTitle);
|
|
@@ -7,6 +7,10 @@ export type PageInfo = {
|
|
|
7
7
|
title: string;
|
|
8
8
|
url: string;
|
|
9
9
|
time: string;
|
|
10
|
+
/**
|
|
11
|
+
* Other names the page can be searched by, such as a people's surnames and pseudonyms.
|
|
12
|
+
*/
|
|
13
|
+
names?: string[];
|
|
10
14
|
html?: string;
|
|
11
15
|
};
|
|
12
16
|
type WordCount = {
|
|
@@ -37,7 +41,7 @@ export declare class SearchVisitor implements FileVisitor {
|
|
|
37
41
|
contentStepEnd(): Promise<void>;
|
|
38
42
|
visit(context: HtmlRR0Context): Promise<void>;
|
|
39
43
|
pageInfo(context: HtmlRR0Context, contentRoot?: string): PageInfo | undefined;
|
|
40
|
-
pageInfoFrom(title: string, url: string, context: HtmlRR0Context): PageInfo | undefined;
|
|
44
|
+
pageInfoFrom(title: string, url: string, context: HtmlRR0Context, names?: string[]): PageInfo | undefined;
|
|
41
45
|
protected handleAlreadyIndexed(resultTitle: string, url: string, titleIndexed: PageInfo): void;
|
|
42
46
|
protected getContents(doc: Document): string;
|
|
43
47
|
protected indexContent(context: HtmlRR0Context, outputFile: HtmlFileContents): void;
|
|
@@ -59,12 +59,17 @@ export class SearchVisitor {
|
|
|
59
59
|
: file.name.startsWith("out/") ? file.name.substring("out/".length) : file.name;
|
|
60
60
|
return this.pageInfoFrom(file.title, url, context);
|
|
61
61
|
}
|
|
62
|
-
pageInfoFrom(title, url, context) {
|
|
62
|
+
pageInfoFrom(title, url, context, names = []) {
|
|
63
63
|
if (!title || this.config.notIndexedUrls.includes(url)) {
|
|
64
64
|
return undefined;
|
|
65
65
|
}
|
|
66
66
|
const time = this.timeTextBuilder.build(context, { year: "numeric", month: "short", day: "numeric" }).toLowerCase();
|
|
67
|
-
|
|
67
|
+
const pageInfo = { title, url, time };
|
|
68
|
+
const otherNames = Array.from(new Set(names.filter(name => name && name !== title)));
|
|
69
|
+
if (otherNames.length > 0) {
|
|
70
|
+
pageInfo.names = otherNames;
|
|
71
|
+
}
|
|
72
|
+
return pageInfo;
|
|
68
73
|
}
|
|
69
74
|
handleAlreadyIndexed(resultTitle, url, titleIndexed) {
|
|
70
75
|
throw new Error(`Search result "${resultTitle}" with URL ${url} is already indexed with URL ${titleIndexed.url}`);
|
package/package.json
CHANGED