core-maugli 1.2.58 → 1.2.60
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/package.json +6 -5
- package/scripts/check-version.js +20 -62
- package/scripts/generate-previews-build.js +104 -0
- package/scripts/generate-previews.js +8 -9
- package/scripts/resize-for-build.cjs +42 -28
- package/src/components/ArticleMeta.astro +41 -2
- package/src/components/Avatar.astro +26 -1
- package/src/components/BaseHead.astro +12 -12
- package/src/components/RubricCard.astro +2 -2
- package/src/components/TableOfContents.astro +3 -3
- package/src/i18n/de.json +2 -1
- package/src/i18n/en.json +2 -1
- package/src/i18n/es.json +2 -1
- package/src/i18n/fr.json +2 -1
- package/src/i18n/ja.json +2 -1
- package/src/i18n/pt.json +2 -1
- package/src/i18n/ru.json +2 -1
- package/src/i18n/zh.json +2 -1
- package/src/utils/image-utils.ts +76 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "core-maugli",
|
|
3
3
|
"description": "Astro & Tailwind CSS blog theme for Maugli.",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "1.2.
|
|
5
|
+
"version": "1.2.60",
|
|
6
6
|
"license": "GPL-3.0-or-later OR Commercial",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
21
|
"typograf": "node typograf-batch.js",
|
|
22
|
-
"dev": "node scripts/generate-previews.js && astro dev",
|
|
23
|
-
"prestart": "node scripts/generate-previews.js",
|
|
22
|
+
"dev": "node resize-all.cjs && node scripts/generate-previews.js && astro dev",
|
|
23
|
+
"prestart": "node resize-all.cjs && node scripts/generate-previews.js",
|
|
24
24
|
"start": "astro dev",
|
|
25
25
|
"build": "node scripts/check-version.js && node scripts/flatten-images.cjs && node scripts/optimize-images.cjs && node typograf-batch.js && node scripts/verify-assets.js && node scripts/generate-previews.js && astro build",
|
|
26
|
-
"build:fast": "node typograf-batch.js && node scripts/verify-assets.js && node scripts/generate-previews.js && astro build",
|
|
26
|
+
"build:fast": "node resize-all.cjs && node typograf-batch.js && node scripts/verify-assets.js && node scripts/generate-previews.js && astro build",
|
|
27
27
|
"build:no-check": "node scripts/flatten-images.cjs && node scripts/optimize-images.cjs && node typograf-batch.js && node scripts/verify-assets.js && node scripts/generate-previews.js && astro build",
|
|
28
28
|
"optimize": "node scripts/optimize-images.cjs",
|
|
29
29
|
"optimize:squoosh": "node scripts/squoosh-optimize.js",
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"init-netlify": "node scripts/copy-netlify-config.js",
|
|
44
44
|
"set-force-update": "node scripts/set-force-update.js",
|
|
45
45
|
"postinstall": "node scripts/upgrade-config.js && node scripts/setup-user-images.js",
|
|
46
|
-
"generate-previews": "node scripts/generate-previews.js"
|
|
46
|
+
"generate-previews": "node scripts/generate-previews.js",
|
|
47
|
+
"generate-netlify": "node scripts/generate-netlify-config.js"
|
|
47
48
|
},
|
|
48
49
|
"dependencies": {
|
|
49
50
|
"@astrojs/mdx": "^4.3.0",
|
package/scripts/check-version.js
CHANGED
|
@@ -46,7 +46,9 @@ async function getMaugliConfig() {
|
|
|
46
46
|
const forceUpdateMatch = configContent.match(/automation:\s*{[^}]*?forceUpdate:\s*(true|false)/s);
|
|
47
47
|
|
|
48
48
|
return {
|
|
49
|
-
|
|
49
|
+
automation: {
|
|
50
|
+
forceUpdate: forceUpdateMatch ? forceUpdateMatch[1] === 'true' : false
|
|
51
|
+
}
|
|
50
52
|
};
|
|
51
53
|
} catch (error) {
|
|
52
54
|
console.warn(colorize('⚠️ Could not read maugli.config.ts', 'yellow'));
|
|
@@ -253,83 +255,39 @@ async function main() {
|
|
|
253
255
|
!process.stdin.isTTY; // Non-interactive terminal
|
|
254
256
|
|
|
255
257
|
// Check forceUpdate setting from maugli.config.ts
|
|
256
|
-
const forceUpdate = maugliConfig?.forceUpdate || false;
|
|
258
|
+
const forceUpdate = maugliConfig?.automation?.forceUpdate || false;
|
|
257
259
|
|
|
258
|
-
if (
|
|
259
|
-
console.log(colorize('\n🤖
|
|
260
|
+
if (isCI) {
|
|
261
|
+
console.log(colorize('\n🤖 CI/CD environment detected. Updating automatically...', 'cyan'));
|
|
260
262
|
const success = await performUpdate();
|
|
261
263
|
if (!success) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
process.exit(1);
|
|
265
|
-
} else {
|
|
266
|
-
console.log(colorize('\n⚠️ Update failed. Continuing with build...', 'yellow'));
|
|
267
|
-
}
|
|
264
|
+
console.log(colorize('\n❌ Auto-update failed in CI/CD environment. Build cancelled.', 'red'));
|
|
265
|
+
process.exit(1);
|
|
268
266
|
}
|
|
269
267
|
return;
|
|
270
268
|
}
|
|
271
269
|
|
|
272
|
-
if (
|
|
273
|
-
console.log(colorize('\n
|
|
274
|
-
console.log(colorize('Press Ctrl+C to cancel and update manually.', 'yellow'));
|
|
275
|
-
|
|
276
|
-
// 10-секундный таймер для критических обновлений
|
|
277
|
-
for (let i = 10; i > 0; i--) {
|
|
278
|
-
process.stdout.write(colorize(`\r⏰ Updating in ${i} seconds... `, 'yellow'));
|
|
279
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
280
|
-
}
|
|
281
|
-
console.log(colorize('\n🔄 Starting automatic update...', 'cyan'));
|
|
282
|
-
|
|
270
|
+
if (forceUpdate) {
|
|
271
|
+
console.log(colorize('\n🤖 Force update enabled in config. Updating automatically...', 'cyan'));
|
|
283
272
|
const success = await performUpdate();
|
|
284
273
|
if (!success) {
|
|
285
|
-
console.log(colorize('\n❌
|
|
286
|
-
console.log(colorize('npm update core-maugli', 'white'));
|
|
287
|
-
process.exit(1);
|
|
274
|
+
console.log(colorize('\n❌ Auto-update failed. Continuing with build...', 'yellow'));
|
|
288
275
|
}
|
|
289
276
|
return;
|
|
290
277
|
}
|
|
291
278
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const shouldUpdate = await promptUpdate();
|
|
279
|
+
// If forceUpdate is false, show update notification without prompts
|
|
280
|
+
console.log(colorize('\n💡 To update core-maugli, run:', 'cyan'));
|
|
281
|
+
console.log(colorize(' npm run update', 'white'));
|
|
282
|
+
console.log(colorize(' # или', 'gray'));
|
|
283
|
+
console.log(colorize(' npm update core-maugli', 'white'));
|
|
300
284
|
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (isCI) {
|
|
305
|
-
console.log(colorize('\n❌ Auto-update failed in CI/CD environment. Build cancelled.', 'red'));
|
|
306
|
-
process.exit(1);
|
|
307
|
-
} else {
|
|
308
|
-
console.log(colorize('\n⚠️ Update failed. You can continue with the build, but some features may not work correctly.', 'yellow'));
|
|
309
|
-
process.stdout.write(colorize('Continue anyway? (Y/n): ', 'yellow'));
|
|
310
|
-
const continueAnyway = await promptUpdate();
|
|
311
|
-
if (!continueAnyway) {
|
|
312
|
-
console.log(colorize('\n❌ Build cancelled. Please update manually and try again.', 'red'));
|
|
313
|
-
process.exit(1);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
} else {
|
|
318
|
-
// Пользователь отказался от обновления
|
|
319
|
-
if (isCritical) {
|
|
320
|
-
console.log(colorize('\n🚨 WARNING: Building with critically outdated version!', 'red'));
|
|
321
|
-
console.log(colorize('This may cause build failures or security issues.', 'red'));
|
|
322
|
-
console.log(colorize('Please update as soon as possible: npm update core-maugli', 'yellow'));
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (isCI) {
|
|
326
|
-
console.log(colorize('\n⚠️ CI/CD auto-update disabled. Continuing with build...', 'yellow'));
|
|
327
|
-
} else {
|
|
328
|
-
console.log(colorize('\n⚠️ Continuing without update. Some features may not work correctly.', 'yellow'));
|
|
329
|
-
console.log(colorize('💡 You can update later by running: npm run update-all-blogs', 'cyan'));
|
|
330
|
-
}
|
|
285
|
+
if (isCritical) {
|
|
286
|
+
console.log(colorize('\n🚨 WARNING: This is a critical update!', 'red'));
|
|
287
|
+
console.log(colorize('Building with this version may cause errors.', 'red'));
|
|
331
288
|
}
|
|
332
289
|
|
|
290
|
+
|
|
333
291
|
console.log(colorize('\n✅ Proceeding with build...\n', 'green'));
|
|
334
292
|
}
|
|
335
293
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
// Универсальное определение корня проекта
|
|
10
|
+
const rootDir = __dirname.includes('node_modules')
|
|
11
|
+
? path.join(__dirname, '../../..')
|
|
12
|
+
: path.join(__dirname, '..');
|
|
13
|
+
|
|
14
|
+
// Размеры для разных типов контента
|
|
15
|
+
const blogPreviewWidth = 400;
|
|
16
|
+
const blogPreviewHeight = 210;
|
|
17
|
+
const rubricPreviewWidth = 210;
|
|
18
|
+
const rubricPreviewHeight = 214;
|
|
19
|
+
|
|
20
|
+
// Генерируем в dist вместо public
|
|
21
|
+
const outputDir = path.join(rootDir, 'dist');
|
|
22
|
+
|
|
23
|
+
// Функция для создания превью в dist
|
|
24
|
+
async function createPreviewForBuild(sourcePath, outputPath, width, height) {
|
|
25
|
+
const previewDir = path.dirname(outputPath);
|
|
26
|
+
if (!fs.existsSync(previewDir)) {
|
|
27
|
+
fs.mkdirSync(previewDir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (fs.existsSync(outputPath)) {
|
|
31
|
+
return; // Превью уже существует
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await sharp(sourcePath)
|
|
36
|
+
.resize(width, height, { fit: 'cover' })
|
|
37
|
+
.toFile(outputPath);
|
|
38
|
+
console.log(`✅ Превью создано: ${path.relative(rootDir, outputPath)}`);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error(`❌ Ошибка при создании превью ${outputPath}:`, error.message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Функция для обработки папки
|
|
45
|
+
async function processDirectory(sourceDir, outputSubDir) {
|
|
46
|
+
if (!fs.existsSync(sourceDir)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const items = fs.readdirSync(sourceDir);
|
|
51
|
+
|
|
52
|
+
for (const item of items) {
|
|
53
|
+
const sourcePath = path.join(sourceDir, item);
|
|
54
|
+
const stat = fs.statSync(sourcePath);
|
|
55
|
+
|
|
56
|
+
if (stat.isDirectory()) {
|
|
57
|
+
if (item === 'previews') continue; // Пропускаем папки превью
|
|
58
|
+
await processDirectory(sourcePath, path.join(outputSubDir, item));
|
|
59
|
+
} else if (item.match(/\.(webp|jpg|jpeg|png)$/i)) {
|
|
60
|
+
const ext = path.extname(item);
|
|
61
|
+
const name = path.basename(item, ext);
|
|
62
|
+
|
|
63
|
+
// Пропускаем файлы, которые уже содержат размер
|
|
64
|
+
const hasResizeSuffix = [400, 800, 1200].some(size => name.includes(`-${size}`));
|
|
65
|
+
if (hasResizeSuffix) continue;
|
|
66
|
+
|
|
67
|
+
// Определяем размер превью
|
|
68
|
+
let previewWidth, previewHeight;
|
|
69
|
+
if (sourcePath.includes('/img/default/') && (name.includes('rubric') || name.includes('tag'))) {
|
|
70
|
+
previewWidth = rubricPreviewWidth;
|
|
71
|
+
previewHeight = rubricPreviewHeight;
|
|
72
|
+
} else {
|
|
73
|
+
previewWidth = blogPreviewWidth;
|
|
74
|
+
previewHeight = blogPreviewHeight;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Создаем превью
|
|
78
|
+
const outputDir = path.join(rootDir, 'dist', outputSubDir, 'previews');
|
|
79
|
+
const outputPath = path.join(outputDir, `${name}${ext}`);
|
|
80
|
+
|
|
81
|
+
console.log(`🎭 Создаем превью для сборки: ${name}`);
|
|
82
|
+
await createPreviewForBuild(sourcePath, outputPath, previewWidth, previewHeight);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Основная функция
|
|
88
|
+
async function generatePreviewsForBuild() {
|
|
89
|
+
console.log('🚀 Начинаем генерацию превью для сборки...');
|
|
90
|
+
|
|
91
|
+
// Обрабатываем системные папки
|
|
92
|
+
await processDirectory(path.join(rootDir, 'public/img/default'), 'img/default');
|
|
93
|
+
await processDirectory(path.join(rootDir, 'public/img/examples'), 'img/examples');
|
|
94
|
+
|
|
95
|
+
// Обрабатываем пользовательские изображения, если они есть
|
|
96
|
+
const pageImagesDir = path.join(rootDir, 'public/img/page-images');
|
|
97
|
+
if (fs.existsSync(pageImagesDir)) {
|
|
98
|
+
await processDirectory(pageImagesDir, 'img/page-images');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log('✅ Генерация превью для сборки завершена!');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
generatePreviewsForBuild().catch(console.error);
|
|
@@ -16,6 +16,8 @@ const blogPreviewWidth = 400;
|
|
|
16
16
|
const blogPreviewHeight = 210;
|
|
17
17
|
const rubricPreviewWidth = 210; // Увеличенный размер для качества на retina дисплеях (105px * 2)
|
|
18
18
|
const rubricPreviewHeight = 214; // Увеличенный размер для качества на retina дисплеях (107px * 2)
|
|
19
|
+
const authorPreviewWidth = 192; // Квадратный размер для аватарок
|
|
20
|
+
const authorPreviewHeight = 192;
|
|
19
21
|
|
|
20
22
|
// Функция для извлечения путей изображений из markdown файлов
|
|
21
23
|
function extractImagePaths() {
|
|
@@ -62,7 +64,7 @@ function extractImagePaths() {
|
|
|
62
64
|
} else {
|
|
63
65
|
imagePath = match.match(/src\s*=\s*['"]*([^'">\s]+)['"]*/)?.[1];
|
|
64
66
|
}
|
|
65
|
-
if (imagePath && !imagePath.startsWith('http') && imagePath.includes('/')
|
|
67
|
+
if (imagePath && !imagePath.startsWith('http') && imagePath.includes('/')) {
|
|
66
68
|
imagePaths.add(imagePath);
|
|
67
69
|
}
|
|
68
70
|
});
|
|
@@ -73,21 +75,16 @@ function extractImagePaths() {
|
|
|
73
75
|
|
|
74
76
|
scanDirectory(contentDir);
|
|
75
77
|
|
|
76
|
-
// Также добавляем изображения из public/img/examples/
|
|
78
|
+
// Также добавляем изображения из public/img/examples/
|
|
77
79
|
const examplesDir = path.join(rootDir, 'public/img/examples');
|
|
78
80
|
if (fs.existsSync(examplesDir)) {
|
|
79
81
|
function addExampleImages(dir, relativePath = '') {
|
|
80
|
-
// Пропускаем папку authors - аватары не нужно обрабатывать
|
|
81
|
-
if (relativePath.includes('authors/')) return;
|
|
82
|
-
|
|
83
82
|
const items = fs.readdirSync(dir);
|
|
84
83
|
for (const item of items) {
|
|
85
84
|
const itemPath = path.join(dir, item);
|
|
86
85
|
const stat = fs.statSync(itemPath);
|
|
87
86
|
|
|
88
87
|
if (stat.isDirectory()) {
|
|
89
|
-
// Пропускаем папку authors
|
|
90
|
-
if (item === 'authors') continue;
|
|
91
88
|
addExampleImages(itemPath, `${relativePath}${item}/`);
|
|
92
89
|
} else if (item.match(/\.(webp|jpg|jpeg|png)$/i) && !dir.includes('previews')) {
|
|
93
90
|
imagePaths.add(`/img/examples/${relativePath}${item}`);
|
|
@@ -103,8 +100,6 @@ function extractImagePaths() {
|
|
|
103
100
|
const items = fs.readdirSync(defaultDir);
|
|
104
101
|
for (const item of items) {
|
|
105
102
|
if (item.match(/\.(webp|jpg|jpeg|png)$/i)) {
|
|
106
|
-
// Исключаем изображения авторов и рубрик
|
|
107
|
-
if (item.includes('autor') || item.includes('author') || item.includes('rubric')) continue;
|
|
108
103
|
imagePaths.add(`/img/default/${item}`);
|
|
109
104
|
}
|
|
110
105
|
}
|
|
@@ -160,6 +155,10 @@ async function createPreview(imagePath) {
|
|
|
160
155
|
previewWidth = rubricPreviewWidth;
|
|
161
156
|
previewHeight = rubricPreviewHeight;
|
|
162
157
|
console.log(`Creating rubric preview (${previewWidth}x${previewHeight}): ${name}`);
|
|
158
|
+
} else if (imagePath.includes('author') || name.includes('autor')) {
|
|
159
|
+
previewWidth = authorPreviewWidth;
|
|
160
|
+
previewHeight = authorPreviewHeight;
|
|
161
|
+
console.log(`Creating author preview (${previewWidth}x${previewHeight}): ${name}`);
|
|
163
162
|
} else {
|
|
164
163
|
previewWidth = blogPreviewWidth;
|
|
165
164
|
previewHeight = blogPreviewHeight;
|
|
@@ -1,26 +1,37 @@
|
|
|
1
|
-
// resize-for-build.cjs -
|
|
1
|
+
// resize-for-build.cjs - Generate resized images for build in dist/
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const sharp = require('sharp');
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// Sizes to generate
|
|
7
7
|
const sizes = [400, 800, 1200];
|
|
8
8
|
|
|
9
9
|
const inputDir = './public';
|
|
10
|
-
const outputDir = './dist';
|
|
10
|
+
const outputDir = './dist';
|
|
11
11
|
const processedFiles = new Set();
|
|
12
12
|
|
|
13
|
-
//
|
|
13
|
+
// Function to create directory if it doesn't exist
|
|
14
14
|
function ensureDir(dir) {
|
|
15
15
|
if (!fs.existsSync(dir)) {
|
|
16
16
|
fs.mkdirSync(dir, { recursive: true });
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
//
|
|
20
|
+
// Copy file if it doesn't exist
|
|
21
|
+
function copyIfNotExists(src, dest) {
|
|
22
|
+
if (!fs.existsSync(dest)) {
|
|
23
|
+
ensureDir(path.dirname(dest));
|
|
24
|
+
fs.copyFileSync(src, dest);
|
|
25
|
+
console.log(`📋 Copied: ${path.relative('./dist', dest)}`);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Recursive function to process directories
|
|
21
32
|
function processDirectory(dir, relativePath = '') {
|
|
22
33
|
if (!fs.existsSync(dir)) {
|
|
23
|
-
console.log(
|
|
34
|
+
console.log(`Directory ${dir} does not exist`);
|
|
24
35
|
return;
|
|
25
36
|
}
|
|
26
37
|
|
|
@@ -32,67 +43,70 @@ function processDirectory(dir, relativePath = '') {
|
|
|
32
43
|
const currentRelativePath = path.join(relativePath, item);
|
|
33
44
|
|
|
34
45
|
if (stat.isDirectory()) {
|
|
35
|
-
//
|
|
46
|
+
// Recursively process subdirectories
|
|
36
47
|
processDirectory(itemPath, currentRelativePath);
|
|
37
48
|
} else if (stat.isFile()) {
|
|
38
49
|
const ext = path.extname(item).toLowerCase();
|
|
39
50
|
const baseName = path.basename(item, ext);
|
|
40
51
|
|
|
41
|
-
//
|
|
52
|
+
// Check if it's an image and doesn't contain size in name
|
|
42
53
|
if (['.jpg', '.jpeg', '.png', '.webp'].includes(ext)) {
|
|
43
|
-
//
|
|
54
|
+
// Exclude PWA icons and system files
|
|
44
55
|
const excludePatterns = [
|
|
45
|
-
'icon-192', 'icon-512', // PWA
|
|
46
|
-
'favicon', //
|
|
47
|
-
'logo', //
|
|
48
|
-
'manifest' //
|
|
56
|
+
'icon-192', 'icon-512', // PWA icons
|
|
57
|
+
'favicon', // Favicons
|
|
58
|
+
'logo', // Logos
|
|
59
|
+
'manifest' // Manifest files
|
|
49
60
|
];
|
|
50
61
|
|
|
51
62
|
const shouldExclude = excludePatterns.some(pattern => baseName.includes(pattern));
|
|
52
63
|
|
|
53
|
-
//
|
|
64
|
+
// Skip files that already contain size
|
|
54
65
|
const hasResizeSuffix = sizes.some(size => baseName.includes(`-${size}`));
|
|
55
66
|
|
|
56
67
|
if (!hasResizeSuffix && !shouldExclude && !processedFiles.has(itemPath)) {
|
|
57
68
|
processedFiles.add(itemPath);
|
|
58
69
|
|
|
59
|
-
console.log(`🔄
|
|
70
|
+
console.log(`🔄 Processing for build: ${currentRelativePath}`);
|
|
60
71
|
|
|
61
|
-
//
|
|
72
|
+
// First copy original
|
|
62
73
|
const outputDirPath = path.join(outputDir, relativePath);
|
|
63
|
-
ensureDir(outputDirPath);
|
|
64
|
-
|
|
65
74
|
const originalOutputPath = path.join(outputDirPath, item);
|
|
66
|
-
|
|
67
|
-
fs.copyFileSync(itemPath, originalOutputPath);
|
|
68
|
-
console.log(`📋 Скопирован оригинал: ${path.relative('./dist', originalOutputPath)}`);
|
|
69
|
-
}
|
|
75
|
+
copyIfNotExists(itemPath, originalOutputPath);
|
|
70
76
|
|
|
71
|
-
//
|
|
77
|
+
// Generate resized versions
|
|
72
78
|
sizes.forEach(width => {
|
|
73
79
|
const outputPath = path.join(outputDirPath, `${baseName}-${width}${ext}`);
|
|
74
80
|
|
|
75
81
|
if (!fs.existsSync(outputPath)) {
|
|
82
|
+
ensureDir(path.dirname(outputPath));
|
|
76
83
|
sharp(itemPath)
|
|
77
84
|
.resize(width)
|
|
78
85
|
.toFile(outputPath, (err) => {
|
|
79
86
|
if (err) {
|
|
80
|
-
console.error(`❌
|
|
87
|
+
console.error(`❌ Error creating ${outputPath}:`, err.message);
|
|
81
88
|
} else {
|
|
82
|
-
console.log(`✅
|
|
89
|
+
console.log(`✅ Created: ${path.relative('./dist', outputPath)}`);
|
|
83
90
|
}
|
|
84
91
|
});
|
|
92
|
+
} else {
|
|
93
|
+
console.log(`⏭️ Skipped (exists): ${path.relative('./dist', outputPath)}`);
|
|
85
94
|
}
|
|
86
95
|
});
|
|
87
96
|
}
|
|
97
|
+
} else {
|
|
98
|
+
// Copy non-image files as is
|
|
99
|
+
const outputDirPath = path.join(outputDir, relativePath);
|
|
100
|
+
const outputPath = path.join(outputDirPath, item);
|
|
101
|
+
copyIfNotExists(itemPath, outputPath);
|
|
88
102
|
}
|
|
89
103
|
}
|
|
90
104
|
});
|
|
91
105
|
}
|
|
92
106
|
|
|
93
|
-
//
|
|
107
|
+
// Ensure dist exists
|
|
94
108
|
ensureDir(outputDir);
|
|
95
109
|
|
|
96
|
-
console.log('🚀
|
|
110
|
+
console.log('🚀 Starting image processing for build...');
|
|
97
111
|
processDirectory(inputDir);
|
|
98
|
-
console.log('✅
|
|
112
|
+
console.log('✅ Image processing completed!');
|
|
@@ -58,6 +58,31 @@ if (!authorData) {
|
|
|
58
58
|
}
|
|
59
59
|
let authorName = authorData.data.name;
|
|
60
60
|
let authorImg = authorData.data.avatar || '/img/default/autor_default.webp';
|
|
61
|
+
|
|
62
|
+
// Функция для получения пути к превью изображения автора
|
|
63
|
+
function getAuthorPreviewSrc(imageSrc: string): string {
|
|
64
|
+
if (!imageSrc) return '/img/default/previews/autor_default.webp';
|
|
65
|
+
|
|
66
|
+
// Если это дефолтное изображение автора
|
|
67
|
+
if (imageSrc.includes('/img/default/autor_default.webp')) {
|
|
68
|
+
return '/img/default/previews/autor_default.webp';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Для изображений авторов из examples
|
|
72
|
+
if (imageSrc.includes('/img/examples/authors/')) {
|
|
73
|
+
const fileName = imageSrc.split('/').pop();
|
|
74
|
+
if (fileName) {
|
|
75
|
+
const baseName = fileName.replace(/\.(webp|jpg|jpeg|png)$/i, '');
|
|
76
|
+
const extension = fileName.match(/\.(webp|jpg|jpeg|png)$/i)?.[0] || '.webp';
|
|
77
|
+
return imageSrc.replace(fileName, `previews/${baseName}${extension}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Fallback к оригинальному изображению если превью нет
|
|
82
|
+
return imageSrc;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const authorImgPreview = getAuthorPreviewSrc(authorImg);
|
|
61
86
|
---
|
|
62
87
|
|
|
63
88
|
<!-- Метаинформация сверху -->
|
|
@@ -72,12 +97,26 @@ let authorImg = authorData.data.avatar || '/img/default/autor_default.webp';
|
|
|
72
97
|
data-astro-reload
|
|
73
98
|
style="text-decoration: none; color: inherit; z-index: 10; position: relative;"
|
|
74
99
|
>
|
|
75
|
-
<img
|
|
100
|
+
<img
|
|
101
|
+
src={authorImgPreview}
|
|
102
|
+
alt={dict.ui?.authorAvatar || 'Author avatar'}
|
|
103
|
+
class="w-8 h-8 rounded-full"
|
|
104
|
+
width="32"
|
|
105
|
+
height="32"
|
|
106
|
+
decoding="async"
|
|
107
|
+
/>
|
|
76
108
|
<span class="font-medium hover:text-[var(--brand-color)] transition-colors duration-200">{authorName}</span>
|
|
77
109
|
</a>
|
|
78
110
|
) : (
|
|
79
111
|
<>
|
|
80
|
-
<img
|
|
112
|
+
<img
|
|
113
|
+
src={authorImgPreview}
|
|
114
|
+
alt={dict.ui?.authorAvatar || 'Author avatar'}
|
|
115
|
+
class="w-8 h-8 rounded-full"
|
|
116
|
+
width="32"
|
|
117
|
+
height="32"
|
|
118
|
+
decoding="async"
|
|
119
|
+
/>
|
|
81
120
|
<span class="font-medium">{authorName}</span>
|
|
82
121
|
</>
|
|
83
122
|
)
|
|
@@ -10,10 +10,35 @@ const { src, alt, size = '100px', class: className } = Astro.props;
|
|
|
10
10
|
|
|
11
11
|
// Определяем размер - может быть строкой (например, "80px", "5rem") или числом (пиксели)
|
|
12
12
|
const avatarSize = typeof size === 'number' ? `${size}px` : size;
|
|
13
|
+
|
|
14
|
+
// Функция для получения пути к превью изображения автора
|
|
15
|
+
function getAuthorPreviewSrc(imageSrc?: string): string {
|
|
16
|
+
if (!imageSrc) return '/img/default/previews/autor_default.webp';
|
|
17
|
+
|
|
18
|
+
// Если это дефолтное изображение автора
|
|
19
|
+
if (imageSrc.includes('/img/default/autor_default.webp')) {
|
|
20
|
+
return '/img/default/previews/autor_default.webp';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Для изображений авторов из examples
|
|
24
|
+
if (imageSrc.includes('/img/examples/authors/')) {
|
|
25
|
+
const fileName = imageSrc.split('/').pop();
|
|
26
|
+
if (fileName) {
|
|
27
|
+
const baseName = fileName.replace(/\.(webp|jpg|jpeg|png)$/i, '');
|
|
28
|
+
const extension = fileName.match(/\.(webp|jpg|jpeg|png)$/i)?.[0] || '.webp';
|
|
29
|
+
return imageSrc.replace(fileName, `previews/${baseName}${extension}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fallback к оригинальному изображению если превью нет
|
|
34
|
+
return imageSrc;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const previewSrc = getAuthorPreviewSrc(src);
|
|
13
38
|
---
|
|
14
39
|
|
|
15
40
|
<div class:list={['avatar-container', className]} style={`width: ${avatarSize}; height: ${avatarSize};`}>
|
|
16
|
-
<img src={
|
|
41
|
+
<img src={previewSrc} alt={alt} decoding="async" class="w-full h-full object-cover" />
|
|
17
42
|
</div>
|
|
18
43
|
|
|
19
44
|
<style>
|
|
@@ -98,8 +98,8 @@ let defaultAuthorName = defaultAuthor.data.name;
|
|
|
98
98
|
--bg-main: #ffffff;
|
|
99
99
|
--bg-muted: rgba(237, 241, 247, 0.621);
|
|
100
100
|
--border-main: rgba(17, 28, 44, 0.13);
|
|
101
|
-
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
102
|
-
--font-serif: 'Geologica', Georgia, 'Times New Roman', serif;
|
|
101
|
+
--font-sans: 'Inter Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
102
|
+
--font-serif: 'Geologica Variable', Georgia, 'Times New Roman', serif;
|
|
103
103
|
}
|
|
104
104
|
html.dark {
|
|
105
105
|
--brand-color-rgb: 13, 211, 18;
|
|
@@ -171,15 +171,15 @@ let defaultAuthorName = defaultAuthor.data.name;
|
|
|
171
171
|
|
|
172
172
|
<!-- PWA Registration -->
|
|
173
173
|
<script>
|
|
174
|
-
// PWA Service Worker Registration
|
|
175
|
-
if ('serviceWorker' in navigator) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
174
|
+
// PWA Service Worker Registration
|
|
175
|
+
if ('serviceWorker' in navigator) {
|
|
176
|
+
window.addEventListener('load', async () => {
|
|
177
|
+
try {
|
|
178
|
+
const registration = await navigator.serviceWorker.register('/sw.js');
|
|
179
|
+
console.log('SW registered: ', registration);
|
|
180
|
+
} catch (registrationError) {
|
|
181
|
+
console.log('SW registration failed: ', registrationError);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
182
184
|
}
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
185
|
</script>
|
|
@@ -250,8 +250,8 @@ if (import.meta.env.DEV) {
|
|
|
250
250
|
</span>
|
|
251
251
|
)
|
|
252
252
|
}
|
|
253
|
-
<div class="flex
|
|
254
|
-
<span class={`flex items-center gap-1 text-[12px]
|
|
253
|
+
<div class="flex items-center gap-1 absolute bottom-0 right-0">
|
|
254
|
+
<span class={`flex items-center gap-1 text-[12px] whitespace-nowrap ${isBrandDate ? 'text-[var(--brand-color)]' : 'text-[var(--text-muted)]'}`}>
|
|
255
255
|
<svg
|
|
256
256
|
width="16"
|
|
257
257
|
height="16"
|
|
@@ -30,16 +30,16 @@ const { headings, title = dict.tableOfContents?.title || fallbackDict.tableOfCon
|
|
|
30
30
|
id="table-of-contents"
|
|
31
31
|
aria-label={dict.tableOfContents.ariaLabel || fallbackDict.tableOfContents.ariaLabel || 'Contents'}
|
|
32
32
|
>
|
|
33
|
-
<h3 class="font-serif font-bold text-[
|
|
33
|
+
<h3 class="font-serif font-bold text-[20px] leading-[110%] text-[var(--brand-color)] flex-none">
|
|
34
34
|
{title}
|
|
35
35
|
</h3>
|
|
36
36
|
|
|
37
|
-
<div class="flex flex-col gap-
|
|
37
|
+
<div class="flex flex-col gap-3 flex-none">
|
|
38
38
|
{
|
|
39
39
|
headings.map((heading) => (
|
|
40
40
|
<a
|
|
41
41
|
href={`#${heading.id}`}
|
|
42
|
-
class="toc-link font-sans font-normal text-[
|
|
42
|
+
class="toc-link font-sans font-normal text-[14px] leading-[18px] text-[var(--text-heading)] hover:text-[var(--brand-color)] transition-all duration-300 ease-in-out hover:translate-x-1 transform"
|
|
43
43
|
data-target={heading.id}
|
|
44
44
|
>
|
|
45
45
|
{heading.title}
|
package/src/i18n/de.json
CHANGED
package/src/i18n/en.json
CHANGED
package/src/i18n/es.json
CHANGED
package/src/i18n/fr.json
CHANGED
package/src/i18n/ja.json
CHANGED
package/src/i18n/pt.json
CHANGED
package/src/i18n/ru.json
CHANGED
package/src/i18n/zh.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/utils/image-utils.ts - Утилиты для работы с изображениями
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Определяет правильный путь к изображению в зависимости от типа контента
|
|
5
|
+
* Все пользовательские изображения должны находиться в /img/page-images/
|
|
6
|
+
* Системные файлы остаются в /img/default/ и /img/examples/
|
|
7
|
+
*/
|
|
8
|
+
export function getImagePath(imageName: string, contentType?: 'blog' | 'author' | 'product' | 'project' | 'tag'): string {
|
|
9
|
+
// Если путь уже абсолютный (начинается с /), возвращаем как есть
|
|
10
|
+
if (imageName.startsWith('/')) {
|
|
11
|
+
return imageName;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Если это системные папки, не меняем путь
|
|
15
|
+
if (imageName.startsWith('default/') || imageName.startsWith('examples/')) {
|
|
16
|
+
return `/img/${imageName}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Все остальные изображения ищем в page-images
|
|
20
|
+
return `/img/page-images/${imageName}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Получает путь к изображению для конкретного типа контента
|
|
25
|
+
* Добавляет префикс типа если его нет
|
|
26
|
+
*/
|
|
27
|
+
export function getContentImagePath(slug: string, contentType: 'blog' | 'author' | 'product' | 'project' | 'tag', extension: string = '.webp'): string {
|
|
28
|
+
const fileName = `${contentType}_${slug}${extension}`;
|
|
29
|
+
return getImagePath(fileName);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Получает путь к превью изображению
|
|
34
|
+
*/
|
|
35
|
+
export function getPreviewImagePath(slug: string, contentType: 'blog' | 'author' | 'product' | 'project' | 'tag', extension: string = '.webp'): string {
|
|
36
|
+
const fileName = `previews_${contentType}_${slug}${extension}`;
|
|
37
|
+
return getImagePath(fileName);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Получает fallback путь для изображения в зависимости от типа контента
|
|
42
|
+
*/
|
|
43
|
+
export function getDefaultImagePath(contentType: 'blog' | 'author' | 'product' | 'project' | 'tag'): string {
|
|
44
|
+
const defaultImages = {
|
|
45
|
+
blog: '/img/default/blog_default.webp',
|
|
46
|
+
author: '/img/default/autor_default.webp',
|
|
47
|
+
product: '/img/default/product_default.webp',
|
|
48
|
+
project: '/img/default/project_default.webp',
|
|
49
|
+
tag: '/img/default/rubric_default.webp'
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return defaultImages[contentType];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Конвертирует старые пути к новой структуре
|
|
57
|
+
* Для обратной совместимости
|
|
58
|
+
*/
|
|
59
|
+
export function convertLegacyImagePath(imagePath: string): string {
|
|
60
|
+
// Конвертируем старые пути в новые
|
|
61
|
+
const conversions = [
|
|
62
|
+
{ from: '/img/blog/', to: '/img/page-images/' },
|
|
63
|
+
{ from: '/img/authors/', to: '/img/page-images/' },
|
|
64
|
+
{ from: '/img/products/', to: '/img/page-images/' },
|
|
65
|
+
{ from: '/img/projects/', to: '/img/page-images/' },
|
|
66
|
+
{ from: '/img/uploads/', to: '/img/page-images/' }
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
for (const conversion of conversions) {
|
|
70
|
+
if (imagePath.startsWith(conversion.from)) {
|
|
71
|
+
return imagePath.replace(conversion.from, conversion.to);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return imagePath;
|
|
76
|
+
}
|