fossbook 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -105,12 +105,7 @@ module.exports = {
105
105
  defaultLanguageInSubdir: false,
106
106
  languages: {
107
107
  en: { languageName: "English", locale: "en-US" },
108
- ko: {
109
- languageName: "한국어",
110
- locale: "ko-KR",
111
- blogName: "나의 블로그",
112
- blogDescription: "한국어 블로그",
113
- },
108
+ ko: { languageName: "한국어", locale: "ko-KR" },
114
109
  },
115
110
 
116
111
  // Optional comments (see "Comments" below)
@@ -132,6 +127,11 @@ module.exports = {
132
127
 
133
128
  ## Content Format
134
129
 
130
+ Fossbook supports GitHub Flavored Markdown plus extensions for image sizing
131
+ and alignment, captions, comic dialogue, aligned links, Mermaid diagrams, and
132
+ syntax-highlighted code. See [Fossbook Markdown](fossbook_markdown.md) for
133
+ syntax and examples.
134
+
135
135
  ### Post front-matter
136
136
 
137
137
  ```markdown
@@ -192,8 +192,6 @@ module.exports = {
192
192
  ko: {
193
193
  languageName: "한국어",
194
194
  locale: "ko-KR",
195
- blogName: "나의 블로그",
196
- blogDescription: "한국어 블로그",
197
195
  },
198
196
  ja: {
199
197
  languageName: "日本語",
@@ -203,14 +201,43 @@ module.exports = {
203
201
  };
204
202
  ```
205
203
 
204
+ Put localized blog information in a sibling config file for each language. For
205
+ example, `fossbook.config.en.js` contains:
206
+
207
+ ```js
208
+ module.exports = {
209
+ blogName: "My Blog",
210
+ authorName: "Jane Doe",
211
+ authorDescription: "Open source developer and writer",
212
+ blogDescription: "An English-language blog",
213
+ };
214
+ ```
215
+
216
+ And `fossbook.config.ko.js` contains:
217
+
218
+ ```js
219
+ module.exports = {
220
+ blogName: "나의 블로그",
221
+ authorName: "이수현",
222
+ authorDescription: "오픈 소스 개발자이자 작가",
223
+ blogDescription: "한국어 블로그",
224
+ homeLabel: "대문",
225
+ allPostsLabel: "모든 글",
226
+ aboutLabel: "소개",
227
+ tagsLabel: "태그",
228
+ allTagsLabel: "모든 태그",
229
+ };
230
+ ```
231
+
206
232
  - `defaultLanguage` identifies the language represented by `index.md` and
207
233
  `about.md`.
208
234
  - `defaultLanguageInSubdir: false` keeps the default language at the site root.
209
235
  Other languages are generated below their language code, such as `/ko/`.
210
236
  - Set `defaultLanguageInSubdir: true` to generate the default language below
211
237
  its code as well, such as `/en/`.
212
- - Each language can override site values such as `blogName` and
213
- `blogDescription`.
238
+ - Language config files use the main config filename with the language code
239
+ inserted before the extension: `fossbook.config.<language>.js`.
240
+ - Values omitted from a language config fall back to `fossbook.config.js`.
214
241
  - `locale` controls language-specific date formatting.
215
242
  - Language keys should use standard language tags such as `en`, `ko`, `ja`, or
216
243
  `zh-Hant`.
package/lib/all_posts.js CHANGED
@@ -9,15 +9,15 @@ module.exports = class AllPostsPage extends PageBase {
9
9
 
10
10
  // posts: array of Page objects
11
11
  generateContent(posts) {
12
- const pageTitle = "All posts";
12
+ const pageTitle = this.config.allPostsLabel || "All posts";
13
13
  const allPostsDir = path.join(this.config.dev.outdir, "all_posts");
14
14
  if (!fs.existsSync(allPostsDir))
15
15
  fs.mkdirSync(allPostsDir);
16
16
 
17
17
  const data = { posts: posts, pageTitle: pageTitle };
18
- this.url = `${this.config.blogsite}/posts.html`;
18
+ this.url = `${this.config.blogsite}/all_posts/`;
19
19
  this.title = `${this.config.blogName}: ${data.pageTitle}`;
20
- this.description = "All posts";
20
+ this.description = pageTitle;
21
21
  this.imageURL = this.config.image;
22
22
 
23
23
  const templatePath = path.join(this.config.themePath, "layouts", "all_posts.html");
package/lib/index.js CHANGED
@@ -75,6 +75,36 @@ function createAboutTranslations(languageConfigs) {
75
75
  });
76
76
  }
77
77
 
78
+ function createHomeTranslations(languageConfigs) {
79
+ return languageConfigs.map((config) => ({
80
+ language: config.language,
81
+ languageName: config.languageName,
82
+ path: config.basePath || "/",
83
+ url: `${config.blogsite}/`,
84
+ isDefault: config.language === config.defaultLanguage,
85
+ }));
86
+ }
87
+
88
+ function createAllPostsTranslations(languageConfigs) {
89
+ return languageConfigs.map((config) => ({
90
+ language: config.language,
91
+ languageName: config.languageName,
92
+ path: `${config.basePath || "/"}all_posts/`,
93
+ url: `${config.blogsite}/all_posts/`,
94
+ isDefault: config.language === config.defaultLanguage,
95
+ }));
96
+ }
97
+
98
+ function createTagListTranslations(languageConfigs) {
99
+ return languageConfigs.map((config) => ({
100
+ language: config.language,
101
+ languageName: config.languageName,
102
+ path: `${config.basePath || "/"}tags/`,
103
+ url: `${config.blogsite}/tags/`,
104
+ isDefault: config.language === config.defaultLanguage,
105
+ }));
106
+ }
107
+
78
108
  function buildLanguage(config) {
79
109
  fs.mkdirSync(config.dev.outdir, { recursive: true });
80
110
 
@@ -88,14 +118,17 @@ function buildLanguage(config) {
88
118
 
89
119
  // Create home page and pagination in output/page directory
90
120
  const homePagination = new HomePagination(config);
121
+ homePagination.translations = config.homeTranslations || [];
91
122
  homePagination.generateContent(postObjects);
92
123
 
93
124
  // Create all posts page in output/all_posts directory
94
125
  const allPostsPage = new AllPostsPage(config);
126
+ allPostsPage.translations = config.allPostsTranslations || [];
95
127
  allPostsPage.generateContent(postObjects);
96
128
 
97
129
  // Create tag pages in output/tags directory
98
130
  const tagPages = new TagPages(config);
131
+ tagPages.translations = config.tagListTranslations || [];
99
132
  tagPages.generateContent(postObjects);
100
133
 
101
134
  // Create about page in output/about directory
@@ -132,9 +165,15 @@ function build(config) {
132
165
  const languageConfigs = config.languageConfigs || [config];
133
166
  const postTranslationIndex = createPostTranslationIndex(languageConfigs);
134
167
  const aboutTranslations = createAboutTranslations(languageConfigs);
168
+ const homeTranslations = createHomeTranslations(languageConfigs);
169
+ const allPostsTranslations = createAllPostsTranslations(languageConfigs);
170
+ const tagListTranslations = createTagListTranslations(languageConfigs);
135
171
  languageConfigs.forEach((languageConfig) => {
136
172
  languageConfig.postTranslationIndex = postTranslationIndex;
137
173
  languageConfig.aboutTranslations = aboutTranslations;
174
+ languageConfig.homeTranslations = homeTranslations;
175
+ languageConfig.allPostsTranslations = allPostsTranslations;
176
+ languageConfig.tagListTranslations = tagListTranslations;
138
177
  });
139
178
  languageConfigs.forEach(buildLanguage);
140
179
 
@@ -150,4 +189,7 @@ module.exports = {
150
189
  buildLanguage,
151
190
  createPostTranslationIndex,
152
191
  createAboutTranslations,
192
+ createHomeTranslations,
193
+ createAllPostsTranslations,
194
+ createTagListTranslations,
153
195
  };
package/lib/init.js CHANGED
@@ -124,8 +124,11 @@ function initProject(options = {}) {
124
124
  defaultLanguageInSubdir: false,
125
125
  // languages: {
126
126
  // en: { languageName: "English", locale: "en-US" },
127
- // ko: { languageName: "한국어", locale: "ko-KR", blogName: "나의 블로그" },
127
+ // ko: { languageName: "한국어", locale: "ko-KR" },
128
128
  // },
129
+ // Add localized blog information to fossbook.config.en.js,
130
+ // fossbook.config.ko.js, and so on. Menu labels can also be localized with
131
+ // homeLabel, allPostsLabel, aboutLabel, tagsLabel, and allTagsLabel.
129
132
 
130
133
  // Comment system (optional)
131
134
  // comments: { provider: "utterances", repo: "user/repo", issueTerm: "pathname", theme: "github-light" },
package/lib/mod/config.js CHANGED
@@ -98,12 +98,12 @@ function loadConfig(configPath) {
98
98
  // Normalize postsPath into a bare segment without surrounding slashes
99
99
  merged.postsPath = (merged.postsPath || "").replace(/^\/+|\/+$/g, "");
100
100
 
101
- merged.languageConfigs = createLanguageConfigs(merged);
101
+ merged.languageConfigs = createLanguageConfigs(merged, resolvedConfigPath);
102
102
 
103
103
  return merged;
104
104
  }
105
105
 
106
- function createLanguageConfigs(config) {
106
+ function createLanguageConfigs(config, configPath) {
107
107
  const configuredLanguages = config.languages;
108
108
  const hasMultipleLanguages = configuredLanguages !== null && configuredLanguages !== undefined;
109
109
  const languages = hasMultipleLanguages
@@ -125,6 +125,10 @@ function createLanguageConfigs(config) {
125
125
  throw new Error(`languages.${language} must be an object`);
126
126
  }
127
127
 
128
+ const fileOverrides = configPath
129
+ ? loadLanguageConfig(configPath, language)
130
+ : {};
131
+
128
132
  const useSubdirectory = config.defaultLanguageInSubdir || language !== config.defaultLanguage;
129
133
  const languagePrefix = useSubdirectory ? language : "";
130
134
  const basePath = appendPathSegment(config.basePath, languagePrefix);
@@ -133,9 +137,12 @@ function createLanguageConfigs(config) {
133
137
  return {
134
138
  ...config,
135
139
  ...overrides,
140
+ ...fileOverrides,
141
+ defaultLanguage: config.defaultLanguage,
142
+ defaultLanguageInSubdir: config.defaultLanguageInSubdir,
136
143
  language,
137
- languageName: overrides.languageName || language,
138
- locale: overrides.locale || language,
144
+ languageName: fileOverrides.languageName || overrides.languageName || language,
145
+ locale: fileOverrides.locale || overrides.locale || language,
139
146
  languagePrefix,
140
147
  basePath,
141
148
  blogsite,
@@ -149,6 +156,22 @@ function createLanguageConfigs(config) {
149
156
  });
150
157
  }
151
158
 
159
+ function loadLanguageConfig(configPath, language) {
160
+ const extension = path.extname(configPath) || ".js";
161
+ const configStem = extension
162
+ ? configPath.slice(0, -extension.length)
163
+ : configPath;
164
+ const languageConfigPath = `${configStem}.${language}${extension}`;
165
+
166
+ if (!fs.existsSync(languageConfigPath)) return {};
167
+
168
+ const languageConfig = require(languageConfigPath);
169
+ if (!languageConfig || typeof languageConfig !== "object" || Array.isArray(languageConfig)) {
170
+ throw new Error(`${path.basename(languageConfigPath)} must export an object`);
171
+ }
172
+ return languageConfig;
173
+ }
174
+
152
175
  function appendPathSegment(basePath, segment) {
153
176
  const normalizedBase = `/${basePath || ""}`.replace(/\/{2,}/g, "/");
154
177
  const withTrailingSlash = normalizedBase.endsWith("/")
package/lib/mod/marked.js CHANGED
@@ -1,6 +1,71 @@
1
1
  const marked = require("marked");
2
2
  const hljs = require("highlight.js");
3
3
 
4
+ function escapeAttribute(value) {
5
+ return value
6
+ .replace(/&/g, "&amp;")
7
+ .replace(/"/g, "&quot;")
8
+ .replace(/</g, "&lt;")
9
+ .replace(/>/g, "&gt;");
10
+ }
11
+
12
+ function parsePanelAttributes(source) {
13
+ const attributes = {};
14
+ const attributePattern = /([a-z][a-z-]*)="([^"]*)"/gy;
15
+ let offset = 0;
16
+
17
+ while (offset < source.length) {
18
+ while (source[offset] === " " || source[offset] === "\t") offset += 1;
19
+ if (offset === source.length) break;
20
+
21
+ attributePattern.lastIndex = offset;
22
+ const match = attributePattern.exec(source);
23
+ if (!match) throw new Error(`Invalid panels attribute near: ${source.slice(offset)}`);
24
+ if (match[1] !== "columns" && match[1] !== "label") {
25
+ throw new Error(`Unsupported panels attribute: ${match[1]}`);
26
+ }
27
+ if (Object.hasOwn(attributes, match[1])) {
28
+ throw new Error(`Duplicate panels attribute: ${match[1]}`);
29
+ }
30
+ attributes[match[1]] = match[2];
31
+ offset = attributePattern.lastIndex;
32
+ }
33
+
34
+ const columns = attributes.columns === undefined ? 2 : Number(attributes.columns);
35
+ if (!Number.isInteger(columns) || columns < 1 || columns > 6) {
36
+ throw new Error("Panel columns must be an integer from 1 to 6");
37
+ }
38
+
39
+ return { columns, label: attributes.label };
40
+ }
41
+
42
+ const panelsExtension = {
43
+ name: "panels",
44
+ level: "block",
45
+ start(source) {
46
+ return source.match(/^:::panels(?:[ \t]|$)/m)?.index;
47
+ },
48
+ tokenizer(source) {
49
+ const match = /^:::panels([^\n]*)\n([\s\S]*?)\n:::(?:\n|$)/.exec(source);
50
+ if (!match) return undefined;
51
+
52
+ const attributes = parsePanelAttributes(match[1].trim());
53
+ return {
54
+ type: "panels",
55
+ raw: match[0],
56
+ columns: attributes.columns,
57
+ label: attributes.label,
58
+ tokens: this.lexer.blockTokens(match[2]),
59
+ };
60
+ },
61
+ renderer(token) {
62
+ const label = token.label
63
+ ? ` role="group" aria-label="${escapeAttribute(token.label)}"`
64
+ : "";
65
+ return `<div class="panel-group" style="--panel-columns: ${token.columns};"${label}>\n${this.parser.parse(token.tokens)}</div>\n`;
66
+ },
67
+ };
68
+
4
69
  marked.setOptions({
5
70
  renderer: new marked.Renderer(),
6
71
  pedantic: false,
@@ -66,7 +131,7 @@ const renderer = {
66
131
  // Construct the image tag with optional size and title
67
132
  let imageTag = `<img src="${href}" alt="${text}"`;
68
133
  if (size) {
69
- imageTag += ` style="width: ${size};"`;
134
+ imageTag += ` class="sized-image" style="width: ${size};"`;
70
135
  }
71
136
  imageTag += ">";
72
137
 
@@ -114,6 +179,7 @@ const renderer = {
114
179
  };
115
180
 
116
181
  marked.use({
182
+ extensions: [panelsExtension],
117
183
  renderer,
118
184
  hooks: {
119
185
  postprocess(html) {
package/lib/tag/index.js CHANGED
@@ -46,10 +46,11 @@ module.exports = class TagList extends PageBase {
46
46
  if (!fs.existsSync(tagsDir))
47
47
  fs.mkdirSync(tagsDir);
48
48
 
49
+ const pageTitle = this.config.allTagsLabel || "All tags";
49
50
  this.imageURL = this.config.image;
50
- this.description = "Tag List";
51
- const data = { tags: tagArray, pageTitle: "All tags" };
52
- this.url = `${this.config.blogsite}/tag_list.html`;
51
+ this.description = pageTitle;
52
+ const data = { tags: tagArray, pageTitle };
53
+ this.url = `${this.config.blogsite}/tags/`;
53
54
  this.title = `${this.config.blogName}: ${data.pageTitle}`;
54
55
 
55
56
  const templatePath = path.join(this.config.themePath, "layouts", "tag_list.html");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fossbook",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "A lightweight static blog site generator for GitHub Pages",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -158,7 +158,7 @@
158
158
  }
159
159
 
160
160
  a {
161
- border-bottom: 3px solid var(--maincolor);
161
+ border-bottom: 2px solid var(--maincolor);
162
162
  color: inherit;
163
163
  text-decoration: none;
164
164
  }
@@ -223,6 +223,46 @@
223
223
  figure img {
224
224
  max-height: 500px;
225
225
  }
226
+
227
+ .panel-group {
228
+ display: grid;
229
+ grid-template-columns: repeat(var(--panel-columns), minmax(0, 1fr));
230
+ gap: 1.5rem;
231
+ align-items: start;
232
+ margin: 1.5rem 0;
233
+ }
234
+
235
+ .panel-group .image-container,
236
+ .panel-group figure {
237
+ min-width: 0;
238
+ width: 100%;
239
+ }
240
+
241
+ .panel-group figure {
242
+ display: block;
243
+ padding: 0;
244
+ }
245
+
246
+ .panel-group img {
247
+ display: block;
248
+ height: auto;
249
+ width: 100%;
250
+ }
251
+
252
+ .panel-group figcaption {
253
+ margin-top: 0.75rem;
254
+ text-align: center;
255
+ }
256
+
257
+ @media screen and (max-width: 599px) {
258
+ .panel-group {
259
+ grid-template-columns: 1fr;
260
+ }
261
+
262
+ .sized-image {
263
+ width: 100% !important;
264
+ }
265
+ }
226
266
 
227
267
  @media screen and (min-width: 600px) {
228
268
  figure {
@@ -291,16 +331,20 @@
291
331
  font-size: 1.5rem;
292
332
  }
293
333
  h1, h2, h3, h4, h5, h6 {
294
- font-size: 1.2rem;
334
+ line-height: 1.3;
335
+ margin-bottom: 0.75em;
295
336
  margin-top: 2em;
296
337
  }
297
-
298
- h1::before { color: var(--maincolor); content: '# '; }
299
- h2::before { color: var(--maincolor); content: '## '; }
300
- h3::before { color: var(--maincolor); content: '### '; }
301
- h4::before { color: var(--maincolor); content: '#### '; }
302
- h5::before { color: var(--maincolor); content: '##### '; }
303
- h6::before { color: var(--maincolor); content: '###### '; }
338
+
339
+ h1 { font-size: 1.4rem; }
340
+ h2 { font-size: 1.2rem; }
341
+ h3 { font-size: 1.1rem; }
342
+ h4 {
343
+ font-size: 1rem;
344
+ font-weight: 700;
345
+ }
346
+ h5 { font-size: 0.9rem; }
347
+ h6 { font-size: 0.8rem; }
304
348
 
305
349
  .meta {
306
350
  color: #999;
@@ -331,6 +375,10 @@
331
375
  nav {
332
376
  font-family: 'Roboto Mono', monospace;
333
377
  }
378
+ html:lang(ko) header,
379
+ html:lang(ko) nav {
380
+ font-family: 'NanumSquareNeo', sans-serif;
381
+ }
334
382
  nav.post-navigation {
335
383
  display: flex;
336
384
  justify-content: end;
@@ -365,6 +413,7 @@
365
413
 
366
414
  /* Common */
367
415
  .title h1 {
416
+ font-size: 1.7rem;
368
417
  margin-bottom: 0;
369
418
  }
370
419
 
@@ -394,7 +443,7 @@
394
443
  }
395
444
 
396
445
  .callout a {
397
- border-bottom: 3px solid #fff;
446
+ border-bottom: 2px solid #fff;
398
447
  }
399
448
 
400
449
  .callout a:hover {
@@ -410,7 +459,7 @@
410
459
  content: "🏷 ";
411
460
  }
412
461
  .tags a{
413
- border-bottom: 3px solid var(--maincolor);
462
+ border-bottom: 2px solid var(--maincolor);
414
463
  }
415
464
  .tags a:hover{
416
465
  color:white;
@@ -10,6 +10,8 @@
10
10
  <!-- Google tag (gtag.js) -->
11
11
  ${page.config.googleAnalyticsID ? page.googleAnalytics(page.config.googleAnalyticsID) : ""}
12
12
  <title>${page.config.blogName}: ${data.pageTitle}</title>
13
+ <link rel="canonical" href="${page.url}" />
14
+ ${page.alternateLanguageLinks()}
13
15
  ${page.openGraph("website")}
14
16
  </head>
15
17
  <body>
@@ -17,13 +19,16 @@
17
19
  <header>
18
20
  <div class="main">${page.config.blogName}</div>
19
21
  <nav>
20
- <a href="${page.config.basePath}">Home</a>
22
+ <a href="${page.config.basePath}">${page.config.homeLabel || "Home"}</a>
21
23
  ${data.pageTitle}
22
- <a href="${page.config.basePath}about">About</a>
23
- <a href="${page.config.basePath}tags">Tags</a>
24
+ <a href="${page.config.basePath}about">${page.config.aboutLabel || "About"}</a>
25
+ <a href="${page.config.basePath}tags">${page.config.tagsLabel || "Tags"}</a>
24
26
  </nav>
25
27
  </header>
26
- <h1 class="page-title">${data.pageTitle}</h1>
28
+ <div class="title">
29
+ <h1 class="page-title">${data.pageTitle}</h1>
30
+ ${page.languageSwitcher()}
31
+ </div>
27
32
  <ul class="posts">
28
33
  ${data.posts
29
34
  .map(
@@ -10,6 +10,8 @@
10
10
  ${page.config.googleAnalyticsID ? page.googleAnalytics(page.config.googleAnalyticsID) : ""}
11
11
  <title>${page.config.blogName}</title>
12
12
  <meta name="description" content="${page.config.blogDescription}" />
13
+ <link rel="canonical" href="${page.url}" />
14
+ ${page.alternateLanguageLinks()}
13
15
  ${page.openGraph("website")}
14
16
  <!-- Twitter Card -->
15
17
  ${page.twitterCard("summary")}
@@ -17,12 +19,15 @@
17
19
  <body>
18
20
  <div class="content">
19
21
  <header>
20
- <div class="main">${page.config.blogName}</div>
22
+ <div>
23
+ <div class="main">${page.config.blogName}</div>
24
+ ${page.languageSwitcher()}
25
+ </div>
21
26
  <nav>
22
- Home
23
- <a href="${page.config.basePath}all_posts">All posts</a>
24
- <a href="${page.config.basePath}about">About</a>
25
- <a href="${page.config.basePath}tags">Tags</a>
27
+ ${page.config.homeLabel || "Home"}
28
+ <a href="${page.config.basePath}all_posts">${page.config.allPostsLabel || "All posts"}</a>
29
+ <a href="${page.config.basePath}about">${page.config.aboutLabel || "About"}</a>
30
+ <a href="${page.config.basePath}tags">${page.config.tagsLabel || "Tags"}</a>
26
31
  </nav>
27
32
  </header>
28
33
 
@@ -28,10 +28,10 @@
28
28
  ${page.config.blogName}
29
29
  </div>
30
30
  <nav class="site-navigation">
31
- <a href="${page.config.basePath}">Home</a>
32
- <a href="${page.config.basePath}all_posts">All posts</a>
33
- About
34
- <a href="${page.config.basePath}tags">Tags</a>
31
+ <a href="${page.config.basePath}">${page.config.homeLabel || "Home"}</a>
32
+ <a href="${page.config.basePath}all_posts">${page.config.allPostsLabel || "All posts"}</a>
33
+ ${page.config.aboutLabel || "About"}
34
+ <a href="${page.config.basePath}tags">${page.config.tagsLabel || "Tags"}</a>
35
35
  </nav>
36
36
  </header>
37
37
  <main>
@@ -42,10 +42,10 @@
42
42
  ${page.config.blogName}
43
43
  </div>
44
44
  <nav class="site-navigation">
45
- <a href="${page.config.basePath}">Home</a>
46
- <a href="${page.config.basePath}all_posts">All posts</a>
47
- <a href="${page.config.basePath}about">About</a>
48
- <a href="${page.config.basePath}tags">Tags</a>
45
+ <a href="${page.config.basePath}">${page.config.homeLabel || "Home"}</a>
46
+ <a href="${page.config.basePath}all_posts">${page.config.allPostsLabel || "All posts"}</a>
47
+ <a href="${page.config.basePath}about">${page.config.aboutLabel || "About"}</a>
48
+ <a href="${page.config.basePath}tags">${page.config.tagsLabel || "Tags"}</a>
49
49
  </nav>
50
50
  </header>
51
51
  <main>
@@ -17,10 +17,10 @@
17
17
  <header>
18
18
  <div class="main">${page.config.blogName}</div>
19
19
  <nav>
20
- <a href="${page.config.basePath}">Home</a>
21
- <a href="${page.config.basePath}all_posts">All posts</a>
22
- <a href="${page.config.basePath}about">About</a>
23
- <a href="${page.config.basePath}tags">Tags</a>
20
+ <a href="${page.config.basePath}">${page.config.homeLabel || "Home"}</a>
21
+ <a href="${page.config.basePath}all_posts">${page.config.allPostsLabel || "All posts"}</a>
22
+ <a href="${page.config.basePath}about">${page.config.aboutLabel || "About"}</a>
23
+ <a href="${page.config.basePath}tags">${page.config.tagsLabel || "Tags"}</a>
24
24
  </nav>
25
25
  </header>
26
26
  <h1 class="page-title">Entries tagged - "${data.tag}"</h1>
@@ -11,6 +11,8 @@
11
11
  <!-- Google tag (gtag.js) -->
12
12
  ${page.config.googleAnalyticsID ? page.googleAnalytics(page.config.googleAnalyticsID) : ""}
13
13
  <title>${page.config.blogName}: ${data.pageTitle}</title>
14
+ <link rel="canonical" href="${page.url}" />
15
+ ${page.alternateLanguageLinks()}
14
16
  ${page.openGraph("website")}
15
17
  </head>
16
18
  <body>
@@ -18,13 +20,16 @@
18
20
  <header>
19
21
  <div class="main">${page.config.blogName}</div>
20
22
  <nav>
21
- <a href="${page.config.basePath}">Home</a>
22
- <a href="${page.config.basePath}all_posts">All posts</a>
23
- <a href="${page.config.basePath}about">About</a>
24
- Tags
23
+ <a href="${page.config.basePath}">${page.config.homeLabel || "Home"}</a>
24
+ <a href="${page.config.basePath}all_posts">${page.config.allPostsLabel || "All posts"}</a>
25
+ <a href="${page.config.basePath}about">${page.config.aboutLabel || "About"}</a>
26
+ ${page.config.tagsLabel || "Tags"}
25
27
  </nav>
26
28
  </header>
27
- <h1 class="page-title">${data.pageTitle}</h1>
29
+ <div class="title">
30
+ <h1 class="page-title">${data.pageTitle}</h1>
31
+ ${page.languageSwitcher()}
32
+ </div>
28
33
  <div class="tag-cloud">
29
34
  <ul class="tags">
30
35
  ${data.tags