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 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.58",
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",
@@ -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
- forceUpdate: forceUpdateMatch ? forceUpdateMatch[1] === 'true' : false
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 (forceUpdate || isCI) {
259
- console.log(colorize('\n🤖 Automatic update enabled. Updating...', 'cyan'));
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
- if (isCI) {
263
- console.log(colorize('\n❌ Auto-update failed in CI/CD environment. Build cancelled.', 'red'));
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 (!isCI && isCritical) {
273
- console.log(colorize('\n🚨 CRITICAL UPDATE: Automatic update will start in 10 seconds...', 'red'));
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❌ Critical update failed! Please update manually:', 'red'));
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
- if (!isCI) {
293
- const prompt = isCritical ?
294
- colorize('\n🚨 Update now? Critical fixes included! (Y/n): ', 'red') :
295
- colorize('\n🔄 Would you like to update now? (Y/n): ', 'bold');
296
- process.stdout.write(prompt);
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 (shouldUpdate) {
302
- const success = await performUpdate();
303
- if (!success) {
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('/') && !imagePath.includes('authors/')) {
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'; // Генерируем прямо в 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(`Папка ${dir} не существует`);
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
- // Исключаем PWA иконки и служебные файлы
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(`🔄 Обрабатываем для сборки: ${currentRelativePath}`);
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
- if (!fs.existsSync(originalOutputPath)) {
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(`❌ Ошибка при создании ${outputPath}:`, err.message);
87
+ console.error(`❌ Error creating ${outputPath}:`, err.message);
81
88
  } else {
82
- console.log(`✅ Создан: ${path.relative('./dist', outputPath)}`);
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
- // Убеждаемся, что dist существует
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 src={authorImg} alt={dict.ui?.authorAvatar || 'Author avatar'} class="w-8 h-8 rounded-full" width="32" height="32" decoding="async" />
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 src={authorImg} alt={dict.ui?.authorAvatar || 'Author avatar'} class="w-8 h-8 rounded-full" width="32" height="32" decoding="async" />
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={src || '/img/default/autor_default.webp'} alt={alt} decoding="async" class="w-full h-full object-cover" />
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
- 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);
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 flex-col items-end gap-1 w-[74px] h-[32px]">
254
- <span class={`flex items-center gap-1 text-[12px] text-right ${isBrandDate ? 'text-[var(--brand-color)]' : 'text-[var(--text-muted)]'}`}>
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-[18px] leading-[100%] text-[var(--brand-color)] flex-none">
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-2 flex-none">
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-[12px] leading-[15px] text-[var(--text-heading)] hover:text-[var(--brand-color)] transition-all duration-300 ease-in-out hover:translate-x-1 transform"
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
@@ -36,7 +36,8 @@
36
36
  "tags": {
37
37
  "title": "Kategorien",
38
38
  "description": "Alle Artikel nach Kategorie",
39
- "blogRubrics": "Blog-Kategorien"
39
+ "blogRubrics": "Blog-Kategorien",
40
+ "moreTags": "Weitere Tags"
40
41
  },
41
42
  "index": {
42
43
  "title": "<Blog>",
package/src/i18n/en.json CHANGED
@@ -36,7 +36,8 @@
36
36
  "tags": {
37
37
  "title": "Categories",
38
38
  "description": "All articles by category",
39
- "blogRubrics": "Blog categories"
39
+ "blogRubrics": "Blog categories",
40
+ "moreTags": "More tags"
40
41
  },
41
42
  "index": {
42
43
  "title": "<Blog>",
package/src/i18n/es.json CHANGED
@@ -36,7 +36,8 @@
36
36
  "tags": {
37
37
  "title": "Categorías",
38
38
  "description": "Todos los artículos por categoría",
39
- "blogRubrics": "Categorías del blog"
39
+ "blogRubrics": "Categorías del blog",
40
+ "moreTags": "Más etiquetas"
40
41
  },
41
42
  "index": {
42
43
  "title": "<Blog>",
package/src/i18n/fr.json CHANGED
@@ -36,7 +36,8 @@
36
36
  "tags": {
37
37
  "title": "Catégories",
38
38
  "description": "Tous les articles par catégorie",
39
- "blogRubrics": "Catégories du blog"
39
+ "blogRubrics": "Catégories du blog",
40
+ "moreTags": "Plus de tags"
40
41
  },
41
42
  "index": {
42
43
  "title": "<Blog>",
package/src/i18n/ja.json CHANGED
@@ -36,7 +36,8 @@
36
36
  "tags": {
37
37
  "title": "カテゴリ",
38
38
  "description": "カテゴリごとの記事一覧",
39
- "blogRubrics": "ブログカテゴリ"
39
+ "blogRubrics": "ブログカテゴリ",
40
+ "moreTags": "その他のタグ"
40
41
  },
41
42
  "index": {
42
43
  "title": "<ブログ>",
package/src/i18n/pt.json CHANGED
@@ -36,7 +36,8 @@
36
36
  "tags": {
37
37
  "title": "Categorias",
38
38
  "description": "Todos os artigos por categoria",
39
- "blogRubrics": "Categorias do blog"
39
+ "blogRubrics": "Categorias do blog",
40
+ "moreTags": "Mais tags"
40
41
  },
41
42
  "index": {
42
43
  "title": "<Blog>",
package/src/i18n/ru.json CHANGED
@@ -33,7 +33,8 @@
33
33
  "tags": {
34
34
  "title": "Рубрики",
35
35
  "description": "Все статьи по рубрикам",
36
- "blogRubrics": "Рубрики блога"
36
+ "blogRubrics": "Рубрики блога",
37
+ "moreTags": "Дополнительные теги"
37
38
  },
38
39
  "index": {
39
40
  "title": "<Блог>",
package/src/i18n/zh.json CHANGED
@@ -36,7 +36,8 @@
36
36
  "tags": {
37
37
  "title": "分类",
38
38
  "description": "按类别查看所有文章",
39
- "blogRubrics": "博客分类"
39
+ "blogRubrics": "博客分类",
40
+ "moreTags": "更多标签"
40
41
  },
41
42
  "index": {
42
43
  "title": "<博客>",
@@ -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
+ }