add timeline
2025-09-06  / site  / feature

create timeline page

Create file timeline/index.md under /source/[lang], set content as below. Data saves the update history of the site, and startPoint is the starting point.

Content of /source/en/timeline/index.md.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
---
title: Timeline
date: 2025-09-01 16:53:19
type: "timeline"
lang: en
startPoint:
title: Singularity
content: Over the course of 14 billion years since the Big Bang, the first light elements formed, neutral atoms coalesced, the earliest stars ignited, galaxies clustered, and eventually, our solar system emerged—giving rise to life on Earth. As part of this vast evolutionary saga, we too will ultimately vanish, becoming but a fleeting chapter in the grand narrative of existence. When that day comes, what part of us will endure? What legacy of our being will persist beyond our physical demise? Will it be the knowledge we have uncovered, the art we have created, the values we have upheld, or the impact we have imprinted upon the world? Will it be the dreams we dared to dream, the lives we touched, or the changes we set in motion? In the face of inevitable oblivion, it is these contributions—our ideas, our culture, our stewardship of the Earth—that may echo through the corridors of time, long after we are gone.
data:
- title: Create a site using the Chic theme.
date: 2023-10-23
link: 2025/07/hello-world
- title: Use Oranges theme and add album.
date: 2025-7-27
- title: Add 404 page and optimize search.
date: 2025-7-28
- title: Use Arch theme to support language switching; Optimize Arch to support Oranges original features; add prompt to view the original text on translated pages.
date: 2025-8-14
link: 2025/08/arch-theme-optimization
- title: Optimize the image storage directory to reduce redundant static resources in multilingual sites; add navigation menu icons and modify some styles.
date: 2025-8-31
- title: Add timeline page which supports viewing by month or year.
date: 2025-9-2
---

Content of /source/zh-CN/timeline/index.md.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
---
title: 时间轴
date: 2025-09-01 16:53:19
type: "timeline"
lang: zh-CN
startPoint:
title: 奇点
content: 自宇宙大爆炸以来的140亿年里,第一批轻元素形成,中性原子聚合,最早的恒星燃烧,星系聚集成团,最终,我们的太阳系诞生——地球上的生命由此而来。在这浩瀚的进化史诗中,我们终将消逝,成为存在宏大叙事中转瞬即逝的一章。当那一天来临,我们何物能永存?超越肉体消亡的永恒印记究竟是什么?是我们发掘的知识、创造的艺术、坚守的价值观,还是对世界留下的烙印?是敢于追逐的梦想、触动过的人生,抑或引发的变革浪潮?面对不可避免的湮没,正是这些贡献——我们的思想,我们的文化,我们对地球的守护——将在我们逝去很久之后,在时间的长河中回荡。
padZero: false
data:
- title: 建立站点,使用Chic主题。
date: 2023-10-23
link: 2025/07/hello-world
- title: 使用Oranges主题,增加相册功能。
date: 2025-7-27
- title: 增加404页面,优化搜索功能。
date: 2025-7-28
- title: 使用Arch主题以支持切换语言;优化Arch以支持Oranges原有功能;翻译的页面增加查看原文的提示。
date: 2025-8-14
link: 2025/08/arch-theme-optimization
- title: 优化图片保存目录以减少多语言站点下冗余的静态资源;添加导航菜单的icon,修改部分样式。
date: 2025-8-31
- title: 增加时间轴页面,支持按月或按年查看。
date: 2025-9-2
---

set page layout

post.ejs

Update file post.ejs under /themes/arch/layout and add content as below.

1
2
3
4
5
6
7
8
9
10
11
12
<!-- timeline -->
<% if(page.type === 'timeline') { %>
<div class="container post-details album-index">
<div class="markdown-body">
<% if (page.data && page.data.length) { %>
<% let viewByYear = __('viewByYear') %>
<% let viewByMonth = __('viewByMonth') %>
<%- partial('_partial/timeline', { data: page.data, lang: page.lang, padZero: page.padZero, viewByYear, viewByMonth,startPoint: page.startPoint, title: page.title }) %>
<% } %>
</div>
</div>
<% } %>
image.png

timeline.ejs

Create file timeline.ejs under /themes/arch/layout/_patial and set content as below.

I have to thanks for author of https://github.com/PrintNow/TimeLine. Most of part of this file is taken from it. Some differences are listed below.

  1. time is rename to date, and it's not timestamp but yyyy-m-d format string.
  2. add groupBy feature (by year or month) , click will change group condition to another.
  3. add startPoint, too many lines will collapse.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
<div class='albums-container'>
<ul class='time-line'></ul>
</div>
<script type='text/javascript'>
let data = <%- JSON.stringify(data || []) %>;
const lang = <%- JSON.stringify(lang || 'en') %>;

const startPoint = <%- JSON.stringify(startPoint || '{}') %>;

const viewByYear = <%- JSON.stringify(viewByYear || 'View by Year') %>;
const viewByMonth = <%- JSON.stringify(viewByMonth || 'View by Month') %>;

const padZero = <%- JSON.stringify(padZero || false) %>;

// 按年月分组,组内降序,key降序
let groupByMonthFlag = true;

const eng_months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];


// 日期降序
data = data.sort((a, b) => new Date(b.date) - new Date(a.date));

let time_line = document.querySelector('.time-line');
// 添加data-lang特性
time_line.dataset.lang = lang;

renderTimeline(data);

// data:[{title: '标题', link: '链接', date: '2025-7-1'}]
function renderTimeline(data = []) {
if (!(Array.isArray(data) && data.length)) return;
let grouped2DArr = groupByMonthFlag ? groupByYearMonth(data) : groupByYear(data);
let html = '';

time_line.innerHTML = '';//清空时间线
for (let item of grouped2DArr) {
html += `
<li class='tl-header group-condition' title='${groupByMonthFlag ? viewByYear : viewByMonth}'>
<p class='group-condition'>${formatGroup(item[0], groupByMonthFlag)}</p>
</li>
<ul class='tl-body'>`;

for (let obj of item[1]) {
html += `<li>
<span>${formatRest(obj['date'], groupByMonthFlag)}</span>
${obj['link'] ? `<p class='title'><a href='/${lang}/${obj['link']}'>${obj['title']}</a></p>` : `<p class='title'>${obj['title']}</p>`}
</li>`
}
html += `</ul>`;

}
if (startPoint) {
html += `<li class='tl-header start'>
<p>${startPoint.title}</p>
</li>`;
html += `<ul class='tl-body start'>
<li>
<div class="title section">
<input class="content-check" type="checkbox" id="c1" hidden />
<div class="content">
<p class="text">${startPoint.content}</p>
<label for="c1" class="btn"></label>
</div>
</div>
</li>
</ul>`;
}
time_line.innerHTML = html;
}

time_line.addEventListener('click', toggleGroupCondition);

// 切换分组条件
function toggleGroupCondition(e) {
// 不是点击 group-condition
if (!e.target.classList.contains('group-condition')) {
return;
}
groupByMonthFlag = !groupByMonthFlag;
if (time_line.classList.contains('group-by-year')) {
time_line.classList.remove('group-by-year');
} else {
time_line.classList.add('group-by-year');
}
renderTimeline(data);
}

// 格式化分组条件
function formatGroup(dateStr, isGroupByMonth) {
let date = new Date(dateStr);

let year = date.getFullYear();
if (!isGroupByMonth) {
if (lang === 'zh-CN') {
return addLeadingZero(year, padZero) + '年';
}
return year;
}

let month = date.getMonth() + 1;

if (lang === 'zh-CN') {
year = addLeadingZero(year, padZero) + '年';
month = addLeadingZero(month, padZero) + '月';
return year + month;
}

// 英语采用day-month-year展示
return eng_months[month - 1] + ' ' + year;
}

// 格式化分组内日期
function formatRest(dateStr, isGroupByMonth) {
let date = new Date(dateStr);

let day = date.getDate();
if (isGroupByMonth) {
if (lang === 'zh-CN') {
return addLeadingZero(day, padZero) + '日';
}
return day + getOrdinalSuffix(day);
}

let month = date.getMonth() + 1;
if (lang === 'zh-CN') {
month = addLeadingZero(month, padZero) + '月';
day = addLeadingZero(day, padZero) + '日';
return month + day;
}

// 英语采用day-month-year展示
month = eng_months[month - 1];
day = day + getOrdinalSuffix(day);
return day + ' ' + month;
}

function groupByYear(data) {
// 按年分组,组内降序
const groupObj = data.reduce((acc, item) => {
const year = new Date(item.date).getFullYear();
if (!acc[year]) acc[year] = [];
acc[year].push(item);
return acc;
}, {});
// key降序
return Object.entries(groupObj).sort((a, b) => b[0] - a[0]);
}

function groupByYearMonth(data) {
// 按月分组,组内降序
const groupObj = data.reduce((acc, item) => {
const date = new Date(item.date);
const yearMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
if (!acc[yearMonth]) acc[yearMonth] = [];
acc[yearMonth].push(item);
return acc;
}, {});
// key降序
return Object.entries(groupObj).sort((a, b) => b[0].localeCompare(a[0]));
}

// 添加前导零
function addLeadingZero(num, padZero) {
return padZero && num < 10 ? '0' + num : num;
}

// 英文序数词
function getOrdinalSuffix(day) {
if (day >= 11 && day <= 13) {
return 'th';
}
switch (day % 10) {
case 1: return 'st';
case 2: return 'nd';
case 3: return 'rd';
default: return 'th';
}
}

</script>
<link rel='stylesheet' href='/css/timeline.css' />

set page style

timeline.css

Create file timeline.css under /themes/arch/source/css and set content as below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
.time-line li {
list-style-type: none;
}

.time-line .tl-header {
cursor: pointer;
background-color: #23b7e5;
color: white;
text-align: center;
display: inline-block;
padding: 0 14px;
border-radius: 8px;
}
.time-line .tl-header p {
font-size: 14px;
line-height: 8px;
}

/*时间垂直线*/
.time-line .tl-body {
position: relative;
border-left: 4px solid #23b7e5;
margin-left: 40px;
min-height: 88px;
padding-top: 1px;
padding-bottom: 1px;
}

/*时间线左侧,日 样式*/
.time-line .tl-body span {
position: absolute;
left: -50px;
top: 16px;
font-weight: 400;
font-size: 14px;
}

.time-line .tl-body li {
display: block;
position: relative;
left: -24px;
margin: 8px 0;
}
.time-line .tl-body li:before {
position: absolute;
content: "";
top: 21px;
left: -15px;
width: 6px;
height: 6px;
background: #fff;
border: 2px solid #23b7e5;
border-radius: 50%;
}

@media (max-width: 770px) {
.time-line .tl-body li:before {
left: -11px;
}
}

.time-line.group-by-year .tl-header:not(.start) {
padding: 0 28px;
}

.time-line.group-by-year[data-lang="zh-CN"] .tl-header:not(.start) {
padding: 0 21px;
}

.time-line[data-lang="zh-CN"]:not(.group-by-year) .tl-body:not(.start) {
margin-left: 48px;
}

.time-line.group-by-year .tl-body:not(.start) span {
left: -76px;
}

/*设置时间线内容样式*/
.time-line .tl-body li .title {
background-color: #23b7e5;
color: white;
display: inline-block;
margin: 8px 0;
margin-left: 10px;
padding: 8px 16px;
border-radius: 6px;
font-weight: 400;
}
.time-line .tl-body li .title:before {
content: "";
position: absolute;
left: -8px;
border: 10px solid rgba(0, 0, 0, 0);
border-right-color: #23b7e5;
}

.time-line .tl-body li a {
text-decoration: none;
color: white;
border-bottom: none;
}

.time-line .tl-body li a:hover {
border-bottom: 1px solid #fff;
}

/*设置时间线开始样式*/
.tl-header.start {
margin-top: 100px;
padding: 0 8px;
position: relative;
left: -4px;
cursor: default;
}

.time-line[data-lang="zh-CN"] .tl-header.start {
padding: 0 30px;
}

.time-line[data-lang="zh-CN"]:not(.group-by-year) .tl-header.start {
left: 4px;
}

.time-line[data-lang="zh-CN"]:not(.group-by-year) .tl-body.start {
margin-left: 94px;
}

.tl-header.start::before {
content: "";
position: absolute;
top: -96px;
height: 92px;
border-left: 4px dotted #23b7e5;
}

.tl-body.start {
border: none;
margin-left: 86px;
margin-top: -50px;
}

.tl-body.start li::before {
content: none !important;
}
.tl-body.start .title {
margin-left: 50px;
}

.tl-body.start .title ::before {
top: 24px !important;
}

.tl-header.start > .title {
font-weight: 500;
}

.tl-body.start .section {
display: flex !important;
}

.tl-body.start .content {
max-height: 68px;
overflow: hidden;
border-radius: 4px;
padding: 0;
}

.tl-body.start .content .text {
box-sizing: border-box;
white-space: pre-wrap;
width: 100%;
float: right;
line-height: 1.5;
margin: 0;
margin-left: -100px;
-webkit-mask: linear-gradient(red 30px, transparent 70px);
}

.tl-body.start .content::before {
content: "";
width: 100px;
height: 100%;
float: left;
}

.tl-body.start .content .btn {
float: right;
width: 100px;
text-align: center;
position: relative;
left: calc(50% - 50px);
transform: translateY(-100%);
cursor: pointer;
}

.tl-body.start .content .btn::after {
content: "";
position: relative;
top: -2px;
display: block;
height: 16px;
background-color: #fff;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E %3Cpath d='M143 352.3L7 216.3c-9.4-9.4-9.4-24.6 0-33.9l22.6-22.6c9.4-9.4 24.6-9.4 33.9 0l96.4 96.4 96.4-96.4c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9l-136 136c-9.2 9.4-24.4 9.4-33.8 0z'%3E%3C/path%3E %3C/svg%3E")
center/ 24px 24px no-repeat;
}

.tl-body.start .content .btn::before {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 16px;
}

.tl-body.start .content-check:checked + .content {
max-height: fit-content;
padding-bottom: 16px;
}

.tl-body.start .content-check:checked + .content .btn {
left: auto;
right: calc(50% - 50px);
}

.tl-body.start .content-check:checked + .content .btn::after {
transform: scaleY(-1) translateY(-18px);
}

.tl-body.start .content-check:checked + .content .text {
-webkit-mask: none;
}

main.styl

Update main.styl under /themes/arch/source/css and add content below.

1
2
3
4
5
6
7
8
9
10
11
.time-line .tl-header, .time-line .title {
background-color: $theme-color !important;
}

.time-line .tl-body, .time-line li:before {
border-color: $theme-color !important;
}

.time-line .title:before {
border-right-color: $theme-color !important;
}
image-1.png

add translation

Update en.yml and zh-CN.yml under /themes/arch/languages and set content as below.

Content of /themes/arch/languages/en.yaml.

1
2
3
Timeline: Timeline
viewByYear: View by Year
viewByMonth: View by Month

Content of /themes/arch/languages/zh-CN.yml

1
2
3
Timeline: 时间轴
viewByYear: 按年查看
viewByMonth: 按月查看

add navbar

Update navbar in _config.arch.yml and add content as below.

1
2
3
4
5
6
navbar:
-
name: Timeline
enable: true
path: /timeline/
key: timeline
image-2.png

screenshot

screenshot.gif