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.
- package/README.md +29 -15
- package/package.json +6 -13
- package/public/img/examples/blog/previews/post-1-avtomatizaciya-marketinga-kak-ii-osvobozhdaet-predprinimatelei-ot-cifrovogo-rabstva.webp +0 -0
- package/public/img/examples/blog/previews/post-2-avtomatizaciya-kontenta-kak-neiroseti-ubivayut-perfekcionizm-v-biznese.webp +0 -0
- package/public/img/examples/blog/previews/post-3-laik-ne-valyuta-kak-avtomatizaciya-marketinga-spasaet-ot-lozhnyh-metrik.webp +0 -0
- package/public/img/examples/blog/previews/post-5-5-fatalnyh-oshibok-marketinga-kotorye-ubivayut-startapy-na-starte.webp +0 -0
- package/public/img/examples/blog/previews/post-6-5-strategii-kontent-marketinga-dlya-startapov-avtomatizaciya-i-revolyuciya.webp +0 -0
- package/public/img/examples/blog/previews/post-7-viralnyi-kontent-ne-udacha-a-strategiya-avtomatizaciya-marketinga.webp +0 -0
- package/public/img/examples/blog/previews/post-agent-experience-mcp-biznes-v-epohu-ii-agentov.webp +0 -0
- package/public/img/examples/blog/previews/post_11.webp +0 -0
- package/public/img/examples/blog/previews/post_12.webp +0 -0
- package/public/img/examples/blog/previews/post_1_jsonld_guide.webp +0 -0
- package/public/img/examples/blog/previews/test-post.webp +0 -0
- package/public/img/examples/blog/previews/tr-post-1.webp +0 -0
- package/public/img/examples/products/previews/product_1.webp +0 -0
- package/public/img/examples/products/previews/product_2.webp +0 -0
- package/public/img/examples/projects/previews/project_1.webp +0 -0
- package/public/img/examples/projects/previews/project_2.webp +0 -0
- package/scripts/generate-previews.js +175 -0
- package/scripts/update-components.js +166 -0
- package/scripts/upgrade-config.js +23 -3
- package/src/components/LanguageSwitcher.astro +2 -16
- package/astro.config.mjs +0 -92
- package/bin/init.js +0 -201
- package/public/img/default/autor_default.webp +0 -0
- package/public/img/default/blog_default.webp +0 -0
- package/public/img/default/default.webp +0 -0
- package/public/img/default/product_default.webp +0 -0
- package/public/img/default/project_default.webp +0 -0
- package/public/img/default/rubric_default.webp +0 -0
- package/public/img/default/test.webp +0 -0
- package/public/img/default/test2.webp +0 -0
- package/resize-all.cjs +0 -29
- package/src/i18n/de.json +0 -126
- package/src/i18n/en.json +0 -126
- package/src/i18n/es.json +0 -126
- package/src/i18n/fr.json +0 -126
- package/src/i18n/index.ts +0 -10
- package/src/i18n/ja.json +0 -126
- package/src/i18n/languages.ts +0 -23
- package/src/i18n/pt.json +0 -126
- package/src/i18n/ru.json +0 -123
- package/src/i18n/zh.json +0 -126
- package/src/icons/ArrowLeft.astro +0 -13
- package/src/icons/ArrowRight.astro +0 -13
- package/src/icons/flags/brazil.svg +0 -14
- package/src/icons/flags/china.svg +0 -15
- package/src/icons/flags/france.svg +0 -12
- package/src/icons/flags/germany.svg +0 -12
- package/src/icons/flags/japan.svg +0 -11
- package/src/icons/flags/russia.svg +0 -12
- package/src/icons/flags/spain.svg +0 -12
- package/src/icons/flags/united arab emirates.svg +0 -13
- package/src/icons/flags/united states.svg +0 -15
- package/src/icons/socials/BlueskyIcon.astro +0 -9
- package/src/icons/socials/EmailIcon.astro +0 -8
- package/src/icons/socials/LinkedinIcon.astro +0 -9
- package/src/icons/socials/MastodonIcon.astro +0 -9
- package/src/icons/socials/MediumIcon.astro +0 -9
- package/src/icons/socials/RedditIcon.astro +0 -11
- package/src/icons/socials/TelegramIcon.astro +0 -11
- package/src/icons/socials/TwitterIcon.astro +0 -9
- package/src/layouts/BaseLayout.astro +0 -59
- package/src/pages/404.astro +0 -24
- package/src/pages/[...id].astro +0 -50
- package/src/pages/about.astro +0 -0
- package/src/pages/authors/[...page].astro +0 -105
- package/src/pages/authors/[id].astro +0 -175
- package/src/pages/blog/[...page].astro +0 -59
- package/src/pages/blog/[id].astro +0 -175
- package/src/pages/index.astro +0 -90
- package/src/pages/products/[...page].astro +0 -50
- package/src/pages/products/[id].astro +0 -221
- package/src/pages/projects/[...page].astro +0 -74
- package/src/pages/projects/[id].astro +0 -165
- package/src/pages/projects/tags/[id]/[...page].astro +0 -58
- package/src/pages/rss.xml.js +0 -5
- package/src/pages/tags/[id]/[...page].astro +0 -110
- package/src/pages/tags/index.astro +0 -124
- package/src/scripts/infoCardFadeIn.js +0 -22
- package/src/styles/global.css +0 -273
- package/src/utils/common-utils.ts +0 -0
- package/src/utils/content-loader.ts +0 -14
- package/src/utils/data-utils.ts +0 -49
- package/src/utils/featuredManager.ts +0 -118
- package/src/utils/posts.ts +0 -43
- package/src/utils/reading-time.ts +0 -28
- package/src/utils/remark-slugify.js +0 -8
- package/src/utils/rss.ts +0 -23
- package/tsconfig.json +0 -8
- package/typograf-batch.js +0 -49
- 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>
|
package/src/pages/index.astro
DELETED
@@ -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>
|