@accountgroove/blog 0.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 +84 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +71 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @accountgroove/blog
|
|
2
|
+
|
|
3
|
+
Client SDK for the AccountGroove **Content API**. Drop it into a client's
|
|
4
|
+
**Astro** or **Next.js** site to render their published blog **on their own
|
|
5
|
+
domain, with their own CSS** — subdirectory URLs (`client.com/blog`), great SEO,
|
|
6
|
+
no iframe.
|
|
7
|
+
|
|
8
|
+
This is the "Bucket A" delivery adapter from the platform architecture
|
|
9
|
+
(`docs/PLATFORM-ARCHITECTURE.md` §7): for sites we build/control, the blog is
|
|
10
|
+
rendered *inside* the site's own app, so styling matches automatically.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @accountgroove/blog
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Set two env vars in the host site:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
ACCOUNTGROOVE_API_BASE=https://mcp.accountgroove.ai # Content API base (default)
|
|
22
|
+
ACCOUNTGROOVE_LOCATION_ID=<the client's GHL locationId>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Core usage (any framework)
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createBlogClient } from "@accountgroove/blog";
|
|
29
|
+
|
|
30
|
+
const blog = createBlogClient({
|
|
31
|
+
apiBase: process.env.ACCOUNTGROOVE_API_BASE!,
|
|
32
|
+
locationId: process.env.ACCOUNTGROOVE_LOCATION_ID!,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const posts = await blog.listPosts({ type: "blog", limit: 20 }); // BlogPostSummary[]
|
|
36
|
+
const post = await blog.getPost("my-slug"); // BlogPost | null
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Only **published** posts (`status='posted'`) are ever returned, scoped to the
|
|
40
|
+
location. `listPosts` gives an `excerpt`; `getPost` gives the full `body`.
|
|
41
|
+
|
|
42
|
+
## Astro (primary)
|
|
43
|
+
|
|
44
|
+
Copy the example routes:
|
|
45
|
+
|
|
46
|
+
- `examples/astro/blog-index.astro` → `src/pages/blog/index.astro`
|
|
47
|
+
- `examples/astro/blog-slug.astro` → `src/pages/blog/[slug].astro`
|
|
48
|
+
|
|
49
|
+
They use `export const prerender = false` (Astro hybrid/SSR) so newly published
|
|
50
|
+
posts appear immediately. Wrap them in your site layout so the blog inherits the
|
|
51
|
+
nav/footer/CSS.
|
|
52
|
+
|
|
53
|
+
## Next.js (App Router)
|
|
54
|
+
|
|
55
|
+
Copy:
|
|
56
|
+
|
|
57
|
+
- `examples/next/blog-page.tsx` → `app/blog/page.tsx`
|
|
58
|
+
- `examples/next/blog-slug-page.tsx` → `app/blog/[slug]/page.tsx`
|
|
59
|
+
- `examples/next/revalidate-route.ts` → `app/api/accountgroove/revalidate/route.ts`
|
|
60
|
+
|
|
61
|
+
The pages tag their fetches (`ag-blog`, `ag-blog:<slug>`) and the revalidate
|
|
62
|
+
route purges those tags on publish — **ISR + on-demand revalidation, no
|
|
63
|
+
redeploy** (docs §11). Set `ACCOUNTGROOVE_REVALIDATE_SECRET` and register the
|
|
64
|
+
route URL as the site's publish webhook in AccountGroove.
|
|
65
|
+
|
|
66
|
+
## Freshness model
|
|
67
|
+
|
|
68
|
+
| Mode | How fresh content arrives |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| Next (App Router) | publish → webhook → `revalidateTag` → live in seconds |
|
|
71
|
+
| Astro (SSR/hybrid) | rendered per request → always current (add a CDN cache + purge for speed) |
|
|
72
|
+
| Static build | rebuild/redeploy on publish (simplest, slowest) |
|
|
73
|
+
|
|
74
|
+
## API
|
|
75
|
+
|
|
76
|
+
- `createBlogClient(options): BlogClient`
|
|
77
|
+
- `options.apiBase` — Content API base URL
|
|
78
|
+
- `options.locationId` — the tenant's GHL locationId
|
|
79
|
+
- `options.fetch?` — fetch override
|
|
80
|
+
- `client.listPosts(params?, init?) → Promise<BlogPostSummary[]>`
|
|
81
|
+
- `params`: `{ type?, limit?, offset? }` · `init`: standard `RequestInit`
|
|
82
|
+
(e.g. `{ next: { tags: ["ag-blog"], revalidate: 60 } }`)
|
|
83
|
+
- `client.getPost(slug, init?) → Promise<BlogPost | null>` (`null` on 404)
|
|
84
|
+
- Throws `BlogApiError` (`.status`, `.url`) on other non-OK responses.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type ContentType = "blog" | "newsletter" | "social";
|
|
2
|
+
/** A post as returned by the list endpoint (excerpt, no full body). */
|
|
3
|
+
export interface BlogPostSummary {
|
|
4
|
+
id: string;
|
|
5
|
+
type: ContentType;
|
|
6
|
+
title: string | null;
|
|
7
|
+
slug: string | null;
|
|
8
|
+
postedAt: string | null;
|
|
9
|
+
excerpt: string | null;
|
|
10
|
+
}
|
|
11
|
+
/** A post as returned by the detail endpoint (full body, no excerpt). */
|
|
12
|
+
export interface BlogPost {
|
|
13
|
+
id: string;
|
|
14
|
+
type: ContentType;
|
|
15
|
+
title: string | null;
|
|
16
|
+
slug: string | null;
|
|
17
|
+
postedAt: string | null;
|
|
18
|
+
body: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface BlogClientOptions {
|
|
21
|
+
/** Base URL of the Content API, e.g. "https://mcp.accountgroove.ai". */
|
|
22
|
+
apiBase: string;
|
|
23
|
+
/** The tenant's GHL locationId (their site key). */
|
|
24
|
+
locationId: string;
|
|
25
|
+
/** Optional fetch override (e.g. a Next-instrumented fetch for caching). */
|
|
26
|
+
fetch?: typeof fetch;
|
|
27
|
+
}
|
|
28
|
+
export interface ListPostsParams {
|
|
29
|
+
type?: ContentType;
|
|
30
|
+
/** 1–100, default 20 (server-clamped). */
|
|
31
|
+
limit?: number;
|
|
32
|
+
offset?: number;
|
|
33
|
+
}
|
|
34
|
+
/** Thrown when the Content API returns a non-OK, non-404 response. */
|
|
35
|
+
export declare class BlogApiError extends Error {
|
|
36
|
+
readonly status: number;
|
|
37
|
+
readonly url: string;
|
|
38
|
+
constructor(message: string, status: number, url: string);
|
|
39
|
+
}
|
|
40
|
+
export interface BlogClient {
|
|
41
|
+
listPosts(params?: ListPostsParams, init?: RequestInit): Promise<BlogPostSummary[]>;
|
|
42
|
+
getPost(slug: string, init?: RequestInit): Promise<BlogPost | null>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create a Content API client bound to one tenant's site.
|
|
46
|
+
*
|
|
47
|
+
* Next.js: pass `init` with `{ next: { tags: ["ag-blog"], revalidate: 60 } }`
|
|
48
|
+
* to get ISR + on-demand revalidation (pair with the revalidate route handler).
|
|
49
|
+
*/
|
|
50
|
+
export declare function createBlogClient(options: BlogClientOptions): BlogClient;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @accountgroove/blog — framework-agnostic client for the AccountGroove
|
|
2
|
+
// Content API. Drop into an Astro/Next (or any JS) site to render a tenant's
|
|
3
|
+
// published blog on the site's own domain, with the site's own CSS.
|
|
4
|
+
//
|
|
5
|
+
// Zero dependencies: uses the global fetch/URL. Works in Node, Astro, Next,
|
|
6
|
+
// Cloudflare Workers, and the browser.
|
|
7
|
+
//
|
|
8
|
+
// The Content API surface it speaks to (public, read-only):
|
|
9
|
+
// GET {apiBase}/v1/sites/{locationId}/posts -> { posts: BlogPostSummary[] }
|
|
10
|
+
// GET {apiBase}/v1/sites/{locationId}/posts/{slug} -> { post: BlogPost }
|
|
11
|
+
/** Thrown when the Content API returns a non-OK, non-404 response. */
|
|
12
|
+
export class BlogApiError extends Error {
|
|
13
|
+
status;
|
|
14
|
+
url;
|
|
15
|
+
constructor(message, status, url) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.url = url;
|
|
19
|
+
this.name = "BlogApiError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create a Content API client bound to one tenant's site.
|
|
24
|
+
*
|
|
25
|
+
* Next.js: pass `init` with `{ next: { tags: ["ag-blog"], revalidate: 60 } }`
|
|
26
|
+
* to get ISR + on-demand revalidation (pair with the revalidate route handler).
|
|
27
|
+
*/
|
|
28
|
+
export function createBlogClient(options) {
|
|
29
|
+
const base = options.apiBase.replace(/\/+$/, "");
|
|
30
|
+
const doFetch = options.fetch ?? fetch;
|
|
31
|
+
const site = encodeURIComponent(options.locationId);
|
|
32
|
+
async function request(path, init) {
|
|
33
|
+
const url = `${base}/v1/sites/${site}${path}`;
|
|
34
|
+
let res;
|
|
35
|
+
try {
|
|
36
|
+
res = await doFetch(url, init);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
throw new BlogApiError(`Network error: ${err.message}`, 0, url);
|
|
40
|
+
}
|
|
41
|
+
return res;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
async listPosts(params = {}, init) {
|
|
45
|
+
const q = new URLSearchParams();
|
|
46
|
+
if (params.type)
|
|
47
|
+
q.set("type", params.type);
|
|
48
|
+
if (params.limit != null)
|
|
49
|
+
q.set("limit", String(params.limit));
|
|
50
|
+
if (params.offset != null)
|
|
51
|
+
q.set("offset", String(params.offset));
|
|
52
|
+
const qs = q.toString();
|
|
53
|
+
const res = await request(`/posts${qs ? `?${qs}` : ""}`, init);
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
throw new BlogApiError(`Failed to list posts (HTTP ${res.status})`, res.status, res.url);
|
|
56
|
+
}
|
|
57
|
+
const data = (await res.json());
|
|
58
|
+
return data.posts ?? [];
|
|
59
|
+
},
|
|
60
|
+
async getPost(slug, init) {
|
|
61
|
+
const res = await request(`/posts/${encodeURIComponent(slug)}`, init);
|
|
62
|
+
if (res.status === 404)
|
|
63
|
+
return null;
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
throw new BlogApiError(`Failed to get post (HTTP ${res.status})`, res.status, res.url);
|
|
66
|
+
}
|
|
67
|
+
const data = (await res.json());
|
|
68
|
+
return data.post ?? null;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@accountgroove/blog",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Client SDK for the AccountGroove Content API — render a tenant's published blog on their own Astro/Next site.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist", "README.md"],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"license": "UNLICENSED",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.6.3"
|
|
28
|
+
}
|
|
29
|
+
}
|