erudit 4.3.2 → 4.3.3-dev.1
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/app/app.vue +1 -0
- package/app/components/main/MainBreadcrumbs.vue +27 -19
- package/app/components/main/MainKeyLinks.vue +13 -7
- package/app/components/main/MainSubTitle.vue +4 -3
- package/app/components/main/MainTopicPartPage.vue +3 -1
- package/app/components/main/connections/MainConnections.vue +47 -40
- package/app/components/main/contentStats/MainContentStats.vue +16 -17
- package/app/composables/analytics.ts +15 -8
- package/app/composables/favicon.ts +49 -26
- package/app/composables/jsonLd.ts +123 -0
- package/app/composables/lastmod.ts +6 -0
- package/app/composables/og.ts +23 -0
- package/app/pages/book/[...bookId].vue +3 -1
- package/app/pages/group/[...groupId].vue +3 -1
- package/app/pages/page/[...pageId].vue +3 -1
- package/app/plugins/prerender.server.ts +1 -0
- package/modules/erudit/setup/runtimeConfig.ts +39 -3
- package/nuxt.config.ts +1 -1
- package/package.json +11 -10
- package/server/api/main/content/[...contentTypePath].ts +2 -0
- package/server/api/prerender/favicons.ts +31 -0
- package/server/erudit/build.ts +2 -0
- package/server/erudit/content/lastmod.ts +206 -0
- package/server/erudit/content/repository/lastmod.ts +12 -0
- package/server/erudit/db/schema/content.ts +1 -0
- package/server/erudit/favicon/convertToPng.ts +48 -0
- package/server/erudit/favicon/loadSource.ts +139 -0
- package/server/erudit/favicon/shared.ts +48 -0
- package/server/erudit/language/list/en.ts +1 -0
- package/server/erudit/language/list/ru.ts +1 -0
- package/server/erudit/repository.ts +2 -0
- package/server/routes/favicon/[...path].ts +89 -0
- package/server/routes/sitemap.xml.ts +19 -10
- package/shared/types/language.ts +1 -0
- package/shared/types/mainContent.ts +1 -0
- package/shared/types/runtimeConfig.ts +4 -1
- package/app/composables/lastChanged.ts +0 -61
|
@@ -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: 'Связи',
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
import { getContentSeo } from './content/repository/seo';
|
|
38
38
|
import { getContentElementSnippets } from './content/repository/elementSnippets';
|
|
39
39
|
import { isContentHidden } from './content/repository/hidden';
|
|
40
|
+
import { getContentLastmod } from './content/repository/lastmod';
|
|
40
41
|
import { serverRawToProse } from './prose/repository/rawToProse';
|
|
41
42
|
|
|
42
43
|
export const repository = {
|
|
@@ -77,6 +78,7 @@ export const repository = {
|
|
|
77
78
|
updateSchemaCounts: updateContentSchemaCounts,
|
|
78
79
|
contentContributions: getContentContributions,
|
|
79
80
|
seo: getContentSeo,
|
|
81
|
+
lastmod: getContentLastmod,
|
|
80
82
|
},
|
|
81
83
|
prose: {
|
|
82
84
|
fromRaw: serverRawToProse,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { loadFaviconSource } from '#layers/erudit/server/erudit/favicon/loadSource';
|
|
2
|
+
import { convertFaviconToPng } from '#layers/erudit/server/erudit/favicon/convertToPng';
|
|
3
|
+
import {
|
|
4
|
+
FAVICON_SIZES,
|
|
5
|
+
FAVICON_KEYS,
|
|
6
|
+
getFaviconHref,
|
|
7
|
+
type FaviconSize,
|
|
8
|
+
type FaviconKey,
|
|
9
|
+
} from '#layers/erudit/server/erudit/favicon/shared';
|
|
10
|
+
|
|
11
|
+
export default defineEventHandler(async (event) => {
|
|
12
|
+
const rawPath = event.context.params?.path;
|
|
13
|
+
if (!rawPath) {
|
|
14
|
+
throw createError({
|
|
15
|
+
statusCode: 404,
|
|
16
|
+
statusMessage: 'Invalid favicon path',
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const slashIdx = rawPath.indexOf('/');
|
|
21
|
+
if (slashIdx === -1) {
|
|
22
|
+
throw createError({
|
|
23
|
+
statusCode: 404,
|
|
24
|
+
statusMessage: 'Invalid favicon path',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const key = rawPath.slice(0, slashIdx);
|
|
29
|
+
const file = rawPath.slice(slashIdx + 1);
|
|
30
|
+
|
|
31
|
+
if (!FAVICON_KEYS.includes(key as FaviconKey)) {
|
|
32
|
+
throw createError({
|
|
33
|
+
statusCode: 404,
|
|
34
|
+
statusMessage: `Unknown favicon key: ${key}`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const href = getFaviconHref(key);
|
|
39
|
+
if (!href) {
|
|
40
|
+
throw createError({
|
|
41
|
+
statusCode: 404,
|
|
42
|
+
statusMessage: `No favicon configured for: ${key}`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const source = await loadFaviconSource(href);
|
|
47
|
+
if (!source) {
|
|
48
|
+
throw createError({
|
|
49
|
+
statusCode: 404,
|
|
50
|
+
statusMessage: 'Failed to load favicon source',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Source route: {key}/source.{ext}
|
|
55
|
+
if (file.startsWith('source.')) {
|
|
56
|
+
setHeader(event, 'Content-Type', source.mime);
|
|
57
|
+
setHeader(event, 'Cache-Control', 'public, max-age=86400, s-maxage=86400');
|
|
58
|
+
return source.buffer;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// PNG route: {key}/{size}.png
|
|
62
|
+
if (!file.endsWith('.png')) {
|
|
63
|
+
throw createError({
|
|
64
|
+
statusCode: 404,
|
|
65
|
+
statusMessage: 'Invalid favicon path',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const size = Number(file.slice(0, -4)) as FaviconSize;
|
|
70
|
+
|
|
71
|
+
if (!FAVICON_SIZES.includes(size)) {
|
|
72
|
+
throw createError({
|
|
73
|
+
statusCode: 404,
|
|
74
|
+
statusMessage: `Invalid favicon size: ${file}`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (source.width !== undefined && source.width < size) {
|
|
79
|
+
throw createError({
|
|
80
|
+
statusCode: 404,
|
|
81
|
+
statusMessage: `Favicon source too small (${source.width}px) for size ${size}px`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const png = await convertFaviconToPng(key, source, size);
|
|
86
|
+
setHeader(event, 'Content-Type', 'image/png');
|
|
87
|
+
setHeader(event, 'Cache-Control', 'public, max-age=86400, s-maxage=86400');
|
|
88
|
+
return png;
|
|
89
|
+
});
|
|
@@ -2,7 +2,14 @@ import { sn } from 'unslash';
|
|
|
2
2
|
|
|
3
3
|
export default defineEventHandler(async (event) => {
|
|
4
4
|
const urls = new Set<string>();
|
|
5
|
-
|
|
5
|
+
const urlLastmod = new Map<string, string>();
|
|
6
|
+
|
|
7
|
+
function addUrl(url: string, lastmod?: string) {
|
|
8
|
+
urls.add(url);
|
|
9
|
+
if (lastmod) urlLastmod.set(url, lastmod);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
addUrl(PAGES.index);
|
|
6
13
|
|
|
7
14
|
//
|
|
8
15
|
// Contributors
|
|
@@ -34,7 +41,7 @@ export default defineEventHandler(async (event) => {
|
|
|
34
41
|
|
|
35
42
|
{
|
|
36
43
|
const dbContentItems = await ERUDIT.db.query.content.findMany({
|
|
37
|
-
columns: { fullId: true },
|
|
44
|
+
columns: { fullId: true, lastmod: true },
|
|
38
45
|
});
|
|
39
46
|
|
|
40
47
|
for (const dbContentItem of dbContentItems) {
|
|
@@ -45,14 +52,15 @@ export default defineEventHandler(async (event) => {
|
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
const contentNode = ERUDIT.contentNav.getNodeOrThrow(fullId);
|
|
55
|
+
const lastmod = dbContentItem.lastmod ?? undefined;
|
|
48
56
|
|
|
49
57
|
if (contentNode.type === 'topic') {
|
|
50
58
|
const parts = await ERUDIT.repository.content.topicParts(fullId);
|
|
51
59
|
for (const part of parts) {
|
|
52
|
-
|
|
60
|
+
addUrl(PAGES.topic(part, contentNode.shortId), lastmod);
|
|
53
61
|
}
|
|
54
62
|
} else {
|
|
55
|
-
|
|
63
|
+
addUrl(PAGES[contentNode.type](fullId), lastmod);
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
const elementSnippets =
|
|
@@ -60,7 +68,7 @@ export default defineEventHandler(async (event) => {
|
|
|
60
68
|
|
|
61
69
|
for (const snippet of elementSnippets || []) {
|
|
62
70
|
if (snippet.seo) {
|
|
63
|
-
|
|
71
|
+
addUrl(snippet.link, lastmod);
|
|
64
72
|
}
|
|
65
73
|
}
|
|
66
74
|
}
|
|
@@ -75,11 +83,12 @@ export default defineEventHandler(async (event) => {
|
|
|
75
83
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
76
84
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
77
85
|
${Array.from(urls)
|
|
78
|
-
.map(
|
|
79
|
-
(url)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
.map((url) => {
|
|
87
|
+
const lastmod = urlLastmod.get(url);
|
|
88
|
+
return ` <url>
|
|
89
|
+
<loc>${sn(runtimeConfig.public.siteUrl, url)}</loc>${lastmod ? `\n <lastmod>${lastmod}</lastmod>` : ''}
|
|
90
|
+
</url>`;
|
|
91
|
+
})
|
|
83
92
|
.join('\n')}
|
|
84
93
|
</urlset>`.trim();
|
|
85
94
|
|
package/shared/types/language.ts
CHANGED
|
@@ -13,6 +13,10 @@ export interface EruditRuntimeConfig {
|
|
|
13
13
|
elements: string[];
|
|
14
14
|
countElements: (string | string[])[];
|
|
15
15
|
indexPage?: EruditIndexPage;
|
|
16
|
+
lastmod?: {
|
|
17
|
+
type: 'git' | 'custom';
|
|
18
|
+
scriptPath?: string;
|
|
19
|
+
};
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export interface EruditPublicRuntimeConfig {
|
|
@@ -27,7 +31,6 @@ export interface EruditPublicRuntimeConfig {
|
|
|
27
31
|
ads: boolean;
|
|
28
32
|
fakeApi: {
|
|
29
33
|
repository: boolean;
|
|
30
|
-
lastChanged: boolean | string;
|
|
31
34
|
};
|
|
32
35
|
analytics?: boolean;
|
|
33
36
|
};
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
type LastChangedSource =
|
|
2
|
-
| { type: 'date'; value: string }
|
|
3
|
-
| { type: 'github'; url: string; path: string };
|
|
4
|
-
|
|
5
|
-
function useLastChangedSource(
|
|
6
|
-
contentRelativePath: MaybeRefOrGetter<string | undefined>,
|
|
7
|
-
) {
|
|
8
|
-
return computed((): LastChangedSource | undefined => {
|
|
9
|
-
const path = toValue(contentRelativePath);
|
|
10
|
-
if (!path) return undefined;
|
|
11
|
-
|
|
12
|
-
const debug = ERUDIT.config.debug.fakeApi.lastChanged;
|
|
13
|
-
if (debug === true) return { type: 'date', value: '2024-01-15T12:00:00Z' };
|
|
14
|
-
if (typeof debug === 'string') return { type: 'date', value: debug };
|
|
15
|
-
|
|
16
|
-
const repo = ERUDIT.config.repository;
|
|
17
|
-
if (!repo || repo.type !== 'github') return undefined;
|
|
18
|
-
const parts = repo.name.split('/');
|
|
19
|
-
if (parts.length !== 2) return undefined;
|
|
20
|
-
const [owner, repoName] = parts;
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
type: 'github',
|
|
24
|
-
url: `https://api.github.com/repos/${owner}/${repoName}/commits`,
|
|
25
|
-
path: `content/${path}`,
|
|
26
|
-
};
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function useLastChanged(
|
|
31
|
-
contentRelativePath: MaybeRefOrGetter<string | undefined>,
|
|
32
|
-
) {
|
|
33
|
-
const source = useLastChangedSource(contentRelativePath);
|
|
34
|
-
const date = ref<Date | undefined>(undefined);
|
|
35
|
-
|
|
36
|
-
onMounted(async () => {
|
|
37
|
-
const s = source.value;
|
|
38
|
-
if (!s) return;
|
|
39
|
-
|
|
40
|
-
if (s.type === 'date') {
|
|
41
|
-
date.value = new Date(s.value);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (s.type === 'github') {
|
|
46
|
-
try {
|
|
47
|
-
const data = await $fetch<any[]>(s.url, {
|
|
48
|
-
query: { path: s.path, per_page: 1 },
|
|
49
|
-
responseType: 'json',
|
|
50
|
-
});
|
|
51
|
-
if (Array.isArray(data) && data[0]?.commit?.committer?.date) {
|
|
52
|
-
date.value = new Date(data[0].commit.committer.date);
|
|
53
|
-
}
|
|
54
|
-
} catch {
|
|
55
|
-
// silently ignore API errors
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
return date;
|
|
61
|
-
}
|