@odla-ai/blog 0.0.1
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 +44 -0
- package/config.schema.json +108 -0
- package/llms.txt +161 -0
- package/package.json +31 -0
- package/src/build.js +206 -0
- package/src/content.js +151 -0
- package/src/excerpt.js +66 -0
- package/src/feeds.js +81 -0
- package/src/index.js +5 -0
- package/src/markdown.js +148 -0
- package/src/templates/archive.js +31 -0
- package/src/templates/base.js +153 -0
- package/src/templates/helpers.js +55 -0
- package/src/templates/index.js +9 -0
- package/src/templates/page.js +20 -0
- package/src/templates/post.js +18 -0
- package/src/templates/tags.js +39 -0
- package/src/themes/chalk/styles.css +720 -0
- package/src/themes/chalk/theme.json +6 -0
- package/src/themes/clay/styles.css +726 -0
- package/src/themes/clay/theme.json +6 -0
- package/src/themes/juniper/styles.css +660 -0
- package/src/themes/juniper/theme.json +6 -0
- package/src/themes/salt/styles.css +728 -0
- package/src/themes/salt/theme.json +6 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Shared template helpers. Templates are plain JS functions that return
|
|
2
|
+
// HTML strings — no template language, no JSX.
|
|
3
|
+
export { escapeHtml } from "../markdown.js";
|
|
4
|
+
import { slugify, escapeHtml } from "../markdown.js";
|
|
5
|
+
|
|
6
|
+
const dateFormat = new Intl.DateTimeFormat("en-US", {
|
|
7
|
+
dateStyle: "long",
|
|
8
|
+
timeZone: "UTC",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export function formatDate(date) {
|
|
12
|
+
return dateFormat.format(date);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isoDate(date) {
|
|
16
|
+
return date.toISOString().slice(0, 10);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Tag URLs are slugified (lowercase) so "Podcast" and "podcast" share one
|
|
20
|
+
// page — mixed-case tag directories silently collide on case-insensitive
|
|
21
|
+
// filesystems (macOS) and split into duplicates on case-sensitive deploys.
|
|
22
|
+
export function tagUrl(base, tag) {
|
|
23
|
+
return `${base}tags/${slugify(tag)}/`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function tagList(base, tags) {
|
|
27
|
+
if (!tags.length) return "";
|
|
28
|
+
const links = tags
|
|
29
|
+
.map((t) => `<a class="post-tag" href="${tagUrl(base, t)}">${escapeHtml(t)}</a>`)
|
|
30
|
+
.join(", ");
|
|
31
|
+
return `<span class="post-tags">${links}</span>`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Post list with content excerpts — shared by the home page and any page
|
|
35
|
+
// with a `listTag` (e.g. a "Sea Stories" page listing posts by tag).
|
|
36
|
+
export function postList({ config, posts, excerpts }) {
|
|
37
|
+
const base = config.basePath ?? "/";
|
|
38
|
+
const items = posts
|
|
39
|
+
.map(
|
|
40
|
+
(post) => `<article class="post-item">
|
|
41
|
+
<header class="post-header">
|
|
42
|
+
<a class="post-title" href="${base}${post.url}">${escapeHtml(post.title)}</a>
|
|
43
|
+
</header>
|
|
44
|
+
<div class="prose post-excerpt">
|
|
45
|
+
${excerpts?.get(post.slug) ?? (post.description ? `<p>${escapeHtml(post.description)}</p>` : "")}
|
|
46
|
+
</div>
|
|
47
|
+
<div class="post-meta">
|
|
48
|
+
<time class="post-date" datetime="${isoDate(post.date)}">${formatDate(post.date)}</time>
|
|
49
|
+
${tagList(base, post.tags)}
|
|
50
|
+
</div>
|
|
51
|
+
</article>`
|
|
52
|
+
)
|
|
53
|
+
.join("\n");
|
|
54
|
+
return `<section class="post-list">\n${items}\n</section>`;
|
|
55
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Home page: recent posts with content excerpts, newest first.
|
|
2
|
+
// config.indexLimit caps how many (default 20; 0 = all).
|
|
3
|
+
import { postList } from "./helpers.js";
|
|
4
|
+
|
|
5
|
+
export function index({ config, posts, excerpts }) {
|
|
6
|
+
const limit = config.indexLimit ?? 20;
|
|
7
|
+
const shown = limit > 0 ? posts.slice(0, limit) : posts;
|
|
8
|
+
return postList({ config, posts: shown, excerpts });
|
|
9
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// A standalone markdown page (about, etc.). A page with `listTag` in its
|
|
2
|
+
// frontmatter appends the excerpted list of posts carrying that tag — the
|
|
3
|
+
// pattern for curated collections like "How I..." or "Sea Stories".
|
|
4
|
+
import { escapeHtml, postList } from "./helpers.js";
|
|
5
|
+
import { slugify } from "../markdown.js";
|
|
6
|
+
|
|
7
|
+
export function page({ config, page, html, posts = [], excerpts }) {
|
|
8
|
+
let list = "";
|
|
9
|
+
if (page.listTag) {
|
|
10
|
+
const wanted = slugify(page.listTag);
|
|
11
|
+
const tagged = posts.filter((p) => p.tags.some((t) => slugify(t) === wanted));
|
|
12
|
+
list = `\n${postList({ config, posts: tagged, excerpts })}`;
|
|
13
|
+
}
|
|
14
|
+
return `<article class="page">
|
|
15
|
+
<h1 class="page-title">${escapeHtml(page.title)}</h1>
|
|
16
|
+
<div class="prose">
|
|
17
|
+
${html}
|
|
18
|
+
</div>
|
|
19
|
+
</article>${list}`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// A single post page: title, content, then meta (date + tags) at the base.
|
|
2
|
+
import { escapeHtml, formatDate, isoDate, tagList } from "./helpers.js";
|
|
3
|
+
|
|
4
|
+
export function post({ config, post, html }) {
|
|
5
|
+
const base = config.basePath ?? "/";
|
|
6
|
+
return `<article class="post">
|
|
7
|
+
<header class="post-header">
|
|
8
|
+
<h1 class="post-title">${escapeHtml(post.title)}</h1>
|
|
9
|
+
</header>
|
|
10
|
+
<div class="prose">
|
|
11
|
+
${html}
|
|
12
|
+
</div>
|
|
13
|
+
<div class="post-meta">
|
|
14
|
+
<time class="post-date" datetime="${isoDate(post.date)}">${formatDate(post.date)}</time>
|
|
15
|
+
${tagList(base, post.tags)}
|
|
16
|
+
</div>
|
|
17
|
+
</article>`;
|
|
18
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Tag index (all tags with counts) and per-tag post lists.
|
|
2
|
+
import { escapeHtml, formatDate, isoDate, tagUrl } from "./helpers.js";
|
|
3
|
+
|
|
4
|
+
export function tagIndex({ config, posts }) {
|
|
5
|
+
const base = config.basePath ?? "/";
|
|
6
|
+
// Group by URL (slugified) so case variants of a tag merge, matching the
|
|
7
|
+
// per-tag pages the build emits.
|
|
8
|
+
const groups = new Map();
|
|
9
|
+
for (const post of posts) {
|
|
10
|
+
for (const tag of post.tags) {
|
|
11
|
+
const url = tagUrl(base, tag);
|
|
12
|
+
if (!groups.has(url)) groups.set(url, { label: tag, count: 0 });
|
|
13
|
+
groups.get(url).count++;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const tags = [...groups.entries()].sort(
|
|
17
|
+
(a, b) => b[1].count - a[1].count || a[1].label.localeCompare(b[1].label)
|
|
18
|
+
);
|
|
19
|
+
const items = tags
|
|
20
|
+
.map(
|
|
21
|
+
([url, { label, count }]) =>
|
|
22
|
+
`<li><a class="post-tag" href="${url}">${escapeHtml(label)}</a> <span class="tag-count">${count}</span></li>`
|
|
23
|
+
)
|
|
24
|
+
.join("\n");
|
|
25
|
+
return `<h1 class="page-title">Tags</h1>\n<ul class="tag-cloud">\n${items}\n</ul>`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function tagPage({ config, tag, posts }) {
|
|
29
|
+
const base = config.basePath ?? "/";
|
|
30
|
+
const items = posts
|
|
31
|
+
.map(
|
|
32
|
+
(p) => `<li class="archive-item">
|
|
33
|
+
<time datetime="${isoDate(p.date)}">${formatDate(p.date)}</time>
|
|
34
|
+
<a href="${base}${p.url}">${escapeHtml(p.title)}</a>
|
|
35
|
+
</li>`
|
|
36
|
+
)
|
|
37
|
+
.join("\n");
|
|
38
|
+
return `<h1 class="page-title">${escapeHtml(tag)}</h1>\n<ul class="archive-list">\n${items}\n</ul>`;
|
|
39
|
+
}
|