Skip to content

Commit 155c6a5

Browse files
committed
прогресс-бар чтения + оглавление статьи (TOC sidebar)
1 parent da9733a commit 155c6a5

2 files changed

Lines changed: 314 additions & 139 deletions

File tree

_layouts/post.html

Lines changed: 212 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -3,184 +3,257 @@
33
---
44
{% assign lang = page.lang | default: site.default_lang | default: "en" %}
55
{% assign t = site.data.i18n[lang] %}
6-
<article class="post" itemscope itemtype="http://schema.org/BlogPosting">
7-
<header class="post-header">
8-
<h1 itemprop="name headline">{{ page.title }}</h1>
9-
<div class="post-meta">
10-
<time datetime="{{ page.date | date_to_xmlschema }}" itemprop="datePublished">
11-
{{ page.date | date: "%d %B %Y" }}
12-
</time>
13-
{% if page.author %}
14-
<span class="post-author">
15-
<span itemprop="author" itemscope itemtype="http://schema.org/Person">
16-
<span itemprop="name">{{ page.author }}</span>
6+
7+
<!-- Reading progress bar -->
8+
<div class="reading-progress" id="reading-progress"></div>
9+
10+
<div class="post-layout">
11+
<!-- Table of contents sidebar (desktop only) -->
12+
<aside class="toc-sidebar" id="toc-sidebar">
13+
<nav class="toc" id="toc"></nav>
14+
</aside>
15+
16+
<article class="post" itemscope itemtype="http://schema.org/BlogPosting">
17+
<header class="post-header">
18+
<h1 itemprop="name headline">{{ page.title }}</h1>
19+
<div class="post-meta">
20+
<time datetime="{{ page.date | date_to_xmlschema }}" itemprop="datePublished">
21+
{{ page.date | date: "%d %B %Y" }}
22+
</time>
23+
{% if page.author %}
24+
<span class="post-author">
25+
<span itemprop="author" itemscope itemtype="http://schema.org/Person">
26+
<span itemprop="name">{{ page.author }}</span>
27+
</span>
1728
</span>
29+
{% endif %}
30+
{% assign words = content | number_of_words %}
31+
<span class="reading-time">
32+
• {% if words < 360 %}1{% else %}{{ words | divided_by: 180 }}{% endif %} {{ t.reading_time_one }}
1833
</span>
19-
{% endif %}
20-
{% assign words = content | number_of_words %}
21-
<span class="reading-time">
22-
• {% if words < 360 %}1{% else %}{{ words | divided_by: 180 }}{% endif %} {{ t.reading_time_one }}
23-
</span>
24-
</div>
25-
</header>
34+
</div>
35+
</header>
2636

27-
{% if page.image %}
28-
<div class="post-featured-image">
29-
<img src="{{ page.image }}" alt="{{ page.title }}" class="post-image-post" itemprop="image">
30-
</div>
31-
{% endif %}
32-
33-
<div class="post-content" itemprop="articleBody">
34-
{{ content }}
35-
</div>
36-
37-
{% if page.tags.size > 0 %}
38-
<div class="post-tags">
39-
<span class="tags-label">{{ t.post_tags }}</span>
40-
{% for tag in page.tags %}
41-
<a href="/{{ lang }}/tags#{{ tag | slugify }}" class="tag-link" rel="tag">{{ tag }}</a>
42-
{% endfor %}
37+
{% if page.image %}
38+
<div class="post-featured-image">
39+
<img src="{{ page.image }}" alt="{{ page.title }}" class="post-image-post" itemprop="image">
40+
</div>
41+
{% endif %}
42+
43+
<div class="post-content" itemprop="articleBody">
44+
{{ content }}
4345
</div>
44-
{% endif %}
45-
46-
<div class="post-sharing">
47-
<h3>{{ t.share_article }}</h3>
48-
<div class="sharing-buttons">
49-
<a href="https://t.me/share/url?url={{ site.url }}{{ page.url | uri_escape }}&text={{ page.title | uri_escape }}"
50-
class="share-btn telegram" target="_blank" rel="noopener noreferrer" aria-label="{{ t.share_telegram }}">
51-
<span>📱 {{ t.share_telegram }}</span>
52-
</a>
53-
54-
<a href="https://twitter.com/intent/tweet?url={{ site.url }}{{ page.url | uri_escape }}&text={{ page.title | uri_escape }}"
55-
class="share-btn twitter" target="_blank" rel="noopener noreferrer" aria-label="{{ t.share_twitter }}">
56-
<span>🐦 {{ t.share_twitter }}</span>
57-
</a>
58-
59-
<a href="https://www.facebook.com/sharer/sharer.php?u={{ site.url }}{{ page.url | uri_escape }}"
60-
class="share-btn facebook" target="_blank" rel="noopener noreferrer" aria-label="{{ t.share_facebook }}">
61-
<span>📘 {{ t.share_facebook }}</span>
62-
</a>
63-
64-
{% if lang == "ru" %}
65-
<a href="https://vk.com/share.php?url={{ site.url }}{{ page.url | uri_escape }}&title={{ page.title | uri_escape }}"
66-
class="share-btn vkontakte" target="_blank" rel="noopener noreferrer" aria-label="{{ t.share_vkontakte }}">
67-
<span>🔵 {{ t.share_vkontakte }}</span>
68-
</a>
69-
{% endif %}
7046

71-
<a href="https://www.linkedin.com/sharing/share-offsite/?url={{ site.url }}{{ page.url | uri_escape }}"
72-
class="share-btn linkedin" target="_blank" rel="noopener noreferrer" aria-label="{{ t.share_linkedin }}">
73-
<span>💼 {{ t.share_linkedin }}</span>
74-
</a>
47+
{% if page.tags.size > 0 %}
48+
<div class="post-tags">
49+
<span class="tags-label">{{ t.post_tags }}</span>
50+
{% for tag in page.tags %}
51+
<a href="/{{ lang }}/tags#{{ tag | slugify }}" class="tag-link" rel="tag">{{ tag }}</a>
52+
{% endfor %}
53+
</div>
54+
{% endif %}
7555

76-
<button class="share-btn copy-link" onclick="copyToClipboard('{{ site.url }}{{ page.url }}')" aria-label="{{ t.share_copy_link }}">
77-
<span>🔗 {{ t.share_copy_link }}</span>
78-
</button>
79-
</div>
80-
</div>
81-
82-
<!-- Related Posts (filtered by language) -->
83-
{% assign lang_posts = site.posts | where: "lang", lang %}
84-
{% assign related_posts = lang_posts | limit: 3 %}
85-
{% if related_posts.size > 0 %}
86-
<div class="related-posts">
87-
<h3>{{ t.related_posts }}</h3>
88-
<div class="related-posts-grid">
89-
{% for post in related_posts %}
90-
{% if post.url != page.url %}
91-
<article class="related-post-card">
92-
{% if post.image %}
93-
<img src="{{ post.image }}" alt="{{ post.title }}" class="related-post-image">
94-
{% else %}
95-
<div class="related-post-placeholder">📰</div>
96-
{% endif %}
97-
<div class="related-post-content">
98-
<h4><a href="{{ post.url }}">{{ post.title }}</a></h4>
99-
<time datetime="{{ post.date | date_to_xmlschema }}">{{ post.date | date: "%d %B %Y" }}</time>
100-
</div>
101-
</article>
56+
<div class="post-sharing">
57+
<h3>{{ t.share_article }}</h3>
58+
<div class="sharing-buttons">
59+
<a href="https://t.me/share/url?url={{ site.url }}{{ page.url | uri_escape }}&text={{ page.title | uri_escape }}"
60+
class="share-btn telegram" target="_blank" rel="noopener noreferrer" aria-label="{{ t.share_telegram }}">
61+
<span>📱 {{ t.share_telegram }}</span>
62+
</a>
63+
64+
<a href="https://twitter.com/intent/tweet?url={{ site.url }}{{ page.url | uri_escape }}&text={{ page.title | uri_escape }}"
65+
class="share-btn twitter" target="_blank" rel="noopener noreferrer" aria-label="{{ t.share_twitter }}">
66+
<span>🐦 {{ t.share_twitter }}</span>
67+
</a>
68+
69+
<a href="https://www.facebook.com/sharer/sharer.php?u={{ site.url }}{{ page.url | uri_escape }}"
70+
class="share-btn facebook" target="_blank" rel="noopener noreferrer" aria-label="{{ t.share_facebook }}">
71+
<span>📘 {{ t.share_facebook }}</span>
72+
</a>
73+
74+
{% if lang == "ru" %}
75+
<a href="https://vk.com/share.php?url={{ site.url }}{{ page.url | uri_escape }}&title={{ page.title | uri_escape }}"
76+
class="share-btn vkontakte" target="_blank" rel="noopener noreferrer" aria-label="{{ t.share_vkontakte }}">
77+
<span>🔵 {{ t.share_vkontakte }}</span>
78+
</a>
10279
{% endif %}
103-
{% endfor %}
80+
81+
<a href="https://www.linkedin.com/sharing/share-offsite/?url={{ site.url }}{{ page.url | uri_escape }}"
82+
class="share-btn linkedin" target="_blank" rel="noopener noreferrer" aria-label="{{ t.share_linkedin }}">
83+
<span>💼 {{ t.share_linkedin }}</span>
84+
</a>
85+
86+
<button class="share-btn copy-link" onclick="copyToClipboard('{{ site.url }}{{ page.url }}')" aria-label="{{ t.share_copy_link }}">
87+
<span>🔗 {{ t.share_copy_link }}</span>
88+
</button>
89+
</div>
90+
</div>
91+
92+
<!-- Related Posts (filtered by language) -->
93+
{% assign lang_posts = site.posts | where: "lang", lang %}
94+
{% assign related_posts = lang_posts | limit: 3 %}
95+
{% if related_posts.size > 0 %}
96+
<div class="related-posts">
97+
<h3>{{ t.related_posts }}</h3>
98+
<div class="related-posts-grid">
99+
{% for post in related_posts %}
100+
{% if post.url != page.url %}
101+
<article class="related-post-card">
102+
{% if post.image %}
103+
<img src="{{ post.image }}" alt="{{ post.title }}" class="related-post-image">
104+
{% else %}
105+
<div class="related-post-placeholder">📰</div>
106+
{% endif %}
107+
<div class="related-post-content">
108+
<h4><a href="{{ post.url }}">{{ post.title }}</a></h4>
109+
<time datetime="{{ post.date | date_to_xmlschema }}">{{ post.date | date: "%d %B %Y" }}</time>
110+
</div>
111+
</article>
112+
{% endif %}
113+
{% endfor %}
114+
</div>
104115
</div>
105-
</div>
106-
{% endif %}
107-
108-
<!-- Navigation to Previous/Next Posts -->
109-
<nav class="post-navigation">
110-
{% if page.previous %}
111-
<a href="{{ page.previous.url }}" class="nav-previous" rel="prev">
112-
← {{ page.previous.title | truncate: 30 }}
113-
</a>
114-
{% endif %}
115-
{% if page.next %}
116-
<a href="{{ page.next.url }}" class="nav-next" rel="next">
117-
{{ page.next.title | truncate: 30 }} →
118-
</a>
119116
{% endif %}
120-
</nav>
121117

122-
<!-- Comments Section -->
123-
<div class="post-comments">
124-
<h3>{{ t.post_discussion }}</h3>
125-
<p>{{ t.post_discussion_text }} <a href="https://t.me/osengine" target="_blank">{{ t.post_discussion_link }}</a>!</p>
126-
<div id="comments">
118+
<!-- Navigation to Previous/Next Posts -->
119+
<nav class="post-navigation">
120+
{% if page.previous %}
121+
<a href="{{ page.previous.url }}" class="nav-previous" rel="prev">
122+
← {{ page.previous.title | truncate: 30 }}
123+
</a>
124+
{% endif %}
125+
{% if page.next %}
126+
<a href="{{ page.next.url }}" class="nav-next" rel="next">
127+
{{ page.next.title | truncate: 30 }} →
128+
</a>
129+
{% endif %}
130+
</nav>
131+
132+
<!-- Comments Section -->
133+
<div class="post-comments">
134+
<h3>{{ t.post_discussion }}</h3>
135+
<p>{{ t.post_discussion_text }} <a href="https://t.me/osengine" target="_blank">{{ t.post_discussion_link }}</a>!</p>
136+
<div id="comments">
137+
</div>
127138
</div>
128-
</div>
129-
</article>
139+
</article>
140+
</div>
130141

131142
<script>
132-
// Copy to clipboard function
143+
// Reading progress bar
144+
(function() {
145+
var progress = document.getElementById('reading-progress');
146+
var article = document.querySelector('.post-content');
147+
if (!progress || !article) return;
148+
149+
function updateProgress() {
150+
var articleTop = article.offsetTop;
151+
var articleHeight = article.offsetHeight;
152+
var windowHeight = window.innerHeight;
153+
var scrollY = window.scrollY || window.pageYOffset;
154+
155+
var start = articleTop;
156+
var end = articleTop + articleHeight - windowHeight;
157+
var pct = 0;
158+
159+
if (scrollY >= end) pct = 100;
160+
else if (scrollY > start) pct = ((scrollY - start) / (end - start)) * 100;
161+
162+
progress.style.width = pct + '%';
163+
}
164+
165+
window.addEventListener('scroll', updateProgress, {passive: true});
166+
updateProgress();
167+
})();
168+
169+
// Table of contents
170+
(function() {
171+
var content = document.querySelector('.post-content');
172+
var toc = document.getElementById('toc');
173+
if (!content || !toc) return;
174+
175+
var headings = content.querySelectorAll('h2, h3');
176+
if (headings.length < 2) {
177+
document.getElementById('toc-sidebar').style.display = 'none';
178+
return;
179+
}
180+
181+
var html = '';
182+
headings.forEach(function(h, i) {
183+
if (!h.id) h.id = 'heading-' + i;
184+
var cls = h.tagName === 'H3' ? ' class="toc-sub"' : '';
185+
html += '<a href="#' + h.id + '"' + cls + ' data-target="' + h.id + '">' + h.textContent + '</a>';
186+
});
187+
toc.innerHTML = html;
188+
189+
// Highlight current section on scroll
190+
var tocLinks = toc.querySelectorAll('a');
191+
192+
function updateToc() {
193+
var scrollY = window.scrollY || window.pageYOffset;
194+
var current = null;
195+
196+
headings.forEach(function(h) {
197+
if (h.offsetTop - 100 <= scrollY) current = h.id;
198+
});
199+
200+
tocLinks.forEach(function(link) {
201+
if (link.getAttribute('data-target') === current) {
202+
link.classList.add('active');
203+
} else {
204+
link.classList.remove('active');
205+
}
206+
});
207+
}
208+
209+
window.addEventListener('scroll', updateToc, {passive: true});
210+
updateToc();
211+
212+
// Smooth scroll on click
213+
tocLinks.forEach(function(link) {
214+
link.addEventListener('click', function(e) {
215+
e.preventDefault();
216+
var target = document.getElementById(this.getAttribute('data-target'));
217+
if (target) {
218+
window.scrollTo({top: target.offsetTop - 80, behavior: 'smooth'});
219+
}
220+
});
221+
});
222+
})();
223+
224+
// Copy to clipboard
133225
function copyToClipboard(text) {
134226
if (navigator.clipboard && window.isSecureContext) {
135-
navigator.clipboard.writeText(text).then(() => {
227+
navigator.clipboard.writeText(text).then(function() {
136228
showCopyMessage('{{ t.link_copied }}');
137229
});
138230
} else {
139-
// Fallback for older browsers
140-
const textArea = document.createElement('textarea');
231+
var textArea = document.createElement('textarea');
141232
textArea.value = text;
142233
textArea.style.position = 'fixed';
143234
textArea.style.left = '-999999px';
144-
textArea.style.top = '-999999px';
145235
document.body.appendChild(textArea);
146236
textArea.focus();
147237
textArea.select();
148-
149238
try {
150239
document.execCommand('copy');
151240
showCopyMessage('{{ t.link_copied }}');
152241
} catch (err) {
153242
showCopyMessage('{{ t.copy_error }}');
154243
}
155-
156244
textArea.remove();
157245
}
158246
}
159247

160248
function showCopyMessage(message) {
161-
// Create and show a temporary message
162-
const msgEl = document.createElement('div');
249+
var msgEl = document.createElement('div');
163250
msgEl.textContent = message;
164-
msgEl.style.cssText = `
165-
position: fixed;
166-
top: 20px;
167-
right: 20px;
168-
background: var(--primary-color);
169-
color: white;
170-
padding: 12px 20px;
171-
border-radius: 8px;
172-
box-shadow: var(--shadow-lg);
173-
z-index: 10000;
174-
font-weight: 500;
175-
transition: all 0.3s ease;
176-
`;
177-
251+
msgEl.style.cssText = 'position:fixed;top:20px;right:20px;background:var(--primary-color);color:white;padding:12px 20px;border-radius:8px;box-shadow:var(--shadow-lg);z-index:10000;font-weight:500;transition:all .3s ease';
178252
document.body.appendChild(msgEl);
179-
180-
setTimeout(() => {
253+
setTimeout(function() {
181254
msgEl.style.opacity = '0';
182255
msgEl.style.transform = 'translateY(-20px)';
183-
setTimeout(() => msgEl.remove(), 300);
256+
setTimeout(function() { msgEl.remove(); }, 300);
184257
}, 2000);
185258
}
186259
</script>

0 commit comments

Comments
 (0)