hexo-theme-gnix 13.0.0 → 14.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.
@@ -1,5 +1,4 @@
1
1
  const { Component, Fragment, isValidDate, parseISO, dateFormatters } = require("../include/util/common");
2
- const { filterByLanguage } = require("../include/util/i18n");
3
2
  const ArticleMedia = require("./common/article_media");
4
3
 
5
4
  function collectPosts(collection) {
@@ -12,6 +11,14 @@ function collectPosts(collection) {
12
11
  return posts;
13
12
  }
14
13
 
14
+ function estimateReadMinutes(content) {
15
+ if (typeof content !== "string" || !content) return 0;
16
+ const stripped = content.replace(/<\/?[a-z][^>]*>/gi, "").trim();
17
+ if (!stripped) return 0;
18
+ const tokens = stripped.match(/[ÿ-￿]|[a-zA-Z]+/g);
19
+ return tokens ? Math.max(1, Math.ceil(tokens.length / 200)) : 0;
20
+ }
21
+
15
22
  function getSeason(month) {
16
23
  if (month >= 2 && month <= 4) return "Spring";
17
24
  if (month >= 5 && month <= 7) return "Summer";
@@ -44,6 +51,47 @@ function groupPostsBySeason(posts) {
44
51
  }, []);
45
52
  }
46
53
 
54
+ function groupSeasonGroupsByYear(seasonGroups) {
55
+ const years = [];
56
+ for (const group of seasonGroups) {
57
+ const last = years[years.length - 1];
58
+ if (last && last.year === group.year) {
59
+ last.groups.push(group);
60
+ last.total += group.posts.length;
61
+ } else {
62
+ years.push({ year: group.year, total: group.posts.length, groups: [group] });
63
+ }
64
+ }
65
+ return years;
66
+ }
67
+
68
+ function toRoman(num) {
69
+ const map = [
70
+ [1000, "M"],
71
+ [900, "CM"],
72
+ [500, "D"],
73
+ [400, "CD"],
74
+ [100, "C"],
75
+ [90, "XC"],
76
+ [50, "L"],
77
+ [40, "XL"],
78
+ [10, "X"],
79
+ [9, "IX"],
80
+ [5, "V"],
81
+ [4, "IV"],
82
+ [1, "I"],
83
+ ];
84
+ let n = num;
85
+ let out = "";
86
+ for (const [value, sym] of map) {
87
+ while (n >= value) {
88
+ out += sym;
89
+ n -= value;
90
+ }
91
+ }
92
+ return out;
93
+ }
94
+
47
95
  function collectArchiveYears(posts) {
48
96
  return Array.from(new Set(posts.map((post) => post.date.year()))).sort((a, b) => b - a);
49
97
  }
@@ -58,104 +106,133 @@ function getPostDateParts(postDate, dateXml, date) {
58
106
  };
59
107
  }
60
108
 
109
+ function renderSeasonGroup({ posts, year, season, month, sectionTitle, url_for, date_xml, date }) {
110
+ const title = sectionTitle || getArchiveRangeLabel(year, month, season);
111
+ const kicker = year ? String(year) : "Archive";
112
+ const marker = season ? season.toLowerCase() : "all";
113
+ const sectionId = `archive-${kicker}-${marker}-${month || "all"}`;
114
+
115
+ return (
116
+ <section class={["archive-group", marker].filter(Boolean).join(" ")} aria-labelledby={sectionId}>
117
+ <header class="archive-group__header">
118
+ <h2 id={sectionId} class="archive-group__title">
119
+ {title}
120
+ </h2>
121
+ <span class="archive-group__count">{String(posts.length).padStart(2, "0")}</span>
122
+ </header>
123
+ <div class="timeline">
124
+ {posts.map((post) => {
125
+ const postDate = getPostDateParts(post.date, date_xml, date);
126
+ const readMinutes = estimateReadMinutes(post._content);
127
+ return (
128
+ <ArticleMedia
129
+ key={post.path}
130
+ url={url_for(post.link || post.path)}
131
+ title={post.title}
132
+ date={postDate.label}
133
+ dateXml={postDate.xml}
134
+ excerpt={post.excerpt || null}
135
+ readTime={readMinutes ? `${readMinutes} min` : null}
136
+ />
137
+ );
138
+ })}
139
+ </div>
140
+ </section>
141
+ );
142
+ }
143
+
61
144
  module.exports = class extends Component {
62
145
  render() {
63
- const { page, helper, site, config } = this.props;
146
+ const { page, helper } = this.props;
64
147
  const { url_for, date_xml, date } = helper;
65
- const langKey = helper.language_key(page);
66
-
67
- function renderArticleList(posts, year, month = null, sectionTitle = null, season = null) {
68
- const title = sectionTitle || getArchiveRangeLabel(year, month, season);
69
- const kicker = year ? String(year) : "Archive";
70
- const marker = season ? season.toLowerCase() : "all";
71
- const countLabel = posts.length === 1 ? "1 entry" : `${posts.length} entries`;
72
- const sectionId = `archive-${kicker}-${marker}-${month || "all"}`;
73
-
74
- return (
75
- <section class={["archive-group", marker].filter(Boolean).join(" ")} aria-labelledby={sectionId}>
76
- <header class="archive-group__header">
77
- <div>
78
- <h2 id={sectionId} class="archive-group__title">
79
- {title}
80
- </h2>
81
- </div>
82
- <span class="archive-group__count">{countLabel}</span>
83
- </header>
84
- <div class="timeline">
85
- {posts.map((post) => {
86
- const postDate = getPostDateParts(post.date, date_xml, date);
87
- return <ArticleMedia key={post.path} url={url_for(post.link || post.path)} title={post.title} date={postDate.label} dateXml={postDate.xml} />;
88
- })}
89
- </div>
90
- </section>
91
- );
92
- }
93
148
 
94
149
  const visiblePosts = collectPosts(page.posts);
95
150
  const totalVisiblePosts = visiblePosts.length;
96
- const latestPost = visiblePosts[0];
97
151
 
98
- const allPostsSource = site?.posts ? filterByLanguage(site.posts.sort("date", -1), langKey, config) : page.posts;
99
- const allPosts = collectPosts(allPostsSource);
100
- const years = collectArchiveYears(allPosts);
152
+ const years = collectArchiveYears(visiblePosts);
153
+
154
+ const currentYear = page.year ? Number(page.year) : null;
155
+ const currentMonth = page.month ? Number(page.month) : null;
156
+ const isTagPage = Boolean(page.tag);
157
+ const entriesLabel = `${totalVisiblePosts} ${totalVisiblePosts === 1 ? "entry" : "entries"}`;
158
+
101
159
  let articleList;
102
160
  if (!page.year) {
103
- articleList = groupPostsBySeason(visiblePosts).map((group) => {
104
- const title = getArchiveRangeLabel(group.year, null, group.season);
105
- return <Fragment key={`${group.year}-${group.season}`}>{renderArticleList(group.posts, group.year, null, title, group.season)}</Fragment>;
106
- });
161
+ const seasonGroups = groupPostsBySeason(visiblePosts);
162
+ const yearBlocks = groupSeasonGroupsByYear(seasonGroups);
163
+ articleList = yearBlocks.map((block) => (
164
+ <Fragment key={block.year}>
165
+ <div class="archive-era" id={`archive-year-${block.year}`}>
166
+ <span class="archive-era__roman">{toRoman(block.year)}</span>
167
+ <span class="archive-era__year" aria-hidden="true">
168
+ {" "}
169
+ {block.year}{" "}
170
+ </span>
171
+ </div>
172
+ {block.groups.map((group) => (
173
+ <Fragment key={`${group.year}-${group.season}`}>
174
+ {renderSeasonGroup({
175
+ posts: group.posts,
176
+ year: group.year,
177
+ season: group.season,
178
+ sectionTitle: group.season,
179
+ url_for,
180
+ date_xml,
181
+ date,
182
+ })}
183
+ </Fragment>
184
+ ))}
185
+ </Fragment>
186
+ ));
107
187
  } else {
108
188
  const season = page.month ? getSeason(page.month - 1) : null;
109
- articleList = renderArticleList(visiblePosts, page.year, page.month, null, season);
189
+ articleList = renderSeasonGroup({
190
+ posts: visiblePosts,
191
+ year: page.year,
192
+ season,
193
+ month: page.month,
194
+ url_for,
195
+ date_xml,
196
+ date,
197
+ });
110
198
  }
111
199
 
112
- const archiveDir = config?.archive_dir || "archives";
113
- const archiveBasePath = helper.localized_url_for(`/${archiveDir}/`, langKey);
114
- const currentYear = page.year ? Number(page.year) : null;
115
- const currentMonth = page.month ? Number(page.month) : null;
116
- const activeScope = getArchiveRangeLabel(currentYear, currentMonth);
117
- const yearCountLabel = years.length === 1 ? "1 year" : `${years.length} years`;
118
- const scopeSummaryLabel = currentYear ? `from ${activeScope}` : `across ${yearCountLabel}`;
119
- const latestLabel = latestPost ? date(latestPost.date) : "No posts yet";
200
+ const heroTitle = isTagPage ? page.tag : currentYear ? getArchiveRangeLabel(currentYear, currentMonth) : "Index";
201
+ const heroKind = isTagPage ? "Tag" : currentYear ? "Archive" : "Volume";
120
202
 
121
203
  return (
122
204
  <Fragment>
123
205
  <link rel="stylesheet" href={url_for("/css/archive.css")} data-page-head />
124
206
  <main class="archive-page">
125
207
  <header class="archive-hero">
126
- <div class="archive-hero__copy">
127
- <p class="archive-eyebrow">Archive Index</p>
128
- <h1>{activeScope}</h1>
129
- <p class="archive-hero__summary">
130
- {totalVisiblePosts ? `Browsing ${totalVisiblePosts} ${totalVisiblePosts === 1 ? "post" : "posts"} ${scopeSummaryLabel}.` : "No posts are available in this archive yet."}
131
- </p>
132
- </div>
133
- <dl class="archive-stats" aria-label="Archive summary">
134
- <div>
135
- <dt>Posts</dt>
136
- <dd>{totalVisiblePosts}</dd>
137
- </div>
138
- <div>
139
- <dt>Years</dt>
140
- <dd>{years.length}</dd>
141
- </div>
142
- <div>
143
- <dt>Latest</dt>
144
- <dd>{latestLabel}</dd>
145
- </div>
146
- </dl>
208
+ <p class="archive-hero__eyebrow">
209
+ <span>{heroKind}</span>
210
+ <span class="archive-hero__sep" aria-hidden="true">
211
+ ·
212
+ </span>
213
+ <span class="archive-hero__count">{entriesLabel}</span>
214
+ </p>
215
+ <h1 class="archive-hero__title">{heroTitle}</h1>
216
+ {years.length > 0 && (
217
+ <span class="archive-hero__roman" aria-hidden="true">
218
+ {years.length === 1 ? toRoman(years[0]) : `${toRoman(years[years.length - 1])}–${toRoman(years[0])}`}
219
+ </span>
220
+ )}
147
221
  </header>
148
222
 
149
- <nav class="archive-years" aria-label="Archive years">
150
- <a href={archiveBasePath} class={!currentYear ? "is-active" : null} aria-current={!currentYear ? "page" : null}>
151
- All
152
- </a>
153
- {years.map((year) => (
154
- <a key={year} href={helper.localized_url_for(`/${archiveDir}/${year}/`, langKey)} class={currentYear === year ? "is-active" : null} aria-current={currentYear === year ? "page" : null}>
155
- {year}
156
- </a>
157
- ))}
158
- </nav>
223
+ {!page.year && years.length > 1 && (
224
+ <aside class="archive-rail" aria-label="Jump to year">
225
+ <ol class="archive-rail__list">
226
+ {years.map((year) => (
227
+ <li key={year} class="archive-rail__item">
228
+ <a href={`#archive-year-${year}`} class="archive-rail__link">
229
+ <span class="archive-rail__year">{year}</span>
230
+ </a>
231
+ </li>
232
+ ))}
233
+ </ol>
234
+ </aside>
235
+ )}
159
236
 
160
237
  <div class="archive-stack">{articleList}</div>
161
238
  </main>
@@ -17,15 +17,14 @@ function getWordCount(content) {
17
17
 
18
18
  module.exports = class extends Component {
19
19
  render() {
20
- // index: true if in article list, false if in article page
21
- const { config, helper, page, index } = this.props;
20
+ const { config, helper, page } = this.props;
22
21
 
23
22
  const { url_for } = helper;
24
23
 
25
24
  const cover = page.cover ? url_for(page.cover) : null;
26
25
  const wordCount = getWordCount(page._content);
27
26
  const readTime = Math.ceil(wordCount / 200); // 假设每分钟阅读200字
28
- const hasComment = !index && config.comment && typeof config.comment.type === "string";
27
+ const hasComment = config.comment && typeof config.comment.type === "string";
29
28
  const translatedCommentsLabel = helper.__("article.comments");
30
29
  const commentsLabel = translatedCommentsLabel === "article.comments" ? "Comments" : translatedCommentsLabel;
31
30
 
@@ -34,7 +33,7 @@ module.exports = class extends Component {
34
33
  {/* Main content */}
35
34
  <div class="card">
36
35
  {/* Cover image */}
37
- {cover ? <ArticleCover page={page} cover={cover} index={index} helper={helper} /> : null}
36
+ {cover ? <ArticleCover page={page} cover={cover} helper={helper} /> : null}
38
37
  <article class={`card-content article${"direction" in page ? ` ${page.direction}` : ""}`}>
39
38
  {/* Metadata - Medium style */}
40
39
  {page.layout !== "page" ? (
@@ -45,102 +44,41 @@ module.exports = class extends Component {
45
44
  {dateFormatters.shortDay.format(page.date)}
46
45
  </time>
47
46
  )}
48
- {page.date && (wordCount > 0 || !index) && <span class="meta-separator">·</span>}
47
+ {page.date && <span class="meta-separator">·</span>}
49
48
  {wordCount > 0 && <span class="article-reading-time">{readTime} min</span>}
50
- {!index && (
51
- <Fragment>
52
- <span class="meta-separator">·</span>
53
- <span
54
- class="article-visit-count"
55
- data-flag-title={page.title}
56
- dangerouslySetInnerHTML={{
57
- __html: '<span id="busuanzi_page_pv"></span> PV',
58
- }}
59
- ></span>
60
- </Fragment>
61
- )}
49
+ <span class="meta-separator">·</span>
50
+ <span
51
+ class="article-visit-count"
52
+ data-flag-title={page.title}
53
+ dangerouslySetInnerHTML={{
54
+ __html: '<span id="busuanzi_page_pv"></span> PV',
55
+ }}
56
+ ></span>
62
57
  </div>
63
58
  </div>
64
59
  ) : null}
65
60
 
66
61
  {/* Title */}
67
- {page.title !== "" && index ? (
68
- <h2 class="article-title">
69
- <a href={url_for(page.link || page.path)}>{page.title}</a>
70
- </h2>
71
- ) : null}
72
- {page.title !== "" && !index ? <h1 class="article-title">{page.title}</h1> : null}
62
+ {page.title !== "" ? <h1 class="article-title">{page.title}</h1> : null}
73
63
 
74
- {!index && page.excerpt && <div class="article-excerpt" dangerouslySetInnerHTML={{ __html: page.excerpt }}></div>}
64
+ {page.excerpt && <div class="article-excerpt" dangerouslySetInnerHTML={{ __html: page.excerpt }}></div>}
75
65
 
76
- {(index || !page.excerpt) && (
77
- <div
78
- class={index && page.excerpt ? "article-excerpt" : "content"}
79
- dangerouslySetInnerHTML={{
80
- __html: index && page.excerpt ? page.excerpt : page.content,
81
- }}
82
- ></div>
83
- )}
84
-
85
- {!index && (
86
- <div class="article-footer article-meta-bar">
87
- <div class="article-tags">
88
- {page.tags?.length
89
- ? page.tags.map((tag, i) => (
90
- <Fragment>
91
- {i > 0 && <span class="meta-separator">·</span>}
92
- <a class="article-tag" rel="tag" href={helper.localized_tag_url(tag, helper.language_key(page))}>
93
- {tag.name}
94
- </a>
95
- </Fragment>
96
- ))
97
- : null}
98
- </div>
99
- <div class="article-title-actions">
100
- {hasComment && (
101
- <button type="button" class="article-action-btn" popovertarget="article-comment-popover" aria-label={commentsLabel} title={commentsLabel}>
102
- <svg
103
- xmlns="http://www.w3.org/2000/svg"
104
- width="24"
105
- height="24"
106
- viewBox="0 0 24 24"
107
- fill="none"
108
- stroke="currentColor"
109
- stroke-width="2"
110
- stroke-linecap="round"
111
- stroke-linejoin="round"
112
- role="img"
113
- aria-label={commentsLabel}
114
- >
115
- <title>{commentsLabel}</title>
116
- <path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719" />
117
- <path d="M8 12h.01" />
118
- <path d="M12 12h.01" />
119
- <path d="M16 12h.01" />
120
- </svg>
121
- </button>
122
- )}
123
- <button type="button" class="article-action-btn" popovertarget="article-font-settings" aria-label={helper.__("article.font_settings")} title={helper.__("article.font_settings")}>
124
- <svg
125
- xmlns="http://www.w3.org/2000/svg"
126
- width="24"
127
- height="24"
128
- viewBox="0 0 24 24"
129
- fill="none"
130
- stroke="currentColor"
131
- stroke-width="2"
132
- stroke-linecap="round"
133
- stroke-linejoin="round"
134
- role="img"
135
- aria-label={helper.__("article.font_settings")}
136
- >
137
- <title>{helper.__("article.font_settings")}</title>
138
- <path d="M12 4v16" />
139
- <path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" />
140
- <path d="M9 20h6" />
141
- </svg>
142
- </button>
143
- <button type="button" class="article-action-btn" popovertarget="article-info-popover" aria-label={helper.__("article.article_info")} title={helper.__("article.article_info")}>
66
+ <div class="article-meta-bar">
67
+ <div class="article-tags">
68
+ {page.tags?.length
69
+ ? page.tags.map((tag, i) => (
70
+ <Fragment>
71
+ {i > 0 && <span class="meta-separator">·</span>}
72
+ <a class="article-tag" rel="tag" href={helper.localized_tag_url(tag, helper.language_key(page))}>
73
+ {tag.name}
74
+ </a>
75
+ </Fragment>
76
+ ))
77
+ : null}
78
+ </div>
79
+ <div class="article-title-actions">
80
+ {hasComment && (
81
+ <button type="button" class="article-action-btn" popovertarget="article-comment-popover" aria-label={commentsLabel} title={commentsLabel}>
144
82
  <svg
145
83
  xmlns="http://www.w3.org/2000/svg"
146
84
  width="24"
@@ -152,40 +90,64 @@ module.exports = class extends Component {
152
90
  stroke-linecap="round"
153
91
  stroke-linejoin="round"
154
92
  role="img"
155
- aria-label={helper.__("article.article_info")}
93
+ aria-label={commentsLabel}
156
94
  >
157
- <title>{helper.__("article.article_info")}</title>
158
- <circle cx="12" cy="12" r="10" />
159
- <path d="M12 16v-4" />
160
- <path d="M12 8h.01" />
95
+ <title>{commentsLabel}</title>
96
+ <path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719" />
97
+ <path d="M8 12h.01" />
98
+ <path d="M12 12h.01" />
99
+ <path d="M16 12h.01" />
161
100
  </svg>
162
101
  </button>
163
- </div>
164
- </div>
165
- )}
166
- {index && page.tags?.length && (
167
- <div class="article-footer">
168
- <div class="article-tags">
169
- {page.tags.map((tag, i) => (
170
- <Fragment>
171
- {i > 0 && <span class="meta-separator">·</span>}
172
- <a class="article-tag" rel="tag" href={helper.localized_tag_url(tag, helper.language_key(page))}>
173
- {tag.name}
174
- </a>
175
- </Fragment>
176
- ))}
177
- </div>
178
- <a class="article-read-more" href={url_for(page.link || page.path)}>
179
- Read More
180
- </a>
102
+ )}
103
+ <button type="button" class="article-action-btn" popovertarget="article-font-settings" aria-label={helper.__("article.font_settings")} title={helper.__("article.font_settings")}>
104
+ <svg
105
+ xmlns="http://www.w3.org/2000/svg"
106
+ width="24"
107
+ height="24"
108
+ viewBox="0 0 24 24"
109
+ fill="none"
110
+ stroke="currentColor"
111
+ stroke-width="2"
112
+ stroke-linecap="round"
113
+ stroke-linejoin="round"
114
+ role="img"
115
+ aria-label={helper.__("article.font_settings")}
116
+ >
117
+ <title>{helper.__("article.font_settings")}</title>
118
+ <path d="M12 4v16" />
119
+ <path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" />
120
+ <path d="M9 20h6" />
121
+ </svg>
122
+ </button>
123
+ <button type="button" class="article-action-btn" popovertarget="article-info-popover" aria-label={helper.__("article.article_info")} title={helper.__("article.article_info")}>
124
+ <svg
125
+ xmlns="http://www.w3.org/2000/svg"
126
+ width="24"
127
+ height="24"
128
+ viewBox="0 0 24 24"
129
+ fill="none"
130
+ stroke="currentColor"
131
+ stroke-width="2"
132
+ stroke-linecap="round"
133
+ stroke-linejoin="round"
134
+ role="img"
135
+ aria-label={helper.__("article.article_info")}
136
+ >
137
+ <title>{helper.__("article.article_info")}</title>
138
+ <circle cx="12" cy="12" r="10" />
139
+ <path d="M12 16v-4" />
140
+ <path d="M12 8h.01" />
141
+ </svg>
142
+ </button>
181
143
  </div>
182
- )}
144
+ </div>
145
+
146
+ <div class="content" dangerouslySetInnerHTML={{ __html: page.content }}></div>
183
147
 
184
- {!index && page.excerpt && <div class="content" dangerouslySetInnerHTML={{ __html: page.content }}></div>}
185
148
  </article>
186
149
 
187
- {!index && (
188
- <div id="article-font-settings" popover="auto" class="article-popover article-font-popover">
150
+ <div id="article-font-settings" popover="auto" class="article-popover article-font-popover">
189
151
  <div class="article-popover-header">
190
152
  <h3>Display Settings</h3>
191
153
  <button type="button" class="article-popover-close" popovertarget="article-font-settings" popovertargetaction="hide" aria-label="Close">
@@ -366,7 +328,6 @@ module.exports = class extends Component {
366
328
  </aside>
367
329
  </div>
368
330
  </div>
369
- )}
370
331
 
371
332
  {hasComment && (
372
333
  <div id="article-comment-popover" popover="auto" class="article-popover article-comment-popover">
@@ -398,7 +359,7 @@ module.exports = class extends Component {
398
359
  </div>
399
360
  )}
400
361
 
401
- {!index && <ArticleInfo page={page} config={config} helper={helper} />}
362
+ <ArticleInfo page={page} config={config} helper={helper} />
402
363
  </div>
403
364
  </Fragment>
404
365
  );
@@ -2,7 +2,7 @@ const { Component } = require("inferno");
2
2
 
3
3
  module.exports = class extends Component {
4
4
  render() {
5
- const { page, cover, helper, index } = this.props;
5
+ const { page, cover, helper } = this.props;
6
6
  const { url_for } = helper;
7
7
 
8
8
  const imageSrcset = `${cover}?w=800 800w, ${cover}?w=1500 1500w, ${cover}?w=2000 2000w, ${cover} 6144w`;
@@ -19,8 +19,8 @@ module.exports = class extends Component {
19
19
  sizes="(max-width: 768px) 100vw, (max-width: 1024px) 100vw, 960px"
20
20
  referrerpolicy="no-referrer"
21
21
  decoding="async"
22
- loading={index ? "lazy" : "eager"}
23
- fetchpriority={index ? undefined : "high"}
22
+ loading="eager"
23
+ fetchpriority="high"
24
24
  />
25
25
  </a>
26
26
  );
@@ -12,11 +12,15 @@ function formatDate(date, dateXml) {
12
12
 
13
13
  module.exports = class extends Component {
14
14
  render() {
15
- const { url, title, date, dateXml } = this.props;
15
+ const { url, title, date, dateXml, excerpt, readTime } = this.props;
16
16
  const formattedDate = formatDate(date, dateXml);
17
+ const hasPreview = excerpt || readTime;
17
18
 
18
19
  return (
19
- <article class="archive-item">
20
+ <article
21
+ class={hasPreview ? "archive-item has-preview" : "archive-item"}
22
+ data-read-time={readTime || null}
23
+ >
20
24
  <div>
21
25
  <p class="article-meta">
22
26
  <time dateTime={dateXml || null}>{formattedDate}</time>
@@ -25,6 +29,9 @@ module.exports = class extends Component {
25
29
  {title}
26
30
  </a>
27
31
  </div>
32
+ {excerpt && (
33
+ <template class="archive-item__excerpt" dangerouslySetInnerHTML={{ __html: excerpt }}></template>
34
+ )}
28
35
  </article>
29
36
  );
30
37
  }
@@ -8,7 +8,10 @@ class Footer extends Component {
8
8
  <footer class="footer">
9
9
  <div class="footer-brand">
10
10
  <p class="footer-credit">
11
- 2022&ndash;PRESENT&ensp;<span class="footer-author">© GnixAij Oag</span> CC BY-NC-SA 4.0
11
+ 2022&ndash;PRESENT&ensp;<span class="footer-author">© GnixAij Oag</span>&ensp;
12
+ <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="license noopener noreferrer">
13
+ CC BY-NC-SA 4.0
14
+ </a>
12
15
  </p>
13
16
  {showVisitorCounter ? (
14
17
  <p class="footer-meta">