core-maugli 1.2.60 → 1.2.62

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.
@@ -0,0 +1,25 @@
1
+ // astro-image-resize.mjs - Astro integration for image processing and previews
2
+ import { execSync } from 'child_process';
3
+
4
+ export default function imageResize() {
5
+ return {
6
+ name: 'image-resize',
7
+ hooks: {
8
+ 'astro:build:start': () => {
9
+ console.log('🖼️ Starting image processing for build...');
10
+ try {
11
+ execSync('node scripts/resize-for-build.cjs', { stdio: 'inherit' });
12
+ } catch (error) {
13
+ console.error('❌ Error during image resizing:', error.message);
14
+ }
15
+
16
+ console.log('🎭 Starting preview generation for build...');
17
+ try {
18
+ execSync('BUILD_MODE=1 node scripts/generate-previews.js', { stdio: 'inherit' });
19
+ } catch (error) {
20
+ console.error('❌ Error during preview generation:', error.message);
21
+ }
22
+ }
23
+ }
24
+ };
25
+ }
@@ -0,0 +1,160 @@
1
+ import mdx from '@astrojs/mdx';
2
+ import sitemap from '@astrojs/sitemap';
3
+ import tailwindcss from '@tailwindcss/vite';
4
+ import { defineConfig } from 'astro/config';
5
+ import remarkSlug from 'remark-slug';
6
+ import { imagetools } from 'vite-imagetools';
7
+ import { VitePWA } from 'vite-plugin-pwa';
8
+ import imageResize from './astro-image-resize.mjs';
9
+ import { maugliConfig } from './src/config/maugli.config';
10
+ import siteConfig from './src/data/site-config';
11
+ import customSlugify from './src/utils/remark-slugify';
12
+
13
+ export const pwaOptions = {
14
+ registerType: 'autoUpdate',
15
+ includeAssets: ['favicon.svg', 'favicon.ico', 'robots.txt', 'apple-touch-icon.png'],
16
+ manifest: {
17
+ name: "Maugli Blog",
18
+ short_name: "Maugli",
19
+ start_url: "/",
20
+ display: "standalone",
21
+ background_color: maugliConfig.pwa?.backgroundColor ?? '#ffffff',
22
+ theme_color: maugliConfig.pwa?.themeColor ?? '#0cbf11',
23
+ icons: maugliConfig.pwa?.icons ?? [
24
+ {
25
+ src: "/icon-192.png",
26
+ sizes: "192x192",
27
+ type: "image/png",
28
+ purpose: "any maskable",
29
+ },
30
+ {
31
+ src: "/icon-512.png",
32
+ sizes: "512x512",
33
+ type: "image/png",
34
+ },
35
+ ],
36
+ },
37
+ workbox: {
38
+ navigateFallback: '/index.html',
39
+ cleanupOutdatedCaches: true,
40
+ navigateFallbackDenylist: [/^\/api\//],
41
+ clientsClaim: true,
42
+ globPatterns: ['**/*.{js,css,html,png,jpg,jpeg,webp,svg}'],
43
+ runtimeCaching: [
44
+ {
45
+ urlPattern: ({ request }) => request.destination === 'image',
46
+ handler: 'CacheFirst',
47
+ options: {
48
+ cacheName: 'images-cache',
49
+ expiration: {
50
+ maxEntries: 50,
51
+ maxAgeSeconds: 30 * 24 * 60 * 60 // 30 дней
52
+ }
53
+ }
54
+ },
55
+ {
56
+ urlPattern: ({ request }) => request.destination === 'font',
57
+ handler: 'CacheFirst',
58
+ options: {
59
+ cacheName: 'fonts-cache',
60
+ expiration: {
61
+ maxEntries: 20,
62
+ maxAgeSeconds: 365 * 24 * 60 * 60 // 1 год
63
+ }
64
+ }
65
+ }
66
+ ]
67
+ },
68
+ devOptions: {
69
+ enabled: true, // чтобы работал в деве
70
+ type: 'module',
71
+ }
72
+ };
73
+
74
+ // https://astro.build/config
75
+ export default defineConfig({
76
+ site: siteConfig.website,
77
+ image: {
78
+ service: {
79
+ entrypoint: 'astro/assets/services/sharp',
80
+ config: {
81
+ limitInputPixels: false,
82
+ // Aggressive optimization for better performance
83
+ jpeg: { quality: 75, progressive: true },
84
+ webp: { quality: 75, effort: 6 },
85
+ avif: { quality: 65, effort: 6 },
86
+ png: { quality: 75, compressionLevel: 9 },
87
+ }
88
+ }
89
+ },
90
+ integrations: [
91
+ mdx(),
92
+ sitemap(),
93
+ imageResize()
94
+ ],
95
+ vite: {
96
+ plugins: [
97
+ tailwindcss(),
98
+ imagetools({
99
+ // Aggressive image optimization
100
+ defaultDirectives: () => {
101
+ return new URLSearchParams({
102
+ format: 'webp',
103
+ quality: '75',
104
+ progressive: 'true',
105
+ // Enable compression
106
+ effort: '6'
107
+ });
108
+ },
109
+ // Additional formats for fallback
110
+ formats: ['webp', 'avif'],
111
+ // Disable for development to speed up build
112
+ disabled: process.env.NODE_ENV === 'development'
113
+ }),
114
+ VitePWA(pwaOptions)
115
+ ],
116
+ build: {
117
+ cssCodeSplit: true,
118
+ minify: 'esbuild',
119
+ target: 'es2020',
120
+ rollupOptions: {
121
+ output: {
122
+ // Separate CSS chunks for better caching
123
+ assetFileNames: (assetInfo) => {
124
+ if (assetInfo.name && assetInfo.name.endsWith('.css')) {
125
+ return 'assets/css/[name].[hash][extname]';
126
+ }
127
+ if (assetInfo.name && /\.(png|jpe?g|svg|gif|webp|avif)$/.test(assetInfo.name)) {
128
+ return 'assets/img/[name].[hash][extname]';
129
+ }
130
+ return 'assets/[name].[hash][extname]';
131
+ },
132
+ chunkFileNames: 'assets/js/[name].[hash].js',
133
+ manualChunks: {
134
+ // Split vendor code for better caching - removed astro from manual chunks
135
+ }
136
+ },
137
+ // Remove problematic external configuration
138
+ },
139
+ // Additional optimization settings
140
+ reportCompressedSize: false, // Faster build
141
+ chunkSizeWarningLimit: 1000
142
+ },
143
+ css: {
144
+ // Optimize CSS processing
145
+ preprocessorOptions: {
146
+ scss: {
147
+ // Additional SCSS options if needed
148
+ }
149
+ }
150
+ },
151
+ optimizeDeps: {
152
+ // Improve dev performance - astro should not be excluded
153
+ }
154
+ },
155
+ markdown: {
156
+ remarkPlugins: [
157
+ [remarkSlug, { slug: customSlugify }]
158
+ ]
159
+ }
160
+ });
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.60",
5
+ "version": "1.2.62",
6
6
  "license": "GPL-3.0-or-later OR Commercial",
7
7
  "repository": {
8
8
  "type": "git",
@@ -76,6 +76,9 @@
76
76
  },
77
77
  "files": [
78
78
  "src",
79
+ "resize-all.cjs",
80
+ "astro-image-resize.mjs",
81
+ "typograf-batch.js",
79
82
  "public/favicon.svg",
80
83
  "public/icon-192.png",
81
84
  "public/icon-512.png",
@@ -89,6 +92,9 @@
89
92
  "public/blackbox*.webp",
90
93
  "scripts",
91
94
  "bin",
95
+ "astro.config.mjs",
96
+ "tsconfig.json",
97
+ "vite.config.js",
92
98
  ".gitignore",
93
99
  "netlify.toml"
94
100
  ],
package/resize-all.cjs ADDED
@@ -0,0 +1,79 @@
1
+ // resize-all.cjs - рекурсивный ресайз изображений
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const sharp = require('sharp');
5
+
6
+ // Размеры для генерации
7
+ const sizes = [400, 800, 1200];
8
+
9
+ const inputDir = './public';
10
+ const processedFiles = new Set(); // Отслеживаем обработанные файлы
11
+
12
+ // Рекурсивная функция для обхода папок
13
+ function processDirectory(dir) {
14
+ if (!fs.existsSync(dir)) {
15
+ console.log(`Папка ${dir} не существует`);
16
+ return;
17
+ }
18
+
19
+ const items = fs.readdirSync(dir);
20
+
21
+ items.forEach(item => {
22
+ const itemPath = path.join(dir, item);
23
+ const stat = fs.statSync(itemPath);
24
+
25
+ if (stat.isDirectory()) {
26
+ // Рекурсивно обрабатываем подпапки
27
+ processDirectory(itemPath);
28
+ } else if (stat.isFile()) {
29
+ const ext = path.extname(item).toLowerCase();
30
+ const baseName = path.basename(item, ext);
31
+
32
+ // Проверяем, что это изображение и не содержит размер в названии
33
+ if (['.jpg', '.jpeg', '.png', '.webp'].includes(ext)) {
34
+ // Исключаем PWA иконки и служебные файлы
35
+ const excludePatterns = [
36
+ 'icon-192', 'icon-512', // PWA иконки
37
+ 'favicon', // Фавиконки
38
+ 'logo', // Логотипы (часто SVG, но на всякий случай)
39
+ 'manifest' // Файлы манифеста
40
+ ];
41
+
42
+ const shouldExclude = excludePatterns.some(pattern => baseName.includes(pattern));
43
+
44
+ // Пропускаем файлы, которые уже содержат размер (например, image-400.webp, image-800-800.webp)
45
+ // Улучшенная проверка: пропускаем файлы с -400, -800, -1200 в любом месте названия
46
+ const hasResizeSuffix = sizes.some(size => baseName.includes(`-${size}`));
47
+
48
+ if (!hasResizeSuffix && !shouldExclude && !processedFiles.has(itemPath)) {
49
+ processedFiles.add(itemPath);
50
+
51
+ console.log(`Обрабатываем: ${itemPath}`);
52
+
53
+ sizes.forEach(width => {
54
+ const outputPath = path.join(path.dirname(itemPath), `${baseName}-${width}${ext}`);
55
+
56
+ // Проверяем, что файл еще не существует
57
+ if (!fs.existsSync(outputPath)) {
58
+ sharp(itemPath)
59
+ .resize(width)
60
+ .toFile(outputPath, (err) => {
61
+ if (err) {
62
+ console.error(`Ошибка при создании ${outputPath}:`, err.message);
63
+ } else {
64
+ console.log(`✅ Создан: ${path.relative('./public', outputPath)}`);
65
+ }
66
+ });
67
+ } else {
68
+ console.log(`⏭️ Пропускаем (уже существует): ${path.relative('./public', outputPath)}`);
69
+ }
70
+ });
71
+ }
72
+ }
73
+ }
74
+ });
75
+ }
76
+
77
+ console.log('🔄 Начинаем ресайз всех изображений...');
78
+ processDirectory(inputDir);
79
+ console.log('✅ Обработка завершена!');
@@ -38,20 +38,35 @@ async function getMaugliConfig() {
38
38
  try {
39
39
  const configPath = path.join(process.cwd(), 'src/config/maugli.config.ts');
40
40
  if (!fs.existsSync(configPath)) {
41
+ console.log(colorize('⚠️ maugli.config.ts not found at src/config/maugli.config.ts', 'yellow'));
41
42
  return null;
42
43
  }
43
44
 
44
45
  // Простое чтение конфига через регулярные выражения
45
46
  const configContent = fs.readFileSync(configPath, 'utf8');
46
- const forceUpdateMatch = configContent.match(/automation:\s*{[^}]*?forceUpdate:\s*(true|false)/s);
47
+ console.log(colorize('🔍 Reading maugli.config.ts...', 'cyan'));
48
+
49
+ // Ищем forceUpdate в automation секции
50
+ const automationMatch = configContent.match(/automation\s*:\s*{([^}]+)}/s);
51
+ if (!automationMatch) {
52
+ console.log(colorize('⚠️ automation section not found in config', 'yellow'));
53
+ return null;
54
+ }
55
+
56
+ const automationSection = automationMatch[1];
57
+ const forceUpdateMatch = automationSection.match(/forceUpdate\s*:\s*(true|false)/);
58
+
59
+ const forceUpdate = forceUpdateMatch ? forceUpdateMatch[1] === 'true' : false;
60
+
61
+ console.log(colorize(`📋 Config found - forceUpdate: ${forceUpdate}`, 'cyan'));
47
62
 
48
63
  return {
49
64
  automation: {
50
- forceUpdate: forceUpdateMatch ? forceUpdateMatch[1] === 'true' : false
65
+ forceUpdate: forceUpdate
51
66
  }
52
67
  };
53
68
  } catch (error) {
54
- console.warn(colorize('⚠️ Could not read maugli.config.ts', 'yellow'));
69
+ console.warn(colorize('⚠️ Could not read maugli.config.ts: ' + error.message, 'yellow'));
55
70
  return null;
56
71
  }
57
72
  }
@@ -257,6 +272,11 @@ async function main() {
257
272
  // Check forceUpdate setting from maugli.config.ts
258
273
  const forceUpdate = maugliConfig?.automation?.forceUpdate || false;
259
274
 
275
+ console.log(colorize(`\n🔧 Configuration check:`, 'cyan'));
276
+ console.log(colorize(` • maugli.config.ts found: ${maugliConfig ? 'Yes' : 'No'}`, 'white'));
277
+ console.log(colorize(` • forceUpdate setting: ${forceUpdate}`, 'white'));
278
+ console.log(colorize(` • CI/CD detected: ${isCI}`, 'white'));
279
+
260
280
  if (isCI) {
261
281
  console.log(colorize('\n🤖 CI/CD environment detected. Updating automatically...', 'cyan'));
262
282
  const success = await performUpdate();
package/src/i18n/ru.json CHANGED
@@ -34,7 +34,7 @@
34
34
  "title": "Рубрики",
35
35
  "description": "Все статьи по рубрикам",
36
36
  "blogRubrics": "Рубрики блога",
37
- "moreTags": "Дополнительные теги"
37
+ "moreTags": "Теги"
38
38
  },
39
39
  "index": {
40
40
  "title": "<Блог>",
@@ -1,9 +1,9 @@
1
1
  // src/utils/image-utils.ts - Утилиты для работы с изображениями
2
2
 
3
3
  /**
4
- * Определяет правильный путь к изображению в зависимости от типа контента
5
- * Все пользовательские изображения должны находиться в /img/page-images/
6
- * Системные файлы остаются в /img/default/ и /img/examples/
4
+ * Определяет правильный путь к изображению
5
+ * Все пользовательские изображения находятся в /img/ и подпапках
6
+ * Автоматическая оптимизация применяется ко всем изображениям в public/img/
7
7
  */
8
8
  export function getImagePath(imageName: string, contentType?: 'blog' | 'author' | 'product' | 'project' | 'tag'): string {
9
9
  // Если путь уже абсолютный (начинается с /), возвращаем как есть
@@ -11,30 +11,26 @@ export function getImagePath(imageName: string, contentType?: 'blog' | 'author'
11
11
  return imageName;
12
12
  }
13
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}`;
14
+ // Все изображения идут в /img/
15
+ return `/img/${imageName}`;
21
16
  }
22
17
 
23
18
  /**
24
19
  * Получает путь к изображению для конкретного типа контента
25
- * Добавляет префикс типа если его нет
20
+ * Все файлы просто в /img/ без подпапок
26
21
  */
27
22
  export function getContentImagePath(slug: string, contentType: 'blog' | 'author' | 'product' | 'project' | 'tag', extension: string = '.webp'): string {
28
23
  const fileName = `${contentType}_${slug}${extension}`;
29
- return getImagePath(fileName);
24
+ return `/img/${fileName}`;
30
25
  }
31
26
 
32
27
  /**
33
28
  * Получает путь к превью изображению
29
+ * Все файлы просто в /img/ без подпапок
34
30
  */
35
31
  export function getPreviewImagePath(slug: string, contentType: 'blog' | 'author' | 'product' | 'project' | 'tag', extension: string = '.webp'): string {
36
32
  const fileName = `previews_${contentType}_${slug}${extension}`;
37
- return getImagePath(fileName);
33
+ return `/img/${fileName}`;
38
34
  }
39
35
 
40
36
  /**
@@ -54,22 +50,12 @@ export function getDefaultImagePath(contentType: 'blog' | 'author' | 'product' |
54
50
 
55
51
  /**
56
52
  * Конвертирует старые пути к новой структуре
57
- * Для обратной совместимости
53
+ * Все изображения теперь просто в /img/ и подпапках
58
54
  */
59
55
  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
- }
56
+ // Убираем page-images из пути - теперь всё просто в /img/
57
+ if (imagePath.includes('/img/page-images/')) {
58
+ return imagePath.replace('/img/page-images/', '/img/');
73
59
  }
74
60
 
75
61
  return imagePath;
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "astro/tsconfigs/strict",
3
+ "include": [".astro/types.d.ts", "**/*"],
4
+ "exclude": ["dist"],
5
+ "compilerOptions": {
6
+ "strictNullChecks": true
7
+ }
8
+ }
@@ -0,0 +1,49 @@
1
+ import Typograf from 'typograf';
2
+ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs';
3
+ import yaml from 'js-yaml';
4
+
5
+ const tp = new Typograf({ locale: ['ru', 'en-US'] });
6
+ const dir = './src/content/blog';
7
+ const cacheFile = './.typograf-cache.json';
8
+
9
+ let cache = {};
10
+ if (existsSync(cacheFile)) {
11
+ cache = JSON.parse(readFileSync(cacheFile, 'utf8'));
12
+ }
13
+
14
+ let cacheUpdated = false;
15
+
16
+ readdirSync(dir)
17
+ .filter(f => f.endsWith('.md'))
18
+ .forEach(f => {
19
+ const file = dir + '/' + f;
20
+ const stats = statSync(file);
21
+ const mtime = stats.mtimeMs;
22
+
23
+ // Если не изменялся — скипаем
24
+ if (cache[f] === mtime) return;
25
+
26
+ const data = readFileSync(file, 'utf8');
27
+ const parts = data.split('---');
28
+ if (parts.length < 3) return;
29
+ let fm = yaml.load(parts[1]);
30
+ if (fm.title) fm.title = tp.execute(fm.title);
31
+ if (fm.description) fm.description = tp.execute(fm.description);
32
+
33
+ const newData = [
34
+ '---',
35
+ yaml.dump(fm).trim(),
36
+ '---',
37
+ tp.execute(parts.slice(2).join('---'))
38
+ ].join('\n');
39
+
40
+ writeFileSync(file, newData);
41
+ cache[f] = mtime;
42
+ cacheUpdated = true;
43
+ console.log(`Typografed: ${file}`);
44
+ });
45
+
46
+ // Сохраняем кеш только если были изменения
47
+ if (cacheUpdated) {
48
+ writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
49
+ }
package/vite.config.js ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'vite';
2
+ import { imagetools } from 'vite-imagetools';
3
+ import { VitePWA } from 'vite-plugin-pwa';
4
+ import { pwaOptions } from './astro.config.mjs';
5
+
6
+ export default defineConfig({
7
+ plugins: [
8
+ imagetools(),
9
+ VitePWA(pwaOptions)
10
+ ]
11
+ });