@micropage-sh/cli 2.6.2 → 2.6.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # micropage CLI
2
2
 
3
- Command-line interface for [micropage.sh](https://app.micropage.sh) — create, edit, publish, and manage microsites from your terminal.
3
+ Command-line interface for [micropage.sh](https://micropage.sh): keep your `.page` files in git and create, edit, publish and manage your sites from the terminal.
4
4
 
5
5
  ## Requirements
6
6
 
@@ -48,7 +48,7 @@ nano landing.page
48
48
  # Push local content as a draft build
49
49
  micropage push
50
50
 
51
- # Publish the current draft (deploys to Cloudflare Pages)
51
+ # Publish the current draft (deploys it live)
52
52
  micropage publish
53
53
 
54
54
  # Pull the latest remote build content to landing.page
@@ -73,6 +73,7 @@ Posts live in the project's `posts/` folder as Markdown files with YAML front-ma
73
73
  ---
74
74
  title: Hello, world
75
75
  slug: hello
76
+ date: 2026-07-07
76
77
  description: A short summary for the archive, meta description, and og tags.
77
78
  visibility: listed
78
79
  hero: ./hello.jpg
@@ -84,7 +85,7 @@ preview: The first post on this site.
84
85
  Body content goes here as standard Markdown.
85
86
  ```
86
87
 
87
- `title` is required. `slug` defaults to the filename minus a leading `YYYY-MM-DD-` date prefix. `visibility` is `listed` (default, appears in the site's `/content` index) or `unlisted`. `list` names a newsletter form and is required to email the post on publish. Local image references in the body are auto-uploaded and rewritten to hosted URLs on `push`.
88
+ `title` is required. `slug` defaults to the filename minus a leading `YYYY-MM-DD-` date prefix. `date` is the post's public date: `YYYY-MM-DD` (midnight UTC) or a full ISO timestamp; it sets the published date of a live post, and a draft keeps it until its first publish. Omit it to use the publish time. Future dates are rejected (scheduling isn't supported); `posts pull` writes it back as the UTC day for published posts. `visibility` is `listed` (default, appears in the site's `/content` index) or `unlisted`. `list` names a newsletter form and is required to email the post on publish. Local image references in the body are auto-uploaded and rewritten to hosted URLs on `push`.
88
89
 
89
90
  ```bash
90
91
  # Save posts/hello.md as a draft
@@ -115,7 +116,7 @@ See [`micropage posts`](#posts) below for the full command set.
115
116
  |---|---|
116
117
  | `micropage projects list [--json]` | List your projects |
117
118
  | `micropage projects show [id] [--json]` | Show project + latest build details |
118
- | `micropage projects create <name> [-d domain]` | Create a project and init local folder. Omit `-d` to let the API assign `{slugified-name}-{6 hex}` (unique Pages/DNS label, max 58 chars). Use `-d` only to override the slug. |
119
+ | `micropage projects create <name> [-d domain]` | Create a project and init local folder. Omit `-d` to let the API assign `{slugified-name}-{6 hex}` (unique `<slug>.micropage.sh` label, max 58 chars). Use `-d` only to override the slug. |
119
120
  | `micropage projects fetch <uuid\|domain>` | Fetch an existing project and init local folder (includes storage → `./assets` sync) |
120
121
  | `micropage projects pull` | Pull latest build raw content → `landing.page`, then sync `./assets` with project storage (1:1) |
121
122
  | `micropage projects delete [-y]` | Delete project (remote + local `.micropage/`) |
@@ -132,7 +133,7 @@ When you create a new project, the CLI also scaffolds:
132
133
  | Command | Description |
133
134
  |---|---|
134
135
  | `micropage push` | Merge local `.page` files and save as a draft build |
135
- | `micropage publish` | Push then deploy to Cloudflare Pages |
136
+ | `micropage publish` | Push then deploy the site live |
136
137
  | `micropage builds list [--json]` | List builds for the current project |
137
138
  | `micropage builds redeploy [version]` | Re-publish an older build as a new deploy |
138
139
  | `micropage builds download [version] [-o file]` | Download a deployed build archive as a `.zip` file |
@@ -227,7 +228,7 @@ If the project folder contains a root-level `llms.txt`, its contents are publish
227
228
  Each project folder has a `.micropage/project.json` file that stores:
228
229
  - `projectId` — the Supabase project ID
229
230
  - `buildId` — the ID of the last draft/deployed build (auto-updated by push/pull)
230
- - `domain` — the Cloudflare Pages domain
231
+ - `domain` — the project slug; the site lives at `<domain>.micropage.sh`
231
232
  - `name` — the project name
232
233
 
233
234
  Add `.micropage/` to your `.gitignore` if you share the content folder.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@micropage-sh/cli",
3
- "version": "2.6.2",
4
- "description": "CLI for micropage.sh - create, sync, and publish microsites",
3
+ "version": "2.6.4",
4
+ "description": "Micropage CLI: keep your .page files in git and publish static sites from the terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "micropage": "bin/micropage.js"
@@ -92,11 +92,84 @@ async function resolveFormId(projectId, listName) {
92
92
  return matches[0].id;
93
93
  }
94
94
 
95
+ const DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
96
+ const ISO_DATETIME_RE =
97
+ /^(\d{4}-\d{2}-\d{2})[Tt ](\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)\s*([Zz]|[+-]\d{2}(?::?\d{2})?)?$/;
98
+ // Date-only values are midnight UTC server-side; allow "today" anywhere on
99
+ // Earth (UTC+14) so a local-calendar date isn't rejected as future.
100
+ const DATE_ONLY_FUTURE_SLACK_MS = 14 * 60 * 60 * 1000;
101
+ const DATETIME_FUTURE_SLACK_MS = 5 * 60 * 1000;
102
+
103
+ function utcDateString(d) {
104
+ return d.toISOString().slice(0, 10);
105
+ }
106
+
107
+ function isoOffset(raw) {
108
+ if (!raw || raw === 'z' || raw === 'Z') return 'Z';
109
+ const digits = raw.slice(1).replace(':', '');
110
+ return `${raw[0]}${digits.slice(0, 2)}:${digits.slice(2) || '00'}`;
111
+ }
112
+
113
+ function isMidnightUtc(d) {
114
+ return (
115
+ d.getUTCHours() === 0 && d.getUTCMinutes() === 0 && d.getUTCSeconds() === 0 && d.getUTCMilliseconds() === 0
116
+ );
117
+ }
118
+
119
+ /**
120
+ * Normalize a front-matter `date` into what upsert-post accepts: `YYYY-MM-DD`
121
+ * or a full ISO timestamp. js-yaml turns unquoted dates into Date objects
122
+ * (UTC), quoted ones stay strings. Returns null when absent; throws on
123
+ * invalid or future values (scheduling isn't supported).
124
+ */
125
+ function normalizePostDate(value, now = new Date()) {
126
+ if (value === undefined || value === null || value === '') return null;
127
+
128
+ let normalized;
129
+ if (value instanceof Date) {
130
+ if (Number.isNaN(value.getTime())) throw new Error('invalid "date" (not a valid date)');
131
+ normalized = isMidnightUtc(value) ? utcDateString(value) : value.toISOString();
132
+ } else if (typeof value === 'string') {
133
+ const s = value.trim();
134
+ const dateOnly = DATE_ONLY_RE.exec(s);
135
+ if (dateOnly) {
136
+ const d = new Date(`${s}T00:00:00.000Z`);
137
+ if (Number.isNaN(d.getTime()) || utcDateString(d) !== s) {
138
+ throw new Error(`invalid "date" (${s}); use YYYY-MM-DD or an ISO timestamp`);
139
+ }
140
+ normalized = s;
141
+ } else {
142
+ const m = ISO_DATETIME_RE.exec(s);
143
+ // A timestamp without an offset is read as UTC, matching how YAML
144
+ // resolves unquoted timestamps.
145
+ const d = m ? new Date(`${m[1]}T${m[2]}${isoOffset(m[3])}`) : null;
146
+ if (!d || Number.isNaN(d.getTime()) || utcDateString(new Date(`${m[1]}T00:00:00.000Z`)) !== m[1]) {
147
+ throw new Error(`invalid "date" (${s}); use YYYY-MM-DD or an ISO timestamp`);
148
+ }
149
+ normalized = d.toISOString();
150
+ }
151
+ } else {
152
+ throw new Error(`invalid "date" (${String(value)}); use YYYY-MM-DD or an ISO timestamp`);
153
+ }
154
+
155
+ const isFuture = DATE_ONLY_RE.test(normalized)
156
+ ? Date.parse(`${normalized}T00:00:00Z`) > now.getTime() + DATE_ONLY_FUTURE_SLACK_MS
157
+ : new Date(normalized).getTime() > now.getTime() + DATETIME_FUTURE_SLACK_MS;
158
+ if (isFuture) {
159
+ throw new Error(`"date" ${normalized} is in the future; scheduling posts is not supported`);
160
+ }
161
+ return normalized;
162
+ }
163
+
95
164
  function frontMatterFromPost(post) {
96
165
  const fmData = {
97
166
  title: post.title || '',
98
167
  };
99
168
  if (post.slug) fmData.slug = post.slug;
169
+ if (post.published_at) {
170
+ const published = new Date(post.published_at);
171
+ if (!Number.isNaN(published.getTime())) fmData.date = utcDateString(published);
172
+ }
100
173
  if (post.description) fmData.description = post.description;
101
174
  if (post.web_visibility && post.web_visibility !== 'listed') fmData.visibility = post.web_visibility;
102
175
  if (post.hero_image) fmData.hero = post.hero_image;
@@ -202,6 +275,15 @@ async function push(options = {}) {
202
275
  continue;
203
276
  }
204
277
 
278
+ let postDate;
279
+ try {
280
+ postDate = normalizePostDate(fm.date);
281
+ } catch (err) {
282
+ console.error(`${relName}: ${err.message}`);
283
+ hadError = true;
284
+ continue;
285
+ }
286
+
205
287
  const emailWanted = fm.email === true;
206
288
  let formId = null;
207
289
  if (emailWanted) {
@@ -270,6 +352,7 @@ async function push(options = {}) {
270
352
  subject: fm.subject || null,
271
353
  preheader: fm.preview || null,
272
354
  };
355
+ if (postDate) payload.date = postDate;
273
356
 
274
357
  try {
275
358
  const result = await fn.invoke('upsert-post', payload);
@@ -643,4 +726,5 @@ module.exports = {
643
726
  slugify,
644
727
  defaultSlugFromFilename,
645
728
  frontMatterFromPost,
729
+ normalizePostDate,
646
730
  };
package/src/index.js CHANGED
@@ -19,7 +19,7 @@ const program = new Command();
19
19
 
20
20
  program
21
21
  .name('micropage')
22
- .description('CLI for micropage.sh – create, sync, and publish microsites')
22
+ .description('Micropage CLI: keep your .page files in git and publish static sites from the terminal')
23
23
  .version(version);
24
24
 
25
25
  // ---------------------------------------------------------------------------
@@ -65,7 +65,7 @@ projectCmd
65
65
  .description('Create a new project and init local folder')
66
66
  .option(
67
67
  '-d, --domain <domain>',
68
- 'Override Cloudflare Pages project name / slug (default: server generates name-slug + 6 hex, max 58 chars)',
68
+ 'Choose the project slug, which sets its address <domain>.micropage.sh (default: server generates name-slug + 6 hex, max 58 chars)',
69
69
  )
70
70
  .action((name, opts) => projects.create(name, opts));
71
71
 
@@ -87,7 +87,7 @@ projectCmd
87
87
 
88
88
  projectCmd
89
89
  .command('publish')
90
- .description('Push local content and publish (deploy to Cloudflare Pages) — alias for `micropage publish`')
90
+ .description('Push local content and publish it live — alias for `micropage publish`')
91
91
  .option('-w, --watch', 'Stream build/deploy events after triggering publish')
92
92
  .action((opts) => builds.publish(opts));
93
93
 
@@ -152,7 +152,7 @@ program
152
152
 
153
153
  program
154
154
  .command('publish')
155
- .description('Push local content and publish (deploy to Cloudflare Pages)')
155
+ .description('Push local content and publish it live')
156
156
  .option('-w, --watch', 'Stream build/deploy events after triggering publish')
157
157
  .action((opts) => builds.publish(opts));
158
158
 
@@ -23,6 +23,7 @@ Each file in `posts/*.md` is one post: YAML front-matter + a Markdown body, push
23
23
  ---
24
24
  title: Launching our new dashboard
25
25
  slug: launching-new-dashboard # optional; defaults to the filename minus a leading date prefix and .md
26
+ date: 2026-07-07 # optional; public date, YYYY-MM-DD or ISO timestamp (default: publish time; no future dates)
26
27
  description: A quick look at what's new.
27
28
  visibility: listed # listed | unlisted | none (default: listed)
28
29
  hero: launch-hero.png # optional; local file, existing uploaded asset filename, or absolute URL