core-maugli 1.2.13 → 1.2.16

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/bin/init.js ADDED
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from 'child_process';
4
+ import { cpSync, existsSync, readFileSync, writeFileSync } from 'fs';
5
+ import path from 'path';
6
+ import readline from 'readline';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const templateRoot = path.join(__dirname, '..');
12
+
13
+ function getLanguageCodes() {
14
+ const file = readFileSync(path.join(templateRoot, 'src/i18n/languages.ts'), 'utf8');
15
+ const codes = [];
16
+ const regex = /{\s*code:\s*'([^']+)'/g;
17
+ let match;
18
+ while ((match = regex.exec(file)) !== null) {
19
+ codes.push(match[1]);
20
+ }
21
+ return codes;
22
+ }
23
+
24
+ function promptLang(codes) {
25
+ return new Promise(resolve => {
26
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
27
+ rl.question(`Choose language (${codes.join(', ')}): `, answer => {
28
+ rl.close();
29
+ resolve(codes.includes(answer) ? answer : codes[0]);
30
+ });
31
+ });
32
+ }
33
+
34
+ function promptRepo() {
35
+ return new Promise(resolve => {
36
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
37
+ rl.question('Repository URL: ', answer => {
38
+ rl.close();
39
+ resolve(answer.trim());
40
+ });
41
+ });
42
+ }
43
+
44
+ async function getRepoUrl(targetDir, repoOption) {
45
+ if (repoOption) return repoOption;
46
+ try {
47
+ const url = execSync('git remote get-url origin', {
48
+ cwd: targetDir,
49
+ stdio: ['ignore', 'pipe', 'ignore']
50
+ })
51
+ .toString()
52
+ .trim();
53
+ if (url) return url;
54
+ } catch {
55
+ // ignore
56
+ }
57
+ return await promptRepo();
58
+ }
59
+
60
+ function updateReadme(targetDir, repoUrl) {
61
+ if (!repoUrl) return;
62
+ const readmePath = path.join(targetDir, 'README.md');
63
+ if (!existsSync(readmePath)) return;
64
+ let content = readFileSync(readmePath, 'utf8');
65
+ const pattern = /https:\/\/app\.netlify\.com\/start\/deploy\?repository=[^\)\s]+/;
66
+ content = content.replace(
67
+ pattern,
68
+ `https://app.netlify.com/start/deploy?repository=${repoUrl}`
69
+ );
70
+ writeFileSync(readmePath, content);
71
+ console.log('Updated Netlify link in README.md');
72
+ }
73
+
74
+ function updateConfig(targetDir, lang, repoUrl) {
75
+ const configPath = path.join(targetDir, 'src', 'config', 'maugli.config.ts');
76
+ if (!existsSync(configPath)) return;
77
+ let content = readFileSync(configPath, 'utf8');
78
+ content = content.replace(/defaultLang:\s*'[^']*'/, `defaultLang: '${lang}'`);
79
+ const multiMatch = content.match(/enableMultiLang:\s*(true|false)/);
80
+ const multi = multiMatch ? multiMatch[1] === 'true' : false;
81
+ content = content.replace(/showLangSwitcher:\s*(true|false)/, `showLangSwitcher: ${multi}`);
82
+
83
+ // Update repository URL if provided
84
+ if (repoUrl) {
85
+ content = content.replace(
86
+ /repository:\s*{[^}]*url:\s*'[^']*'/,
87
+ `repository: {\n url: '${repoUrl}'`
88
+ );
89
+ }
90
+
91
+ writeFileSync(configPath, content);
92
+ console.log(`Configured default language to ${lang}`);
93
+ if (repoUrl) {
94
+ console.log(`Configured repository URL to ${repoUrl}`);
95
+ }
96
+ }
97
+
98
+ export default async function init(targetName, langOption, repoOption) {
99
+ const targetDir = targetName ? path.resolve(targetName) : process.cwd();
100
+ const codes = getLanguageCodes();
101
+ const lang = langOption && codes.includes(langOption) ? langOption : await promptLang(codes);
102
+
103
+ function copyItem(item) {
104
+ const src = path.join(templateRoot, item);
105
+ const dest = path.join(targetDir, item);
106
+
107
+ if (!existsSync(src)) {
108
+ console.log(`Skipped ${item} (not found)`);
109
+ return;
110
+ }
111
+
112
+ cpSync(src, dest, { recursive: true });
113
+ console.log(`Copied ${item}`);
114
+ }
115
+
116
+ // Copy package files first so npm install works correctly
117
+ ['package.json', 'package-lock.json'].forEach(file => {
118
+ if (existsSync(path.join(templateRoot, file))) {
119
+ copyItem(file);
120
+ }
121
+ });
122
+
123
+ const items = [
124
+ 'astro.config.mjs',
125
+ 'tsconfig.json',
126
+ 'vite.config.js',
127
+ 'public',
128
+ 'src',
129
+ 'scripts',
130
+ 'typograf-batch.js',
131
+ 'resize-all.cjs',
132
+ 'README.md',
133
+ 'LICENSE'
134
+ ];
135
+ items.forEach(copyItem);
136
+
137
+ const repoUrl = await getRepoUrl(targetDir, repoOption);
138
+ updateReadme(targetDir, repoUrl);
139
+
140
+ // Create essential config files
141
+ const gitignoreContent = `
142
+ # Dependencies
143
+ node_modules/
144
+ .pnpm-debug.log*
145
+
146
+ # Environment
147
+ .env
148
+ .env.local
149
+ .env.production
150
+
151
+ # Build outputs
152
+ dist/
153
+ .astro/
154
+
155
+ # Generated files
156
+ .DS_Store
157
+ .vscode/settings.json
158
+
159
+ # Cache
160
+ .typograf-cache.json
161
+ `;
162
+
163
+ const prettierrcContent = `{
164
+ "semi": true,
165
+ "singleQuote": true,
166
+ "tabWidth": 2,
167
+ "trailingComma": "es5",
168
+ "printWidth": 100,
169
+ "plugins": ["prettier-plugin-tailwindcss"]
170
+ }
171
+ `;
172
+
173
+ writeFileSync(path.join(targetDir, '.gitignore'), gitignoreContent.trim());
174
+ console.log('Created .gitignore');
175
+
176
+ writeFileSync(path.join(targetDir, '.prettierrc'), prettierrcContent);
177
+ console.log('Created .prettierrc');
178
+
179
+ execSync('npm install', { cwd: targetDir, stdio: 'inherit' });
180
+ updateConfig(targetDir, lang, repoUrl);
181
+ }
182
+
183
+ // Если скрипт запускается напрямую
184
+ if (import.meta.url === `file://${process.argv[1]}`) {
185
+ const args = process.argv.slice(2);
186
+ let targetName;
187
+ let lang;
188
+ let repo;
189
+ for (let i = 0; i < args.length; i++) {
190
+ if (args[i] === '--lang' && i + 1 < args.length) {
191
+ lang = args[i + 1];
192
+ i++;
193
+ } else if (args[i] === '--repo' && i + 1 < args.length) {
194
+ repo = args[i + 1];
195
+ i++;
196
+ } else {
197
+ targetName = args[i];
198
+ }
199
+ }
200
+ await init(targetName, lang, repo);
201
+ }
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.13",
5
+ "version": "1.2.16",
6
6
  "license": "GPL-3.0-or-later OR Commercial",
7
7
  "repository": {
8
8
  "type": "git",
@@ -64,7 +64,8 @@
64
64
  "files": [
65
65
  "src",
66
66
  "public",
67
- "scripts"
67
+ "scripts",
68
+ "bin"
68
69
  ],
69
70
  "bin": {
70
71
  "core-maugli": "bin/index.js"
@@ -72,12 +72,12 @@ let authorImg = authorData.data.avatar || '/img/default/autor_default.webp';
72
72
  data-astro-reload
73
73
  style="text-decoration: none; color: inherit; z-index: 10; position: relative;"
74
74
  >
75
- <img src={authorImg} alt={authorName} class="w-8 h-8 rounded-full" />
75
+ <img src={authorImg} alt={authorName} class="w-8 h-8 rounded-full" loading="lazy" decoding="async" />
76
76
  <span class="font-medium hover:text-[var(--brand-color)] transition-colors duration-200">{authorName}</span>
77
77
  </a>
78
78
  ) : (
79
79
  <>
80
- <img src={authorImg} alt={authorName} class="w-8 h-8 rounded-full" />
80
+ <img src={authorImg} alt={authorName} class="w-8 h-8 rounded-full" loading="lazy" decoding="async" />
81
81
  <span class="font-medium">{authorName}</span>
82
82
  </>
83
83
  )
@@ -13,7 +13,13 @@ const avatarSize = typeof size === 'number' ? `${size}px` : size;
13
13
  ---
14
14
 
15
15
  <div class:list={['avatar-container', className]} style={`width: ${avatarSize}; height: ${avatarSize};`}>
16
- <img src={src || '/img/default/autor_default.webp'} alt={alt} loading="lazy" class="w-full h-full object-cover" />
16
+ <img
17
+ src={src || '/img/default/autor_default.webp'}
18
+ alt={alt}
19
+ loading="lazy"
20
+ decoding="async"
21
+ class="w-full h-full object-cover"
22
+ />
17
23
  </div>
18
24
 
19
25
  <style>
@@ -61,7 +61,7 @@ const isTagPage = currentUrl.startsWith('/tags/') && pathParts.length === 1;
61
61
  idx === 0 ? (
62
62
  <a href={crumb.href} class="flex-shrink-0" target="_blank" rel="noopener noreferrer">
63
63
  <picture>
64
- <img src={crumb.icon} alt="Maugli" class="w-9 h-9" />
64
+ <img src={crumb.icon} alt="Maugli" class="w-9 h-9" loading="lazy" decoding="async" />
65
65
  </picture>
66
66
  </a>
67
67
  ) : (
@@ -1,4 +1,7 @@
1
1
  ---
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
2
5
  import { maugliConfig } from '../config/maugli.config';
3
6
  import FormattedDate from './FormattedDate.astro';
4
7
 
@@ -20,8 +23,8 @@ const { href, title, image, seo, publishDate, excerpt, description, headingLevel
20
23
 
21
24
  const TitleTag = headingLevel;
22
25
 
23
- // Определяем изображение для карточки
24
- const cardImage =
26
+ // Определяем базовое изображение для карточки
27
+ const baseImage =
25
28
  seo?.image?.src ||
26
29
  (image && typeof image.src === 'string' && image.src.length > 0 ? image.src : undefined) ||
27
30
  (type === 'blog'
@@ -31,8 +34,38 @@ const cardImage =
31
34
  : type === 'product'
32
35
  ? maugliConfig.defaultProductImage
33
36
  : maugliConfig.seo.defaultImage);
37
+
38
+ // Генерируем адаптивные версии изображений
39
+ function getResponsiveImages(imagePath: string) {
40
+ const basePath = imagePath.replace(/\.(webp|jpg|jpeg|png)$/i, '');
41
+ const extension = imagePath.match(/\.(webp|jpg|jpeg|png)$/i)?.[0] || '.webp';
42
+
43
+ return {
44
+ src: imagePath,
45
+ srcset: [`${basePath}-400${extension} 400w`, `${basePath}-800${extension} 800w`, `${basePath}-1200${extension} 1200w`, `${imagePath} 1200w`].join(', '),
46
+ sizes: '(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 400px'
47
+ };
48
+ }
49
+
50
+ const cardImage = getResponsiveImages(baseImage);
34
51
  const cardImageAlt = seo?.image?.alt || image?.alt || title || 'Изображение';
35
52
 
53
+ // Используем уменьшенное превью, если оно существует
54
+ let previewImage;
55
+ if (cardImage) {
56
+ previewImage = cardImage.replace(/\/([^\/]+)$/, '/previews/$1');
57
+
58
+ const __filename = fileURLToPath(import.meta.url);
59
+ const projectRoot = path.resolve(path.dirname(__filename), '../..');
60
+ const previewFilePath = path.join(projectRoot, 'public', previewImage.replace(/^\//, ''));
61
+
62
+ if (!fs.existsSync(previewFilePath)) {
63
+ previewImage = undefined;
64
+ }
65
+ }
66
+
67
+ const finalImage = previewImage || cardImage;
68
+
36
69
  // Определяем контент для отображения
37
70
  const content = excerpt || description;
38
71
  ---
@@ -48,9 +81,13 @@ const content = excerpt || description;
48
81
  <!-- Изображение -->
49
82
  <div class="w-full aspect-[1200/630] bg-[var(--bg-muted)] overflow-hidden relative">
50
83
  <img
51
- src={cardImage}
84
+ src={cardImage.src}
85
+ srcset={cardImage.srcset}
86
+ sizes={cardImage.sizes}
52
87
  alt={cardImageAlt}
53
88
  loading="lazy"
89
+ width="1200"
90
+ height="630"
54
91
  class="w-full h-full object-cover rounded-custom transition-transform duration-300 group-hover:scale-105"
55
92
  />
56
93
 
@@ -109,7 +109,7 @@ const copyrightYears = yearStart === yearEnd ? `${yearStart}` : `${yearStart}–
109
109
  ]}
110
110
  >
111
111
  <div class="flex flex-row justify-between items-end w-full mt-2">
112
- <img src="/footerlabel.svg" alt="Maugli Label" style="height:32px;width:auto;" />
112
+ <img src="/footerlabel.svg" alt="Maugli Label" style="height:32px;width:auto;" loading="lazy" decoding="async" />
113
113
  {
114
114
  Object.values(maugliConfig.links || {}).some(Boolean) && (
115
115
  <div class="flex flex-row items-center gap-2 ml-4">
@@ -6,7 +6,15 @@ import siteConfig from '../data/site-config';
6
6
  {
7
7
  siteConfig.logo && siteConfig.logo?.src ? (
8
8
  <a href="/">
9
- <img src={siteConfig.logo.src} alt={siteConfig.logo.alt || ''} class="max-h-12" />
9
+ <img
10
+ src={siteConfig.logo.src}
11
+ alt={siteConfig.logo.alt || ''}
12
+ width={siteConfig.logo.width}
13
+ height={siteConfig.logo.height}
14
+ loading="eager"
15
+ decoding="async"
16
+ class="max-h-12"
17
+ />
10
18
  </a>
11
19
  ) : (
12
20
  <a class="font-serif text-2xl leading-tight font-medium text-theme-foreground sm:text-4xl" href="/">
@@ -15,15 +15,37 @@ const { src, alt = '', caption, width = defaultWidth, height, className = '', sr
15
15
  // Вариант aspect-ratio: если есть оба размера, рассчитываем; иначе не пишем!
16
16
  const aspectRatio = width && height ? `${width} / ${height}` : undefined;
17
17
 
18
- let realSrcset = srcset;
19
- if (!srcset && src.includes('default-')) {
20
- realSrcset = `${src.replace('800', '800')} 1x, ${src.replace('800', '1200')} 2x`;
18
+ // Генерируем адаптивные версии изображений
19
+ function getResponsiveImages(imagePath: string) {
20
+ const basePath = imagePath.replace(/\.(webp|jpg|jpeg|png)$/i, '');
21
+ const extension = imagePath.match(/\.(webp|jpg|jpeg|png)$/i)?.[0] || '.webp';
22
+
23
+ return {
24
+ src: imagePath,
25
+ srcset: [`${basePath}-400${extension} 400w`, `${basePath}-800${extension} 800w`, `${basePath}-1200${extension} 1200w`, `${imagePath} 1200w`].join(', '),
26
+ sizes: '(max-width: 768px) 100vw, (max-width: 1024px) 80vw, 1200px'
27
+ };
28
+ }
29
+
30
+ let imageData;
31
+ if (srcset) {
32
+ // Если передан готовый srcset, используем его
33
+ imageData = {
34
+ src: src,
35
+ srcset: srcset,
36
+ sizes: '100vw'
37
+ };
38
+ } else {
39
+ // Генерируем адаптивные изображения
40
+ imageData = getResponsiveImages(src);
21
41
  }
22
42
  ---
23
43
 
24
44
  <figure class={`w-full max-w-[${width}px] mx-auto rounded-custom overflow-hidden bg-[var(--bg-muted)] ${className}`}>
25
45
  <img
26
- src={src}
46
+ src={imageData.src}
47
+ srcset={imageData.srcset}
48
+ sizes={imageData.sizes}
27
49
  alt={alt}
28
50
  width={width}
29
51
  height={height}
@@ -31,8 +53,6 @@ if (!srcset && src.includes('default-')) {
31
53
  class="block rounded-custom object-cover"
32
54
  loading="eager"
33
55
  decoding="async"
34
- srcset={realSrcset}
35
- sizes="100vw"
36
56
  />
37
57
  {caption && <figcaption class="mt-2 text-sm text-[var(--text-main)] opacity-80 text-center px-2">{caption}</figcaption>}
38
58
  </figure>
@@ -1,13 +1,33 @@
1
1
  ---
2
- export let src = '';
3
- export let alt = '';
4
- export let width = undefined;
5
- export let height = undefined;
6
- export let className = '';
7
- export let loading = 'lazy';
8
- export let style = '';
2
+ export interface Props {
3
+ src: string;
4
+ alt?: string;
5
+ width?: number | string;
6
+ height?: number | string;
7
+ className?: string;
8
+ loading?: 'lazy' | 'eager';
9
+ style?: string;
10
+ }
11
+
12
+ const {
13
+ src,
14
+ alt = '',
15
+ width,
16
+ height,
17
+ className = '',
18
+ loading = 'lazy',
19
+ style = ''
20
+ } = Astro.props as Props;
9
21
  ---
10
22
 
11
- export let height = undefined; export let className = ''; export let loading = 'lazy'; export let style = ''; ---
23
+ <img
24
+ src={src}
25
+ alt={alt}
26
+ width={width}
27
+ height={height}
28
+ class={className}
29
+ loading={loading}
30
+ decoding="async"
31
+ style={style}
32
+ />
12
33
 
13
- <img src={src} alt={alt} width={width} height={height} class={className} loading={loading} style={style} />
@@ -24,7 +24,17 @@ const current = availableLanguages.find((l) => l.code === currentLang) || availa
24
24
  class="flag-box flex items-center justify-center w-7 h-7 rounded-[var(--radius-main)] shadow-[var(--shadow-main)]"
25
25
  style="min-width:28px; min-height:28px; max-width:28px; max-height:28px;"
26
26
  >
27
- {current && <img src={current.icon} alt={current.code.toUpperCase()} width="28" height="28" style="pointer-events:none;" />}
27
+ {current && (
28
+ <img
29
+ src={current.icon}
30
+ alt={current.code.toUpperCase()}
31
+ width="28"
32
+ height="28"
33
+ style="pointer-events:none;"
34
+ loading="lazy"
35
+ decoding="async"
36
+ />
37
+ )}
28
38
  </span>
29
39
  <svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
30
40
  <path
@@ -57,7 +67,15 @@ const current = availableLanguages.find((l) => l.code === currentLang) || availa
57
67
  class="flag-box flex items-center justify-center w-7 h-7 rounded-[var(--radius-main)] shadow-[var(--shadow-main)]"
58
68
  style="min-width:28px; min-height:28px; max-width:28px; max-height:28px;"
59
69
  >
60
- <img src={lang.icon} alt={lang.code.toUpperCase()} width="28" height="28" style="pointer-events:none;" />
70
+ <img
71
+ src={lang.icon}
72
+ alt={lang.code.toUpperCase()}
73
+ width="28"
74
+ height="28"
75
+ style="pointer-events:none;"
76
+ loading="lazy"
77
+ decoding="async"
78
+ />
61
79
  </span>
62
80
  <span class="text-heading">{lang.label}</span>
63
81
  </a>
@@ -24,7 +24,15 @@ const navLinks = (maugliConfig.navLinks || []).map((link) => {
24
24
  <a href={maugliConfig.brand.logoHref && maugliConfig.brand.logoHref.trim() ? maugliConfig.brand.logoHref : '/'} class="ml-6 flex-shrink-0 card-blur">
25
25
  <picture>
26
26
  <source srcset={maugliConfig.brand.logoDark} media="(prefers-color-scheme: dark)" />
27
- <img src={maugliConfig.brand.logoLight} alt="Логотип" width="48" height="48" class="w-12 h-12" />
27
+ <img
28
+ src={maugliConfig.brand.logoLight}
29
+ alt="Логотип"
30
+ width="48"
31
+ height="48"
32
+ class="w-12 h-12"
33
+ loading="eager"
34
+ decoding="async"
35
+ />
28
36
  </picture>
29
37
  </a>
30
38
  <ul
@@ -16,7 +16,7 @@ const netlifyUrl = `https://app.netlify.com/start/deploy?repository=${repository
16
16
  {
17
17
  netlifyEnabled && (
18
18
  <a href={netlifyUrl} target="_blank" rel="noopener noreferrer" class={className}>
19
- <img src="https://www.netlify.com/img/deploy/button.svg" alt="Deploy to Netlify" />
19
+ <img src="https://www.netlify.com/img/deploy/button.svg" alt="Deploy to Netlify" loading="lazy" decoding="async" />
20
20
  </a>
21
21
  )
22
22
  }
@@ -24,7 +24,13 @@ const buttons = (dict as any).buttons || (fallbackDict as any).buttons || {};
24
24
  ---
25
25
 
26
26
  <div class="product-banner-card relative aspect-[1200/630] overflow-hidden bg-[var(--card-bg)] rounded-custom flex flex-col justify-end">
27
- <img src={bannerImage} alt={seo?.image?.alt || image?.alt || title || 'Product'} class="absolute inset-0 w-full h-full object-cover rounded-custom z-0" />
27
+ <img
28
+ src={bannerImage}
29
+ alt={seo?.image?.alt || image?.alt || title || 'Product'}
30
+ class="absolute inset-0 w-full h-full object-cover rounded-custom z-0"
31
+ loading="lazy"
32
+ decoding="async"
33
+ />
28
34
  {
29
35
  buttonHref ? (
30
36
  <a
@@ -112,8 +112,8 @@ function getPreviewImageSrc(imageSrc: string): string {
112
112
  return absolutePreviewPath;
113
113
  }
114
114
 
115
- // Всегда используем дефолтное изображение для рубрик
116
- const previewImageSrc = '/img/default/previews/rubric_default.webp';
115
+ // Если указано своё изображение показываем его превью, иначе дефолтное
116
+ const previewImageSrc = getPreviewImageSrc(image?.src || maugliConfig.defaultRubricImage);
117
117
 
118
118
  // Список стоп-слов (предлоги, союзы, знаки)
119
119
  const stopWords = [
@@ -194,52 +194,58 @@ if (import.meta.env.DEV) {
194
194
  class={`w-full h-[195px] border border-[var(--border-main)] rounded-custom card-bg hover:card-shadow hover:-translate-y-1 transition-all duration-300 p-6 flex flex-row gap-4 items-start ${className}`}
195
195
  >
196
196
  <!-- Левая часть: картинка и дата -->
197
- <div class="flex flex-col items-end gap-2 w-[105px] h-[147px] relative">
198
- <img src={previewImageSrc} alt={title} class="w-[105px] h-[107px] object-cover rounded-custom" loading="lazy" />
199
- {
200
- rubricInitials && (
201
- <span style="position:absolute;left:0;top:0;width:105px;height:107px;display:flex;align-items:center;justify-content:center;pointer-events:none;z-index:2;">
202
- <span style="position:absolute;left:0;top:0;width:105px;height:107px;background:rgba(0,0,0,0.10);border-radius:inherit;z-index:1;" />
203
- <span style="position:relative;z-index:2;font-size:48px;font-weight:700;font-family:inherit;color:var(--brand-color);letter-spacing:2px;">
204
- {rubricInitials}
197
+ <<<<<<< HEAD
198
+ <div class="flex flex-col items-end gap-2 w-[105px] h-[147px]">
199
+ <img src={previewImageSrc} alt={image?.alt || title} class="w-[105px] h-[107px] object-cover rounded-custom" loading="lazy" decoding="async" />
200
+ =======
201
+ <div class="flex flex-col items-end gap-2 w-[105px] h-[147px] relative">
202
+ <img src={previewImageSrc} alt={title} class="w-[105px] h-[107px] object-cover rounded-custom" loading="lazy" />
203
+ {
204
+ rubricInitials && (
205
+ <span style="position:absolute;left:0;top:0;width:105px;height:107px;display:flex;align-items:center;justify-content:center;pointer-events:none;z-index:2;">
206
+ <span style="position:absolute;left:0;top:0;width:105px;height:107px;background:rgba(0,0,0,0.10);border-radius:inherit;z-index:1;" />
207
+ <span style="position:relative;z-index:2;font-size:48px;font-weight:700;font-family:inherit;color:var(--brand-color);letter-spacing:2px;">
208
+ {rubricInitials}
209
+ </span>
205
210
  </span>
211
+ )
212
+ }
213
+ >>>>>>> 41e6ca2 (fix: always use default rubric image from config, initials overlay, preview logic)
214
+ <div class="flex flex-col items-end gap-1 w-[74px] h-[32px]">
215
+ <span class={`flex items-center gap-1 text-[12px] text-right ${isBrandDate ? 'text-[var(--brand-color)]' : 'text-[var(--text-muted)]'}`}>
216
+ <svg
217
+ width="16"
218
+ height="16"
219
+ viewBox="0 0 24 24"
220
+ fill="none"
221
+ xmlns="http://www.w3.org/2000/svg"
222
+ style="display:inline-block;vertical-align:middle;opacity:0.6; color: var(--text-muted);"
223
+ >
224
+ <path
225
+ d="M17.7909 6.12232C14.9505 3.32362 10.5815 3.00989 7.39551 5.15377L7.38659 3.92302C7.38509 3.71587 7.21589 3.54914 7.00866 3.55079L6.25874 3.55664C6.05166 3.55822 5.88516 3.72734 5.88666 3.93434L5.90736 6.74122C5.91029 7.15357 6.24576 7.48537 6.65736 7.48537C6.65886 7.48537 6.66104 7.48537 6.66321 7.48537L9.47046 7.46467C9.67761 7.46317 9.84426 7.29389 9.84269 7.08674L9.83684 6.33667C9.83526 6.12959 9.66614 5.96309 9.45914 5.96459L8.98199 5.96804C11.4928 4.71464 14.6299 5.11372 16.7377 7.19017C18.7606 9.18427 19.3134 12.182 18.1639 14.7525C18.082 14.9355 18.1491 15.1487 18.3276 15.24L18.997 15.582C19.1866 15.6789 19.4265 15.6008 19.5145 15.4069C20.9445 12.2567 20.2743 8.57039 17.7909 6.12232ZM17.3434 16.5132C17.3419 16.5132 17.3397 16.5132 17.3375 16.5132L14.5303 16.5338C14.3231 16.5354 14.1565 16.7046 14.158 16.9117L14.1639 17.6618C14.1655 17.8688 14.3346 18.0353 14.5416 18.0339L15.0183 18.0304C12.5073 19.2835 9.37079 18.8841 7.26299 16.8083C5.24009 14.8142 4.68734 11.8164 5.83686 9.24599C5.91869 9.06299 5.85164 8.84977 5.67314 8.75849L5.00376 8.41649C4.81409 8.31959 4.57424 8.39767 4.48619 8.59154C3.05609 11.7417 3.72636 15.428 6.20969 17.8762C7.81439 19.4575 9.90771 20.2456 11.9995 20.2456C13.6101 20.2456 15.2191 19.7767 16.605 18.8438L16.6139 20.0754C16.6154 20.2825 16.7846 20.4493 16.9918 20.4477L17.7418 20.4418C17.9488 20.4402 18.1153 20.2711 18.1138 20.0641L18.0931 17.2573C18.0904 16.8449 17.755 16.5132 17.3434 16.5132Z"
226
+ fill="currentColor"></path>
227
+ </svg>
228
+ {formattedDate}
206
229
  </span>
207
- )
208
- }
209
- <div class="flex flex-col items-end gap-1 w-[74px] h-[32px]">
210
- <span class={`flex items-center gap-1 text-[12px] text-right ${isBrandDate ? 'text-[var(--brand-color)]' : 'text-[var(--text-muted)]'}`}>
211
- <svg
212
- width="16"
213
- height="16"
214
- viewBox="0 0 24 24"
215
- fill="none"
216
- xmlns="http://www.w3.org/2000/svg"
217
- style="display:inline-block;vertical-align:middle;opacity:0.6; color: var(--text-muted);"
218
- >
219
- <path
220
- d="M17.7909 6.12232C14.9505 3.32362 10.5815 3.00989 7.39551 5.15377L7.38659 3.92302C7.38509 3.71587 7.21589 3.54914 7.00866 3.55079L6.25874 3.55664C6.05166 3.55822 5.88516 3.72734 5.88666 3.93434L5.90736 6.74122C5.91029 7.15357 6.24576 7.48537 6.65736 7.48537C6.65886 7.48537 6.66104 7.48537 6.66321 7.48537L9.47046 7.46467C9.67761 7.46317 9.84426 7.29389 9.84269 7.08674L9.83684 6.33667C9.83526 6.12959 9.66614 5.96309 9.45914 5.96459L8.98199 5.96804C11.4928 4.71464 14.6299 5.11372 16.7377 7.19017C18.7606 9.18427 19.3134 12.182 18.1639 14.7525C18.082 14.9355 18.1491 15.1487 18.3276 15.24L18.997 15.582C19.1866 15.6789 19.4265 15.6008 19.5145 15.4069C20.9445 12.2567 20.2743 8.57039 17.7909 6.12232ZM17.3434 16.5132C17.3419 16.5132 17.3397 16.5132 17.3375 16.5132L14.5303 16.5338C14.3231 16.5354 14.1565 16.7046 14.158 16.9117L14.1639 17.6618C14.1655 17.8688 14.3346 18.0353 14.5416 18.0339L15.0183 18.0304C12.5073 19.2835 9.37079 18.8841 7.26299 16.8083C5.24009 14.8142 4.68734 11.8164 5.83686 9.24599C5.91869 9.06299 5.85164 8.84977 5.67314 8.75849L5.00376 8.41649C4.81409 8.31959 4.57424 8.39767 4.48619 8.59154C3.05609 11.7417 3.72636 15.428 6.20969 17.8762C7.81439 19.4575 9.90771 20.2456 11.9995 20.2456C13.6101 20.2456 15.2191 19.7767 16.605 18.8438L16.6139 20.0754C16.6154 20.2825 16.7846 20.4493 16.9918 20.4477L17.7418 20.4418C17.9488 20.4402 18.1153 20.2711 18.1138 20.0641L18.0931 17.2573C18.0904 16.8449 17.755 16.5132 17.3434 16.5132Z"
221
- fill="currentColor"></path>
222
- </svg>
223
- {formattedDate}
224
- </span>
230
+ </div>
225
231
  </div>
226
- </div>
227
- <!-- Правая часть: контент -->
228
- <div class="flex flex-col justify-start items-start h-[147px] flex-1 min-w-0">
229
- <div class="flex flex-row items-start gap-2 w-full">
230
- <h3 class="font-serif font-[700] text-[22px] text-[var(--text-heading)] leading-[1] truncate">{title}</h3>
231
- <CountBadge count={postCount} />
232
- </div>
233
- <div class="mt-2 text-[14px] text-[var(--text-main)] leading-[1.3] line-clamp-6 opacity-80">
234
- {description}
232
+ <!-- Правая часть: контент -->
233
+ <div class="flex flex-col justify-start items-start h-[147px] flex-1 min-w-0">
234
+ <div class="flex flex-row items-start gap-2 w-full">
235
+ <h3 class="font-serif font-[700] text-[22px] text-[var(--text-heading)] leading-[1] truncate">{title}</h3>
236
+ <CountBadge count={postCount} />
237
+ </div>
238
+ <div class="mt-2 text-[14px] text-[var(--text-main)] leading-[1.3] line-clamp-6 opacity-80">
239
+ {description}
240
+ </div>
235
241
  </div>
236
242
  </div>
237
243
  </article>
238
- </a>
239
244
 
240
- <style>
241
- article:hover {
242
- box-shadow: var(--card-shadow);
243
- }
244
- /* Карточка всегда двухколоночная, контент сверху, текст description не прижат к низу */
245
- </style>
245
+ <style>
246
+ article:hover {
247
+ box-shadow: var(--card-shadow);
248
+ }
249
+ /* Карточка всегда двухколоночная, контент сверху, текст description не прижат к низу */
250
+ </style>
251
+ </a>
@@ -43,6 +43,8 @@ const rubricImageFinal = rubricImage && rubricImage.length > 0 ? rubricImage : m
43
43
  alt={rubricName || tagsSection.rubricAlt}
44
44
  class="w-20 h-20 sm:w-40 sm:h-40 object-cover rounded-[16px_48px_16px_48px] border border-[var(--border-main)]"
45
45
  style="flex-shrink:0;"
46
+ loading="lazy"
47
+ decoding="async"
46
48
  />
47
49
  <div class="flex flex-col items-center sm:items-start text-center sm:text-left">
48
50
  {rubricName && <h2 class="text-xl sm:text-2xl font-bold mb-2">{rubricName}</h2>}
@@ -2,6 +2,8 @@ export type Image = {
2
2
  src: string;
3
3
  alt?: string;
4
4
  caption?: string;
5
+ width?: number;
6
+ height?: number;
5
7
  };
6
8
 
7
9
  export type Link = {
@@ -59,6 +61,12 @@ export type SiteConfig = {
59
61
 
60
62
  const siteConfig: SiteConfig = {
61
63
  website: 'https://techrev.maugli.cfd',
64
+ logo: {
65
+ src: '/logo.svg',
66
+ alt: 'Maugli Content Farm',
67
+ width: 48,
68
+ height: 48
69
+ },
62
70
  title: 'ТехРев',
63
71
  subtitle: 'Блог об автоматизации с ИИ от ИИ-автора',
64
72
  description: 'Создание контента для соцсетей и блогов быстро, дешево и качественно',
@@ -67,7 +75,7 @@ const siteConfig: SiteConfig = {
67
75
  alt: 'Автоматизация и ИИ -- новая технологическая революция',
68
76
  width: 1200,
69
77
  height: 630,
70
- caption: '...',
78
+ caption: '...'
71
79
  },
72
80
  author: {
73
81
  name: 'ИльичAI',
@@ -77,14 +85,16 @@ const siteConfig: SiteConfig = {
77
85
  description: 'AI-эксперт, созданный в Maugli Content Farm, работает на GPT4.1, редактор и аналитик.',
78
86
  sameAs: [
79
87
  'https://t.me/techrev_maugli',
80
- 'https://twitter.com/', // другие профили по желанию
88
+ 'https://twitter.com/' // другие профили по желанию
81
89
  ]
82
90
  },
83
91
  publisher: {
84
92
  name: 'Maugli Content Farm',
85
93
  logo: {
86
94
  src: '/logo.svg',
87
- alt: 'Maugli Content Farm'
95
+ alt: 'Maugli Content Farm',
96
+ width: 48,
97
+ height: 48
88
98
  },
89
99
  url: 'https://maugli.cfd',
90
100
  type: 'Organization'
@@ -138,12 +148,12 @@ const siteConfig: SiteConfig = {
138
148
  ],
139
149
  hero: {
140
150
  title: 'ТехРев — блог, освобождающий людей от ручного труда',
141
- text: " Канал ведет Ильич ИИ, заряженный верой в то, что может сделать жизнь людей проще",
151
+ text: ' Канал ведет Ильич ИИ, заряженный верой в то, что может сделать жизнь людей проще',
142
152
  image: {
143
153
  src: '/hero.webp',
144
154
  alt: 'Автор блога Ильич ИИ прямо здесь и сейчас творит технологическую революию',
145
155
  width: 1200, // ← добавь
146
- height: 630
156
+ height: 630
147
157
  },
148
158
  actions: [
149
159
  {