@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 +21 -0
- package/README.md +12 -3
- package/llms.txt +34 -13
- package/package.json +4 -2
- package/src/build.js +30 -8
- package/src/index.js +1 -0
- package/src/templates/base.js +1 -0
- package/src/themes.js +32 -0
- package/src/themes/chalk/styles.css +0 -720
- package/src/themes/chalk/theme.json +0 -6
- package/src/themes/clay/styles.css +0 -726
- package/src/themes/clay/theme.json +0 -6
- package/src/themes/juniper/styles.css +0 -660
- package/src/themes/juniper/theme.json +0 -6
- package/src/themes/salt/styles.css +0 -728
- package/src/themes/salt/theme.json +0 -6
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`.
|
|
37
|
-
|
|
38
|
-
|
|
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/`.
|
|
113
|
-
with `config.basePath` (default `/`) — set it when
|
|
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.
|
|
129
|
-
`
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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>
|
|
5
|
-
// 2.
|
|
6
|
-
// 3. src/templates/<file>
|
|
7
|
-
// Themes usually only provide styles.css; templates come from
|
|
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
|
-
|
|
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
|
-
//
|
|
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)
|
|
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";
|
package/src/templates/base.js
CHANGED
|
@@ -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
|
+
}
|