@odla-ai/blog 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cory Ondrejka
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # @odla-ai/blog
2
2
 
3
+ > ⚠️ **Experimental — an agentic-coding experiment.** This package is built and
4
+ > operated by autonomous coding agents as an experiment in agentic loops. APIs
5
+ > change without notice and nothing here is production-hardened. **Use at your
6
+ > own risk.**
7
+
3
8
  Minimal-dependency, static-first blogging platform. A site is a directory of
4
9
  markdown plus one JSON config; `build()` emits a fully static site that works on
5
10
  GitHub Pages or Cloudflare. When [odla-db](https://github.com/odla-ai/odla-db) is
@@ -33,9 +38,13 @@ Four bundled themes, all with light + dark modes (system preference + ☾/☀ to
33
38
  - **chalk** — chalkboard on graph paper; copper/cerulean, mono chrome
34
39
  - **clay** — sand paper and terracotta; system fonts only, zero external requests
35
40
 
36
- Pick one with `"theme": "salt"` in `blog.config.json`. Override any theme file by
37
- placing a same-named file in your site's `theme/` directory. Preview all four
38
- against the example site with `npm run preview` (from the monorepo root).
41
+ Pick one with `"theme": "salt"` in `blog.config.json`. Themes ship in the
42
+ [`@odla-ai/ui`](https://www.npmjs.com/package/@odla-ai/ui) dependency (the odla
43
+ design system); every built site also gets ui's class-scoped component layer
44
+ (`assets/odla-ui.css` — buttons, form fields, cards, …) theme-matched via the
45
+ theme's `ui.css` token aliases. Override any theme file by placing a
46
+ same-named file in your site's `theme/` directory. Preview all four against
47
+ the example site with `npm run preview` (from the monorepo root).
39
48
 
40
49
  ## Deploy (Cloudflare)
41
50
 
package/llms.txt CHANGED
@@ -1,5 +1,9 @@
1
1
  # @odla-ai/blog — agent reference (llms.txt)
2
2
 
3
+ > EXPERIMENTAL — an agentic-coding experiment. This package is built and operated
4
+ > by autonomous coding agents as an experiment in agentic loops. APIs change
5
+ > without notice and nothing here is production-hardened. Use at your own risk.
6
+
3
7
  This file ships in the npm package so an agent working in a *site* repo can
4
8
  operate the platform from `node_modules/@odla-ai/blog/llms.txt` alone. The
5
9
  source in `src/` is small and dependency-light — read it when in doubt; it is
@@ -109,12 +113,13 @@ const result = await build({
109
113
 
110
114
  Output: `index.html`, `posts/<slug>/index.html`, `<page>/index.html`,
111
115
  `archive/`, `tags/`, `tags/<tag>/`, `rss.xml`, `sitemap.xml`,
112
- `assets/styles.css`, plus everything from `public/`. All URLs are prefixed
113
- with `config.basePath` (default `/`) — set it when deploying under a subpath
114
- (e.g. GitHub project pages).
116
+ `assets/styles.css`, `assets/odla-ui.css`, plus everything from `public/`.
117
+ All URLs are prefixed with `config.basePath` (default `/`) — set it when
118
+ deploying under a subpath (e.g. GitHub project pages).
115
119
 
116
120
  Also exported: `loadPosts`, `loadPages`, `parseFrontmatter`, `slugFromFilename`,
117
- `renderMarkdown`, `slugify`, `escapeHtml`, `resolveThemeFile`.
121
+ `renderMarkdown`, `slugify`, `escapeHtml`, `resolveThemeFile`, `listThemes`,
122
+ `THEME_NAMES`.
118
123
 
119
124
  ## Markdown
120
125
 
@@ -125,13 +130,19 @@ no client-side JS. Headings get slugified `id` anchors.
125
130
  ## Theming
126
131
 
127
132
  Bundled themes: `juniper` (default), `salt`, `chalk`, `clay` — pick with
128
- `"theme": "<name>"` in config. Each theme is a single self-contained
129
- `src/themes/<name>/styles.css` plus `theme.json` metadata.
130
-
131
- Invariants every theme upholds (keep them when editing or adding one):
133
+ `"theme": "<name>"` in config. **Themes live in the `@odla-ai/ui`
134
+ dependency** (`node_modules/@odla-ai/ui/themes/<name>/`): `styles.css` (the
135
+ full theme), `ui.css` (maps the theme's palette onto the `--ui-*` design
136
+ tokens), `theme.json` (metadata). Resolution order for any theme file:
137
+ `<site>/theme/<file>` → `@odla-ai/ui/themes/<name>/<file>` →
138
+ `src/templates/<file>`. (`@odla-ai/ui` also ships a `paper` theme — it is a
139
+ dashboard theme, NOT a blog theme; it doesn't style the selector contract.)
140
+
141
+ Invariants every theme upholds (keep them when editing or adding one;
142
+ tested in @odla-ai/ui's `test/themes.test.js`):
132
143
  - Light tokens on `:root`; dark tokens duplicated on BOTH `[data-theme="dark"]`
133
144
  and `@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) }`.
134
- The two dark blocks must stay identical (tested in `test/themes.test.js`).
145
+ The two dark blocks must stay identical.
135
146
  - Common token names: `--bg`, `--text`, `--accent`, `--border`, `--code-bg`,
136
147
  and the highlight set `--hl-keyword`, `--hl-string`, `--hl-comment`,
137
148
  `--hl-number`, `--hl-title`, `--hl-attr`, `--hl-type`, `--hl-meta`,
@@ -141,15 +152,25 @@ Invariants every theme upholds (keep them when editing or adding one):
141
152
  `.post-title`, `.post-meta`, `.post-tag`, `.prose`, `.page-title`,
142
153
  `.archive-year`, `.archive-list`, `.archive-item`, `.tag-cloud`, `.hljs-*`.
143
154
 
155
+ **Component classes on any page:** every built site also ships
156
+ `assets/odla-ui.css` (linked before the theme stylesheet) — the class-scoped
157
+ @odla-ai/ui component layer (`.btn`, `.field`/`.input`/`.textarea`, `.card`,
158
+ `.table`, `.banner`, chat classes, …), theme-correct because the build
159
+ appends the theme's `ui.css` token aliases into `assets/styles.css`. Use
160
+ these classes in pages/templates instead of inventing inline styles; the
161
+ full inventory is in `node_modules/@odla-ai/ui/llms.txt`. A site
162
+ `public/assets/odla-ui.css` overrides the shipped copy (public/ copies
163
+ last), same valve as styles.css.
164
+
144
165
  **Site-local overrides:** any file placed in the site's `theme/` directory
145
- shadows the packaged theme file of the same name (`styles.css`, or any
146
- template like `post.js` or `index.js`). Copy the packaged file out, edit,
166
+ shadows the packaged theme file of the same name (`styles.css`, `ui.css`, or
167
+ any template like `post.js` or `index.js`). Copy the packaged file out, edit,
147
168
  done. Templates are plain JS functions returning template-literal HTML
148
169
  strings; they may import shared helpers from the package
149
170
  (`import { postList } from "@odla-ai/blog"`). To ADD styles without forking
150
171
  the whole stylesheet, put them in `theme/custom.css` — the build appends it
151
- to the theme's CSS. Post URLs live under `config.postsPath` (default
152
- "posts").
172
+ to the theme's CSS (after the `ui.css` aliases). Post URLs live under
173
+ `config.postsPath` (default "posts").
153
174
 
154
175
  ## Deploy (Cloudflare)
155
176
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@odla-ai/blog",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Minimal-dependency, static-first blogging platform. Files in, site out; odla-db lights up interactivity when configured.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,9 +21,11 @@
21
21
  },
22
22
  "scripts": {
23
23
  "preview": "node scripts/preview.mjs",
24
- "test": "node --test --test-reporter=spec"
24
+ "test": "node --test --test-reporter=spec",
25
+ "coverage": "c8 --src=src --reporter=text --reporter=html node --test"
25
26
  },
26
27
  "dependencies": {
28
+ "@odla-ai/ui": "^0.1.0",
27
29
  "highlight.js": "^11.11.0",
28
30
  "marked": "^16.0.0",
29
31
  "yaml": "^2.8.0"
package/src/build.js CHANGED
@@ -1,10 +1,11 @@
1
1
  // Static build: content + theme → dist/. Pure pipeline, top to bottom.
2
2
  //
3
3
  // Theme resolution (file-by-file shadowing):
4
- // 1. <site>/theme/<file> — site-local override
5
- // 2. src/themes/<name>/<file> — the chosen packaged theme
6
- // 3. src/templates/<file> — shared default templates
7
- // Themes usually only provide styles.css; templates come from the shared set.
4
+ // 1. <site>/theme/<file> — site-local override
5
+ // 2. @odla-ai/ui/themes/<name>/<file> — the chosen packaged theme
6
+ // 3. src/templates/<file> — shared default templates
7
+ // Themes usually only provide styles.css (+ ui.css); templates come from
8
+ // the shared set. Themes live in the @odla-ai/ui dependency.
8
9
  import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
9
10
  import { existsSync } from "node:fs";
10
11
  import { join, dirname } from "node:path";
@@ -13,15 +14,16 @@ import { loadPosts, loadPages, loadJsPages, loadCollections } from "./content.js
13
14
  import { renderMarkdown, slugify } from "./markdown.js";
14
15
  import { excerpt } from "./excerpt.js";
15
16
  import { atom, rss, sitemap } from "./feeds.js";
17
+ import { THEME_NAMES, uiFile, uiThemeFile } from "./themes.js";
16
18
 
17
19
  const SRC = dirname(fileURLToPath(import.meta.url));
18
20
 
19
21
  export function resolveThemeFile(siteDir, themeName, file) {
20
22
  const candidates = [
21
23
  join(siteDir, "theme", file),
22
- join(SRC, "themes", themeName, file),
24
+ uiThemeFile(themeName, file),
23
25
  join(SRC, "templates", file),
24
- ];
26
+ ].filter(Boolean);
25
27
  return candidates.find((c) => existsSync(c)) ?? null;
26
28
  }
27
29
 
@@ -188,17 +190,37 @@ export async function build({
188
190
  await write("atom.xml", atom({ config, posts }));
189
191
  await write("sitemap.xml", sitemap({ config, posts, pages }));
190
192
 
191
- // theme/custom.css is appended to the theme stylesheet additions without
193
+ // Theme stylesheet assembly: styles.css, then the theme's ui.css (maps its
194
+ // palette onto the @odla-ai/ui --ui-* token contract so component classes
195
+ // render theme-correct), then site theme/custom.css — additions without
192
196
  // forking the whole file (full replacement: shadow styles.css itself).
193
197
  const styles = resolveThemeFile(siteDir, themeName, "styles.css");
194
- if (!styles) throw new Error(`Theme "${themeName}" has no styles.css`);
198
+ if (!styles) {
199
+ throw new Error(
200
+ `Theme "${themeName}" has no styles.css — bundled themes: ${THEME_NAMES.join(", ")} (or add <site>/theme/styles.css)`
201
+ );
202
+ }
195
203
  let css = await readFile(styles, "utf8");
204
+ const uiAlias = resolveThemeFile(siteDir, themeName, "ui.css");
205
+ if (uiAlias) {
206
+ css += `\n/* ─── ${themeName}/ui.css (odla-ui token aliases) ─── */\n${await readFile(uiAlias, "utf8")}`;
207
+ }
196
208
  const customCss = join(siteDir, "theme", "custom.css");
197
209
  if (existsSync(customCss)) {
198
210
  css += `\n/* ─── site custom.css ─── */\n${await readFile(customCss, "utf8")}`;
199
211
  }
200
212
  await write(join("assets", "styles.css"), css);
201
213
 
214
+ // Every site ships the ui component layer (class-scoped, inert until a
215
+ // page uses the classes). base.js links it BEFORE styles.css so the theme
216
+ // and site css win ties. A site public/assets/odla-ui.css overrides it
217
+ // (public/ copies last), same valve as styles.css.
218
+ const uiCss = uiFile("odla-ui.css");
219
+ if (!uiCss) {
220
+ throw new Error("@odla-ai/ui not found — run npm install (blog themes and component CSS ship there)");
221
+ }
222
+ await write(join("assets", "odla-ui.css"), await readFile(uiCss, "utf8"));
223
+
202
224
  const publicDir = join(siteDir, "public");
203
225
  if (existsSync(publicDir)) await cp(publicDir, outDir, { recursive: true });
204
226
 
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { build, resolveThemeFile } from "./build.js";
2
+ export { listThemes, THEME_NAMES } from "./themes.js";
2
3
  export { loadPosts, loadPages, parseFrontmatter, slugFromFilename } from "./content.js";
3
4
  export { renderMarkdown, slugify, escapeHtml } from "./markdown.js";
4
5
  export { postList, tagList, tagUrl, formatDate, isoDate } from "./templates/helpers.js";
@@ -125,6 +125,7 @@ export function base({ config, title, description, path, image, content }) {
125
125
  <meta charset="utf-8">
126
126
  <meta name="viewport" content="width=device-width, initial-scale=1">
127
127
  ${headMeta(config, { fullTitle, description, path, image })}
128
+ <link rel="stylesheet" href="${base}assets/odla-ui.css">
128
129
  <link rel="stylesheet" href="${base}assets/styles.css">
129
130
  <script>${NO_FLASH}</script>
130
131
  </head>
package/src/themes.js ADDED
@@ -0,0 +1,32 @@
1
+ // Themes ship in @odla-ai/ui (themes/<name>/{styles.css,ui.css,theme.json});
2
+ // this module is the blog's bridge to them. createRequire (not
3
+ // import.meta.resolve) because it works on every Node 20.x, returns plain
4
+ // filesystem paths, and verifies existence with a catchable error.
5
+ import { createRequire } from "node:module";
6
+ import { readFileSync } from "node:fs";
7
+
8
+ const require = createRequire(import.meta.url);
9
+
10
+ // The bundled themes that uphold the blog selector contract. @odla-ai/ui
11
+ // also ships "paper", which does not — keep it out of this list.
12
+ export const THEME_NAMES = ["chalk", "clay", "juniper", "salt"];
13
+
14
+ // Absolute path of a file shipped by @odla-ai/ui, or null if absent.
15
+ export function uiFile(subpath) {
16
+ try {
17
+ return require.resolve(`@odla-ai/ui/${subpath}`);
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ export function uiThemeFile(themeName, file) {
24
+ return uiFile(`themes/${themeName}/${file}`);
25
+ }
26
+
27
+ // Metadata for every bundled blog theme (for pickers/galleries).
28
+ export function listThemes() {
29
+ return THEME_NAMES.map((name) =>
30
+ JSON.parse(readFileSync(uiThemeFile(name, "theme.json"), "utf8"))
31
+ );
32
+ }