@tenjuu99/blog 0.1.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.
Files changed (44) hide show
  1. package/.env.prod.sample +8 -0
  2. package/.env.sample +8 -0
  3. package/.github/workflows/deploy.yml.sample +51 -0
  4. package/.github/workflows/github-page.yml +47 -0
  5. package/.gitignore +9 -0
  6. package/README.md +145 -0
  7. package/generate.js +11 -0
  8. package/helper/add.js +3 -0
  9. package/helper/index.js +53 -0
  10. package/lib/applyTemplate.js +41 -0
  11. package/lib/cssGenerator.js +61 -0
  12. package/lib/dir.js +15 -0
  13. package/lib/distribute.js +35 -0
  14. package/lib/filter.js +167 -0
  15. package/lib/includeFilter.js +38 -0
  16. package/lib/indexer.js +82 -0
  17. package/lib/minify.js +22 -0
  18. package/lib/replaceIfFilter.js +67 -0
  19. package/lib/replaceScriptFilter.js +71 -0
  20. package/package.json +40 -0
  21. package/performance.js +22 -0
  22. package/server.js +48 -0
  23. package/src-sample/css/color.css +5 -0
  24. package/src-sample/css/layout.css +143 -0
  25. package/src-sample/css/markdown.css +49 -0
  26. package/src-sample/css/page.css +0 -0
  27. package/src-sample/css/reset.css +2 -0
  28. package/src-sample/helper/index.js +3 -0
  29. package/src-sample/image/.gitkeep +0 -0
  30. package/src-sample/pages/abc.md +5 -0
  31. package/src-sample/pages/fuga.md +3 -0
  32. package/src-sample/pages/hogehoge.md +8 -0
  33. package/src-sample/pages/html_render.html +20 -0
  34. package/src-sample/pages/index.md +35 -0
  35. package/src-sample/pages/notfound.md +8 -0
  36. package/src-sample/pages/rss.md +8 -0
  37. package/src-sample/pages/sample.md +145 -0
  38. package/src-sample/template/css.html +6 -0
  39. package/src-sample/template/default.html +55 -0
  40. package/src-sample/template/footer.html +7 -0
  41. package/src-sample/template/gtag.html +9 -0
  42. package/src-sample/template/index.html +24 -0
  43. package/src-sample/template/prevNext.html +35 -0
  44. package/src-sample/template/rss.xml +41 -0
@@ -0,0 +1,38 @@
1
+ import fs from "node:fs/promises";
2
+ import { minifyCss } from './minify.js'
3
+ import { templateDir, cssDir } from './dir.js'
4
+
5
+ const alreadyLoaded = {}
6
+
7
+ const includeFilter = async (text) => {
8
+ let replaced = text
9
+ const includeRegexp = new RegExp(/\{\s*include\('(template|css)\/([\w\./]+)'\)\s*\}/g)
10
+ const include = [...text.matchAll(includeRegexp)].map(matched => [matched[0], matched[1], matched[2]])
11
+ for (const index in include) {
12
+ const [toBeReplace, type, filename] = [...include[index]]
13
+ let content
14
+ const cacheKey = `${type}/${filename}`
15
+ if (!alreadyLoaded[cacheKey]) {
16
+ switch (type) {
17
+ case 'template':
18
+ content = await fs.readFile(`${templateDir}/${filename}`, 'utf8')
19
+ break
20
+ case 'css':
21
+ content = await fs.readFile(`${cssDir}/${filename}`, 'utf8')
22
+ break
23
+ default:
24
+ throw new Error('type does not match neither `template` nor `css`.');
25
+ }
26
+ // include を再帰的に解決する
27
+ if (content.match(includeRegexp)) {
28
+ content = await includeFilter(content)
29
+ }
30
+ alreadyLoaded[cacheKey] = content
31
+ } else {
32
+ content = alreadyLoaded[cacheKey]
33
+ }
34
+ replaced = replaced.replace(toBeReplace, content)
35
+ }
36
+ return replaced
37
+ }
38
+ export default includeFilter
package/lib/indexer.js ADDED
@@ -0,0 +1,82 @@
1
+ "use strict"
2
+ import fs from "node:fs/promises";
3
+ import { cacheDir } from './dir.js'
4
+
5
+ const parseMetaData = (markdown, filename) => {
6
+ const regexp = new RegExp(/^(<!|-)--(?<variables>[\s\S]*?)--(-|>)/)
7
+ const matched = markdown.match(regexp)
8
+ const markdownReplaced = markdown.replace(regexp, '')
9
+ const metaDataDefault = {
10
+ name: filename,
11
+ title: filename,
12
+ url: `/${filename}`,
13
+ description: '',
14
+ og_description: '',
15
+ published: '1970-01-01',
16
+ index: true,
17
+ noindex: false,
18
+ lang: 'ja',
19
+ site_name: process.env.SITE_NAME,
20
+ url_base: process.env.URL_BASE,
21
+ gtag_id: process.env.GTAG_ID,
22
+ markdown: markdownReplaced,
23
+ relative_path: process.env.RELATIVE_PATH || '',
24
+ template: 'default.html',
25
+ ext: 'html',
26
+ __output: `/${filename}.html`
27
+ }
28
+ if (!matched) {
29
+ return metaDataDefault
30
+ }
31
+ const metaData = Object.fromEntries(
32
+ matched.groups.variables.split('\n').filter(line => line.includes(':'))
33
+ .map(line => {
34
+ const index = line.indexOf(':')
35
+ const key = line.slice(0, index)
36
+ let value = line.slice(index + 1).trim()
37
+ if (value === 'true' || value === 'false') {
38
+ value = JSON.parse(value)
39
+ }
40
+ return [key, value]
41
+ })
42
+ )
43
+ const metaDataMerged = Object.assign(metaDataDefault, metaData)
44
+ if (!metaDataMerged.description) {
45
+ metaDataMerged.description = markdownReplaced.replace(/(<([^>]+)>)/gi, '').slice(0, 200).replaceAll("\n", '') + '...'
46
+ }
47
+ if (!metaDataMerged.og_description) {
48
+ metaDataMerged.og_description = metaDataMerged.og_description
49
+ }
50
+ metaDataMerged['__output'] = filename === 'index' ? '/index.html' : `${metaDataMerged.url}.${metaDataMerged.ext}`
51
+
52
+ return metaDataMerged
53
+ }
54
+
55
+ const indexFile = `${cacheDir}/index.json`
56
+
57
+ const newIndex = []
58
+ const allData = {}
59
+
60
+ const indexing = async (targetDir) => {
61
+ const targets = await fs.readdir(targetDir).then(files => {
62
+ return files.filter(fileName => fileName.match('\.(md|html)$'))
63
+ })
64
+ for (const file of targets) {
65
+ const markdownText = await fs.readFile(`${targetDir}/${file}`, 'utf8')
66
+ const [target, ext] = file.split('.')
67
+ const metaData = parseMetaData(markdownText, target)
68
+ metaData.__filetype = ext
69
+ allData[target] = metaData
70
+ let { name, title, index, url, published, modified, __output } = metaData
71
+ newIndex.push({ name, title, index, url, published, modified, __output })
72
+ }
73
+ await fs.writeFile(indexFile, JSON.stringify(newIndex))
74
+
75
+ // 旧インデックスから差分を計算して削除対象をピックアップする
76
+ const oldIndex = await fs.readFile(indexFile, 'utf8').then(text => JSON.parse(text)).catch(error => [])
77
+ const deleted = oldIndex.filter(oi => !newIndex.map(ni => ni.__output).includes(oi.__output))
78
+ allData['__deleted'] = deleted
79
+ return allData
80
+ }
81
+
82
+ export { indexing, allData }
package/lib/minify.js ADDED
@@ -0,0 +1,22 @@
1
+
2
+ /**
3
+ * @param {string} css
4
+ * @returns {string}
5
+ */
6
+ export function minifyCss (css) {
7
+ return css.split('\n')
8
+ .map(s => s.trim()
9
+ .replace(/\s+{/g, '{')
10
+ .replace(/:\s/, ':'))
11
+ .filter(s => !!s)
12
+ .join('')
13
+ .replaceAll(/\/\*[\s\S]+?\*\//g, '')
14
+ }
15
+
16
+ /**
17
+ * @param {string} html
18
+ * @returns {string}
19
+ */
20
+ export function minifyHtml (html) {
21
+ return html.replace(/\s+(<(?!\/?style).+>?)/g, '$1')
22
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @param {string} condition
3
+ * @params {object} variables
4
+ * @return {bool}
5
+ */
6
+ const ifConditionEvaluator = (condition, variables) => {
7
+ if (condition.includes('=')) {
8
+ const segmented = condition.match(/(?<left>[\S]+)\s(?<operator>!=|==)\s(?<right>[\S]+)/)
9
+ let {left, operator, right} = segmented.groups
10
+ if (variables.hasOwnProperty(left)) {
11
+ left = variables[left]
12
+ } else {
13
+ try {
14
+ left = eval(left)
15
+ } catch (e) {
16
+ left = undefined
17
+ }
18
+ }
19
+ if (variables.hasOwnProperty(right)) {
20
+ right = variables[right]
21
+ } else {
22
+ try {
23
+ right = eval(right)
24
+ } catch (e) {
25
+ right = undefined
26
+ }
27
+ }
28
+ switch (operator) {
29
+ case '==':
30
+ return left == right
31
+ case '!=':
32
+ return left != right
33
+ }
34
+ } else {
35
+ if (variables.hasOwnProperty(condition)) {
36
+ return !!variables[condition]
37
+ }
38
+ return false
39
+ }
40
+ }
41
+
42
+ const ifRegexp =
43
+ new RegExp(/(\{|<)if\s(?<condition>[\s\S]+?)(}|>)(?<content>[\s\S]*?)((\{|<)else(\}|>)(?<elsecontent>[\s\S]*?))?(\{<)\/if(>|})/g)
44
+
45
+ /**
46
+ * @param {string} text
47
+ * @param {object} variables
48
+ * @returns {string}
49
+ */
50
+ const replaceIfFilter = (text, variables) => {
51
+ const matched = [...text.matchAll(ifRegexp)]
52
+ for (const item of matched) {
53
+ const target = item[0]
54
+ const content = item.groups.content
55
+ const elseContent = item.groups.elsecontent
56
+ if (ifConditionEvaluator(item.groups.condition, variables)) {
57
+ text = text.replace(target, content)
58
+ } else if (elseContent) {
59
+ text = text.replace(target, elseContent)
60
+ } else {
61
+ text = text.replace(target, '')
62
+ }
63
+ }
64
+ return text
65
+ }
66
+
67
+ export default replaceIfFilter
@@ -0,0 +1,71 @@
1
+ import * as helper from '../helper/index.js'
2
+ import vm from 'vm'
3
+
4
+ /**
5
+ * 配列を再帰的に順不同リストに変換する
6
+ * @param {Array|string} arrayOrText
7
+ * @returns {mixed}
8
+ */
9
+ const arrayToList = (arrayOrText) => {
10
+ if (typeof arrayOrText === 'string') {
11
+ return `<li>${arrayOrText}</li>`
12
+ }
13
+ if (Array.isArray(arrayOrText)) {
14
+ let resultListText = '<ul>'
15
+ for (const item of arrayOrText) {
16
+ if (Array.isArray(item)) {
17
+ resultListText += `<li>${arrayToList(item)}</li>`
18
+ } else {
19
+ resultListText += `<li>${item}</li>`
20
+ }
21
+ }
22
+ resultListText += '</ul>'
23
+ arrayOrText = resultListText
24
+ }
25
+ return arrayOrText
26
+ }
27
+
28
+ const replaceScriptFilter = async (text, variables) => {
29
+ let replaced = text
30
+ const scriptRegexp = new RegExp(/({script}|\<script\s.*type="ssg".*>)(?<script>[\s\S]*?)(\{\/script}|\<\/script>)/g)
31
+ const scripts = [...text.matchAll(scriptRegexp)].map((matched) => {
32
+ return {
33
+ replace: matched[0],
34
+ script: matched.groups.script.trim("\n"),
35
+ }
36
+ })
37
+ const context = vm.createContext({
38
+ helper,
39
+ variables,
40
+ })
41
+ for (const script of scripts) {
42
+ const s = new vm.Script(script.script)
43
+ console.log(script.script)
44
+ console.log(s)
45
+ console.log('------------------------------------------------')
46
+ const func = vm.runInContext(script, context)
47
+ console.log(func)
48
+ let result = await func(variables, helper)
49
+ if (result instanceof Promise) {
50
+ result = await result
51
+ }
52
+ if (Array.isArray(result)) {
53
+ result = arrayToList(result)
54
+ } else if (typeof result === 'object') {
55
+ const resultText = []
56
+ for (const key in result) {
57
+ resultText.push(`<h3>${key}</h3>`)
58
+ if (Array.isArray(result[key])) {
59
+ resultText.push(arrayToList(result[key]))
60
+ } else {
61
+ resultText.push(`<p>${result[key]}</p>`)
62
+ }
63
+ }
64
+ result = resultText.join('\n')
65
+ }
66
+ replaced = replaced.replace(script.replace, result)
67
+ }
68
+ return replaced
69
+ }
70
+
71
+ export default replaceScriptFilter
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@tenjuu99/blog",
3
+ "version": "0.1.0",
4
+ "description": "blog template",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "build": "npm run generate && npm run serve",
8
+ "serve": "eval $(cat .env | tr \"\n\" \" \") node server.js",
9
+ "generate": "eval $(cat .env | tr \"\n\" \" \") node generate.js",
10
+ "test": "echo \"Error: no test specified\" && exit 1",
11
+ "watch": "npm-watch build"
12
+ },
13
+ "watch": {
14
+ "build": {
15
+ "patterns": [
16
+ "src/",
17
+ "src-sample/",
18
+ "../src/",
19
+ "helper/",
20
+ "lib/",
21
+ "generate.js"
22
+ ],
23
+ "extensions": "html,md,css,js",
24
+ "quite": true
25
+ }
26
+ },
27
+ "author": "AmashigeSeiji",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/amashigeseiji/blog_template.git"
31
+ },
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "marked": "^13.x"
35
+ },
36
+ "devDependencies": {
37
+ "npm-watch": "^0.x"
38
+ },
39
+ "type": "module"
40
+ }
package/performance.js ADDED
@@ -0,0 +1,22 @@
1
+ "use strict"
2
+ import distribute from './lib/distribute.js'
3
+ import { indexing } from './lib/indexer.js'
4
+ import { srcDir, distDir } from './lib/dir.js'
5
+
6
+ process.env.FORCE_BUILD = true
7
+ const execute = async () => {
8
+ const start = performance.now()
9
+ const data = await indexing(srcDir)
10
+ await distribute(data, srcDir, distDir)
11
+ const end = performance.now()
12
+ return end - start
13
+ }
14
+ const times = 100
15
+ let executed = 0
16
+ //for (let i = 0; i < times; i++) {
17
+ //console.log(await executed())
18
+ const buildTime = await execute()
19
+ //executed += (buildTime)
20
+ console.log(buildTime)
21
+ //}
22
+ //console.log('build average: 100 times: ' + (executed/times) + "ms")
package/server.js ADDED
@@ -0,0 +1,48 @@
1
+ import http from 'http'
2
+ import url from 'url'
3
+ import fs from 'node:fs/promises'
4
+ import { distDir } from './lib/dir.js'
5
+
6
+ const contentType = (ext) => {
7
+ switch (ext) {
8
+ case 'html':
9
+ return 'text/html'
10
+ case 'css':
11
+ return 'text/css'
12
+ case 'js':
13
+ case 'javascript':
14
+ return 'text/javascript'
15
+ case 'json':
16
+ return 'application/json'
17
+ case 'jpg':
18
+ case 'jpeg':
19
+ return 'image/jpeg'
20
+ case 'svg':
21
+ return 'image/svg+xml'
22
+ case 'xml':
23
+ return 'application/xml'
24
+ case 'rdf':
25
+ return 'application/rdf+xml.rdf'
26
+ default:
27
+ return 'application/octet-stream'
28
+ }
29
+ }
30
+
31
+ http.createServer((request, response) => {
32
+ console.log(request.method, request.url)
33
+ const url = new URL(`http://${request.headers.host}${request.url}`)
34
+ let path = url.pathname === '/' ? '/index.html' : decodeURIComponent(url.pathname)
35
+ if (!path.includes('.')) {
36
+ path += '.html'
37
+ }
38
+ fs.readFile(`${distDir}${path}`, 'binary').catch(async (error) => {
39
+ const errorContent = await fs.readFile(`${distDir}/404.html`)
40
+ console.log(error)
41
+ response.writeHead(404)
42
+ response.end(errorContent)
43
+ }).then(file => {
44
+ const ext = path.split('.')[1]
45
+ response.writeHead(200, { 'Content-Type': `${contentType(ext)}; charset=utf-8` })
46
+ response.end(file, 'binary')
47
+ })
48
+ }).listen(process.env.PORT || 8000)
@@ -0,0 +1,5 @@
1
+ :root {
2
+ --header-bg: #333;
3
+ --header-logo-color: #fff;
4
+ --footer-bg: #eee;
5
+ }
@@ -0,0 +1,143 @@
1
+ :root {
2
+ --headline-1: 28px;
3
+ --headline-2: 20px;
4
+ --headline-3: 16px;
5
+ --text-size: 16px;
6
+ --text-size-small: 12px;
7
+ }
8
+
9
+ body {
10
+ display: flex;
11
+ flex-direction: column;
12
+ min-height: 100dvh !important;
13
+ text-align: justify;
14
+ font-feature-settings: "palt";
15
+ font-size: var(--text-size);
16
+ word-wrap: break-word;
17
+ word-break: break-all;
18
+ }
19
+
20
+ .container {
21
+ margin: 0 auto;
22
+ padding: 5px 30px;
23
+ max-width: 80%;
24
+ min-width: 600px;
25
+ width: 70vw;
26
+ }
27
+
28
+ @media screen and (max-width: 600px) {
29
+ .container {
30
+ max-width: 100%;
31
+ width: 100%;
32
+ min-width: 100%;
33
+ padding: 5px 15px;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * header, main, footer
39
+ */
40
+ header {
41
+ padding: 10px 0;
42
+ border-bottom: 0.5px solid #ccc;
43
+ background: var(--header-bg);
44
+ }
45
+
46
+ header h1, header p {
47
+ font: normal bold var(--headline-1)/1 monospace;
48
+ margin: 0;
49
+ }
50
+
51
+ header a, header a:visited {
52
+ text-decoration: none;
53
+ color: var(--header-logo-color);
54
+ }
55
+
56
+ main {
57
+ flex-grow: 1;
58
+ display: flex;
59
+ flex-direction: column;
60
+ justify-content: space-between;
61
+ }
62
+
63
+ footer {
64
+ border-top: 1px solid #ccc;
65
+ padding: 15px 0 0;
66
+ font-size: var(--text-size-small);
67
+ background: var(--footer-bg);
68
+ }
69
+ footer ul {
70
+ display: flex;
71
+ justify-content: flex-start;
72
+ }
73
+ footer ul li {
74
+ list-style: none;
75
+ margin-left: 10px;
76
+ }
77
+
78
+ /**
79
+ * headline
80
+ */
81
+ h1 {
82
+ font-size: var(--headline-1);
83
+ }
84
+ h2 {
85
+ font-size: var(--headline-2);
86
+ }
87
+ h3 {
88
+ font-size: var(--headline-3);
89
+ }
90
+ main h1, main h2, main h3, main h4 {
91
+ margin: 15px 0 5px;
92
+ }
93
+ main h1, main h2 {
94
+ border-bottom: 0.5px solid #ccc;
95
+ }
96
+ /**
97
+ * aside
98
+ */
99
+ aside, aside * {
100
+ font-size: var(--text-size-small);
101
+ color: #503838;
102
+ line-height: 1.125rem;
103
+ }
104
+
105
+ /**
106
+ * p
107
+ */
108
+ p {
109
+ margin: 15px 0;
110
+ }
111
+
112
+ .prevNext {
113
+ list-style: none;
114
+ display: flex;
115
+ padding: 0;
116
+ }
117
+ .prevNext li {
118
+ display: flex;
119
+ align-items: center;
120
+ flex-basis: 50%;
121
+ }
122
+ .prevNext a {
123
+ padding: 10px;
124
+ display: contents;
125
+ }
126
+ .prevNext__prev {
127
+ padding-right: 5px;
128
+ justify-content: flex-start;
129
+ }
130
+ .prevNext__next {
131
+ padding-left: 5px;
132
+ justify-content: flex-end;
133
+ }
134
+ .prevNext__next a::after {
135
+ content: '>';
136
+ color: #ccc;
137
+ padding-left: 10px;
138
+ }
139
+ .prevNext__prev a::before {
140
+ content: '<';
141
+ color: #ccc;
142
+ padding-right: 10px;
143
+ }
@@ -0,0 +1,49 @@
1
+ blockquote {
2
+ padding: 3px 20px 3px 20px;
3
+ margin: 0 10px;
4
+ font-style: italic;
5
+ border-left: 2px solid #999;
6
+ background: #e3e8f57a;
7
+ }
8
+ blockquote p {
9
+ color: #5b6e5c;
10
+ line-height: 1.75;
11
+ }
12
+ table, td, th {
13
+ border: 0.5px solid #bbb;
14
+ border-spacing: 0;
15
+ border-collapse: collapse;
16
+ }
17
+ td, th {
18
+ padding: 3px 5px;
19
+ font-size: .7rem;
20
+ background: #eee;
21
+ }
22
+ th {
23
+ background: #767676;
24
+ color: #efefef;
25
+ }
26
+ td ul {
27
+ padding: 0 0 0 10px;
28
+ }
29
+ em {
30
+ font-style: italic;
31
+ font-weight: 600;
32
+ }
33
+ pre {
34
+ white-space: pre-wrap;
35
+ background: #333;
36
+ color: #fff;
37
+ padding: 10px;
38
+ }
39
+ pre > code {
40
+ background: #333;
41
+ color: #fff;
42
+ padding: 0;
43
+ }
44
+ code {
45
+ background: #eee;
46
+ color: #444;
47
+ padding: 0 5px;
48
+ border-radius: 5px;
49
+ }
File without changes
@@ -0,0 +1,2 @@
1
+ /* https://github.com/Andy-set-studio/modern-css-reset */
2
+ *,*::before,*::after{box-sizing:border-box}body,h1,h2,h3,h4,p,figure,blockquote,dl,dd{margin:0}ul[role="list"],ol[role="list"]{list-style:none}html:focus-within{scroll-behavior:smooth}body{min-height:100vh;text-rendering:optimizeSpeed;line-height:1.5}a:not([class]){text-decoration-skip-ink:auto}img,picture{max-width:100%;display:block}input,button,textarea,select{font:inherit}@media(prefers-reduced-motion:reduce){html:focus-within{scroll-behavior:auto}*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;scroll-behavior:auto !important}}
@@ -0,0 +1,3 @@
1
+ export function additionalHelper() {
2
+ return 'これは追加ヘルパーによって出力されているメッセージです。'
3
+ }
File without changes
@@ -0,0 +1,5 @@
1
+ ---
2
+ title: abc
3
+ url: /abc
4
+ published: 2024-08-31
5
+ ---
@@ -0,0 +1,3 @@
1
+ ## fuga
2
+
3
+ これは変数がないです
@@ -0,0 +1,8 @@
1
+ ---
2
+ title: hoge
3
+ url: /hoge
4
+ published: 2024/03/19 00:00
5
+ ifTrueVariable: true
6
+ ---
7
+
8
+ hoge
@@ -0,0 +1,20 @@
1
+ ---
2
+ title: html render
3
+ url: /html_render
4
+ ---
5
+
6
+ <section>
7
+ section
8
+ <h2>h2</h2>
9
+ <p>p</p>
10
+ <ul>
11
+ <li>list</li>
12
+ <li>list</li>
13
+ </ul>
14
+ </section>
15
+
16
+ aaa
17
+
18
+ ### マークダウンがパースされないことを確認します
19
+
20
+ [abc](/a)
@@ -0,0 +1,35 @@
1
+ ---
2
+ title: INDEX
3
+ template: index.html
4
+ index: false
5
+ url: /
6
+ published: 2023-03-03 20:21
7
+ modified: 2023-03-03 20:21
8
+ ---
9
+ ## INDEX
10
+
11
+ {script}
12
+ const data = helper.readIndex()
13
+
14
+ const pages = {}
15
+ for (const page of data) {
16
+ if (page.index) {
17
+ const url = variables.relative_path ? variables.relative_path + page.url : page.url
18
+ if (page.published === '1970-01-01') {
19
+ if (!pages['日付なし']) {
20
+ pages['日付なし'] = []
21
+ }
22
+ pages['日付なし'].push(`<a href="${url}">${page.title}</a>`)
23
+ continue
24
+ }
25
+ const published = new Date(page.published)
26
+ const year = `${published.getFullYear()}年`
27
+ const date = `${published.getMonth() +1}月${published.getDate()}日`
28
+ if (!pages[year]) {
29
+ pages[year] = []
30
+ }
31
+ pages[year].push(`<a href="${url}">${page.title}</a> (${date})`)
32
+ }
33
+ }
34
+ return pages
35
+ {/script}
@@ -0,0 +1,8 @@
1
+ ---
2
+ title: 404 not found
3
+ url: /404
4
+ index: false
5
+ noindex: true
6
+ ---
7
+
8
+ 記事が見つかりませんでした。