@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.
@@ -95,16 +95,43 @@ export default defineConfig({
95
95
 
96
96
  ## `PUBLIC_SCRIBE_API_URL`
97
97
 
98
- Where forms post. It's a **repo variable**, and GitHub resolves an unset
99
- `vars.*` to an empty string rather than leaving it undefined — so `??` won't
100
- fall back. Use `||`:
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
- const apiBase = import.meta.env.PUBLIC_SCRIBE_API_URL || 'https://scribe.weaverdigital.io';
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
- Any `PUBLIC_*` value is inlined into the client bundle at build time, so changing
107
- it needs a rebuild, not just a redeploy.
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
- To test forms locally, point `PUBLIC_SCRIBE_API_URL` at a local admin
118
- (`http://localhost:3040`) — but note the endpoint only allows CORS from the
119
- Site's registered domain, so use the admin's own host or expect to test
120
- submissions against the deployed site.
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 = import.meta.env.PUBLIC_SCRIBE_API_URL || 'https://scribe.weaverdigital.io',
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.