core-maugli 1.2.11 → 1.2.15

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.11",
5
+ "version": "1.2.15",
6
6
  "license": "GPL-3.0-or-later OR Commercial",
7
7
  "repository": {
8
8
  "type": "git",
@@ -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
  )
@@ -1,6 +1,6 @@
1
1
  ---
2
- import { getFilteredCollection } from '../utils/content-loader';
3
2
  import { maugliConfig } from '../config/maugli.config';
3
+ import { getFilteredCollection } from '../utils/content-loader';
4
4
  import { getPostsByAuthor } from '../utils/data-utils';
5
5
  import AuthorLinksGroup from './AuthorLinksGroup.astro';
6
6
  import Avatar from './Avatar.astro';
@@ -25,10 +25,21 @@ export interface Props {
25
25
  slug: string;
26
26
  };
27
27
  class?: string;
28
+ authorsList?: Array<{ slug: string }>;
28
29
  }
29
30
 
30
- const { author, class: className } = Astro.props;
31
- const { name, position, description, avatar, socials, slug } = author;
31
+ const { author, class: className, authorsList = [] } = Astro.props;
32
+ let currentAuthor = author;
33
+ // Проверяем, есть ли автор среди списка авторов
34
+ if (!authorsList.some((a) => a.slug === author.slug)) {
35
+ // Если нет — используем дефолтного автора
36
+ // Здесь предполагается, что authorsList содержит дефолтного автора
37
+ const defaultAuthor = authorsList.find((a) => a.slug === maugliConfig.defaultAuthorId);
38
+ if (defaultAuthor) {
39
+ currentAuthor = defaultAuthor;
40
+ }
41
+ }
42
+ const { name, position, description, avatar, socials, slug } = currentAuthor;
32
43
 
33
44
  // Получаем количество статей автора через getPostsByAuthor
34
45
  let posts: import('astro:content').CollectionEntry<'blog'>[] = [];
@@ -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
  {
@@ -39,8 +39,27 @@ export function getPostsByTag(posts: CollectionEntry<'blog'>[], tagId: string) {
39
39
  }
40
40
 
41
41
  // Получение автора поста с fallback на дефолтного автора из конфига
42
+
43
+ // Получение автора поста с fallback на дефолтного автора из конфига
44
+ // Если автор не найден среди авторов, возвращаем дефолтного
45
+ import fs from 'fs';
46
+ import path from 'path';
47
+
48
+ function getAuthorSlugs() {
49
+ try {
50
+ const authorsDir = path.join(process.cwd(), 'src/content/authors');
51
+ return fs.readdirSync(authorsDir)
52
+ .filter(f => f.endsWith('.md'))
53
+ .map(f => f.replace('.md', ''));
54
+ } catch {
55
+ return [];
56
+ }
57
+ }
58
+
42
59
  export function getPostAuthor(post: CollectionEntry<'blog'>): string {
43
- return post.data.author || maugliConfig.defaultAuthorId || 'unknown-author';
60
+ const author = post.data.author || maugliConfig.defaultAuthorId || 'unknown-author';
61
+ const authorSlugs = getAuthorSlugs();
62
+ return authorSlugs.includes(author) ? author : (maugliConfig.defaultAuthorId || 'unknown-author');
44
63
  }
45
64
 
46
65
  // Получение постов по автору с учетом дефолтного автора
@@ -1,14 +0,0 @@
1
- <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <g clip-path="url(#clip0_105_27)">
3
- <path d="M256 512C397.385 512 512 397.385 512 256C512 114.615 397.385 0 256 0C114.615 0 0 114.615 0 256C0 397.385 114.615 512 256 512Z" fill="#6DA544"/>
4
- <path d="M255.999 100.174L467.477 256L255.999 411.826L44.5215 256L255.999 100.174Z" fill="#FFDA44"/>
5
- <path d="M256 345.043C305.177 345.043 345.043 305.177 345.043 256C345.043 206.823 305.177 166.957 256 166.957C206.823 166.957 166.957 206.823 166.957 256C166.957 305.177 206.823 345.043 256 345.043Z" fill="#F0F0F0"/>
6
- <path d="M211.477 250.435C195.993 250.435 181.05 252.79 166.984 257.16C167.607 305.8 207.211 345.044 255.999 345.044C286.167 345.044 312.811 330.027 328.918 307.076C301.361 272.579 258.96 250.435 211.477 250.435V250.435Z" fill="#0052B4"/>
7
- <path d="M343.392 273.06C344.464 267.536 345.043 261.837 345.043 256C345.043 206.822 305.177 166.957 256 166.957C219.306 166.957 187.806 189.158 174.174 220.856C186.224 218.359 198.7 217.044 211.479 217.044C263.196 217.043 309.982 238.541 343.392 273.06V273.06Z" fill="#0052B4"/>
8
- </g>
9
- <defs>
10
- <clipPath id="clip0_105_27">
11
- <rect width="512" height="512" fill="white"/>
12
- </clipPath>
13
- </defs>
14
- </svg>
@@ -1,15 +0,0 @@
1
- <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <g clip-path="url(#clip0_105_19)">
3
- <path d="M256 512C397.385 512 512 397.385 512 256C512 114.615 397.385 0 256 0C114.615 0 0 114.615 0 256C0 397.385 114.615 512 256 512Z" fill="#D80027"/>
4
- <path d="M140.1 155.8L162.2 223.8H233.7L175.9 265.9L198 333.9L140.1 291.9L82.2 333.9L104.4 265.9L46.5 223.8H118L140.1 155.8Z" fill="#FFDA44"/>
5
- <path d="M303.499 396.5L286.599 375.7L261.599 385.4L276.099 362.9L259.199 342L285.099 348.9L299.699 326.4L301.099 353.2L327.099 360.1L301.999 369.7L303.499 396.5Z" fill="#FFDA44"/>
6
- <path d="M337.099 335.5L345.099 309.9L323.199 294.4L349.999 294L357.899 268.4L366.599 293.8L393.399 293.5L371.899 309.5L380.499 334.9L358.599 319.4L337.099 335.5Z" fill="#FFDA44"/>
7
- <path d="M382.401 187.9L370.601 212L389.801 230.7L363.301 226.9L351.501 250.9L346.901 224.5L320.301 220.7L344.101 208.2L339.501 181.7L358.701 200.4L382.401 187.9Z" fill="#FFDA44"/>
8
- <path d="M304.2 114.9L302.2 141.6L327.1 151.7L301 158.1L299.1 184.9L285 162.1L258.9 168.5L276.2 148L262 125.3L286.9 135.4L304.2 114.9Z" fill="#FFDA44"/>
9
- </g>
10
- <defs>
11
- <clipPath id="clip0_105_19">
12
- <rect width="512" height="512" fill="white"/>
13
- </clipPath>
14
- </defs>
15
- </svg>
@@ -1,12 +0,0 @@
1
- <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <g clip-path="url(#clip0_105_34)">
3
- <path d="M256 512C397.385 512 512 397.385 512 256C512 114.615 397.385 0 256 0C114.615 0 0 114.615 0 256C0 397.385 114.615 512 256 512Z" fill="#F0F0F0"/>
4
- <path d="M512 256C512 145.929 442.528 52.094 345.043 15.923V496.078C442.528 459.906 512 366.071 512 256V256Z" fill="#D80027"/>
5
- <path d="M0 256C0 366.071 69.473 459.906 166.957 496.077V15.923C69.473 52.094 0 145.929 0 256Z" fill="#0052B4"/>
6
- </g>
7
- <defs>
8
- <clipPath id="clip0_105_34">
9
- <rect width="512" height="512" fill="white"/>
10
- </clipPath>
11
- </defs>
12
- </svg>
@@ -1,12 +0,0 @@
1
- <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <g clip-path="url(#clip0_3719_7205)">
3
- <path d="M15.9238 345.043C52.0948 442.527 145.93 512 256.001 512C366.072 512 459.907 442.527 496.078 345.043L256.001 322.783L15.9238 345.043Z" fill="#FFDA44"/>
4
- <path d="M256.001 0C145.93 0 52.0948 69.472 15.9238 166.957L256.001 189.217L496.078 166.956C459.907 69.472 366.072 0 256.001 0Z" fill="black"/>
5
- <path d="M15.923 166.957C5.633 194.69 0 224.686 0 256C0 287.314 5.633 317.31 15.923 345.043H496.078C506.368 317.31 512 287.314 512 256C512 224.686 506.368 194.69 496.077 166.957H15.923Z" fill="#D80027"/>
6
- </g>
7
- <defs>
8
- <clipPath id="clip0_3719_7205">
9
- <rect width="512" height="512" fill="white"/>
10
- </clipPath>
11
- </defs>
12
- </svg>
@@ -1,11 +0,0 @@
1
- <svg width="512" height="513" viewBox="0 0 512 513" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <g clip-path="url(#clip0_105_38)">
3
- <path d="M256 512.989C397.385 512.989 512 398.374 512 256.989C512 115.604 397.385 0.989258 256 0.989258C114.615 0.989258 0 115.604 0 256.989C0 398.374 114.615 512.989 256 512.989Z" fill="#F0F0F0"/>
4
- <path d="M255.999 368.293C317.471 368.293 367.303 318.461 367.303 256.989C367.303 195.518 317.471 145.685 255.999 145.685C194.528 145.685 144.695 195.518 144.695 256.989C144.695 318.461 194.528 368.293 255.999 368.293Z" fill="#D80027"/>
5
- </g>
6
- <defs>
7
- <clipPath id="clip0_105_38">
8
- <rect width="512" height="512" fill="white" transform="translate(0 0.989258)"/>
9
- </clipPath>
10
- </defs>
11
- </svg>
@@ -1,12 +0,0 @@
1
- <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <g clip-path="url(#clip0_3719_7207)">
3
- <path d="M256 512C397.385 512 512 397.385 512 256C512 114.615 397.385 0 256 0C114.615 0 0 114.615 0 256C0 397.385 114.615 512 256 512Z" fill="#F0F0F0"/>
4
- <path d="M496.077 345.043C506.368 317.31 512 287.314 512 256C512 224.686 506.368 194.69 496.077 166.957H15.923C5.633 194.69 0 224.686 0 256C0 287.314 5.633 317.31 15.923 345.043L256 367.304L496.077 345.043Z" fill="#0052B4"/>
5
- <path d="M256.001 512C366.072 512 459.907 442.528 496.078 345.043H15.9238C52.0948 442.528 145.93 512 256.001 512Z" fill="#D80027"/>
6
- </g>
7
- <defs>
8
- <clipPath id="clip0_3719_7207">
9
- <rect width="512" height="512" fill="white"/>
10
- </clipPath>
11
- </defs>
12
- </svg>
@@ -1,12 +0,0 @@
1
- <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <g clip-path="url(#clip0_3719_7208)">
3
- <path d="M0 255.999C0 287.313 5.633 317.309 15.923 345.042L256 367.303L496.077 345.042C506.367 317.309 512 287.313 512 255.999C512 224.685 506.367 194.689 496.077 166.956L256 144.695L15.923 166.956C5.633 194.689 0 224.685 0 255.999H0Z" fill="#FFDA44"/>
4
- <path d="M496.078 166.957C459.907 69.473 366.072 0 256.001 0C145.93 0 52.0948 69.473 15.9238 166.957H496.078Z" fill="#D80027"/>
5
- <path d="M15.9238 345.043C52.0948 442.527 145.93 512 256.001 512C366.072 512 459.907 442.527 496.078 345.043H15.9238Z" fill="#D80027"/>
6
- </g>
7
- <defs>
8
- <clipPath id="clip0_3719_7208">
9
- <rect width="512" height="512" fill="white"/>
10
- </clipPath>
11
- </defs>
12
- </svg>
@@ -1,13 +0,0 @@
1
- <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <g clip-path="url(#clip0_105_41)">
3
- <path d="M256 511.999C397.385 511.999 512 397.384 512 255.999C512 114.614 397.385 -0.000976562 256 -0.000976562C114.615 -0.000976562 0 114.614 0 255.999C0 397.384 114.615 511.999 256 511.999Z" fill="#F0F0F0"/>
4
- <path d="M144.695 345.042L166.956 496.078C194.688 506.369 224.685 511.999 255.999 511.999C366.069 511.999 459.905 442.527 496.075 345.042H144.695Z" fill="black"/>
5
- <path d="M144.695 166.956L166.956 15.92C194.688 5.62902 224.685 -0.000976562 255.999 -0.000976562C366.069 -0.000976562 459.905 69.471 496.075 166.956H144.695Z" fill="#6DA544"/>
6
- <path d="M0 255.999C0 366.07 69.473 459.905 166.957 496.076V15.9221C69.473 52.0931 0 145.928 0 255.999Z" fill="#A2001D"/>
7
- </g>
8
- <defs>
9
- <clipPath id="clip0_105_41">
10
- <rect width="512" height="512" fill="white" transform="translate(0 -0.000976562)"/>
11
- </clipPath>
12
- </defs>
13
- </svg>
@@ -1,15 +0,0 @@
1
- <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <g clip-path="url(#clip0_3719_7206)">
3
- <path d="M256 512C397.385 512 512 397.385 512 256C512 114.615 397.385 0 256 0C114.615 0 0 114.615 0 256C0 397.385 114.615 512 256 512Z" fill="#F0F0F0"/>
4
- <path d="M244.869 256H511.999C511.999 232.894 508.919 210.51 503.18 189.217H244.869V256Z" fill="#D80027"/>
5
- <path d="M244.869 122.435H474.425C458.754 96.8633 438.717 74.2603 415.355 55.6523H244.869V122.435Z" fill="#D80027"/>
6
- <path d="M255.999 512C316.248 512 371.625 491.176 415.355 456.348H96.6426C140.373 491.176 195.75 512 255.999 512Z" fill="#D80027"/>
7
- <path d="M37.5734 389.566H474.425C487.006 369.037 496.763 346.597 503.18 322.783H8.81836C15.2354 346.597 24.9924 369.037 37.5734 389.566V389.566Z" fill="#D80027"/>
8
- <path d="M118.584 39.978H141.913L120.213 55.743L128.502 81.252L106.803 65.487L85.104 81.252L92.264 59.215C73.158 75.13 56.412 93.776 42.612 114.552H50.087L36.274 124.587C34.122 128.177 32.058 131.824 30.08 135.525L36.676 155.826L24.37 146.885C21.311 153.366 18.513 159.993 15.998 166.758L23.265 189.126H50.087L28.387 204.891L36.676 230.4L14.977 214.635L1.979 224.079C0.678 234.537 0 245.189 0 256H256C256 114.616 256 97.948 256 0C205.428 0 158.285 14.67 118.584 39.978V39.978ZM128.502 230.4L106.803 214.635L85.104 230.4L93.393 204.891L71.693 189.126H98.515L106.803 163.617L115.091 189.126H141.913L120.213 204.891L128.502 230.4ZM120.213 130.317L128.502 155.826L106.803 140.061L85.104 155.826L93.393 130.317L71.693 114.552H98.515L106.803 89.043L115.091 114.552H141.913L120.213 130.317ZM220.328 230.4L198.629 214.635L176.93 230.4L185.219 204.891L163.519 189.126H190.341L198.629 163.617L206.917 189.126H233.739L212.039 204.891L220.328 230.4ZM212.039 130.317L220.328 155.826L198.629 140.061L176.93 155.826L185.219 130.317L163.519 114.552H190.341L198.629 89.043L206.917 114.552H233.739L212.039 130.317ZM212.039 55.743L220.328 81.252L198.629 65.487L176.93 81.252L185.219 55.743L163.519 39.978H190.341L198.629 14.469L206.917 39.978H233.739L212.039 55.743Z" fill="#0052B4"/>
9
- </g>
10
- <defs>
11
- <clipPath id="clip0_3719_7206">
12
- <rect width="512" height="512" fill="white"/>
13
- </clipPath>
14
- </defs>
15
- </svg>