@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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/bin/jtk.mjs +41 -0
  4. package/docs/booking.md +164 -0
  5. package/docs/catalogue.md +459 -0
  6. package/docs/collections.md +249 -0
  7. package/docs/css.md +86 -0
  8. package/docs/gallery.md +127 -0
  9. package/docs/hero-motion.md +189 -0
  10. package/docs/kit.md +454 -0
  11. package/docs/languages.md +182 -0
  12. package/docs/lead-form.md +109 -0
  13. package/docs/pages.md +193 -0
  14. package/docs/photos.md +314 -0
  15. package/docs/scaffold.md +75 -0
  16. package/docs/shapes.md +140 -0
  17. package/docs/surface.md +187 -0
  18. package/lib/catalogue.mjs +1678 -0
  19. package/lib/codes.mjs +171 -0
  20. package/lib/create.mjs +282 -0
  21. package/package.json +16 -0
  22. package/template/astro.config.mjs +84 -0
  23. package/template/figures.mjs +122 -0
  24. package/template/gitignore +16 -0
  25. package/template/jtakeit-meta.mjs +112 -0
  26. package/template/jtk/content/index.json +38 -0
  27. package/template/jtk/design.json +24 -0
  28. package/template/markdown.mjs +36 -0
  29. package/template/package-lock.json +5320 -0
  30. package/template/package.json +26 -0
  31. package/template/specimens.mjs +46 -0
  32. package/template/src/components/Blocks.astro +151 -0
  33. package/template/src/components/BookingForm.astro +506 -0
  34. package/template/src/components/Clip.astro +155 -0
  35. package/template/src/components/Hero.astro +66 -0
  36. package/template/src/components/LeadForm.astro +347 -0
  37. package/template/src/components/OpeningHours.astro +69 -0
  38. package/template/src/components/Pile.astro +185 -0
  39. package/template/src/components/Shot.astro +472 -0
  40. package/template/src/components/gallery/Gallery.astro +381 -0
  41. package/template/src/components/gallery/galleries.ts +139 -0
  42. package/template/src/components/motion/HeroField.astro +520 -0
  43. package/template/src/components/motion/fields.ts +430 -0
  44. package/template/src/components/surface/Pattern.astro +278 -0
  45. package/template/src/components/surface/patterns.ts +187 -0
  46. package/template/src/content/blocks.ts +758 -0
  47. package/template/src/content.config.ts +19 -0
  48. package/template/src/copy/LOCALE.ts +324 -0
  49. package/template/src/data/site.ts +137 -0
  50. package/template/src/layouts/Layout.astro +282 -0
  51. package/template/src/lib/alive.ts +49 -0
  52. package/template/src/lib/entries.ts +106 -0
  53. package/template/src/lib/entryLoader.ts +315 -0
  54. package/template/src/lib/noise.ts +26 -0
  55. package/template/src/lib/page.ts +287 -0
  56. package/template/src/lib/photos.ts +168 -0
  57. package/template/src/lib/under.ts +32 -0
  58. package/template/src/lib/uploads.ts +85 -0
  59. package/template/src/pages/[...entry].astro +207 -0
  60. package/template/src/pages/[...feed].xml.ts +64 -0
  61. package/template/src/pages/index.astro +90 -0
  62. package/template/src/pages/llms.txt.ts +50 -0
  63. package/template/src/pages/privacy.astro +59 -0
  64. package/template/src/pages/robots.txt.ts +21 -0
  65. package/template/src/pages/sitemap.xml.ts +50 -0
  66. package/template/src/styles/global.css +411 -0
  67. package/template/src/styles/surface.css +375 -0
  68. package/template/tsconfig.json +5 -0
@@ -0,0 +1,122 @@
1
+ /**
2
+ * A picture in a post is a figure, and pictures side by side are a row.
3
+ *
4
+ * ── the shape, and why it is not a new syntax ───────────────────────────────
5
+ *
6
+ * Markdown gives a body one shape of picture: `![alt](x.jpg)`, alone, as wide
7
+ * as the stylesheet says. There is no caption and no way to say "these three
8
+ * belong together" — and inventing a syntax for it would make the body a
9
+ * private dialect. A `.md` file in `jtk/content/` is committed, read by
10
+ * people and written by agents, and the one thing it has to stay is markdown.
11
+ *
12
+ * It turns out CommonMark says both things already, and nobody was listening:
13
+ *
14
+ * ![](one.jpg) one paragraph, one image → a figure
15
+ *
16
+ * ![](one.jpg) ONE paragraph, three → a row
17
+ * ![](two.jpg)
18
+ * ![](three.jpg)
19
+ *
20
+ * Images on consecutive lines with no blank line between them are a single
21
+ * paragraph. That is plain CommonMark, it renders anywhere, and it is already
22
+ * an honest statement that they belong together — so it is the row, with
23
+ * nothing added. A blank line between them is somebody saying they are
24
+ * separate, and this believes them.
25
+ *
26
+ * The caption is the title slot, which CommonMark has had all along:
27
+ *
28
+ * ![A healed sleeve](healed.jpg "Three weeks later, unretouched")
29
+ *
30
+ * `alt` stays the description somebody hears; the title becomes the caption
31
+ * somebody reads. Two jobs that already had two slots. Left alone it would
32
+ * render as a tooltip, which is nobody's idea of a caption.
33
+ *
34
+ * ── what comes out ─────────────────────────────────────────────────────────
35
+ *
36
+ * <figure class="fl-figure"><img …><figcaption>…</figcaption></figure>
37
+ *
38
+ * <div class="fl-row fl-row--3">
39
+ * <figure class="fl-figure"><img …></figure>
40
+ * …
41
+ * </div>
42
+ *
43
+ * `fl-row--2`, `--3`, `--4`; four means four or more and the stylesheet wraps.
44
+ * Those two class names are the whole surface — a site restyles them and does
45
+ * not invent others, which is what keeps "what a body can look like" a decision
46
+ * the kit made once rather than one every site makes again.
47
+ *
48
+ * ── where it sits in the pipeline ──────────────────────────────────────────
49
+ *
50
+ * A user hast plugin runs **before** Astro's image marker, which is what makes
51
+ * this safe: the marker finds `<img>` by tag anywhere in the tree, so wrapping
52
+ * one changes nothing about how it is optimised. It also strips every property
53
+ * off an `<img>` except `className`, which is why the caption has to be taken
54
+ * off the picture here rather than read off the built page later.
55
+ */
56
+
57
+ /** How many pictures a row names before the stylesheet is left to wrap them. */
58
+ const WIDEST = 4;
59
+
60
+ export function figures() {
61
+ return {
62
+ name: 'jtakeit-figures',
63
+ element: {
64
+ filter: ['p'],
65
+ visit(node, ctx) {
66
+ const pictures = onlyPictures(node);
67
+ if (pictures === null) return;
68
+
69
+ const made = pictures.map(figureOf);
70
+ ctx.replaceNode(node, made.length === 1 ? made[0] : rowOf(made));
71
+ },
72
+ },
73
+ };
74
+ }
75
+
76
+ /**
77
+ * The pictures in this paragraph, if that is all it holds.
78
+ *
79
+ * A paragraph with a picture and a sentence is a paragraph with a picture in
80
+ * it, and turning that into a figure would take the sentence out of the text it
81
+ * belongs to. The whitespace is what the newlines left behind, not content.
82
+ */
83
+ function onlyPictures(node) {
84
+ const pictures = [];
85
+
86
+ for (const child of node.children ?? []) {
87
+ if (child.type === 'text' && String(child.value ?? '').trim() === '') continue;
88
+ if (child.type === 'element' && child.tagName === 'img') {
89
+ pictures.push(child);
90
+ continue;
91
+ }
92
+ return null;
93
+ }
94
+
95
+ return pictures.length === 0 ? null : pictures;
96
+ }
97
+
98
+ function figureOf(image) {
99
+ const { title, ...rest } = image.properties ?? {};
100
+ const said = typeof title === 'string' ? title.trim() : '';
101
+
102
+ const children = [{ type: 'element', tagName: 'img', properties: rest, children: [] }];
103
+ if (said !== '') {
104
+ children.push({
105
+ type: 'element',
106
+ tagName: 'figcaption',
107
+ properties: {},
108
+ children: [{ type: 'text', value: said }],
109
+ });
110
+ }
111
+
112
+ return { type: 'element', tagName: 'figure', properties: { className: ['fl-figure'] }, children };
113
+ }
114
+
115
+ function rowOf(made) {
116
+ return {
117
+ type: 'element',
118
+ tagName: 'div',
119
+ properties: { className: ['fl-row', `fl-row--${Math.min(made.length, WIDEST)}`] },
120
+ children: made,
121
+ };
122
+ }
@@ -0,0 +1,16 @@
1
+ node_modules/
2
+ dist/
3
+ .astro/
4
+ .env
5
+ .env.*
6
+ .DS_Store
7
+
8
+ # Assembled at build time by the platform's pipeline from jtk/content/*.json.
9
+ # A build artifact that happens to sit at the repository root; a stale copy in
10
+ # git is a second answer to "what does this site say".
11
+ jtk-content.json
12
+
13
+ # An entry's pictures, downloaded beside the entry at build time: a picture in
14
+ # a post's body is markdown — `![alt](media/<site>/<hash>.jpg)` — resolved
15
+ # relative to the file, so the build puts the file exactly where the body says.
16
+ jtk/content/**/media/
@@ -0,0 +1,112 @@
1
+ import { readdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { join, relative, sep } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { specimenAddresses } from './specimens.mjs';
5
+ import { BLOCKS, COLLECTIONS, LOCALES } from './src/content/blocks.ts';
6
+
7
+ /**
8
+ * `_meta.json` — what the edge needs to know that the HTML cannot say.
9
+ *
10
+ * Cloudflare Pages read `_redirects` and `_headers` out of the build. The
11
+ * studio serves sites from its own Worker instead, and this file is what
12
+ * replaced them: one small object beside the pages, written by the build.
13
+ *
14
+ * ── drafts, and why the list has to exist ───────────────────────────────────
15
+ *
16
+ * An entry nobody has finished is **built** — that is how its author looks at
17
+ * it before it is out — and it is in no listing, no feed and no sitemap. What
18
+ * stops a stranger reading it is the edge, and the edge is serving static
19
+ * objects: it has no way to tell a draft from a page except to be told. This is
20
+ * being told.
21
+ *
22
+ * It is the same mechanism that already decides who sees `data-jtk-path`: one
23
+ * artifact, one place that decides who sees what is in it.
24
+ *
25
+ * ── and the specimens, which have no file at all ────────────────────────────
26
+ *
27
+ * The build also draws one page per arrangement, so that the admin can lift
28
+ * this site's own markup for a block a post has just been given. They are
29
+ * generated from the declaration rather than written, so looking for their
30
+ * documents finds nothing — and a draft nobody can find is a draft the edge
31
+ * hands to anybody. They are derived the same way the loader derives them,
32
+ * from `specimens.mjs`, which is why that rule lives in one file.
33
+ *
34
+ * The file is written whatever happens, empty lists and all, because a missing
35
+ * one and an empty one mean different things to whoever is debugging.
36
+ *
37
+ * ── the disclosure is not here any more ────────────────────────────────────
38
+ *
39
+ * It briefly was: a version number this file wrote, which the platform read and
40
+ * believed. It is gone, and what replaced it is better in the way that matters
41
+ * — the platform now reads the built pages and looks for the address itself,
42
+ * so what used to be a claim is a check. See PROCESSING_URL in
43
+ * src/content/blocks.ts.
44
+ */
45
+ export function jtakeitMeta() {
46
+ return {
47
+ name: 'jtakeit-meta',
48
+ hooks: {
49
+ 'astro:build:done': async ({ dir, logger }) => {
50
+ const drafts = [
51
+ ...(await hiddenEntries()),
52
+ ...specimenAddresses(COLLECTIONS, BLOCKS, LOCALES),
53
+ ].sort();
54
+ const meta = { redirects: [], drafts };
55
+
56
+ await writeFile(join(fileURLToPath(dir), '_meta.json'), JSON.stringify(meta, null, 2) + '\n');
57
+ if (drafts.length > 0) {
58
+ logger.info(`${drafts.length} unfinished entr${drafts.length === 1 ? 'y' : 'ies'} — served only to an editing session`);
59
+ }
60
+ },
61
+ },
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Every entry whose document says it is not on the site yet.
67
+ *
68
+ * A document, not frontmatter: an entry is a page of blocks in a `.json` file
69
+ * now, and this went on reading `.md` after that changed — so it found no
70
+ * drafts at all, and every unfinished post was served to anybody who guessed
71
+ * the address. Nothing failed and nothing was logged, which is how a silent
72
+ * list stays wrong.
73
+ */
74
+ async function hiddenEntries() {
75
+ const root = join(process.cwd(), 'jtk', 'content');
76
+ const out = [];
77
+
78
+ const walk = async (at) => {
79
+ let entries;
80
+ try {
81
+ entries = await readdir(at, { withFileTypes: true });
82
+ } catch {
83
+ return; // a site with no content directory is a site with no drafts
84
+ }
85
+
86
+ for (const entry of entries) {
87
+ const full = join(at, entry.name);
88
+ if (entry.isDirectory()) {
89
+ // The pictures the build downloaded beside an entry.
90
+ if (entry.name !== 'media') await walk(full);
91
+ continue;
92
+ }
93
+ if (!entry.name.endsWith('.json')) continue;
94
+
95
+ let document;
96
+ try {
97
+ document = JSON.parse(await readFile(full, 'utf8'));
98
+ } catch {
99
+ continue; // unreadable JSON is the build's error to report, not this one
100
+ }
101
+ // Only an entry carries this key at all; a page has no `visible` and is
102
+ // not a draft. `=== false` and not falsy: absent means listed.
103
+ if (document?.visible === false) {
104
+ const name = relative(root, full).split(sep).join('/').replace(/\.json$/, '');
105
+ out.push(`/${name}/`);
106
+ }
107
+ }
108
+ };
109
+
110
+ await walk(root);
111
+ return out.sort();
112
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "path": "/",
3
+ "seo": {
4
+ "title": "",
5
+ "description": ""
6
+ },
7
+ "blocks": [
8
+ {
9
+ "_key": "hero0001",
10
+ "type": "hero",
11
+ "v": 1,
12
+ "eyebrow": "",
13
+ "title": "",
14
+ "lead": "",
15
+ "cta_label": "",
16
+ "cta_href": "#contact",
17
+ "image": "",
18
+ "image_alt": ""
19
+ },
20
+ {
21
+ "_key": "ctab0001",
22
+ "type": "cta_banner",
23
+ "v": 1,
24
+ "title": "",
25
+ "lead": "",
26
+ "cta_label": "",
27
+ "cta_href": "#contact",
28
+ "form": true
29
+ },
30
+ {
31
+ "_key": "ques0001",
32
+ "type": "questions",
33
+ "v": 1,
34
+ "title": "",
35
+ "items": []
36
+ }
37
+ ]
38
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "v": 2,
3
+ "field": "",
4
+ "scrim": true,
5
+ "ground": "",
6
+ "colors": [],
7
+ "paper": "",
8
+ "ink": "",
9
+ "accent": "",
10
+ "surface": {
11
+ "pattern": "",
12
+ "scope": "page",
13
+ "size": "",
14
+ "weight": "",
15
+ "opacity": 0,
16
+ "draw": false,
17
+ "edge": "",
18
+ "reveal": ""
19
+ },
20
+ "photos": {
21
+ "treatment": "",
22
+ "amount": 0
23
+ }
24
+ }
@@ -0,0 +1,36 @@
1
+ import { figures } from './figures.mjs';
2
+
3
+ /**
4
+ * How this site turns markdown into HTML, in one place because two things ask.
5
+ *
6
+ * `astro.config.mjs` asks, for every `.md` Astro renders itself. And
7
+ * `src/lib/entryLoader.ts` asks, because a post's prose is a *string* inside a
8
+ * JSON document now and Astro's own `renderMarkdown` builds its renderer
9
+ * **without** the configured plugins — that is its code, not a setting: it
10
+ * passes `image`, `syntaxHighlight`, `shikiConfig`, `gfm` and `smartypants`,
11
+ * and nothing else.
12
+ *
13
+ * So a body rendered through the loader would silently lose `figures.mjs`: a
14
+ * row of three photographs would come out as three paragraphs, on the page but
15
+ * not in the preview beside the editor, which is the worst way to find out.
16
+ * The loader builds the same renderer with the same options, and this is the
17
+ * one place either of them reads.
18
+ */
19
+ /**
20
+ * The default layout for a picture that has no props to carry one — which is
21
+ * every picture in a body, because a body is markdown.
22
+ *
23
+ * Typed, because without the annotation `{ layout: 'constrained' }` widens to
24
+ * `{ layout: string }` and `string` is not an `ImageLayout`. Under `// @ts-check`
25
+ * that is an error in astro.config.mjs rather than a note here.
26
+ *
27
+ * This belongs to the **site's** image config and to nothing else. The markdown
28
+ * processor also takes an `image` option, and it is a different thing entirely
29
+ * — `{ domains, remotePatterns }`, about which remote pictures may be fetched —
30
+ * so this must not be handed to it. It was, and it meant nothing there.
31
+ *
32
+ * @type {import('astro').AstroUserConfig['image']}
33
+ */
34
+ export const IMAGE = { layout: 'constrained' };
35
+
36
+ export const HAST_PLUGINS = [figures()];