astro-atmosphere-publishing 0.1.0 → 0.1.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/CHANGELOG.md +6 -1
- package/README.md +18 -3
- package/bin/cli.mjs +4 -3
- package/index.d.ts +13 -1
- package/lib/account.mjs +3 -0
- package/lib/config.mjs +3 -0
- package/lib/entries.mjs +5 -2
- package/lib/publish.mjs +46 -5
- package/lib/publishers/standard-site.mjs +1 -1
- package/lib/text.mjs +22 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1 (7 November 2026)
|
|
4
|
+
|
|
5
|
+
- **@mentions, #hashtags and links in announcements work**, as in the Bluesky app: they become links, and mentioned accounts are notified. The preview lists what each announcement will link.
|
|
6
|
+
- **`share` setting:** how much of each post goes into its record's searchable text: `"more"` (default, as before: everything above `<!--more-->`, or the whole post without a marker), `"intro"` (the first paragraph when there's no marker), `"full"` or `"none"`. Set it for the site, a collection, or a post (`share:` in its frontmatter). Nothing changes for existing sites.
|
|
7
|
+
|
|
8
|
+
## 0.1.0 (7 November 2026)
|
|
4
9
|
|
|
5
10
|
First release, built from saltedlolly.com's own publishing script.
|
|
6
11
|
|
package/README.md
CHANGED
|
@@ -92,12 +92,26 @@ npx atmosphere-publishing publish # do it
|
|
|
92
92
|
|
|
93
93
|
`publish` needs `ATPROTO_HANDLE` and `ATPROTO_APP_PASSWORD`, from the environment or a `.env` file. Then commit `src/data/atmosphere-publishing.json`, build and deploy.
|
|
94
94
|
|
|
95
|
+
## How much of each post is shared
|
|
96
|
+
|
|
97
|
+
Each record carries the post's title, description, tags, dates and cover image, plus searchable text: always plain text, with no formatting, images or link addresses. Reader apps link to your site to read the post. The `share` setting decides how much text that is:
|
|
98
|
+
|
|
99
|
+
| `share` | Searchable text |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `"more"` (default) | Everything above `<!--more-->`, or the whole post when there's no marker |
|
|
102
|
+
| `"intro"` | Everything above `<!--more-->`, or just the first paragraph when there's no marker |
|
|
103
|
+
| `"full"` | The whole post |
|
|
104
|
+
| `"none"` | None: the record keeps its title and description |
|
|
105
|
+
|
|
106
|
+
Set it for the whole site (`share` in the settings), a collection, or one post (`share: full` in its frontmatter).
|
|
107
|
+
|
|
95
108
|
## Announcements
|
|
96
109
|
|
|
97
110
|
- **New posts** (published within `announceWithinDays`, default 14) are announced, dated now.
|
|
98
111
|
- The announcement's text is the post's own wording, `announce: "…"` in its frontmatter, shown above the link card. **Write it for every post:** the card already shows the title and description, so say something new. Without it, the announcement is the link card alone.
|
|
99
112
|
- **Older posts** are announced only when they ask (`announce: "…"`, or `announce: true` for the card alone). Their announcement is dated to the post itself, so Bluesky files it in the past and it doesn't reach followers' timelines, but it still gives the post a comment thread.
|
|
100
113
|
- `announce: false` never announces a post.
|
|
114
|
+
- @mentions, #hashtags and links in the wording work as in the Bluesky app: they become links, and mentioned accounts are notified. The preview lists what each announcement will link.
|
|
101
115
|
- Each post is announced once.
|
|
102
116
|
|
|
103
117
|
## With Atmosphere Comments for Astro
|
|
@@ -160,7 +174,8 @@ Commit the state file even when publishing fails part-way: it's saved after ever
|
|
|
160
174
|
| `site` | required | Your site's address. |
|
|
161
175
|
| `account` | required | The publishing account's DID. |
|
|
162
176
|
| `publication.name`, `.description`, `.icon` | name required | The publication record. |
|
|
163
|
-
| `collections` | required | One or more: `files` (a glob) and `path` (a function), plus optional overrides: `id`, `title`, `description`, `textContent`, `tags`, `publishedAt`, `updatedAt`, `cover`, `draft` and `archived` (posts never treated as new, such as ones imported from an old site). Each takes the post (`id`, `file`, `folder`, `data` for the frontmatter, `body`). |
|
|
177
|
+
| `collections` | required | One or more: `files` (a glob) and `path` (a function), plus optional overrides: `id`, `title`, `description`, `share`, `textContent`, `tags`, `publishedAt`, `updatedAt`, `cover`, `draft` and `archived` (posts never treated as new, such as ones imported from an old site). Each takes the post (`id`, `file`, `folder`, `data` for the frontmatter, `body`). |
|
|
178
|
+
| `share` | default `"more"` | How much of each post goes into its record (see "How much of each post is shared"). |
|
|
164
179
|
| `announceWithinDays` | default 14 | How recent a post must be to count as new. |
|
|
165
180
|
| `langs` | default `["en"]` | The announcements' languages. |
|
|
166
181
|
|
|
@@ -168,11 +183,11 @@ Commit the state file even when publishing fails part-way: it's saved after ever
|
|
|
168
183
|
|
|
169
184
|
- Node 22.13 or later, Astro 5 or later.
|
|
170
185
|
- Markdown (`.md`, `.mdx`) posts. Collections from other loaders (a CMS, say) aren't supported yet.
|
|
171
|
-
- Records carry plain text for search, not the full formatted post.
|
|
186
|
+
- Records carry plain text for search (see `share`), not the full formatted post.
|
|
172
187
|
|
|
173
188
|
## Saying thanks
|
|
174
189
|
|
|
175
|
-
It's free and open source (MIT). If it's useful, a link to [the post about it on saltedlolly.com](https://saltedlolly.com) helps other developers find it. You can also support the work through [GitHub Sponsors](https://github.com/sponsors/saltedlolly) or [Ko-fi](https://ko-fi.com/saltedlolly).
|
|
190
|
+
It's free and open source (MIT). If it's useful, a link to [the post about it on saltedlolly.com](https://saltedlolly.com/atmosphere-publishing-for-astro/) helps other developers find it. You can also support the work through [GitHub Sponsors](https://github.com/sponsors/saltedlolly) or [Ko-fi](https://ko-fi.com/saltedlolly).
|
|
176
191
|
|
|
177
192
|
## License
|
|
178
193
|
|
package/bin/cli.mjs
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// there is read too, if present.
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
11
|
import { loadConfig } from "../lib/config.mjs";
|
|
12
|
-
import { describe, plan, publish } from "../lib/publish.mjs";
|
|
13
|
-
import { reader } from "../lib/account.mjs";
|
|
12
|
+
import { describe, linkedFor, plan, publish } from "../lib/publish.mjs";
|
|
13
|
+
import { publicAgent, reader } from "../lib/account.mjs";
|
|
14
14
|
import { saveState } from "../lib/state.mjs";
|
|
15
15
|
import { syncState } from "../lib/sync.mjs";
|
|
16
16
|
|
|
@@ -21,7 +21,8 @@ async function main() {
|
|
|
21
21
|
if (existsSync(".env")) process.loadEnvFile(".env");
|
|
22
22
|
const config = await loadConfig();
|
|
23
23
|
if (command === "preview") {
|
|
24
|
-
const
|
|
24
|
+
const result = await plan(config);
|
|
25
|
+
const lines = describe(result, config, undefined, await linkedFor(result, publicAgent()));
|
|
25
26
|
if (!lines.some((line) => !line.startsWith("note:"))) {
|
|
26
27
|
for (const line of lines) console.log(` • ${line}`);
|
|
27
28
|
console.log("Everything is already published and up to date.");
|
package/index.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type CollectionSettings = {
|
|
|
23
23
|
title?: (post: Entry) => string;
|
|
24
24
|
/** Default: the frontmatter's description, or the first paragraph. */
|
|
25
25
|
description?: (post: Entry) => string;
|
|
26
|
-
/** Plain text for search
|
|
26
|
+
/** Plain text for search, replacing `share`'s choice. */
|
|
27
27
|
textContent?: (post: Entry) => string;
|
|
28
28
|
tags?: (post: Entry) => string[];
|
|
29
29
|
/** Default: pubDate, publishDate or date. */
|
|
@@ -32,12 +32,16 @@ export type CollectionSettings = {
|
|
|
32
32
|
updatedAt?: (post: Entry) => string | Date | undefined;
|
|
33
33
|
/** The cover image file, relative to the post's folder. Default: heroImage, cover or image. */
|
|
34
34
|
cover?: (post: Entry) => string | undefined;
|
|
35
|
+
/** How much text this collection's records share (see AtmospherePublishingConfig.share). A post's own `share:` overrides it. */
|
|
36
|
+
share?: Share;
|
|
35
37
|
/** Default: the frontmatter's draft. Drafts are never published. */
|
|
36
38
|
draft?: (post: Entry) => boolean;
|
|
37
39
|
/** Posts that are never "new" (imported from an old site, say), so they're only announced when they ask. */
|
|
38
40
|
archived?: (post: Entry) => boolean;
|
|
39
41
|
};
|
|
40
42
|
|
|
43
|
+
export type Share = "more" | "intro" | "full" | "none";
|
|
44
|
+
|
|
41
45
|
export type AtmospherePublishingConfig = {
|
|
42
46
|
/** Your site's address, e.g. "https://example.com". */
|
|
43
47
|
site: string;
|
|
@@ -50,6 +54,14 @@ export type AtmospherePublishingConfig = {
|
|
|
50
54
|
icon?: string;
|
|
51
55
|
};
|
|
52
56
|
collections: CollectionSettings[];
|
|
57
|
+
/**
|
|
58
|
+
* How much of each post goes into its record's searchable text (always plain text):
|
|
59
|
+
* "more" (default) everything above <!--more-->, or the whole post when there's no marker;
|
|
60
|
+
* "intro" everything above <!--more-->, or the first paragraph when there's no marker;
|
|
61
|
+
* "full" the whole post; "none" nothing (the record keeps its title and description).
|
|
62
|
+
* A collection's `share`, or a post's own `share:` in its frontmatter, overrides it.
|
|
63
|
+
*/
|
|
64
|
+
share?: Share;
|
|
53
65
|
/** Posts published within this many days are announced as new. Default 14. */
|
|
54
66
|
announceWithinDays?: number;
|
|
55
67
|
/** The announcements' languages. Default ["en"]. */
|
package/lib/account.mjs
CHANGED
|
@@ -33,6 +33,9 @@ export async function signIn(account) {
|
|
|
33
33
|
return agent;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** Bluesky's public service, for looking up handles without signing in (the preview's @mentions). */
|
|
37
|
+
export const publicAgent = () => new AtpAgent({ service: "https://public.api.bsky.app" });
|
|
38
|
+
|
|
36
39
|
/** An agent for reading the account's public records, without signing in. */
|
|
37
40
|
export async function reader(account) {
|
|
38
41
|
return new AtpAgent({ service: await hostOf(account) });
|
package/lib/config.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { SHARE_OPTIONS } from "./text.mjs";
|
|
7
8
|
|
|
8
9
|
export const CONFIG_FILE = "atmosphere-publishing.config.mjs";
|
|
9
10
|
export const STATE_FILE = "src/data/atmosphere-publishing.json";
|
|
@@ -27,6 +28,7 @@ export async function loadConfig(root = process.cwd()) {
|
|
|
27
28
|
if (!DID.test(config?.account ?? "")) problems.push("`account` must be the DID of the account that publishes (did:plc:… or did:web:…)");
|
|
28
29
|
if (!config?.publication?.name) problems.push("`publication.name` is required");
|
|
29
30
|
if (!config?.collections?.length) problems.push("`collections` must list at least one collection");
|
|
31
|
+
if (config?.share !== undefined && !SHARE_OPTIONS.includes(config.share)) problems.push(`\`share\` must be one of ${SHARE_OPTIONS.join(", ")}`);
|
|
30
32
|
for (const [index, collection] of (config?.collections ?? []).entries()) {
|
|
31
33
|
if (!collection.files) problems.push(`collections[${index}].files must be a glob such as "src/content/blog/**/*.md"`);
|
|
32
34
|
if (typeof collection.path !== "function") problems.push(`collections[${index}].path must be a function giving each post's address`);
|
|
@@ -39,6 +41,7 @@ export async function loadConfig(root = process.cwd()) {
|
|
|
39
41
|
publication: { description: "", ...config.publication },
|
|
40
42
|
collections: config.collections,
|
|
41
43
|
announceWithinDays: config.announceWithinDays ?? 14,
|
|
44
|
+
share: config.share ?? "more",
|
|
42
45
|
langs: config.langs ?? ["en"],
|
|
43
46
|
stateFile: path.join(root, STATE_FILE),
|
|
44
47
|
};
|
package/lib/entries.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { glob, readFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { parse as parseYaml } from "yaml";
|
|
7
|
-
import {
|
|
7
|
+
import { SHARE_OPTIONS, firstParagraph, sharedText } from "./text.mjs";
|
|
8
8
|
|
|
9
9
|
const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
10
10
|
|
|
@@ -39,6 +39,9 @@ export async function readEntries(config) {
|
|
|
39
39
|
const publishedAt = dateOf((collection.publishedAt ?? ((e) => e.data.pubDate ?? e.data.publishDate ?? e.data.date))(entry));
|
|
40
40
|
if (!publishedAt || Number.isNaN(publishedAt.valueOf())) throw new Error(`${relative}: no publication date (pubDate, publishDate or date).`);
|
|
41
41
|
const postPath = collection.path({ ...entry, publishedAt });
|
|
42
|
+
// How much text the record shares: the post's own `share:`, then the collection's, then the site's.
|
|
43
|
+
const share = entry.data.share ?? collection.share ?? config.share;
|
|
44
|
+
if (!SHARE_OPTIONS.includes(share)) throw new Error(`${relative}: share must be one of ${SHARE_OPTIONS.join(", ")}.`);
|
|
42
45
|
const cover = (collection.cover ?? ((e) => e.data.heroImage ?? e.data.cover ?? e.data.image))(entry);
|
|
43
46
|
// The field order matters: it is part of each record's fingerprint.
|
|
44
47
|
posts.push({
|
|
@@ -47,7 +50,7 @@ export async function readEntries(config) {
|
|
|
47
50
|
path: postPath,
|
|
48
51
|
title: (collection.title ?? ((e) => e.data.title))(entry),
|
|
49
52
|
description: (collection.description ?? ((e) => e.data.description || firstParagraph(e.body)))(entry),
|
|
50
|
-
textContent: (collection.textContent ?? ((e) =>
|
|
53
|
+
textContent: (collection.textContent ?? ((e) => sharedText(e.body, share)))(entry),
|
|
51
54
|
tags: (collection.tags ?? ((e) => e.data.tags ?? []))(entry),
|
|
52
55
|
publishedAt,
|
|
53
56
|
updatedAt: dateOf((collection.updatedAt ?? ((e) => e.data.updatedDate ?? e.data.updatedAt))(entry)),
|
package/lib/publish.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// posts (or older posts that ask), which becomes the post's comment thread.
|
|
4
4
|
import { readEntries } from "./entries.mjs";
|
|
5
5
|
import { STANDARD_SITE, isEmpty, readState, saveState } from "./state.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { RichText } from "@atproto/api";
|
|
7
|
+
import { publicAgent, signIn } from "./account.mjs";
|
|
7
8
|
import { syncState } from "./sync.mjs";
|
|
8
9
|
import {
|
|
9
10
|
DOCUMENT,
|
|
@@ -20,6 +21,33 @@ const rkeyOf = (uri) => uri.split("/").pop();
|
|
|
20
21
|
/** The post's own `announce: "…"` wording, or none: the link card alone already shows its title and introduction. */
|
|
21
22
|
export const announcementText = (post) => (typeof post.announce === "string" ? post.announce : "");
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The announcement's text with its @mentions, #hashtags and links marked, as
|
|
26
|
+
* the Bluesky app does, so they're links and mentioned accounts are notified.
|
|
27
|
+
* A mention of a handle that doesn't exist stays plain text.
|
|
28
|
+
*/
|
|
29
|
+
export async function richTextOf(text, agent) {
|
|
30
|
+
const rich = new RichText({ text });
|
|
31
|
+
try {
|
|
32
|
+
await rich.detectFacets(agent);
|
|
33
|
+
} catch {
|
|
34
|
+
rich.detectFacetsWithoutResolution();
|
|
35
|
+
}
|
|
36
|
+
// A handle that couldn't be found leaves a mention with no account: Bluesky
|
|
37
|
+
// rejects those, so they stay plain text.
|
|
38
|
+
rich.facets = (rich.facets ?? []).filter(
|
|
39
|
+
(facet) => !facet.features.some((feature) => feature.$type === "app.bsky.richtext.facet#mention" && !feature.did),
|
|
40
|
+
);
|
|
41
|
+
return rich;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** What the announcement's text will link, for the preview. */
|
|
45
|
+
export function linkedParts(rich, text) {
|
|
46
|
+
const bytes = new TextEncoder().encode(text);
|
|
47
|
+
const decode = (start, end) => new TextDecoder().decode(bytes.slice(start, end));
|
|
48
|
+
return (rich.facets ?? []).map((facet) => decode(facet.index.byteStart, facet.index.byteEnd));
|
|
49
|
+
}
|
|
50
|
+
|
|
23
51
|
/** New posts are announced with today's date, so followers see them. */
|
|
24
52
|
export const isNew = (post, config) =>
|
|
25
53
|
!post.archived && (Date.now() - post.publishedAt.valueOf()) / 86_400_000 <= config.announceWithinDays;
|
|
@@ -57,8 +85,8 @@ export async function plan(config, state = readState(config.stateFile)) {
|
|
|
57
85
|
return { posts, state, pubHash, publicationChange, changes, orphans };
|
|
58
86
|
}
|
|
59
87
|
|
|
60
|
-
/** The plan in plain words, as the preview prints it. */
|
|
61
|
-
export function describe(result, config, handle) {
|
|
88
|
+
/** The plan in plain words, as the preview prints it. `linked` lists, by post id, what each announcement will link. */
|
|
89
|
+
export function describe(result, config, handle, linked = {}) {
|
|
62
90
|
const lines = [];
|
|
63
91
|
if (result.publicationChange) lines.push(`${result.publicationChange} the publication record (${config.site})`);
|
|
64
92
|
for (const { post, action, announce } of result.changes) {
|
|
@@ -74,6 +102,7 @@ export function describe(result, config, handle) {
|
|
|
74
102
|
(text ? text.split("\n").map((line) => ` │ ${line}`).join("\n") : " │ (no announce: wording, so no text: just the link card)") +
|
|
75
103
|
`\n │ [link card: ${post.title} — ${post.url}]\n` +
|
|
76
104
|
" └────────────────────────────────────────\n" +
|
|
105
|
+
(text ? ` Linked: ${linked[post.id]?.length ? linked[post.id].join(", ") : "nothing (no @mentions, #hashtags or links found)"}\n` : "") +
|
|
77
106
|
` To change the wording, add announce: "…" to ${post.source};\n` +
|
|
78
107
|
" announce: false skips the announcement.",
|
|
79
108
|
);
|
|
@@ -82,6 +111,16 @@ export function describe(result, config, handle) {
|
|
|
82
111
|
return lines;
|
|
83
112
|
}
|
|
84
113
|
|
|
114
|
+
/** What each planned announcement will link, by post id (mentions are checked with `agent`). */
|
|
115
|
+
export async function linkedFor(result, agent) {
|
|
116
|
+
const linked = {};
|
|
117
|
+
for (const { post, announce } of result.changes) {
|
|
118
|
+
const text = announcementText(post);
|
|
119
|
+
if (announce && text) linked[post.id] = linkedParts(await richTextOf(text, agent), text);
|
|
120
|
+
}
|
|
121
|
+
return linked;
|
|
122
|
+
}
|
|
123
|
+
|
|
85
124
|
/** Publishes everything the plan lists. Saves the state file after each record, so an interruption loses nothing. */
|
|
86
125
|
export async function publish(config, log = console.log) {
|
|
87
126
|
const agent = await signIn(config.account);
|
|
@@ -93,7 +132,7 @@ export async function publish(config, log = console.log) {
|
|
|
93
132
|
await saveState(config.stateFile, state);
|
|
94
133
|
}
|
|
95
134
|
const result = await plan(config, state);
|
|
96
|
-
const lines = describe(result, config, agent.session.handle);
|
|
135
|
+
const lines = describe(result, config, agent.session.handle, await linkedFor(result, agent));
|
|
97
136
|
if (!lines.some((line) => !line.startsWith("note:"))) {
|
|
98
137
|
for (const line of lines) log(` • ${line}`);
|
|
99
138
|
log("Everything is already published and up to date.");
|
|
@@ -124,8 +163,10 @@ export async function publish(config, log = console.log) {
|
|
|
124
163
|
for (const { post, announce } of result.changes) {
|
|
125
164
|
const cover = post.coverFile ? await upload(await coverImage(post.coverFile), "image/jpeg") : undefined;
|
|
126
165
|
if (announce) {
|
|
166
|
+
const rich = await richTextOf(announcementText(post), agent);
|
|
127
167
|
const created = await agent.post({
|
|
128
|
-
text:
|
|
168
|
+
text: rich.text,
|
|
169
|
+
...(rich.facets?.length && { facets: rich.facets }),
|
|
129
170
|
langs: config.langs,
|
|
130
171
|
createdAt: announcementDate(post, config).toISOString(),
|
|
131
172
|
embed: { $type: "app.bsky.embed.external", external: { uri: post.url, title: post.title, description: post.description, thumb: cover } },
|
|
@@ -58,7 +58,7 @@ export function documentRecord(post, publicationUri, cover, announcement) {
|
|
|
58
58
|
path: post.path,
|
|
59
59
|
title: post.title,
|
|
60
60
|
description: post.description,
|
|
61
|
-
textContent: post.textContent,
|
|
61
|
+
...(post.textContent && { textContent: post.textContent }),
|
|
62
62
|
tags: post.tags,
|
|
63
63
|
publishedAt: post.publishedAt.toISOString(),
|
|
64
64
|
...(post.updatedAt && { updatedAt: post.updatedAt.toISOString() }),
|
package/lib/text.mjs
CHANGED
|
@@ -30,6 +30,28 @@ export function previewMarkdown(markdown) {
|
|
|
30
30
|
return MORE_MARKER.test(markdown) ? markdown.split(MORE_MARKER)[0] : markdown;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The searchable text for a post's record, by the `share` setting:
|
|
35
|
+
* "more" everything above <!--more-->, or the whole post when there's no marker (default)
|
|
36
|
+
* "intro" everything above <!--more-->, or the first paragraph when there's no marker
|
|
37
|
+
* "full" the whole post
|
|
38
|
+
* "none" nothing (the record still has the title and description)
|
|
39
|
+
* Always plain text: no formatting, images or link addresses.
|
|
40
|
+
*/
|
|
41
|
+
export function sharedText(markdown, share = "more") {
|
|
42
|
+
if (share === "none") return "";
|
|
43
|
+
if (share === "full") return plainParagraphs(markdown.replace(MORE_MARKER, "")).join("\n\n");
|
|
44
|
+
if (share === "intro" && !MORE_MARKER.test(markdown)) {
|
|
45
|
+
const [first = ""] = plainParagraphs(
|
|
46
|
+
markdown.split(/\n{2,}/).filter((paragraph) => !paragraph.trim().startsWith("#")).join("\n\n"),
|
|
47
|
+
);
|
|
48
|
+
return first;
|
|
49
|
+
}
|
|
50
|
+
return plainParagraphs(previewMarkdown(markdown)).join("\n\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const SHARE_OPTIONS = ["more", "intro", "full", "none"];
|
|
54
|
+
|
|
33
55
|
/** The first paragraph with text (headings skipped), cut to 160 characters. */
|
|
34
56
|
export function firstParagraph(markdown) {
|
|
35
57
|
const text = plainParagraphs(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-atmosphere-publishing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Publish your Astro site to the Atmosphere with Standard.site: records for every post, Bluesky announcements, and the tags that connect each page to them. Pairs with Atmosphere Comments for Astro.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|