@tenjuu99/blog 0.2.54 → 0.2.56

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 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
- files.forEach(async file => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenjuu99/blog",
3
- "version": "0.2.54",
3
+ "version": "0.2.56",
4
4
  "description": "blog template",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,129 @@
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 urlSeparator = conf.category?.url_separator || '-'
15
+ const rawUrlPrefix = conf.category?.url_prefix || ''
16
+ const urlPrefix = rawUrlPrefix
17
+ ? '/' + rawUrlPrefix.replace(/^\/+|\/+$/g, '')
18
+ : ''
19
+ const maxDepth = conf.category?.max_depth || 3
20
+
21
+ for (const [name, page] of Object.entries(data)) {
22
+ if (!page.category || !Array.isArray(page.category)) {
23
+ continue
24
+ }
25
+
26
+ const categoryPath = page.category.slice(0, maxDepth)
27
+ let currentPath = urlPrefix
28
+
29
+ for (let i = 0; i < categoryPath.length; i++) {
30
+ const category = categoryPath[i]
31
+ let categoryUrl = urlCase === 'lower' ? category.toLowerCase() : category
32
+ categoryUrl = categoryUrl.replace(/\s+/g, urlSeparator)
33
+ currentPath += `/${categoryUrl}`
34
+
35
+ if (!tree[currentPath]) {
36
+ tree[currentPath] = {
37
+ title: category,
38
+ path: categoryPath.slice(0, i + 1),
39
+ pages: [],
40
+ children: {}
41
+ }
42
+ }
43
+
44
+ tree[currentPath].pages.push(name)
45
+ }
46
+ }
47
+
48
+ // children を計算
49
+ for (const [url, node] of Object.entries(tree)) {
50
+ const depth = node.path.length
51
+ for (const [childUrl, childNode] of Object.entries(tree)) {
52
+ if (childNode.path.length === depth + 1 && childUrl.startsWith(url + '/')) {
53
+ node.children[childUrl] = childNode
54
+ }
55
+ }
56
+ }
57
+
58
+ return tree
59
+ }
60
+
61
+ /**
62
+ * カテゴリーツリーを取得する(キャッシュあり)
63
+ * @returns {Object} カテゴリーツリー
64
+ */
65
+ export function getCategoryTree() {
66
+ if (!categoryTreeCache) {
67
+ categoryTreeCache = buildCategoryTree()
68
+ }
69
+ return categoryTreeCache
70
+ }
71
+
72
+ /**
73
+ * 特定のカテゴリーに所属するページを取得する(完全一致)
74
+ * @param {Array} categoryPath - カテゴリーパス(例: ["Art", "Painting"])
75
+ * @returns {Array} ページデータの配列
76
+ */
77
+ export function getCategoryPages(categoryPath) {
78
+ const pages = []
79
+
80
+ for (const [name, page] of Object.entries(allData)) {
81
+ if (!page.category || !Array.isArray(page.category)) {
82
+ continue
83
+ }
84
+
85
+ if (JSON.stringify(page.category) === JSON.stringify(categoryPath)) {
86
+ pages.push(page)
87
+ }
88
+ }
89
+
90
+ return pages
91
+ }
92
+
93
+ /**
94
+ * 特定のカテゴリーに所属するページを再帰的に取得する(サブカテゴリー含む)
95
+ * @param {Array} categoryPath - カテゴリーパス(例: ["Art"])
96
+ * @returns {Array} ページデータの配列
97
+ */
98
+ export function getCategoryPagesRecursive(categoryPath) {
99
+ const pages = []
100
+
101
+ for (const [name, page] of Object.entries(allData)) {
102
+ if (!page.category || !Array.isArray(page.category)) {
103
+ continue
104
+ }
105
+
106
+ // categoryPath が page.category の先頭部分と一致するか確認
107
+ if (page.category.length >= categoryPath.length) {
108
+ let match = true
109
+ for (let i = 0; i < categoryPath.length; i++) {
110
+ if (page.category[i] !== categoryPath[i]) {
111
+ match = false
112
+ break
113
+ }
114
+ }
115
+ if (match) {
116
+ pages.push(page)
117
+ }
118
+ }
119
+ }
120
+
121
+ return pages
122
+ }
123
+
124
+ /**
125
+ * カテゴリーキャッシュをクリアする(テスト用)
126
+ */
127
+ export function clearCategoryCache() {
128
+ categoryTreeCache = null
129
+ }
@@ -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>
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: Art Article
3
+ category: ["Art", "Painting"]
4
+ published: 2024-01-18
5
+ ---
6
+
7
+ This is a test article for the Art > Painting category.
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: Tech Backend Article
3
+ category: ["Tech", "Backend"]
4
+ published: 2024-01-17
5
+ ---
6
+
7
+ This is a test article for the Tech > Backend category.
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: Tech Frontend Article 1
3
+ category: ["Tech", "Frontend"]
4
+ published: 2024-01-15
5
+ ---
6
+
7
+ This is a test article for the Tech > Frontend category.
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: Tech Frontend Article 2
3
+ category: ["Tech", "Frontend"]
4
+ published: 2024-01-16
5
+ ---
6
+
7
+ This is another test article for the Tech > Frontend category.