erudit 4.3.3 → 4.3.4-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.
@@ -1,28 +1,36 @@
1
1
  <script lang="ts" setup>
2
2
  defineProps<{ breadcrumbs: Breadcrumbs }>();
3
+
4
+ const phrase = await usePhrases('breadcrumb');
3
5
  </script>
4
6
 
5
7
  <template>
6
- <section
8
+ <nav
7
9
  v-if="breadcrumbs.length > 0"
8
- class="gap-small max-micro:justify-center px-main py-main-half flex
9
- flex-wrap"
10
+ :aria-label="phrase.breadcrumb"
11
+ class="px-main py-main-half"
10
12
  >
11
- <EruditLink
12
- v-for="(breadcrumb, i) of breadcrumbs"
13
- :to="breadcrumb.link"
14
- class="gap-small text-text-dimmed hocus:text-text-muted flex items-center
15
- transition-[color]"
13
+ <ol
14
+ class="gap-small max-micro:justify-center m-0 flex list-none flex-wrap
15
+ p-0"
16
16
  >
17
- <MaybeMyIcon :name="breadcrumb.icon" class="text-[1.2em]" />
18
- <span>{{ formatText(breadcrumb.title) }}</span>
19
- <MyIcon
20
- name="chevron-right"
21
- :class="{
22
- 'relative -left-[3px]': true,
23
- 'rotate-90': i === breadcrumbs.length - 1,
24
- }"
25
- />
26
- </EruditLink>
27
- </section>
17
+ <li v-for="(breadcrumb, i) of breadcrumbs">
18
+ <EruditLink
19
+ :to="breadcrumb.link"
20
+ class="gap-small text-text-dimmed hocus:text-text-muted flex
21
+ items-center transition-[color]"
22
+ >
23
+ <MaybeMyIcon :name="breadcrumb.icon" class="text-[1.2em]" />
24
+ <span>{{ formatText(breadcrumb.title) }}</span>
25
+ <MyIcon
26
+ name="chevron-right"
27
+ :class="{
28
+ 'relative -left-[3px]': true,
29
+ 'rotate-90': i === breadcrumbs.length - 1,
30
+ }"
31
+ />
32
+ </EruditLink>
33
+ </li>
34
+ </ol>
35
+ </nav>
28
36
  </template>
@@ -19,16 +19,22 @@ const phrase = await usePhrases('key_elements');
19
19
 
20
20
  <template>
21
21
  <template v-if="keyLinks">
22
- <section v-if="mode === 'single'" class="px-main py-main-half">
22
+ <nav
23
+ v-if="mode === 'single'"
24
+ :aria-label="phrase.key_elements"
25
+ class="px-main py-main-half"
26
+ >
23
27
  <MainSubTitle :title="phrase.key_elements + ':'" />
24
- <div
28
+ <ul
25
29
  :style="{ '--keyBg': 'var(--color-bg-aside)' }"
26
- class="gap-small micro:gap-normal micro:justify-start flex flex-wrap
27
- justify-center"
30
+ class="gap-small micro:gap-normal micro:justify-start m-0 flex list-none
31
+ flex-wrap justify-center p-0"
28
32
  >
29
- <MainKeyLink v-for="keyLink of keyLinks" :keyLink />
30
- </div>
31
- </section>
33
+ <li v-for="keyLink of keyLinks">
34
+ <MainKeyLink :keyLink />
35
+ </li>
36
+ </ul>
37
+ </nav>
32
38
  <div
33
39
  v-else
34
40
  :style="{ '--keyBg': 'var(--color-bg-main)' }"
@@ -3,9 +3,10 @@ defineProps<{ title: string }>();
3
3
  </script>
4
4
 
5
5
  <template>
6
- <div
7
- class="text-main-sm micro:text-left pb-main-half text-center font-semibold"
6
+ <h2
7
+ class="text-main-sm micro:text-left pb-main-half m-0 text-center
8
+ font-semibold"
8
9
  >
9
10
  {{ formatText(title) }}
10
- </div>
11
+ </h2>
11
12
  </template>
@@ -27,11 +27,15 @@ const parentExternalsCount = computed(() => {
27
27
  </script>
28
28
 
29
29
  <template>
30
- <section v-if="connections" class="px-main py-main-half">
30
+ <section
31
+ v-if="connections"
32
+ :aria-label="phrase.connections"
33
+ class="px-main py-main-half"
34
+ >
31
35
  <MainSubTitle :title="phrase.connections + ':'" />
32
- <div
33
- class="gap-small micro:gap-normal micro:justify-start flex flex-wrap
34
- justify-center"
36
+ <ul
37
+ class="gap-small micro:gap-normal micro:justify-start m-0 flex list-none
38
+ flex-wrap justify-center p-0"
35
39
  >
36
40
  <template
37
41
  v-for="(items, type) of {
@@ -40,49 +44,52 @@ const parentExternalsCount = computed(() => {
40
44
  dependents: connections.dependents,
41
45
  }"
42
46
  >
47
+ <li v-if="items && items.length > 0">
48
+ <MainConnectionsButton
49
+ :type="type"
50
+ :count="items.length"
51
+ :active="currentType === type"
52
+ @click="
53
+ currentType === type
54
+ ? (currentType = undefined)
55
+ : (currentType = type)
56
+ "
57
+ />
58
+ </li>
59
+ </template>
60
+ <li v-if="connections.externals">
43
61
  <MainConnectionsButton
44
- v-if="items && items.length > 0"
45
- :type="type"
46
- :count="items.length"
47
- :active="currentType === type"
62
+ type="externals"
63
+ :active="currentType === 'externals'"
48
64
  @click="
49
- currentType === type
65
+ currentType === 'externals'
50
66
  ? (currentType = undefined)
51
- : (currentType = type)
67
+ : (currentType = 'externals')
52
68
  "
53
- />
54
- </template>
55
- <MainConnectionsButton
56
- v-if="connections.externals"
57
- type="externals"
58
- :active="currentType === 'externals'"
59
- @click="
60
- currentType === 'externals'
61
- ? (currentType = undefined)
62
- : (currentType = 'externals')
63
- "
64
- >
65
- <template #after>
66
- <div
67
- v-if="connections.externals"
68
- class="gap-small *:border-border *:pl-small flex items-center
69
- font-bold *:border-l"
70
- >
69
+ >
70
+ <template #after>
71
71
  <div
72
- v-if="ownExternalsCount"
73
- class="flex items-center gap-1 text-amber-600 dark:text-amber-400"
72
+ v-if="connections.externals"
73
+ class="gap-small *:border-border *:pl-small flex items-center
74
+ font-bold *:border-l"
74
75
  >
75
- <MyIcon name="arrow/left" class="-scale-x-100" />
76
- <span>{{ ownExternalsCount }}</span>
77
- </div>
78
- <div v-if="parentExternalsCount" class="flex items-center gap-1">
79
- <MyIcon name="arrow/up-to-right" />
80
- <span>{{ parentExternalsCount }}</span>
76
+ <div
77
+ v-if="ownExternalsCount"
78
+ class="flex items-center gap-1 text-amber-600
79
+ dark:text-amber-400"
80
+ >
81
+ <MyIcon name="arrow/left" class="-scale-x-100" />
82
+ <span>{{ ownExternalsCount }}</span>
83
+ </div>
84
+ <div v-if="parentExternalsCount" class="flex items-center gap-1">
85
+ <MyIcon name="arrow/up-to-right" />
86
+ <span>{{ parentExternalsCount }}</span>
87
+ </div>
81
88
  </div>
82
- </div>
83
- </template>
84
- </MainConnectionsButton>
85
- </div>
89
+ </template>
90
+ </MainConnectionsButton>
91
+ </li>
92
+ </ul>
86
93
  <template v-if="currentType && connections[currentType]">
87
94
  <Deps
88
95
  v-if="currentType !== 'externals'"
@@ -15,27 +15,26 @@ const phrase = await usePhrases('stats');
15
15
  <template>
16
16
  <section
17
17
  v-if="mode === 'single' && (stats || lastChangedDate)"
18
+ :aria-label="phrase.stats"
18
19
  class="px-main py-main-half"
19
20
  >
20
21
  <MainSubTitle :title="phrase.stats + ':'" />
21
- <div
22
- class="micro:justify-start gap-small micro:gap-normal flex flex-wrap
23
- justify-center"
22
+ <ul
23
+ class="micro:justify-start gap-small micro:gap-normal m-0 flex list-none
24
+ flex-wrap justify-center p-0"
24
25
  >
25
- <ItemMaterials
26
- v-if="stats?.materials"
27
- :count="stats.materials"
28
- mode="detailed"
29
- />
30
- <ItemElement
31
- v-if="stats?.elements"
32
- v-for="(count, schemaName) of stats.elements"
33
- :schemaName
34
- :count
35
- mode="detailed"
36
- />
37
- <ItemLastChanged v-if="lastChangedDate" :date="lastChangedDate" />
38
- </div>
26
+ <li v-if="stats?.materials">
27
+ <ItemMaterials :count="stats.materials" mode="detailed" />
28
+ </li>
29
+ <template v-if="stats?.elements">
30
+ <li v-for="(count, schemaName) of stats.elements">
31
+ <ItemElement :schemaName :count mode="detailed" />
32
+ </li>
33
+ </template>
34
+ <li v-if="lastChangedDate">
35
+ <ItemLastChanged :date="lastChangedDate" />
36
+ </li>
37
+ </ul>
39
38
  </section>
40
39
  <div
41
40
  v-else-if="mode === 'children' && stats"
@@ -112,7 +112,7 @@ export function initFavicon() {
112
112
  let stopWatchingRoute: ReturnType<typeof watch> | undefined;
113
113
  onMounted(() => {
114
114
  stopWatchingRoute = watch(
115
- route,
115
+ () => route.path,
116
116
  () => {
117
117
  clearTimeout(contentFaviconChangeTimeout);
118
118
 
@@ -1,4 +1,5 @@
1
1
  import type { Breadcrumbs } from '../../shared/types/breadcrumbs';
2
+ import type { ElementSnippet } from '../../shared/types/elementSnippet';
2
3
 
3
4
  export function useJsonLd(key: string, data: Record<string, unknown>) {
4
5
  useHead({
@@ -60,6 +61,8 @@ export function useContentArticleJsonLd(args: {
60
61
  urlPath: string;
61
62
  contentType: string;
62
63
  lastmod?: string;
64
+ keyElements?: ElementSnippet[];
65
+ breadcrumbs?: Breadcrumbs;
63
66
  }) {
64
67
  const withSiteUrl = useSiteUrl();
65
68
 
@@ -90,12 +93,31 @@ export function useContentArticleJsonLd(args: {
90
93
  data.dateModified = args.lastmod;
91
94
  }
92
95
 
93
- if (siteTitle) {
96
+ const parentBreadcrumb =
97
+ args.breadcrumbs && args.breadcrumbs.length >= 1
98
+ ? args.breadcrumbs[args.breadcrumbs.length - 1]
99
+ : undefined;
100
+
101
+ if (parentBreadcrumb) {
102
+ data.isPartOf = {
103
+ '@type': 'WebPage',
104
+ name: formatText(parentBreadcrumb.title),
105
+ url: withSiteUrl(parentBreadcrumb.link),
106
+ };
107
+ } else if (siteTitle) {
94
108
  data.isPartOf = {
95
109
  '@type': 'WebSite',
96
110
  name: siteTitle,
97
111
  };
98
112
  }
99
113
 
114
+ if (args.keyElements && args.keyElements.length > 0) {
115
+ data.hasPart = args.keyElements.map((el) => ({
116
+ '@type': 'DefinedTerm',
117
+ name: formatText(el.seo?.title || el.key?.title || el.title),
118
+ url: withSiteUrl(el.link),
119
+ }));
120
+ }
121
+
100
122
  useJsonLd('jsonld-content', data);
101
123
  }
@@ -161,6 +161,8 @@ export async function useContentSeo(args: {
161
161
  ? 'article'
162
162
  : args.contentTypePath.type,
163
163
  lastmod: args.lastmod,
164
+ keyElements: args.snippets?.filter((snippet) => !!snippet.key),
165
+ breadcrumbs: args.breadcrumbs,
164
166
  });
165
167
 
166
168
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "erudit",
3
- "version": "4.3.3",
3
+ "version": "4.3.4-dev.2",
4
4
  "type": "module",
5
5
  "description": "🤓 CMS for perfect educational sites.",
6
6
  "license": "MIT",
@@ -24,9 +24,9 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@erudit-js/cli": "4.3.3",
28
- "@erudit-js/core": "4.3.3",
29
- "@erudit-js/prose": "4.3.3",
27
+ "@erudit-js/cli": "4.3.4-dev.2",
28
+ "@erudit-js/core": "4.3.4-dev.2",
29
+ "@erudit-js/prose": "4.3.4-dev.2",
30
30
  "@floating-ui/vue": "^1.1.11",
31
31
  "@resvg/resvg-js": "^2.6.2",
32
32
  "@tailwindcss/vite": "^4.2.1",
@@ -43,7 +43,7 @@
43
43
  "nuxt": "4.4.2",
44
44
  "nuxt-my-icons": "1.2.2",
45
45
  "perfect-debounce": "^2.1.0",
46
- "satori": "^0.25.0",
46
+ "satori": "^0.26.0",
47
47
  "sharp": "^0.34.5",
48
48
  "tailwindcss": "^4.2.1",
49
49
  "ts-xor": "^1.3.0",
@@ -61,6 +61,14 @@ export async function buildServerErudit() {
61
61
  // Watcher
62
62
  //
63
63
 
64
+ const watchedProjectDirs = [
65
+ 'content',
66
+ 'contributors',
67
+ 'cameos',
68
+ 'sponsors',
69
+ 'news',
70
+ ] as const;
71
+
64
72
  export async function tryServerWatchProject() {
65
73
  if (ERUDIT.mode === 'static') {
66
74
  return;
@@ -93,39 +101,14 @@ export async function tryServerWatchProject() {
93
101
  }
94
102
  }, 300);
95
103
 
96
- function isWatched(path: string) {
97
- if (path.startsWith(ERUDIT.paths.project('content') + '/')) {
98
- return true;
99
- }
100
-
101
- if (path.startsWith(ERUDIT.paths.project('contributors') + '/')) {
102
- return true;
103
- }
104
-
105
- if (path.startsWith(ERUDIT.paths.project('cameos') + '/')) {
106
- return true;
107
- }
108
-
109
- if (path.startsWith(ERUDIT.paths.project('sponsors') + '/')) {
110
- return true;
111
- }
112
-
113
- if (path.startsWith(ERUDIT.paths.project('news') + '/')) {
114
- return true;
115
- }
116
- }
117
-
118
- const watcher = chokidar.watch(ERUDIT.paths.project(), {
119
- ignoreInitial: true,
120
- });
104
+ const watcher = chokidar.watch(
105
+ watchedProjectDirs.map((dir) => ERUDIT.paths.project(dir)),
106
+ { ignoreInitial: true },
107
+ );
121
108
 
122
109
  watcher.on('all', (_, path) => {
123
110
  path = sn(path);
124
111
 
125
- if (!isWatched(path)) {
126
- return;
127
- }
128
-
129
112
  if (pendingRebuild) {
130
113
  return;
131
114
  }
@@ -23,8 +23,11 @@ export async function pushProblemScript(
23
23
  relativePath = problemScriptSrc;
24
24
  }
25
25
 
26
- await ERUDIT.db.insert(ERUDIT.db.schema.problemScripts).values({
27
- problemScriptSrc: relativePath,
28
- contentFullId,
29
- });
26
+ await ERUDIT.db
27
+ .insert(ERUDIT.db.schema.problemScripts)
28
+ .values({
29
+ problemScriptSrc: relativePath,
30
+ contentFullId,
31
+ })
32
+ .onConflictDoNothing();
30
33
  }
@@ -46,6 +46,7 @@ export const phrases: LanguagePhrases = {
46
46
  flag_secondary: 'Additional',
47
47
  flag_secondary_description:
48
48
  'This is an optional material is for learners who want to dive deeper and gain additional knowledge and context.',
49
+ breadcrumb: 'Breadcrumb',
49
50
  key_elements: 'Key elements',
50
51
  stats: 'Statistics',
51
52
  connections: 'Connections',
@@ -47,6 +47,7 @@ export const phrases: LanguagePhrases = {
47
47
  flag_secondary: 'Дополнение',
48
48
  flag_secondary_description:
49
49
  'Это дополнительный материал для тех, кто хочет глубже погрузиться в предмет и получить дополнительные знания и контекст.',
50
+ breadcrumb: 'Путь',
50
51
  key_elements: 'Ключевые элементы',
51
52
  stats: 'Статистика',
52
53
  connections: 'Связи',
@@ -52,6 +52,7 @@ export type LanguagePhrases = Phrases<{
52
52
  flag_advanced_description: string;
53
53
  flag_secondary: string;
54
54
  flag_secondary_description: string;
55
+ breadcrumb: string;
55
56
  key_elements: string;
56
57
  stats: string;
57
58
  connections: string;