core-maugli 1.2.2 → 1.2.4

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.
Files changed (92) hide show
  1. package/README.md +29 -15
  2. package/package.json +6 -13
  3. package/public/img/examples/blog/previews/post-1-avtomatizaciya-marketinga-kak-ii-osvobozhdaet-predprinimatelei-ot-cifrovogo-rabstva.webp +0 -0
  4. package/public/img/examples/blog/previews/post-2-avtomatizaciya-kontenta-kak-neiroseti-ubivayut-perfekcionizm-v-biznese.webp +0 -0
  5. package/public/img/examples/blog/previews/post-3-laik-ne-valyuta-kak-avtomatizaciya-marketinga-spasaet-ot-lozhnyh-metrik.webp +0 -0
  6. package/public/img/examples/blog/previews/post-5-5-fatalnyh-oshibok-marketinga-kotorye-ubivayut-startapy-na-starte.webp +0 -0
  7. package/public/img/examples/blog/previews/post-6-5-strategii-kontent-marketinga-dlya-startapov-avtomatizaciya-i-revolyuciya.webp +0 -0
  8. package/public/img/examples/blog/previews/post-7-viralnyi-kontent-ne-udacha-a-strategiya-avtomatizaciya-marketinga.webp +0 -0
  9. package/public/img/examples/blog/previews/post-agent-experience-mcp-biznes-v-epohu-ii-agentov.webp +0 -0
  10. package/public/img/examples/blog/previews/post_11.webp +0 -0
  11. package/public/img/examples/blog/previews/post_12.webp +0 -0
  12. package/public/img/examples/blog/previews/post_1_jsonld_guide.webp +0 -0
  13. package/public/img/examples/blog/previews/test-post.webp +0 -0
  14. package/public/img/examples/blog/previews/tr-post-1.webp +0 -0
  15. package/public/img/examples/products/previews/product_1.webp +0 -0
  16. package/public/img/examples/products/previews/product_2.webp +0 -0
  17. package/public/img/examples/projects/previews/project_1.webp +0 -0
  18. package/public/img/examples/projects/previews/project_2.webp +0 -0
  19. package/scripts/generate-previews.js +175 -0
  20. package/scripts/update-components.js +166 -0
  21. package/scripts/upgrade-config.js +23 -3
  22. package/src/components/LanguageSwitcher.astro +2 -16
  23. package/astro.config.mjs +0 -92
  24. package/bin/init.js +0 -201
  25. package/public/img/default/autor_default.webp +0 -0
  26. package/public/img/default/blog_default.webp +0 -0
  27. package/public/img/default/default.webp +0 -0
  28. package/public/img/default/product_default.webp +0 -0
  29. package/public/img/default/project_default.webp +0 -0
  30. package/public/img/default/rubric_default.webp +0 -0
  31. package/public/img/default/test.webp +0 -0
  32. package/public/img/default/test2.webp +0 -0
  33. package/resize-all.cjs +0 -29
  34. package/src/i18n/de.json +0 -126
  35. package/src/i18n/en.json +0 -126
  36. package/src/i18n/es.json +0 -126
  37. package/src/i18n/fr.json +0 -126
  38. package/src/i18n/index.ts +0 -10
  39. package/src/i18n/ja.json +0 -126
  40. package/src/i18n/languages.ts +0 -23
  41. package/src/i18n/pt.json +0 -126
  42. package/src/i18n/ru.json +0 -123
  43. package/src/i18n/zh.json +0 -126
  44. package/src/icons/ArrowLeft.astro +0 -13
  45. package/src/icons/ArrowRight.astro +0 -13
  46. package/src/icons/flags/brazil.svg +0 -14
  47. package/src/icons/flags/china.svg +0 -15
  48. package/src/icons/flags/france.svg +0 -12
  49. package/src/icons/flags/germany.svg +0 -12
  50. package/src/icons/flags/japan.svg +0 -11
  51. package/src/icons/flags/russia.svg +0 -12
  52. package/src/icons/flags/spain.svg +0 -12
  53. package/src/icons/flags/united arab emirates.svg +0 -13
  54. package/src/icons/flags/united states.svg +0 -15
  55. package/src/icons/socials/BlueskyIcon.astro +0 -9
  56. package/src/icons/socials/EmailIcon.astro +0 -8
  57. package/src/icons/socials/LinkedinIcon.astro +0 -9
  58. package/src/icons/socials/MastodonIcon.astro +0 -9
  59. package/src/icons/socials/MediumIcon.astro +0 -9
  60. package/src/icons/socials/RedditIcon.astro +0 -11
  61. package/src/icons/socials/TelegramIcon.astro +0 -11
  62. package/src/icons/socials/TwitterIcon.astro +0 -9
  63. package/src/layouts/BaseLayout.astro +0 -59
  64. package/src/pages/404.astro +0 -24
  65. package/src/pages/[...id].astro +0 -50
  66. package/src/pages/about.astro +0 -0
  67. package/src/pages/authors/[...page].astro +0 -105
  68. package/src/pages/authors/[id].astro +0 -175
  69. package/src/pages/blog/[...page].astro +0 -59
  70. package/src/pages/blog/[id].astro +0 -175
  71. package/src/pages/index.astro +0 -90
  72. package/src/pages/products/[...page].astro +0 -50
  73. package/src/pages/products/[id].astro +0 -221
  74. package/src/pages/projects/[...page].astro +0 -74
  75. package/src/pages/projects/[id].astro +0 -165
  76. package/src/pages/projects/tags/[id]/[...page].astro +0 -58
  77. package/src/pages/rss.xml.js +0 -5
  78. package/src/pages/tags/[id]/[...page].astro +0 -110
  79. package/src/pages/tags/index.astro +0 -124
  80. package/src/scripts/infoCardFadeIn.js +0 -22
  81. package/src/styles/global.css +0 -273
  82. package/src/utils/common-utils.ts +0 -0
  83. package/src/utils/content-loader.ts +0 -14
  84. package/src/utils/data-utils.ts +0 -49
  85. package/src/utils/featuredManager.ts +0 -118
  86. package/src/utils/posts.ts +0 -43
  87. package/src/utils/reading-time.ts +0 -28
  88. package/src/utils/remark-slugify.js +0 -8
  89. package/src/utils/rss.ts +0 -23
  90. package/tsconfig.json +0 -8
  91. package/typograf-batch.js +0 -49
  92. package/vite.config.js +0 -11
@@ -1,175 +0,0 @@
1
- ---
2
- import { type CollectionEntry, render } from 'astro:content';
3
- import Breadcrumbs from '../../components/Breadcrumbs.astro';
4
- import PostPreview from '../../components/PostPreview.astro';
5
- import Subscribe from '../../components/Subscribe.astro';
6
- import TagsSection from '../../components/TagsSection.astro';
7
- import { maugliConfig } from '../../config/maugli.config';
8
- import BlueskyIcon from '../../icons/socials/BlueskyIcon.astro';
9
- import EmailIcon from '../../icons/socials/EmailIcon.astro';
10
- import LinkedinIcon from '../../icons/socials/LinkedinIcon.astro';
11
- import MastodonIcon from '../../icons/socials/MastodonIcon.astro';
12
- import MediumIcon from '../../icons/socials/MediumIcon.astro';
13
- import RedditIcon from '../../icons/socials/RedditIcon.astro';
14
- import TelegramIcon from '../../icons/socials/TelegramIcon.astro';
15
- import TwitterIcon from '../../icons/socials/TwitterIcon.astro';
16
- import BaseLayout from '../../layouts/BaseLayout.astro';
17
- import { getFilteredCollection } from '../../utils/content-loader';
18
- import { getAllTags, getPostsByTag, sortItemsWithFeaturedFirst } from '../../utils/data-utils';
19
-
20
- export async function getStaticPaths() {
21
- const authors = await getFilteredCollection('authors');
22
- return authors.map((author) => ({
23
- params: { id: author.id },
24
- props: { author }
25
- }));
26
- }
27
-
28
- type Props = { author: CollectionEntry<'authors'> };
29
-
30
- const { author } = Astro.props;
31
- const { name, position, description, avatar: rawAvatar, socials, seo } = author.data;
32
- // Приводим путь к аватару к public/img/authors/...
33
- const avatar = rawAvatar ? rawAvatar.replace('/img/examples/authors/', '/img/authors/') : undefined;
34
- const { Content } = await render(author);
35
-
36
- // Получаем все статьи блога
37
- let allPosts = await getFilteredCollection('blog');
38
-
39
- // Фильтруем статьи этого автора (с учетом дефолтного автора)
40
- const authorPosts = allPosts
41
- .filter(post => (post.data.author || maugliConfig.defaultAuthorId) === author.id)
42
- .sort(sortItemsWithFeaturedFirst);
43
-
44
- // Получаем теги статей автора
45
- const authorTags = getAllTags(authorPosts).map((tag) => ({
46
- id: tag.id,
47
- name: tag.name,
48
- count: getPostsByTag(authorPosts, tag.id).length
49
- }));
50
-
51
- // Получаем доступные соцсети
52
- const availableSocials = socials ? Object.entries(socials).filter(([_, url]) => url) : [];
53
-
54
- // Функция для получения компонента иконки
55
- const getIconComponent = (platform: string) => {
56
- switch (platform) {
57
- case 'email':
58
- return EmailIcon;
59
- case 'telegram':
60
- return TelegramIcon;
61
- case 'mastodon':
62
- return MastodonIcon;
63
- case 'medium':
64
- return MediumIcon;
65
- case 'bluesky':
66
- return BlueskyIcon;
67
- case 'reddit':
68
- return RedditIcon;
69
- case 'linkedin':
70
- return LinkedinIcon;
71
- case 'twitter':
72
- return TwitterIcon;
73
- default:
74
- return EmailIcon; // fallback
75
- }
76
- };
77
-
78
- // SEO данные
79
- const pageTitle = seo?.title || `${name} - ${position} | Maugli Content Farm`;
80
- const pageDescription = seo?.description || description;
81
- ---
82
-
83
- <BaseLayout title={pageTitle} description={pageDescription} image={{ src: avatar || '/img/default/autor_default.webp', alt: `Фото ${name}` }} showHeader={false} fullWidth={true}>
84
- <div class="max-w-[1280px] mx-auto">
85
- <!-- Хлебные крошки -->
86
- <Breadcrumbs />
87
-
88
- <!-- Информация об авторе на плашке -->
89
- <div class="mb-16 sm:mb-24">
90
- <div class="bg-[var(--bg-muted)] rounded-custom p-8 md:p-12">
91
- <div class="flex flex-col sm:flex-row gap-6 sm:gap-8 items-start">
92
- <!-- Аватар -->
93
- <div class="w-32 h-32 sm:w-40 sm:h-40 bg-[var(--bg-main)] rounded-full overflow-hidden flex-shrink-0">
94
- <img src={avatar || '/img/default/autor_default.webp'} alt={`Фото ${name}`} class="w-full h-full object-cover" />
95
- </div>
96
-
97
- <!-- Информация -->
98
- <div class="flex-grow">
99
- <h1 class="mb-2 text-[4rem] leading-tight font-serif font-[800] sm:text-4xl" style="color: var(--text-heading)">
100
- {name}
101
- </h1>
102
- <p class="text-[1rem] text-[var(--text-muted)] font-medium mb-4">
103
- {position}
104
- </p>
105
- <p class="text-lg text-[var(--text-main)] leading-[1.35] mb-6">
106
- <span style="opacity:0.7;">
107
- {description}
108
- </span>
109
- </p>
110
- <!-- Основной текст автора (после фронтматтера) -->
111
- {Content && (
112
- <div class="prose prose-lg max-w-none mb-6 text-[var(--text-main)]">
113
- <Content />
114
- </div>
115
- )}
116
-
117
- <!-- Соцсети -->
118
- {
119
- availableSocials.length > 0 && (
120
- <div class="flex flex-wrap gap-3">
121
- {availableSocials.map(([platform, url]) => {
122
- const IconComponent = getIconComponent(platform);
123
- return (
124
- <a
125
- href={platform === 'email' ? `mailto:${url}` : url as string}
126
- target={platform === 'email' ? '_self' : '_blank'}
127
- rel={platform === 'email' ? undefined : 'noopener noreferrer'}
128
- class="inline-flex opacity-60 hover:opacity-100 items-center gap-2 px-4 py-2 border border-[var(--border-main)] rounded-custom hover:bg-[var(--bg-main)] transition-colors duration-200"
129
- title={platform === 'email' ? `Написать ${name}` : `${name} в ${platform}`}
130
- >
131
- <IconComponent class="w-4 h-4 text-[var(--text-heading)] " />
132
- <span class="text-sm font-medium text-[var(--text-heading)] capitalize">{platform}</span>
133
- </a>
134
- );
135
- })}
136
- </div>
137
- )
138
- }
139
- </div>
140
- </div>
141
- </div>
142
- </div>
143
-
144
- <!-- Статьи автора -->
145
- {
146
- authorPosts.length > 0 && (
147
- <div class="mb-16 sm:mb-24">
148
- <h2 class="mb-12 text-2xl leading-tight font-serif font-[800] sm:mb-16 sm:text-3xl" style="color: var(--text-heading)">
149
- Статьи автора
150
- </h2>
151
-
152
- <!-- Теги -->
153
- {authorTags.length > 0 && (
154
- <TagsSection
155
- tags={authorTags}
156
- totalCount={authorPosts.length}
157
- basePath={`/authors/${author.id}`}
158
- class="mb-8"
159
- />
160
- )}
161
-
162
- <!-- Сетка статей -->
163
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
164
- {authorPosts.map((post) => (
165
- <PostPreview post={post} headingLevel="h3" />
166
- ))}
167
- </div>
168
- </div>
169
- )
170
- }
171
-
172
- <!-- Подписка -->
173
- <Subscribe class="my-16 sm:my-24" />
174
- </div>
175
- </BaseLayout>
@@ -1,59 +0,0 @@
1
- ---
2
- import type { GetStaticPathsOptions, Page } from 'astro';
3
- import { type CollectionEntry } from 'astro:content';
4
- import { getFilteredCollection } from '../../utils/content-loader';
5
- import Pagination from '../../components/Pagination.astro';
6
- import PostPreview from '../../components/PostPreview.astro';
7
- import Subscribe from '../../components/Subscribe.astro';
8
- import TagsSection from '../../components/TagsSection.astro';
9
- import { maugliConfig } from '../../config/maugli.config';
10
- import siteConfig from '../../data/site-config';
11
- import BaseLayout from '../../layouts/BaseLayout.astro';
12
- import { getAllTags, getPostsByTag, sortItemsWithFeaturedFirst } from '../../utils/data-utils';
13
-
14
- export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
15
- let posts = (await getFilteredCollection('blog')).sort(sortItemsWithFeaturedFirst);
16
- return paginate(posts, { pageSize: siteConfig.postsPerPage || 4 });
17
- }
18
-
19
- type Props = { page: Page<CollectionEntry<'blog'>> };
20
-
21
- const { page } = Astro.props;
22
- const blog = page.data;
23
-
24
- // Получаем все теги с количеством постов и выделяем рубрики
25
- let allPosts = await getFilteredCollection('blog');
26
- let allRubrics = await getFilteredCollection('tags');
27
- const rubricSlugs = allRubrics.map((r) => r.data.slug);
28
- let tagsWithCount = getAllTags(allPosts).map((tag) => ({
29
- id: tag.id,
30
- name: tag.name,
31
- count: getPostsByTag(allPosts, tag.id).length,
32
- isRubric: rubricSlugs.includes(tag.id)
33
- }));
34
- if (maugliConfig.showOnlyRubricsTags) {
35
- tagsWithCount = tagsWithCount.filter((tag) => tag.isRubric);
36
- } else {
37
- tagsWithCount = [...tagsWithCount.filter((tag) => tag.isRubric), ...tagsWithCount.filter((tag) => !tag.isRubric)];
38
- }
39
- ---
40
-
41
- <BaseLayout
42
- title="<Блог>"
43
- description="Блог об автоматизации глазами ИИ"
44
- image={{ src: '/tr-prewiew.png', alt: 'The preview of the site' }}
45
- showHeader={false}
46
- fullWidth={true}
47
- >
48
- <div class="max-w-[1280px] mx-auto">
49
- <h1 class="mb-12 text-3xl leading-tight font-serif font-[800] sm:mb-16 sm:text-5xl" style="color: var(--text-heading)">Статьи</h1>
50
- <TagsSection tags={tagsWithCount} totalCount={allPosts.length} />
51
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8 mb-16">
52
- {blog.map((post) => <PostPreview post={post} />)}
53
- </div>
54
-
55
- <!-- Пагинация (показывается только если больше одной страницы) -->
56
- {page.lastPage > 1 && <Pagination page={page} class="my-16 sm:my-24" />}
57
- </div>
58
- <Subscribe class="my-16 sm:my-24" />
59
- </BaseLayout>
@@ -1,175 +0,0 @@
1
- ---
2
- import { type CollectionEntry, render } from 'astro:content';
3
- import { getFilteredCollection } from '../../utils/content-loader';
4
- import ArticleMeta from '../../components/ArticleMeta.astro';
5
- import ContentFooter from '../../components/ContentFooter.astro';
6
- import HeroImage from '../../components/HeroImage.astro';
7
- import PostPreview from '../../components/PostPreview.astro';
8
- import ProductBannerCard from '../../components/ProductBannerCard.astro';
9
- import Subscribe from '../../components/Subscribe.astro';
10
- import SummaryFAQCard from '../../components/SummaryFAQCard.astro';
11
- import TableOfContents from '../../components/TableOfContents.astro';
12
- import TagsAndShare from '../../components/TagsAndShare.astro';
13
- import { maugliConfig } from '../../config/maugli.config';
14
- import BaseLayout from '../../layouts/BaseLayout.astro';
15
- import { sortItemsByDateDesc } from '../../utils/data-utils';
16
- import { calculateReadingTime } from '../../utils/reading-time';
17
-
18
- import { LANGUAGES } from '../../i18n/languages';
19
- const dicts: Record<string, any> = Object.fromEntries(LANGUAGES.map((l) => [l.code, l.dict]));
20
- const lang: string = typeof maugliConfig.defaultLang === 'string' && dicts[maugliConfig.defaultLang] ? maugliConfig.defaultLang : 'en';
21
- const dict = dicts[lang] || dicts['en'];
22
- const morePostsTitle = dict.buttons?.morePosts || dicts['en'].buttons.morePosts;
23
-
24
- export async function getStaticPaths() {
25
- const posts = (await getFilteredCollection('blog')).sort(sortItemsByDateDesc);
26
- const postCount = posts.length;
27
- return posts.map((post, index) => ({
28
- params: { id: post.id },
29
- props: {
30
- post,
31
- prevPost: index + 1 !== postCount ? posts[index + 1] : null,
32
- nextPost: index !== 0 ? posts[index - 1] : null
33
- }
34
- }));
35
- }
36
-
37
- type Props = { post: CollectionEntry<'blog'>; prevPost: CollectionEntry<'blog'>; nextPost: CollectionEntry<'blog'> };
38
-
39
- const { href } = Astro.url;
40
- const { post, prevPost, nextPost } = Astro.props;
41
- const { title, publishDate, updatedDate, excerpt, tags = [], seo } = post.data;
42
- const { Content, headings } = await render(post);
43
-
44
- // Вычисляем время чтения на основе контента
45
- const readingTime = calculateReadingTime(post.body || '');
46
-
47
- // Преобразуем встроенные заголовки Astro в нужный формат
48
- const tocItems = headings
49
- .filter((heading) => heading.depth === 2 || heading.depth === 3) // Только H2 и H3
50
- .map((heading) => ({
51
- title: heading.text,
52
- id: heading.slug
53
- }));
54
-
55
- // Получаем картинку для баннера по productID (если есть)
56
- let bannerImage = { src: '/default.webp', alt: title };
57
- if (post.data.productID) {
58
- const allProducts = await getFilteredCollection('products');
59
- const product = allProducts.find((p) => p.id === post.data.productID);
60
- if (product && product.data.image) {
61
- bannerImage = {
62
- ...product.data.image,
63
- alt: product.data.image.alt || product.data.title || 'Product'
64
- };
65
- } else if (product) {
66
- bannerImage = { src: '/default.webp', alt: product.data.title || 'Product' };
67
- }
68
- }
69
- ---
70
-
71
- <BaseLayout
72
- title={seo?.title ?? title}
73
- description={seo?.description ?? excerpt}
74
- image={seo?.image || post.data.image || { src: '/default.webp', alt: title }}
75
- pageType="article"
76
- showHeader={false}
77
- fullWidth={true}
78
- >
79
- <!-- Расширяем контейнер для двухколоночной структуры -->
80
- <div class="w-full max-w-none lg:max-w-[1280px] mx-auto lg:px-8 -mt-0">
81
- <!-- Метаинформация над картинкой -->
82
-
83
- <article class="mb-16 sm:mb-24 max-w-3xl mx-auto lg:max-w-none">
84
- <header class="mb-8">
85
- {
86
- (post.data.seo?.image?.src || post.data.image?.src) && (
87
- <HeroImage
88
- src={post.data.seo?.image?.src || post.data.image?.src || ''}
89
- alt={post.data.seo?.image?.alt || post.data.image?.alt || title}
90
- caption={post.data.seo?.image?.caption || post.data.image?.caption}
91
- width={post.data.seo?.image?.width || post.data.image?.width || 1200}
92
- height={post.data.seo?.image?.height || post.data.image?.height || 630}
93
- />
94
- )
95
- }
96
- <div class="px-4 mt-2 md:px-0">
97
- <ArticleMeta publishDate={publishDate} readingTime={readingTime} post={post} />
98
- </div>
99
- <h1 class="mt-25 text-3xl px-4 md:px-0 font-serif font-[800] sm:text-5xl tracking-tight !leading-[1.05]" style="color: var(--text-heading)">
100
- {title}
101
- </h1>
102
- </header>
103
- </article>
104
-
105
- <!-- 16-колоночная сетка -->
106
- <div class="md:grid md:grid-cols-16 md:gap-9">
107
- <!-- Боковая панель с Table of Contents -->
108
- <aside class="hidden sm:block md:col-span-5 lg:col-span-5 min-h-screen">
109
- <div class="sticky top-8">
110
- {tocItems.length > 0 && <TableOfContents headings={tocItems} />}
111
- <TagsAndShare tags={tags} shareUrl={href} title={title} basePath="/blog" />
112
- {/* Баннер продукта, если есть productLink в посте */}
113
- {
114
- post.data.productLink && (
115
- <div class="mt-8">
116
- <ProductBannerCard
117
- product={{
118
- data: {
119
- productLink: post.data.productLink,
120
- image: bannerImage,
121
- title: post.data.title || 'Продукт'
122
- }
123
- }}
124
- />
125
- </div>
126
- )
127
- }
128
- </div>
129
- </aside>
130
-
131
- <!-- Основной контент -->
132
- <div class="md:col-span-11 px-4 md:px-0 h-auto lg:col-span-11">
133
- <!-- Summary/FAQ в начале контента -->
134
- {
135
- post.data.generativeEngineOptimization?.generated &&
136
- (post.data.generativeEngineOptimization.generated.summary ||
137
- post.data.generativeEngineOptimization.generated.highlights?.length > 0 ||
138
- post.data.generativeEngineOptimization.generated.faq?.length > 0) && (
139
- <div class="not-prose mb-8">
140
- <SummaryFAQCard
141
- summary={post.data.generativeEngineOptimization.generated.summary}
142
- highlights={post.data.generativeEngineOptimization.generated.highlights}
143
- faq={post.data.generativeEngineOptimization.generated.faq}
144
- />
145
- </div>
146
- )
147
- }
148
-
149
- <!-- Контент с prose стилями -->
150
- <div class="prose sm:prose-lg lg:prose-xl max-w-none">
151
- <Content />
152
- </div>
153
-
154
- <!-- Блок тегов и кнопки поделиться под контентом (только на мобилке) -->
155
- <ContentFooter tags={tags} shareUrl={href} title={title} basePath="/blog" productLink={post.data.productLink} />
156
-
157
- {
158
- (prevPost || nextPost) && (
159
- <div class="my-16 sm:my-24">
160
- <h2 class="mb-12 text-xl font-serif sm:mb-16 sm:text-2xl text-heading">{morePostsTitle}</h2>
161
- <div class="max-w-[1280px] mx-auto">
162
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8">
163
- {nextPost && <PostPreview post={nextPost} headingLevel="h3" />}
164
- {prevPost && <PostPreview post={prevPost} headingLevel="h3" />}
165
- </div>
166
- </div>
167
- </div>
168
- )
169
- }
170
-
171
- {maugliConfig.subscribe?.enabled && <Subscribe class="my-16 sm:my-24" />}
172
- </div>
173
- </div>
174
- </div>
175
- </BaseLayout>
@@ -1,90 +0,0 @@
1
- ---
2
- import { getEntry } from 'astro:content';
3
- import { getFilteredCollection } from '../utils/content-loader';
4
- import Button from '../components/Button.astro';
5
- import InfoCard from '../components/InfoCard.astro';
6
- import PostPreview from '../components/PostPreview.astro';
7
- import Subscribe from '../components/Subscribe.astro';
8
- import TagsSection from '../components/TagsSection.astro';
9
- import { maugliConfig } from '../config/maugli.config';
10
- import siteConfig from '../data/site-config';
11
- import BaseLayout from '../layouts/BaseLayout.astro';
12
- import { getAllTags, getPostsByTag, sortItemsWithFeaturedFirst } from '../utils/data-utils';
13
-
14
- import { LANGUAGES } from '../i18n/languages';
15
-
16
- const lang = typeof maugliConfig.defaultLang === 'string' ? maugliConfig.defaultLang : 'en';
17
- const languageObj = LANGUAGES.find((l) => l.code === lang) || LANGUAGES.find((l) => l.code === 'en');
18
- const fallbackDict = LANGUAGES.find((l) => l.code === 'en')?.dict || {};
19
- const dict = languageObj?.dict && Object.keys(languageObj.dict).length > 0 ? languageObj.dict : fallbackDict;
20
- const pages = (dict as any).pages || (fallbackDict as any).pages || {};
21
- const buttons = (dict as any).buttons || (fallbackDict as any).buttons || {};
22
-
23
- // Получаем все посты и показываем первые N постов
24
- let allPosts = (await getFilteredCollection('blog')).sort(sortItemsWithFeaturedFirst);
25
- const displayPosts = allPosts.slice(0, siteConfig.postsPerPage || 4);
26
-
27
- // Получаем все рубрики
28
- let allRubrics = await getFilteredCollection('tags');
29
- const rubricSlugs = allRubrics.map((r) => r.data.slug);
30
-
31
- // Получаем все теги с количеством постов и флагом рубрики
32
- let tagsWithCount = getAllTags(allPosts).map((tag) => ({
33
- id: tag.id,
34
- name: tag.name,
35
- count: getPostsByTag(allPosts, tag.id).length,
36
- isRubric: rubricSlugs.includes(tag.id)
37
- }));
38
- if (maugliConfig.showOnlyRubricsTags) {
39
- tagsWithCount = tagsWithCount.filter((tag) => tag.isRubric);
40
- } else {
41
- tagsWithCount = [...tagsWithCount.filter((tag) => tag.isRubric), ...tagsWithCount.filter((tag) => !tag.isRubric)];
42
- }
43
-
44
- // Проверяем, есть ли еще посты для пагинации
45
- const hasMorePosts = allPosts.length > (siteConfig.postsPerPage || 4);
46
-
47
- // Получаем данные для секции блога
48
- const blogSection = await getEntry('pages', 'blog');
49
- ---
50
-
51
- <BaseLayout
52
- title={maugliConfig.indexTitle || pages.index?.title || ''}
53
- description={pages.index?.description || ''}
54
- image={{ src: '/tr-prewiew.png', alt: 'The preview of the site' }}
55
- showHeader={false}
56
- fullWidth={true}
57
- >
58
- <div class="max-w-[1280px] mx-auto">
59
- <h1 class="mb-12 text-3xl leading-tight font-serif font-[800] sm:mb-16 sm:text-5xl" style="color: var(--text-heading)">
60
- {pages.index?.articles || ''}
61
- </h1>
62
- <TagsSection tags={tagsWithCount} totalCount={allPosts.length} />
63
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8 mb-16">
64
- {displayPosts.map((post) => <PostPreview post={post} />)}
65
- </div>
66
-
67
- {/* Кнопка "Ещё статьи" если есть еще посты */}
68
- {
69
- hasMorePosts && (
70
- <div class="text-center mb-8">
71
- <Button href="/blog/2" style="color: var(--text-heading)">
72
- {buttons.morePosts || ''}
73
- </Button>
74
- </div>
75
- )
76
- }
77
- {
78
- blogSection && (
79
- <InfoCard
80
- title={blogSection.data.title}
81
- description={blogSection.body}
82
- image={blogSection.data.image}
83
- jsonld={blogSection.data.jsonld}
84
- class="mb-16"
85
- />
86
- )
87
- }
88
- </div>
89
- {maugliConfig.subscribe?.enabled && <Subscribe class="my-16 sm:my-24" />}
90
- </BaseLayout>
@@ -1,50 +0,0 @@
1
- ---
2
- import type { GetStaticPathsOptions, Page } from 'astro';
3
- import { getEntry, type CollectionEntry } from 'astro:content';
4
- import { getFilteredCollection } from '../../utils/content-loader';
5
- import Breadcrumbs from '../../components/Breadcrumbs.astro';
6
- import InfoCard from '../../components/InfoCard.astro';
7
- import Pagination from '../../components/Pagination.astro';
8
- import ProductPreview from '../../components/ProductPreview.astro';
9
- import { maugliConfig } from '../../config/maugli.config';
10
- import siteConfig from '../../data/site-config';
11
- import { LANGUAGES } from '../../i18n/languages';
12
- import BaseLayout from '../../layouts/BaseLayout.astro';
13
-
14
- const dicts: Record<string, any> = Object.fromEntries(LANGUAGES.map((l) => [l.code, l.dict]));
15
- const lang: string = typeof maugliConfig.defaultLang === 'string' && dicts[maugliConfig.defaultLang] ? maugliConfig.defaultLang : 'en';
16
- const dict = dicts[lang] || dicts['en'];
17
-
18
- const productsTitle = maugliConfig.pageTitles?.products?.trim() || dict.pages?.products?.title || dicts['en'].pages.products.title;
19
- const productsDescription = maugliConfig.productsDescription?.trim() || dict.pages?.products?.description || dicts['en'].pages.products.description;
20
-
21
- export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
22
- const products = (await getFilteredCollection('products')).sort((a, b) => {
23
- if (b.data.isFeatured !== a.data.isFeatured) return b.data.isFeatured - a.data.isFeatured;
24
- return (b.data.publishDate?.getTime?.() || 0) - (a.data.publishDate?.getTime?.() || 0);
25
- });
26
- return paginate(products, { pageSize: siteConfig.productsPerPage || 6 });
27
- }
28
-
29
- type Props = { page: Page<CollectionEntry<'products'>> };
30
-
31
- const { page } = Astro.props;
32
- let productList = page.data;
33
- const productsSection = await getEntry('pages', 'products');
34
- ---
35
-
36
- <BaseLayout title={productsTitle} description={productsDescription} showHeader={false} fullWidth={true}>
37
- <div class="max-w-[1280px] mx-auto">
38
- <Breadcrumbs />
39
- <h1 class="mb-12 text-3xl leading-tight font-serif font-[800] sm:mb-16 sm:text-5xl" style="color: var(--text-heading)">{productsTitle}</h1>
40
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8 mb-16">
41
- {productList.map((product) => <ProductPreview product={product} headingLevel="h2" />)}
42
- </div>
43
- {page.lastPage > 1 && <Pagination page={page} class="my-16 sm:my-24" />}
44
- {
45
- productsSection && (
46
- <InfoCard title={productsSection.data.title} description={productsSection.body} jsonld={productsSection.data.jsonld} class="mt-16" />
47
- )
48
- }
49
- </div>
50
- </BaseLayout>