@inglorious/ssx 0.4.1 → 1.1.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,882 +1,664 @@
1
- # Inglorious Store
1
+ # @inglorious/ssx
2
2
 
3
- [![NPM version](https://img.shields.io/npm/v/@inglorious/store.svg)](https://www.npmjs.com/package/@inglorious/store)
3
+ [![NPM version](https://img.shields.io/npm/v/@inglorious/ssx.svg)](https://www.npmjs.com/package/@inglorious/ssx)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- A Redux-compatible, ECS-inspired state library that makes state management as elegant as game logic.
6
+ **Static Site Xecution** - Build blazing-fast static sites with [@inglorious/web](https://www.npmjs.com/package/@inglorious/web), complete with server-side rendering, client-side hydration, and zero-config routing.
7
7
 
8
- **Drop-in replacement for Redux.** Works with `react-redux` and Redux DevTools. Borrows concepts from Entity-Component-System architectures and Functional Programming to provide an environment where you can write simple, predictable, and testable code.
9
-
10
- ```javascript
11
- // from redux
12
- import { createStore } from "redux"
13
- // to
14
- import { createStore } from "@inglorious/store"
15
- ```
8
+ SSX takes your entity-based web apps and generates optimized static HTML with full hydration support. Think Next.js SSG or Astro, but with the simplicity and predictability of Inglorious Web's entity architecture.
16
9
 
17
10
  ---
18
11
 
19
- ## Why Inglorious Store?
12
+ ## Why SSX?
20
13
 
21
- Redux is powerful but verbose. You need action creators, reducers, middleware for async operations, and a bunch of decisions about where logic should live. Redux Toolkit cuts the boilerplate, but you're still writing a lot of ceremony.
14
+ ### ⚡️ Fast by Default
22
15
 
23
- Inglorious Store eliminates the boilerplate entirely with an **entity-based architecture** inspired by game engines. Some of the patterns that power AAA games now power your state management.
16
+ - **Pre-rendered HTML** - Every page is built at compile time
17
+ - **Instant load times** - No waiting for server responses
18
+ - **CDN-ready** - Deploy anywhere static files are served
19
+ - **Perfect Lighthouse scores** - SEO and performance out of the box
24
20
 
25
- Game engines solved state complexity years ago — Inglorious Store brings those lessons to web development.
21
+ ### 🎯 Simple Architecture
26
22
 
27
- **Key benefits:**
23
+ - **No server required** - Pure static files
24
+ - **No complex build configs** - Convention over configuration
25
+ - **File-based routing** - Pages are just files in `src/pages/`
26
+ - **Entity-based state** - Same familiar patterns from @inglorious/web
28
27
 
29
- - Drop-in Redux replacement (same API with `react-redux`)
30
- - ✅ Entity-based state (manage multiple instances effortlessly)
31
- - ✅ No action creators, thunks, or slices
32
- - ✅ Predictable, testable, purely functional code
33
- - ✅ Built-in lifecycle events (`add`, `remove`)
34
- - ✅ 10x faster immutability than Redux Toolkit (Mutative vs Immer)
28
+ ### 🔥 Modern DX
35
29
 
36
- ---
30
+ - **Hot reload dev server** - See changes instantly
31
+ - **Lazy-loaded routes** - Code splitting automatically
32
+ - **lit-html hydration** - Interactive UI without the bloat
33
+ - **TypeScript ready** - Full type support (coming soon)
37
34
 
38
- ## Quick Comparison: Redux vs RTK vs Inglorious Store
35
+ ### 🚀 Production Ready
39
36
 
40
- ### Redux
37
+ - **Automatic code splitting** - Per-page bundles
38
+ - **Optimized builds** - Minified, tree-shaken output
39
+ - **Source maps** - Debug production like development
40
+ - **Error boundaries** - Graceful failure handling
41
41
 
42
- ```javascript
43
- // Action creators
44
- const addTodo = (text) => ({ type: "ADD_TODO", payload: text })
42
+ ---
45
43
 
46
- // Reducer
47
- const todosReducer = (state = [], action) => {
48
- switch (action.type) {
49
- case "ADD_TODO":
50
- return [...state, { id: Date.now(), text: action.payload }]
44
+ ## Quick Start
51
45
 
52
- case "OTHER_ACTION":
53
- // Handle other action
46
+ ### Installation
54
47
 
55
- default:
56
- return state
57
- }
58
- }
48
+ ```bash
49
+ npm install @inglorious/ssx @inglorious/web
50
+ ```
59
51
 
60
- // Store setup
61
- const store = configureStore({
62
- reducer: {
63
- work: todosReducer,
64
- personal: todosReducer,
65
- },
66
- })
52
+ ### Create Your First Site
67
53
 
68
- store.dispatch({ type: "ADD_TODO", payload: "Buy groceries" })
69
- store.dispatch({ type: "OTHER_ACTION" })
54
+ <!-- ```bash
55
+ npx @inglorious/create-app my-site --template ssx
56
+ cd my-site
57
+ npm run dev
70
58
  ```
71
59
 
72
- ### Redux Toolkit
60
+ Or manually: -->
73
61
 
74
62
  ```javascript
75
- const otherAction = createAction("app:otherAction")
76
-
77
- const todosSlice = createSlice({
78
- name: "todos",
79
- initialState: [],
80
- reducers: {
81
- addTodo: (state, action) => {
82
- state.push({ id: Date.now(), text: action.payload })
83
- },
63
+ // src/pages/index.js
64
+ import { html } from "@inglorious/web"
65
+
66
+ export const index = {
67
+ render() {
68
+ return html`
69
+ <div>
70
+ <h1>Welcome to SSX!</h1>
71
+ <p>This page was pre-rendered at build time.</p>
72
+ <nav>
73
+ <a href="/about">About</a>
74
+ </nav>
75
+ </div>
76
+ `
84
77
  },
85
- extraReducers: (builder) => {
86
- builder.addCase(otherAction, (state, action) => {
87
- // Handle external action
88
- })
89
- },
90
- })
78
+ }
91
79
 
92
- const store = configureStore({
93
- reducer: {
94
- work: todosSlice.reducer,
95
- personal: todosSlice.reducer,
80
+ export const metadata = {
81
+ title: "Home",
82
+ meta: {
83
+ description: "Welcome to our site",
84
+ "og:image": "/og-image.png",
96
85
  },
97
- })
98
-
99
- store.dispatch(slice.actions.addTodo("Buy groceries"))
100
- store.dispatch(otherAction())
86
+ }
101
87
  ```
102
88
 
103
- ### Inglorious Store
104
-
105
- ```javascript
106
- // Define entity types and their behavior
107
- const types = {
108
- todoList: {
109
- addTodo(entity, text) {
110
- entity.todos.push({ id: Date.now(), text })
111
- },
112
-
113
- otherAction(entity) {
114
- // Handle other action
115
- },
116
- },
117
- }
89
+ ### Development
118
90
 
119
- // Define initial entities
120
- const entities = {
121
- work: { type: "todoList", todos: [] },
122
- personal: { type: "todoList", todos: [] },
123
- }
91
+ ```bash
92
+ npm run dev
93
+ # Dev server at http://localhost:3000
94
+ ```
124
95
 
125
- // Create store
126
- const store = createStore({ types, entities })
96
+ ### Build
127
97
 
128
- store.dispatch({ type: "addTodo", payload: "Buy groceries" })
129
- store.dispatch({ type: "otherAction" })
98
+ ```bash
99
+ npm run build
100
+ # → Static site in dist/
101
+ ```
130
102
 
131
- // or, even better:
132
- store.notify("addTodo", "Buy groceries")
133
- store.notify("otherAction")
103
+ ### Deploy
134
104
 
135
- // same result, 10x simpler
105
+ ```bash
106
+ npm run preview
107
+ # → Preview production build
136
108
  ```
137
109
 
138
- **Key differences:**
110
+ Deploy `dist/` to:
139
111
 
140
- - No action creators
141
- - No switch statements or cases
142
- - No slice definitions with extraReducers
143
- - Define what each entity type can do
144
- - Add multiple instances by adding entities, not code
112
+ - **Vercel** - Zero config
113
+ - **Netlify** - Drop folder
114
+ - **GitHub Pages** - Push and done
115
+ - **Cloudflare Pages** - Instant edge
116
+ - **Any CDN** - It's just files!
145
117
 
146
118
  ---
147
119
 
148
- ## Core Concepts
120
+ ## Features
149
121
 
150
- ### 🎮 Entities and Types
122
+ ### �️ Sitemap & RSS Generation
151
123
 
152
- State consists of **entities** (instances) that have a **type** (behavior definition). Think of a type as a class and entities as instances:
124
+ SSX automatically generates `sitemap.xml` and `rss.xml` based on your pages. Configure them in `src/site.config.js`:
153
125
 
154
126
  ```javascript
155
- const types = {
156
- todoList: {
157
- addTodo(entity, text) {
158
- entity.todos.push({ id: Date.now(), text })
159
- },
160
- toggle(entity, id) {
161
- const todo = entity.todos.find((t) => t.id === id)
162
- if (todo) todo.completed = !todo.completed
163
- },
127
+ export default {
128
+ // Basic metadata
129
+ title: "My Awesome Site",
130
+ meta: {
131
+ description: "A site built with SSX",
132
+ "og:type": "website",
133
+ "og:site_name": "My Site",
164
134
  },
165
135
 
166
- settings: {
167
- setTheme(entity, theme) {
168
- entity.theme = theme
136
+ // Sitemap configuration
137
+ sitemap: {
138
+ hostname: "https://myblog.com",
139
+ filter: (page) => !["/admin", "/draft-*", "/test"].includes(page.pattern),
140
+ defaults: {
141
+ changefreq: "weekly",
142
+ priority: 0.5,
169
143
  },
170
144
  },
171
- }
172
145
 
173
- const entities = {
174
- workTodos: { type: "todoList", todos: [], priority: "high" },
175
- personalTodos: { type: "todoList", todos: [], priority: "low" },
176
- settings: { type: "settings", theme: "dark", language: "en" },
146
+ // RSS configuration
147
+ rss: {
148
+ title: "My Blog",
149
+ description: "Latest posts from my blog",
150
+ link: "https://myblog.com",
151
+ feedPath: "/feed.xml",
152
+ language: "en",
153
+ copyright: "© 2026 My Blog",
154
+ maxItems: 10,
155
+ filter: (page) => page.path.startsWith("/posts/"),
156
+ },
177
157
  }
178
158
  ```
179
159
 
180
- **Why this matters:**
160
+ Pages with a `published` date in metadata are included in RSS feeds.
161
+
162
+ ### �📁 File-Based Routing
181
163
 
182
- - Same behavior applies to all instances of that type
183
- - No need to write separate code for each instance
184
- - Your mental model matches your code structure
164
+ Your file structure defines your routes:
185
165
 
186
- ### 🔄 Event Handlers (Not Methods)
166
+ ```
167
+ src/pages/
168
+ ├── index.js → /
169
+ ├── about.js → /about
170
+ ├── blog.js → /blog
171
+ └── posts/
172
+ └── _slug.js → /posts/:slug
173
+ ```
187
174
 
188
- Even though it looks like types expose methods, they are actually **event handlers**, very similar to Redux reducers. There are a few differences though:
175
+ Dynamic routes use underscore prefix: `_id.js`, `_slug.js`, etc.
189
176
 
190
- 1. Just like RTK reducers, you can mutate the entity directly since event handlers are using an immutability library under the hood. Not Immer, but Mutative — which claims to be 10x faster than Immer.
177
+ ### ⚛️ Entity-Based State And Behavior
191
178
 
192
179
  ```javascript
193
- const types = {
194
- counter: {
195
- increment(counter) {
196
- counter.value++ // Looks like mutation, immutable in reality
197
- },
180
+ // src/pages/about.js
181
+ import { html } from "@inglorious/web"
182
+
183
+ export const about = {
184
+ click(entity) {
185
+ entity.name += "!"
186
+ },
187
+
188
+ render(entity, api) {
189
+ return html`<h1>
190
+ About
191
+ <span @click=${() => api.notify(`#${entity.id}:click`)}
192
+ >${entity.name}</span
193
+ >
194
+ </h1>`
198
195
  },
199
196
  }
200
197
  ```
201
198
 
202
- 2. Event handlers accept as arguments the current entity, the event payload, and an API object that exposes a few convenient methods:
203
-
204
199
  ```javascript
205
- const types = {
206
- counter: {
207
- increment(counter, value, api) {
208
- api.getEntities() // access the whole state in read-only mode
209
- api.getEntity(id) // access some other entity in read-only mode
210
- api.notify(type, payload) // similar to dispatch. Yes, you can dispatch inside of a reducer!
211
- api.dispatch(action) // optional, if you prefer Redux-style dispatching
212
- },
200
+ // src/entities.js
201
+ export const entities = {
202
+ about: {
203
+ type: "about",
204
+ name: "Us",
213
205
  },
214
206
  }
215
207
  ```
216
208
 
217
- ---
218
-
219
- ## Installation & Setup
220
-
221
- The Inglorious store, just like Redux, can be used standalone. However, it's commonly used together with component libraries such as React.
209
+ ### 🔄 Data Loading
222
210
 
223
- ### Basic Setup with `react-redux`
211
+ Load data at build time with the `load` export:
224
212
 
225
213
  ```javascript
226
- import { createStore } from "@inglorious/store"
227
- import { Provider, useSelector, useDispatch } from "react-redux"
228
-
229
- // 1. Define entity types
230
- const types = {
231
- counter: {
232
- increment(counter) {
233
- counter.value++
234
- },
235
- decrement(counter) {
236
- counter.value--
237
- },
214
+ // src/pages/blog.js
215
+ import { html } from "@inglorious/web"
216
+
217
+ export const blog = {
218
+ render(entity) {
219
+ return html`
220
+ <h1>Blog Posts</h1>
221
+ <ul>
222
+ ${entity.posts?.map(
223
+ (post) => html`
224
+ <li>
225
+ <a href="/posts/${post.id}">${post.title}</a>
226
+ </li>
227
+ `,
228
+ )}
229
+ </ul>
230
+ `
238
231
  },
239
232
  }
240
233
 
241
- // 2. Define initial entities
242
- const entities = {
243
- counter1: { type: "counter", value: 0 },
244
- }
245
-
246
- // 3. Create the store
247
- const store = createStore({ types, entities })
248
-
249
- // 4. Provide the store with react-redux
250
- function App() {
251
- return (
252
- <Provider store={store}>
253
- <Counter />
254
- </Provider>
255
- )
234
+ // SSR: Load data during build
235
+ export async function load(entity) {
236
+ const response = await fetch("https://api.example.com/posts")
237
+ entity.posts = await response.json()
256
238
  }
257
239
 
258
- // 5. Wire components to the store
259
- function Counter() {
260
- const dispatch = useDispatch()
261
- const count = useSelector((state) => state.counter1.value)
262
-
263
- return (
264
- <div>
265
- <p>{count}</p>
266
- <button onClick={() => dispatch({ type: "increment" })}>+</button>
267
- <button onClick={() => dispatch({ type: "decrement" })}>-</button>
268
- </div>
269
- )
270
- }
240
+ export const title = "Blog"
271
241
  ```
272
242
 
273
- ### With `@inglorious/react-store` (Recommended)
274
-
275
- For React applications, `@inglorious/react-store` provides a set of hooks and a Provider that are tightly integrated with the store. It's a lightweight wrapper around `react-redux` that offers a more ergonomic API.
276
-
277
- ```javascript
278
- import { createStore } from "@inglorious/store"
279
- import { createReactStore } from "@inglorious/react-store"
243
+ The `load` function runs on the server during build. Data is serialized into the HTML and available immediately on the client.
280
244
 
281
- const store = createStore({ types, entities })
245
+ ### 🎨 Dynamic Routes with `staticPaths`
282
246
 
283
- export const { Provider, useSelector, useNotify } = createReactStore(store)
247
+ Generate multiple pages from data:
284
248
 
285
- function App() {
286
- return (
287
- // No store prop needed!
288
- <Provider>
289
- <Counter />
290
- </Provider>
291
- )
249
+ ```javascript
250
+ // src/pages/posts/_slug.js
251
+ import { html } from "@inglorious/web"
252
+
253
+ export const post = {
254
+ render(entity) {
255
+ return html`
256
+ <article>
257
+ <h1>${entity.post.title}</h1>
258
+ <div>${entity.post.body}</div>
259
+ </article>
260
+ `
261
+ },
292
262
  }
293
263
 
294
- function Counter() {
295
- const notify = useNotify() // less verbose than dispatch
296
- const count = useSelector((state) => state.counter1.value)
297
-
298
- return (
299
- <div>
300
- <p>{count}</p>
301
- <button onClick={() => notify("increment")}>+</button> // simplified
302
- syntax
303
- <button onClick={() => notify("decrement")}>-</button>
304
- </div>
264
+ // Load data for a specific post
265
+ export async function load(entity, page) {
266
+ const response = await fetch(
267
+ `https://api.example.com/posts/${page.params.slug}`,
305
268
  )
269
+ entity.post = await response.json()
306
270
  }
307
- ```
308
-
309
- The package is fully typed, providing a great developer experience with TypeScript.
310
271
 
311
- ---
312
-
313
- ## Core Features
314
-
315
- ### 🎮 Entity-Based State
272
+ // Tell SSX which pages to generate
273
+ export async function staticPaths() {
274
+ const response = await fetch(`https://api.example.com/posts`)
275
+ const posts = await response.json()
316
276
 
317
- The real power: add entities dynamically without code changes.
318
-
319
- **Redux/RTK:** To manage three counters, you can reuse a reducer. But what if you want to add a new counter at runtime? Your best option is probably to reshape the whole state.
320
-
321
- ```javascript
322
- // The original list of counters:
323
- const store = configureStore({
324
- reducer: {
325
- counter1: counterReducer,
326
- counter2: counterReducer,
327
- counter3: counterReducer,
328
- },
329
- })
277
+ return posts.map((post) => ({
278
+ params: { slug: post.slug },
279
+ path: `/posts/${post.slug}`,
280
+ }))
281
+ }
330
282
 
331
- // becomes:
332
- const store = configureStore({
333
- reducer: {
334
- counters: countersReducer,
283
+ export const metadata = (entity) => ({
284
+ title: entity.post.title ?? "Post",
285
+ meta: {
286
+ description: entity.post.excerpt,
335
287
  },
336
288
  })
337
-
338
- // with extra actions to manage adding/removing counters:
339
- store.dispatch({ type: "addCounter", payload: "counter4" })
340
289
  ```
341
290
 
342
- **Inglorious Store** makes it trivial:
291
+ ### 📄 Page Metadata
292
+
293
+ Export metadata for HTML `<head>`. The `metadata` export can be a plain object or a function:
343
294
 
344
295
  ```javascript
345
- const types = {
346
- counter: {
347
- increment(entity) {
348
- entity.value++
349
- },
296
+ export const index = {
297
+ render() {
298
+ return html`<h1>Home</h1>`
350
299
  },
351
300
  }
352
301
 
353
- const entities = {
354
- counter1: { type: "counter", value: 0 },
355
- counter2: { type: "counter", value: 0 },
356
- counter3: { type: "counter", value: 0 },
302
+ // Static metadata
303
+ export const metadata = {
304
+ title: "My Site",
305
+ meta: {
306
+ description: "An awesome static site",
307
+ "og:image": "/og-image.png",
308
+ },
357
309
  }
358
310
 
359
- store.notify("add", { id: "counter4", type: "counter", value: 0 })
360
- ```
361
-
362
- Inglorious Store has a few built-in events that you can use:
363
-
364
- - `add`: adds a new entity to the state. Triggers a `create` lifecycle event.
365
- - `remove`: removes an entity from the state. Triggers a `destroy` lifecycle event.
366
-
367
- The lifecycle events can be used to define event handlers similar to constructor and destructor methods in OOP:
368
-
369
- > Remember: events are broadcast to all entities, just like with reducers! Each handler decides if it should respond. More on that in the section below.
370
-
371
- ```javascript
372
- const types = {
373
- counter: {
374
- create(entity, id) {
375
- if (entity.id !== id) return // "are you talking to me?"
376
- entity.createdAt = Date.now()
377
- },
378
-
379
- destroy(entity, id) {
380
- if (entity.id !== id) return // "are you talking to me?"
381
- entity.destroyedAt = Date.now()
382
- },
311
+ // Or dynamic metadata (uses entity data)
312
+ export const metadata = (entity) => ({
313
+ title: `${entity.user.name}'s Profile`,
314
+ meta: {
315
+ description: entity.user.bio,
316
+ "og:image": entity.user.avatar,
383
317
  },
384
- }
318
+ })
385
319
  ```
386
320
 
387
- ### 🔊 Event Broadcasting
321
+ ### 🔥 Client-Side Hydration
388
322
 
389
- Events are broadcast to all entities via pub/sub. Every entity handler receives every event of that type, just like it does in Redux.
323
+ Pages hydrate automatically with lit-html. Interactivity works immediately:
390
324
 
391
325
  ```javascript
392
- const types = {
393
- todoList: {
394
- taskCompleted(entity, taskId) {
395
- const task = entity.tasks.find((t) => t.id === taskId)
396
- if (task) task.completed = true
397
- },
398
- },
399
- stats: {
400
- taskCompleted(entity, taskId) {
401
- entity.completedCount++
402
- },
403
- },
404
- notifications: {
405
- taskCompleted(entity, taskId) {
406
- entity.messages.push("Nice! Task completed.")
407
- },
326
+ export const counter = {
327
+ click(entity) {
328
+ entity.count++
408
329
  },
409
- }
410
-
411
- // One notify call, all three entity types respond
412
- store.notify("taskCompleted", "task123")
413
- ```
414
330
 
415
- In RTK, such action would have be to be defined outside of the slice with `createAction` and then processed with the builder callback notation inside of the `extraReducers` section.
416
-
417
- - What if you want to notify the event only to entities of one specific type? Define an event handler for that event only on that type.
418
- - What if you want to notify the event only on one entity of that type? Add an if that checks if the entity should be bothered or not by it.
419
-
420
- ```javascript
421
- const types = {
422
- todoList: {
423
- toggle(entity, id) {
424
- // This runs for EVERY todoList entity, but only acts if it's the right one
425
- if (entity.id !== id) return
426
-
427
- const todo = entity.todos.find((t) => t.id === id)
428
- if (todo) todo.completed = !todo.completed
429
- },
331
+ render(entity, api) {
332
+ return html`
333
+ <div>
334
+ <p>Count: ${entity.count}</p>
335
+ <button @click=${() => api.notify(`#${entity.id}:click`)}>
336
+ Increment
337
+ </button>
338
+ </div>
339
+ `
430
340
  },
431
341
  }
432
-
433
- // Broadcast to all todo lists
434
- store.notify("toggle", "todo1")
435
- // Each list's toggle handler runs; only the one with todo1 actually updates
436
342
  ```
437
343
 
438
- ### Async Operations
344
+ The HTML is pre-rendered on the server. When JavaScript loads, lit-html hydrates the existing DOM and wires up event handlers. No flash of unstyled content, no duplicate rendering.
439
345
 
440
- In **Redux/RTK**, logic should be written inside pure functions as much as possible — specifically in reducers, not action creators. But what if I need to access some other part of the state that is not visible to the reducer? What if I need to combine async behavior with sync behavior? This is where the choice of "where does my logic live?" matters.
346
+ ### 🧭 Client-Side Navigation
441
347
 
442
- In **Inglorious Store:** your event handlers can be async, and you get deterministic behavior automatically. Inside an async handler, you can access other parts of state (read-only), and you can trigger other events via `api.notify()`. Even if we give up on some purity, everything still maintains predictability because of the underlying **event queue**:
348
+ After hydration, navigation is instant:
443
349
 
444
350
  ```javascript
445
- const types = {
446
- todoList: {
447
- async loadTodos(entity, payload, api) {
448
- try {
449
- entity.loading = true
450
- const { name } = api.getEntity("user")
451
- const response = await fetch(`/api/todos/${name}`)
452
- const data = await response.json()
453
- api.notify("todosLoaded", todos)
454
- } catch (error) {
455
- api.notify("loadFailed", error.message)
456
- }
457
- },
351
+ // Links navigate without page reload
352
+ ;<a href="/about">About</a> // Client-side routing
458
353
 
459
- todosLoaded(entity, todos) {
460
- entity.todos = todos
461
- entity.loading = false
462
- },
354
+ // Programmatic navigation
355
+ api.notify("navigate", "/posts")
463
356
 
464
- loadFailed(entity, error) {
465
- entity.error = error
466
- entity.loading = false
467
- },
468
- },
469
- }
357
+ // With options
358
+ api.notify("navigate", {
359
+ to: "/posts/123",
360
+ replace: true,
361
+ })
470
362
  ```
471
363
 
472
- Notice: you don't need pending/fulfilled/rejected actions. You stay in control of the flow — no hidden action chains. The `api` object passed to handlers provides:
473
-
474
- - **`api.getEntities()`** - read entire state
475
- - **`api.getEntity(id)`** - read one entity
476
- - **`api.notify(type, payload)`** - trigger other events (queued, not immediate)
477
- - **`api.dispatch(action)`** - optional, if you prefer Redux-style dispatching
478
- - **`api.getTypes()`** - access type definitions (mainly for middleware/plugins)
479
- - **`api.getType(typeName)`** - access type definition (mainly for overrides)
364
+ Routes are lazy-loaded on demand, keeping initial bundle size small.
480
365
 
481
- All events triggered via `api.notify()` enter the queue and process together, maintaining predictability and testability.
482
-
483
- ### 🧪 Testing
484
-
485
- Event handlers are pure functions (or can be treated as such), making them easy to test in isolation, much like Redux reducers. The `@inglorious/store/test` module provides utility functions to make this even simpler.
366
+ ---
486
367
 
487
- #### `trigger(entity, handler, payload, api?)`
368
+ ## CLI
488
369
 
489
- The `trigger` function executes an event handler on a single entity and returns the new state and any events that were dispatched.
370
+ SSX provides a simple CLI for building and developing:
490
371
 
491
- ```javascript
492
- import { trigger } from "@inglorious/store/test"
372
+ ### `ssx build`
493
373
 
494
- // Define your entity handler
495
- function increment(entity, payload, api) {
496
- entity.value += payload.amount
497
- if (entity.value > 100) {
498
- api.notify("overflow", { id: entity.id })
499
- }
500
- }
374
+ Builds your static site:
501
375
 
502
- // Test it
503
- const { entity, events } = trigger(
504
- { type: "counter", id: "counter1", value: 99 },
505
- increment,
506
- { amount: 5 },
507
- )
376
+ ```bash
377
+ ssx build [options]
508
378
 
509
- expect(entity.value).toBe(104)
510
- expect(events).toEqual([{ type: "overflow", payload: { id: "counter1" } }])
379
+ Options:
380
+ -c, --config <file> Config file (default: "site.config.js")
381
+ -r, --root <dir> Source root directory (default: "src")
382
+ -o, --out <dir> Output directory (default: "dist")
383
+ -i, --incremental Enable incremental builds (default: true)
384
+ -f, --force Force clean build, ignore cache
511
385
  ```
512
386
 
513
- #### `createMockApi(entities)`
387
+ ### `ssx dev`
514
388
 
515
- If your handler needs to interact with other entities via the `api`, you can create a mock API. This is useful for testing handlers that read from other parts of the state.
389
+ Starts development server with hot reload:
516
390
 
517
- ```javascript
518
- import { createMockApi, trigger } from "@inglorious/store/test"
391
+ ```bash
392
+ ssx dev [options]
519
393
 
520
- // Create a mock API with some initial entities
521
- const api = createMockApi({
522
- counter1: { type: "counter", value: 10 },
523
- counter2: { type: "counter", value: 20 },
524
- })
394
+ Options:
395
+ -c, --config <file> Config file (default: "site.config.js")
396
+ -r, --root <dir> Source root directory (default: "src")
397
+ -p, --port <port> Dev server port (default: 3000)
398
+ ```
525
399
 
526
- // A handler that copies a value from another entity
527
- function copyValue(entity, payload, api) {
528
- const source = api.getEntity(payload.sourceId)
529
- entity.value = source.value
400
+ ### Package.json Scripts
401
+
402
+ ```json
403
+ {
404
+ "scripts": {
405
+ "dev": "ssx dev",
406
+ "build": "ssx build",
407
+ "preview": "pnpm dlx serve dist"
408
+ }
530
409
  }
410
+ ```
531
411
 
532
- // Trigger the handler with the custom mock API
533
- const { entity } = trigger(
534
- { type: "counter", id: "counter2", value: 20 },
535
- copyValue,
536
- { sourceId: "counter1" },
537
- api,
538
- )
412
+ ---
413
+
414
+ ## Project Structure
539
415
 
540
- expect(entity.value).toBe(10)
416
+ ```
417
+ my-site/
418
+ ├── src/
419
+ │ ├── pages/ # File-based routes
420
+ │ │ ├── index.js # Home page
421
+ │ │ ├── about.js # About page
422
+ │ │ └── posts/
423
+ │ │ ├── index.js # /posts
424
+ │ │ └── _id.js # /posts/:id
425
+ │ ├── entities.js # Entity definitions
426
+ │ └── types/ # Custom entity types (optional)
427
+ ├── dist/ # Build output
428
+ ├── package.json
429
+ └── site.config.js # Site configuration
541
430
  ```
542
431
 
543
- The mock API provides:
432
+ ---
544
433
 
545
- - `getEntities()`: Returns all entities (frozen).
546
- - `getEntity(id)`: Returns a specific entity by ID (frozen).
547
- - `dispatch(event)`: Records an event for later assertions.
548
- - `notify(type, payload)`: A convenience wrapper around `dispatch`.
549
- - `getEvents()`: Returns all events that were dispatched.
434
+ ## Comparison to Other Tools
550
435
 
551
- ### 🌍 Systems for Global Logic
436
+ | Feature | SSX | Next.js (SSG) | Astro | Eleventy |
437
+ | ----------------------- | ----------- | ------------- | ------ | -------- |
438
+ | Pre-rendered HTML | ✅ | ✅ | ✅ | ✅ |
439
+ | Client hydration | ✅ lit-html | ✅ React | ✅ Any | ❌ |
440
+ | Client routing | ✅ | ✅ | ✅ | ❌ |
441
+ | Lazy loading | ✅ | ✅ | ✅ | ❌ |
442
+ | Entity-based state | ✅ | ❌ | ❌ | ❌ |
443
+ | No compilation required | ✅ | ❌ | ❌ | ✅ |
444
+ | Zero config | ✅ | ❌ | ❌ | ❌ |
445
+ | Framework agnostic | ❌ | ❌ | ✅ | ✅ |
552
446
 
553
- When you need to coordinate updates across multiple entities (not just respond to individual events), use systems. Systems run after all entity handlers for the same event, ensuring global consistency, and have write access to the entire state. This concept is the 'S' in the ECS Architecture (Entity-Component-System)!
447
+ SSX is perfect if you:
554
448
 
555
- ```javascript
556
- const systems = [
557
- {
558
- taskCompleted(state, taskId) {
559
- // Read from multiple todo lists
560
- const allTodos = Object.values(state)
561
- .filter((e) => e.type === "todoList")
562
- .flatMap((e) => e.todos)
563
-
564
- // Update global stats
565
- state.stats.total = allTodos.length
566
- state.stats.completed = allTodos.filter((t) => t.completed).length
567
- },
568
- },
569
- ]
449
+ - Want static site performance
450
+ - Love entity-based architecture
451
+ - Prefer convention over configuration
452
+ - Need full client-side interactivity
453
+ - Don't want React/Vue lock-in
570
454
 
571
- const store = createStore({ types, entities, systems })
572
- ```
455
+ ---
573
456
 
574
- Systems receive the entire state and can modify any entity. They're useful for cross-cutting concerns, maintaining derived state, or coordinating complex state updates that can't be expressed as individual entity handlers.
457
+ ## Advanced Usage
575
458
 
576
- ### 🔗 Behavior Composition
459
+ ### Site Configuration
577
460
 
578
- A type can be a single behavior object, or an array of behaviors.
461
+ Customize SSX behavior in `src/site.config.js`:
579
462
 
580
463
  ```javascript
581
- // single-behavior type
582
- const counter = {
583
- increment(entity) {
584
- entity.value++
464
+ export default {
465
+ // Basic metadata
466
+ lang: "en",
467
+ charset: "UTF-8",
468
+ title: "My Awesome Site",
469
+ meta: {
470
+ description: "A site built with SSX",
471
+ "og:type": "website",
585
472
  },
586
473
 
587
- decrement(entity) {
588
- entity.value--
474
+ // Global assets
475
+ styles: ["./styles/reset.css", "./styles/theme.css"],
476
+ scripts: ["./scripts/analytics.js"],
477
+
478
+ // Build options
479
+ basePath: "/",
480
+ rootDir: "src",
481
+ outDir: "dist",
482
+ publicDir: "public",
483
+ favicon: "/favicon.ico",
484
+
485
+ // Router config
486
+ router: {
487
+ trailingSlash: false,
488
+ scrollBehavior: "smooth",
589
489
  },
590
- }
591
490
 
592
- // multiple behavior type
593
- const resettableCounter = [
594
- counter,
595
- {
596
- reset(entity) {
597
- entity.value = 0
491
+ // Vite config passthrough
492
+ vite: {
493
+ server: {
494
+ port: 3000,
495
+ open: true,
598
496
  },
599
497
  },
600
- ]
601
- ```
602
-
603
- A behavior is defined as either an object with event handlers, or a function that takes a type and returns an enhanced behavior (decorator pattern):
604
498
 
605
- ```javascript
606
- // Base behavior
607
- const resettable = {
608
- submit(entity, value) {
609
- entity.value = ""
499
+ // Build hooks
500
+ hooks: {
501
+ beforeBuild: async (config) => console.log("Starting build..."),
502
+ afterBuild: async (result) => console.log(`Built ${result.pages} pages`),
610
503
  },
611
504
  }
612
-
613
- // Function that wraps and enhances a behavior
614
- const validated = (type) => ({
615
- submit(entity, value, api) {
616
- if (!value.trim()) return
617
- type.submit?.(entity, value, api) // remember to always pass all args!
618
- },
619
- })
620
-
621
- // Another wrapper
622
- const withLoading = (type) => ({
623
- submit(entity, value, api) {
624
- entity.loading = true
625
- type.submit?.(entity, value, api)
626
- entity.loading = false
627
- },
628
- })
629
-
630
- // Compose them together to form a type
631
- const form = [resettable, validated, withLoading]
632
505
  ```
633
506
 
634
- When multiple behaviors define the same event, they all run in order. This allows you to build middleware-like patterns: validation, logging, error handling, loading states, etc.
635
-
636
- ### ⏱️ Batched Mode
507
+ ### Environment Variables
637
508
 
638
- The Inglorious Store features an **event queue**. In the default `auto` update mode, each notified event will trigger and update of the state (same as Redux). But in `manual` update mode, you can process multiple events together before re-rendering:
509
+ Use Vite's environment variables:
639
510
 
640
511
  ```javascript
641
- const store = createStore({ types, entities, updateMode: "manual" })
512
+ // Access in your code
513
+ const apiUrl = import.meta.env.VITE_API_URL
642
514
 
643
- // add events to the event queue
644
- store.notify("playerMoved", { x: 100, y: 50 })
645
- store.notify("enemyAttacked", { damage: 10 })
646
- store.notify("particleCreated", { type: "explosion" })
647
-
648
- // process them all in batch
649
- store.update()
515
+ // .env file
516
+ VITE_API_URL=https://api.example.com
650
517
  ```
651
518
 
652
- Instead of re-rendering after each event, you can batch them and re-render once. This is what powers high-performance game engines and smooth animations.
653
-
654
- ---
655
-
656
- ## Comparison with Other State Libraries
657
-
658
- | Feature | Redux | RTK | Zustand | Jotai | Pinia | MobX | Inglorious Store |
659
- | ------------------------- | ------------ | ------------ | ---------- | ---------- | ---------- | ---------- | ---------------- |
660
- | **Boilerplate** | 🔴 High | 🟡 Medium | 🟢 Low | 🟢 Low | 🟡 Medium | 🟢 Low | 🟢 Low |
661
- | **Multiple instances** | 🔴 Manual | 🔴 Manual | 🔴 Manual | 🔴 Manual | 🟡 Medium | 🟡 Medium | 🟢 Built-in |
662
- | **Lifecycle events** | 🔴 No | 🔴 No | 🔴 No | 🔴 No | 🔴 No | 🔴 No | 🟢 Yes |
663
- | **Async logic placement** | 🟡 Thunks | 🟡 Complex | 🟢 Free | 🟢 Free | 🟢 Free | 🟢 Free | 🟢 In handlers |
664
- | **Redux DevTools** | 🟢 Yes | 🟢 Yes | 🟡 Partial | 🟡 Partial | 🟡 Partial | 🟢 Yes | 🟢 Yes |
665
- | **Time-travel debugging** | 🟢 Yes | 🟢 Yes | 🔴 No | 🔴 No | 🔴 No | 🟡 Limited | 🟢 Yes |
666
- | **Testability** | 🟢 Excellent | 🟢 Excellent | 🟡 Good | 🟡 Good | 🟡 Good | 🟡 Medium | 🟢 Excellent |
667
- | **Immutability** | 🔴 Manual | 🟢 Immer | 🔴 Manual | 🔴 Manual | 🔴 Manual | 🔴 Manual | 🟢 Mutative |
668
-
669
- ---
670
-
671
- ## API Reference
519
+ ### Custom 404 Page
672
520
 
673
- ### `createStore(options)`
521
+ Create a fallback route:
674
522
 
675
523
  ```javascript
676
- const store = createStore({
677
- types, // Object: entity type definitions
678
- entities, // Object: initial entities
679
- systems, // Array (optional): global state handlers
680
- updateMode, // String (optional): 'auto' (default) or 'manual'
681
- })
682
- ```
683
-
684
- **Returns:** A Redux-compatible store
685
-
686
- ### Types Definition
524
+ // src/pages/404.js
525
+ export const notFound = {
526
+ render() {
527
+ return html`
528
+ <div>
529
+ <h1>404 - Page Not Found</h1>
530
+ <a href="/">Go Home</a>
531
+ </div>
532
+ `
533
+ },
534
+ }
687
535
 
688
- ```javascript
689
- const types = {
690
- entityType: [
691
- // Behavior objects
692
- {
693
- eventName(entity, payload, api) {
694
- entity.value = payload
695
- api.notify("otherEvent", data)
696
- },
697
- },
698
- // Behavior functions (decorators)
699
- (behavior) => ({
700
- eventName(entity, payload, api) {
701
- // Wrap the behavior
702
- behavior.eventName?.(entity, payload, api)
703
- },
704
- }),
705
- ],
536
+ export const metadata = {
537
+ title: "404",
706
538
  }
707
539
  ```
708
540
 
709
- ### Event Handler API
710
-
711
- Each handler receives three arguments:
712
-
713
- - **`entity`** - the entity instance (mutate freely, immutability guaranteed)
714
- - **`payload`** - data passed with the event
715
- - **`api`** - access to store methods:
716
- - `getEntities()` - entire state (read-only)
717
- - `getEntity(id)` - single entity (read-only)
718
- - `notify(type, payload)` - trigger other events
719
- - `dispatch(action)` - optional, if you prefer Redux-style dispatching
720
- - `getTypes()` - type definitions (for middleware)
721
- - `getType(typeName)` - type definition (for overriding)
722
- - `setType(typeName, type)` - change the behavior of a type
723
-
724
- ### Built-in Events
725
-
726
- - **`create(entity)`** - triggered when entity added via `add` event, visible only to that entity
727
- - **`destroy(entity)`** - triggered when entity removed via `remove` event, visible only to that entity
728
-
729
- ### Notify vs Dispatch
730
-
731
- Both work (`dispatch` is provided just for Redux compatibility), but `notify` is cleaner (and uses `dispatch` internally):
541
+ Register it in your router:
732
542
 
733
543
  ```javascript
734
- store.notify("eventName", payload)
735
- store.dispatch({ type: "eventName", payload }) // Redux-compatible alternative
544
+ // src/entities.js
545
+ import { setRoutes } from "@inglorious/web/router"
546
+
547
+ setRoutes({
548
+ // ... other routes
549
+ "*": "notFound", // Fallback
550
+ })
736
551
  ```
737
552
 
738
- ### 🧩 Type Safety
553
+ ### Incremental Builds
739
554
 
740
- Inglorious Store is written in JavaScript but comes with powerful TypeScript support out of the box, allowing for a fully type-safe experience similar to Redux Toolkit, but with less boilerplate.
555
+ SSX enables incremental builds by default. Only changed pages are rebuilt, dramatically speeding up your build process:
741
556
 
742
- You can achieve strong type safety by defining an interface for your `types` configuration. This allows you to statically define the shape of your entity handlers, ensuring that all required handlers are present and correctly typed.
557
+ ```bash
558
+ ssx build
559
+ # Only changed pages are rebuilt
743
560
 
744
- Here’s how you can set it up for a TodoMVC-style application:
561
+ ssx build --force
562
+ # Force a clean rebuild of all pages
563
+ ```
745
564
 
746
- **1. Define Your Types**
565
+ Incremental builds respect your page dependencies and invalidate cache when dependencies change.
747
566
 
748
- First, create an interface that describes your entire `types` configuration. This interface will enforce the structure of your event handlers.
567
+ ---
749
568
 
750
- ```typescript
751
- // src/store/types.ts
752
- import type {
753
- FormEntity,
754
- ListEntity,
755
- FooterEntity,
756
- // ... other payload types
757
- } from "../../types"
569
+ ## API Reference
758
570
 
759
- // Define the static shape of the types configuration
760
- interface TodoListTypes {
761
- form: {
762
- inputChange: (entity: FormEntity, value: string) => void
763
- formSubmit: (entity: FormEntity) => void
764
- }
765
- list: {
766
- formSubmit: (entity: ListEntity, value: string) => void
767
- toggleClick: (entity: ListEntity, id: number) => void
768
- // ... other handlers
769
- }
770
- footer: {
771
- filterClick: (entity: FooterEntity, id: string) => void
772
- }
773
- }
571
+ ### Build API
774
572
 
775
- export const types: TodoListTypes = {
776
- form: {
777
- inputChange(entity, value) {
778
- entity.value = value
779
- },
780
- formSubmit(entity) {
781
- entity.value = ""
782
- },
783
- },
784
- // ... other type implementations
785
- }
573
+ ```javascript
574
+ import { build } from "@inglorious/ssx/build"
575
+
576
+ await build({
577
+ rootDir: "src",
578
+ outDir: "dist",
579
+ configFile: "site.config.js",
580
+ incremental: true,
581
+ clean: false,
582
+ })
786
583
  ```
787
584
 
788
- With `TodoListTypes`, TypeScript will throw an error if you forget a handler (e.g., `formSubmit`) or if its signature is incorrect.
789
-
790
- **2. Create the Store**
585
+ ### Dev Server API
791
586
 
792
- When creating your store, you'll pass the `types` object. To satisfy the store's generic `TypesConfig`, you may need to use a double cast (`as unknown as`). This is a safe and intentional way to bridge your specific, statically-checked configuration with the store's more generic type.
793
-
794
- ```typescript
795
- // src/store/index.ts
796
- import { createStore, type TypesConfig } from "@inglorious/store"
797
- import { types } from "./types"
798
- import type { TodoListEntity, TodoListState } from "../../types"
587
+ ```javascript
588
+ import { dev } from "@inglorious/ssx/dev"
799
589
 
800
- export const store = createStore<TodoListEntity, TodoListState>({
801
- types: types as unknown as TypesConfig<TodoListEntity>,
802
- // ... other store config
590
+ await dev({
591
+ rootDir: "src",
592
+ port: 3000,
593
+ configFile: "site.config.js",
803
594
  })
804
595
  ```
805
596
 
806
- **3. Enjoy Full Type Safety**
807
-
808
- Now, your store is fully type-safe. The hooks provided by `@inglorious/react-store` will also be correctly typed.
809
-
810
597
  ---
811
598
 
812
- ## Use Cases
599
+ <!-- ## Examples
813
600
 
814
- ### Perfect For
601
+ Check out these example projects:
815
602
 
816
- - 🎮 Apps with multiple instances of the same entity type
817
- - 🎯 Real-time collaborative features
818
- - Complex state coordination and async operations
819
- - 📊 High-frequency updates (animations, games)
820
- - 🔄 Undo/redo, time-travel debugging
603
+ - **[Basic Blog](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/ssx-blog)** - Simple blog with posts
604
+ - **[Documentation Site](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/ssx-docs)** - Multi-page docs
605
+ - **[E-commerce](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/ssx-shop)** - Product catalog
606
+ - **[Portfolio](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/ssx-portfolio)** - Personal portfolio
821
607
 
822
- ### Still Great For
608
+ --- -->
823
609
 
824
- - Any Redux use case (true drop-in replacement)
825
- - Migration path from Redux (keep using react-redux)
610
+ ## Roadmap
826
611
 
827
- ---
612
+ - [ ] TypeScript support
613
+ - [ ] Image optimization
614
+ - [ ] API routes (serverless functions)
615
+ - [ ] MDX support
616
+ - [ ] i18n helpers
828
617
 
829
- ### Demos
618
+ ---
830
619
 
831
- Check out the following demos to see the Inglorious Store in action on real-case scenarios:
620
+ ## Philosophy
832
621
 
833
- **React Examples:**
622
+ SSX embraces the philosophy of [@inglorious/web](https://www.npmjs.com/package/@inglorious/web):
834
623
 
835
- - **[React TodoMVC](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/react-todomvc)** - An (ugly) clone of Kent Dodds' [TodoMVC](https://todomvc.com/) experiments, showing the full compatibility with react-redux and The Redux DevTools.
836
- - **[React TodoMVC-CS](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/react-todomvc-cs)** - A client-server version of the TodoMVC, which showcases the use of `notify` as a cleaner alternative to `dispatch` and async event handlers.
837
- - **[React TodoMVC-RT](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/react-todomvc-rt)** - A "multiplayer" version, in which multiple clients are able to synchronize through a real-time server.
838
- - **[React TodoMVC-TS](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/react-todomvc-ts)** - A typesafe version of the base TodoMVC.
624
+ - **Simplicity over cleverness** - Obvious beats clever
625
+ - **Convention over configuration** - Sensible defaults
626
+ - **Predictability over magic** - Explicit is better than implicit
627
+ - **Standards over abstractions** - Use the platform
839
628
 
840
- For more demos and examples with `@inglorious/web`, see the [`@inglorious/web` README](../web/README.md).
629
+ Static site generation should be simple. SSX makes it simple.
841
630
 
842
631
  ---
843
632
 
844
- ## Frequently Unsolicited Complaints (FUCs)
845
-
846
- It's hard to accept the new, especially on Reddit. Here are the main objections to the Inglorious Store.
633
+ ## Contributing
847
634
 
848
- **"This is not ECS."**
635
+ Contributions are welcome! Please read our [Contributing Guidelines](../../CONTRIBUTING.md) first.
849
636
 
850
- It's not. The Inglorious Store is _inspired_ by ECS, but doesn't strictly follow ECS. Heck, not even the major game engines out there follow ECS by the book!
637
+ ---
851
638
 
852
- Let's compare the two:
639
+ ## License
853
640
 
854
- | ECS Architecture | Inglorious Store |
855
- | ------------------------------------- | -------------------------------------- |
856
- | Entities are ids | Entities have an id |
857
- | Components are pure, consecutive data | Entities are pure bags of related data |
858
- | Data and behavior are separated | Data and behavior are separated |
859
- | Systems operate on the whole state | Systems operate on the whole state |
860
- | Usually written in an OOP environment | Written in an FP environment |
641
+ **MIT License** - Free and open source
861
642
 
862
- **"This is not FP."**
643
+ Created by [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
863
644
 
864
- It looks like it's not, and that's a feature. If you're used to classes and instances, the Inglorious Store will feel natural to you. Even behavior composition looks like inheritance, but it's actually function composition. The same [Three Principles](https://redux.js.org/understanding/thinking-in-redux/three-principles) that describe Redux are applied here (with some degree of freedom on function purity).
645
+ ---
865
646
 
866
- **"This is not Data-Oriented Design."**
647
+ ## Related Packages
867
648
 
868
- It's not. Please grep this README and count how many occurrences of DoD you can find. This is not [Data-Oriented Design](https://en.wikipedia.org/wiki/Data-oriented_design), which is related to low-level CPU cache optimization. It's more similar to [Data-Driven Programming](https://en.wikipedia.org/wiki/Data-driven_programming), which is related to separating data and behavior. The Inglorious Store separates behavior in... behaviors (grouped into so-called types), while the data is stored in plain objects called entities.
649
+ - [@inglorious/web](https://www.npmjs.com/package/@inglorious/web) - Entity-based web framework
650
+ - [@inglorious/store](https://www.npmjs.com/package/@inglorious/store) - State management
651
+ - [@inglorious/engine](https://www.npmjs.com/package/@inglorious/engine) - Game engine
869
652
 
870
653
  ---
871
654
 
872
- ## License
873
-
874
- MIT © [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
655
+ ## Support
875
656
 
876
- Free to use, modify, and distribute.
657
+ - 📖 [Documentation](https://inglorious-engine.vercel.app)
658
+ - 💬 [Discord Community](https://discord.gg/Byx85t2eFp)
659
+ - 🐛 [Issue Tracker](https://github.com/IngloriousCoderz/inglorious-forge/issues)
660
+ - 📧 [Email Support](mailto:antony.mistretta@gmail.com)
877
661
 
878
662
  ---
879
663
 
880
- ## Contributing
881
-
882
- Contributions welcome! Please read our [Contributing Guidelines](../../CONTRIBUTING.md) first.
664
+ **Build static sites the Inglorious way. Simple. Predictable. Fast.** 🚀