@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,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: ``, 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 paragraph, one image → a figure
|
|
15
|
+
*
|
|
16
|
+
*  ONE paragraph, three → a row
|
|
17
|
+
* 
|
|
18
|
+
* 
|
|
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
|
+
* 
|
|
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 — `` — 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()];
|