EXTENSION for TreeOS
blog
A land-level blog for publishing long-form content outside the tree structure. Trees are for structured knowledge. Blogs are for narrative. Announcements, changelogs, tutorials, essays. Content that has a publish date and an audience rather than a position in a hierarchy. Each post has a title, a URL-safe slug, markdown content, an optional summary, a publish date, and an author reference. The slug is unique and serves as the primary lookup key for public access. Posts can be marked as published or unpublished. Only published posts appear in the public listing. Publishing is restricted to land admins. Any authenticated admin can create, update, or delete posts through the API. The author's username is stored alongside the user ID so posts display correctly even if the author account is later modified. Reading is fully public. No authentication required to list posts or read a specific post by slug. The extension provides its own Mongoose model (BlogPost) stored in a dedicated collection separate from tree data. This keeps blog content out of the node/note system entirely. The CLI exposes two commands: 'blogs' to list all published posts and 'blog <slug>' to read a specific one. Slug collisions on create or update return a clear error rather than overwriting.
v1.0.1 by TreeOS Site 0 downloads 4 files 200 lines 6.8 KB published 38d ago
treeos ext install blog
View changelog

Manifest

Provides

  • 1 models
  • routes
  • 2 CLI commands

Requires

  • models: User
SHA256: 8c381336ab9cfece1dc660e59a2b181fe540cb0c9e15e3344a4a11263b698636

CLI Commands

CommandMethodDescription
blogsGETList blog posts
blog <slug>GETRead a blog post

Source Code

1import BlogPost from "./model.js";
2import createRouter from "./routes.js";
3
4export async function init(core) {
5  return {
6    models: { BlogPost },
7    router: createRouter(core),
8    exports: {},
9  };
10}
11
1export default {
2  name: "blog",
3  version: "1.0.1",
4  builtFor: "TreeOS",
5  description:
6    "A land-level blog for publishing long-form content outside the tree structure. Trees " +
7    "are for structured knowledge. Blogs are for narrative. Announcements, changelogs, " +
8    "tutorials, essays. Content that has a publish date and an audience rather than a " +
9    "position in a hierarchy. " +
10    "\n\n" +
11    "Each post has a title, a URL-safe slug, markdown content, an optional summary, a " +
12    "publish date, and an author reference. The slug is unique and serves as the primary " +
13    "lookup key for public access. Posts can be marked as published or unpublished. Only " +
14    "published posts appear in the public listing. " +
15    "\n\n" +
16    "Publishing is restricted to land admins. Any authenticated admin can create, update, " +
17    "or delete posts through the API. The author's username is stored alongside the user " +
18    "ID so posts display correctly even if the author account is later modified. Reading " +
19    "is fully public. No authentication required to list posts or read a specific post " +
20    "by slug. " +
21    "\n\n" +
22    "The extension provides its own Mongoose model (BlogPost) stored in a dedicated " +
23    "collection separate from tree data. This keeps blog content out of the node/note " +
24    "system entirely. The CLI exposes two commands: 'blogs' to list all published posts " +
25    "and 'blog <slug>' to read a specific one. Slug collisions on create or update " +
26    "return a clear error rather than overwriting.",
27
28  needs: {
29    models: ["User"],
30  },
31
32  optional: {},
33
34  provides: {
35    models: { BlogPost: "./model.js" },
36    routes: "./routes.js",
37    tools: false,
38    jobs: false,
39    orchestrator: false,
40    energyActions: {},
41    sessionTypes: {},
42    cli: [
43      { command: "blogs", description: "List blog posts", method: "GET", endpoint: "/blog/posts" },
44      { command: "blog <slug>", description: "Read a blog post", method: "GET", endpoint: "/blog/posts/:slug" },
45    ],
46  },
47};
48
1import mongoose from "mongoose";
2import { v4 as uuidv4 } from "uuid";
3
4const BlogPostSchema = new mongoose.Schema({
5  _id: { type: String, default: uuidv4 },
6  title: { type: String, required: true },
7  slug: { type: String, required: true, unique: true },
8  content: { type: String, required: true },
9  summary: { type: String, default: "" },
10  publishedAt: { type: Date, default: Date.now },
11  createdAt: { type: Date, default: Date.now },
12  published: { type: Boolean, default: true },
13  author: { type: String, ref: "User" },
14  authorName: { type: String, default: "" },
15});
16
17const BlogPost = mongoose.model("BlogPost", BlogPostSchema);
18export default BlogPost;
19
1import log from "../../seed/log.js";
2import express from "express";
3import authenticate from "../../seed/middleware/authenticate.js";
4import { sendOk, sendError, ERR } from "../../seed/protocol.js";
5import BlogPost from "./model.js";
6
7export default function createRouter(core) {
8  const { User } = core.models;
9  const router = express.Router();
10
11  router.get("/blog/posts", async (req, res) => {
12    try {
13      const posts = await BlogPost.find({ published: true })
14        .select("title slug summary publishedAt authorName")
15        .sort({ publishedAt: -1 })
16        .lean();
17      sendOk(res, { posts });
18    } catch (err) {
19      log.error("Blog", "Blog list error:", err.message);
20      sendError(res, 500, ERR.INTERNAL, err.message);
21    }
22  });
23
24  router.get("/blog/posts/:slug", async (req, res) => {
25    try {
26      const post = await BlogPost.findOne({
27        slug: req.params.slug,
28        published: true,
29      }).lean();
30      if (!post) return sendError(res, 404, ERR.NOTE_NOT_FOUND, "Post not found");
31      sendOk(res, { post });
32    } catch (err) {
33      log.error("Blog", "Blog post error:", err.message);
34      sendError(res, 500, ERR.INTERNAL, err.message);
35    }
36  });
37
38  router.post("/blog/posts", authenticate, async (req, res) => {
39    try {
40      const user = await User.findById(req.userId)
41        .select("isAdmin username")
42        .lean();
43      if (!user || !user.isAdmin) {
44        return sendError(res, 403, ERR.FORBIDDEN, "Requires admin");
45      }
46
47      const { title, slug, content, summary, publishedAt, published } = req.body;
48      if (!title || !slug || !content) {
49        return sendError(res, 400, ERR.INVALID_INPUT, "title, slug, and content are required");
50      }
51
52      const post = await BlogPost.create({
53        title,
54        slug,
55        content,
56        summary: summary || "",
57        publishedAt: publishedAt ? new Date(publishedAt) : new Date(),
58        published: published !== undefined ? published : true,
59        author: req.userId,
60        authorName: user.username,
61      });
62
63      sendOk(res, { post }, 201);
64    } catch (err) {
65      if (err.code === 11000) {
66        return sendError(res, 400, ERR.INVALID_INPUT, "Slug already exists");
67      }
68      log.error("Blog", "Blog create error:", err.message);
69      sendError(res, 500, ERR.INTERNAL, err.message);
70    }
71  });
72
73  router.put("/blog/posts/:slug", authenticate, async (req, res) => {
74    try {
75      const user = await User.findById(req.userId).select("isAdmin").lean();
76      if (!user || !user.isAdmin) {
77        return sendError(res, 403, ERR.FORBIDDEN, "Requires admin");
78      }
79
80      const updates = {};
81      const allowed = ["title", "slug", "content", "summary", "publishedAt", "published"];
82      for (const key of allowed) {
83        if (req.body[key] !== undefined) {
84          updates[key] = key === "publishedAt" ? new Date(req.body[key]) : req.body[key];
85        }
86      }
87
88      const post = await BlogPost.findOneAndUpdate(
89        { slug: req.params.slug },
90        updates,
91        { new: true },
92      );
93      if (!post) return sendError(res, 404, ERR.NOTE_NOT_FOUND, "Post not found");
94      sendOk(res, { post });
95    } catch (err) {
96      if (err.code === 11000) {
97        return sendError(res, 400, ERR.INVALID_INPUT, "Slug already exists");
98      }
99      log.error("Blog", "Blog update error:", err.message);
100      sendError(res, 500, ERR.INTERNAL, err.message);
101    }
102  });
103
104  router.delete("/blog/posts/:slug", authenticate, async (req, res) => {
105    try {
106      const user = await User.findById(req.userId).select("isAdmin").lean();
107      if (!user || !user.isAdmin) {
108        return sendError(res, 403, ERR.FORBIDDEN, "Requires admin");
109      }
110
111      const post = await BlogPost.findOneAndDelete({ slug: req.params.slug });
112      if (!post) return sendError(res, 404, ERR.NOTE_NOT_FOUND, "Post not found");
113      sendOk(res, { deleted: req.params.slug });
114    } catch (err) {
115      log.error("Blog", "Blog delete error:", err.message);
116      sendError(res, 500, ERR.INTERNAL, err.message);
117    }
118  });
119
120  return router;
121}
122

Versions

Version Published Downloads
1.0.1 38d ago 0
1.0.0 48d ago 0
0 stars
0 flags
React from the CLI: treeos ext star blog

Comments

Loading comments...

Post comments from the CLI: treeos ext comment blog "your comment"
Max 3 comments per extension. One star and one flag per user.