astro-buffalo 0.0.0-stage → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Agustín Díaz
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,3 +1,232 @@
1
- # Temporary Holding Version
1
+ # astro-buffalo
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Share newly published Astro articles to Buffer using its experimental `createContentItem` API.
4
+ One request per article, with a variant for each channel. No SDK, database, or browser JavaScript.
5
+
6
+ An independent integration; not affiliated with or endorsed by Buffer.
7
+
8
+ ## Setup
9
+
10
+ Requires Astro 7.x and Node 24 or newer.
11
+
12
+ ```sh
13
+ npm install astro-buffalo
14
+ ```
15
+
16
+ Configure your article endpoint and source directory in `astro.config.mjs`:
17
+
18
+ ```js
19
+ import { defineConfig } from "astro/config";
20
+ import buffer from "astro-buffalo";
21
+
22
+ export default defineConfig({
23
+ site: "https://example.com",
24
+ integrations: [
25
+ buffer({
26
+ posts: new URL("./src/buffer-posts.ts", import.meta.url),
27
+ contentDirectory: "src/content/blog",
28
+ }),
29
+ ],
30
+ });
31
+ ```
32
+
33
+ In `src/buffer-posts.ts`, adapt your content collection and public routes:
34
+
35
+ ```ts
36
+ import type { APIRoute } from "astro";
37
+ import { getCollection } from "astro:content";
38
+
39
+ export const GET: APIRoute = async ({ site }) => {
40
+ if (!site) throw new Error("Configure site before publishing.");
41
+ const posts = await getCollection("blog");
42
+ return Response.json(
43
+ posts.map((post) => {
44
+ if (!post.filePath) throw new Error(`Missing source path for "${post.id}".`);
45
+ return {
46
+ filePath: post.filePath,
47
+ title: post.data.title,
48
+ url: new URL(`/${post.id}/`, site).href,
49
+ };
50
+ }),
51
+ );
52
+ };
53
+ ```
54
+
55
+ The endpoint is injected only during publishing builds and removed before any API requests.
56
+
57
+ ### Article manifest
58
+
59
+ The endpoint must return a JSON array with these fields on each article:
60
+
61
+ | Field | Required | Meaning |
62
+ | --- | --- | --- |
63
+ | `filePath` | Yes | Source file path relative to the Astro project root, using forward slashes; no absolute paths or `.`/`..` segments. Must match the Git path, e.g. `src/content/blog/hello.md`. |
64
+ | `title` | Yes | Non-empty article title, also used as the default announcement. |
65
+ | `url` | Yes | Absolute HTTP(S) canonical production URL, without whitespace, credentials, or a fragment. Must match the deployed URL without a redirect. |
66
+ | `text` | No | Non-empty custom announcement text; the article URL is always appended. |
67
+ | `mode` | No | `shareNow` or `addToQueue`; overrides the integration's default for this article. |
68
+
69
+ An empty array is valid; invalid entries stop the build before any Buffer requests.
70
+ Adapt the collection name and routes to your site. Include only public articles, filtering drafts
71
+ and future dates if your schema defines them:
72
+
73
+ ```ts
74
+ const posts = await getCollection(
75
+ "blog",
76
+ ({ data }) => !data.draft && data.pubDate <= new Date(),
77
+ );
78
+ ```
79
+
80
+ ### Article detection
81
+
82
+ The integration compares `BUFFER_BASE_REF` with `HEAD`, sharing added articles and committed
83
+ drafts becoming public. Ordinary edits, published-article renames, and archive entries are ignored.
84
+ Paths are relative to the Astro project root.
85
+
86
+ For Markdown or MDX drafts, use a top-level YAML boolean `draft: true`. Keep drafts out of your
87
+ public routes and manifest. To publish, set it to `false` or remove it and include the article in
88
+ the manifest. Renaming a draft during publication is supported; sources still marked as drafts
89
+ are never shared.
90
+
91
+ Quoted strings, custom fields, and schema defaults are not draft flags. Your adapter handles
92
+ custom publication rules and future dates; a date passing without a source change does not trigger
93
+ an announcement. Blog drafts are not synchronized with Buffer.
94
+
95
+ ### Optional settings
96
+
97
+ By default, announcements publish immediately to every connected, unlocked channel.
98
+ To select channels or use their Buffer posting schedules, add:
99
+
100
+ ```js
101
+ channelIds: ["your_linkedin_channel_id", "your_bluesky_channel_id"],
102
+ mode: "addToQueue",
103
+ ```
104
+
105
+ `channelIds` must be non-empty and contain known IDs. Omit it to use all connected channels.
106
+ `mode` supports `shareNow` (default) and `addToQueue`; both use automatic delivery.
107
+ Disconnected or locked channels are reported and skipped.
108
+
109
+ For different choices per article, return `mode` in each manifest entry. Articles without a
110
+ mode use the integration setting.
111
+
112
+ Announcements use `title + "\n\n" + url`. Add `text: post.data.description` to an article's manifest
113
+ entry for custom copy; the content item's title stays unchanged. Custom text is trimmed and must
114
+ be non-empty. The URL is always appended for duplicate detection.
115
+
116
+ ## Publishing
117
+
118
+ Your site can be hosted anywhere. GitHub Actions is optional: run the publishing build locally
119
+ or in any CI environment with Node 24+ and Git. It needs a Git checkout containing both the
120
+ publishing commit (`HEAD`) and `BUFFER_BASE_REF`; source archives without Git history are not enough.
121
+ If your host cannot provide these, run publishing separately from deployment.
122
+
123
+ Normal builds and development never publish. Set these environment variables for a publishing build:
124
+
125
+ | Variable | Purpose |
126
+ | --- | --- |
127
+ | `BUFFER_PUBLISH=true` | Explicitly enable publishing. |
128
+ | `BUFFER_DRY_RUN=true` | Preview announcements without creating posts (also requires `BUFFER_PUBLISH=true`). |
129
+ | `BUFFER_API_KEY` | Buffer personal API key; keep this secret. |
130
+ | `BUFFER_ORGANIZATION_ID` | Organization containing the channels to share to. |
131
+ | `BUFFER_BASE_REF` | Full commit SHA before the articles were added or changed from draft to published. |
132
+
133
+ Create a personal API key in Buffer's **Settings → API** with `account:read`, `posts:read`,
134
+ and `posts:write` permissions.
135
+
136
+ Store the key in CI secrets or an ignored `.env` file. The integration reads `process.env`; it
137
+ does not load `.env` itself. Find your organization ID using Buffer's
138
+ `account { organizations { id name } }` query.
139
+
140
+ ### Preview locally
141
+
142
+ Add your key and organization ID to an ignored `.env` file:
143
+
144
+ ```dotenv
145
+ BUFFER_API_KEY=your_api_key
146
+ BUFFER_ORGANIZATION_ID=your_organization_id
147
+ ```
148
+
149
+ From the commit publishing the articles, run this POSIX shell command (macOS/Linux). Replace `HEAD^`
150
+ with the revision immediately before publication if the changes span multiple commits:
151
+
152
+ ```sh
153
+ BUFFER_PUBLISH=true BUFFER_DRY_RUN=true BUFFER_BASE_REF="$(git rev-parse HEAD^)" \
154
+ node --env-file=.env ./node_modules/astro/bin/astro.mjs build
155
+ ```
156
+
157
+ Dry runs require credentials and a base revision. They read channels and history, then print the
158
+ planned text, missing channels, and mode. They skip deployment checks and all writes; existing
159
+ failed posts still cause the run to fail.
160
+
161
+ ### GitHub Actions example
162
+
163
+ After installing the package and committing your lockfile, save this as
164
+ `.github/workflows/buffer.yml`. Configure a `BUFFER_API_KEY` repository secret and a
165
+ `BUFFER_ORGANIZATION_ID` repository variable. Adjust the branch and content path for your site.
166
+ Your normal production deployment must run separately; the integration waits for article URLs
167
+ to become available before creating announcements.
168
+
169
+ ```yaml
170
+ name: Share new articles
171
+
172
+ on:
173
+ push:
174
+ branches: [main]
175
+ paths: ["src/content/blog/**"]
176
+ workflow_dispatch:
177
+ inputs:
178
+ base_ref:
179
+ description: Full commit SHA before the new articles (reuse when retrying)
180
+ required: true
181
+
182
+ permissions:
183
+ contents: read
184
+
185
+ concurrency:
186
+ group: buffer-publishing
187
+ queue: max
188
+
189
+ jobs:
190
+ publish:
191
+ if: github.ref == 'refs/heads/main'
192
+ runs-on: ubuntu-latest
193
+ timeout-minutes: 15
194
+ steps:
195
+ - uses: actions/checkout@v4
196
+ with:
197
+ fetch-depth: 0
198
+ - uses: actions/setup-node@v4
199
+ with:
200
+ node-version: "24"
201
+ cache: npm
202
+ - run: npm ci
203
+ - run: node ./node_modules/astro/bin/astro.mjs build
204
+ env:
205
+ BUFFER_PUBLISH: "true"
206
+ BUFFER_API_KEY: ${{ secrets.BUFFER_API_KEY }}
207
+ BUFFER_ORGANIZATION_ID: ${{ vars.BUFFER_ORGANIZATION_ID }}
208
+ BUFFER_BASE_REF: ${{ github.event.before || inputs.base_ref }}
209
+ ```
210
+
211
+ For a CI preview, add `BUFFER_DRY_RUN: "true"` to the final step's environment. Keep publishing
212
+ out of pull-request and preview deployment workflows. For retries, use manual dispatch with
213
+ the original base SHA; do not replace it with the current commit.
214
+
215
+ ### Deployment and retries
216
+
217
+ Build from the exact publishing commit with the base revision available in Git. Production URLs
218
+ must respond successfully without redirects; the integration waits up to five minutes for deployment.
219
+ Announcements contain no media, so networks requiring it or extra metadata may reject them.
220
+
221
+ On failure, rerun with the same base SHA. Every history page is checked for matching URLs per
222
+ channel. Existing failed posts, drafts, or approval requests need attention in Buffer rather than
223
+ replacement posts; keep their records and URL text intact.
224
+
225
+ Validation finishes before any API requests, but later failures can occur after records are created.
226
+ Inspect Buffer for delivery errors. Creation requests are not retried automatically, and exactly-once
227
+ delivery is not guaranteed. Run publishing builds serially to avoid races.
228
+
229
+ For development and release instructions, see [CONTRIBUTING.md](CONTRIBUTING.md).
230
+
231
+ References: [Astro Integration API](https://docs.astro.build/en/reference/integrations-reference/)
232
+ and [Buffer content items](https://developers.buffer.com/guides/content-items).
@@ -0,0 +1,8 @@
1
+ import type { AstroIntegration } from "astro";
2
+ export interface BufferOptions {
3
+ posts: URL;
4
+ contentDirectory: string;
5
+ channelIds?: string[];
6
+ mode?: "shareNow" | "addToQueue";
7
+ }
8
+ export default function buffer({ posts: entrypoint, contentDirectory, channelIds, mode, }: BufferOptions): AstroIntegration;
package/dist/index.js ADDED
@@ -0,0 +1,285 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { readFile, unlink } from "node:fs/promises";
3
+ import timers from "node:timers/promises";
4
+ import { fileURLToPath } from "node:url";
5
+ import { z } from "astro/zod";
6
+ import { parse } from "yaml";
7
+ const articlesSchema = z.array(z.object({
8
+ filePath: z
9
+ .string()
10
+ .refine((path) => path.trim() &&
11
+ !path.startsWith("/") &&
12
+ !path.includes("\\") &&
13
+ !path.split("/").some((part) => part === "." || part === "..") &&
14
+ !/^[a-z]:/i.test(path), "filePath must be a project-relative path using forward slashes."),
15
+ title: z.string().refine((title) => title.trim(), "title must be a non-empty string."),
16
+ url: z.string().refine((value) => {
17
+ if (/\s/.test(value) || !URL.canParse(value))
18
+ return false;
19
+ const url = new URL(value);
20
+ return (["http:", "https:"].includes(url.protocol) && !url.username && !url.password && !url.hash);
21
+ }, "url must be an absolute HTTP(S) URL without whitespace, credentials, or a fragment."),
22
+ text: z
23
+ .string({ message: "announcement text must be a non-empty string." })
24
+ .refine((text) => text.trim(), "announcement text must be a non-empty string.")
25
+ .optional(),
26
+ mode: z.enum(["shareNow", "addToQueue"]).optional(),
27
+ }, { message: "must be an object." }), { message: "Buffer manifest must be an array of articles." });
28
+ function isDraft(source) {
29
+ const lines = (source.startsWith("\uFEFF") ? source.slice(1) : source).split("\n");
30
+ if (lines.length < 2 || lines[0].trimEnd() !== "---")
31
+ return false;
32
+ const end = lines.findIndex((line, index) => index > 0 && line.trimEnd() === "---");
33
+ if (end === -1)
34
+ throw new Error("Missing closing YAML frontmatter delimiter.");
35
+ const data = parse(lines.slice(1, end).join("\n"));
36
+ return typeof data === "object" && data !== null && "draft" in data && data.draft === true;
37
+ }
38
+ async function waitForDeployment(url, logger) {
39
+ const deadline = Date.now() + 300_000;
40
+ while (Date.now() < deadline) {
41
+ let response;
42
+ try {
43
+ response = await fetch(url, {
44
+ method: "HEAD",
45
+ signal: AbortSignal.timeout(10_000),
46
+ });
47
+ }
48
+ catch (error) {
49
+ if (!(error instanceof TypeError) &&
50
+ !(error instanceof Error && error.name === "TimeoutError")) {
51
+ throw error;
52
+ }
53
+ logger.warn(`Cannot reach ${url}: ${error.message}. Retrying.`);
54
+ }
55
+ if (response) {
56
+ if (response.ok && response.url === url)
57
+ return;
58
+ if (![404, 502, 503, 504].includes(response.status)) {
59
+ throw new Error(`Cannot verify ${url}: HTTP ${response.status}.`);
60
+ }
61
+ }
62
+ logger.info(`Waiting for ${url} to be deployed...`);
63
+ await timers.setTimeout(10_000);
64
+ }
65
+ throw new Error(`${url} is not live yet. Rerun after deployment succeeds.`);
66
+ }
67
+ async function request(token, query, variables) {
68
+ const response = await fetch("https://api.buffer.com", {
69
+ method: "POST",
70
+ headers: {
71
+ Authorization: `Bearer ${token}`,
72
+ "Content-Type": "application/json",
73
+ },
74
+ body: JSON.stringify({ query, variables }),
75
+ signal: AbortSignal.timeout(30_000),
76
+ });
77
+ if (!response.ok) {
78
+ const retryAfter = response.headers.get("Retry-After");
79
+ throw new Error(`Buffer request failed: HTTP ${response.status}.${retryAfter ? ` Retry after ${retryAfter} seconds.` : ""}`);
80
+ }
81
+ const result = await response.json();
82
+ if (result.errors?.length) {
83
+ throw new Error(`Buffer: ${result.errors.map((error) => error.message).join("; ")}`);
84
+ }
85
+ if (!result.data) {
86
+ throw new Error("Buffer returned no data.");
87
+ }
88
+ return result.data;
89
+ }
90
+ export default function buffer({ posts: entrypoint, contentDirectory, channelIds, mode = "shareNow", }) {
91
+ if (mode !== "shareNow" && mode !== "addToQueue") {
92
+ throw new Error('mode must be "shareNow" or "addToQueue".');
93
+ }
94
+ if (channelIds && (!channelIds.length || channelIds.some((id) => !id.trim()))) {
95
+ throw new Error("channelIds must contain at least one non-empty Buffer channel ID.");
96
+ }
97
+ let root;
98
+ let enabled = false;
99
+ const dryRun = process.env.BUFFER_DRY_RUN === "true";
100
+ return {
101
+ name: "astro-buffalo",
102
+ hooks: {
103
+ "astro:config:setup": ({ injectRoute, command }) => {
104
+ enabled = command === "build" && process.env.BUFFER_PUBLISH === "true";
105
+ if (enabled) {
106
+ injectRoute({
107
+ pattern: "/buffer-posts.json",
108
+ entrypoint: fileURLToPath(entrypoint),
109
+ prerender: true,
110
+ });
111
+ }
112
+ },
113
+ "astro:config:done": ({ config }) => {
114
+ root = config.root;
115
+ },
116
+ "astro:build:done": async ({ dir, logger }) => {
117
+ if (!enabled)
118
+ return;
119
+ const manifest = new URL("buffer-posts.json", dir);
120
+ const contents = await readFile(manifest, "utf8");
121
+ await unlink(manifest);
122
+ const result = articlesSchema.safeParse(JSON.parse(contents));
123
+ if (!result.success) {
124
+ throw new Error(result.error.issues
125
+ .map(({ path, message }) => [
126
+ typeof path[0] === "number" && `Buffer manifest article ${path[0] + 1}`,
127
+ path[1] && `(${String(path[1])})`,
128
+ message,
129
+ ]
130
+ .filter(Boolean)
131
+ .join(" "))
132
+ .join("\n"));
133
+ }
134
+ const articles = result.data;
135
+ const base = process.env.BUFFER_BASE_REF;
136
+ if (!base || !/^[a-f0-9]{40}$/i.test(base) || /^0+$/.test(base)) {
137
+ throw new Error("Set BUFFER_BASE_REF to the full commit SHA before the new posts.");
138
+ }
139
+ const git = (...args) => execFileSync("git", args, { cwd: fileURLToPath(root), encoding: "utf8" });
140
+ const changes = git("diff", "--name-status", "--relative", "-z", "--diff-filter=AMR", "--find-renames", base, "HEAD", "--", contentDirectory)
141
+ .split("\0")
142
+ .filter(Boolean);
143
+ const previousPaths = new Map();
144
+ for (let index = 0; index < changes.length;) {
145
+ const status = changes[index++];
146
+ const path = changes[index++];
147
+ if (status.startsWith("R")) {
148
+ previousPaths.set(changes[index++], path);
149
+ }
150
+ else {
151
+ previousPaths.set(path, status === "A" ? undefined : path);
152
+ }
153
+ }
154
+ const posts = articles
155
+ .filter(({ filePath }) => {
156
+ if (!previousPaths.has(filePath))
157
+ return false;
158
+ const previousPath = previousPaths.get(filePath);
159
+ if (previousPath && !isDraft(git("show", `${base}:./${previousPath}`))) {
160
+ return false;
161
+ }
162
+ return !isDraft(git("show", `HEAD:./${filePath}`));
163
+ })
164
+ .map((article) => ({
165
+ ...article,
166
+ text: `${article.text?.trim() ?? article.title}\n\n${article.url}`,
167
+ mode: article.mode ?? mode,
168
+ }));
169
+ if (!posts.length) {
170
+ logger.info("No new blog posts to share.");
171
+ return;
172
+ }
173
+ const token = process.env.BUFFER_API_KEY;
174
+ const organizationId = process.env.BUFFER_ORGANIZATION_ID;
175
+ if (!token || !organizationId) {
176
+ throw new Error("Set BUFFER_API_KEY and BUFFER_ORGANIZATION_ID before publishing.");
177
+ }
178
+ const { channels } = await request(token, `query Channels($input: ChannelsInput!) {
179
+ channels(input: $input) { id name isDisconnected isLocked }
180
+ }`, { input: { organizationId } });
181
+ if (channelIds) {
182
+ const unknown = channelIds.filter((id) => !channels.some((channel) => channel.id === id));
183
+ if (unknown.length) {
184
+ throw new Error(`Unknown Buffer channel IDs: ${unknown.join(", ")}.`);
185
+ }
186
+ }
187
+ const connected = channels.filter((channel) => {
188
+ if (channelIds && !channelIds.includes(channel.id))
189
+ return false;
190
+ if (channel.isDisconnected || channel.isLocked) {
191
+ logger.warn(`Cannot publish to "${channel.name}": disconnected or locked in Buffer.`);
192
+ return false;
193
+ }
194
+ return true;
195
+ });
196
+ if (!connected.length) {
197
+ throw new Error("No connected, unlocked Buffer channels found.");
198
+ }
199
+ const existing = [];
200
+ let after = null;
201
+ do {
202
+ const { posts: page } = await request(token, `query Posts($input: PostsInput!, $after: String) {
203
+ posts(input: $input, first: 100, after: $after) {
204
+ edges { node { id text channelId status } }
205
+ pageInfo { hasNextPage endCursor }
206
+ }
207
+ }`, {
208
+ input: { organizationId, filter: { channelIds: connected.map(({ id }) => id) } },
209
+ after,
210
+ });
211
+ if (!page.edges) {
212
+ throw new Error("Buffer returned no post history.");
213
+ }
214
+ existing.push(...page.edges.map(({ node }) => node));
215
+ if (page.pageInfo.hasNextPage &&
216
+ (!page.pageInfo.endCursor || page.pageInfo.endCursor === after)) {
217
+ throw new Error("Buffer returned an invalid pagination cursor.");
218
+ }
219
+ after = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
220
+ } while (after);
221
+ const failures = [];
222
+ for (const article of posts) {
223
+ const pending = [];
224
+ for (const channel of connected) {
225
+ const shared = existing.find((post) => post.channelId === channel.id && post.text.split(/\s+/).includes(article.url));
226
+ if (shared) {
227
+ if (!["sent", "scheduled", "sending"].includes(shared.status)) {
228
+ failures.push(new Error(`Review Buffer post ${shared.id} for "${channel.name}": ${shared.status}.`));
229
+ }
230
+ logger.info(`Already in Buffer: "${article.title}" -> "${channel.name}".`);
231
+ continue;
232
+ }
233
+ pending.push(channel);
234
+ }
235
+ if (!pending.length)
236
+ continue;
237
+ if (dryRun) {
238
+ logger.info(`Dry run: ${JSON.stringify({
239
+ title: article.title,
240
+ text: article.text,
241
+ channels: pending.map(({ id, name }) => ({ id, name })),
242
+ mode: article.mode,
243
+ })}`);
244
+ continue;
245
+ }
246
+ await waitForDeployment(article.url, logger);
247
+ const { createContentItem } = await request(token, `mutation Publish($input: CreateContentItemInput!) {
248
+ createContentItem(input: $input) {
249
+ ... on CreateContentItemSuccess { content { id } }
250
+ ... on CreateContentItemFailure {
251
+ message
252
+ errors { ... on MutationError { message } }
253
+ }
254
+ }
255
+ }`, {
256
+ input: {
257
+ organizationId,
258
+ title: article.title,
259
+ posts: pending.map(({ id }) => ({
260
+ channelId: id,
261
+ text: article.text,
262
+ schedulingType: "automatic",
263
+ mode: article.mode,
264
+ })),
265
+ },
266
+ });
267
+ if (!createContentItem.content?.id) {
268
+ failures.push(new Error(`"${article.title}": ${[
269
+ createContentItem.message ?? "No content item ID returned.",
270
+ ...(createContentItem.errors ?? []).map(({ message }) => message),
271
+ ].join("; ")}`));
272
+ continue;
273
+ }
274
+ logger.info(`Submitted "${article.title}" to ${pending.length} channels (${createContentItem.content.id}).`);
275
+ }
276
+ if (failures.length) {
277
+ throw new AggregateError(failures, [
278
+ "Some Buffer posts need attention; reruns skip existing posts.",
279
+ ...failures.map((error) => error.message),
280
+ ].join("\n"));
281
+ }
282
+ },
283
+ },
284
+ };
285
+ }
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "astro-buffalo",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Share newly published Astro articles through Buffer.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/HiroAgustin/astro-buffalo#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/HiroAgustin/astro-buffalo.git"
10
+ },
11
+ "keywords": [
12
+ "astro-integration",
13
+ "withastro",
14
+ "buffer",
15
+ "social-media"
16
+ ],
17
+ "type": "module",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "test": "npm run build && node --test test/*.test.mjs",
30
+ "format:check": "prettier --check src test",
31
+ "prepare": "npm run build"
32
+ },
33
+ "engines": {
34
+ "node": ">=24"
35
+ },
36
+ "peerDependencies": {
37
+ "astro": "^7.0.0"
38
+ },
39
+ "dependencies": {
40
+ "yaml": "^2.8.1"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^26.6.6",
44
+ "astro": "^7.4.5",
45
+ "prettier": "^3.9.10",
46
+ "typescript": "^6.0.3"
47
+ }
48
+ }