@stati/core 1.7.0 → 1.8.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.
- package/README.md +615 -100
- package/dist/core/build.js +2 -2
- package/dist/seo/auto-inject.js +2 -2
- package/dist/seo/generator.d.ts.map +1 -1
- package/dist/seo/generator.js +4 -4
- package/dist/seo/sitemap.d.ts.map +1 -1
- package/dist/seo/sitemap.js +10 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,70 +1,248 @@
|
|
|
1
1
|
# @stati/core
|
|
2
2
|
|
|
3
|
-
The core engine
|
|
3
|
+
**The core engine powering Stati — a minimal, TypeScript-first static site generator that's fast to learn and even faster to build with.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Built for developers who want modern tooling without the complexity. Write in Markdown, template with Eta, and deploy anywhere.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Getting Started
|
|
10
|
+
|
|
11
|
+
### The Easy Way (Recommended)
|
|
12
|
+
|
|
13
|
+
If you're new to Stati, start with our scaffolding tool:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx create-stati my-site
|
|
17
|
+
cd my-site
|
|
18
|
+
npm install
|
|
19
|
+
npm run dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This creates a complete Stati project with sensible defaults, your choice of CSS framework, and everything configured for you.
|
|
23
|
+
|
|
24
|
+
### Using the Core Package Directly
|
|
25
|
+
|
|
26
|
+
For advanced users who want programmatic control or to integrate Stati into existing tooling:
|
|
6
27
|
|
|
7
28
|
```bash
|
|
8
29
|
npm install @stati/core
|
|
9
30
|
```
|
|
10
31
|
|
|
11
|
-
|
|
32
|
+
---
|
|
12
33
|
|
|
13
|
-
|
|
34
|
+
## Quick Example
|
|
14
35
|
|
|
15
|
-
|
|
16
|
-
|
|
36
|
+
Here's how simple it is to build a static site with Stati:
|
|
37
|
+
|
|
38
|
+
**1. Create a configuration file (`stati.config.js`):**
|
|
17
39
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
staticDir: './public',
|
|
40
|
+
```javascript
|
|
41
|
+
import { defineConfig } from '@stati/core';
|
|
42
|
+
|
|
43
|
+
export default defineConfig({
|
|
23
44
|
site: {
|
|
24
45
|
title: 'My Site',
|
|
25
46
|
baseUrl: 'https://example.com',
|
|
26
47
|
},
|
|
27
48
|
});
|
|
49
|
+
```
|
|
28
50
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
**2. Add some content (`site/index.md`):**
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
---
|
|
55
|
+
title: Welcome
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
# Hello Stati
|
|
59
|
+
|
|
60
|
+
This is my first page. It's just Markdown.
|
|
61
|
+
```
|
|
35
62
|
|
|
36
|
-
|
|
63
|
+
**3. Build or develop:**
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { build, createDevServer } from '@stati/core';
|
|
67
|
+
|
|
68
|
+
// Development with live reload
|
|
37
69
|
const server = await createDevServer({
|
|
38
70
|
port: 3000,
|
|
39
71
|
open: true,
|
|
40
72
|
});
|
|
73
|
+
|
|
74
|
+
// Or build for production
|
|
75
|
+
const stats = await build({
|
|
76
|
+
clean: true,
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
That's it! Your site is ready.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Why Choose @stati/core?
|
|
85
|
+
|
|
86
|
+
### For New Users
|
|
87
|
+
|
|
88
|
+
- **Zero Configuration Required** — Works out of the box with sensible defaults
|
|
89
|
+
- **Simple File Structure** — Markdown files automatically become pages
|
|
90
|
+
- **Live Reload Built-In** — See changes instantly during development
|
|
91
|
+
- **SEO Ready** — Automatic meta tags and structured data
|
|
92
|
+
|
|
93
|
+
### For Advanced Users
|
|
94
|
+
|
|
95
|
+
- **Programmatic API** — Full control over the build process
|
|
96
|
+
- **TypeScript-First** — Complete type safety throughout
|
|
97
|
+
- **Extensible Hooks** — Customize every stage of the build
|
|
98
|
+
- **Smart Caching** — Incremental builds with intelligent invalidation
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Core Concepts
|
|
103
|
+
|
|
104
|
+
### How Stati Works
|
|
105
|
+
|
|
106
|
+
Stati uses a simple, file-based approach:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
my-site/
|
|
110
|
+
├── site/ # Your content (Markdown + templates)
|
|
111
|
+
│ ├── index.md # Homepage → /
|
|
112
|
+
│ └── blog/
|
|
113
|
+
│ └── hello.md # Blog post → /blog/hello/
|
|
114
|
+
├── public/ # Static assets (CSS, images)
|
|
115
|
+
└── stati.config.js # Configuration (optional)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**The workflow:**
|
|
119
|
+
|
|
120
|
+
1. Write content in Markdown with front-matter
|
|
121
|
+
2. Stati processes it through templates
|
|
122
|
+
3. Static HTML is generated with smart caching
|
|
123
|
+
4. Deploy anywhere (Netlify, Vercel, GitHub Pages, etc.)
|
|
124
|
+
|
|
125
|
+
### Key Features
|
|
126
|
+
|
|
127
|
+
#### Markdown-First Content
|
|
128
|
+
|
|
129
|
+
Write naturally with front-matter for metadata:
|
|
130
|
+
|
|
131
|
+
```markdown
|
|
132
|
+
---
|
|
133
|
+
title: My Post
|
|
134
|
+
description: A great article
|
|
135
|
+
date: 2024-01-15
|
|
136
|
+
tags: [tutorial, stati]
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
# My Post
|
|
140
|
+
|
|
141
|
+
Your content here...
|
|
41
142
|
```
|
|
42
143
|
|
|
43
|
-
|
|
144
|
+
#### Flexible Templating
|
|
145
|
+
|
|
146
|
+
Customize layouts with [Eta templates](https://eta.js.org):
|
|
147
|
+
|
|
148
|
+
```html
|
|
149
|
+
<!DOCTYPE html>
|
|
150
|
+
<html>
|
|
151
|
+
<head>
|
|
152
|
+
<title><%= stati.page.title %></title>
|
|
153
|
+
</head>
|
|
154
|
+
<body>
|
|
155
|
+
<%~ stati.page.content %>
|
|
156
|
+
</body>
|
|
157
|
+
</html>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### Incremental Static Generation (ISG)
|
|
161
|
+
|
|
162
|
+
Only rebuild what changed:
|
|
163
|
+
|
|
164
|
+
- Smart caching based on file modification times
|
|
165
|
+
- TTL-based refresh for dynamic content
|
|
166
|
+
- Tag-based invalidation for related content
|
|
44
167
|
|
|
45
168
|
```typescript
|
|
169
|
+
import { invalidate } from '@stati/core';
|
|
170
|
+
|
|
171
|
+
// Invalidate by tag
|
|
172
|
+
await invalidate('tag:blog');
|
|
173
|
+
|
|
174
|
+
// Invalidate by path
|
|
175
|
+
await invalidate('path:/posts');
|
|
176
|
+
|
|
177
|
+
// Invalidate old content (3+ months)
|
|
178
|
+
await invalidate('age:3months');
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### Built-In SEO
|
|
182
|
+
|
|
183
|
+
SEO optimization works automatically with zero configuration:
|
|
184
|
+
|
|
185
|
+
- Meta tags (title, description, keywords)
|
|
186
|
+
- Open Graph for social sharing
|
|
187
|
+
- Twitter Cards
|
|
188
|
+
- Structured data (JSON-LD)
|
|
189
|
+
|
|
190
|
+
Enable sitemap and robots.txt with one config option.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Complete Configuration Reference
|
|
195
|
+
|
|
196
|
+
Stati works with **zero configuration**, but you can customize every aspect when needed.
|
|
197
|
+
|
|
198
|
+
### Minimal Setup (Recommended for Beginners)
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
46
201
|
import { defineConfig } from '@stati/core';
|
|
47
202
|
|
|
48
203
|
export default defineConfig({
|
|
49
|
-
|
|
50
|
-
|
|
204
|
+
site: {
|
|
205
|
+
title: 'My Stati Site',
|
|
206
|
+
baseUrl: 'https://example.com',
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
```
|
|
51
210
|
|
|
52
|
-
|
|
53
|
-
outDir: './dist',
|
|
211
|
+
This is all you need! Stati automatically enables:
|
|
54
212
|
|
|
55
|
-
|
|
56
|
-
|
|
213
|
+
- **ISG caching** with 6-hour TTL
|
|
214
|
+
- **SEO auto-injection** for all pages
|
|
215
|
+
- **Markdown processing** with standard features
|
|
216
|
+
|
|
217
|
+
### Extended Configuration (Common Options)
|
|
218
|
+
|
|
219
|
+
Below are commonly used configuration options. This is **not a complete list** — see the [full configuration documentation](https://docs.stati.build/configuration/) for all available options.
|
|
220
|
+
|
|
221
|
+
```javascript
|
|
222
|
+
import { defineConfig } from '@stati/core';
|
|
223
|
+
|
|
224
|
+
export default defineConfig({
|
|
225
|
+
// Directory configuration
|
|
226
|
+
srcDir: 'site', // Content source (default: 'site')
|
|
227
|
+
outDir: 'dist', // Build output (default: 'dist')
|
|
228
|
+
staticDir: 'public', // Static assets (default: 'public')
|
|
57
229
|
|
|
58
230
|
// Site metadata
|
|
59
231
|
site: {
|
|
60
|
-
title: 'My Site',
|
|
232
|
+
title: 'My Stati Site',
|
|
61
233
|
baseUrl: 'https://example.com',
|
|
234
|
+
defaultLocale: 'en-US',
|
|
62
235
|
},
|
|
63
236
|
|
|
64
|
-
// Markdown
|
|
237
|
+
// Markdown processing
|
|
65
238
|
markdown: {
|
|
66
|
-
plugins: [
|
|
239
|
+
plugins: [
|
|
240
|
+
'anchor', // Anchor links to headings
|
|
241
|
+
'toc-done-right', // Table of contents
|
|
242
|
+
['external-links', { externalTarget: '_blank' }], // External links in new tab
|
|
243
|
+
],
|
|
67
244
|
configure: (md) => {
|
|
245
|
+
// Configure MarkdownIt instance directly
|
|
68
246
|
md.set({
|
|
69
247
|
html: true,
|
|
70
248
|
linkify: true,
|
|
@@ -73,21 +251,61 @@ export default defineConfig({
|
|
|
73
251
|
},
|
|
74
252
|
},
|
|
75
253
|
|
|
76
|
-
// Eta template
|
|
254
|
+
// Eta template engine
|
|
77
255
|
eta: {
|
|
78
256
|
filters: {
|
|
79
|
-
|
|
257
|
+
formatDate: (date) => new Date(date).toLocaleDateString('en-US'),
|
|
258
|
+
slugify: (text) => text.toLowerCase().replace(/\s+/g, '-'),
|
|
80
259
|
},
|
|
81
260
|
},
|
|
82
261
|
|
|
83
|
-
// Incremental Static Generation
|
|
262
|
+
// Incremental Static Generation (enabled by default)
|
|
84
263
|
isg: {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
264
|
+
ttlSeconds: 86400, // Cache TTL: 24 hours (default: 21600 / 6 hours)
|
|
265
|
+
maxAgeCapDays: 30, // Max age for aging rules (default: 365)
|
|
266
|
+
aging: [
|
|
267
|
+
{ untilDays: 7, ttlSeconds: 86400 }, // 1 day cache for week-old content
|
|
268
|
+
{ untilDays: 30, ttlSeconds: 604800 }, // 1 week cache for month-old content
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
// SEO configuration (auto-injection enabled by default)
|
|
273
|
+
seo: {
|
|
274
|
+
defaultAuthor: {
|
|
275
|
+
name: 'John Doe',
|
|
276
|
+
email: 'john@example.com',
|
|
277
|
+
url: 'https://johndoe.com',
|
|
278
|
+
},
|
|
279
|
+
debug: false, // Enable SEO debug logging
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
// Sitemap generation (opt-in)
|
|
283
|
+
sitemap: {
|
|
284
|
+
enabled: true, // Generate sitemap.xml
|
|
285
|
+
defaultPriority: 0.5, // Default priority for pages
|
|
286
|
+
defaultChangeFreq: 'monthly',
|
|
287
|
+
excludePatterns: ['/draft/**', '/admin/**'], // Exclude patterns
|
|
288
|
+
priorityRules: [
|
|
289
|
+
{ pattern: '/', priority: 1.0 }, // Homepage highest priority
|
|
290
|
+
{ pattern: '/blog/**', priority: 0.8 }, // Blog posts high priority
|
|
291
|
+
{ pattern: '/docs/**', priority: 0.7 }, // Documentation
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
// Robots.txt generation (opt-in)
|
|
296
|
+
robots: {
|
|
297
|
+
enabled: true, // Generate robots.txt
|
|
298
|
+
disallow: ['/admin/', '/draft/', '/private/'], // Paths to block
|
|
299
|
+
crawlDelay: 10, // Crawl delay in seconds
|
|
300
|
+
sitemap: true, // Auto-include sitemap URL
|
|
301
|
+
customLines: [
|
|
302
|
+
'# Custom directives',
|
|
303
|
+
'User-agent: GPTBot',
|
|
304
|
+
'Disallow: /',
|
|
305
|
+
],
|
|
88
306
|
},
|
|
89
307
|
|
|
90
|
-
// Development server
|
|
308
|
+
// Development server
|
|
91
309
|
dev: {
|
|
92
310
|
port: 3000,
|
|
93
311
|
host: 'localhost',
|
|
@@ -109,161 +327,458 @@ export default defineConfig({
|
|
|
109
327
|
});
|
|
110
328
|
```
|
|
111
329
|
|
|
112
|
-
|
|
330
|
+
> **For the complete configuration reference** including all options, advanced features, and detailed explanations, see the [Configuration Guide](https://docs.stati.build/configuration/).
|
|
331
|
+
|
|
332
|
+
---
|
|
113
333
|
|
|
114
|
-
|
|
334
|
+
## API Reference
|
|
115
335
|
|
|
116
|
-
|
|
336
|
+
### Build Functions
|
|
117
337
|
|
|
118
|
-
|
|
338
|
+
#### `build(options?: BuildOptions): Promise<BuildStats>`
|
|
339
|
+
|
|
340
|
+
Build your static site for production.
|
|
341
|
+
|
|
342
|
+
**Options:**
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
{
|
|
346
|
+
force?: boolean; // Force rebuild of all pages (ignores cache)
|
|
347
|
+
clean?: boolean; // Clean output directory before build
|
|
348
|
+
includeDrafts?: boolean; // Include draft pages in build
|
|
349
|
+
configPath?: string; // Custom config file path
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Example:**
|
|
119
354
|
|
|
120
355
|
```typescript
|
|
121
356
|
import { build } from '@stati/core';
|
|
122
357
|
|
|
123
|
-
await build({
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
includeDrafts: false,
|
|
127
|
-
configPath: './stati.config.js', // Custom config file path
|
|
358
|
+
const stats = await build({
|
|
359
|
+
clean: true,
|
|
360
|
+
force: false,
|
|
361
|
+
includeDrafts: false,
|
|
128
362
|
});
|
|
363
|
+
|
|
364
|
+
console.log(`Built ${stats.totalPages} pages in ${stats.buildTimeMs}ms`);
|
|
129
365
|
```
|
|
130
366
|
|
|
131
|
-
|
|
367
|
+
**Returns:** Build statistics including pages built, duration, and cache hits.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
#### `createDevServer(options?: DevServerOptions): Promise<DevServer>`
|
|
132
372
|
|
|
133
|
-
|
|
373
|
+
Start a development server with live reload.
|
|
374
|
+
|
|
375
|
+
**Options:**
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
{
|
|
379
|
+
port?: number; // Port number (default: 3000)
|
|
380
|
+
host?: string; // Host address (default: 'localhost')
|
|
381
|
+
open?: boolean; // Auto-open browser (default: false)
|
|
382
|
+
configPath?: string; // Custom config file path
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Example:**
|
|
134
387
|
|
|
135
388
|
```typescript
|
|
136
389
|
import { createDevServer } from '@stati/core';
|
|
137
390
|
|
|
138
391
|
const server = await createDevServer({
|
|
139
392
|
port: 3000,
|
|
140
|
-
host: 'localhost',
|
|
141
393
|
open: true,
|
|
142
|
-
configPath: './stati.config.js',
|
|
143
394
|
});
|
|
395
|
+
|
|
396
|
+
console.log(`Dev server running at http://localhost:3000`);
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**Returns:** Server instance with methods to stop and restart.
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
#### `createPreviewServer(options?: PreviewServerOptions): Promise<PreviewServer>`
|
|
404
|
+
|
|
405
|
+
Start a production preview server to test your built site locally.
|
|
406
|
+
|
|
407
|
+
**Options:**
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
{
|
|
411
|
+
port?: number; // Port number (default: 4000)
|
|
412
|
+
host?: string; // Host address (default: 'localhost')
|
|
413
|
+
open?: boolean; // Auto-open browser (default: false)
|
|
414
|
+
configPath?: string; // Custom config file path
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
**Example:**
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
import { createPreviewServer } from '@stati/core';
|
|
422
|
+
|
|
423
|
+
// First build your site
|
|
424
|
+
await build({ clean: true });
|
|
425
|
+
|
|
426
|
+
// Then preview it
|
|
427
|
+
const server = await createPreviewServer({
|
|
428
|
+
port: 4000,
|
|
429
|
+
open: true,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
console.log(`Preview server running at http://localhost:4000`);
|
|
144
433
|
```
|
|
145
434
|
|
|
435
|
+
**Returns:** Server instance with methods to stop.
|
|
436
|
+
|
|
437
|
+
**Note:** Unlike `createDevServer`, the preview server serves the static files from your `dist/` directory without live reload or rebuilding. This is useful for testing your production build locally before deployment.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
146
441
|
#### `invalidate(query?: string): Promise<InvalidationResult>`
|
|
147
442
|
|
|
148
443
|
Invalidate cache by tags, paths, patterns, or age.
|
|
149
444
|
|
|
445
|
+
**Query Syntax:**
|
|
446
|
+
|
|
447
|
+
- `tag:blog` — Invalidate pages with specific tag
|
|
448
|
+
- `path:/posts` — Invalidate pages under path
|
|
449
|
+
- `glob:/blog/**` — Invalidate by glob pattern
|
|
450
|
+
- `age:3months` — Invalidate content younger than 3 months
|
|
451
|
+
- No query — Clear entire cache
|
|
452
|
+
|
|
453
|
+
**Examples:**
|
|
454
|
+
|
|
150
455
|
```typescript
|
|
151
456
|
import { invalidate } from '@stati/core';
|
|
152
457
|
|
|
153
458
|
// Invalidate by tag
|
|
154
|
-
await invalidate('tag:
|
|
459
|
+
await invalidate('tag:news');
|
|
155
460
|
|
|
156
461
|
// Invalidate by path prefix
|
|
157
|
-
await invalidate('path:/
|
|
158
|
-
|
|
159
|
-
// Invalidate by glob pattern
|
|
160
|
-
await invalidate('glob:/blog/**');
|
|
462
|
+
await invalidate('path:/blog/2024/');
|
|
161
463
|
|
|
162
|
-
// Invalidate content
|
|
163
|
-
await invalidate('age:
|
|
464
|
+
// Invalidate old content
|
|
465
|
+
await invalidate('age:6months');
|
|
164
466
|
|
|
165
467
|
// Multiple criteria (OR logic)
|
|
166
468
|
await invalidate('tag:blog age:1month');
|
|
167
469
|
|
|
168
|
-
// Clear
|
|
470
|
+
// Clear everything
|
|
169
471
|
await invalidate();
|
|
170
472
|
```
|
|
171
473
|
|
|
172
|
-
|
|
474
|
+
**Returns:** Invalidation result with count of pages affected.
|
|
475
|
+
|
|
476
|
+
---
|
|
173
477
|
|
|
174
478
|
#### `defineConfig(config: StatiConfig): StatiConfig`
|
|
175
479
|
|
|
176
480
|
Define a type-safe configuration with full TypeScript support.
|
|
177
481
|
|
|
482
|
+
**Example:**
|
|
483
|
+
|
|
178
484
|
```typescript
|
|
179
485
|
import { defineConfig } from '@stati/core';
|
|
180
486
|
|
|
181
487
|
export default defineConfig({
|
|
182
|
-
|
|
488
|
+
site: {
|
|
489
|
+
title: 'My Site',
|
|
490
|
+
baseUrl: 'https://example.com',
|
|
491
|
+
},
|
|
492
|
+
// TypeScript provides autocomplete and validation
|
|
183
493
|
});
|
|
184
494
|
```
|
|
185
495
|
|
|
496
|
+
---
|
|
497
|
+
|
|
186
498
|
#### `loadConfig(cwd?: string): Promise<StatiConfig>`
|
|
187
499
|
|
|
188
|
-
Load and validate Stati configuration from
|
|
500
|
+
Load and validate Stati configuration from a project directory.
|
|
501
|
+
|
|
502
|
+
**Example:**
|
|
189
503
|
|
|
190
504
|
```typescript
|
|
191
505
|
import { loadConfig } from '@stati/core';
|
|
192
506
|
|
|
193
|
-
|
|
194
|
-
const
|
|
507
|
+
// Load from current directory
|
|
508
|
+
const config = await loadConfig();
|
|
509
|
+
|
|
510
|
+
// Load from specific directory
|
|
511
|
+
const config2 = await loadConfig('/path/to/project');
|
|
195
512
|
```
|
|
196
513
|
|
|
197
|
-
|
|
514
|
+
---
|
|
198
515
|
|
|
199
|
-
|
|
516
|
+
### TypeScript Types
|
|
517
|
+
|
|
518
|
+
Full type definitions for TypeScript users:
|
|
200
519
|
|
|
201
520
|
```typescript
|
|
202
521
|
import type {
|
|
522
|
+
// Configuration
|
|
203
523
|
StatiConfig,
|
|
524
|
+
ISGConfig,
|
|
525
|
+
BuildHooks,
|
|
526
|
+
|
|
527
|
+
// Build
|
|
204
528
|
BuildOptions,
|
|
205
|
-
DevServerOptions,
|
|
206
|
-
InvalidationResult,
|
|
207
|
-
PageModel,
|
|
208
|
-
FrontMatter,
|
|
209
529
|
BuildContext,
|
|
530
|
+
BuildStats,
|
|
531
|
+
|
|
532
|
+
// Pages
|
|
533
|
+
PageModel,
|
|
210
534
|
PageContext,
|
|
211
|
-
|
|
535
|
+
FrontMatter,
|
|
536
|
+
|
|
537
|
+
// Navigation
|
|
212
538
|
NavNode,
|
|
213
|
-
|
|
539
|
+
|
|
540
|
+
// Development
|
|
541
|
+
DevServerOptions,
|
|
542
|
+
PreviewServerOptions,
|
|
543
|
+
|
|
544
|
+
// Cache
|
|
545
|
+
InvalidationResult,
|
|
214
546
|
AgingRule,
|
|
215
|
-
BuildStats,
|
|
216
547
|
} from '@stati/core/types';
|
|
217
548
|
```
|
|
218
549
|
|
|
219
|
-
|
|
550
|
+
> **Need more types?** This is a curated list of commonly used types. For the complete type reference including all SEO, sitemap, and configuration types, see the [API Types Documentation](https://docs.stati.build/api/types/).
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## Features Deep Dive
|
|
220
555
|
|
|
221
556
|
### Markdown Processing
|
|
222
557
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
- **
|
|
226
|
-
- **
|
|
558
|
+
Stati supports rich Markdown features out of the box:
|
|
559
|
+
|
|
560
|
+
- **Front-matter** — YAML, TOML, or JSON metadata
|
|
561
|
+
- **Plugins** — Full markdown-it ecosystem compatibility
|
|
562
|
+
- **Syntax highlighting** — Code blocks with language support
|
|
563
|
+
- **Custom rendering** — Configure parser behavior
|
|
564
|
+
- **Draft mode** — Mark pages as `draft: true`
|
|
565
|
+
|
|
566
|
+
**Example with plugins:**
|
|
567
|
+
|
|
568
|
+
```javascript
|
|
569
|
+
export default defineConfig({
|
|
570
|
+
markdown: {
|
|
571
|
+
plugins: [
|
|
572
|
+
'anchor', // Add anchor links
|
|
573
|
+
'table', // Enhanced tables
|
|
574
|
+
'footnote', // Footnote support
|
|
575
|
+
['abbr', { /* options */ }], // Abbreviations
|
|
576
|
+
],
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
```
|
|
227
580
|
|
|
228
581
|
### Template Engine
|
|
229
582
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
- **
|
|
233
|
-
- **
|
|
583
|
+
Powered by [Eta](https://eta.js.org) for fast, flexible templates:
|
|
584
|
+
|
|
585
|
+
- **Layouts** — Template inheritance via `layout` property
|
|
586
|
+
- **Partials** — Reusable components
|
|
587
|
+
- **Custom filters** — Transform data in templates
|
|
588
|
+
- **Type-safe helpers** — Access page data with autocomplete
|
|
589
|
+
- **Hot reload** — See template changes instantly
|
|
590
|
+
|
|
591
|
+
**Template structure:**
|
|
592
|
+
|
|
593
|
+
```html
|
|
594
|
+
<!-- site/layout.eta -->
|
|
595
|
+
<!DOCTYPE html>
|
|
596
|
+
<html>
|
|
597
|
+
<head>
|
|
598
|
+
<title><%= stati.page.title %> - <%= stati.site.title %></title>
|
|
599
|
+
</head>
|
|
600
|
+
<body>
|
|
601
|
+
<%~ include('_partials/header') %>
|
|
602
|
+
<main>
|
|
603
|
+
<%~ stati.page.content %>
|
|
604
|
+
</main>
|
|
605
|
+
<%~ include('_partials/footer') %>
|
|
606
|
+
</body>
|
|
607
|
+
</html>
|
|
608
|
+
```
|
|
234
609
|
|
|
235
610
|
### Navigation System
|
|
236
611
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
- **
|
|
240
|
-
- **
|
|
612
|
+
Automatic navigation hierarchy based on your file structure:
|
|
613
|
+
|
|
614
|
+
- **Auto-generated tree** — Reflects directory structure
|
|
615
|
+
- **Breadcrumbs** — Parent-child relationships
|
|
616
|
+
- **Custom ordering** — Use `order` in front-matter
|
|
617
|
+
- **Index pages** — Special handling for `index.md`
|
|
618
|
+
|
|
619
|
+
**Access in templates:**
|
|
620
|
+
|
|
621
|
+
```html
|
|
622
|
+
<!-- Show breadcrumbs -->
|
|
623
|
+
<nav>
|
|
624
|
+
<% stati.page.breadcrumbs.forEach(crumb => { %>
|
|
625
|
+
<a href="<%= crumb.url %>"><%= crumb.title %></a>
|
|
626
|
+
<% }) %>
|
|
627
|
+
</nav>
|
|
628
|
+
|
|
629
|
+
<!-- Show navigation tree -->
|
|
630
|
+
<ul>
|
|
631
|
+
<% stati.navigation.forEach(item => { %>
|
|
632
|
+
<li><a href="<%= item.url %>"><%= item.title %></a></li>
|
|
633
|
+
<% }) %>
|
|
634
|
+
</ul>
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
### Development Experience
|
|
241
638
|
|
|
242
|
-
|
|
639
|
+
Built for productivity:
|
|
243
640
|
|
|
244
|
-
- **Live reload**
|
|
245
|
-
- **
|
|
246
|
-
- **
|
|
247
|
-
- **
|
|
641
|
+
- **Live reload** — WebSocket-based instant updates
|
|
642
|
+
- **Fast rebuilds** — Only rebuild changed pages
|
|
643
|
+
- **Error overlay** — See build errors in browser
|
|
644
|
+
- **Static assets** — Served from `public/` directory
|
|
645
|
+
- **Source maps** — Debug with original code
|
|
248
646
|
|
|
249
647
|
### Caching & Performance
|
|
250
648
|
|
|
251
|
-
|
|
252
|
-
- **Incremental builds** for faster rebuilds
|
|
253
|
-
- **Tag-based invalidation** for selective cache clearing
|
|
254
|
-
- **Memory optimization** for large sites
|
|
649
|
+
Smart caching for lightning-fast builds:
|
|
255
650
|
|
|
256
|
-
|
|
651
|
+
- **Modification tracking** — Rebuild only changed files
|
|
652
|
+
- **Incremental builds** — Skip unchanged pages
|
|
653
|
+
- **Tag invalidation** — Update related content together
|
|
654
|
+
- **TTL-based refresh** — Control cache lifetime
|
|
655
|
+
- **Age-based rules** — Different TTL for old content
|
|
257
656
|
|
|
258
|
-
|
|
657
|
+
**Cache strategies:**
|
|
259
658
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
659
|
+
```javascript
|
|
660
|
+
export default defineConfig({
|
|
661
|
+
isg: {
|
|
662
|
+
ttlSeconds: 21600, // 6 hours default
|
|
663
|
+
aging: [
|
|
664
|
+
{ untilDays: 7, ttlSeconds: 3600 }, // Recent: 1 hour
|
|
665
|
+
{ untilDays: 30, ttlSeconds: 86400 }, // Month: 1 day
|
|
666
|
+
// Older content uses default TTL
|
|
667
|
+
],
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## Use Cases
|
|
675
|
+
|
|
676
|
+
### Documentation Sites
|
|
677
|
+
|
|
678
|
+
Perfect for technical documentation:
|
|
679
|
+
|
|
680
|
+
```javascript
|
|
681
|
+
export default defineConfig({
|
|
682
|
+
site: {
|
|
683
|
+
title: 'My Project Docs',
|
|
684
|
+
baseUrl: 'https://docs.example.com',
|
|
685
|
+
},
|
|
686
|
+
markdown: {
|
|
687
|
+
plugins: ['anchor', 'toc-done-right', 'container'],
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### Blogs
|
|
693
|
+
|
|
694
|
+
Great for content-heavy sites:
|
|
695
|
+
|
|
696
|
+
```javascript
|
|
697
|
+
export default defineConfig({
|
|
698
|
+
site: {
|
|
699
|
+
title: 'My Blog',
|
|
700
|
+
baseUrl: 'https://blog.example.com',
|
|
701
|
+
},
|
|
702
|
+
isg: {
|
|
703
|
+
aging: [
|
|
704
|
+
{ untilDays: 30, ttlSeconds: 3600 }, // Fresh posts: 1 hour
|
|
705
|
+
{ untilDays: 365, ttlSeconds: 86400 }, // Year-old: 1 day
|
|
706
|
+
],
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### Landing Pages
|
|
712
|
+
|
|
713
|
+
Fast, SEO-optimized marketing sites:
|
|
714
|
+
|
|
715
|
+
```javascript
|
|
716
|
+
export default defineConfig({
|
|
717
|
+
site: {
|
|
718
|
+
title: 'Product Name',
|
|
719
|
+
baseUrl: 'https://example.com',
|
|
720
|
+
},
|
|
721
|
+
seo: {
|
|
722
|
+
defaultAuthor: {
|
|
723
|
+
name: 'Company Name',
|
|
724
|
+
url: 'https://example.com',
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
});
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
732
|
+
## Requirements
|
|
733
|
+
|
|
734
|
+
- **Node.js** 22.0.0 or higher
|
|
735
|
+
- **npm** 8.0.0 or higher (or equivalent package manager)
|
|
736
|
+
|
|
737
|
+
---
|
|
738
|
+
|
|
739
|
+
## Learn More
|
|
740
|
+
|
|
741
|
+
- [**Full Documentation**](https://docs.stati.build) — Complete guides and tutorials
|
|
742
|
+
- [**Configuration Guide**](https://docs.stati.build/configuration/) — All options explained
|
|
743
|
+
- [**API Reference**](https://docs.stati.build/api/) — Detailed API docs
|
|
744
|
+
- [**Examples**](https://docs.stati.build/examples/) — Real-world projects
|
|
745
|
+
- [**Contributing**](https://github.com/ianchak/stati/blob/main/CONTRIBUTING.md) — Help improve Stati
|
|
746
|
+
|
|
747
|
+
---
|
|
748
|
+
|
|
749
|
+
## Philosophy
|
|
750
|
+
|
|
751
|
+
Stati Core is built on these principles:
|
|
752
|
+
|
|
753
|
+
- **Simplicity First** — Sensible defaults that just work
|
|
754
|
+
- **Performance Matters** — Fast builds, smart caching
|
|
755
|
+
- **Developer Experience** — Great tooling, clear errors
|
|
756
|
+
- **Type Safety** — TypeScript throughout
|
|
757
|
+
- **Extensibility** — Customize when you need to
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
## Support & Community
|
|
762
|
+
|
|
763
|
+
- [GitHub Issues](https://github.com/ianchak/stati/issues) — Report bugs or request features
|
|
764
|
+
- [Discussions](https://github.com/ianchak/stati/discussions) — Ask questions, share ideas
|
|
765
|
+
- [Documentation](https://docs.stati.build) — Comprehensive guides
|
|
766
|
+
|
|
767
|
+
---
|
|
266
768
|
|
|
267
769
|
## License
|
|
268
770
|
|
|
269
771
|
MIT © [Imre Csige](https://github.com/ianchak)
|
|
772
|
+
|
|
773
|
+
---
|
|
774
|
+
|
|
775
|
+
**Ready to build?**
|
|
776
|
+
|
|
777
|
+
```bash
|
|
778
|
+
# New project (easiest way)
|
|
779
|
+
npx create-stati my-site
|
|
780
|
+
|
|
781
|
+
# Or use core directly
|
|
782
|
+
npm install @stati/core
|
|
783
|
+
```
|
|
784
|
+
|
package/dist/core/build.js
CHANGED
|
@@ -352,8 +352,6 @@ async function buildInternal(options = {}) {
|
|
|
352
352
|
logger.building('Building your site...');
|
|
353
353
|
// Load configuration
|
|
354
354
|
const { config, outDir, cacheDir } = await loadAndValidateConfig(options);
|
|
355
|
-
// Load cache manifest for ISG
|
|
356
|
-
const { manifest } = await setupCacheAndManifest(cacheDir);
|
|
357
355
|
// Initialize cache stats
|
|
358
356
|
let cacheHits = 0;
|
|
359
357
|
let cacheMisses = 0;
|
|
@@ -364,6 +362,8 @@ async function buildInternal(options = {}) {
|
|
|
364
362
|
await remove(cacheDir);
|
|
365
363
|
}
|
|
366
364
|
await ensureDir(outDir);
|
|
365
|
+
// Load cache manifest for ISG (after potential clean operation)
|
|
366
|
+
const { manifest } = await setupCacheAndManifest(cacheDir);
|
|
367
367
|
// Load content and build navigation
|
|
368
368
|
console.log(); // Add spacing before content loading
|
|
369
369
|
const { pages, navigation, md, eta } = await loadContentAndBuildNavigation(config, options, logger);
|
package/dist/seo/auto-inject.js
CHANGED
|
@@ -86,8 +86,8 @@ export function autoInjectSEO(html, options) {
|
|
|
86
86
|
// Inject SEO metadata before </head>
|
|
87
87
|
const before = html.substring(0, headClosePos);
|
|
88
88
|
const after = html.substring(headClosePos);
|
|
89
|
-
// Add proper indentation (
|
|
90
|
-
const injected = `${before}
|
|
89
|
+
// Add proper indentation (4 spaces) and newline
|
|
90
|
+
const injected = `${before} ${seoMetadata}\n${after}`;
|
|
91
91
|
logDebug(`Injected ${existingTags.size === 0 ? 'all' : 'missing'} SEO tags into ${page.url}`, {
|
|
92
92
|
debug,
|
|
93
93
|
config,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/seo/generator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAElE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/seo/generator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAElE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAS7C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAsH3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,EAAE,CAyE/D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,EAAE,CAuDjE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE;IACP,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB,EACD,IAAI,CAAC,EAAE,KAAK,CAAC,UAAU,GAAG,MAAM,CAAC,GAChC,MAAM,CA6CR"}
|
package/dist/seo/generator.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Generates meta tags, Open Graph tags, Twitter Cards, and structured data
|
|
4
4
|
*/
|
|
5
5
|
import { SEOTagType } from '../types/seo.js';
|
|
6
|
-
import { escapeHtml, validateSEOMetadata, generateRobotsContent } from './utils/index.js';
|
|
6
|
+
import { escapeHtml, validateSEOMetadata, generateRobotsContent, resolveAbsoluteUrl, } from './utils/index.js';
|
|
7
7
|
import { sanitizeStructuredData } from './utils/escape-and-validation.js';
|
|
8
8
|
/**
|
|
9
9
|
* Generate complete SEO metadata for a page.
|
|
@@ -94,7 +94,7 @@ export function generateSEOMetadata(ctx) {
|
|
|
94
94
|
}
|
|
95
95
|
// Canonical link
|
|
96
96
|
if (shouldGenerate(SEOTagType.Canonical)) {
|
|
97
|
-
const canonical = seo.canonical ||
|
|
97
|
+
const canonical = seo.canonical || resolveAbsoluteUrl(page.url || '/', siteUrl);
|
|
98
98
|
meta.push(`<link rel="canonical" href="${escapeHtml(canonical)}">`);
|
|
99
99
|
}
|
|
100
100
|
// Robots meta tag
|
|
@@ -125,7 +125,7 @@ export function generateSEOMetadata(ctx) {
|
|
|
125
125
|
const sanitized = sanitizeStructuredData(seo.structuredData, logger);
|
|
126
126
|
meta.push(`<script type="application/ld+json">${JSON.stringify(sanitized)}</script>`);
|
|
127
127
|
}
|
|
128
|
-
return meta.join('\n
|
|
128
|
+
return meta.join('\n ');
|
|
129
129
|
}
|
|
130
130
|
/**
|
|
131
131
|
* Generate Open Graph protocol meta tags.
|
|
@@ -148,7 +148,7 @@ export function generateOpenGraphTags(ctx) {
|
|
|
148
148
|
// Basic OG tags with fallback chain
|
|
149
149
|
const ogTitle = og.title || seo.title || page.frontMatter.title || config.site.title;
|
|
150
150
|
const ogDescription = og.description || seo.description || page.frontMatter.description;
|
|
151
|
-
const ogUrl = og.url || seo.canonical ||
|
|
151
|
+
const ogUrl = og.url || seo.canonical || resolveAbsoluteUrl(page.url || '/', siteUrl);
|
|
152
152
|
const ogType = og.type || 'website';
|
|
153
153
|
const ogSiteName = og.siteName || config.site.title;
|
|
154
154
|
tags.push(`<meta property="og:title" content="${escapeHtml(ogTitle)}">`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../src/seo/sitemap.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EAEb,uBAAuB,EACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../src/seo/sitemap.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EAEb,uBAAuB,EACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAqJtD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,aAAa,EAAE,aAAa,GAC3B,YAAY,GAAG,IAAI,CA4ErB;AA2BD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAUlE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAatF;AAoBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,EAAE,EAClB,MAAM,EAAE,WAAW,EACnB,aAAa,EAAE,aAAa,GAC3B,uBAAuB,CA8CzB"}
|
package/dist/seo/sitemap.js
CHANGED
|
@@ -122,8 +122,16 @@ function determinePriority(page, rules, defaultPriority = 0.5) {
|
|
|
122
122
|
return validatePriority(rule.priority);
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
-
else
|
|
126
|
-
|
|
125
|
+
else {
|
|
126
|
+
// For non-glob patterns, check exact match or path prefix
|
|
127
|
+
if (page.url === pattern) {
|
|
128
|
+
return validatePriority(rule.priority);
|
|
129
|
+
}
|
|
130
|
+
// For path prefix matching, ensure we match at path boundaries
|
|
131
|
+
// e.g., "/api" matches "/api/foo" but "/" only matches "/" exactly
|
|
132
|
+
if (pattern !== '/' && page.url.startsWith(pattern + '/')) {
|
|
133
|
+
return validatePriority(rule.priority);
|
|
134
|
+
}
|
|
127
135
|
}
|
|
128
136
|
}
|
|
129
137
|
return defaultPriority;
|