@scribe-platform/cli 0.10.0 → 0.12.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/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +25 -4
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/profile.d.ts +12 -0
- package/dist/commands/profile.d.ts.map +1 -0
- package/dist/commands/profile.js +81 -0
- package/dist/commands/profile.js.map +1 -0
- package/dist/commands/site.d.ts.map +1 -1
- package/dist/commands/site.js +43 -1
- package/dist/commands/site.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/client.d.ts +82 -0
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +106 -3
- package/dist/lib/client.js.map +1 -1
- package/dist/lib/config.d.ts +53 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +116 -8
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/seo-report.d.ts +19 -0
- package/dist/lib/seo-report.d.ts.map +1 -0
- package/dist/lib/seo-report.js +82 -0
- package/dist/lib/seo-report.js.map +1 -0
- package/package.json +1 -1
- package/skills/scribe-site-builder/SKILL.md +28 -2
- package/skills/scribe-site-builder/recipes/README.md +1 -0
- package/skills/scribe-site-builder/recipes/deploy.md +41 -10
- package/skills/scribe-site-builder/recipes/forms.md +21 -1
- package/skills/scribe-site-builder/recipes/seo.md +207 -0
|
@@ -95,16 +95,43 @@ export default defineConfig({
|
|
|
95
95
|
|
|
96
96
|
## `PUBLIC_SCRIBE_API_URL`
|
|
97
97
|
|
|
98
|
-
Where forms post. It's
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
Where forms post. It's an **organisation variable** on the GitHub org that owns
|
|
99
|
+
the repo, so every Site follows one value and nobody has to remember to set it
|
|
100
|
+
per repo. Don't add a repo-level variable of the same name: it shadows the org
|
|
101
|
+
one, and `scribe site verify`'s `binding` check reports it as a failure for that
|
|
102
|
+
reason.
|
|
103
|
+
|
|
104
|
+
**It has no default in a production build, and that is deliberate.** Any
|
|
105
|
+
`PUBLIC_*` value is inlined into the client bundle at build time, so a Site that
|
|
106
|
+
falls back to a guess ships the wrong endpoint — posting a Visitor's message to
|
|
107
|
+
whichever Scribe the fallback named, found out, if ever, by a Client wondering
|
|
108
|
+
why nobody replied. So a missing value fails `astro build` instead. Note that
|
|
109
|
+
`binding` does **not** cover this: it asserts no repo-level override exists, not
|
|
110
|
+
that the org value resolves to anything.
|
|
111
|
+
|
|
112
|
+
GitHub resolves an unset `vars.*` to an empty string rather than leaving it
|
|
113
|
+
undefined, so `??` would not fall back and an empty value has to count as
|
|
114
|
+
missing:
|
|
101
115
|
|
|
102
116
|
```ts
|
|
103
|
-
|
|
117
|
+
function scribeApiBase(): string {
|
|
118
|
+
const configured = import.meta.env.PUBLIC_SCRIBE_API_URL?.trim();
|
|
119
|
+
if (configured) return configured;
|
|
120
|
+
if (import.meta.env.PROD) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'PUBLIC_SCRIBE_API_URL is unset. Set it as an organisation variable on the GitHub org that owns ' +
|
|
123
|
+
'this repo — it has no default in a production build, so this Site posts nowhere by accident.',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
// Dev only, and a *local* admin rather than production: the point of failing
|
|
127
|
+
// loudly is that a Site cannot reach a real Client's database by omission.
|
|
128
|
+
return 'http://localhost:3040';
|
|
129
|
+
}
|
|
104
130
|
```
|
|
105
131
|
|
|
106
|
-
|
|
107
|
-
|
|
132
|
+
Call it where the endpoint is actually needed, not at the top of a component
|
|
133
|
+
that takes the endpoint as a prop — otherwise it throws at a caller who already
|
|
134
|
+
supplied one.
|
|
108
135
|
|
|
109
136
|
## Local loop
|
|
110
137
|
|
|
@@ -114,10 +141,11 @@ npm run dev # iterate on design
|
|
|
114
141
|
npm run build && npm run preview
|
|
115
142
|
```
|
|
116
143
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
144
|
+
`npm run dev` needs no env file: with nothing set, `scribeApiBase()` above
|
|
145
|
+
returns `http://localhost:3040`, which is where a local admin listens. Set
|
|
146
|
+
`PUBLIC_SCRIBE_API_URL` only to point somewhere else. Either way the endpoint
|
|
147
|
+
allows CORS from the Site's registered domain only, so use the admin's own host
|
|
148
|
+
or expect to test submissions against the deployed site.
|
|
121
149
|
|
|
122
150
|
## Checklist when a deploy looks wrong
|
|
123
151
|
|
|
@@ -127,5 +155,8 @@ submissions against the deployed site.
|
|
|
127
155
|
workflow failed; check the Actions tab.
|
|
128
156
|
- Form says "Failed to fetch" → `PUBLIC_SCRIBE_API_URL` baked in wrong, or the
|
|
129
157
|
origin isn't the Site's registered domain.
|
|
158
|
+
- Build fails with *"PUBLIC_SCRIBE_API_URL is unset"* → the org variable is
|
|
159
|
+
missing or scoped to selected repositories that don't include this one. That
|
|
160
|
+
is the check working: fix the variable rather than adding a fallback.
|
|
130
161
|
- Build fails after an editor added a field → the content zod schema is too
|
|
131
162
|
strict (see `collections.md`).
|
|
@@ -126,13 +126,33 @@ export interface Props {
|
|
|
126
126
|
prefill?: Record<string, string>;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// Where forms post. **No default in a production build, deliberately** — see
|
|
130
|
+
// `deploy.md`. Dev falls back to a local admin, never production, so
|
|
131
|
+
// `npm run dev` works with no env file and still cannot write to a real
|
|
132
|
+
// Client's database by accident.
|
|
133
|
+
function scribeApiBase(): string {
|
|
134
|
+
const configured = import.meta.env.PUBLIC_SCRIBE_API_URL?.trim();
|
|
135
|
+
if (configured) return configured;
|
|
136
|
+
if (import.meta.env.PROD) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
'PUBLIC_SCRIBE_API_URL is unset. Set it as an organisation variable on the GitHub org that owns ' +
|
|
139
|
+
'this repo — it has no default in a production build, so this Site posts nowhere by accident.',
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return 'http://localhost:3040';
|
|
143
|
+
}
|
|
144
|
+
|
|
129
145
|
const {
|
|
130
146
|
site, collection, turnstileSiteKey, fields,
|
|
131
|
-
apiBase
|
|
147
|
+
apiBase: apiBaseProp,
|
|
132
148
|
submitLabel = 'Send',
|
|
133
149
|
prefill = {},
|
|
134
150
|
} = Astro.props;
|
|
135
151
|
|
|
152
|
+
// Resolved after the props, and only when `apiBase` was not passed: a caller who
|
|
153
|
+
// named it has already answered the question `scribeApiBase()` would throw about.
|
|
154
|
+
const apiBase = apiBaseProp ?? scribeApiBase();
|
|
155
|
+
|
|
136
156
|
// Scoped to the collection so two forms can share a page.
|
|
137
157
|
const formId = `scribe-form-${collection}`;
|
|
138
158
|
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Recipe: being found
|
|
2
|
+
|
|
3
|
+
> **The commands below are yours to run, not hers to read.** If she's asking
|
|
4
|
+
> what this is or how to do it, answer it the way *Who you're talking to* in
|
|
5
|
+
> [`../SKILL.md`](../SKILL.md) says to: what it is, what she'd say to get it,
|
|
6
|
+
> what's her call, and an offer to do it now.
|
|
7
|
+
|
|
8
|
+
Everything `scribe site seo <site>` checks, and how to make each one pass.
|
|
9
|
+
|
|
10
|
+
**This is a going-live job, not a build-time one.** Build the site the way the
|
|
11
|
+
client wants it; then, before promoting, run the check and work through
|
|
12
|
+
whatever it lists. Each failure names the fix below, so the report is the work
|
|
13
|
+
list.
|
|
14
|
+
|
|
15
|
+
**Run `scribe site seo <site>` before `scribe site promote`**, not after. Going
|
|
16
|
+
live is the deadline for being findable, not the starting gun — and `promote`
|
|
17
|
+
refuses on a failure, the same way it already refuses when DNS is not ready.
|
|
18
|
+
|
|
19
|
+
Four checks (`robots`, `sitemap`, `canonical`, `indexable`) skip until the site
|
|
20
|
+
is on the client's own domain. That is expected, not a problem, and not
|
|
21
|
+
something to fix — before the cutover they are all asking about the wrong
|
|
22
|
+
address.
|
|
23
|
+
|
|
24
|
+
`robots` is the one worth understanding, because the site looks wrong when it
|
|
25
|
+
is not. A crawler only ever reads `robots.txt` from the **root of a domain**.
|
|
26
|
+
A site on the free Scribe address lives at `<org>.github.io/<repo>/`, so the
|
|
27
|
+
file a crawler would read belongs to the whole org, not to this site — the
|
|
28
|
+
site's own `public/robots.txt` lands at `/<repo>/robots.txt`, where nothing
|
|
29
|
+
looks. Write the file anyway; it starts working the moment the domain is
|
|
30
|
+
live.
|
|
31
|
+
|
|
32
|
+
## The four files
|
|
33
|
+
|
|
34
|
+
Almost everything here comes from four pieces. A site with all four passes all
|
|
35
|
+
eleven checks.
|
|
36
|
+
|
|
37
|
+
### 1. `site` in `astro.config.mjs`
|
|
38
|
+
|
|
39
|
+
Nothing else works without it. Astro needs the site's real address to build
|
|
40
|
+
absolute URLs, and a canonical or a sitemap entry that is not absolute is not
|
|
41
|
+
valid.
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import { defineConfig } from 'astro/config';
|
|
45
|
+
import sitemap from '@astrojs/sitemap';
|
|
46
|
+
|
|
47
|
+
export default defineConfig({
|
|
48
|
+
// The live address. Set it to the client's domain from the start, even
|
|
49
|
+
// before the cutover — it is what the sitemap and every canonical are built
|
|
50
|
+
// from, and it is the one value that cannot be derived from the environment.
|
|
51
|
+
site: 'https://riversidedental.com',
|
|
52
|
+
base: process.env.PUBLIC_BASE_PATH || '/',
|
|
53
|
+
integrations: [sitemap()],
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`npm install @astrojs/sitemap` first. The integration writes `sitemap-index.xml`
|
|
58
|
+
and `sitemap-0.xml` into `dist/` on every build, so the page list can never go
|
|
59
|
+
stale the way a hand-written one does.
|
|
60
|
+
|
|
61
|
+
### 2. `public/robots.txt`
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
User-agent: *
|
|
65
|
+
Allow: /
|
|
66
|
+
|
|
67
|
+
Sitemap: https://riversidedental.com/sitemap-index.xml
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Three things the check looks for: that the file exists at all, that it does not
|
|
71
|
+
carry a blanket `Disallow: /`, and that it names a `Sitemap:` line. A commented
|
|
72
|
+
`# Sitemap:` does not count.
|
|
73
|
+
|
|
74
|
+
### 3. A `<SeoHead>` component
|
|
75
|
+
|
|
76
|
+
One component, used by every layout, so a page cannot be built that forgets
|
|
77
|
+
half of it. This is the piece that covers five of the eleven checks at once.
|
|
78
|
+
|
|
79
|
+
```astro
|
|
80
|
+
---
|
|
81
|
+
// src/components/SeoHead.astro
|
|
82
|
+
interface Props {
|
|
83
|
+
title: string;
|
|
84
|
+
description: string;
|
|
85
|
+
/** Falls back to the site-wide card image. */
|
|
86
|
+
image?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The default is resolved against `Astro.site` below, so it stays correct on
|
|
90
|
+
// the free Scribe address as well as on the client's domain.
|
|
91
|
+
const { title, description, image = '/og-card.png' } = Astro.props;
|
|
92
|
+
|
|
93
|
+
// Astro.site comes from astro.config.mjs. Astro.url.pathname is this page.
|
|
94
|
+
const canonical = new URL(Astro.url.pathname, Astro.site);
|
|
95
|
+
const imageUrl = new URL(image, Astro.site);
|
|
96
|
+
---
|
|
97
|
+
<title>{title}</title>
|
|
98
|
+
<meta name="description" content={description} />
|
|
99
|
+
<link rel="canonical" href={canonical} />
|
|
100
|
+
|
|
101
|
+
<meta property="og:title" content={title} />
|
|
102
|
+
<meta property="og:description" content={description} />
|
|
103
|
+
<meta property="og:image" content={imageUrl} />
|
|
104
|
+
<meta property="og:url" content={canonical} />
|
|
105
|
+
<meta property="og:type" content="website" />
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Then in the layout:
|
|
109
|
+
|
|
110
|
+
```astro
|
|
111
|
+
---
|
|
112
|
+
import SeoHead from '../components/SeoHead.astro';
|
|
113
|
+
const { title, description } = Astro.props;
|
|
114
|
+
---
|
|
115
|
+
<html lang="en">
|
|
116
|
+
<head>
|
|
117
|
+
<meta charset="utf-8" />
|
|
118
|
+
<meta name="viewport" content="width=device-width" />
|
|
119
|
+
<SeoHead title={title} description={description} />
|
|
120
|
+
</head>
|
|
121
|
+
<body><slot /></body>
|
|
122
|
+
</html>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**`lang="en"` on `<html>` is its own check** and is easy to drop when a layout
|
|
126
|
+
gets rewritten.
|
|
127
|
+
|
|
128
|
+
**Every page passes its own title and description.** Two pages sharing either
|
|
129
|
+
one is a failure, not a warning — it is the normal result of a layout setting a
|
|
130
|
+
default that a page forgets to override, and it cannot be seen by looking at any
|
|
131
|
+
single page.
|
|
132
|
+
|
|
133
|
+
### 4. The business, in structured data
|
|
134
|
+
|
|
135
|
+
On the home page only. This is what puts a business in a local result rather
|
|
136
|
+
than a plain blue link.
|
|
137
|
+
|
|
138
|
+
```astro
|
|
139
|
+
---
|
|
140
|
+
const business = {
|
|
141
|
+
'@context': 'https://schema.org',
|
|
142
|
+
'@type': 'LocalBusiness', // or a more specific type: Dentist, Plumber, HairSalon
|
|
143
|
+
name: 'Riverside Dental',
|
|
144
|
+
url: 'https://riversidedental.com',
|
|
145
|
+
telephone: '+1-717-555-0100',
|
|
146
|
+
address: {
|
|
147
|
+
'@type': 'PostalAddress',
|
|
148
|
+
streetAddress: '14 Market Street',
|
|
149
|
+
addressLocality: 'Lancaster',
|
|
150
|
+
addressRegion: 'PA',
|
|
151
|
+
postalCode: '17602',
|
|
152
|
+
addressCountry: 'US',
|
|
153
|
+
},
|
|
154
|
+
areaServed: 'Lancaster County, PA',
|
|
155
|
+
};
|
|
156
|
+
---
|
|
157
|
+
<script type="application/ld+json" set:html={JSON.stringify(business)} />
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`name`, `url` and `telephone` are what the check requires. The address and
|
|
161
|
+
`areaServed` are not checked and are worth having anyway — they are most of what
|
|
162
|
+
a local result is made of.
|
|
163
|
+
|
|
164
|
+
**A business that does not want its street address public can leave
|
|
165
|
+
`address` out** and keep `areaServed`. Google's own guidance is that a business
|
|
166
|
+
customers do not visit should hide the address and declare a service area.
|
|
167
|
+
|
|
168
|
+
**Do not mark up reviews you are displaying for yourself.** Google reserves
|
|
169
|
+
review markup for a site reviewing *other* businesses, and a demo's placeholder
|
|
170
|
+
five stars is exactly the thing that gets a site penalised.
|
|
171
|
+
|
|
172
|
+
## The two smaller ones
|
|
173
|
+
|
|
174
|
+
**`contactable`** — a phone number on the home page and on any contact page. A
|
|
175
|
+
`tel:` link counts, and so does a plain number in the text. This exists because
|
|
176
|
+
a real site went live with its number on four guide articles and on none of the
|
|
177
|
+
pages anybody lands on.
|
|
178
|
+
|
|
179
|
+
```astro
|
|
180
|
+
<a href="tel:+17175550100">(717) 555-0100</a>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**`alt`** — every `<img>` needs an `alt` attribute. `alt=""` passes and is the
|
|
184
|
+
correct markup for a decorative image; a *missing* attribute is the failure.
|
|
185
|
+
|
|
186
|
+
## What the checks will not tell you
|
|
187
|
+
|
|
188
|
+
Eleven checks are mechanical. The things that actually decide whether a site
|
|
189
|
+
ranks are not, and `scribe site seo` ends by asking about them rather than
|
|
190
|
+
pretending to measure them:
|
|
191
|
+
|
|
192
|
+
- Does the site say which town and area the business serves, on the pages a
|
|
193
|
+
visitor lands on? A local business that never names its town is the single
|
|
194
|
+
most common reason one does not show up.
|
|
195
|
+
- Is there real client proof — a name, a review, a photo of finished work —
|
|
196
|
+
rather than a placeholder?
|
|
197
|
+
- Is there a page for each main thing they sell, rather than one page with
|
|
198
|
+
sections? One page cannot rank for six different searches.
|
|
199
|
+
- Does the contact form sit next to a privacy policy, if it collects a name,
|
|
200
|
+
email or phone?
|
|
201
|
+
- Would someone who has never heard of this business understand what it does
|
|
202
|
+
from the home page alone?
|
|
203
|
+
- Is the business claimed on Google with the same name, address and phone number
|
|
204
|
+
as the site?
|
|
205
|
+
|
|
206
|
+
Ask the client. "Not yet" is a real answer — the point is that it gets said
|
|
207
|
+
before the site goes live, not that every site has all six.
|