@scribe-platform/cli 0.10.0 → 0.11.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,193 @@
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. Build
9
+ a site with all of it and the check is a regression guard rather than a list of
10
+ things to go and fix.
11
+
12
+ **Run `scribe site seo <site>` before `scribe site promote`**, not after. Going
13
+ live is the deadline for being findable, not the starting gun — and `promote`
14
+ refuses on a failure, the same way it already refuses when DNS is not ready.
15
+
16
+ Three checks (`sitemap`, `canonical`, `indexable`) skip until the site is on the
17
+ client's own domain, because before the cutover those all correctly name
18
+ `*.github.io`. That is expected, not a problem, and not something to fix.
19
+
20
+ ## The four files
21
+
22
+ Almost everything here comes from four pieces. A site with all four passes all
23
+ eleven checks.
24
+
25
+ ### 1. `site` in `astro.config.mjs`
26
+
27
+ Nothing else works without it. Astro needs the site's real address to build
28
+ absolute URLs, and a canonical or a sitemap entry that is not absolute is not
29
+ valid.
30
+
31
+ ```js
32
+ import { defineConfig } from 'astro/config';
33
+ import sitemap from '@astrojs/sitemap';
34
+
35
+ export default defineConfig({
36
+ // The live address. Set it to the client's domain from the start, even
37
+ // before the cutover — it is what the sitemap and every canonical are built
38
+ // from, and it is the one value that cannot be derived from the environment.
39
+ site: 'https://riversidedental.com',
40
+ base: process.env.PUBLIC_BASE_PATH || '/',
41
+ integrations: [sitemap()],
42
+ });
43
+ ```
44
+
45
+ `npm install @astrojs/sitemap` first. The integration writes `sitemap-index.xml`
46
+ and `sitemap-0.xml` into `dist/` on every build, so the page list can never go
47
+ stale the way a hand-written one does.
48
+
49
+ ### 2. `public/robots.txt`
50
+
51
+ ```
52
+ User-agent: *
53
+ Allow: /
54
+
55
+ Sitemap: https://riversidedental.com/sitemap-index.xml
56
+ ```
57
+
58
+ Three things the check looks for: that the file exists at all, that it does not
59
+ carry a blanket `Disallow: /`, and that it names a `Sitemap:` line. A commented
60
+ `# Sitemap:` does not count.
61
+
62
+ ### 3. A `<SeoHead>` component
63
+
64
+ One component, used by every layout, so a page cannot be built that forgets
65
+ half of it. This is the piece that covers five of the eleven checks at once.
66
+
67
+ ```astro
68
+ ---
69
+ // src/components/SeoHead.astro
70
+ interface Props {
71
+ title: string;
72
+ description: string;
73
+ /** Falls back to the site-wide card image. */
74
+ image?: string;
75
+ }
76
+
77
+ const { title, description, image = '/og-card.png' } = Astro.props;
78
+
79
+ // Astro.site comes from astro.config.mjs. Astro.url.pathname is this page.
80
+ const canonical = new URL(Astro.url.pathname, Astro.site);
81
+ const imageUrl = new URL(image, Astro.site);
82
+ ---
83
+ <title>{title}</title>
84
+ <meta name="description" content={description} />
85
+ <link rel="canonical" href={canonical} />
86
+
87
+ <meta property="og:title" content={title} />
88
+ <meta property="og:description" content={description} />
89
+ <meta property="og:image" content={imageUrl} />
90
+ <meta property="og:url" content={canonical} />
91
+ <meta property="og:type" content="website" />
92
+ ```
93
+
94
+ Then in the layout:
95
+
96
+ ```astro
97
+ ---
98
+ import SeoHead from '../components/SeoHead.astro';
99
+ const { title, description } = Astro.props;
100
+ ---
101
+ <html lang="en">
102
+ <head>
103
+ <meta charset="utf-8" />
104
+ <meta name="viewport" content="width=device-width" />
105
+ <SeoHead title={title} description={description} />
106
+ </head>
107
+ <body><slot /></body>
108
+ </html>
109
+ ```
110
+
111
+ **`lang="en"` on `<html>` is its own check** and is easy to drop when a layout
112
+ gets rewritten.
113
+
114
+ **Every page passes its own title and description.** Two pages sharing either
115
+ one is a failure, not a warning — it is the normal result of a layout setting a
116
+ default that a page forgets to override, and it cannot be seen by looking at any
117
+ single page.
118
+
119
+ ### 4. The business, in structured data
120
+
121
+ On the home page only. This is what puts a business in a local result rather
122
+ than a plain blue link.
123
+
124
+ ```astro
125
+ ---
126
+ const business = {
127
+ '@context': 'https://schema.org',
128
+ '@type': 'LocalBusiness', // or a more specific type: Dentist, Plumber, HairSalon
129
+ name: 'Riverside Dental',
130
+ url: 'https://riversidedental.com',
131
+ telephone: '+1-717-555-0100',
132
+ address: {
133
+ '@type': 'PostalAddress',
134
+ streetAddress: '14 Market Street',
135
+ addressLocality: 'Lancaster',
136
+ addressRegion: 'PA',
137
+ postalCode: '17602',
138
+ addressCountry: 'US',
139
+ },
140
+ areaServed: 'Lancaster County, PA',
141
+ };
142
+ ---
143
+ <script type="application/ld+json" set:html={JSON.stringify(business)} />
144
+ ```
145
+
146
+ `name`, `url` and `telephone` are what the check requires. The address and
147
+ `areaServed` are not checked and are worth having anyway — they are most of what
148
+ a local result is made of.
149
+
150
+ **A business that does not want its street address public can leave
151
+ `address` out** and keep `areaServed`. Google's own guidance is that a business
152
+ customers do not visit should hide the address and declare a service area.
153
+
154
+ **Do not mark up reviews you are displaying for yourself.** Google reserves
155
+ review markup for a site reviewing *other* businesses, and a demo's placeholder
156
+ five stars is exactly the thing that gets a site penalised.
157
+
158
+ ## The two smaller ones
159
+
160
+ **`contactable`** — a phone number on the home page and on any contact page. A
161
+ `tel:` link counts, and so does a plain number in the text. This exists because
162
+ a real site went live with its number on four guide articles and on none of the
163
+ pages anybody lands on.
164
+
165
+ ```astro
166
+ <a href="tel:+17175550100">(717) 555-0100</a>
167
+ ```
168
+
169
+ **`alt`** — every `<img>` needs an `alt` attribute. `alt=""` passes and is the
170
+ correct markup for a decorative image; a *missing* attribute is the failure.
171
+
172
+ ## What the checks will not tell you
173
+
174
+ Eleven checks are mechanical. The things that actually decide whether a site
175
+ ranks are not, and `scribe site seo` ends by asking about them rather than
176
+ pretending to measure them:
177
+
178
+ - Does the site say which town and area the business serves, on the pages a
179
+ visitor lands on? A local business that never names its town is the single
180
+ most common reason one does not show up.
181
+ - Is there real client proof — a name, a review, a photo of finished work —
182
+ rather than a placeholder?
183
+ - Is there a page for each main thing they sell, rather than one page with
184
+ sections? One page cannot rank for six different searches.
185
+ - Does the contact form sit next to a privacy policy, if it collects a name,
186
+ email or phone?
187
+ - Would someone who has never heard of this business understand what it does
188
+ from the home page alone?
189
+ - Is the business claimed on Google with the same name, address and phone number
190
+ as the site?
191
+
192
+ Ask the client. "Not yet" is a real answer — the point is that it gets said
193
+ before the site goes live, not that every site has all six.