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,221 +0,0 @@
|
|
1
|
-
---
|
2
|
-
import { render, type CollectionEntry } from 'astro:content';
|
3
|
-
import { getFilteredCollection } from '../../utils/content-loader';
|
4
|
-
import ArticleMeta from '../../components/ArticleMeta.astro';
|
5
|
-
import Breadcrumbs from '../../components/Breadcrumbs.astro';
|
6
|
-
import ContentFooter from '../../components/ContentFooter.astro';
|
7
|
-
import HeroImage from '../../components/HeroImage.astro';
|
8
|
-
import PostPreview from '../../components/PostPreview.astro';
|
9
|
-
import ProjectPreview from '../../components/ProjectPreview.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 { LANGUAGES } from '../../i18n/languages';
|
15
|
-
import BaseLayout from '../../layouts/BaseLayout.astro';
|
16
|
-
import { sortItemsByDateDesc } from '../../utils/data-utils';
|
17
|
-
import { calculateReadingTime } from '../../utils/reading-time';
|
18
|
-
|
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 articlesByTag = dict.pages?.productsId?.articlesByTag || dicts['en'].pages.productsId.articlesByTag;
|
23
|
-
const casesByTag = dict.pages?.productsId?.casesByTag || dicts['en'].pages.productsId.casesByTag;
|
24
|
-
|
25
|
-
export async function getStaticPaths() {
|
26
|
-
const products = (await getFilteredCollection('products')).sort(sortItemsByDateDesc);
|
27
|
-
return products.map((product) => ({
|
28
|
-
params: { id: product.id },
|
29
|
-
props: { product }
|
30
|
-
}));
|
31
|
-
}
|
32
|
-
|
33
|
-
type Props = {
|
34
|
-
product: CollectionEntry<'products'>;
|
35
|
-
};
|
36
|
-
|
37
|
-
const { href } = Astro.url;
|
38
|
-
const { product } = Astro.props;
|
39
|
-
const { title, description, seo, image, tags = [], publishDate } = product.data;
|
40
|
-
const { Content, headings } = await render(product);
|
41
|
-
|
42
|
-
// Вычисляем время чтения на основе контента
|
43
|
-
const readingTime = calculateReadingTime(product.body || '');
|
44
|
-
|
45
|
-
// Преобразуем встроенные заголовки Astro в нужный формат
|
46
|
-
const tocItems = headings
|
47
|
-
.filter((heading) => heading.depth === 2 || heading.depth === 3)
|
48
|
-
.map((heading) => ({
|
49
|
-
title: heading.text,
|
50
|
-
id: heading.slug
|
51
|
-
}));
|
52
|
-
|
53
|
-
// Получаем связанные статьи (blog) и кейсы (projects), где productID совпадает с productID продукта
|
54
|
-
const relatedPosts = (await getFilteredCollection('blog')).filter((post) => post.data.productID === product.data.productID);
|
55
|
-
const relatedProjects = (await getFilteredCollection('projects')).filter((project) => project.data.productID === product.data.productID);
|
56
|
-
|
57
|
-
// Сортировка: сначала featured, потом по дате
|
58
|
-
const sortByFeaturedAndDate = (a, b) => {
|
59
|
-
if ((b.data.isFeatured ? 1 : 0) !== (a.data.isFeatured ? 1 : 0)) return (b.data.isFeatured ? 1 : 0) - (a.data.isFeatured ? 1 : 0);
|
60
|
-
return (b.data.publishDate?.getTime?.() || 0) - (a.data.publishDate?.getTime?.() || 0);
|
61
|
-
};
|
62
|
-
|
63
|
-
const sortedPosts = relatedPosts.sort(sortByFeaturedAndDate);
|
64
|
-
const sortedProjects = relatedProjects.sort(sortByFeaturedAndDate);
|
65
|
-
const moreLabel = dict.buttons?.more || dicts['en'].buttons.more || 'More';
|
66
|
-
---
|
67
|
-
|
68
|
-
<BaseLayout title={seo?.title ?? title} description={seo?.description ?? description} image={seo?.image} pageType="article" showHeader={false} fullWidth={true}>
|
69
|
-
<div class="w-full max-w-none lg:max-w-[1280px] mx-auto lg:px-8 mt-2">
|
70
|
-
<div class="px-4 md:px-0">
|
71
|
-
<Breadcrumbs />
|
72
|
-
</div>
|
73
|
-
<article class="mb-16 sm:mb-24 max-w-3xl mx-auto lg:max-w-none">
|
74
|
-
{
|
75
|
-
(seo?.image?.src || image?.src) && (
|
76
|
-
<HeroImage
|
77
|
-
src={seo?.image?.src || image?.src}
|
78
|
-
alt={seo?.image?.alt || image?.alt || title || 'Продукт без названия'}
|
79
|
-
width={seo?.image?.width || image?.width || 1200}
|
80
|
-
height={seo?.image?.height || image?.height || 630}
|
81
|
-
caption={seo?.image?.caption || image?.caption}
|
82
|
-
/>
|
83
|
-
)
|
84
|
-
}
|
85
|
-
<div class="px-4 mt-2 md:px-0">
|
86
|
-
<ArticleMeta publishDate={publishDate} readingTime={readingTime} />
|
87
|
-
</div>
|
88
|
-
<header class="mb-8 px-4 md:px-0">
|
89
|
-
<h1 class="text-3xl leading-tight font-serif font-[800] sm:text-5xl sm:leading-tight" style="color: var(--text-heading)">{title}</h1>
|
90
|
-
{description && <p class="mt-4 text-lg text-muted leading-relaxed">{description}</p>}
|
91
|
-
</header>
|
92
|
-
</article>
|
93
|
-
<div class="md:grid md:grid-cols-16 md:gap-9">
|
94
|
-
<aside class="hidden sm:block md:col-span-5 lg:col-span-5 min-h-screen">
|
95
|
-
<div class="sticky top-8">
|
96
|
-
{tocItems.length > 0 && <TableOfContents headings={tocItems} />}
|
97
|
-
<TagsAndShare tags={tags} shareUrl={href} title={title} />
|
98
|
-
</div>
|
99
|
-
</aside>
|
100
|
-
<div class="md:col-span-11 px-4 md:px-0 h-auto lg:col-span-11">
|
101
|
-
{/* Summary/FAQ для продукта */}
|
102
|
-
{
|
103
|
-
product.data.generativeEngineOptimization?.generated &&
|
104
|
-
(product.data.generativeEngineOptimization.generated.summary ||
|
105
|
-
product.data.generativeEngineOptimization.generated.highlights?.length > 0 ||
|
106
|
-
product.data.generativeEngineOptimization.generated.faq?.length > 0) && (
|
107
|
-
<div class="not-prose mb-8">
|
108
|
-
<SummaryFAQCard
|
109
|
-
summary={product.data.generativeEngineOptimization.generated.summary}
|
110
|
-
highlights={product.data.generativeEngineOptimization.generated.highlights}
|
111
|
-
faq={product.data.generativeEngineOptimization.generated.faq}
|
112
|
-
/>
|
113
|
-
</div>
|
114
|
-
)
|
115
|
-
}
|
116
|
-
<div class="prose sm:prose-lg lg:prose-xl max-w-none">
|
117
|
-
<Content />
|
118
|
-
</div>
|
119
|
-
<ContentFooter tags={tags} shareUrl={href} title={title} basePath="/products" />
|
120
|
-
{/* Связанные статьи */}
|
121
|
-
{
|
122
|
-
sortedPosts.length > 0 && (
|
123
|
-
<div class="my-16">
|
124
|
-
<h2 class="mb-6 text-xl font-serif text-heading">{articlesByTag}</h2>
|
125
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
126
|
-
{sortedPosts.slice(0, 2).map((post) => (
|
127
|
-
<PostPreview post={post} />
|
128
|
-
))}
|
129
|
-
</div>
|
130
|
-
{sortedPosts.length > 2 && (
|
131
|
-
<details class="mt-4 group">
|
132
|
-
<summary class="cursor-pointer text-brand font-semibold flex items-center gap-2">
|
133
|
-
{moreLabel} ({sortedPosts.length - 2})
|
134
|
-
<svg
|
135
|
-
class="transition-transform duration-300 group-open:rotate-90"
|
136
|
-
width="16"
|
137
|
-
height="16"
|
138
|
-
viewBox="0 0 16 16"
|
139
|
-
fill="none"
|
140
|
-
xmlns="http://www.w3.org/2000/svg"
|
141
|
-
>
|
142
|
-
<g clip-path="url(#clip0_3589_7018)">
|
143
|
-
<path
|
144
|
-
d="M6.16116 0.382763L12.6315 7.09276C13.1219 7.60137 13.122 8.42598 12.6315 8.93458L12.6136 8.95319C12.1231 9.4618 11.328 9.4618 10.8375 8.95319L4.36717 2.24319C3.87673 1.73459 3.87673 0.909975 4.36717 0.40137L4.38511 0.382763C4.87555 -0.125843 5.67071 -0.125842 6.16116 0.382763Z"
|
145
|
-
fill="var(--brand-color)"
|
146
|
-
/>
|
147
|
-
<path
|
148
|
-
d="M12.6136 7.04876L12.6315 7.06737C13.122 7.57597 13.122 8.40059 12.6315 8.90919L6.16116 15.6192C5.67071 16.1278 4.87555 16.1278 4.38511 15.6192L4.36717 15.6006C3.87673 15.092 3.87673 14.2674 4.36717 13.7588L10.8375 7.04876C11.328 6.54016 12.1231 6.54016 12.6136 7.04876Z"
|
149
|
-
fill="var(--brand-color)"
|
150
|
-
/>
|
151
|
-
</g>
|
152
|
-
<defs>
|
153
|
-
<clipPath id="clip0_3589_7018">
|
154
|
-
<rect width="9" height="16" fill="white" transform="matrix(-1 0 0 1 13 0)" />
|
155
|
-
</clipPath>
|
156
|
-
</defs>
|
157
|
-
</svg>
|
158
|
-
</summary>
|
159
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
160
|
-
{sortedPosts.slice(2).map((post) => (
|
161
|
-
<PostPreview post={post} />
|
162
|
-
))}
|
163
|
-
</div>
|
164
|
-
</details>
|
165
|
-
)}
|
166
|
-
</div>
|
167
|
-
)
|
168
|
-
}
|
169
|
-
{/* Связанные кейсы */}
|
170
|
-
{
|
171
|
-
sortedProjects.length > 0 && (
|
172
|
-
<div class="my-16">
|
173
|
-
<h2 class="mb-6 text-xl font-serif text-heading">{casesByTag}</h2>
|
174
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
175
|
-
{sortedProjects.slice(0, 2).map((project) => (
|
176
|
-
<ProjectPreview project={project} />
|
177
|
-
))}
|
178
|
-
</div>
|
179
|
-
{sortedProjects.length > 2 && (
|
180
|
-
<details class="mt-4 group">
|
181
|
-
<summary class="cursor-pointer text-brand font-semibold flex items-center gap-2">
|
182
|
-
{moreLabel} ({sortedProjects.length - 2})
|
183
|
-
<svg
|
184
|
-
class="transition-transform duration-300 group-open:rotate-90"
|
185
|
-
width="16"
|
186
|
-
height="16"
|
187
|
-
viewBox="0 0 16 16"
|
188
|
-
fill="none"
|
189
|
-
xmlns="http://www.w3.org/2000/svg"
|
190
|
-
>
|
191
|
-
<g clip-path="url(#clip0_3589_7018)">
|
192
|
-
<path
|
193
|
-
d="M6.16116 0.382763L12.6315 7.09276C13.1219 7.60137 13.122 8.42598 12.6315 8.93458L12.6136 8.95319C12.1231 9.4618 11.328 9.4618 10.8375 8.95319L4.36717 2.24319C3.87673 1.73459 3.87673 0.909975 4.36717 0.40137L4.38511 0.382763C4.87555 -0.125843 5.67071 -0.125842 6.16116 0.382763Z"
|
194
|
-
fill="var(--brand-color)"
|
195
|
-
/>
|
196
|
-
<path
|
197
|
-
d="M12.6136 7.04876L12.6315 7.06737C13.122 7.57597 13.122 8.40059 12.6315 8.90919L6.16116 15.6192C5.67071 16.1278 4.87555 16.1278 4.38511 15.6192L4.36717 15.6006C3.87673 15.092 3.87673 14.2674 4.36717 13.7588L10.8375 7.04876C11.328 6.54016 12.1231 6.54016 12.6136 7.04876Z"
|
198
|
-
fill="var(--brand-color)"
|
199
|
-
/>
|
200
|
-
</g>
|
201
|
-
<defs>
|
202
|
-
<clipPath id="clip0_3589_7018">
|
203
|
-
<rect width="9" height="16" fill="white" transform="matrix(-1 0 0 1 13 0)" />
|
204
|
-
</clipPath>
|
205
|
-
</defs>
|
206
|
-
</svg>
|
207
|
-
</summary>
|
208
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
209
|
-
{sortedProjects.slice(2).map((project) => (
|
210
|
-
<ProjectPreview project={project} />
|
211
|
-
))}
|
212
|
-
</div>
|
213
|
-
</details>
|
214
|
-
)}
|
215
|
-
</div>
|
216
|
-
)
|
217
|
-
}
|
218
|
-
</div>
|
219
|
-
</div>
|
220
|
-
</div>
|
221
|
-
</BaseLayout>
|
@@ -1,74 +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 ProjectPreview from '../../components/ProjectPreview.astro';
|
9
|
-
import TagsSection from '../../components/TagsSection.astro';
|
10
|
-
import { maugliConfig } from '../../config/maugli.config';
|
11
|
-
import siteConfig from '../../data/site-config';
|
12
|
-
import { LANGUAGES } from '../../i18n/languages';
|
13
|
-
import BaseLayout from '../../layouts/BaseLayout.astro';
|
14
|
-
import { slugify } from '../../utils/common-utils';
|
15
|
-
import { sortItemsByDateDesc } from '../../utils/data-utils';
|
16
|
-
|
17
|
-
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
|
18
|
-
const projects = (await getFilteredCollection('projects')).sort(sortItemsByDateDesc);
|
19
|
-
return paginate(projects, { pageSize: siteConfig.projectsPerPage || 6 });
|
20
|
-
}
|
21
|
-
|
22
|
-
type Props = { page: Page<CollectionEntry<'projects'>> };
|
23
|
-
|
24
|
-
const { page } = Astro.props;
|
25
|
-
let portfolio = page.data;
|
26
|
-
let allProjects = await getFilteredCollection('projects');
|
27
|
-
// Получаем все уникальные теги проектов
|
28
|
-
const allTags = [...new Set(allProjects.flatMap((project) => project.data.tags || []))];
|
29
|
-
const tagsWithCount = allTags.map((tag) => ({
|
30
|
-
id: slugify(tag),
|
31
|
-
name: tag,
|
32
|
-
count: allProjects.filter((project) => (project.data.tags || []).includes(tag)).length
|
33
|
-
}));
|
34
|
-
const projectsSection = await getEntry('pages', 'projects');
|
35
|
-
|
36
|
-
const dicts: Record<string, any> = Object.fromEntries(LANGUAGES.map((l) => [l.code, l.dict]));
|
37
|
-
const lang: string = typeof maugliConfig.defaultLang === 'string' && dicts[maugliConfig.defaultLang] ? maugliConfig.defaultLang : 'en';
|
38
|
-
const dict = dicts[lang] || dicts['en'];
|
39
|
-
|
40
|
-
// Получаем мультиязычный title для проектов (кейсов)
|
41
|
-
const projectsTitle = maugliConfig.pageTitles?.projects?.trim() || dict.pages?.projects?.title || dicts['en'].pages.projects.title;
|
42
|
-
// Описание для проектов (кейсов) — если появится в словаре или конфиге
|
43
|
-
const projectsDescription = maugliConfig.pageTitles?.projectsDescription?.trim?.() || '';
|
44
|
-
---
|
45
|
-
|
46
|
-
<BaseLayout
|
47
|
-
title={projectsTitle}
|
48
|
-
description={projectsDescription}
|
49
|
-
image={{ src: '/dante-preview.jpg', alt: 'The preview of the site' }}
|
50
|
-
showHeader={false}
|
51
|
-
fullWidth={true}
|
52
|
-
>
|
53
|
-
<div class="max-w-[1280px] mx-auto">
|
54
|
-
<Breadcrumbs />
|
55
|
-
|
56
|
-
<h1 class="mb-12 text-3xl leading-tight font-serif font-[800] sm:mb-16 sm:text-5xl" style="color: var(--text-heading)">{projectsTitle}</h1>
|
57
|
-
|
58
|
-
<!-- Блок тегов -->
|
59
|
-
{tagsWithCount.length > 0 && <TagsSection tags={tagsWithCount} basePath="/projects" totalCount={allProjects.length} class="mb-12" />}
|
60
|
-
|
61
|
-
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8 mb-16">
|
62
|
-
{portfolio.map((project) => <ProjectPreview project={project} />)}
|
63
|
-
</div>
|
64
|
-
|
65
|
-
<!-- Пагинация (показывается только если больше одной страницы) -->
|
66
|
-
{page.lastPage > 1 && <Pagination page={page} class="my-16 sm:my-24" />}
|
67
|
-
|
68
|
-
{
|
69
|
-
projectsSection && (
|
70
|
-
<InfoCard title={projectsSection.data.title} description={projectsSection.body} jsonld={projectsSection.data.jsonld} class="mt-16" />
|
71
|
-
)
|
72
|
-
}
|
73
|
-
</div>
|
74
|
-
</BaseLayout>
|
@@ -1,165 +0,0 @@
|
|
1
|
-
---
|
2
|
-
import { getCollection, render, type CollectionEntry } from 'astro:content';
|
3
|
-
import ArticleMeta from '../../components/ArticleMeta.astro';
|
4
|
-
import Breadcrumbs from '../../components/Breadcrumbs.astro';
|
5
|
-
import ContentFooter from '../../components/ContentFooter.astro';
|
6
|
-
import HeroImage from '../../components/HeroImage.astro';
|
7
|
-
import ProductBannerCard from '../../components/ProductBannerCard.astro';
|
8
|
-
import ProjectPreview from '../../components/ProjectPreview.astro';
|
9
|
-
import SummaryFAQCard from '../../components/SummaryFAQCard.astro';
|
10
|
-
import TableOfContents from '../../components/TableOfContents.astro';
|
11
|
-
import TagsAndShare from '../../components/TagsAndShare.astro';
|
12
|
-
import { maugliConfig } from '../../config/maugli.config';
|
13
|
-
import { LANGUAGES } from '../../i18n/languages';
|
14
|
-
import BaseLayout from '../../layouts/BaseLayout.astro';
|
15
|
-
import { sortItemsByDateDesc } from '../../utils/data-utils';
|
16
|
-
import { calculateReadingTime } from '../../utils/reading-time';
|
17
|
-
|
18
|
-
export async function getStaticPaths() {
|
19
|
-
const projects = (await getCollection('projects')).sort(sortItemsByDateDesc);
|
20
|
-
const projectCount = projects.length;
|
21
|
-
return projects.map((project, index) => ({
|
22
|
-
params: { id: project.id },
|
23
|
-
props: {
|
24
|
-
project,
|
25
|
-
prevProject: index + 1 !== projectCount ? projects[index + 1] : null,
|
26
|
-
nextProject: index !== 0 ? projects[index - 1] : null
|
27
|
-
}
|
28
|
-
}));
|
29
|
-
}
|
30
|
-
|
31
|
-
type Props = {
|
32
|
-
project: CollectionEntry<'projects'>;
|
33
|
-
prevProject: CollectionEntry<'projects'>;
|
34
|
-
nextProject: CollectionEntry<'projects'>;
|
35
|
-
};
|
36
|
-
|
37
|
-
const { href } = Astro.url;
|
38
|
-
const { project, prevProject, nextProject } = Astro.props;
|
39
|
-
const { title, description, seo, image, tags = [], publishDate } = project.data;
|
40
|
-
const { Content, headings } = await render(project);
|
41
|
-
|
42
|
-
// Вычисляем время чтения на основе контента
|
43
|
-
const readingTime = calculateReadingTime(project.body || '');
|
44
|
-
|
45
|
-
// Преобразуем встроенные заголовки Astro в нужный формат
|
46
|
-
const tocItems = headings
|
47
|
-
.filter((heading) => heading.depth === 2 || heading.depth === 3) // Только H2 и H3
|
48
|
-
.map((heading) => ({
|
49
|
-
title: heading.text,
|
50
|
-
id: heading.slug
|
51
|
-
}));
|
52
|
-
|
53
|
-
const productIds = project.data.products || [];
|
54
|
-
const allProducts = await getCollection('products');
|
55
|
-
const products = productIds.map((id) => allProducts.find((p) => p.id === id)).filter(Boolean);
|
56
|
-
|
57
|
-
const lang: string = typeof maugliConfig.defaultLang === 'string' ? maugliConfig.defaultLang : 'en';
|
58
|
-
const languageObj = LANGUAGES.find((l) => l.code === lang) || LANGUAGES.find((l) => l.code === 'en');
|
59
|
-
const fallbackDict = LANGUAGES.find((l) => l.code === 'en')?.dict || {};
|
60
|
-
const dict = languageObj?.dict && Object.keys(languageObj.dict).length > 0 ? languageObj.dict : fallbackDict;
|
61
|
-
const pages = (dict as any).pages || (fallbackDict as any).pages || {};
|
62
|
-
const moreByTag = pages.projects?.moreByTag || 'More cases';
|
63
|
-
---
|
64
|
-
|
65
|
-
<BaseLayout title={seo?.title ?? title} description={seo?.description ?? description} image={seo?.image} pageType="article" showHeader={false} fullWidth={true}>
|
66
|
-
<!-- Расширяем контейнер для двухколоночной структуры -->
|
67
|
-
<div class="w-full max-w-none lg:max-w-[1280px] mx-auto lg:px-8 mt-2">
|
68
|
-
<!-- Хлебные крошки -->
|
69
|
-
<div class="px-4 md:px-0">
|
70
|
-
<Breadcrumbs />
|
71
|
-
</div>
|
72
|
-
|
73
|
-
<!-- Метаинформация над картинкой -->
|
74
|
-
|
75
|
-
<article class="mb-16 sm:mb-24 max-w-3xl mx-auto lg:max-w-none">
|
76
|
-
<HeroImage
|
77
|
-
src={seo?.image?.src || image?.src || '/default.webp'}
|
78
|
-
alt={seo?.image?.alt || image?.alt || title || 'No name project'}
|
79
|
-
width={seo?.image?.width || image?.width || 1200}
|
80
|
-
height={seo?.image?.height || image?.height || 630}
|
81
|
-
caption={seo?.image?.caption || image?.caption}
|
82
|
-
/>
|
83
|
-
<div class="px-4 mt-2 md:px-0">
|
84
|
-
<ArticleMeta publishDate={publishDate} readingTime={readingTime} />
|
85
|
-
</div>
|
86
|
-
|
87
|
-
<header class="mb-8 px-4 mt-12 md:px-0">
|
88
|
-
<h1 class="text-3xl leading-tight font-serif font-[800] sm:text-5xl sm:leading-tight" style="color: var(--text-heading)">{title}</h1>
|
89
|
-
{description && <p class="mt-4 text-lg text-muted leading-relaxed">{description}</p>}
|
90
|
-
</header>
|
91
|
-
</article>
|
92
|
-
|
93
|
-
<!-- 16-колоночная сетка -->
|
94
|
-
<div class="md:grid md:grid-cols-16 md:gap-9">
|
95
|
-
<!-- Боковая панель с Table of Contents -->
|
96
|
-
<aside class="hidden sm:block md:col-span-5 lg:col-span-5 min-h-screen">
|
97
|
-
<div class="sticky top-8">
|
98
|
-
{tocItems.length > 0 && <TableOfContents headings={tocItems} />}
|
99
|
-
<TagsAndShare tags={tags} shareUrl={href} title={title} />
|
100
|
-
{/* Карточка продукта: показываем если есть productLink в проекте */}
|
101
|
-
{
|
102
|
-
project.data.productLink && (
|
103
|
-
<div class="mt-1">
|
104
|
-
<ProductBannerCard
|
105
|
-
product={{
|
106
|
-
data: {
|
107
|
-
productLink: project.data.productLink,
|
108
|
-
image: {
|
109
|
-
src: project.data.productID ? `/public/${project.data.productID}.webp` : maugliConfig.seo.defaultImage,
|
110
|
-
alt: project.data.title || 'Продукт'
|
111
|
-
},
|
112
|
-
title: project.data.title || 'Продукт'
|
113
|
-
}
|
114
|
-
}}
|
115
|
-
/>
|
116
|
-
</div>
|
117
|
-
)
|
118
|
-
}
|
119
|
-
</div>
|
120
|
-
</aside>
|
121
|
-
|
122
|
-
<!-- Основной контент -->
|
123
|
-
<div class="md:col-span-11 px-4 md:px-0 h-auto lg:col-span-11">
|
124
|
-
<!-- Summary/FAQ в начале контента -->
|
125
|
-
{
|
126
|
-
project.data.generativeEngineOptimization?.generated &&
|
127
|
-
(project.data.generativeEngineOptimization.generated.summary ||
|
128
|
-
project.data.generativeEngineOptimization.generated.highlights?.length > 0 ||
|
129
|
-
project.data.generativeEngineOptimization.generated.faq?.length > 0) && (
|
130
|
-
<div class="not-prose mb-8">
|
131
|
-
<SummaryFAQCard
|
132
|
-
summary={project.data.generativeEngineOptimization.generated.summary}
|
133
|
-
highlights={project.data.generativeEngineOptimization.generated.highlights}
|
134
|
-
faq={project.data.generativeEngineOptimization.generated.faq}
|
135
|
-
/>
|
136
|
-
</div>
|
137
|
-
)
|
138
|
-
}
|
139
|
-
|
140
|
-
<div class="prose sm:prose-lg lg:prose-xl max-w-none">
|
141
|
-
<Content />
|
142
|
-
</div>
|
143
|
-
|
144
|
-
<!-- Блок тегов и кнопки поделиться под контентом -->
|
145
|
-
<ContentFooter tags={tags} shareUrl={href} title={title} basePath="/projects" class="hidden sm:block" />
|
146
|
-
|
147
|
-
{/* Мобильный блок: поделиться и "Больше о продукте" в один ряд */}
|
148
|
-
<ContentFooter tags={tags} shareUrl={href} title={title} basePath="/projects" class="sm:hidden" productLink={project.data.productLink} />
|
149
|
-
|
150
|
-
<!-- Блок "Еще по теме" в контенте -->
|
151
|
-
{
|
152
|
-
(prevProject || nextProject) && (
|
153
|
-
<div class="my-16 sm:my-24">
|
154
|
-
<h2 class="mb-12 text-xl font-serif sm:mb-16 sm:text-2xl text-heading">{moreByTag}</h2>
|
155
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8">
|
156
|
-
{nextProject && <ProjectPreview project={nextProject} headingLevel="h3" />}
|
157
|
-
{prevProject && <ProjectPreview project={prevProject} headingLevel="h3" />}
|
158
|
-
</div>
|
159
|
-
</div>
|
160
|
-
)
|
161
|
-
}
|
162
|
-
</div>
|
163
|
-
</div>
|
164
|
-
</div>
|
165
|
-
</BaseLayout>
|
@@ -1,58 +0,0 @@
|
|
1
|
-
---
|
2
|
-
import type { GetStaticPathsOptions, Page } from 'astro';
|
3
|
-
import { getCollection, type CollectionEntry } from 'astro:content';
|
4
|
-
import Breadcrumbs from '../../../../components/Breadcrumbs.astro';
|
5
|
-
import Pagination from '../../../../components/Pagination.astro';
|
6
|
-
import ProjectPreview from '../../../../components/ProjectPreview.astro';
|
7
|
-
import TagsSection from '../../../../components/TagsSection.astro';
|
8
|
-
import siteConfig from '../../../../data/site-config';
|
9
|
-
import BaseLayout from '../../../../layouts/BaseLayout.astro';
|
10
|
-
import { slugify } from '../../../../utils/common-utils';
|
11
|
-
import { sortItemsByDateDesc } from '../../../../utils/data-utils';
|
12
|
-
|
13
|
-
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
|
14
|
-
const allProjects = (await getCollection('projects')).sort(sortItemsByDateDesc);
|
15
|
-
const allTags = [...new Set(allProjects.flatMap((project) => project.data.tags || []))];
|
16
|
-
|
17
|
-
return allTags.flatMap((tag) => {
|
18
|
-
const filteredProjects = allProjects.filter((project) => (project.data.tags || []).includes(tag));
|
19
|
-
return paginate(filteredProjects, {
|
20
|
-
params: { id: slugify(tag) },
|
21
|
-
pageSize: siteConfig.projectsPerPage || 6,
|
22
|
-
props: { tag }
|
23
|
-
});
|
24
|
-
});
|
25
|
-
}
|
26
|
-
|
27
|
-
type Props = { page: Page<CollectionEntry<'projects'>>; tag: string };
|
28
|
-
|
29
|
-
const { page, tag } = Astro.props;
|
30
|
-
const projects = page.data;
|
31
|
-
|
32
|
-
// Получаем все теги для отображения в TagsSection
|
33
|
-
const allProjects = await getCollection('projects');
|
34
|
-
const allTags = [...new Set(allProjects.flatMap((project) => project.data.tags || []))];
|
35
|
-
const tagsWithCount = allTags.map((tagName) => ({
|
36
|
-
id: slugify(tagName),
|
37
|
-
name: tagName,
|
38
|
-
count: allProjects.filter((project) => (project.data.tags || []).includes(tagName)).length
|
39
|
-
}));
|
40
|
-
---
|
41
|
-
|
42
|
-
<BaseLayout title={`Сервисы по тегу "${tag}"`} description={`Explore projects tagged with "${tag}"`} showHeader={false} fullWidth={true}>
|
43
|
-
<div class="max-w-[1280px] mx-auto">
|
44
|
-
<Breadcrumbs />
|
45
|
-
|
46
|
-
<h1 class="mb-12 text-3xl leading-tight font-serif font-[800] sm:mb-16 sm:text-5xl" style="color: var(--text-heading)">Сервисы</h1>
|
47
|
-
|
48
|
-
<!-- Блок тегов -->
|
49
|
-
<TagsSection tags={tagsWithCount} activeTagId={slugify(tag)} basePath="/projects" totalCount={allProjects.length} class="mb-12" />
|
50
|
-
|
51
|
-
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8 mb-16">
|
52
|
-
{projects.map((project) => <ProjectPreview project={project} />)}
|
53
|
-
</div>
|
54
|
-
|
55
|
-
<!-- Пагинация (показывается только если больше одной страницы) -->
|
56
|
-
{page.lastPage > 1 && <Pagination page={page} class="my-16 sm:my-24" />}
|
57
|
-
</div>
|
58
|
-
</BaseLayout>
|
package/src/pages/rss.xml.js
DELETED
@@ -1,110 +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 Pagination from '../../../components/Pagination.astro';
|
7
|
-
import PostPreview from '../../../components/PostPreview.astro';
|
8
|
-
import Subscribe from '../../../components/Subscribe.astro';
|
9
|
-
import TagsSection from '../../../components/TagsSection.astro';
|
10
|
-
import { maugliConfig } from '../../../config/maugli.config';
|
11
|
-
import siteConfig from '../../../data/site-config';
|
12
|
-
import { LANGUAGES } from '../../../i18n/languages';
|
13
|
-
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
14
|
-
import { getAllTags, getPostsByTag, sortItemsWithFeaturedFirst } from '../../../utils/data-utils';
|
15
|
-
|
16
|
-
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
|
17
|
-
const posts = (await getFilteredCollection('blog')).sort(sortItemsWithFeaturedFirst);
|
18
|
-
const tags = getAllTags(posts);
|
19
|
-
|
20
|
-
return tags.flatMap((tag) => {
|
21
|
-
const filteredPosts = getPostsByTag(posts, tag.id);
|
22
|
-
return paginate(filteredPosts, {
|
23
|
-
params: { id: tag.id },
|
24
|
-
pageSize: siteConfig.postsPerPage || 4
|
25
|
-
});
|
26
|
-
});
|
27
|
-
}
|
28
|
-
|
29
|
-
type Props = { page: Page<CollectionEntry<'blog'>> };
|
30
|
-
|
31
|
-
const { page } = Astro.props;
|
32
|
-
const blog = page.data;
|
33
|
-
const params = Astro.params;
|
34
|
-
const allPosts = await getFilteredCollection('blog');
|
35
|
-
const allTags = getAllTags(allPosts);
|
36
|
-
const allRubrics = await getFilteredCollection('tags');
|
37
|
-
const currentTag = allTags.find((tag) => tag.id === params.id);
|
38
|
-
|
39
|
-
// Получаем все теги с количеством постов для TagsSection
|
40
|
-
let tagsWithCount = allTags.map((tag) => {
|
41
|
-
// Проверяем, есть ли карточка рубрики для этого тега
|
42
|
-
const hasRubricCard = allRubrics.some((rubric) => rubric.data.slug === tag.id);
|
43
|
-
return {
|
44
|
-
id: tag.id,
|
45
|
-
name: tag.name,
|
46
|
-
count: getPostsByTag(allPosts, tag.id).length,
|
47
|
-
isRubric: hasRubricCard
|
48
|
-
};
|
49
|
-
});
|
50
|
-
|
51
|
-
tagsWithCount = [...tagsWithCount.filter((tag) => tag.isRubric), ...tagsWithCount.filter((tag) => !tag.isRubric)];
|
52
|
-
|
53
|
-
// Получаем рубрику из коллекции tags (если есть)
|
54
|
-
const rubric = allRubrics.find((tag) => tag.data.slug === params.id && tag.data.isRubric);
|
55
|
-
let rubricCard = null;
|
56
|
-
if (rubric) {
|
57
|
-
const postCount = getPostsByTag(allPosts, rubric.data.slug).length;
|
58
|
-
const lastPost = allPosts
|
59
|
-
.filter((post) => (post.data.tags || []).includes(rubric.data.slug))
|
60
|
-
.sort((a, b) => new Date(b.data.updatedDate || b.data.publishDate).getTime() - new Date(a.data.updatedDate || a.data.publishDate).getTime())[0];
|
61
|
-
const updatedAt = lastPost ? lastPost.data.updatedDate || lastPost.data.publishDate : undefined;
|
62
|
-
rubricCard = {
|
63
|
-
slug: rubric.data.slug,
|
64
|
-
title: rubric.data.title,
|
65
|
-
description: rubric.data.description,
|
66
|
-
image: rubric.data.image,
|
67
|
-
postCount,
|
68
|
-
updatedAt,
|
69
|
-
quote: rubric.data.quote,
|
70
|
-
isFeatured: rubric.data.isFeatured
|
71
|
-
};
|
72
|
-
}
|
73
|
-
const rubricsSection = await getEntry('pages', 'rubrics');
|
74
|
-
|
75
|
-
const dicts: Record<string, any> = Object.fromEntries(LANGUAGES.map((l) => [l.code, l.dict]));
|
76
|
-
const lang: string = typeof maugliConfig.defaultLang === 'string' && dicts[maugliConfig.defaultLang] ? maugliConfig.defaultLang : 'en';
|
77
|
-
const dict = dicts[lang] || dicts['en'];
|
78
|
-
|
79
|
-
function interpolate(str, vars) {
|
80
|
-
return str.replace(/\{(\w+)\}/g, (_, k) => vars[k] ?? '');
|
81
|
-
}
|
82
|
-
|
83
|
-
const tagName = rubricCard ? rubricCard.title : currentTag?.name;
|
84
|
-
const titleTemplate = dict.pages?.tags?.titleWithTag || dicts['en'].pages.tags.titleWithTag || 'Posts tagged {tag}';
|
85
|
-
const descTemplate =
|
86
|
-
dict.pages?.tags?.descriptionWithTag || dicts['en'].pages.tags.descriptionWithTag || 'Explore a curated collection of blog posts under {tag}';
|
87
|
-
const pageTitle = interpolate(titleTemplate, { tag: tagName });
|
88
|
-
const pageDesc = interpolate(descTemplate, { tag: tagName });
|
89
|
-
---
|
90
|
-
|
91
|
-
<BaseLayout title={pageTitle} description={pageDesc} image={{ src: '/dante-preview.jpg', alt: 'The preview of the site' }} showHeader={false} fullWidth={true}>
|
92
|
-
<div class="max-w-[1280px] mx-auto">
|
93
|
-
<Breadcrumbs />
|
94
|
-
|
95
|
-
<div class="mb-12 sm:mb-16">
|
96
|
-
<h1 class="mb-4 text-3xl leading-tight font-serif font-[800] sm:text-5xl" style="color: var(--text-heading)">
|
97
|
-
{currentTag?.name}
|
98
|
-
</h1>
|
99
|
-
</div>
|
100
|
-
<TagsSection tags={tagsWithCount} totalCount={allPosts.length} activeTagId={params.id} />
|
101
|
-
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8 mb-16">
|
102
|
-
{blog.map((post) => <PostPreview post={post} />)}
|
103
|
-
</div>
|
104
|
-
|
105
|
-
<!-- Пагинация (показывается только если больше одной страницы) -->
|
106
|
-
{page.lastPage > 1 && <Pagination page={page} class="my-16 sm:my-24" />}
|
107
|
-
{/* InfoCard о рубриках перенесён на страницу всех тегов */}
|
108
|
-
</div>
|
109
|
-
<Subscribe class="my-16 sm:my-24" />
|
110
|
-
</BaseLayout>
|