fumadocs-core 13.4.9 → 14.0.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.
Files changed (36) hide show
  1. package/dist/{search-algolia/client.js → algolia-NTWLS6J3.js} +10 -27
  2. package/dist/breadcrumb.d.ts +15 -7
  3. package/dist/breadcrumb.js +47 -25
  4. package/dist/chunk-2V6SCS43.js +12 -0
  5. package/dist/chunk-2ZSMGYVH.js +78 -0
  6. package/dist/{chunk-UQV4A7HQ.js → chunk-4MNUWZIW.js} +9 -7
  7. package/dist/{chunk-KGMG4N3Y.js → chunk-I5BWASD6.js} +2 -2
  8. package/dist/dynamic-link.js +2 -1
  9. package/dist/fetch-4K7QOPFM.js +17 -0
  10. package/dist/i18n/index.js +64 -3
  11. package/dist/mdx-plugins/index.d.ts +20 -5
  12. package/dist/mdx-plugins/index.js +140 -14
  13. package/dist/{page-tree-BTCDMLTU.d.ts → page-tree-r8qjoUla.d.ts} +7 -4
  14. package/dist/{search-algolia/server.d.ts → search/algolia.d.ts} +1 -1
  15. package/dist/{search-algolia/server.js → search/algolia.js} +1 -1
  16. package/dist/search/client.d.ts +37 -7
  17. package/dist/search/client.js +74 -22
  18. package/dist/search/server.d.ts +65 -32
  19. package/dist/search/server.js +272 -234
  20. package/dist/server/index.d.ts +56 -4
  21. package/dist/server/index.js +59 -1
  22. package/dist/sidebar.js +7 -5
  23. package/dist/source/index.d.ts +20 -29
  24. package/dist/source/index.js +89 -81
  25. package/dist/static-5GPJ7RUY.js +60 -0
  26. package/dist/toc.js +1 -1
  27. package/dist/{search/shared.d.ts → types-Ch8gnVgO.d.ts} +1 -1
  28. package/dist/utils/use-on-change.d.ts +6 -1
  29. package/dist/utils/use-on-change.js +1 -1
  30. package/package.json +27 -86
  31. package/dist/chunk-MXOJWF66.js +0 -67
  32. package/dist/chunk-NREWOIVI.js +0 -19
  33. package/dist/middleware.d.ts +0 -3
  34. package/dist/middleware.js +0 -7
  35. package/dist/search/shared.js +0 -0
  36. package/dist/search-algolia/client.d.ts +0 -37
@@ -1,13 +1,23 @@
1
+ import {
2
+ searchAdvanced,
3
+ searchSimple
4
+ } from "../chunk-2ZSMGYVH.js";
5
+ import "../chunk-2V6SCS43.js";
1
6
  import "../chunk-MLKGABMK.js";
2
7
 
3
8
  // src/search/server.ts
4
- import { Document } from "flexsearch";
9
+ import {
10
+ save
11
+ } from "@orama/orama";
5
12
 
6
13
  // src/search/create-endpoint.ts
7
14
  function createEndpoint(server) {
8
15
  const { search } = server;
9
16
  return {
10
- search,
17
+ ...server,
18
+ async staticGET() {
19
+ return Response.json(await server.export());
20
+ },
11
21
  async GET(request) {
12
22
  const query = request.nextUrl.searchParams.get("query");
13
23
  if (!query) return Response.json([]);
@@ -21,70 +31,257 @@ function createEndpoint(server) {
21
31
  };
22
32
  }
23
33
 
24
- // src/search/i18n-api.ts
25
- function createI18nSearchAPI(type, options) {
26
- const map = /* @__PURE__ */ new Map();
27
- async function init() {
28
- if (options.i18n.languages.length === 0) {
29
- return;
34
+ // src/search/create-db.ts
35
+ import {
36
+ create,
37
+ insertMultiple
38
+ } from "@orama/orama";
39
+ var advancedSchema = {
40
+ content: "string",
41
+ page_id: "string",
42
+ type: "string",
43
+ keywords: "string",
44
+ tag: "string",
45
+ url: "string"
46
+ };
47
+ async function createDB({
48
+ indexes,
49
+ tokenizer,
50
+ search: _,
51
+ ...rest
52
+ }) {
53
+ const items = typeof indexes === "function" ? await indexes() : indexes;
54
+ const db = create({
55
+ ...rest,
56
+ schema: advancedSchema,
57
+ components: {
58
+ tokenizer
30
59
  }
31
- const indexes = typeof options.indexes === "function" ? await options.indexes() : options.indexes;
32
- for (const locale of options.i18n.languages) {
33
- const localeIndexes = indexes.filter((index) => index.locale === locale);
34
- if (type === "simple") {
35
- map.set(
36
- locale,
37
- initSimpleSearch({
38
- ...options,
39
- language: locale,
40
- indexes: localeIndexes
41
- })
42
- );
43
- continue;
44
- }
45
- map.set(
46
- locale,
47
- initAdvancedSearch({
48
- ...options,
49
- language: locale,
50
- indexes: localeIndexes
51
- })
52
- );
60
+ });
61
+ const mapTo = [];
62
+ items.forEach((page) => {
63
+ const data = page.structuredData;
64
+ let id = 0;
65
+ mapTo.push({
66
+ id: page.id,
67
+ page_id: page.id,
68
+ type: "page",
69
+ content: page.title,
70
+ keywords: page.keywords,
71
+ tag: page.tag,
72
+ url: page.url
73
+ });
74
+ if (page.description) {
75
+ mapTo.push({
76
+ id: `${page.id}-${(id++).toString()}`,
77
+ page_id: page.id,
78
+ tag: page.tag,
79
+ type: "text",
80
+ url: page.url,
81
+ content: page.description
82
+ });
53
83
  }
54
- }
55
- return createEndpoint({
56
- search: async (query, searchOptions) => {
57
- if (map.size === 0) await init();
58
- const handler = map.get(
59
- searchOptions?.locale ?? options.i18n.defaultLanguage
60
- );
61
- if (handler) return handler.search(query, searchOptions);
62
- return [];
84
+ for (const heading of data.headings) {
85
+ mapTo.push({
86
+ id: `${page.id}-${(id++).toString()}`,
87
+ page_id: page.id,
88
+ type: "heading",
89
+ tag: page.tag,
90
+ url: `${page.url}#${heading.id}`,
91
+ content: heading.content
92
+ });
93
+ }
94
+ for (const content of data.contents) {
95
+ mapTo.push({
96
+ id: `${page.id}-${(id++).toString()}`,
97
+ page_id: page.id,
98
+ tag: page.tag,
99
+ type: "text",
100
+ url: content.heading ? `${page.url}#${content.heading}` : page.url,
101
+ content: content.content
102
+ });
103
+ }
104
+ });
105
+ await insertMultiple(db, mapTo);
106
+ return db;
107
+ }
108
+
109
+ // src/search/create-db-simple.ts
110
+ import {
111
+ create as create2,
112
+ insertMultiple as insertMultiple2
113
+ } from "@orama/orama";
114
+ async function createDBSimple({
115
+ indexes,
116
+ language
117
+ }) {
118
+ const items = typeof indexes === "function" ? await indexes() : indexes;
119
+ const db = create2({
120
+ language,
121
+ schema: {
122
+ url: "string",
123
+ title: "string",
124
+ description: "string",
125
+ content: "string",
126
+ keywords: "string"
63
127
  }
64
128
  });
129
+ await insertMultiple2(
130
+ db,
131
+ items.map((page) => ({
132
+ title: page.title,
133
+ description: page.description,
134
+ url: page.url,
135
+ content: page.content,
136
+ keywords: page.keywords
137
+ }))
138
+ );
139
+ return db;
140
+ }
141
+
142
+ // src/search/create-from-source.ts
143
+ function defaultToIndex(page) {
144
+ if (!("structuredData" in page.data)) {
145
+ throw new Error(
146
+ "Cannot find structured data from page, please define the page to index function."
147
+ );
148
+ }
149
+ return {
150
+ title: page.data.title,
151
+ description: "description" in page.data ? page.data.description : void 0,
152
+ url: page.url,
153
+ id: page.url,
154
+ structuredData: page.data.structuredData
155
+ };
156
+ }
157
+ function createFromSource(source, pageToIndex = defaultToIndex, options = {}) {
158
+ if (source._i18n) {
159
+ return createI18nSearchAPI("advanced", {
160
+ ...options,
161
+ i18n: source._i18n,
162
+ indexes: source.getLanguages().flatMap((entry) => {
163
+ return entry.pages.map((page) => {
164
+ return {
165
+ ...pageToIndex(page),
166
+ locale: entry.language
167
+ };
168
+ });
169
+ })
170
+ });
171
+ }
172
+ return createSearchAPI("advanced", {
173
+ ...options,
174
+ indexes: source.getPages().map((page) => {
175
+ return pageToIndex(page);
176
+ })
177
+ });
65
178
  }
66
179
 
67
- // src/search/legacy-i18n-api.ts
68
- function createI18nSearchAPI2(type, options) {
180
+ // src/search/_stemmers.ts
181
+ var STEMMERS = {
182
+ arabic: "ar",
183
+ armenian: "am",
184
+ bulgarian: "bg",
185
+ danish: "dk",
186
+ dutch: "nl",
187
+ english: "en",
188
+ finnish: "fi",
189
+ french: "fr",
190
+ german: "de",
191
+ greek: "gr",
192
+ hungarian: "hu",
193
+ indian: "in",
194
+ indonesian: "id",
195
+ irish: "ie",
196
+ italian: "it",
197
+ lithuanian: "lt",
198
+ nepali: "np",
199
+ norwegian: "no",
200
+ portuguese: "pt",
201
+ romanian: "ro",
202
+ russian: "ru",
203
+ serbian: "rs",
204
+ slovenian: "ru",
205
+ spanish: "es",
206
+ swedish: "se",
207
+ tamil: "ta",
208
+ turkish: "tr",
209
+ ukrainian: "uk",
210
+ sanskrit: "sk"
211
+ };
212
+
213
+ // src/search/i18n-api.ts
214
+ function defaultLocaleMap(locale) {
215
+ const map = STEMMERS;
216
+ return Object.keys(map).find((lang) => map[lang] === locale) ?? locale;
217
+ }
218
+ async function initSimple(options) {
219
+ const map = /* @__PURE__ */ new Map();
220
+ if (options.i18n.languages.length === 0) {
221
+ return map;
222
+ }
223
+ const indexes = typeof options.indexes === "function" ? await options.indexes() : options.indexes;
224
+ for (const locale of options.i18n.languages) {
225
+ const localeIndexes = indexes.filter((index) => index.locale === locale);
226
+ const searchLocale = options.localeMap?.[locale] ?? defaultLocaleMap(locale);
227
+ map.set(
228
+ locale,
229
+ typeof searchLocale === "object" ? initSimpleSearch({
230
+ ...options,
231
+ ...searchLocale,
232
+ indexes: localeIndexes
233
+ }) : initSimpleSearch({
234
+ ...options,
235
+ language: searchLocale,
236
+ indexes: localeIndexes
237
+ })
238
+ );
239
+ }
240
+ return map;
241
+ }
242
+ async function initAdvanced(options) {
69
243
  const map = /* @__PURE__ */ new Map();
70
- for (const entry of options.indexes) {
71
- const v = Array.isArray(entry) ? { language: entry[0], indexes: entry[1] } : entry;
244
+ if (options.i18n.languages.length === 0) {
245
+ return map;
246
+ }
247
+ const indexes = typeof options.indexes === "function" ? await options.indexes() : options.indexes;
248
+ for (const locale of options.i18n.languages) {
249
+ const localeIndexes = indexes.filter((index) => index.locale === locale);
250
+ const searchLocale = options.localeMap?.[locale] ?? defaultLocaleMap(locale);
72
251
  map.set(
73
- v.language,
74
- // @ts-expect-error -- Index depends on generic types
75
- createSearchAPI(type, {
252
+ locale,
253
+ typeof searchLocale === "object" ? initAdvancedSearch({
254
+ ...options,
255
+ indexes: localeIndexes,
256
+ ...searchLocale
257
+ }) : initAdvancedSearch({
76
258
  ...options,
77
- language: v.language,
78
- indexes: v.indexes
259
+ language: searchLocale,
260
+ indexes: localeIndexes
79
261
  })
80
262
  );
81
263
  }
264
+ return map;
265
+ }
266
+ function createI18nSearchAPI(type, options) {
267
+ const get = type === "simple" ? initSimple(options) : initAdvanced(options);
82
268
  return createEndpoint({
269
+ async export() {
270
+ const map = await get;
271
+ const entries = Object.entries(map).map(async ([k, v]) => [
272
+ k,
273
+ await v.export()
274
+ ]);
275
+ return {
276
+ type: "i18n",
277
+ data: Object.fromEntries(await Promise.all(entries))
278
+ };
279
+ },
83
280
  search: async (query, searchOptions) => {
84
- if (searchOptions?.locale) {
85
- const handler = map.get(searchOptions.locale);
86
- if (handler) return handler.search(query, searchOptions);
87
- }
281
+ const map = await get;
282
+ const locale = searchOptions?.locale ?? options.i18n.defaultLanguage;
283
+ const handler = map.get(locale);
284
+ if (handler) return handler.search(query, searchOptions);
88
285
  return [];
89
286
  }
90
287
  });
@@ -97,198 +294,39 @@ function createSearchAPI(type, options) {
97
294
  }
98
295
  return createEndpoint(initAdvancedSearch(options));
99
296
  }
100
- function initSimpleSearch({
101
- indexes,
102
- language
103
- }) {
104
- const store = ["title", "url"];
105
- async function getDocument() {
106
- const items = typeof indexes === "function" ? await indexes() : indexes;
107
- const index = new Document({
108
- language,
109
- optimize: true,
110
- cache: 100,
111
- document: {
112
- id: "url",
113
- store,
114
- index: [
115
- {
116
- field: "title",
117
- tokenize: "forward",
118
- resolution: 9
119
- },
120
- {
121
- field: "description",
122
- tokenize: "strict",
123
- context: {
124
- depth: 1,
125
- resolution: 9
126
- }
127
- },
128
- {
129
- field: "content",
130
- tokenize: "strict",
131
- context: {
132
- depth: 1,
133
- resolution: 9
134
- }
135
- },
136
- {
137
- field: "keywords",
138
- tokenize: "strict",
139
- resolution: 9
140
- }
141
- ]
142
- }
143
- });
144
- for (const page of items) {
145
- index.add({
146
- title: page.title,
147
- description: page.description,
148
- url: page.url,
149
- content: page.content,
150
- keywords: page.keywords
151
- });
152
- }
153
- return index;
154
- }
155
- const doc = getDocument();
297
+ function initSimpleSearch(options) {
298
+ const doc = createDBSimple(options);
156
299
  return {
300
+ async export() {
301
+ return {
302
+ type: "simple",
303
+ ...save(await doc)
304
+ };
305
+ },
157
306
  search: async (query) => {
158
- const results = (await doc).search(query, 5, {
159
- enrich: true,
160
- suggest: true
161
- });
162
- if (results.length === 0) return [];
163
- return results[0].result.map((page) => ({
164
- type: "page",
165
- content: page.doc.title,
166
- id: page.doc.url,
167
- url: page.doc.url
168
- }));
307
+ const db = await doc;
308
+ return searchSimple(db, query, options.search);
169
309
  }
170
310
  };
171
311
  }
172
- function initAdvancedSearch({
173
- indexes,
174
- language,
175
- tag = false
176
- }) {
177
- const store = ["id", "url", "content", "page_id", "type", "keywords"];
178
- async function getDocument() {
179
- const items = typeof indexes === "function" ? await indexes() : indexes;
180
- const index = new Document({
181
- language,
182
- cache: 100,
183
- optimize: true,
184
- document: {
185
- id: "id",
186
- tag: tag ? "tag" : void 0,
187
- store,
188
- index: [
189
- {
190
- field: "content",
191
- tokenize: "forward",
192
- context: { depth: 2, bidirectional: true, resolution: 9 }
193
- },
194
- {
195
- field: "keywords",
196
- tokenize: "strict",
197
- resolution: 9
198
- }
199
- ]
200
- }
201
- });
202
- for (const page of items) {
203
- const data = page.structuredData;
204
- let id = 0;
205
- index.add({
206
- id: page.id,
207
- page_id: page.id,
208
- type: "page",
209
- content: page.title,
210
- keywords: page.keywords,
211
- tag: page.tag,
212
- url: page.url
213
- });
214
- if (page.description) {
215
- index.add({
216
- id: page.id + (id++).toString(),
217
- page_id: page.id,
218
- tag: page.tag,
219
- type: "text",
220
- url: page.url,
221
- content: page.description
222
- });
223
- }
224
- for (const heading of data.headings) {
225
- index.add({
226
- id: page.id + (id++).toString(),
227
- page_id: page.id,
228
- type: "heading",
229
- tag: page.tag,
230
- url: `${page.url}#${heading.id}`,
231
- content: heading.content
232
- });
233
- }
234
- for (const content of data.contents) {
235
- index.add({
236
- id: page.id + (id++).toString(),
237
- page_id: page.id,
238
- tag: page.tag,
239
- type: "text",
240
- url: content.heading ? `${page.url}#${content.heading}` : page.url,
241
- content: content.content
242
- });
243
- }
244
- }
245
- return index;
246
- }
247
- const doc = getDocument();
312
+ function initAdvancedSearch(options) {
313
+ const get = createDB(options);
248
314
  return {
249
- search: async (query, options) => {
250
- const index = await doc;
251
- const results = index.search(query, 5, {
252
- enrich: true,
253
- tag: options?.tag,
254
- limit: 6
255
- });
256
- const map = /* @__PURE__ */ new Map();
257
- for (const item of results[0]?.result ?? []) {
258
- if (item.doc.type === "page") {
259
- if (!map.has(item.doc.id)) {
260
- map.set(item.doc.id, []);
261
- }
262
- continue;
263
- }
264
- const list = map.get(item.doc.page_id) ?? [];
265
- list.push({
266
- id: item.doc.id,
267
- content: item.doc.content,
268
- type: item.doc.type,
269
- url: item.doc.url
270
- });
271
- map.set(item.doc.page_id, list);
272
- }
273
- const sortedResult = [];
274
- for (const [id, items] of map.entries()) {
275
- const page = index.get(id);
276
- if (!page) continue;
277
- sortedResult.push({
278
- id: page.id,
279
- content: page.content,
280
- type: "page",
281
- url: page.url
282
- });
283
- sortedResult.push(...items);
284
- }
285
- return sortedResult;
315
+ async export() {
316
+ return {
317
+ type: "advanced",
318
+ ...save(await get)
319
+ };
320
+ },
321
+ search: async (query, searchOptions) => {
322
+ const db = await get;
323
+ return searchAdvanced(db, query, searchOptions?.tag, options.search);
286
324
  }
287
325
  };
288
326
  }
289
327
  export {
290
- createI18nSearchAPI2 as createI18nSearchAPI,
291
- createI18nSearchAPI as createI18nSearchAPIExperimental,
328
+ createFromSource,
329
+ createI18nSearchAPI,
292
330
  createSearchAPI,
293
331
  initAdvancedSearch,
294
332
  initSimpleSearch
@@ -1,8 +1,12 @@
1
1
  export { a as TOCItemType, T as TableOfContents, g as getTableOfContents } from '../get-toc-CM4X3hbw.js';
2
- import { N as Node, I as Item, R as Root } from '../page-tree-BTCDMLTU.js';
3
- export { p as PageTree } from '../page-tree-BTCDMLTU.js';
4
- export { SortedResult } from '../search/shared.js';
2
+ import { N as Node, I as Item, R as Root } from '../page-tree-r8qjoUla.js';
3
+ export { p as PageTree } from '../page-tree-r8qjoUla.js';
4
+ export { S as SortedResult } from '../types-Ch8gnVgO.js';
5
+ import { Metadata } from 'next';
6
+ import { NextRequest } from 'next/server';
7
+ import { LoaderOutput, LoaderConfig, InferPageType } from '../source/index.js';
5
8
  import 'react';
9
+ import '../config-inq6kP6y.js';
6
10
 
7
11
  /**
8
12
  * Flatten tree to an array of page nodes
@@ -53,4 +57,52 @@ interface GetGithubLastCommitOptions {
53
57
  */
54
58
  declare function getGithubLastEdit({ repo, token, owner, path, sha, options, params: customParams, }: GetGithubLastCommitOptions): Promise<Date | null>;
55
59
 
56
- export { type GetGithubLastCommitOptions, findNeighbour, flattenTree, getGithubLastEdit, separatePageTree };
60
+ interface ImageMeta {
61
+ alt: string;
62
+ url: string;
63
+ width: number;
64
+ height: number;
65
+ }
66
+ declare function createMetadataImage<S extends LoaderOutput<LoaderConfig>>(options: {
67
+ source: S;
68
+ /**
69
+ * the route of your OG image generator.
70
+ *
71
+ * @example '/docs-og'
72
+ * @defaultValue '/docs-og'
73
+ */
74
+ imageRoute?: string;
75
+ /**
76
+ * The filename of generated OG Image
77
+ *
78
+ * @defaultValue 'image.png'
79
+ */
80
+ filename?: string;
81
+ }): {
82
+ getImageMeta: (slugs: string[]) => ImageMeta;
83
+ /**
84
+ * Add image meta tags to metadata
85
+ */
86
+ withImage: (slugs: string[], metadata?: Metadata) => Metadata;
87
+ /**
88
+ * Generate static params for OG Image Generator
89
+ */
90
+ generateParams: () => {
91
+ slug: string[];
92
+ lang?: string;
93
+ }[];
94
+ /**
95
+ * create route handler for OG Image Generator
96
+ */
97
+ createAPI: (handler: (page: InferPageType<S>, request: NextRequest, options: {
98
+ params: {
99
+ slug: string[];
100
+ lang?: string;
101
+ } | Promise<{
102
+ slug: string[];
103
+ lang?: string;
104
+ }>;
105
+ }) => Response | Promise<Response>) => (request: NextRequest, options: any) => Response | Promise<Response>;
106
+ };
107
+
108
+ export { type GetGithubLastCommitOptions, createMetadataImage, findNeighbour, flattenTree, getGithubLastEdit, separatePageTree };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  remarkHeading
3
- } from "../chunk-UQV4A7HQ.js";
3
+ } from "../chunk-4MNUWZIW.js";
4
4
  import "../chunk-MLKGABMK.js";
5
5
 
6
6
  // src/server/get-toc.ts
@@ -87,8 +87,66 @@ async function getGithubLastEdit({
87
87
  if (data.length === 0) return null;
88
88
  return new Date(data[0].commit.committer.date);
89
89
  }
90
+
91
+ // src/server/metadata.ts
92
+ import { notFound } from "next/navigation";
93
+ function createMetadataImage(options) {
94
+ const { filename = "image.png", imageRoute = "/docs-og" } = options;
95
+ function getImageMeta(slugs) {
96
+ return {
97
+ alt: "Banner",
98
+ url: `/${[...imageRoute.split("/"), ...slugs, filename].filter((v) => v.length > 0).join("/")}`,
99
+ width: 1200,
100
+ height: 630
101
+ };
102
+ }
103
+ return {
104
+ getImageMeta,
105
+ withImage(slugs, data) {
106
+ const imageData = getImageMeta(slugs);
107
+ return {
108
+ ...data,
109
+ openGraph: {
110
+ images: imageData,
111
+ ...data?.openGraph
112
+ },
113
+ twitter: {
114
+ images: imageData,
115
+ card: "summary_large_image",
116
+ ...data?.twitter
117
+ }
118
+ };
119
+ },
120
+ generateParams() {
121
+ return options.source.generateParams().map((params) => ({
122
+ ...params,
123
+ slug: [...params.slug, filename]
124
+ }));
125
+ },
126
+ createAPI(handler) {
127
+ return async (req, args) => {
128
+ const params = await args.params;
129
+ if (!params || !("slug" in params) || params.slug === void 0)
130
+ throw new Error(`Invalid params: ${JSON.stringify(params)}`);
131
+ const lang = "lang" in params && typeof params.lang === "string" ? params.lang : void 0;
132
+ const input = {
133
+ slug: Array.isArray(params.slug) ? params.slug : [params.slug],
134
+ lang
135
+ };
136
+ const page = options.source.getPage(
137
+ input.slug.slice(0, -1),
138
+ //remove filename
139
+ lang
140
+ );
141
+ if (!page) notFound();
142
+ return handler(page, req, { params: input });
143
+ };
144
+ }
145
+ };
146
+ }
90
147
  export {
91
148
  page_tree_exports as PageTree,
149
+ createMetadataImage,
92
150
  findNeighbour,
93
151
  flattenTree,
94
152
  getGithubLastEdit,