@tenjuu99/blog 0.2.54 → 0.2.55
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/lib/generate.js +43 -1
- package/lib/helper.js +2 -2
- package/package.json +1 -1
- package/packages/category/helper/category.js +123 -0
- package/packages/category/helper/categoryIndexer.js +55 -0
- package/packages/category/template/category.html +112 -0
- package/src-sample/pages/art/painting/test-category-4.md +7 -0
- package/src-sample/pages/tech/backend/test-category-3.md +7 -0
- package/src-sample/pages/tech/frontend/test-category-1.md +7 -0
- package/src-sample/pages/tech/frontend/test-category-2.md +7 -0
package/lib/generate.js
CHANGED
|
@@ -1,14 +1,48 @@
|
|
|
1
1
|
"use strict"
|
|
2
2
|
import distribute from './distribute.js'
|
|
3
3
|
import { indexing, allData } from './indexer.js'
|
|
4
|
-
import { srcDir, distDir, cache } from './dir.js'
|
|
4
|
+
import { srcDir, distDir, cache, helperDir } from './dir.js'
|
|
5
5
|
import { warmUpTemplate } from './applyTemplate.js'
|
|
6
6
|
import { styleText } from 'node:util'
|
|
7
|
+
import config from './config.js'
|
|
8
|
+
import { existsSync } from 'node:fs'
|
|
7
9
|
|
|
8
10
|
const beforeGenerate = async () => {
|
|
9
11
|
cache()
|
|
10
12
|
await warmUpTemplate()
|
|
11
13
|
}
|
|
14
|
+
|
|
15
|
+
export const runHooks = async (hookName, customHelperDir = null, customAllData = null, customConfig = null) => {
|
|
16
|
+
const targetConfig = customConfig || config
|
|
17
|
+
const targetAllData = customAllData || allData
|
|
18
|
+
const targetHelperDir = customHelperDir || helperDir
|
|
19
|
+
|
|
20
|
+
if (!targetConfig.hooks || !targetConfig.hooks[hookName]) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const hookFiles = Array.isArray(targetConfig.hooks[hookName])
|
|
25
|
+
? targetConfig.hooks[hookName]
|
|
26
|
+
: [targetConfig.hooks[hookName]]
|
|
27
|
+
|
|
28
|
+
for (const hookFile of hookFiles) {
|
|
29
|
+
const hookPath = `${targetHelperDir}/${hookFile}`
|
|
30
|
+
|
|
31
|
+
if (existsSync(hookPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const hookModule = await import(hookPath)
|
|
34
|
+
if (typeof hookModule[hookName] === 'function') {
|
|
35
|
+
await hookModule[hookName](targetAllData, targetConfig)
|
|
36
|
+
console.log(styleText('blue', `[hook] ${hookName} executed: ${hookFile}`))
|
|
37
|
+
}
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.error(styleText('red', `[hook error] ${hookName}:`), e)
|
|
40
|
+
throw e
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
12
46
|
const generate = async () => {
|
|
13
47
|
let start = performance.now()
|
|
14
48
|
await beforeGenerate()
|
|
@@ -16,6 +50,14 @@ const generate = async () => {
|
|
|
16
50
|
let end = performance.now()
|
|
17
51
|
console.log(styleText('blue', '[indexing: ' + (end - start) + "ms]"))
|
|
18
52
|
|
|
53
|
+
// フックポイント: afterIndexing
|
|
54
|
+
start = performance.now()
|
|
55
|
+
await runHooks('afterIndexing')
|
|
56
|
+
end = performance.now()
|
|
57
|
+
if (config.hooks?.afterIndexing) {
|
|
58
|
+
console.log(styleText('blue', '[afterIndexing hook: ' + (end - start) + "ms]"))
|
|
59
|
+
}
|
|
60
|
+
|
|
19
61
|
start = performance.now()
|
|
20
62
|
await distribute(allData, srcDir, distDir)
|
|
21
63
|
end = performance.now()
|
package/lib/helper.js
CHANGED
|
@@ -6,12 +6,12 @@ let helper = {}
|
|
|
6
6
|
|
|
7
7
|
if (config.helper) {
|
|
8
8
|
const files = config.helper.split(',')
|
|
9
|
-
|
|
9
|
+
for (const file of files) {
|
|
10
10
|
if (existsSync(`${helperDir}/${file}`)) {
|
|
11
11
|
const helperAdditional = await import(`${helperDir}/${file}`)
|
|
12
12
|
helper = Object.assign(helper, helperAdditional)
|
|
13
13
|
}
|
|
14
|
-
}
|
|
14
|
+
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export default helper
|
package/package.json
CHANGED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { allData, config } from '@tenjuu99/blog'
|
|
2
|
+
|
|
3
|
+
let categoryTreeCache = null
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* カテゴリーツリーを構築する
|
|
7
|
+
* @param {Object} allData - 全ページデータ
|
|
8
|
+
* @param {Object} config - 設定オブジェクト
|
|
9
|
+
* @returns {Object} カテゴリーツリー
|
|
10
|
+
*/
|
|
11
|
+
export function buildCategoryTree(data = allData, conf = config) {
|
|
12
|
+
const tree = {}
|
|
13
|
+
const urlCase = conf.category?.url_case || 'lower'
|
|
14
|
+
const maxDepth = conf.category?.max_depth || 3
|
|
15
|
+
|
|
16
|
+
for (const [name, page] of Object.entries(data)) {
|
|
17
|
+
if (!page.category || !Array.isArray(page.category)) {
|
|
18
|
+
continue
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const categoryPath = page.category.slice(0, maxDepth)
|
|
22
|
+
let currentPath = ''
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < categoryPath.length; i++) {
|
|
25
|
+
const category = categoryPath[i]
|
|
26
|
+
const categoryUrl = urlCase === 'lower' ? category.toLowerCase() : category
|
|
27
|
+
currentPath += `/${categoryUrl}`
|
|
28
|
+
|
|
29
|
+
if (!tree[currentPath]) {
|
|
30
|
+
tree[currentPath] = {
|
|
31
|
+
title: category,
|
|
32
|
+
path: categoryPath.slice(0, i + 1),
|
|
33
|
+
pages: [],
|
|
34
|
+
children: {}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
tree[currentPath].pages.push(name)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// children を計算
|
|
43
|
+
for (const [url, node] of Object.entries(tree)) {
|
|
44
|
+
const depth = node.path.length
|
|
45
|
+
for (const [childUrl, childNode] of Object.entries(tree)) {
|
|
46
|
+
if (childNode.path.length === depth + 1 && childUrl.startsWith(url + '/')) {
|
|
47
|
+
node.children[childUrl] = childNode
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return tree
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* カテゴリーツリーを取得する(キャッシュあり)
|
|
57
|
+
* @returns {Object} カテゴリーツリー
|
|
58
|
+
*/
|
|
59
|
+
export function getCategoryTree() {
|
|
60
|
+
if (!categoryTreeCache) {
|
|
61
|
+
categoryTreeCache = buildCategoryTree()
|
|
62
|
+
}
|
|
63
|
+
return categoryTreeCache
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 特定のカテゴリーに所属するページを取得する(完全一致)
|
|
68
|
+
* @param {Array} categoryPath - カテゴリーパス(例: ["Art", "Painting"])
|
|
69
|
+
* @returns {Array} ページデータの配列
|
|
70
|
+
*/
|
|
71
|
+
export function getCategoryPages(categoryPath) {
|
|
72
|
+
const pages = []
|
|
73
|
+
|
|
74
|
+
for (const [name, page] of Object.entries(allData)) {
|
|
75
|
+
if (!page.category || !Array.isArray(page.category)) {
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (JSON.stringify(page.category) === JSON.stringify(categoryPath)) {
|
|
80
|
+
pages.push(page)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return pages
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 特定のカテゴリーに所属するページを再帰的に取得する(サブカテゴリー含む)
|
|
89
|
+
* @param {Array} categoryPath - カテゴリーパス(例: ["Art"])
|
|
90
|
+
* @returns {Array} ページデータの配列
|
|
91
|
+
*/
|
|
92
|
+
export function getCategoryPagesRecursive(categoryPath) {
|
|
93
|
+
const pages = []
|
|
94
|
+
|
|
95
|
+
for (const [name, page] of Object.entries(allData)) {
|
|
96
|
+
if (!page.category || !Array.isArray(page.category)) {
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// categoryPath が page.category の先頭部分と一致するか確認
|
|
101
|
+
if (page.category.length >= categoryPath.length) {
|
|
102
|
+
let match = true
|
|
103
|
+
for (let i = 0; i < categoryPath.length; i++) {
|
|
104
|
+
if (page.category[i] !== categoryPath[i]) {
|
|
105
|
+
match = false
|
|
106
|
+
break
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (match) {
|
|
110
|
+
pages.push(page)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return pages
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* カテゴリーキャッシュをクリアする(テスト用)
|
|
120
|
+
*/
|
|
121
|
+
export function clearCategoryCache() {
|
|
122
|
+
categoryTreeCache = null
|
|
123
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { buildCategoryTree } from './category.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* afterIndexing フック関数
|
|
5
|
+
* カテゴリーツリーを構築し、仮想カテゴリーインデックスページを生成する
|
|
6
|
+
* @param {Object} allData - 全ページデータ
|
|
7
|
+
* @param {Object} config - 設定オブジェクト
|
|
8
|
+
*/
|
|
9
|
+
export async function afterIndexing(allData, config) {
|
|
10
|
+
// auto_generate が false の場合は何もしない
|
|
11
|
+
if (config.category?.auto_generate === false) {
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// カテゴリーツリーを構築
|
|
16
|
+
const tree = buildCategoryTree(allData, config)
|
|
17
|
+
|
|
18
|
+
// 各カテゴリーに対して仮想ページを生成
|
|
19
|
+
for (const [url, categoryData] of Object.entries(tree)) {
|
|
20
|
+
const pageName = url.replace(/^\//, '') + '/index'
|
|
21
|
+
|
|
22
|
+
// 既存ページ(手動作成)が存在する場合はスキップ
|
|
23
|
+
if (allData[pageName]) {
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 仮想カテゴリーページを生成
|
|
28
|
+
allData[pageName] = {
|
|
29
|
+
name: pageName,
|
|
30
|
+
url: url,
|
|
31
|
+
__output: `${url}/index.html`,
|
|
32
|
+
title: categoryData.title,
|
|
33
|
+
template: config.category?.template || 'category.html',
|
|
34
|
+
markdown: '',
|
|
35
|
+
category_path: categoryData.path,
|
|
36
|
+
category_pages: categoryData.pages,
|
|
37
|
+
category_children: Object.keys(categoryData.children),
|
|
38
|
+
__is_auto_category: true,
|
|
39
|
+
distribute: true,
|
|
40
|
+
index: false,
|
|
41
|
+
noindex: false,
|
|
42
|
+
lang: 'ja',
|
|
43
|
+
published: '1970-01-01',
|
|
44
|
+
ext: 'html',
|
|
45
|
+
site_name: config.site_name || 'default',
|
|
46
|
+
url_base: config.url_base || 'http://localhost:8000',
|
|
47
|
+
relative_path: config.relative_path || '',
|
|
48
|
+
description: `${categoryData.title} カテゴリーのページ一覧`,
|
|
49
|
+
og_description: `${categoryData.title} カテゴリーのページ一覧`,
|
|
50
|
+
__filetype: 'md',
|
|
51
|
+
markdown_not_parsed: '',
|
|
52
|
+
full_url: `${config.url_base || 'http://localhost:8000'}${url}`
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="{{LANG}}">
|
|
3
|
+
<head prefix="og: http://ogp.me/ns#">
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{{TITLE}} | {{SITE_NAME}}</title>
|
|
7
|
+
<meta property="og:url" content="{{FULL_URL}}">
|
|
8
|
+
<meta property="og:title" content="{{TITLE}}">
|
|
9
|
+
<meta property="og:description" content="{{OG_DESCRIPTION}}">
|
|
10
|
+
<meta property="og:type" content="article">
|
|
11
|
+
<meta property="og:site_name" content="{{SITE_NAME}}">
|
|
12
|
+
{script}
|
|
13
|
+
const url = variables.url_base + encodeURI(variables.url)
|
|
14
|
+
return `<link rel="canonical" href="${url}">`
|
|
15
|
+
{/script}
|
|
16
|
+
<meta name="description" content="{{DESCRIPTION}}">
|
|
17
|
+
|
|
18
|
+
{include('template/css.html')}
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<header>
|
|
22
|
+
<p class="container"><a href="{{RELATIVE_PATH}}/">{{SITE_NAME}}</a></p>
|
|
23
|
+
</header>
|
|
24
|
+
<main>
|
|
25
|
+
<div class="container">
|
|
26
|
+
{{ breadcrumbList(name) }}
|
|
27
|
+
</div>
|
|
28
|
+
<article class="container">
|
|
29
|
+
<div class="category-header">
|
|
30
|
+
<h1>{{TITLE}}</h1>
|
|
31
|
+
<p class="category-description">{{DESCRIPTION}}</p>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- サブカテゴリーリスト -->
|
|
35
|
+
{if category_children}
|
|
36
|
+
<script type="ssg">
|
|
37
|
+
const children = variables.category_children || []
|
|
38
|
+
if (children.length === 0) {
|
|
39
|
+
return ''
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const relativePath = variables.relative_path || ''
|
|
43
|
+
|
|
44
|
+
let html = '<div class="category-section">'
|
|
45
|
+
html += '<h2>サブカテゴリー</h2>'
|
|
46
|
+
html += '<div class="category-children">'
|
|
47
|
+
|
|
48
|
+
for (const childUrl of children) {
|
|
49
|
+
const childPageName = childUrl.replace(/^\//, '') + '/index'
|
|
50
|
+
const childData = helper.getPageData(childPageName)
|
|
51
|
+
|
|
52
|
+
if (childData) {
|
|
53
|
+
html += '<div class="category-child-card">'
|
|
54
|
+
html += `<a href="${relativePath}${childUrl}/">${childData.title}</a>`
|
|
55
|
+
html += '</div>'
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
html += '</div></div>'
|
|
60
|
+
return html
|
|
61
|
+
</script>
|
|
62
|
+
{/if}
|
|
63
|
+
|
|
64
|
+
<!-- ページリスト -->
|
|
65
|
+
<div class="category-section">
|
|
66
|
+
<h2>コンテンツ</h2>
|
|
67
|
+
<script type="ssg">
|
|
68
|
+
const pageNames = variables.category_pages || []
|
|
69
|
+
const pages = pageNames
|
|
70
|
+
.map(name => helper.getPageData(name))
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.filter(page => !page.__is_auto_category)
|
|
73
|
+
.sort((a, b) => new Date(b.published) - new Date(a.published))
|
|
74
|
+
|
|
75
|
+
if (pages.length === 0) {
|
|
76
|
+
return '<p>このカテゴリーにはまだコンテンツがありません。</p>'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const relativePath = variables.relative_path || ''
|
|
80
|
+
|
|
81
|
+
let html = '<ul class="category-page-list">'
|
|
82
|
+
|
|
83
|
+
for (const page of pages) {
|
|
84
|
+
html += '<li class="category-page-item">'
|
|
85
|
+
html += '<div class="category-page-title">'
|
|
86
|
+
html += `<a href="${relativePath}${page.url}">${page.title}</a>`
|
|
87
|
+
html += '</div>'
|
|
88
|
+
|
|
89
|
+
if (page.published && page.published !== '1970-01-01') {
|
|
90
|
+
html += '<div class="category-page-meta">'
|
|
91
|
+
html += `投稿: ${helper.dateFormat(page.published)}`
|
|
92
|
+
html += '</div>'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (page.description) {
|
|
96
|
+
html += '<div class="category-page-description">'
|
|
97
|
+
html += page.description
|
|
98
|
+
html += '</div>'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
html += '</li>'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
html += '</ul>'
|
|
105
|
+
return html
|
|
106
|
+
</script>
|
|
107
|
+
</div>
|
|
108
|
+
</article>
|
|
109
|
+
</main>
|
|
110
|
+
{include('template/footer.html')}
|
|
111
|
+
</body>
|
|
112
|
+
</html>
|