@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 CHANGED
@@ -1,70 +1,248 @@
1
1
  # @stati/core
2
2
 
3
- The core engine for Stati, a lightweight TypeScript static site generator built with modern architecture.
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
- ## Installation
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
- ## Usage
32
+ ---
12
33
 
13
- ### Basic Setup
34
+ ## Quick Example
14
35
 
15
- ```typescript
16
- import { build, createDevServer, defineConfig, loadConfig } from '@stati/core';
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
- // Define configuration
19
- const config = defineConfig({
20
- srcDir: './site',
21
- outDir: './dist',
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
- // Load configuration and build site
30
- await build({
31
- clean: false,
32
- force: false,
33
- includeDrafts: false,
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
- // Or start development server
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
- ### Configuration
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
- // Source directory for content files
50
- srcDir: './site',
204
+ site: {
205
+ title: 'My Stati Site',
206
+ baseUrl: 'https://example.com',
207
+ },
208
+ });
209
+ ```
51
210
 
52
- // Output directory for built site
53
- outDir: './dist',
211
+ This is all you need! Stati automatically enables:
54
212
 
55
- // Static assets directory
56
- staticDir: './public',
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 configuration
237
+ // Markdown processing
65
238
  markdown: {
66
- plugins: ['anchor'],
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 configuration
254
+ // Eta template engine
77
255
  eta: {
78
256
  filters: {
79
- // Custom template filters
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
- enabled: true,
86
- ttlSeconds: 3600,
87
- maxAgeCapDays: 30,
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 options
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
- ## API
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
- ### Core Functions
334
+ ## API Reference
115
335
 
116
- #### `build(options: BuildOptions): Promise<BuildStats>`
336
+ ### Build Functions
117
337
 
118
- Build a static site.
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
- force: false, // Force rebuild of all pages
125
- clean: false, // Clean output directory before build
126
- includeDrafts: false, // Include draft pages in build
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
- #### `createDevServer(options: DevServerOptions): Promise<DevServer>`
367
+ **Returns:** Build statistics including pages built, duration, and cache hits.
368
+
369
+ ---
370
+
371
+ #### `createDevServer(options?: DevServerOptions): Promise<DevServer>`
132
372
 
133
- Create a development server with live reload.
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:blog');
459
+ await invalidate('tag:news');
155
460
 
156
461
  // Invalidate by path prefix
157
- await invalidate('path:/posts');
158
-
159
- // Invalidate by glob pattern
160
- await invalidate('glob:/blog/**');
462
+ await invalidate('path:/blog/2024/');
161
463
 
162
- // Invalidate content younger than 3 months (exact calendar arithmetic)
163
- await invalidate('age:3months');
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 entire cache
470
+ // Clear everything
169
471
  await invalidate();
170
472
  ```
171
473
 
172
- ### Configuration
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
- // Your configuration here
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 the project directory.
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
- const config = await loadConfig(); // Load from current directory
194
- const config2 = await loadConfig('/path/to/project'); // Load from specific directory
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
- ## Types
514
+ ---
198
515
 
199
- The package exports comprehensive TypeScript types:
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
- BuildHooks,
535
+ FrontMatter,
536
+
537
+ // Navigation
212
538
  NavNode,
213
- ISGConfig,
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
- ## Features
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
- - **Front-matter support** with YAML, TOML, or JSON
224
- - **Plugin system** using markdown-it ecosystem
225
- - **Custom rendering** with configurable options
226
- - **Draft pages** with `draft: true` in front-matter
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
- - **Eta templates** with layouts and partials
231
- - **Template inheritance** with `layout` front-matter property
232
- - **Custom helpers** and filters
233
- - **Hot reload** during development
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
- - **Automatic hierarchy** based on filesystem structure
238
- - **Breadcrumbs** and navigation trees
239
- - **Custom sorting** with `order` front-matter property
240
- - **Index pages** with special handling
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
- ### Development Server
639
+ Built for productivity:
243
640
 
244
- - **Live reload** with WebSocket integration
245
- - **Hot rebuilding** on file changes
246
- - **Static asset serving** from public directory
247
- - **Error overlay** for development debugging
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
- - **Smart caching** based on file modification times
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
- ## Architecture
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
- Stati Core is built with a modular architecture:
657
+ **Cache strategies:**
259
658
 
260
- - **Content processing** - Markdown parsing and front-matter extraction
261
- - **Template rendering** - Eta engine with layouts and partials
262
- - **Navigation building** - Automatic hierarchy generation
263
- - **Asset handling** - Static file copying and optimization
264
- - **Development server** - Live reload and hot rebuilding
265
- - **Build system** - Production optimization and output generation
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
+
@@ -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);
@@ -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 (2 spaces) and newline
90
- const injected = `${before} ${seoMetadata}\n${after}`;
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;AAI7C;;;;;;;;;;;;;;;;;;;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"}
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"}
@@ -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 || `${siteUrl}${page.url}`;
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 || `${siteUrl}${page.url}`;
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;AA6ItD;;;;;;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"}
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"}
@@ -122,8 +122,16 @@ function determinePriority(page, rules, defaultPriority = 0.5) {
122
122
  return validatePriority(rule.priority);
123
123
  }
124
124
  }
125
- else if (page.url === pattern || page.url.startsWith(pattern)) {
126
- return validatePriority(rule.priority);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stati/core",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",