koztv-blog-tools 1.0.0

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.
@@ -0,0 +1,117 @@
1
+ interface Post {
2
+ idx: number;
3
+ msg_id: number;
4
+ date: string;
5
+ channel_id: number;
6
+ channel_title: string;
7
+ channel_username: string;
8
+ link: string;
9
+ views: number;
10
+ forwards: number;
11
+ has_media: boolean;
12
+ content: string;
13
+ slug: string;
14
+ attachments: string[];
15
+ }
16
+ interface GroupedPost {
17
+ id: string;
18
+ slug: string;
19
+ title: string;
20
+ excerpt: string;
21
+ date: string;
22
+ category: string;
23
+ posts: Post[];
24
+ hasMedia: boolean;
25
+ totalViews: number;
26
+ coverImage?: string;
27
+ images: string[];
28
+ videos: string[];
29
+ }
30
+ interface ParsePostOptions {
31
+ /** Generate slug from content */
32
+ generateSlug?: boolean;
33
+ /** Extract attachments from content */
34
+ extractAttachments?: boolean;
35
+ }
36
+ /**
37
+ * Clean content by removing attachment sections and media references
38
+ */
39
+ declare function cleanContent(text: string): string;
40
+ /**
41
+ * Generate URL-safe slug from text
42
+ */
43
+ declare function generateSlug(text: string, fallbackIdx: number): string;
44
+ /**
45
+ * Extract title from post content
46
+ */
47
+ declare function extractTitle(content: string, fallback?: string): string;
48
+ /**
49
+ * Extract excerpt from post content (skips first line which is title)
50
+ */
51
+ declare function extractExcerpt(content: string, maxLength?: number): string;
52
+ /**
53
+ * Extract media attachments from content
54
+ */
55
+ declare function extractAttachments(content: string): string[];
56
+ /**
57
+ * Categorize post based on content keywords
58
+ */
59
+ declare function categorizePost(content: string, defaultCategory?: string): string;
60
+ /**
61
+ * Parse a markdown file content into a Post object
62
+ */
63
+ declare function parsePost(fileContent: string, options?: ParsePostOptions): Post;
64
+ /**
65
+ * Group posts by time proximity (posts within timeWindowMs are grouped together)
66
+ */
67
+ declare function groupPosts(posts: Post[], timeWindowMs?: number): GroupedPost[];
68
+ /**
69
+ * Deduplicate posts by msg_id
70
+ */
71
+ declare function deduplicatePosts(posts: Post[]): Post[];
72
+
73
+ /**
74
+ * Analytics tracking utilities
75
+ * Works with Yandex Metrica, Google Analytics, or custom trackers
76
+ */
77
+ type GoalName = 'click_book_appointment' | 'click_telegram_blog' | 'click_telegram_header' | 'click_telegram_footer' | 'click_telegram_contact' | 'click_learn_more' | 'click_service' | string;
78
+ interface GoalParams {
79
+ page?: string;
80
+ service?: string;
81
+ source?: string;
82
+ [key: string]: unknown;
83
+ }
84
+ interface AnalyticsConfig {
85
+ /** Yandex Metrika counter ID */
86
+ yandexId?: number;
87
+ /** Google Analytics measurement ID */
88
+ googleId?: string;
89
+ /** Custom tracking function */
90
+ customTracker?: (goal: string, params?: GoalParams) => void;
91
+ }
92
+ /**
93
+ * Configure analytics tracking
94
+ */
95
+ declare function configureAnalytics(options: AnalyticsConfig): void;
96
+ /**
97
+ * Track a goal/event
98
+ */
99
+ declare function trackGoal(goal: GoalName, params?: GoalParams): void;
100
+ /**
101
+ * Track appointment booking click
102
+ */
103
+ declare function trackBookAppointment(source: string): void;
104
+ /**
105
+ * Track Telegram link click
106
+ */
107
+ declare function trackTelegramClick(source: 'blog' | 'header' | 'footer' | 'contact' | string, page?: string): void;
108
+ /**
109
+ * Track "Learn more" click
110
+ */
111
+ declare function trackLearnMore(service: string): void;
112
+ /**
113
+ * Track service click
114
+ */
115
+ declare function trackServiceClick(service: string): void;
116
+
117
+ export { type AnalyticsConfig, type GoalName, type GoalParams, type GroupedPost, type ParsePostOptions, type Post, categorizePost, cleanContent, configureAnalytics, deduplicatePosts, extractAttachments, extractExcerpt, extractTitle, generateSlug, groupPosts, parsePost, trackBookAppointment, trackGoal, trackLearnMore, trackServiceClick, trackTelegramClick };
@@ -0,0 +1,117 @@
1
+ interface Post {
2
+ idx: number;
3
+ msg_id: number;
4
+ date: string;
5
+ channel_id: number;
6
+ channel_title: string;
7
+ channel_username: string;
8
+ link: string;
9
+ views: number;
10
+ forwards: number;
11
+ has_media: boolean;
12
+ content: string;
13
+ slug: string;
14
+ attachments: string[];
15
+ }
16
+ interface GroupedPost {
17
+ id: string;
18
+ slug: string;
19
+ title: string;
20
+ excerpt: string;
21
+ date: string;
22
+ category: string;
23
+ posts: Post[];
24
+ hasMedia: boolean;
25
+ totalViews: number;
26
+ coverImage?: string;
27
+ images: string[];
28
+ videos: string[];
29
+ }
30
+ interface ParsePostOptions {
31
+ /** Generate slug from content */
32
+ generateSlug?: boolean;
33
+ /** Extract attachments from content */
34
+ extractAttachments?: boolean;
35
+ }
36
+ /**
37
+ * Clean content by removing attachment sections and media references
38
+ */
39
+ declare function cleanContent(text: string): string;
40
+ /**
41
+ * Generate URL-safe slug from text
42
+ */
43
+ declare function generateSlug(text: string, fallbackIdx: number): string;
44
+ /**
45
+ * Extract title from post content
46
+ */
47
+ declare function extractTitle(content: string, fallback?: string): string;
48
+ /**
49
+ * Extract excerpt from post content (skips first line which is title)
50
+ */
51
+ declare function extractExcerpt(content: string, maxLength?: number): string;
52
+ /**
53
+ * Extract media attachments from content
54
+ */
55
+ declare function extractAttachments(content: string): string[];
56
+ /**
57
+ * Categorize post based on content keywords
58
+ */
59
+ declare function categorizePost(content: string, defaultCategory?: string): string;
60
+ /**
61
+ * Parse a markdown file content into a Post object
62
+ */
63
+ declare function parsePost(fileContent: string, options?: ParsePostOptions): Post;
64
+ /**
65
+ * Group posts by time proximity (posts within timeWindowMs are grouped together)
66
+ */
67
+ declare function groupPosts(posts: Post[], timeWindowMs?: number): GroupedPost[];
68
+ /**
69
+ * Deduplicate posts by msg_id
70
+ */
71
+ declare function deduplicatePosts(posts: Post[]): Post[];
72
+
73
+ /**
74
+ * Analytics tracking utilities
75
+ * Works with Yandex Metrica, Google Analytics, or custom trackers
76
+ */
77
+ type GoalName = 'click_book_appointment' | 'click_telegram_blog' | 'click_telegram_header' | 'click_telegram_footer' | 'click_telegram_contact' | 'click_learn_more' | 'click_service' | string;
78
+ interface GoalParams {
79
+ page?: string;
80
+ service?: string;
81
+ source?: string;
82
+ [key: string]: unknown;
83
+ }
84
+ interface AnalyticsConfig {
85
+ /** Yandex Metrika counter ID */
86
+ yandexId?: number;
87
+ /** Google Analytics measurement ID */
88
+ googleId?: string;
89
+ /** Custom tracking function */
90
+ customTracker?: (goal: string, params?: GoalParams) => void;
91
+ }
92
+ /**
93
+ * Configure analytics tracking
94
+ */
95
+ declare function configureAnalytics(options: AnalyticsConfig): void;
96
+ /**
97
+ * Track a goal/event
98
+ */
99
+ declare function trackGoal(goal: GoalName, params?: GoalParams): void;
100
+ /**
101
+ * Track appointment booking click
102
+ */
103
+ declare function trackBookAppointment(source: string): void;
104
+ /**
105
+ * Track Telegram link click
106
+ */
107
+ declare function trackTelegramClick(source: 'blog' | 'header' | 'footer' | 'contact' | string, page?: string): void;
108
+ /**
109
+ * Track "Learn more" click
110
+ */
111
+ declare function trackLearnMore(service: string): void;
112
+ /**
113
+ * Track service click
114
+ */
115
+ declare function trackServiceClick(service: string): void;
116
+
117
+ export { type AnalyticsConfig, type GoalName, type GoalParams, type GroupedPost, type ParsePostOptions, type Post, categorizePost, cleanContent, configureAnalytics, deduplicatePosts, extractAttachments, extractExcerpt, extractTitle, generateSlug, groupPosts, parsePost, trackBookAppointment, trackGoal, trackLearnMore, trackServiceClick, trackTelegramClick };
package/dist/index.js ADDED
@@ -0,0 +1,324 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ categorizePost: () => categorizePost,
34
+ cleanContent: () => cleanContent,
35
+ configureAnalytics: () => configureAnalytics,
36
+ deduplicatePosts: () => deduplicatePosts,
37
+ extractAttachments: () => extractAttachments,
38
+ extractExcerpt: () => extractExcerpt,
39
+ extractTitle: () => extractTitle,
40
+ generateSlug: () => generateSlug,
41
+ groupPosts: () => groupPosts,
42
+ parsePost: () => parsePost,
43
+ trackBookAppointment: () => trackBookAppointment,
44
+ trackGoal: () => trackGoal,
45
+ trackLearnMore: () => trackLearnMore,
46
+ trackServiceClick: () => trackServiceClick,
47
+ trackTelegramClick: () => trackTelegramClick
48
+ });
49
+ module.exports = __toCommonJS(index_exports);
50
+
51
+ // src/posts.ts
52
+ var import_gray_matter = __toESM(require("gray-matter"));
53
+ var translitMap = {
54
+ "\u0430": "a",
55
+ "\u0431": "b",
56
+ "\u0432": "v",
57
+ "\u0433": "g",
58
+ "\u0434": "d",
59
+ "\u0435": "e",
60
+ "\u0451": "yo",
61
+ "\u0436": "zh",
62
+ "\u0437": "z",
63
+ "\u0438": "i",
64
+ "\u0439": "y",
65
+ "\u043A": "k",
66
+ "\u043B": "l",
67
+ "\u043C": "m",
68
+ "\u043D": "n",
69
+ "\u043E": "o",
70
+ "\u043F": "p",
71
+ "\u0440": "r",
72
+ "\u0441": "s",
73
+ "\u0442": "t",
74
+ "\u0443": "u",
75
+ "\u0444": "f",
76
+ "\u0445": "h",
77
+ "\u0446": "ts",
78
+ "\u0447": "ch",
79
+ "\u0448": "sh",
80
+ "\u0449": "sch",
81
+ "\u044A": "",
82
+ "\u044B": "y",
83
+ "\u044C": "",
84
+ "\u044D": "e",
85
+ "\u044E": "yu",
86
+ "\u044F": "ya"
87
+ };
88
+ function cleanContent(text) {
89
+ return text.replace(/##\s*Attachments\n(?:- [^\n]+\n?)*/g, "").replace(/- media\/\d+\/[^\n]+/g, "").replace(/\*\*([^*]+)\n\*\*\n/g, "**$1**\n\n").replace(/\*\*([^*\n]+)\n([^*\n]+)\*\*/g, "**$1**\n\n**$2**").trim();
90
+ }
91
+ function generateSlug(text, fallbackIdx) {
92
+ const cleaned = cleanContent(text);
93
+ if (!cleaned || cleaned.length === 0) {
94
+ return `post-${fallbackIdx}`;
95
+ }
96
+ const slug = cleaned.toLowerCase().substring(0, 60).split("").map((char) => translitMap[char] || char).join("").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
97
+ return slug || `post-${fallbackIdx}`;
98
+ }
99
+ function extractTitle(content, fallback = "\u0411\u0435\u0437 \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0430") {
100
+ if (!content) return fallback;
101
+ const cleaned = cleanContent(content);
102
+ if (!cleaned) return fallback;
103
+ const lines = cleaned.split("\n").filter((l) => l.trim());
104
+ if (lines.length === 0) return fallback;
105
+ let title = lines[0].replace(/[#@\[\]*]/g, "").replace(/https?:\/\/[^\s]+/g, "").trim();
106
+ if (title.length > 80) {
107
+ title = title.substring(0, 77) + "...";
108
+ }
109
+ return title || fallback;
110
+ }
111
+ function extractExcerpt(content, maxLength = 200) {
112
+ if (!content) return "";
113
+ const lines = content.replace(/https?:\/\/[^\s]+/g, "").replace(/##\s*Attachments[\s\S]*/g, "").replace(/[#@\[\]*]/g, "").split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
114
+ const text = lines.slice(1).join(" ").trim();
115
+ if (!text) return "";
116
+ if (text.length <= maxLength) return text;
117
+ return text.substring(0, maxLength).replace(/\s+\S*$/, "") + "...";
118
+ }
119
+ function extractAttachments(content) {
120
+ const attachments = [];
121
+ const regex = /media\/\d+\/[^\s\n]+/g;
122
+ let match;
123
+ while ((match = regex.exec(content)) !== null) {
124
+ attachments.push(match[0]);
125
+ }
126
+ return attachments;
127
+ }
128
+ function categorizePost(content, defaultCategory = "\u0411\u043B\u043E\u0433") {
129
+ const text = content.toLowerCase();
130
+ const categories = {
131
+ "\u0420\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B": [
132
+ { keywords: ["\u0434\u043E/\u043F\u043E\u0441\u043B\u0435", "\u0434\u043E\u043F\u043E\u0441\u043B\u0435", "\u0434\u043E \u043A\u043E\u0440\u0440\u0435\u043A\u0446\u0438\u0438", "\u043F\u043E\u0441\u043B\u0435 \u043A\u043E\u0440\u0440\u0435\u043A\u0446\u0438\u0438", "\u043F\u043E\u0441\u043B\u0435 \u0441\u0435\u0430\u043D\u0441\u0430"], weight: 3 },
133
+ { keywords: ["\u043A\u0435\u0439\u0441 \u0438\u0437 \u043F\u0440\u0430\u043A\u0442\u0438\u043A\u0438", "\u043A\u0435\u0439\u0441:", "\u0438\u0441\u0442\u043E\u0440\u0438\u044F \u043A\u043B\u0438\u0435\u043D\u0442", "\u0441\u043B\u0443\u0447\u0430\u0439 \u0438\u0437 \u043F\u0440\u0430\u043A\u0442\u0438\u043A\u0438"], weight: 3 },
134
+ { keywords: ["\u043F\u0440\u0438\u0448\u0451\u043B \u0441 \u0436\u0430\u043B\u043E\u0431\u043E\u0439", "\u043F\u0440\u0438\u0448\u043B\u0430 \u0441 \u0436\u0430\u043B\u043E\u0431\u043E\u0439", "\u043E\u0431\u0440\u0430\u0442\u0438\u043B", "\u043E\u0431\u0440\u0430\u0442\u0438\u043B\u0430"], weight: 2 },
135
+ { keywords: ["\u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442", "\u0443\u0434\u0430\u043B\u043E\u0441\u044C", "\u043F\u043E\u043C\u043E\u0433", "\u0438\u0437\u0431\u0430\u0432\u0438\u043B", "\u0443\u0448\u043B\u0430 \u0431\u043E\u043B\u044C", "\u043F\u0440\u043E\u0448\u043B\u0430 \u0431\u043E\u043B\u044C"], weight: 2 }
136
+ ],
137
+ "\u0423\u043F\u0440\u0430\u0436\u043D\u0435\u043D\u0438\u044F": [
138
+ { keywords: ["\u0443\u043F\u0440\u0430\u0436\u043D\u0435\u043D\u0438", "\u043A\u043E\u043C\u043F\u043B\u0435\u043A\u0441 \u0443\u043F\u0440\u0430\u0436\u043D\u0435\u043D\u0438\u0439", "\u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A"], weight: 3 },
139
+ { keywords: ["\u0440\u0430\u0441\u0442\u044F\u0436\u043A", "\u0440\u0430\u0441\u0442\u044F\u0436\u0435\u043D\u0438", "\u0440\u0430\u0437\u043C\u0438\u043D\u043A"], weight: 2 },
140
+ { keywords: ["\u0441\u0430\u043C\u043E\u043C\u0430\u0441\u0441\u0430\u0436", "\u043C\u0430\u0441\u0441\u0430\u0436 \u0441\u0435\u0431\u0435", "\u0441\u0430\u043C\u043E\u0441\u0442\u043E\u044F\u0442\u0435\u043B\u044C\u043D"], weight: 2 },
141
+ { keywords: ["\u043F\u043E\u0432\u0442\u043E\u0440\u0438", "\u0432\u044B\u043F\u043E\u043B\u043D\u0438", "\u0441\u0434\u0435\u043B\u0430\u0439", "\u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439"], weight: 1 },
142
+ { keywords: ["\u043A\u0443\u0440\u0441 \u0443\u043F\u0440\u0430\u0436\u043D\u0435\u043D\u0438\u0439", "\u0432\u0438\u0434\u0435\u043E \u0443\u0440\u043E\u043A", "\u0443\u0440\u043E\u043A"], weight: 2 }
143
+ ],
144
+ "\u041E\u0431\u0440\u0430\u0437\u043E\u0432\u0430\u043D\u0438\u0435": [
145
+ { keywords: ["\u043C\u0438\u0444", "\u043C\u0438\u0444\u043E\u0432", "\u0437\u0430\u0431\u043B\u0443\u0436\u0434\u0435\u043D\u0438"], weight: 3 },
146
+ { keywords: ["\u043F\u043E\u0447\u0435\u043C\u0443", "\u043F\u0440\u0438\u0447\u0438\u043D", "\u0438\u0437-\u0437\u0430 \u0447\u0435\u0433\u043E", "\u043E\u0442\u043A\u0443\u0434\u0430"], weight: 1 },
147
+ { keywords: ["\u0430\u043D\u0430\u0442\u043E\u043C\u0438", "\u043C\u044B\u0448\u0446", "\u0441\u0443\u0441\u0442\u0430\u0432", "\u043F\u043E\u0437\u0432\u043E\u043D", "\u043D\u0435\u0440\u0432", "\u0441\u0432\u044F\u0437\u043A"], weight: 2 },
148
+ { keywords: ["\u043A\u0430\u043A \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442", "\u043C\u0435\u0445\u0430\u043D\u0438\u0437\u043C", "\u0444\u0438\u0437\u0438\u043E\u043B\u043E\u0433\u0438"], weight: 2 },
149
+ { keywords: ["\u0447\u0442\u043E \u0442\u0430\u043A\u043E\u0435", "\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u043C", "\u043E\u0431\u044A\u044F\u0441\u043D\u044F", "\u0440\u0430\u0441\u0441\u043A\u0430\u0436\u0443"], weight: 1 }
150
+ ],
151
+ "\u0417\u0434\u043E\u0440\u043E\u0432\u044C\u0435": [
152
+ { keywords: ["\u0431\u043E\u043B\u044C \u0432", "\u0431\u043E\u043B\u0438\u0442", "\u0431\u043E\u043B\u0435\u0432", "\u0431\u043E\u043B\u0435\u0437\u043D"], weight: 2 },
153
+ { keywords: ["\u0441\u043F\u0438\u043D\u0430", "\u0441\u043F\u0438\u043D", "\u043F\u043E\u044F\u0441\u043D\u0438\u0446", "\u0448\u0435\u044F", "\u0448\u0435\u0439\u043D", "\u043F\u043B\u0435\u0447", "\u043A\u043E\u043B\u0435\u043D", "\u0433\u043E\u043B\u043E\u0432"], weight: 2 },
154
+ { keywords: ["\u0437\u0430\u0449\u0435\u043C\u043B\u0435\u043D", "\u0433\u0440\u044B\u0436", "\u043F\u0440\u043E\u0442\u0440\u0443\u0437\u0438", "\u0441\u043A\u043E\u043B\u0438\u043E\u0437", "\u043E\u0441\u0442\u0435\u043E\u0445\u043E\u043D\u0434\u0440\u043E\u0437"], weight: 3 },
155
+ { keywords: ["\u0442\u0440\u0430\u0432\u043C", "\u043F\u0435\u0440\u0435\u043B\u043E\u043C", "\u0432\u044B\u0432\u0438\u0445", "\u0440\u0430\u0441\u0442\u044F\u0436\u0435\u043D"], weight: 2 },
156
+ { keywords: ["\u043B\u0435\u0447\u0435\u043D\u0438", "\u0442\u0435\u0440\u0430\u043F\u0438", "\u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", "\u0440\u0435\u0430\u0431\u0438\u043B\u0438\u0442\u0430\u0446\u0438"], weight: 1 }
157
+ ],
158
+ "\u041D\u043E\u0432\u043E\u0441\u0442\u0438": [
159
+ { keywords: ["\u043F\u0440\u0438\u0435\u0437\u0436\u0430\u044E", "\u043F\u0440\u0438\u0435\u0434\u0443", "\u0431\u0443\u0434\u0443 \u0432", "\u0437\u0430\u043F\u0438\u0441\u044C \u043E\u0442\u043A\u0440\u044B\u0442\u0430", "\u0437\u0430\u043F\u0438\u0441\u044C \u043D\u0430"], weight: 3 },
160
+ { keywords: ["\u043E\u0431\u044A\u044F\u0432\u043B\u0435\u043D\u0438", "\u0430\u043D\u043E\u043D\u0441", "\u043D\u043E\u0432\u043E\u0441\u0442\u044C", "\u0441\u043E\u043E\u0431\u0449\u0430\u044E"], weight: 2 },
161
+ { keywords: ["\u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B \u043E\u0431\u0443\u0447\u0435\u043D\u0438\u0435", "\u043F\u043E\u043B\u0443\u0447\u0438\u043B \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442", "\u043F\u0440\u043E\u0448\u0451\u043B \u043A\u0443\u0440\u0441"], weight: 2 },
162
+ { keywords: ["\u0434\u0440\u0443\u0437\u044C\u044F,", "\u0434\u043E\u0440\u043E\u0433\u0438\u0435", "\u0440\u0430\u0434 \u0441\u043E\u043E\u0431\u0449\u0438\u0442\u044C"], weight: 1 },
163
+ { keywords: ["\u0442\u0443\u043B\u0430", "\u0433\u0435\u043B\u0435\u043D\u0434\u0436\u0438\u043A", "\u043C\u043E\u0441\u043A\u0432\u0430", "\u0433\u043E\u0440\u043E\u0434"], weight: 1 }
164
+ ],
165
+ "\u041E\u0431\u0440\u0430\u0437 \u0436\u0438\u0437\u043D\u0438": [
166
+ { keywords: ["\u0441\u043E\u043D", "\u0441\u043F\u0430\u0442\u044C", "\u0431\u0435\u0441\u0441\u043E\u043D\u043D\u0438\u0446", "\u043D\u0435\u0434\u043E\u0441\u044B\u043F"], weight: 2 },
167
+ { keywords: ["\u0441\u0442\u0440\u0435\u0441\u0441", "\u0442\u0440\u0435\u0432\u043E\u0433", "\u043D\u0435\u0440\u0432\u043D", "\u043D\u0430\u043F\u0440\u044F\u0436\u0435\u043D"], weight: 2 },
168
+ { keywords: ["\u0443\u0441\u0442\u0430\u043B\u043E\u0441\u0442\u044C", "\u0443\u0441\u0442\u0430\u043B", "\u044D\u043D\u0435\u0440\u0433\u0438", "\u0431\u043E\u0434\u0440\u043E\u0441\u0442\u044C", "\u0441\u0438\u043B\u044B"], weight: 2 },
169
+ { keywords: ["\u043F\u0440\u0438\u0432\u044B\u0447\u043A", "\u0440\u0435\u0436\u0438\u043C", "\u043E\u0431\u0440\u0430\u0437 \u0436\u0438\u0437\u043D\u0438", "\u043F\u0438\u0442\u0430\u043D\u0438"], weight: 2 },
170
+ { keywords: ["\u0441\u043E\u0432\u0435\u0442", "\u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0430\u0446\u0438", "\u043B\u0430\u0439\u0444\u0445\u0430\u043A", "\u043F\u0440\u0430\u0432\u0438\u043B"], weight: 1 }
171
+ ]
172
+ };
173
+ const scores = {};
174
+ for (const [category, rules] of Object.entries(categories)) {
175
+ scores[category] = 0;
176
+ for (const rule of rules) {
177
+ for (const keyword of rule.keywords) {
178
+ if (text.includes(keyword)) {
179
+ scores[category] += rule.weight;
180
+ }
181
+ }
182
+ }
183
+ }
184
+ let maxScore = 0;
185
+ let bestCategory = defaultCategory;
186
+ for (const [category, score] of Object.entries(scores)) {
187
+ if (score > maxScore) {
188
+ maxScore = score;
189
+ bestCategory = category;
190
+ }
191
+ }
192
+ return maxScore >= 2 ? bestCategory : defaultCategory;
193
+ }
194
+ function parsePost(fileContent, options = {}) {
195
+ const { data, content } = (0, import_gray_matter.default)(fileContent);
196
+ const { generateSlug: shouldGenerateSlug = true, extractAttachments: shouldExtractAttachments = true } = options;
197
+ return {
198
+ idx: data.idx || 0,
199
+ msg_id: data.msg_id || 0,
200
+ date: data.date || "",
201
+ channel_id: data.channel_id || 0,
202
+ channel_title: data.channel_title || "",
203
+ channel_username: data.channel_username || "",
204
+ link: data.link || "",
205
+ views: data.views || 0,
206
+ forwards: data.forwards || 0,
207
+ has_media: data.has_media || false,
208
+ content: content.trim(),
209
+ slug: shouldGenerateSlug ? generateSlug(content, data.idx) : "",
210
+ attachments: shouldExtractAttachments ? extractAttachments(content) : []
211
+ };
212
+ }
213
+ function groupPosts(posts, timeWindowMs = 30 * 60 * 1e3) {
214
+ const sortedPosts = [...posts].sort(
215
+ (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
216
+ );
217
+ const groups = [];
218
+ let currentGroup = [];
219
+ let lastDate = null;
220
+ for (const post of sortedPosts) {
221
+ const postDate = new Date(post.date);
222
+ if (lastDate && Math.abs(postDate.getTime() - lastDate.getTime()) < timeWindowMs) {
223
+ currentGroup.push(post);
224
+ } else {
225
+ if (currentGroup.length > 0) {
226
+ groups.push(createGroupedPost(currentGroup));
227
+ }
228
+ currentGroup = [post];
229
+ }
230
+ lastDate = postDate;
231
+ }
232
+ if (currentGroup.length > 0) {
233
+ groups.push(createGroupedPost(currentGroup));
234
+ }
235
+ return groups;
236
+ }
237
+ function createGroupedPost(posts) {
238
+ const chronologicalGroup = [...posts].reverse();
239
+ const mainPost = chronologicalGroup[0];
240
+ const allContent = chronologicalGroup.map((p) => p.content).join("\n\n");
241
+ const allAttachments = chronologicalGroup.flatMap((p) => p.attachments);
242
+ const uniqueAttachments = [...new Set(allAttachments)];
243
+ const images = uniqueAttachments.filter((a) => a.match(/\.(jpg|jpeg|png|webp|gif)$/i));
244
+ const videos = uniqueAttachments.filter((a) => a.match(/\.(mp4|mov|webm|avi|m4v)$/i));
245
+ return {
246
+ id: `group-${mainPost.idx}`,
247
+ slug: generateSlug(allContent, mainPost.idx),
248
+ title: extractTitle(allContent),
249
+ excerpt: extractExcerpt(allContent),
250
+ date: mainPost.date,
251
+ category: categorizePost(allContent),
252
+ posts: chronologicalGroup,
253
+ hasMedia: chronologicalGroup.some((p) => p.has_media),
254
+ totalViews: chronologicalGroup.reduce((sum, p) => sum + p.views, 0),
255
+ coverImage: images[0],
256
+ images,
257
+ videos
258
+ };
259
+ }
260
+ function deduplicatePosts(posts) {
261
+ const seenMsgIds = /* @__PURE__ */ new Set();
262
+ return posts.filter((p) => {
263
+ if (seenMsgIds.has(p.msg_id)) return false;
264
+ seenMsgIds.add(p.msg_id);
265
+ return true;
266
+ });
267
+ }
268
+
269
+ // src/analytics.ts
270
+ var config = {};
271
+ function configureAnalytics(options) {
272
+ config = { ...config, ...options };
273
+ }
274
+ function trackGoal(goal, params) {
275
+ if (typeof window === "undefined") return;
276
+ if (config.yandexId && window.ym) {
277
+ window.ym(config.yandexId, "reachGoal", goal, params);
278
+ }
279
+ if (config.googleId && window.gtag) {
280
+ window.gtag("event", goal, {
281
+ event_category: "engagement",
282
+ ...params
283
+ });
284
+ }
285
+ if (config.customTracker) {
286
+ config.customTracker(goal, params);
287
+ }
288
+ }
289
+ function trackBookAppointment(source) {
290
+ trackGoal("click_book_appointment", { source });
291
+ }
292
+ function trackTelegramClick(source, page) {
293
+ const goalMap = {
294
+ blog: "click_telegram_blog",
295
+ header: "click_telegram_header",
296
+ footer: "click_telegram_footer",
297
+ contact: "click_telegram_contact"
298
+ };
299
+ trackGoal(goalMap[source] || `click_telegram_${source}`, { page });
300
+ }
301
+ function trackLearnMore(service) {
302
+ trackGoal("click_learn_more", { service });
303
+ }
304
+ function trackServiceClick(service) {
305
+ trackGoal("click_service", { service });
306
+ }
307
+ // Annotate the CommonJS export names for ESM import in node:
308
+ 0 && (module.exports = {
309
+ categorizePost,
310
+ cleanContent,
311
+ configureAnalytics,
312
+ deduplicatePosts,
313
+ extractAttachments,
314
+ extractExcerpt,
315
+ extractTitle,
316
+ generateSlug,
317
+ groupPosts,
318
+ parsePost,
319
+ trackBookAppointment,
320
+ trackGoal,
321
+ trackLearnMore,
322
+ trackServiceClick,
323
+ trackTelegramClick
324
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,273 @@
1
+ // src/posts.ts
2
+ import matter from "gray-matter";
3
+ var translitMap = {
4
+ "\u0430": "a",
5
+ "\u0431": "b",
6
+ "\u0432": "v",
7
+ "\u0433": "g",
8
+ "\u0434": "d",
9
+ "\u0435": "e",
10
+ "\u0451": "yo",
11
+ "\u0436": "zh",
12
+ "\u0437": "z",
13
+ "\u0438": "i",
14
+ "\u0439": "y",
15
+ "\u043A": "k",
16
+ "\u043B": "l",
17
+ "\u043C": "m",
18
+ "\u043D": "n",
19
+ "\u043E": "o",
20
+ "\u043F": "p",
21
+ "\u0440": "r",
22
+ "\u0441": "s",
23
+ "\u0442": "t",
24
+ "\u0443": "u",
25
+ "\u0444": "f",
26
+ "\u0445": "h",
27
+ "\u0446": "ts",
28
+ "\u0447": "ch",
29
+ "\u0448": "sh",
30
+ "\u0449": "sch",
31
+ "\u044A": "",
32
+ "\u044B": "y",
33
+ "\u044C": "",
34
+ "\u044D": "e",
35
+ "\u044E": "yu",
36
+ "\u044F": "ya"
37
+ };
38
+ function cleanContent(text) {
39
+ return text.replace(/##\s*Attachments\n(?:- [^\n]+\n?)*/g, "").replace(/- media\/\d+\/[^\n]+/g, "").replace(/\*\*([^*]+)\n\*\*\n/g, "**$1**\n\n").replace(/\*\*([^*\n]+)\n([^*\n]+)\*\*/g, "**$1**\n\n**$2**").trim();
40
+ }
41
+ function generateSlug(text, fallbackIdx) {
42
+ const cleaned = cleanContent(text);
43
+ if (!cleaned || cleaned.length === 0) {
44
+ return `post-${fallbackIdx}`;
45
+ }
46
+ const slug = cleaned.toLowerCase().substring(0, 60).split("").map((char) => translitMap[char] || char).join("").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
47
+ return slug || `post-${fallbackIdx}`;
48
+ }
49
+ function extractTitle(content, fallback = "\u0411\u0435\u0437 \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0430") {
50
+ if (!content) return fallback;
51
+ const cleaned = cleanContent(content);
52
+ if (!cleaned) return fallback;
53
+ const lines = cleaned.split("\n").filter((l) => l.trim());
54
+ if (lines.length === 0) return fallback;
55
+ let title = lines[0].replace(/[#@\[\]*]/g, "").replace(/https?:\/\/[^\s]+/g, "").trim();
56
+ if (title.length > 80) {
57
+ title = title.substring(0, 77) + "...";
58
+ }
59
+ return title || fallback;
60
+ }
61
+ function extractExcerpt(content, maxLength = 200) {
62
+ if (!content) return "";
63
+ const lines = content.replace(/https?:\/\/[^\s]+/g, "").replace(/##\s*Attachments[\s\S]*/g, "").replace(/[#@\[\]*]/g, "").split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
64
+ const text = lines.slice(1).join(" ").trim();
65
+ if (!text) return "";
66
+ if (text.length <= maxLength) return text;
67
+ return text.substring(0, maxLength).replace(/\s+\S*$/, "") + "...";
68
+ }
69
+ function extractAttachments(content) {
70
+ const attachments = [];
71
+ const regex = /media\/\d+\/[^\s\n]+/g;
72
+ let match;
73
+ while ((match = regex.exec(content)) !== null) {
74
+ attachments.push(match[0]);
75
+ }
76
+ return attachments;
77
+ }
78
+ function categorizePost(content, defaultCategory = "\u0411\u043B\u043E\u0433") {
79
+ const text = content.toLowerCase();
80
+ const categories = {
81
+ "\u0420\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B": [
82
+ { keywords: ["\u0434\u043E/\u043F\u043E\u0441\u043B\u0435", "\u0434\u043E\u043F\u043E\u0441\u043B\u0435", "\u0434\u043E \u043A\u043E\u0440\u0440\u0435\u043A\u0446\u0438\u0438", "\u043F\u043E\u0441\u043B\u0435 \u043A\u043E\u0440\u0440\u0435\u043A\u0446\u0438\u0438", "\u043F\u043E\u0441\u043B\u0435 \u0441\u0435\u0430\u043D\u0441\u0430"], weight: 3 },
83
+ { keywords: ["\u043A\u0435\u0439\u0441 \u0438\u0437 \u043F\u0440\u0430\u043A\u0442\u0438\u043A\u0438", "\u043A\u0435\u0439\u0441:", "\u0438\u0441\u0442\u043E\u0440\u0438\u044F \u043A\u043B\u0438\u0435\u043D\u0442", "\u0441\u043B\u0443\u0447\u0430\u0439 \u0438\u0437 \u043F\u0440\u0430\u043A\u0442\u0438\u043A\u0438"], weight: 3 },
84
+ { keywords: ["\u043F\u0440\u0438\u0448\u0451\u043B \u0441 \u0436\u0430\u043B\u043E\u0431\u043E\u0439", "\u043F\u0440\u0438\u0448\u043B\u0430 \u0441 \u0436\u0430\u043B\u043E\u0431\u043E\u0439", "\u043E\u0431\u0440\u0430\u0442\u0438\u043B", "\u043E\u0431\u0440\u0430\u0442\u0438\u043B\u0430"], weight: 2 },
85
+ { keywords: ["\u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442", "\u0443\u0434\u0430\u043B\u043E\u0441\u044C", "\u043F\u043E\u043C\u043E\u0433", "\u0438\u0437\u0431\u0430\u0432\u0438\u043B", "\u0443\u0448\u043B\u0430 \u0431\u043E\u043B\u044C", "\u043F\u0440\u043E\u0448\u043B\u0430 \u0431\u043E\u043B\u044C"], weight: 2 }
86
+ ],
87
+ "\u0423\u043F\u0440\u0430\u0436\u043D\u0435\u043D\u0438\u044F": [
88
+ { keywords: ["\u0443\u043F\u0440\u0430\u0436\u043D\u0435\u043D\u0438", "\u043A\u043E\u043C\u043F\u043B\u0435\u043A\u0441 \u0443\u043F\u0440\u0430\u0436\u043D\u0435\u043D\u0438\u0439", "\u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A"], weight: 3 },
89
+ { keywords: ["\u0440\u0430\u0441\u0442\u044F\u0436\u043A", "\u0440\u0430\u0441\u0442\u044F\u0436\u0435\u043D\u0438", "\u0440\u0430\u0437\u043C\u0438\u043D\u043A"], weight: 2 },
90
+ { keywords: ["\u0441\u0430\u043C\u043E\u043C\u0430\u0441\u0441\u0430\u0436", "\u043C\u0430\u0441\u0441\u0430\u0436 \u0441\u0435\u0431\u0435", "\u0441\u0430\u043C\u043E\u0441\u0442\u043E\u044F\u0442\u0435\u043B\u044C\u043D"], weight: 2 },
91
+ { keywords: ["\u043F\u043E\u0432\u0442\u043E\u0440\u0438", "\u0432\u044B\u043F\u043E\u043B\u043D\u0438", "\u0441\u0434\u0435\u043B\u0430\u0439", "\u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439"], weight: 1 },
92
+ { keywords: ["\u043A\u0443\u0440\u0441 \u0443\u043F\u0440\u0430\u0436\u043D\u0435\u043D\u0438\u0439", "\u0432\u0438\u0434\u0435\u043E \u0443\u0440\u043E\u043A", "\u0443\u0440\u043E\u043A"], weight: 2 }
93
+ ],
94
+ "\u041E\u0431\u0440\u0430\u0437\u043E\u0432\u0430\u043D\u0438\u0435": [
95
+ { keywords: ["\u043C\u0438\u0444", "\u043C\u0438\u0444\u043E\u0432", "\u0437\u0430\u0431\u043B\u0443\u0436\u0434\u0435\u043D\u0438"], weight: 3 },
96
+ { keywords: ["\u043F\u043E\u0447\u0435\u043C\u0443", "\u043F\u0440\u0438\u0447\u0438\u043D", "\u0438\u0437-\u0437\u0430 \u0447\u0435\u0433\u043E", "\u043E\u0442\u043A\u0443\u0434\u0430"], weight: 1 },
97
+ { keywords: ["\u0430\u043D\u0430\u0442\u043E\u043C\u0438", "\u043C\u044B\u0448\u0446", "\u0441\u0443\u0441\u0442\u0430\u0432", "\u043F\u043E\u0437\u0432\u043E\u043D", "\u043D\u0435\u0440\u0432", "\u0441\u0432\u044F\u0437\u043A"], weight: 2 },
98
+ { keywords: ["\u043A\u0430\u043A \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442", "\u043C\u0435\u0445\u0430\u043D\u0438\u0437\u043C", "\u0444\u0438\u0437\u0438\u043E\u043B\u043E\u0433\u0438"], weight: 2 },
99
+ { keywords: ["\u0447\u0442\u043E \u0442\u0430\u043A\u043E\u0435", "\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u043C", "\u043E\u0431\u044A\u044F\u0441\u043D\u044F", "\u0440\u0430\u0441\u0441\u043A\u0430\u0436\u0443"], weight: 1 }
100
+ ],
101
+ "\u0417\u0434\u043E\u0440\u043E\u0432\u044C\u0435": [
102
+ { keywords: ["\u0431\u043E\u043B\u044C \u0432", "\u0431\u043E\u043B\u0438\u0442", "\u0431\u043E\u043B\u0435\u0432", "\u0431\u043E\u043B\u0435\u0437\u043D"], weight: 2 },
103
+ { keywords: ["\u0441\u043F\u0438\u043D\u0430", "\u0441\u043F\u0438\u043D", "\u043F\u043E\u044F\u0441\u043D\u0438\u0446", "\u0448\u0435\u044F", "\u0448\u0435\u0439\u043D", "\u043F\u043B\u0435\u0447", "\u043A\u043E\u043B\u0435\u043D", "\u0433\u043E\u043B\u043E\u0432"], weight: 2 },
104
+ { keywords: ["\u0437\u0430\u0449\u0435\u043C\u043B\u0435\u043D", "\u0433\u0440\u044B\u0436", "\u043F\u0440\u043E\u0442\u0440\u0443\u0437\u0438", "\u0441\u043A\u043E\u043B\u0438\u043E\u0437", "\u043E\u0441\u0442\u0435\u043E\u0445\u043E\u043D\u0434\u0440\u043E\u0437"], weight: 3 },
105
+ { keywords: ["\u0442\u0440\u0430\u0432\u043C", "\u043F\u0435\u0440\u0435\u043B\u043E\u043C", "\u0432\u044B\u0432\u0438\u0445", "\u0440\u0430\u0441\u0442\u044F\u0436\u0435\u043D"], weight: 2 },
106
+ { keywords: ["\u043B\u0435\u0447\u0435\u043D\u0438", "\u0442\u0435\u0440\u0430\u043F\u0438", "\u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", "\u0440\u0435\u0430\u0431\u0438\u043B\u0438\u0442\u0430\u0446\u0438"], weight: 1 }
107
+ ],
108
+ "\u041D\u043E\u0432\u043E\u0441\u0442\u0438": [
109
+ { keywords: ["\u043F\u0440\u0438\u0435\u0437\u0436\u0430\u044E", "\u043F\u0440\u0438\u0435\u0434\u0443", "\u0431\u0443\u0434\u0443 \u0432", "\u0437\u0430\u043F\u0438\u0441\u044C \u043E\u0442\u043A\u0440\u044B\u0442\u0430", "\u0437\u0430\u043F\u0438\u0441\u044C \u043D\u0430"], weight: 3 },
110
+ { keywords: ["\u043E\u0431\u044A\u044F\u0432\u043B\u0435\u043D\u0438", "\u0430\u043D\u043E\u043D\u0441", "\u043D\u043E\u0432\u043E\u0441\u0442\u044C", "\u0441\u043E\u043E\u0431\u0449\u0430\u044E"], weight: 2 },
111
+ { keywords: ["\u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B \u043E\u0431\u0443\u0447\u0435\u043D\u0438\u0435", "\u043F\u043E\u043B\u0443\u0447\u0438\u043B \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442", "\u043F\u0440\u043E\u0448\u0451\u043B \u043A\u0443\u0440\u0441"], weight: 2 },
112
+ { keywords: ["\u0434\u0440\u0443\u0437\u044C\u044F,", "\u0434\u043E\u0440\u043E\u0433\u0438\u0435", "\u0440\u0430\u0434 \u0441\u043E\u043E\u0431\u0449\u0438\u0442\u044C"], weight: 1 },
113
+ { keywords: ["\u0442\u0443\u043B\u0430", "\u0433\u0435\u043B\u0435\u043D\u0434\u0436\u0438\u043A", "\u043C\u043E\u0441\u043A\u0432\u0430", "\u0433\u043E\u0440\u043E\u0434"], weight: 1 }
114
+ ],
115
+ "\u041E\u0431\u0440\u0430\u0437 \u0436\u0438\u0437\u043D\u0438": [
116
+ { keywords: ["\u0441\u043E\u043D", "\u0441\u043F\u0430\u0442\u044C", "\u0431\u0435\u0441\u0441\u043E\u043D\u043D\u0438\u0446", "\u043D\u0435\u0434\u043E\u0441\u044B\u043F"], weight: 2 },
117
+ { keywords: ["\u0441\u0442\u0440\u0435\u0441\u0441", "\u0442\u0440\u0435\u0432\u043E\u0433", "\u043D\u0435\u0440\u0432\u043D", "\u043D\u0430\u043F\u0440\u044F\u0436\u0435\u043D"], weight: 2 },
118
+ { keywords: ["\u0443\u0441\u0442\u0430\u043B\u043E\u0441\u0442\u044C", "\u0443\u0441\u0442\u0430\u043B", "\u044D\u043D\u0435\u0440\u0433\u0438", "\u0431\u043E\u0434\u0440\u043E\u0441\u0442\u044C", "\u0441\u0438\u043B\u044B"], weight: 2 },
119
+ { keywords: ["\u043F\u0440\u0438\u0432\u044B\u0447\u043A", "\u0440\u0435\u0436\u0438\u043C", "\u043E\u0431\u0440\u0430\u0437 \u0436\u0438\u0437\u043D\u0438", "\u043F\u0438\u0442\u0430\u043D\u0438"], weight: 2 },
120
+ { keywords: ["\u0441\u043E\u0432\u0435\u0442", "\u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0430\u0446\u0438", "\u043B\u0430\u0439\u0444\u0445\u0430\u043A", "\u043F\u0440\u0430\u0432\u0438\u043B"], weight: 1 }
121
+ ]
122
+ };
123
+ const scores = {};
124
+ for (const [category, rules] of Object.entries(categories)) {
125
+ scores[category] = 0;
126
+ for (const rule of rules) {
127
+ for (const keyword of rule.keywords) {
128
+ if (text.includes(keyword)) {
129
+ scores[category] += rule.weight;
130
+ }
131
+ }
132
+ }
133
+ }
134
+ let maxScore = 0;
135
+ let bestCategory = defaultCategory;
136
+ for (const [category, score] of Object.entries(scores)) {
137
+ if (score > maxScore) {
138
+ maxScore = score;
139
+ bestCategory = category;
140
+ }
141
+ }
142
+ return maxScore >= 2 ? bestCategory : defaultCategory;
143
+ }
144
+ function parsePost(fileContent, options = {}) {
145
+ const { data, content } = matter(fileContent);
146
+ const { generateSlug: shouldGenerateSlug = true, extractAttachments: shouldExtractAttachments = true } = options;
147
+ return {
148
+ idx: data.idx || 0,
149
+ msg_id: data.msg_id || 0,
150
+ date: data.date || "",
151
+ channel_id: data.channel_id || 0,
152
+ channel_title: data.channel_title || "",
153
+ channel_username: data.channel_username || "",
154
+ link: data.link || "",
155
+ views: data.views || 0,
156
+ forwards: data.forwards || 0,
157
+ has_media: data.has_media || false,
158
+ content: content.trim(),
159
+ slug: shouldGenerateSlug ? generateSlug(content, data.idx) : "",
160
+ attachments: shouldExtractAttachments ? extractAttachments(content) : []
161
+ };
162
+ }
163
+ function groupPosts(posts, timeWindowMs = 30 * 60 * 1e3) {
164
+ const sortedPosts = [...posts].sort(
165
+ (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
166
+ );
167
+ const groups = [];
168
+ let currentGroup = [];
169
+ let lastDate = null;
170
+ for (const post of sortedPosts) {
171
+ const postDate = new Date(post.date);
172
+ if (lastDate && Math.abs(postDate.getTime() - lastDate.getTime()) < timeWindowMs) {
173
+ currentGroup.push(post);
174
+ } else {
175
+ if (currentGroup.length > 0) {
176
+ groups.push(createGroupedPost(currentGroup));
177
+ }
178
+ currentGroup = [post];
179
+ }
180
+ lastDate = postDate;
181
+ }
182
+ if (currentGroup.length > 0) {
183
+ groups.push(createGroupedPost(currentGroup));
184
+ }
185
+ return groups;
186
+ }
187
+ function createGroupedPost(posts) {
188
+ const chronologicalGroup = [...posts].reverse();
189
+ const mainPost = chronologicalGroup[0];
190
+ const allContent = chronologicalGroup.map((p) => p.content).join("\n\n");
191
+ const allAttachments = chronologicalGroup.flatMap((p) => p.attachments);
192
+ const uniqueAttachments = [...new Set(allAttachments)];
193
+ const images = uniqueAttachments.filter((a) => a.match(/\.(jpg|jpeg|png|webp|gif)$/i));
194
+ const videos = uniqueAttachments.filter((a) => a.match(/\.(mp4|mov|webm|avi|m4v)$/i));
195
+ return {
196
+ id: `group-${mainPost.idx}`,
197
+ slug: generateSlug(allContent, mainPost.idx),
198
+ title: extractTitle(allContent),
199
+ excerpt: extractExcerpt(allContent),
200
+ date: mainPost.date,
201
+ category: categorizePost(allContent),
202
+ posts: chronologicalGroup,
203
+ hasMedia: chronologicalGroup.some((p) => p.has_media),
204
+ totalViews: chronologicalGroup.reduce((sum, p) => sum + p.views, 0),
205
+ coverImage: images[0],
206
+ images,
207
+ videos
208
+ };
209
+ }
210
+ function deduplicatePosts(posts) {
211
+ const seenMsgIds = /* @__PURE__ */ new Set();
212
+ return posts.filter((p) => {
213
+ if (seenMsgIds.has(p.msg_id)) return false;
214
+ seenMsgIds.add(p.msg_id);
215
+ return true;
216
+ });
217
+ }
218
+
219
+ // src/analytics.ts
220
+ var config = {};
221
+ function configureAnalytics(options) {
222
+ config = { ...config, ...options };
223
+ }
224
+ function trackGoal(goal, params) {
225
+ if (typeof window === "undefined") return;
226
+ if (config.yandexId && window.ym) {
227
+ window.ym(config.yandexId, "reachGoal", goal, params);
228
+ }
229
+ if (config.googleId && window.gtag) {
230
+ window.gtag("event", goal, {
231
+ event_category: "engagement",
232
+ ...params
233
+ });
234
+ }
235
+ if (config.customTracker) {
236
+ config.customTracker(goal, params);
237
+ }
238
+ }
239
+ function trackBookAppointment(source) {
240
+ trackGoal("click_book_appointment", { source });
241
+ }
242
+ function trackTelegramClick(source, page) {
243
+ const goalMap = {
244
+ blog: "click_telegram_blog",
245
+ header: "click_telegram_header",
246
+ footer: "click_telegram_footer",
247
+ contact: "click_telegram_contact"
248
+ };
249
+ trackGoal(goalMap[source] || `click_telegram_${source}`, { page });
250
+ }
251
+ function trackLearnMore(service) {
252
+ trackGoal("click_learn_more", { service });
253
+ }
254
+ function trackServiceClick(service) {
255
+ trackGoal("click_service", { service });
256
+ }
257
+ export {
258
+ categorizePost,
259
+ cleanContent,
260
+ configureAnalytics,
261
+ deduplicatePosts,
262
+ extractAttachments,
263
+ extractExcerpt,
264
+ extractTitle,
265
+ generateSlug,
266
+ groupPosts,
267
+ parsePost,
268
+ trackBookAppointment,
269
+ trackGoal,
270
+ trackLearnMore,
271
+ trackServiceClick,
272
+ trackTelegramClick
273
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "koztv-blog-tools",
3
+ "version": "1.0.0",
4
+ "description": "Shared utilities for Telegram-based blog sites",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "keywords": [
23
+ "blog",
24
+ "telegram",
25
+ "markdown",
26
+ "static-site"
27
+ ],
28
+ "author": "Koz TV",
29
+ "license": "MIT",
30
+ "devDependencies": {
31
+ "tsup": "^8.0.0",
32
+ "typescript": "^5.0.0"
33
+ },
34
+ "dependencies": {
35
+ "gray-matter": "^4.0.3"
36
+ },
37
+ "peerDependencies": {
38
+ "gray-matter": "^4.0.0"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "gray-matter": {
42
+ "optional": true
43
+ }
44
+ }
45
+ }