erudit 4.0.0-dev.1 → 4.0.0-dev.2

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 (62) hide show
  1. package/app/app.vue +1 -2
  2. package/app/components/FancyBold.vue +0 -1
  3. package/app/components/FancyCard.vue +1 -2
  4. package/app/components/ads/AdsBannerAside.vue +1 -2
  5. package/app/components/ads/AdsReplacer.vue +2 -2
  6. package/app/components/aside/AsideListItem.vue +1 -1
  7. package/app/components/aside/AsideSwitch.vue +18 -8
  8. package/app/components/aside/major/PaneSwitcher.vue +1 -1
  9. package/app/components/aside/minor/AsideMinorPlainHeader.vue +2 -3
  10. package/app/components/aside/minor/content/AsideMinorContentTopic.vue +1 -4
  11. package/app/components/aside/minor/content/ButtonPaneContributions.vue +2 -4
  12. package/app/components/aside/minor/content/ButtonPaneImprove.vue +2 -3
  13. package/app/components/aside/minor/content/Contribution.vue +1 -1
  14. package/app/components/aside/minor/content/TocItem.vue +35 -21
  15. package/app/components/aside/minor/news/AsideMinorNews.vue +1 -2
  16. package/app/components/aside/minor/news/NewsItem.vue +2 -2
  17. package/app/components/aside/minor/news/elements/Ref.vue +1 -1
  18. package/app/components/main/MainContentChild.vue +2 -3
  19. package/app/components/main/MainDescription.vue +1 -1
  20. package/app/components/main/MainQuickLink.vue +20 -5
  21. package/app/components/main/MainQuickLinks.vue +1 -3
  22. package/app/components/main/MainQuote.vue +3 -6
  23. package/app/components/main/MainSection.vue +6 -21
  24. package/app/components/main/MainTitle.vue +1 -2
  25. package/app/components/main/MainTopicPartSwitch.vue +1 -1
  26. package/app/components/main/connections/Deps.vue +1 -1
  27. package/app/components/main/connections/Externals.vue +92 -34
  28. package/app/components/main/connections/MainConnections.vue +61 -8
  29. package/app/components/main/connections/MainConnectionsButton.vue +3 -2
  30. package/app/components/main/connections/ScrollPane.vue +2 -3
  31. package/app/components/main/contentStats/Item.vue +1 -2
  32. package/app/components/main/contentStats/MainContentStats.vue +6 -3
  33. package/app/components/preview/Preview.vue +1 -2
  34. package/app/components/preview/PreviewScreen.vue +1 -2
  35. package/app/components/site/SiteAside.vue +2 -4
  36. package/app/components/site/SiteMain.vue +1 -4
  37. package/app/components/tree/TreeItem.vue +1 -1
  38. package/app/composables/og.ts +19 -2
  39. package/app/pages/contributor/[contributorId].vue +1 -2
  40. package/app/pages/index.vue +1 -4
  41. package/app/styles/main.css +0 -1
  42. package/package.json +4 -4
  43. package/server/erudit/cameos/build.ts +77 -27
  44. package/server/erudit/content/global/build.ts +27 -1
  45. package/server/erudit/content/nav/build.ts +36 -4
  46. package/server/erudit/content/repository/elementSnippets.ts +51 -11
  47. package/server/erudit/content/repository/externals.ts +38 -9
  48. package/server/erudit/content/resolve/index.ts +172 -21
  49. package/server/erudit/content/resolve/topic.ts +93 -32
  50. package/server/erudit/content/resolve/utils/insertContentResolved.ts +48 -9
  51. package/server/erudit/content/search.ts +31 -3
  52. package/server/erudit/contributors/build.ts +106 -51
  53. package/server/erudit/db/repository/pushFile.ts +7 -4
  54. package/server/erudit/db/schema/content.ts +2 -2
  55. package/server/erudit/db/schema/contentSnippets.ts +3 -4
  56. package/server/erudit/language/list/en.ts +2 -0
  57. package/server/erudit/language/list/ru.ts +2 -0
  58. package/server/erudit/news/build.ts +85 -48
  59. package/server/erudit/sponsors/build.ts +77 -26
  60. package/shared/types/contentConnections.ts +2 -2
  61. package/shared/types/elementSnippet.ts +9 -3
  62. package/shared/types/language.ts +2 -0
@@ -2,13 +2,24 @@
2
2
  import Deps from './Deps.vue';
3
3
  import Externals from './Externals.vue';
4
4
 
5
- defineProps<{ connections?: ContentConnections }>();
5
+ const { connections } = defineProps<{ connections?: ContentConnections }>();
6
6
 
7
7
  const phrase = await usePhrases('connections');
8
8
 
9
9
  const currentType = ref<keyof ContentConnections | undefined>(
10
10
  'hardDependencies',
11
11
  );
12
+
13
+ const ownExternalsCount = computed(() => {
14
+ return connections?.externals?.find((ext) => ext.type === 'own')?.items
15
+ .length;
16
+ });
17
+
18
+ const parentExternalsCount = computed(() => {
19
+ return connections?.externals
20
+ ?.filter((ext) => ext.type === 'parent')
21
+ .reduce((sum, ext) => sum + ext.items.length, 0);
22
+ });
12
23
  </script>
13
24
 
14
25
  <template>
@@ -18,17 +29,59 @@ const currentType = ref<keyof ContentConnections | undefined>(
18
29
  class="gap-small micro:gap-normal micro:justify-start flex flex-wrap
19
30
  justify-center"
20
31
  >
32
+ <template
33
+ v-for="(items, type) of {
34
+ hardDependencies: connections.hardDependencies,
35
+ autoDependencies: connections.autoDependencies,
36
+ dependents: connections.dependents,
37
+ }"
38
+ >
39
+ <MainConnectionsButton
40
+ v-if="items && items.length > 0"
41
+ :type="type"
42
+ :count="items.length"
43
+ :active="currentType === type"
44
+ @click="
45
+ currentType === type
46
+ ? (currentType = undefined)
47
+ : (currentType = type)
48
+ "
49
+ />
50
+ </template>
21
51
  <MainConnectionsButton
22
- v-for="(value, type) of connections"
23
- :type="type"
24
- :count="value!.length"
25
- :active="currentType === type"
52
+ v-if="connections.externals"
53
+ type="externals"
54
+ :active="currentType === 'externals'"
26
55
  @click="
27
- currentType === type
56
+ currentType === 'externals'
28
57
  ? (currentType = undefined)
29
- : (currentType = type)
58
+ : (currentType = 'externals')
30
59
  "
31
- />
60
+ >
61
+ <template #after>
62
+ <div
63
+ v-if="connections.externals"
64
+ class="gap-small *:border-border *:pl-small flex
65
+ items-center font-bold *:border-l"
66
+ >
67
+ <div
68
+ v-if="ownExternalsCount"
69
+ class="flex items-center gap-1 text-amber-600
70
+ dark:text-amber-400"
71
+ >
72
+ <MyIcon name="arrow/left" class="-scale-x-100" />
73
+ <span>{{ ownExternalsCount }}</span>
74
+ </div>
75
+ <div
76
+ v-if="parentExternalsCount"
77
+ class="flex items-center gap-1"
78
+ >
79
+ <MyIcon name="arrow/up-to-right" />
80
+ <span>{{ parentExternalsCount }}</span>
81
+ </div>
82
+ </div>
83
+ </template>
84
+ </MainConnectionsButton>
32
85
  </div>
33
86
  <template v-if="currentType && connections[currentType]">
34
87
  <Deps
@@ -4,7 +4,7 @@ import type { MyIconName } from '#my-icons';
4
4
  const { type, active } = defineProps<{
5
5
  type: 'hardDependencies' | 'autoDependencies' | 'dependents' | 'externals';
6
6
  active?: boolean;
7
- count: number;
7
+ count?: number;
8
8
  }>();
9
9
 
10
10
  const isHard = type === 'hardDependencies';
@@ -75,6 +75,7 @@ const dynamicClasses = computed(() => {
75
75
  >
76
76
  <MyIcon :name="icon" class="text-[1.2em]" />
77
77
  <span>{{ formatText(title) }}</span>
78
- <span class="font-bold">{{ count }}</span>
78
+ <span v-if="count" class="font-bold">{{ count }}</span>
79
+ <slot name="after"></slot>
79
80
  </button>
80
81
  </template>
@@ -2,9 +2,8 @@
2
2
 
3
3
  <template>
4
4
  <div
5
- class="nice-scrollbars border-border gap-main-half mt-normal py-normal
6
- relative flex max-h-[500px] flex-col overflow-auto border-t border-b
7
- transition-[border]"
5
+ class="nice-scrollbars border-border mt-normal relative overflow-auto
6
+ border-t border-b"
8
7
  >
9
8
  <slot></slot>
10
9
  </div>
@@ -22,8 +22,7 @@ const { mode = 'detailed' } = defineProps<{
22
22
  <div
23
23
  v-else
24
24
  class="gap-small px-small text-main-sm border-border bg-bg-aside flex
25
- items-center rounded-xl border py-1
26
- transition-[background,color,border]"
25
+ items-center rounded-xl border py-1"
27
26
  >
28
27
  <MaybeMyIcon
29
28
  :name="icon"
@@ -15,8 +15,8 @@ const phrase = await usePhrases('stats');
15
15
  <section v-if="mode === 'single'" class="px-main py-main-half">
16
16
  <MainSubTitle :title="phrase.stats + ':'" />
17
17
  <div
18
- class="micro:justify-start gap-normal flex flex-wrap
19
- justify-center"
18
+ class="micro:justify-start gap-small micro:gap-normal flex
19
+ flex-wrap justify-center"
20
20
  >
21
21
  <ItemMaterials
22
22
  v-if="stats.materials"
@@ -32,7 +32,10 @@ const phrase = await usePhrases('stats');
32
32
  />
33
33
  </div>
34
34
  </section>
35
- <div v-else class="gap-normal text-main-sm flex flex-wrap">
35
+ <div
36
+ v-else
37
+ class="gap-small micro:gap-normal text-main-sm flex flex-wrap"
38
+ >
36
39
  <ItemMaterials
37
40
  v-if="stats.materials"
38
41
  :count="stats.materials"
@@ -117,8 +117,7 @@ await usePhrases(
117
117
  `border-border bg-bg-main micro:max-h-[70dvh]
118
118
  pointer-events-auto absolute bottom-0 max-h-[90dvh] w-full
119
119
  touch-auto overflow-hidden rounded-[25px] rounded-b-none
120
- border-t
121
- transition-[box-shadow,background,border,max-height,height,translate]`,
120
+ border-t transition-[max-height,height,translate]`,
122
121
  previewState.opened
123
122
  ? `translate-y-0
124
123
  shadow-[0px_-10px_15px_5px_light-dark(rgba(0,0,0,0.1),rgba(255,255,255,0.05))]`
@@ -19,8 +19,7 @@ const { closePreview, hasPreviousRequest, setPreviousPreview } = usePreview();
19
19
  </div>
20
20
  <div
21
21
  class="border-border gap-small micro:gap-normal micro:h-[60px]
22
- px-main flex h-[54px] shrink-0 items-center border-t
23
- transition-[border]"
22
+ px-main flex h-[54px] shrink-0 items-center border-t"
24
23
  >
25
24
  <MaybeMyIcon
26
25
  :name="icon"
@@ -31,8 +31,7 @@ const opened = computed(() => {
31
31
  <div
32
32
  :class="[
33
33
  `bg-bg-aside absolute top-0 left-0 h-full w-full
34
- border-[light-dark(var(--color-neutral-200),var(--color-neutral-800))]
35
- transition-[background,border,backdrop-filter,box-shadow]`,
34
+ border-[light-dark(var(--color-neutral-200),var(--color-neutral-800))]`,
36
35
  {
37
36
  'border-e': isMajor,
38
37
  'border-s': isMinor,
@@ -55,8 +54,7 @@ const opened = computed(() => {
55
54
  `pointer-events-none absolute top-0 h-full w-full
56
55
  touch-none bg-linear-to-l
57
56
  from-[light-dark(rgba(0,0,0,0.02),rgba(0,0,0,0.1))]
58
- via-transparent via-[3px] opacity-100
59
- transition-opacity`,
57
+ via-transparent via-[3px] opacity-100`,
60
58
  {
61
59
  '-scale-100': isMinor,
62
60
  'max-aside1:opacity-0': opened && isMajor,
@@ -5,10 +5,7 @@
5
5
  >
6
6
  <div class="text-main" data-erudit-main>
7
7
  <Preview />
8
- <div
9
- class="bg-bg-main min-h-dvh transition-[background]
10
- dark:bg-[#212121]"
11
- >
8
+ <div class="bg-bg-main min-h-dvh dark:bg-[#212121]">
12
9
  <slot></slot>
13
10
  </div>
14
11
  </div>
@@ -16,7 +16,7 @@ defineProps<{
16
16
  :style="{ '--level': level ? +level : 0 }"
17
17
  :class="[
18
18
  `px-normal py-small hocus:bg-bg-accent flex cursor-pointer
19
- items-center gap-[calc(var(--spacing-normal)/1.6)] text-sm
19
+ items-center gap-[calc(var(--spacing-normal)/2)] text-[.85em]
20
20
  transition-[background,color]`,
21
21
  'pl-[calc(var(--spacing-normal)*(var(--level)+1)-2px)]',
22
22
  {
@@ -118,9 +118,26 @@ export async function useContentSeo(args: {
118
118
  }
119
119
 
120
120
  const elementPhrase = await getElementPhrase(snippet.schemaName);
121
+
122
+ const title = (() => {
123
+ if (snippet.seo?.title) {
124
+ return snippet.seo.title;
125
+ } else {
126
+ return snippet.title;
127
+ }
128
+ })();
129
+
130
+ const description = (() => {
131
+ if (snippet.seo?.description) {
132
+ return snippet.seo.description;
133
+ } else {
134
+ return snippet.description;
135
+ }
136
+ })();
137
+
121
138
  setupSeo({
122
- title: `${snippet.title} [${elementPhrase.element_name}] - ${seoSiteTitle}`,
123
- description: snippet.description || '',
139
+ title: `${title} [${elementPhrase.element_name}] - ${seoSiteTitle}`,
140
+ description: description || '',
124
141
  urlPath: snippet.link,
125
142
  });
126
143
  },
@@ -86,8 +86,7 @@ useStandartSeo({
86
86
  :style="{ '--mediaColor': color }"
87
87
  class="border-bg-main micro:size-[110px] size-[80px]
88
88
  rounded-full border-2
89
- [box-shadow:0_0_16px_0_var(--color)]
90
- transition-[border]"
89
+ [box-shadow:0_0_16px_0_var(--color)]"
91
90
  />
92
91
  </div>
93
92
  <div
@@ -38,10 +38,7 @@ const phrase = await usePhrases('x_contributors', 'x_sponsors');
38
38
  :style="{
39
39
  'max-width': `min(${indexPage.topImage.maxWidth || '100%'}, 100%)`,
40
40
  }"
41
- :class="[
42
- 'pt-main-half px-main mx-auto block transition-[filter]',
43
- logotypeInvertClass,
44
- ]"
41
+ :class="['pt-main-half px-main mx-auto block', logotypeInvertClass]"
45
42
  />
46
43
 
47
44
  <!-- Main Data -->
@@ -90,7 +90,6 @@
90
90
  color: var(--color-text);
91
91
  font-size: 18px;
92
92
  interpolate-size: allow-keywords;
93
- @apply transition-[background];
94
93
  }
95
94
 
96
95
  :root[data-theme='light'] {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "erudit",
3
- "version": "4.0.0-dev.1",
3
+ "version": "4.0.0-dev.2",
4
4
  "type": "module",
5
5
  "description": "🤓 CMS for perfect educational sites.",
6
6
  "license": "MIT",
@@ -29,9 +29,9 @@
29
29
  "test": "bun vitest run"
30
30
  },
31
31
  "dependencies": {
32
- "@erudit-js/cli": "4.0.0-dev.1",
33
- "@erudit-js/core": "4.0.0-dev.1",
34
- "@erudit-js/prose": "4.0.0-dev.1",
32
+ "@erudit-js/cli": "4.0.0-dev.2",
33
+ "@erudit-js/core": "4.0.0-dev.2",
34
+ "@erudit-js/prose": "4.0.0-dev.2",
35
35
  "@floating-ui/vue": "^1.1.9",
36
36
  "@jsprose/core": "^1.0.0",
37
37
  "@tailwindcss/vite": "^4.1.17",
@@ -1,44 +1,96 @@
1
- import { readdirSync, readFileSync } from 'node:fs';
1
+ import { readdirSync, readFileSync, existsSync } from 'node:fs';
2
2
  import { eq } from 'drizzle-orm';
3
3
  import type { Cameo, CameoConfig } from '@erudit-js/core/cameo';
4
4
 
5
+ let initialBuild = true;
6
+
7
+ const cameosRoot = () => `${ERUDIT.config.paths.project}/cameos`;
8
+
5
9
  export async function buildCameos() {
6
10
  ERUDIT.log.debug.start('Building cameos...');
7
11
 
8
- await ERUDIT.db.delete(ERUDIT.db.schema.cameos);
9
- await ERUDIT.db
10
- .delete(ERUDIT.db.schema.files)
11
- .where(eq(ERUDIT.db.schema.files.role, 'cameo-avatar'));
12
-
13
- let cameoIds: string[] = [];
12
+ const isInitial = initialBuild;
13
+ initialBuild = false;
14
14
 
15
- try {
16
- cameoIds = readdirSync(ERUDIT.config.paths.project + '/cameos', {
17
- withFileTypes: true,
18
- })
19
- .filter((entry) => entry.isDirectory())
20
- .map((entry) => entry.name);
21
- } catch {}
15
+ const cameoIds = collectCameoIds(isInitial);
22
16
 
23
- let cameoCount = 0;
17
+ if (!cameoIds.size) {
18
+ ERUDIT.log.info(
19
+ isInitial
20
+ ? 'Skipping cameos — no cameos found.'
21
+ : 'Skipping cameos — nothing changed.',
22
+ );
23
+ return;
24
+ }
24
25
 
25
26
  for (const cameoId of cameoIds) {
27
+ await cleanupCameo(cameoId);
28
+ }
29
+
30
+ const existingIds = [...cameoIds].filter((id) =>
31
+ existsSync(`${cameosRoot()}/${id}`),
32
+ );
33
+
34
+ if (!existingIds.length) {
35
+ return;
36
+ }
37
+
38
+ for (const cameoId of existingIds) {
26
39
  await buildCameo(cameoId);
27
- cameoCount++;
28
40
  }
29
41
 
30
42
  ERUDIT.log.success(
31
- `Cameos build complete! (${ERUDIT.log.stress(cameoCount)})`,
43
+ isInitial
44
+ ? `Cameos build complete! (${ERUDIT.log.stress(cameoIds.size)})`
45
+ : `Cameos updated: ${ERUDIT.log.stress(existingIds.join(', '))}`,
32
46
  );
33
47
  }
34
48
 
49
+ //
50
+ //
51
+ //
52
+
53
+ function collectCameoIds(initial: boolean): Set<string> {
54
+ if (initial) {
55
+ try {
56
+ return new Set(
57
+ readdirSync(cameosRoot(), { withFileTypes: true })
58
+ .filter((entry) => entry.isDirectory())
59
+ .map((entry) => entry.name),
60
+ );
61
+ } catch {
62
+ return new Set();
63
+ }
64
+ }
65
+
66
+ const ids = new Set<string>();
67
+
68
+ for (const file of ERUDIT.changedFiles.values()) {
69
+ if (!file.startsWith(`${cameosRoot()}/`)) continue;
70
+ const id = file.replace(`${cameosRoot()}/`, '').split('/')[0];
71
+ if (id) ids.add(id);
72
+ }
73
+
74
+ return ids;
75
+ }
76
+
77
+ async function cleanupCameo(cameoId: string) {
78
+ await ERUDIT.db
79
+ .delete(ERUDIT.db.schema.cameos)
80
+ .where(eq(ERUDIT.db.schema.cameos.cameoId, cameoId));
81
+
82
+ await ERUDIT.db
83
+ .delete(ERUDIT.db.schema.files)
84
+ .where(eq(ERUDIT.db.schema.files.role, 'cameo-avatar'));
85
+ }
86
+
35
87
  async function buildCameo(cameoId: string) {
36
88
  ERUDIT.log.debug.start(`Building cameo ${ERUDIT.log.stress(cameoId)}...`);
37
89
 
38
- const cameoDirectory = ERUDIT.config.paths.project + '/cameos/' + cameoId;
39
- const cameoFiles = readdirSync(cameoDirectory);
90
+ const dir = `${cameosRoot()}/${cameoId}`;
91
+ const files = readdirSync(dir);
40
92
 
41
- const hasConfig = cameoFiles.some(
93
+ const hasConfig = files.some(
42
94
  (file) => file === 'cameo.ts' || file === 'cameo.js',
43
95
  );
44
96
 
@@ -52,9 +104,7 @@ async function buildCameo(cameoId: string) {
52
104
  let cameoConfig: CameoConfig;
53
105
 
54
106
  try {
55
- cameoConfig = (await ERUDIT.import(
56
- `${cameoDirectory}/cameo`,
57
- )) as CameoConfig;
107
+ cameoConfig = (await ERUDIT.import(`${dir}/cameo`)) as CameoConfig;
58
108
  } catch (error) {
59
109
  const message = error instanceof Error ? error.message : String(error);
60
110
  ERUDIT.log.error(
@@ -63,7 +113,7 @@ async function buildCameo(cameoId: string) {
63
113
  return;
64
114
  }
65
115
 
66
- const avatarExtension = cameoFiles
116
+ const avatarExtension = files
67
117
  .find((file) => file.startsWith('avatar.'))
68
118
  ?.split('.')
69
119
  .pop();
@@ -76,14 +126,14 @@ async function buildCameo(cameoId: string) {
76
126
  }
77
127
 
78
128
  await ERUDIT.repository.db.pushFile(
79
- `${cameoDirectory}/avatar.${avatarExtension}`,
129
+ `${dir}/avatar.${avatarExtension}`,
80
130
  'cameo-avatar',
81
131
  );
82
132
 
83
133
  const icon = (() => {
84
- const iconFile = cameoFiles.find((file) => file === 'icon.svg');
134
+ const iconFile = files.find((file) => file === 'icon.svg');
85
135
  if (iconFile) {
86
- return readFileSync(cameoDirectory + '/' + iconFile, 'utf-8');
136
+ return readFileSync(dir + '/' + iconFile, 'utf-8');
87
137
  }
88
138
  })();
89
139
 
@@ -5,9 +5,21 @@ import { $CONTENT } from './singleton';
5
5
  // Call singleton to trigger initialization
6
6
  $CONTENT;
7
7
 
8
+ let initialBuild = true;
9
+
10
+ const contentRoot = () => `${ERUDIT.config.paths.project}/content`;
11
+
8
12
  export async function buildGlobalContent() {
9
13
  ERUDIT.log.debug.start('Building global content...');
10
14
 
15
+ const isInitial = initialBuild;
16
+ initialBuild = false;
17
+
18
+ if (!isInitial && !hasContentChanges()) {
19
+ ERUDIT.log.info('Skipping global content — nothing changed.');
20
+ return;
21
+ }
22
+
11
23
  const linkObject = await buildLinkObject();
12
24
 
13
25
  const linkTypes = linkObjectToTypes(linkObject);
@@ -17,7 +29,21 @@ export async function buildGlobalContent() {
17
29
  'utf-8',
18
30
  );
19
31
 
20
- ERUDIT.log.success('Global content built successfully!');
32
+ ERUDIT.log.success(
33
+ isInitial
34
+ ? 'Global content build complete!'
35
+ : 'Global content updated!',
36
+ );
37
+ }
38
+
39
+ function hasContentChanges() {
40
+ for (const file of (ERUDIT.changedFiles || new Set<string>()).values()) {
41
+ if (file.startsWith(`${contentRoot()}/`)) {
42
+ return true;
43
+ }
44
+ }
45
+
46
+ return false;
21
47
  }
22
48
 
23
49
  function linkObjectToTypes(linkObject: any): string {
@@ -1,19 +1,31 @@
1
1
  import chalk from 'chalk';
2
2
  import { globSync } from 'glob';
3
- import { readdirSync } from 'node:fs';
3
+ import { existsSync, readdirSync } from 'node:fs';
4
4
  import { contentTypes, type ContentType } from '@erudit-js/core/content/type';
5
5
 
6
6
  import type { ContentNavNode, ContentNavMap } from './types';
7
7
 
8
+ let initialBuild = true;
9
+
10
+ const contentRoot = () => `${ERUDIT.config.paths.project}/content`;
11
+
8
12
  export async function buildContentNav() {
9
13
  ERUDIT.log.debug.start('Building content navigation...');
10
14
 
15
+ const isInitial = initialBuild;
16
+ initialBuild = false;
17
+
18
+ if (!isInitial && !hasContentChanges()) {
19
+ ERUDIT.log.info('Skipping content navigation — nothing changed.');
20
+ return;
21
+ }
22
+
11
23
  ERUDIT.contentNav.id2Node = new Map();
12
24
  ERUDIT.contentNav.id2Root = new Map();
13
25
  ERUDIT.contentNav.id2Books = new Map();
14
26
  ERUDIT.contentNav.short2Full = new Map();
15
27
 
16
- const cwd = ERUDIT.config.paths.project + '/content';
28
+ const cwd = contentRoot();
17
29
  const contentDirectories = globSync('**/*/', {
18
30
  cwd,
19
31
  posix: true,
@@ -122,7 +134,11 @@ export async function buildContentNav() {
122
134
  ? getContentNavStats(ERUDIT.contentNav.id2Node)
123
135
  : chalk.gray('empty');
124
136
 
125
- ERUDIT.log.success(`Content navigation built successfully! (${stats})`);
137
+ ERUDIT.log.success(
138
+ isInitial
139
+ ? `Content navigation build complete! (${stats})`
140
+ : `Content navigation updated! (${stats})`,
141
+ );
126
142
  }
127
143
 
128
144
  /**
@@ -136,7 +152,13 @@ async function createStandaloneContentNavNode(
136
152
  const parsedContentPath = parseContentPath(relPath);
137
153
  if (!parsedContentPath) return;
138
154
 
139
- const files = readdirSync(cwd + '/' + relPath).reduce<Record<string, null>>(
155
+ const dirPath = `${cwd}/${relPath}`;
156
+
157
+ if (!existsSync(dirPath)) {
158
+ return;
159
+ }
160
+
161
+ const files = readdirSync(dirPath).reduce<Record<string, null>>(
140
162
  (acc, name) => {
141
163
  acc[name] = null;
142
164
  return acc;
@@ -271,6 +293,16 @@ function getContentNavStats(id2Node: ContentNavMap) {
271
293
  return parts.join('; ');
272
294
  }
273
295
 
296
+ function hasContentChanges() {
297
+ for (const file of (ERUDIT.changedFiles || new Set<string>()).values()) {
298
+ if (file.startsWith(`${contentRoot()}/`)) {
299
+ return true;
300
+ }
301
+ }
302
+
303
+ return false;
304
+ }
305
+
274
306
  //
275
307
  // Validators
276
308
  //
@@ -10,10 +10,7 @@ export async function getContentElementSnippets(
10
10
  contentProseType: true,
11
11
  schemaName: true,
12
12
  elementId: true,
13
- title: true,
14
- description: true,
15
- quick: true,
16
- seo: true,
13
+ snippetData: true,
17
14
  },
18
15
  where: and(
19
16
  eq(ERUDIT.db.schema.contentSnippets.contentFullId, fullId),
@@ -40,22 +37,65 @@ export async function getContentElementSnippets(
40
37
  );
41
38
  })();
42
39
 
40
+ const snippetData = dbSnippet.snippetData;
41
+
43
42
  const snippet: ElementSnippet = {
44
43
  link,
45
44
  schemaName: dbSnippet.schemaName,
46
- title: dbSnippet.title,
45
+ title: snippetData.title!,
47
46
  };
48
47
 
49
- if (dbSnippet.quick) {
50
- snippet.quick = true;
48
+ if (snippetData.quick) {
49
+ snippet.quick = {};
50
+ let quickTitle: string | undefined;
51
+ let quickDescription: string | undefined;
52
+
53
+ if (typeof snippetData.quick === 'string') {
54
+ quickTitle = snippetData.quick;
55
+ } else if (typeof snippetData.quick === 'object') {
56
+ if (snippetData.quick.title) {
57
+ quickTitle = snippetData.quick.title;
58
+ }
59
+ if (snippetData.quick.description) {
60
+ quickDescription = snippetData.quick.description;
61
+ }
62
+ }
63
+
64
+ if (quickTitle) {
65
+ snippet.quick.title = quickTitle;
66
+ }
67
+ if (quickDescription) {
68
+ snippet.quick.description = quickDescription;
69
+ }
51
70
  }
52
71
 
53
- if (dbSnippet.seo) {
54
- snippet.seo = true;
72
+ if (snippetData.seo) {
73
+ snippet.seo = {};
74
+
75
+ let seoTitle: string | undefined;
76
+ let seoDescription: string | undefined;
77
+
78
+ if (typeof snippetData.seo === 'string') {
79
+ seoTitle = snippetData.seo;
80
+ } else if (typeof snippetData.seo === 'object') {
81
+ if (snippetData.seo.title) {
82
+ seoTitle = snippetData.seo.title;
83
+ }
84
+ if (snippetData.seo.description) {
85
+ seoDescription = snippetData.seo.description;
86
+ }
87
+ }
88
+
89
+ if (seoTitle) {
90
+ snippet.seo.title = seoTitle;
91
+ }
92
+ if (seoDescription) {
93
+ snippet.seo.description = seoDescription;
94
+ }
55
95
  }
56
96
 
57
- if (dbSnippet.description) {
58
- snippet.description = dbSnippet.description;
97
+ if (snippetData.description) {
98
+ snippet.description = snippetData.description;
59
99
  }
60
100
 
61
101
  snippets.push(snippet);