@jtakeit/astro 0.1.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/LICENSE +21 -0
- package/README.md +59 -0
- package/bin/jtk.mjs +41 -0
- package/docs/booking.md +164 -0
- package/docs/catalogue.md +459 -0
- package/docs/collections.md +249 -0
- package/docs/css.md +86 -0
- package/docs/gallery.md +127 -0
- package/docs/hero-motion.md +189 -0
- package/docs/kit.md +454 -0
- package/docs/languages.md +182 -0
- package/docs/lead-form.md +109 -0
- package/docs/pages.md +193 -0
- package/docs/photos.md +314 -0
- package/docs/scaffold.md +75 -0
- package/docs/shapes.md +140 -0
- package/docs/surface.md +187 -0
- package/lib/catalogue.mjs +1678 -0
- package/lib/codes.mjs +171 -0
- package/lib/create.mjs +282 -0
- package/package.json +16 -0
- package/template/astro.config.mjs +84 -0
- package/template/figures.mjs +122 -0
- package/template/gitignore +16 -0
- package/template/jtakeit-meta.mjs +112 -0
- package/template/jtk/content/index.json +38 -0
- package/template/jtk/design.json +24 -0
- package/template/markdown.mjs +36 -0
- package/template/package-lock.json +5320 -0
- package/template/package.json +26 -0
- package/template/specimens.mjs +46 -0
- package/template/src/components/Blocks.astro +151 -0
- package/template/src/components/BookingForm.astro +506 -0
- package/template/src/components/Clip.astro +155 -0
- package/template/src/components/Hero.astro +66 -0
- package/template/src/components/LeadForm.astro +347 -0
- package/template/src/components/OpeningHours.astro +69 -0
- package/template/src/components/Pile.astro +185 -0
- package/template/src/components/Shot.astro +472 -0
- package/template/src/components/gallery/Gallery.astro +381 -0
- package/template/src/components/gallery/galleries.ts +139 -0
- package/template/src/components/motion/HeroField.astro +520 -0
- package/template/src/components/motion/fields.ts +430 -0
- package/template/src/components/surface/Pattern.astro +278 -0
- package/template/src/components/surface/patterns.ts +187 -0
- package/template/src/content/blocks.ts +758 -0
- package/template/src/content.config.ts +19 -0
- package/template/src/copy/LOCALE.ts +324 -0
- package/template/src/data/site.ts +137 -0
- package/template/src/layouts/Layout.astro +282 -0
- package/template/src/lib/alive.ts +49 -0
- package/template/src/lib/entries.ts +106 -0
- package/template/src/lib/entryLoader.ts +315 -0
- package/template/src/lib/noise.ts +26 -0
- package/template/src/lib/page.ts +287 -0
- package/template/src/lib/photos.ts +168 -0
- package/template/src/lib/under.ts +32 -0
- package/template/src/lib/uploads.ts +85 -0
- package/template/src/pages/[...entry].astro +207 -0
- package/template/src/pages/[...feed].xml.ts +64 -0
- package/template/src/pages/index.astro +90 -0
- package/template/src/pages/llms.txt.ts +50 -0
- package/template/src/pages/privacy.astro +59 -0
- package/template/src/pages/robots.txt.ts +21 -0
- package/template/src/pages/sitemap.xml.ts +50 -0
- package/template/src/styles/global.css +411 -0
- package/template/src/styles/surface.css +375 -0
- package/template/tsconfig.json +5 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# The catalogue
|
|
2
|
+
|
|
3
|
+
`jtk/catalogue.json` is what lets a site built here be handed to its owner.
|
|
4
|
+
|
|
5
|
+
The studio's admin edits a site by tapping the text on it. It can do that for a
|
|
6
|
+
site built any way at all — any markup, any components, any layout — because it
|
|
7
|
+
reads the catalogue rather than knowing anything about the site. **That is what
|
|
8
|
+
makes bespoke and editable the same sentence rather than a trade.**
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
src/ the design. Yours. Anything at all.
|
|
12
|
+
jtk/ the values. The owner's, edited in the admin.
|
|
13
|
+
catalogue which values there are, and what each one is
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Declare, do not write
|
|
17
|
+
|
|
18
|
+
Nobody edits `catalogue.json`. It is emitted from `src/content/blocks.ts`, which
|
|
19
|
+
sits beside the components, is type-checked, and is where the decision actually
|
|
20
|
+
belongs:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @jtakeit/astro catalogue
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**A field declared there can be edited by the owner. A field left out is part of
|
|
27
|
+
the design.** Both are right answers — a headline is content, the way a section
|
|
28
|
+
is laid out is not — and the choice is per field rather than something a
|
|
29
|
+
framework makes for you.
|
|
30
|
+
|
|
31
|
+
Then it checks itself against the built page, in both directions:
|
|
32
|
+
|
|
33
|
+
| | what it means |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| declared, not rendered | a control in the admin that edits nothing |
|
|
36
|
+
| rendered, not declared | text the owner can see and cannot touch |
|
|
37
|
+
|
|
38
|
+
The second is the one that hides, which is why `annotation-lint.mjs` catches it
|
|
39
|
+
again at build time. Between them a catalogue cannot drift from the page.
|
|
40
|
+
|
|
41
|
+
## The codes, and who is right about them
|
|
42
|
+
|
|
43
|
+
Everything `jtk catalogue` refuses is named:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
✗ JTK_E_MEDIA_INVALID hero.clip: accept: 'video' without multiple — …
|
|
47
|
+
✗ FL_LOCAL src/Nav.astro: "/preise/" is written from the root — …
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A `JTK_E_…` code is the platform's, not this skill's. What it means and what to
|
|
51
|
+
do about it are one row in the admin's generated `docs/reference/errors.md`, and
|
|
52
|
+
that page is the one to read — it is written from the validator itself, and
|
|
53
|
+
anything restated here would be a second wording of a rule that has one. The
|
|
54
|
+
same code comes back from the admin when a site is attached, so the two answers
|
|
55
|
+
about one file are legibly the same answer. `FL_LOCAL` is the handful of checks
|
|
56
|
+
that are ours alone: whether the build survives being served under the preview's
|
|
57
|
+
`/p/<slug>/` prefix, which the catalogue format has no opinion about.
|
|
58
|
+
|
|
59
|
+
**The judge is the admin, not this checker.** The rules above are implemented
|
|
60
|
+
twice — in Go on the server, and in `lib/catalogue.mjs` so the answer arrives with
|
|
61
|
+
a line number before anything is deployed — and two implementations of one
|
|
62
|
+
contract drift silently. So the local one sits the other's exam:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx @jtakeit/astro catalogue --judge # needs JTK_API, JTK_TOKEN
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
It posts the emitted catalogue to the real validator and reports every code the
|
|
69
|
+
two disagree about. Run it on client projects: those runs are the only thing
|
|
70
|
+
keeping the copy true, and a divergence is a bug in the script rather than in the
|
|
71
|
+
site.
|
|
72
|
+
|
|
73
|
+
## What a field is
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
{ key: 'title', label: 'Заголовок', kind: 'text', max: 80,
|
|
77
|
+
required: true, client_editable: true, seo_sensitive: true }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- **`key`** is permanent. It is the key in the content document, so renaming one
|
|
81
|
+
loses whatever the owner had written in it.
|
|
82
|
+
- **`label` is the sentence itself**, in the site's language. The admin's own
|
|
83
|
+
blocks label their fields through a locale file; a bespoke block has no locale
|
|
84
|
+
file, so the text goes here and the admin shows it as written.
|
|
85
|
+
- **`kind`** decides the control and the validation: `text`, `textarea`,
|
|
86
|
+
`richtext_lite`, `number`, `money`, `duration`, `time_of_day`, `tel`, `url`,
|
|
87
|
+
`email`, `date`, `select`, `bool`, `media`, `list`.
|
|
88
|
+
- **`money`** is a whole number of **minor units** — `25000` is 250.00. Never a
|
|
89
|
+
decimal: money that is a float is money that is wrong the day somebody sums
|
|
90
|
+
a column, and minor units are what Stripe is handed later. The currency is
|
|
91
|
+
the site's and is not part of the value — the template writes the symbol
|
|
92
|
+
beside the amount it renders.
|
|
93
|
+
- **`duration`** is a whole number of **minutes** — `90` is an hour and a half.
|
|
94
|
+
Minutes because that is the unit slot arithmetic works in, so nothing
|
|
95
|
+
converts on the way. The panel reads it back as "1 год 30 хв".
|
|
96
|
+
- **`time_of_day`** is whole minutes from **local midnight** — `540` is nine in
|
|
97
|
+
the morning, and there is no such value as 1440. No zone travels with it:
|
|
98
|
+
the zone is the site's, declared once.
|
|
99
|
+
- **`client_editable`** separates the owner from the studio. A headline is
|
|
100
|
+
theirs; a button's destination points at a section they did not build.
|
|
101
|
+
- **`no_tap_target`** is for a value that never appears as its own element — a
|
|
102
|
+
message a script writes later, a string that lands in an attribute, a label
|
|
103
|
+
only a screen reader hears. It is edited in the panel beside the preview, and
|
|
104
|
+
the checks must not look for something to tap.
|
|
105
|
+
- **`seo_sensitive`** shows a warning that changing it moves the page in search
|
|
106
|
+
results.
|
|
107
|
+
|
|
108
|
+
`list` holds rows: `of` is the fields of one row, and a list inside a list is
|
|
109
|
+
not something the admin has a control for.
|
|
110
|
+
|
|
111
|
+
## Attaching the site
|
|
112
|
+
|
|
113
|
+
Once `jtk catalogue` passes:
|
|
114
|
+
|
|
115
|
+
1. Push the repository.
|
|
116
|
+
2. In the admin, create the site and paste the repository's URL.
|
|
117
|
+
3. Import — the admin reads the ref into the draft.
|
|
118
|
+
|
|
119
|
+
The import refuses a ref with no catalogue, which is the whole reason this file
|
|
120
|
+
exists rather than being optional.
|
|
121
|
+
|
|
122
|
+
## What the owner may break, and what they may not
|
|
123
|
+
|
|
124
|
+
Nothing in `src/` is reachable from the admin. An owner can empty a headline and
|
|
125
|
+
they cannot move a section, change a colour or delete a component — those are
|
|
126
|
+
the design, and the design is a commit.
|
|
127
|
+
|
|
128
|
+
That is also the answer to "what happens when both of us are working": the
|
|
129
|
+
developer writes `src/`, the admin writes `jtk/`, and they never touch the
|
|
130
|
+
same file.
|
|
131
|
+
|
|
132
|
+
## Every sentence a visitor reads is content, unless it is machinery
|
|
133
|
+
|
|
134
|
+
The same rule as photographs, and it catches more sites: a string written into
|
|
135
|
+
a component is a string only we can change.
|
|
136
|
+
|
|
137
|
+
**Content** is anything said in the business's voice — the wordmark and the line
|
|
138
|
+
under it, the footer's note about how to book, and, the one that gets missed
|
|
139
|
+
every time, **what the form asks for**. "The idea, where it goes on the body,
|
|
140
|
+
roughly how big" is a tattooist deciding what she needs before she can answer;
|
|
141
|
+
the next business needs a date, a headcount, a registration number. A visitor
|
|
142
|
+
reads it and the owner is the only person who knows what it should say.
|
|
143
|
+
|
|
144
|
+
**Machinery** is the short list around it: "Sending…", "Sent.", the sentence a
|
|
145
|
+
browser shows for an empty field, the skip link. There is nothing an owner wants
|
|
146
|
+
to say in those, and the server keeps its own copy of the refusals anyway.
|
|
147
|
+
|
|
148
|
+
The scaffold's form now declares its questions — `name_label`, `contact_label`,
|
|
149
|
+
`message_label`, `required_note` on `cta_banner` — and its machinery stays in
|
|
150
|
+
the copy adapter.
|
|
151
|
+
|
|
152
|
+
**The navigation is content too**, and it is the one most often left out. The
|
|
153
|
+
labels in the header and the footer are the words a customer navigates by —
|
|
154
|
+
«Preise», «Kurse», «Galerie» — and a business renames them: a price list becomes
|
|
155
|
+
a menu, courses become workshops. The *addresses* stay the design's, because a
|
|
156
|
+
route the owner could retype is a route they could break; so declare the labels
|
|
157
|
+
and leave the hrefs out of the catalogue, or declare them `client_editable:
|
|
158
|
+
false`. A nav list in `src/data/nav.ts` is fine as the shape; the words in it
|
|
159
|
+
come from the shared document.
|
|
160
|
+
|
|
161
|
+
### Say which, in the repository, for every sentence
|
|
162
|
+
|
|
163
|
+
The rule above is a judgement, and a judgement that is not written down is not a
|
|
164
|
+
decision. Every visible sentence on a built page is one of two things:
|
|
165
|
+
|
|
166
|
+
| | how it is written | who changes it |
|
|
167
|
+
| --- | --- | --- |
|
|
168
|
+
| the owner's | declared in the catalogue, annotated `data-jtk-path` | they do, by tapping it |
|
|
169
|
+
| the design's | marked `data-jtk-fixed` | we do, in a commit |
|
|
170
|
+
|
|
171
|
+
`data-jtk-fixed` is a boolean attribute and it silences everything inside the
|
|
172
|
+
element it is on, so one attribute covers an ornament made of six spans:
|
|
173
|
+
|
|
174
|
+
```astro
|
|
175
|
+
<a class="skip" href="#main" data-jtk-fixed>Zum Inhalt springen</a>
|
|
176
|
+
|
|
177
|
+
<div class="trap" aria-hidden="true" data-jtk-fixed>
|
|
178
|
+
<label for="lead-website">Website</label>
|
|
179
|
+
<input id="lead-website" name="website" type="text" tabindex="-1" />
|
|
180
|
+
</div>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
It is for machinery and for ornament: a skip link, a honeypot, a decorative
|
|
184
|
+
caption, a counter drawn by the design. It is not a way to make a decision go
|
|
185
|
+
away — a nav label marked `data-jtk-fixed` is a nav label the owner cannot change,
|
|
186
|
+
and that has to be a thing somebody chose rather than a thing somebody skipped.
|
|
187
|
+
|
|
188
|
+
**`jtk catalogue` fails on a sentence that is neither.** It used to list them and
|
|
189
|
+
pass, which made the decision optional — and optional is how a site shipped with
|
|
190
|
+
twenty-six strings belonging to nobody: the navigation, the footer and the
|
|
191
|
+
labels on the form, with a line in the handover saying the boundary had been
|
|
192
|
+
drawn deliberately. Nothing had been drawn: the strings were simply written into
|
|
193
|
+
components, and the owner found out by wanting to change one.
|
|
194
|
+
|
|
195
|
+
### The wordmark and the footer
|
|
196
|
+
|
|
197
|
+
They are content and they sit outside `<main>`, which is the only thing that
|
|
198
|
+
makes them awkward: **an annotation path is `blocks[i].field`, so a tappable
|
|
199
|
+
string has to be a field of a block.** Nothing requires that block to render
|
|
200
|
+
inside the article — where a block appears is the design's business, and the
|
|
201
|
+
lint looks for its path anywhere on the page.
|
|
202
|
+
|
|
203
|
+
So a site that wants them editable declares one:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
{
|
|
207
|
+
type: 'signature',
|
|
208
|
+
v: 1,
|
|
209
|
+
label: 'Підпис сайту',
|
|
210
|
+
fields: [
|
|
211
|
+
{ key: 'wordmark', label: 'Назва', kind: 'text', max: 60, client_editable: true },
|
|
212
|
+
{ key: 'role', label: 'Хто це і де', kind: 'text', max: 90, client_editable: true },
|
|
213
|
+
{ key: 'footnote', label: 'Рядок у підвалі', kind: 'text', max: 140, client_editable: true },
|
|
214
|
+
],
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
and the header and the footer read it through `block('signature')`. The same
|
|
219
|
+
field can be annotated in both places — a patch reaches every element carrying
|
|
220
|
+
the path, so a wordmark in the corner and the same wordmark in the foot stay one
|
|
221
|
+
value.
|
|
222
|
+
|
|
223
|
+
Two things not to do. Do not leave them in a `SITE` constant because they are
|
|
224
|
+
"furniture": the owner's own name is not furniture, and neither is the sentence
|
|
225
|
+
telling somebody how to book. And do not reach for `business_facts` — it has
|
|
226
|
+
`name` and `city`, it is edited in a panel rather than by tapping, and there is
|
|
227
|
+
no annotation path shape for it.
|
|
228
|
+
|
|
229
|
+
**A separator between two values is CSS, never a text node.** The footer joins
|
|
230
|
+
the wordmark and the role on one line, and written the obvious way
|
|
231
|
+
|
|
232
|
+
```astro
|
|
233
|
+
<span data-jtk-path={…}>{sign.name}</span> — <span data-jtk-path={…}>{sign.role}</span>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
the dash and the spaces around it belong to neither field. Nothing on the page
|
|
237
|
+
can edit them, and the owner sees two outlined boxes with a dead gap between
|
|
238
|
+
them and reasonably asks what the gap is. Put it where it belongs:
|
|
239
|
+
|
|
240
|
+
```css
|
|
241
|
+
.foot__role::before { content: ' — '; }
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Now nothing between the two is unowned, the line reads as one, and a tap on the
|
|
245
|
+
dash opens the role — which is the field it is attached to.
|
|
246
|
+
|
|
247
|
+
The same trap, one level up: a value repeated in two places is one field
|
|
248
|
+
annotated twice, not two fields. A patch reaches every element carrying the
|
|
249
|
+
path, so a wordmark in the corner and the same wordmark in the foot stay one
|
|
250
|
+
value however many times the design prints it.
|
|
251
|
+
|
|
252
|
+
## Media: a wall of work, and a set of clips
|
|
253
|
+
|
|
254
|
+
Two shapes turn up on nearly every site now. Both are `media` fields, and
|
|
255
|
+
between them they add two words to the catalogue's vocabulary.
|
|
256
|
+
|
|
257
|
+
**A gallery is one field, not thirty-seven.**
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
{
|
|
261
|
+
key: 'work',
|
|
262
|
+
label: 'Роботи',
|
|
263
|
+
kind: 'media',
|
|
264
|
+
multiple: true,
|
|
265
|
+
max: 120,
|
|
266
|
+
client_editable: true,
|
|
267
|
+
hint: 'Порядок тут — порядок на сторінці.',
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Declared as a `list` of rows it becomes a repeater — add a row, open it, choose
|
|
272
|
+
a file, close it, thirty-seven times — and that is the wrong shape for what a
|
|
273
|
+
gallery is: a collection you drop pictures into and take pictures out of. One
|
|
274
|
+
field, an ordered array of items, each carrying its own `alt`, because a
|
|
275
|
+
photograph with no alt text is announced as a file name.
|
|
276
|
+
|
|
277
|
+
A `list` of rows is still right where every piece has **words** beside it and
|
|
278
|
+
the words are the point — the `index` arrangement in [gallery.md](gallery.md).
|
|
279
|
+
The question is whether the owner is editing entries or filling a bag.
|
|
280
|
+
|
|
281
|
+
**The arrangement is not in the catalogue and must not be.** How the work is
|
|
282
|
+
shown — drifting columns, a dense contact sheet, a few large pieces — is a
|
|
283
|
+
design decision taken from how much work there is, and it is not content.
|
|
284
|
+
|
|
285
|
+
**A set of clips** is the same field with what it holds declared:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
{
|
|
289
|
+
key: 'clips',
|
|
290
|
+
label: 'Відео процесу',
|
|
291
|
+
kind: 'media',
|
|
292
|
+
accept: 'video',
|
|
293
|
+
multiple: true,
|
|
294
|
+
max: 6,
|
|
295
|
+
client_editable: true,
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`accept: 'video'` is there because what the page does with a clip is different
|
|
300
|
+
from what it does with a photograph: it autoplays it, loops it, and falls back
|
|
301
|
+
to a poster frame every time autoplay does not happen. The clips and their posters are cut from the client's own video with whatever tooling you have; `<Clip>` plays them.
|
|
302
|
+
|
|
303
|
+
Both words are for `media` fields and nothing else, and `jtk catalogue` says so.
|
|
304
|
+
|
|
305
|
+
**A clip is always `multiple`, even when there is one of it.** A clip is stored
|
|
306
|
+
with its poster, and a plain media field is one key with nowhere to keep one, so
|
|
307
|
+
one clip is `multiple: true, max: 1`. It reads oddly for a moment and it means
|
|
308
|
+
every video on every site has the same value shape.
|
|
309
|
+
|
|
310
|
+
### The frame, and what must survive it
|
|
311
|
+
|
|
312
|
+
A client uploads a photograph in whatever shape their camera gave it and the
|
|
313
|
+
page has a slot of its own. Declare the slot:
|
|
314
|
+
|
|
315
|
+
```jsonc
|
|
316
|
+
{ key: 'work', kind: 'media', multiple: true, ratio: '3:2', client_editable: true }
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**The frame is yours and the subject is theirs.** How a page is proportioned is
|
|
320
|
+
the design's answer, so `ratio` is declared here and there is no handle in the
|
|
321
|
+
admin to drag — two pages of one site coming out differently proportioned is
|
|
322
|
+
the thing this prevents. What the owner *does* say is which part of their
|
|
323
|
+
picture must survive the crop, because that is the one thing the design cannot
|
|
324
|
+
know: they press the subject and the frame moves to it.
|
|
325
|
+
|
|
326
|
+
There is no stretching. A photograph with its geometry changed is a spoiled
|
|
327
|
+
photograph, not a laid-out one.
|
|
328
|
+
|
|
329
|
+
A field with a `ratio` gives each item a **`focus`** — the CSS value itself,
|
|
330
|
+
`"50% 25%"`, so it drops straight into `object-position` with nothing to
|
|
331
|
+
convert. `Shot.astro` already takes both:
|
|
332
|
+
|
|
333
|
+
```astro
|
|
334
|
+
<Shot name={item.src} ratio="3 / 2" fit="cover" focus={item.focus} … />
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**A framed field is always `multiple`**, for the same reason a clip is: the
|
|
338
|
+
focus is stored beside the picture, and a plain media field is one key with
|
|
339
|
+
nowhere to put it. One framed picture is `multiple: true, max: 1`.
|
|
340
|
+
|
|
341
|
+
### What one item is
|
|
342
|
+
|
|
343
|
+
```jsonc
|
|
344
|
+
"work": [ { "src": "media/<site>/<hash>.jpg", "alt": "Дракон, передпліччя" } ]
|
|
345
|
+
"framed":[ { "src": "media/<site>/<hash>.jpg", "alt": "Дракон", "focus": "50% 25%" } ]
|
|
346
|
+
"clips": [ { "src": "media/<site>/<hash>.mp4",
|
|
347
|
+
"poster": "media/<site>/<hash>.jpg", "alt": "Стенсіл" } ]
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`src`, `alt`, `poster`, and nothing else. **The three keys are not declared per
|
|
351
|
+
site** — the admin synthesises them from the field, so a site that named them
|
|
352
|
+
`file` and `caption` would be a site the editor cannot follow. `src` is a key in
|
|
353
|
+
the studio's bucket, never a URL; `alt` is optional to store and required to be
|
|
354
|
+
worth reading, because a photograph without it is announced as a file name; a
|
|
355
|
+
clip's `poster` is generated from its own first frame when the owner uploads it,
|
|
356
|
+
the same frame the clip's poster is.
|
|
357
|
+
|
|
358
|
+
Order is position. There is no id and nothing to sort by: the array is the page.
|
|
359
|
+
|
|
360
|
+
### How a component reads one
|
|
361
|
+
|
|
362
|
+
The keys are downloaded into `src/assets/<key>` before the build, so a gallery
|
|
363
|
+
goes through `astro:assets` exactly as `src/assets` photographs do — variants,
|
|
364
|
+
`srcset`, the lot. What a component must not do is address `src` as a URL: that
|
|
365
|
+
is our bucket's address and it changes.
|
|
366
|
+
|
|
367
|
+
### The annotation
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
blocks[3].work[7].src
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
One picture is one tap target, and **the container is not one** — tapping a wall
|
|
374
|
+
of thirty-seven and being offered "the wall" to edit is not an edit anybody
|
|
375
|
+
means to make. Tapping any picture opens the whole gallery in the admin with
|
|
376
|
+
that one selected, which is where add, remove, reorder and re-caption live.
|
|
377
|
+
|
|
378
|
+
`alt` and `poster` carry no annotation and are not looked for: neither of them
|
|
379
|
+
appears on the page as itself. Annotating them anyway is allowed and does
|
|
380
|
+
nothing.
|
|
381
|
+
|
|
382
|
+
### What is served from where
|
|
383
|
+
|
|
384
|
+
`/media/…` is the studio's prefix on the hosting edge: an owner's uploads are
|
|
385
|
+
served from it. A file the site ships under that path is not reachable, which is
|
|
386
|
+
why clips go in `public/clips/`. Anything else in `public/` is the site's
|
|
387
|
+
own and is served as it always was.
|
|
388
|
+
|
|
389
|
+
`public/_headers` is a Cloudflare Pages convention the platform's edge does not
|
|
390
|
+
read — headers are the platform's, and a site cannot set its own. The scaffold
|
|
391
|
+
ships none.
|
|
392
|
+
|
|
393
|
+
## Turning a wall that was built into a wall that is owned
|
|
394
|
+
|
|
395
|
+
A site built before galleries were content has its work in the components: a
|
|
396
|
+
list of slot names in a page, or a `PHOTOS` registry, and every picture a
|
|
397
|
+
decision only we can change. Four steps, in this order, and the site is
|
|
398
|
+
attachable at the end of them.
|
|
399
|
+
|
|
400
|
+
**0. Bring the components that changed.** A site built before this has its own
|
|
401
|
+
copies, and the mechanism lives in six files:
|
|
402
|
+
|
|
403
|
+
```
|
|
404
|
+
src/lib/uploads.ts new — resolves a content key to a local file
|
|
405
|
+
src/lib/photos.ts photo() now answers for keys as well as slots
|
|
406
|
+
src/components/Shot.astro takes `path`, and puts it on the <img>
|
|
407
|
+
src/components/Clip.astro new — one silent looping clip, done properly
|
|
408
|
+
src/components/gallery/ Gallery.astro + galleries.ts: path onto the picture
|
|
409
|
+
src/copy/<locale>.ts gallery(), the seam
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Copy them over the site's own, then reconcile: everything else in those files is
|
|
413
|
+
the kit's and unchanged, so a diff shows only what this added.
|
|
414
|
+
|
|
415
|
+
**1. Declare the fields.** In `src/content/blocks.ts`, on the block the work
|
|
416
|
+
belongs to — not a new block. The type name is permanent, so it is the one this
|
|
417
|
+
page already uses.
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
{ key: 'work', label: 'Роботи', kind: 'media', multiple: true, max: 120, client_editable: true },
|
|
421
|
+
{ key: 'clips', label: 'Відео процесу', kind: 'media', accept: 'video', multiple: true, max: 6, client_editable: true },
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**2. Move the pictures into the content document**, as items, keeping the order
|
|
425
|
+
the page already had and carrying each one's alt text with it. Slot names, not
|
|
426
|
+
keys: the files stay exactly where they are.
|
|
427
|
+
|
|
428
|
+
```js
|
|
429
|
+
// One-off, from PHOTOS — thirty-seven of these by hand is thirty-seven chances
|
|
430
|
+
// to lose an alt.
|
|
431
|
+
const items = PHOTOS.filter((p) => p.name.startsWith('work-'))
|
|
432
|
+
.map((p) => ({ src: p.name, alt: p.alt }));
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Put them under the block's key in `jtk/content/index.json`. The alt text
|
|
436
|
+
now lives in two places — `photos.ts` and the document — and the document is the
|
|
437
|
+
one the page reads; leave the registry alone, it still describes the files.
|
|
438
|
+
|
|
439
|
+
**3. Render through the seam.** `gallery()` for the pictures, `<Clip>` for the
|
|
440
|
+
loops — [gallery.md](gallery.md) has both. What the page loses is the hard-coded
|
|
441
|
+
list; what it keeps is every visual decision, because the arrangement was never
|
|
442
|
+
content.
|
|
443
|
+
|
|
444
|
+
**4. Check, and push.**
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
npx @jtakeit/astro catalogue # emits, builds, checks both ways
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Zero means the admin can be handed this repository: every declared field renders
|
|
451
|
+
with its path, and nothing on the page is text the owner cannot touch.
|
|
452
|
+
|
|
453
|
+
One thing to look at with your own eyes before pushing: **the annotation goes on
|
|
454
|
+
the `<img>`, never on the frame around it.** The admin replaces a picture by
|
|
455
|
+
setting `src` on the element it was told about, and an element that is not an
|
|
456
|
+
image gets its *text* replaced instead — so a `data-jtk-path` on a `<figure>`
|
|
457
|
+
turns the client's photograph into a URL printed where the picture was. `<Shot
|
|
458
|
+
path={...}>` puts it in the right place; a hand-rolled `<figure data-jtk-path>`
|
|
459
|
+
does not.
|