@jet-w/astro-blog 0.2.3 → 0.2.4
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/package.json +1 -1
- package/src/plugins/remark-containers.mjs +235 -79
- package/templates/default/content/posts/blog_docs_en/01.get-started/03-create-post.md +124 -8
- package/templates/default/content/posts/blog_docs_zh/01.get-started/03-create-post.md +124 -9
- package/templates/default/package-lock.json +2 -2
package/package.json
CHANGED
|
@@ -2,105 +2,223 @@ import { visit } from 'unist-util-visit';
|
|
|
2
2
|
|
|
3
3
|
export function remarkContainers() {
|
|
4
4
|
return (tree, file) => {
|
|
5
|
-
// Pre-process: Handle
|
|
5
|
+
// Pre-process: Handle containers that span across multiple sibling nodes
|
|
6
6
|
// This handles cases like:
|
|
7
|
-
// ::: tip
|
|
8
|
-
// Content
|
|
7
|
+
// ::: tip Title
|
|
8
|
+
// Content text here
|
|
9
|
+
// - list item 1
|
|
10
|
+
// - list item 2
|
|
9
11
|
// :::
|
|
10
|
-
// Where the
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const lastChild = node.children[node.children.length - 1];
|
|
12
|
+
// Where the container starts in one paragraph, has sibling nodes (lists, etc),
|
|
13
|
+
// and the closing ::: may be in a later text node
|
|
14
|
+
function processMultiNodeContainers(tree) {
|
|
15
|
+
visit(tree, 'paragraph', (node, index, parent) => {
|
|
16
|
+
if (!node.children || node.children.length === 0) return;
|
|
17
|
+
if (!parent || !parent.children) return;
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// Last child must be text ending with :::
|
|
21
|
-
if (lastChild.type !== 'text') return;
|
|
19
|
+
const firstChild = node.children[0];
|
|
20
|
+
if (firstChild.type !== 'text') return;
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
const firstText = firstChild.value;
|
|
23
|
+
|
|
24
|
+
// Check if first text starts with ::: type [title]
|
|
25
|
+
// Allow content to follow on subsequent lines within this paragraph
|
|
26
|
+
const startMatch = firstText.match(/^(:{3,})\s+(tip|note|warning|danger|info|details)([ \t]+[^\n]*)?\n?/);
|
|
27
|
+
if (!startMatch) return;
|
|
28
|
+
|
|
29
|
+
const [fullMatch, openColons, type, titlePart] = startMatch;
|
|
30
|
+
const colonCount = openColons.length;
|
|
31
|
+
const customTitle = titlePart ? titlePart.trim() : '';
|
|
32
|
+
const title = customTitle || getDefaultTitle(type);
|
|
33
|
+
|
|
34
|
+
// Now we need to find the closing ::: which could be:
|
|
35
|
+
// 1. At the end of this same paragraph's text
|
|
36
|
+
// 2. In a sibling paragraph
|
|
37
|
+
// 3. Inside a text node in a list item (no blank line before :::)
|
|
38
|
+
|
|
39
|
+
const siblings = parent.children;
|
|
40
|
+
let endIndex = -1;
|
|
41
|
+
let endNodeInfo = null; // { siblingIndex, childPath, closeMatch }
|
|
42
|
+
|
|
43
|
+
// First check if closing is in the same paragraph
|
|
44
|
+
const lastChild = node.children[node.children.length - 1];
|
|
45
|
+
if (lastChild.type === 'text') {
|
|
46
|
+
const closeInSame = lastChild.value.match(/\n(:{3,})\s*$/);
|
|
47
|
+
if (closeInSame && closeInSame[1].length === colonCount) {
|
|
48
|
+
// Closing is in the same paragraph - handle as single paragraph container
|
|
49
|
+
endNodeInfo = { type: 'same-paragraph', closeMatch: closeInSame };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
25
52
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
53
|
+
if (!endNodeInfo) {
|
|
54
|
+
// Search through siblings for the closing :::
|
|
55
|
+
for (let i = index + 1; i < siblings.length; i++) {
|
|
56
|
+
const sibling = siblings[i];
|
|
29
57
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
58
|
+
// Check if sibling is a paragraph with just :::
|
|
59
|
+
if (sibling.type === 'paragraph' &&
|
|
60
|
+
sibling.children &&
|
|
61
|
+
sibling.children.length === 1 &&
|
|
62
|
+
sibling.children[0].type === 'text') {
|
|
63
|
+
const text = sibling.children[0].value.trim();
|
|
64
|
+
const closeMatch = text.match(/^(:{3,})$/);
|
|
65
|
+
if (closeMatch && closeMatch[1].length === colonCount) {
|
|
66
|
+
endIndex = i;
|
|
67
|
+
endNodeInfo = { type: 'sibling-paragraph', siblingIndex: i };
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
33
71
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
72
|
+
// Check if closing ::: is embedded in a text node (e.g., after a list item)
|
|
73
|
+
// This happens when there's no blank line before :::
|
|
74
|
+
if (sibling.type === 'list' && sibling.children) {
|
|
75
|
+
// Check the last item of the list
|
|
76
|
+
const lastItem = sibling.children[sibling.children.length - 1];
|
|
77
|
+
if (lastItem.children) {
|
|
78
|
+
const lastItemPara = lastItem.children[lastItem.children.length - 1];
|
|
79
|
+
if (lastItemPara.type === 'paragraph' && lastItemPara.children) {
|
|
80
|
+
const lastText = lastItemPara.children[lastItemPara.children.length - 1];
|
|
81
|
+
if (lastText.type === 'text') {
|
|
82
|
+
const closeMatch = lastText.value.match(/\n(:{3,})\s*$/);
|
|
83
|
+
if (closeMatch && closeMatch[1].length === colonCount) {
|
|
84
|
+
endIndex = i;
|
|
85
|
+
endNodeInfo = {
|
|
86
|
+
type: 'in-list',
|
|
87
|
+
siblingIndex: i,
|
|
88
|
+
lastText: lastText,
|
|
89
|
+
closeMatch: closeMatch
|
|
90
|
+
};
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
38
99
|
|
|
39
|
-
|
|
40
|
-
const title = customTitle || getDefaultTitle(type);
|
|
100
|
+
if (!endNodeInfo) return; // No closing found
|
|
41
101
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
102
|
+
// Create HTML wrapper
|
|
103
|
+
let openingHTML, closingHTML;
|
|
104
|
+
if (type === 'details') {
|
|
105
|
+
openingHTML = `<details class="container-details custom-container" data-container-type="details">
|
|
46
106
|
<summary class="container-title">${title}</summary>
|
|
47
107
|
<div class="container-content">`;
|
|
48
|
-
|
|
108
|
+
closingHTML = `</div>
|
|
49
109
|
</details>`;
|
|
50
|
-
|
|
51
|
-
|
|
110
|
+
} else {
|
|
111
|
+
openingHTML = `<div class="container-${type} custom-container" data-container-type="${type}">
|
|
52
112
|
<div class="container-title">${title}</div>
|
|
53
113
|
<div class="container-content">`;
|
|
54
|
-
|
|
114
|
+
closingHTML = `</div>
|
|
55
115
|
</div>`;
|
|
56
|
-
|
|
116
|
+
}
|
|
57
117
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
118
|
+
const htmlStartNode = { type: 'html', value: openingHTML };
|
|
119
|
+
const htmlEndNode = { type: 'html', value: closingHTML };
|
|
120
|
+
|
|
121
|
+
if (endNodeInfo.type === 'same-paragraph') {
|
|
122
|
+
// Handle single paragraph container
|
|
123
|
+
const lastChild = node.children[node.children.length - 1];
|
|
124
|
+
const newFirstText = firstText.slice(fullMatch.length);
|
|
125
|
+
const newLastText = lastChild.value.slice(0, endNodeInfo.closeMatch.index);
|
|
126
|
+
|
|
127
|
+
const contentChildren = [];
|
|
128
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
129
|
+
const child = node.children[i];
|
|
130
|
+
if (i === 0) {
|
|
131
|
+
if (node.children.length === 1) {
|
|
132
|
+
// Single text node
|
|
133
|
+
const middleText = newFirstText.slice(0, newFirstText.length - (firstText.length - lastChild.value.length) - endNodeInfo.closeMatch[0].length);
|
|
134
|
+
if (middleText.trim()) {
|
|
135
|
+
contentChildren.push({ ...child, value: middleText.trim() });
|
|
136
|
+
}
|
|
137
|
+
} else if (newFirstText.trim()) {
|
|
138
|
+
contentChildren.push({ ...child, value: newFirstText });
|
|
139
|
+
}
|
|
140
|
+
} else if (i === node.children.length - 1) {
|
|
141
|
+
if (newLastText.trim()) {
|
|
142
|
+
contentChildren.push({ ...child, value: newLastText });
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
contentChildren.push({ ...child });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
63
148
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
149
|
+
if (contentChildren.length === 0) return;
|
|
150
|
+
|
|
151
|
+
const contentParagraph = { type: 'paragraph', children: contentChildren };
|
|
152
|
+
parent.children.splice(index, 1, htmlStartNode, contentParagraph, htmlEndNode);
|
|
153
|
+
return index + 3;
|
|
154
|
+
|
|
155
|
+
} else if (endNodeInfo.type === 'sibling-paragraph') {
|
|
156
|
+
// Closing is in a sibling paragraph
|
|
157
|
+
// Extract content from opening paragraph (after the ::: line)
|
|
158
|
+
const newFirstText = firstText.slice(fullMatch.length);
|
|
159
|
+
const contentNodes = [];
|
|
160
|
+
|
|
161
|
+
// Add remaining content from opening paragraph if any
|
|
162
|
+
if (newFirstText.trim() || node.children.length > 1) {
|
|
163
|
+
const newParaChildren = [];
|
|
164
|
+
if (newFirstText.trim()) {
|
|
165
|
+
newParaChildren.push({ ...firstChild, value: newFirstText });
|
|
166
|
+
}
|
|
167
|
+
for (let i = 1; i < node.children.length; i++) {
|
|
168
|
+
newParaChildren.push({ ...node.children[i] });
|
|
169
|
+
}
|
|
170
|
+
if (newParaChildren.length > 0) {
|
|
171
|
+
contentNodes.push({ type: 'paragraph', children: newParaChildren });
|
|
75
172
|
}
|
|
76
|
-
} else if (newFirstText.trim()) {
|
|
77
|
-
contentChildren.push({ ...child, value: newFirstText });
|
|
78
173
|
}
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
174
|
+
|
|
175
|
+
// Add all siblings between opening and closing
|
|
176
|
+
for (let i = index + 1; i < endIndex; i++) {
|
|
177
|
+
contentNodes.push(siblings[i]);
|
|
83
178
|
}
|
|
84
|
-
} else {
|
|
85
|
-
// Middle children - keep as-is
|
|
86
|
-
contentChildren.push({ ...child });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
179
|
|
|
90
|
-
|
|
91
|
-
|
|
180
|
+
const replaceCount = endIndex - index + 1;
|
|
181
|
+
const newNodes = [htmlStartNode, ...contentNodes, htmlEndNode];
|
|
182
|
+
parent.children.splice(index, replaceCount, ...newNodes);
|
|
183
|
+
return index + newNodes.length;
|
|
92
184
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
children: contentChildren
|
|
98
|
-
};
|
|
99
|
-
const htmlEndNode = { type: 'html', value: closingHTML };
|
|
185
|
+
} else if (endNodeInfo.type === 'in-list') {
|
|
186
|
+
// Closing ::: is inside the last list item
|
|
187
|
+
// Remove the closing from the text node
|
|
188
|
+
endNodeInfo.lastText.value = endNodeInfo.lastText.value.slice(0, endNodeInfo.closeMatch.index);
|
|
100
189
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
190
|
+
// Extract content from opening paragraph
|
|
191
|
+
const newFirstText = firstText.slice(fullMatch.length);
|
|
192
|
+
const contentNodes = [];
|
|
193
|
+
|
|
194
|
+
if (newFirstText.trim() || node.children.length > 1) {
|
|
195
|
+
const newParaChildren = [];
|
|
196
|
+
if (newFirstText.trim()) {
|
|
197
|
+
newParaChildren.push({ ...firstChild, value: newFirstText });
|
|
198
|
+
}
|
|
199
|
+
for (let i = 1; i < node.children.length; i++) {
|
|
200
|
+
newParaChildren.push({ ...node.children[i] });
|
|
201
|
+
}
|
|
202
|
+
if (newParaChildren.length > 0) {
|
|
203
|
+
contentNodes.push({ type: 'paragraph', children: newParaChildren });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Add all siblings including the list (which now has the ::: removed)
|
|
208
|
+
for (let i = index + 1; i <= endIndex; i++) {
|
|
209
|
+
contentNodes.push(siblings[i]);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const replaceCount = endIndex - index + 1;
|
|
213
|
+
const newNodes = [htmlStartNode, ...contentNodes, htmlEndNode];
|
|
214
|
+
parent.children.splice(index, replaceCount, ...newNodes);
|
|
215
|
+
return index + newNodes.length;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Run the multi-node container processor
|
|
221
|
+
processMultiNodeContainers(tree);
|
|
104
222
|
|
|
105
223
|
// Pre-process: Extract ::: from text nodes where it appears at the end
|
|
106
224
|
// This handles cases where ::: is on a new line but without a blank line separator
|
|
@@ -355,11 +473,35 @@ export function remarkContainers() {
|
|
|
355
473
|
console.log('DEBUG containerDirective:', type, 'children:', node.children?.length || 0, JSON.stringify(node.children?.map(c => c.type)));
|
|
356
474
|
}
|
|
357
475
|
|
|
358
|
-
// Get custom title from directive label
|
|
476
|
+
// Get custom title from directive label
|
|
477
|
+
// remark-directive v4 may store label in different places:
|
|
478
|
+
// 1. node.data.directiveLabel (standard location)
|
|
479
|
+
// 2. node.children[0] as a paragraph with the label text
|
|
359
480
|
let customTitle = '';
|
|
481
|
+
let contentChildren = node.children || [];
|
|
482
|
+
|
|
483
|
+
// Check node.data.directiveLabel first (standard remark-directive behavior)
|
|
360
484
|
if (node.data && node.data.directiveLabel) {
|
|
361
485
|
customTitle = node.data.directiveLabel;
|
|
362
486
|
}
|
|
487
|
+
// Check if first child is a paragraph that contains just the title text
|
|
488
|
+
// This happens when label is on same line: ::: warning Title Here
|
|
489
|
+
else if (contentChildren.length > 0) {
|
|
490
|
+
const firstChild = contentChildren[0];
|
|
491
|
+
// If first child is paragraph with single text node, it might be the title
|
|
492
|
+
if (firstChild.type === 'paragraph' &&
|
|
493
|
+
firstChild.children &&
|
|
494
|
+
firstChild.children.length === 1 &&
|
|
495
|
+
firstChild.children[0].type === 'text') {
|
|
496
|
+
const text = firstChild.children[0].value.trim();
|
|
497
|
+
// Check if this looks like a title (single line, no markdown formatting)
|
|
498
|
+
// and the directive has more children (actual content)
|
|
499
|
+
if (!text.includes('\n') && contentChildren.length > 1) {
|
|
500
|
+
customTitle = text;
|
|
501
|
+
contentChildren = contentChildren.slice(1); // Remove title from content
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
363
505
|
|
|
364
506
|
const title = customTitle || getDefaultTitle(type);
|
|
365
507
|
|
|
@@ -382,7 +524,7 @@ export function remarkContainers() {
|
|
|
382
524
|
const htmlStartNode = { type: 'html', value: openingHTML };
|
|
383
525
|
const htmlEndNode = { type: 'html', value: closingHTML };
|
|
384
526
|
|
|
385
|
-
const newNodes = [htmlStartNode, ...
|
|
527
|
+
const newNodes = [htmlStartNode, ...contentChildren, htmlEndNode];
|
|
386
528
|
parent.children.splice(index, 1, ...newNodes);
|
|
387
529
|
|
|
388
530
|
return index + newNodes.length;
|
|
@@ -395,10 +537,24 @@ export function remarkContainers() {
|
|
|
395
537
|
return;
|
|
396
538
|
}
|
|
397
539
|
|
|
540
|
+
// Get custom title from directive label
|
|
398
541
|
let customTitle = '';
|
|
542
|
+
let contentChildren = node.children || [];
|
|
543
|
+
|
|
399
544
|
if (node.data && node.data.directiveLabel) {
|
|
400
545
|
customTitle = node.data.directiveLabel;
|
|
401
546
|
}
|
|
547
|
+
// Check if first child is a text node that could be the title
|
|
548
|
+
else if (contentChildren.length > 0) {
|
|
549
|
+
const firstChild = contentChildren[0];
|
|
550
|
+
if (firstChild.type === 'text') {
|
|
551
|
+
const text = firstChild.value.trim();
|
|
552
|
+
if (!text.includes('\n') && contentChildren.length > 1) {
|
|
553
|
+
customTitle = text;
|
|
554
|
+
contentChildren = contentChildren.slice(1);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
402
558
|
|
|
403
559
|
const title = customTitle || getDefaultTitle(type);
|
|
404
560
|
|
|
@@ -421,7 +577,7 @@ export function remarkContainers() {
|
|
|
421
577
|
const htmlStartNode = { type: 'html', value: openingHTML };
|
|
422
578
|
const htmlEndNode = { type: 'html', value: closingHTML };
|
|
423
579
|
|
|
424
|
-
const newNodes = [htmlStartNode, ...
|
|
580
|
+
const newNodes = [htmlStartNode, ...contentChildren, htmlEndNode];
|
|
425
581
|
parent.children.splice(index, 1, ...newNodes);
|
|
426
582
|
|
|
427
583
|
return index + newNodes.length;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Create
|
|
3
|
-
description:
|
|
2
|
+
title: Create Blog & First Post
|
|
3
|
+
description: Use CLI to create a blog project and write your first article
|
|
4
4
|
pubDate: 2025-01-01
|
|
5
5
|
author: jet-w
|
|
6
6
|
categories:
|
|
@@ -8,13 +8,114 @@ categories:
|
|
|
8
8
|
tags:
|
|
9
9
|
- Getting Started
|
|
10
10
|
- Writing
|
|
11
|
+
- CLI
|
|
11
12
|
---
|
|
12
13
|
|
|
13
|
-
# Create
|
|
14
|
+
# Create Blog & First Post
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
This guide will walk you through creating a blog project using the CLI tool and writing your first post.
|
|
16
17
|
|
|
17
|
-
##
|
|
18
|
+
## Create a Blog Project with CLI
|
|
19
|
+
|
|
20
|
+
### Quick Start
|
|
21
|
+
|
|
22
|
+
Use `@jet-w/astro-blog-cli` to quickly create a new blog project:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @jet-w/astro-blog-cli my-blog
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Interactive Mode
|
|
29
|
+
|
|
30
|
+
Run without options to enter interactive mode, which guides you through the configuration:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx @jet-w/astro-blog-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
You will be prompted for:
|
|
37
|
+
- Project name
|
|
38
|
+
- Blog title (English & Chinese)
|
|
39
|
+
- Blog description (English & Chinese)
|
|
40
|
+
- Author name
|
|
41
|
+
- Author email
|
|
42
|
+
- Site URL
|
|
43
|
+
- Template selection
|
|
44
|
+
|
|
45
|
+
### Non-Interactive Mode
|
|
46
|
+
|
|
47
|
+
Use `-y` flag to skip prompts and use defaults or provided options:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Quick create with defaults
|
|
51
|
+
npx @jet-w/astro-blog-cli my-blog -y
|
|
52
|
+
|
|
53
|
+
# With custom options
|
|
54
|
+
npx @jet-w/astro-blog-cli my-blog -y \
|
|
55
|
+
--title "My Awesome Blog" \
|
|
56
|
+
--title-zh "我的博客" \
|
|
57
|
+
--description "A blog about tech" \
|
|
58
|
+
--description-zh "一个关于技术的博客" \
|
|
59
|
+
--author "John Doe" \
|
|
60
|
+
--email "john@example.com" \
|
|
61
|
+
--site "https://myblog.com"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### CLI Command Options
|
|
65
|
+
|
|
66
|
+
| Option | Description | Default |
|
|
67
|
+
|--------|-------------|---------|
|
|
68
|
+
| `-t, --template <template>` | Template to use | `default` |
|
|
69
|
+
| `--title <title>` | Blog title (English) | `My Astro Blog` |
|
|
70
|
+
| `--title-zh <titleZh>` | Blog title (Chinese) | `我的Astro博客` |
|
|
71
|
+
| `--description <description>` | Blog description (English) | - |
|
|
72
|
+
| `--description-zh <descriptionZh>` | Blog description (Chinese) | - |
|
|
73
|
+
| `--author <author>` | Author name | `Author` |
|
|
74
|
+
| `--email <email>` | Author email | `email@example.com` |
|
|
75
|
+
| `--site <site>` | Site URL | `https://example.com` |
|
|
76
|
+
| `--lang <lang>` | CLI language (`en`/`zh`) | Auto-detect |
|
|
77
|
+
| `-y, --yes` | Skip prompts, use defaults | - |
|
|
78
|
+
| `-f, --force` | Overwrite existing directory | - |
|
|
79
|
+
|
|
80
|
+
### Start Development Server
|
|
81
|
+
|
|
82
|
+
After project creation:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
cd my-blog
|
|
86
|
+
npm install
|
|
87
|
+
npm run dev
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Your blog will be running at `http://localhost:4321`.
|
|
91
|
+
|
|
92
|
+
### Project Structure
|
|
93
|
+
|
|
94
|
+
The created project includes:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
my-blog/
|
|
98
|
+
├── astro.config.mjs # Astro configuration
|
|
99
|
+
├── package.json
|
|
100
|
+
├── src/
|
|
101
|
+
│ ├── config/
|
|
102
|
+
│ │ ├── site.ts # Main site configuration
|
|
103
|
+
│ │ └── locales/ # i18n configurations
|
|
104
|
+
│ │ ├── en/ # English locale
|
|
105
|
+
│ │ └── zh-CN/ # Chinese locale
|
|
106
|
+
│ └── content.config.ts # Content collections schema
|
|
107
|
+
├── content/
|
|
108
|
+
│ ├── posts/ # Blog posts (Markdown/MDX)
|
|
109
|
+
│ ├── pages/ # Static pages
|
|
110
|
+
│ └── slides/ # Presentation slides
|
|
111
|
+
└── public/ # Static assets
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Create Your First Post
|
|
115
|
+
|
|
116
|
+
Now let's write your first blog post!
|
|
117
|
+
|
|
118
|
+
### Where Posts Live
|
|
18
119
|
|
|
19
120
|
All blog posts go in the `content/posts/` directory:
|
|
20
121
|
|
|
@@ -27,7 +128,7 @@ content/
|
|
|
27
128
|
└── tutorial.md
|
|
28
129
|
```
|
|
29
130
|
|
|
30
|
-
|
|
131
|
+
### Create a New Post
|
|
31
132
|
|
|
32
133
|
Create a file called `hello-world.md` in `content/posts/`:
|
|
33
134
|
|
|
@@ -68,7 +169,7 @@ Stay tuned for more posts about:
|
|
|
68
169
|
Thanks for reading!
|
|
69
170
|
```
|
|
70
171
|
|
|
71
|
-
|
|
172
|
+
### View Your Post
|
|
72
173
|
|
|
73
174
|
Save the file and visit:
|
|
74
175
|
|
|
@@ -98,6 +199,7 @@ star: false # true = featured post
|
|
|
98
199
|
```
|
|
99
200
|
|
|
100
201
|
::: tip Required vs Optional
|
|
202
|
+
|
|
101
203
|
Only `title` is required. All other fields are optional but recommended for better organization and SEO.
|
|
102
204
|
:::
|
|
103
205
|
|
|
@@ -171,6 +273,20 @@ Then use:
|
|
|
171
273
|

|
|
172
274
|
```
|
|
173
275
|
|
|
276
|
+
## Blog Features Overview
|
|
277
|
+
|
|
278
|
+
Blogs created with this theme support the following features:
|
|
279
|
+
|
|
280
|
+
- **Multi-language support (i18n)**: Switch between English and Chinese
|
|
281
|
+
- **Markdown & MDX**: Powerful content authoring capabilities
|
|
282
|
+
- **Code syntax highlighting**: Built-in syntax highlighting support
|
|
283
|
+
- **Mermaid diagrams**: Flowcharts, sequence diagrams, and more
|
|
284
|
+
- **LaTeX math equations**: Math formula rendering support
|
|
285
|
+
- **Custom containers**: tip, warning, danger, and other callout blocks
|
|
286
|
+
- **RSS feed**: Auto-generated RSS feed
|
|
287
|
+
- **Tailwind CSS**: Modern styling support
|
|
288
|
+
- **Vue.js components**: Use Vue components in MDX
|
|
289
|
+
|
|
174
290
|
---
|
|
175
291
|
|
|
176
|
-
Congratulations on your first post! Next, let's understand the [project structure](./04-structure).
|
|
292
|
+
Congratulations on creating your blog and publishing your first post! Next, let's understand the [project structure](./04-structure).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
3
|
-
description:
|
|
2
|
+
title: 创建博客与文章
|
|
3
|
+
description: 使用 CLI 创建博客项目并编写你的第一篇文章
|
|
4
4
|
pubDate: 2025-01-01
|
|
5
5
|
author: jet-w
|
|
6
6
|
categories:
|
|
@@ -8,13 +8,114 @@ categories:
|
|
|
8
8
|
tags:
|
|
9
9
|
- 入门
|
|
10
10
|
- 写作
|
|
11
|
+
- CLI
|
|
11
12
|
---
|
|
12
13
|
|
|
13
|
-
#
|
|
14
|
+
# 创建博客与文章
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
本文将指导你使用 CLI 工具快速创建博客项目,并编写发布你的第一篇文章。
|
|
16
17
|
|
|
17
|
-
##
|
|
18
|
+
## 使用 CLI 创建博客项目
|
|
19
|
+
|
|
20
|
+
### 快速开始
|
|
21
|
+
|
|
22
|
+
使用 `@jet-w/astro-blog-cli` 可以快速创建一个新的博客项目:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @jet-w/astro-blog-cli my-blog
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 交互模式
|
|
29
|
+
|
|
30
|
+
不带参数运行命令进入交互模式,系统会逐步引导你配置:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx @jet-w/astro-blog-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
你将被提示输入:
|
|
37
|
+
- 项目名称
|
|
38
|
+
- 博客标题(中英文)
|
|
39
|
+
- 博客描述(中英文)
|
|
40
|
+
- 作者名称
|
|
41
|
+
- 作者邮箱
|
|
42
|
+
- 网站 URL
|
|
43
|
+
- 模板选择
|
|
44
|
+
|
|
45
|
+
### 非交互模式
|
|
46
|
+
|
|
47
|
+
使用 `-y` 参数跳过提示,直接使用默认值或指定的选项:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# 使用默认配置快速创建
|
|
51
|
+
npx @jet-w/astro-blog-cli my-blog -y
|
|
52
|
+
|
|
53
|
+
# 指定自定义选项
|
|
54
|
+
npx @jet-w/astro-blog-cli my-blog -y \
|
|
55
|
+
--title "我的技术博客" \
|
|
56
|
+
--title-zh "我的技术博客" \
|
|
57
|
+
--description "A blog about tech" \
|
|
58
|
+
--description-zh "一个关于技术的博客" \
|
|
59
|
+
--author "张三" \
|
|
60
|
+
--email "zhangsan@example.com" \
|
|
61
|
+
--site "https://myblog.com"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### CLI 命令选项
|
|
65
|
+
|
|
66
|
+
| 选项 | 描述 | 默认值 |
|
|
67
|
+
|------|------|--------|
|
|
68
|
+
| `-t, --template <template>` | 使用的模板 | `default` |
|
|
69
|
+
| `--title <title>` | 博客标题(英文) | `My Astro Blog` |
|
|
70
|
+
| `--title-zh <titleZh>` | 博客标题(中文) | `我的Astro博客` |
|
|
71
|
+
| `--description <description>` | 博客描述(英文) | - |
|
|
72
|
+
| `--description-zh <descriptionZh>` | 博客描述(中文) | - |
|
|
73
|
+
| `--author <author>` | 作者名称 | `Author` |
|
|
74
|
+
| `--email <email>` | 作者邮箱 | `email@example.com` |
|
|
75
|
+
| `--site <site>` | 网站 URL | `https://example.com` |
|
|
76
|
+
| `--lang <lang>` | CLI 语言(`en`/`zh`) | 自动检测 |
|
|
77
|
+
| `-y, --yes` | 跳过提示,使用默认值 | - |
|
|
78
|
+
| `-f, --force` | 覆盖已存在的目录 | - |
|
|
79
|
+
|
|
80
|
+
### 启动开发服务器
|
|
81
|
+
|
|
82
|
+
项目创建完成后:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
cd my-blog
|
|
86
|
+
npm install
|
|
87
|
+
npm run dev
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
博客将在 `http://localhost:4321` 运行。
|
|
91
|
+
|
|
92
|
+
### 项目结构
|
|
93
|
+
|
|
94
|
+
创建的项目包含以下结构:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
my-blog/
|
|
98
|
+
├── astro.config.mjs # Astro 配置文件
|
|
99
|
+
├── package.json
|
|
100
|
+
├── src/
|
|
101
|
+
│ ├── config/
|
|
102
|
+
│ │ ├── site.ts # 网站主配置
|
|
103
|
+
│ │ └── locales/ # i18n 配置
|
|
104
|
+
│ │ ├── en/ # 英文语言包
|
|
105
|
+
│ │ └── zh-CN/ # 中文语言包
|
|
106
|
+
│ └── content.config.ts # 内容集合配置
|
|
107
|
+
├── content/
|
|
108
|
+
│ ├── posts/ # 博客文章(Markdown/MDX)
|
|
109
|
+
│ ├── pages/ # 静态页面
|
|
110
|
+
│ └── slides/ # 演示幻灯片
|
|
111
|
+
└── public/ # 静态资源
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## 创建第一篇文章
|
|
115
|
+
|
|
116
|
+
现在让我们来写你的第一篇博客文章!
|
|
117
|
+
|
|
118
|
+
### 文章存放位置
|
|
18
119
|
|
|
19
120
|
所有博客文章都放在 `content/posts/` 目录:
|
|
20
121
|
|
|
@@ -27,7 +128,7 @@ content/
|
|
|
27
128
|
└── tutorial.md
|
|
28
129
|
```
|
|
29
130
|
|
|
30
|
-
|
|
131
|
+
### 创建新文章
|
|
31
132
|
|
|
32
133
|
在 `content/posts/` 中创建一个名为 `hello-world.md` 的文件:
|
|
33
134
|
|
|
@@ -68,7 +169,7 @@ categories:
|
|
|
68
169
|
感谢阅读!
|
|
69
170
|
```
|
|
70
171
|
|
|
71
|
-
|
|
172
|
+
### 查看你的文章
|
|
72
173
|
|
|
73
174
|
保存文件后访问:
|
|
74
175
|
|
|
@@ -101,7 +202,7 @@ star: false # true = 精选文章
|
|
|
101
202
|
只有 `title` 是必填的。其他字段都是可选的,但建议填写以便更好地组织和优化 SEO。
|
|
102
203
|
:::
|
|
103
204
|
|
|
104
|
-
##
|
|
205
|
+
## 使用子目录组织文章
|
|
105
206
|
|
|
106
207
|
使用子目录来组织相关文章:
|
|
107
208
|
|
|
@@ -171,6 +272,20 @@ public/
|
|
|
171
272
|

|
|
172
273
|
```
|
|
173
274
|
|
|
275
|
+
## 博客特性一览
|
|
276
|
+
|
|
277
|
+
使用本主题创建的博客支持以下特性:
|
|
278
|
+
|
|
279
|
+
- **多语言支持(i18n)**:中英文双语切换
|
|
280
|
+
- **Markdown & MDX**:强大的内容编写能力
|
|
281
|
+
- **代码高亮**:内置语法高亮支持
|
|
282
|
+
- **Mermaid 图表**:流程图、时序图等
|
|
283
|
+
- **LaTeX 数学公式**:支持数学公式渲染
|
|
284
|
+
- **自定义容器**:tip、warning、danger 等提示块
|
|
285
|
+
- **RSS 订阅**:自动生成 RSS feed
|
|
286
|
+
- **Tailwind CSS**:现代化的样式支持
|
|
287
|
+
- **Vue.js 组件**:可在 MDX 中使用 Vue 组件
|
|
288
|
+
|
|
174
289
|
---
|
|
175
290
|
|
|
176
|
-
|
|
291
|
+
恭喜你创建了博客并发布了第一篇文章!接下来,让我们了解 [项目结构](./04-structure)。
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"@astrojs/rss": "^4.0.14",
|
|
13
13
|
"@astrojs/tailwind": "^5.1.3",
|
|
14
14
|
"@astrojs/vue": "^5.0.6",
|
|
15
|
-
"@jet-w/astro-blog": "
|
|
15
|
+
"@jet-w/astro-blog": "^0.2.1",
|
|
16
16
|
"@tailwindcss/typography": "^0.5.15",
|
|
17
17
|
"astro": "^5.14.1",
|
|
18
18
|
"echarts": "^6.0.0",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"../..": {
|
|
31
31
|
"name": "@jet-w/astro-blog",
|
|
32
|
-
"version": "0.
|
|
32
|
+
"version": "0.2.3",
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@tailwindcss/typography": "^0.5.15",
|