create-astroid 0.1.2 → 0.2.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/README.md +16 -7
- package/index.mjs +244 -15
- package/package.json +5 -3
- package/template/README.md +10 -0
- package/template/_env.example +36 -0
- package/template/astro.config.mjs +29 -12
- package/template/package.json +5 -2
- package/template/pnpm-workspace.yaml +38 -0
- package/template/seed/home.seed.sql +48 -1
- package/template/src/auth.ts +10 -6
- package/template/src/components/LouiseEdit.astro +46 -3
- package/template/src/env.d.ts +54 -5
- package/template/src/layouts/Site.astro +84 -4
- package/template/src/pages/contact.astro +122 -0
- package/template/src/pages/index.astro +78 -5
- package/template/src/pages/login.astro +52 -2
- package/template/src/pages/robots.txt.ts +36 -0
- package/template/src/pages/sitemap.xml.ts +40 -0
- package/template/src/styles/site.css +9 -0
|
@@ -3,12 +3,29 @@
|
|
|
3
3
|
// clicking it starts a session and lands on `/?louise` (edit mode on). The gate
|
|
4
4
|
// is enumeration-safe — the response is identical whether or not the email is an
|
|
5
5
|
// editor — so the UI always says "check your email".
|
|
6
|
+
import { env } from "cloudflare:workers";
|
|
7
|
+
import { turnstileSiteKey } from "louise-toolkit/auth";
|
|
6
8
|
import Site from "../layouts/Site.astro";
|
|
7
9
|
|
|
8
10
|
export const prerender = false;
|
|
11
|
+
|
|
12
|
+
// Turnstile, rendered ONLY when a real (non-test) site key is set.
|
|
13
|
+
//
|
|
14
|
+
// This has to match the server exactly. `getLouiseAuth` registers Better Auth's
|
|
15
|
+
// captcha plugin on /sign-in/magic-link the moment BOTH halves of the pair are
|
|
16
|
+
// real, and that plugin rejects any request without an `x-captcha-response`
|
|
17
|
+
// header. So a page that never rendered a widget meant provisioning Turnstile —
|
|
18
|
+
// which .env.example openly invites you to do — locked the owner out of their
|
|
19
|
+
// own sign-in, with a 403 and no clue why.
|
|
20
|
+
//
|
|
21
|
+
// `turnstileSiteKey` returns null for the always-passing TEST key, which is the
|
|
22
|
+
// same condition `activeCaptchaSecret` uses server-side. One shared rule, so the
|
|
23
|
+
// widget appears exactly when the server starts demanding a token.
|
|
24
|
+
const siteKey = turnstileSiteKey(env);
|
|
9
25
|
---
|
|
10
26
|
|
|
11
|
-
|
|
27
|
+
{/* noindex: an editor sign-in page has nothing for a search result to offer. */}
|
|
28
|
+
<Site title="Sign in" noindex>
|
|
12
29
|
<main class="hero min-h-screen">
|
|
13
30
|
<div class="hero-content w-full max-w-sm">
|
|
14
31
|
<form id="magic" class="card w-full bg-base-200 shadow-sm">
|
|
@@ -26,6 +43,13 @@ export const prerender = false;
|
|
|
26
43
|
class="input input-bordered w-full"
|
|
27
44
|
/>
|
|
28
45
|
</label>
|
|
46
|
+
{
|
|
47
|
+
siteKey && (
|
|
48
|
+
/* Turnstile injects a hidden `cf-turnstile-response` input here once
|
|
49
|
+
it solves. The submit handler reads it out of the form. */
|
|
50
|
+
<div class="cf-turnstile" data-sitekey={siteKey} data-size="flexible" />
|
|
51
|
+
)
|
|
52
|
+
}
|
|
29
53
|
<button type="submit" class="btn btn-primary">Email me a sign-in link</button>
|
|
30
54
|
<p id="msg" class="text-sm opacity-70" aria-live="polite"></p>
|
|
31
55
|
</div>
|
|
@@ -33,6 +57,20 @@ export const prerender = false;
|
|
|
33
57
|
</div>
|
|
34
58
|
</main>
|
|
35
59
|
|
|
60
|
+
{
|
|
61
|
+
/* Turnstile's widget script. `is:inline` because it's a remote src Astro must
|
|
62
|
+
not process; the CSP already allows challenges.cloudflare.com for script,
|
|
63
|
+
frame, and connect (astroidSecurity). Only loaded when there's a widget. */
|
|
64
|
+
siteKey && (
|
|
65
|
+
<script
|
|
66
|
+
is:inline
|
|
67
|
+
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
|
|
68
|
+
async
|
|
69
|
+
defer
|
|
70
|
+
/>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
36
74
|
{/* Processed (NOT is:inline) so Astro's security.csp hashes it into script-src.
|
|
37
75
|
Keep it dependency-free and static — an is:inline or define:vars script here
|
|
38
76
|
would be unhashable and CSP-blocked. */}
|
|
@@ -45,10 +83,22 @@ export const prerender = false;
|
|
|
45
83
|
const email = input?.value.trim();
|
|
46
84
|
if (!email || !msg) return;
|
|
47
85
|
msg.textContent = "Sending…";
|
|
86
|
+
// Turnstile writes its token into a hidden input it injects into the form.
|
|
87
|
+
// Better Auth's captcha plugin reads it from the `x-captcha-response`
|
|
88
|
+
// HEADER (not the body), and rejects the request outright when it's
|
|
89
|
+
// missing — so this header is what makes a captcha-enabled sign-in work.
|
|
90
|
+
// Absent when no widget rendered, in which case the plugin isn't active
|
|
91
|
+
// either and the header is simply not sent.
|
|
92
|
+
const token = (form as HTMLFormElement).querySelector<HTMLInputElement>(
|
|
93
|
+
'input[name="cf-turnstile-response"]',
|
|
94
|
+
)?.value;
|
|
48
95
|
try {
|
|
49
96
|
await fetch("/api/auth/sign-in/magic-link", {
|
|
50
97
|
method: "POST",
|
|
51
|
-
headers: {
|
|
98
|
+
headers: {
|
|
99
|
+
"content-type": "application/json",
|
|
100
|
+
...(token ? { "x-captcha-response": token } : {}),
|
|
101
|
+
},
|
|
52
102
|
body: JSON.stringify({ email, callbackURL: "/?louise" }),
|
|
53
103
|
});
|
|
54
104
|
msg.textContent = "If that email is an editor, a sign-in link is on its way.";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// robots.txt — origin-aware, with the disallow list derived from your Astroid
|
|
2
|
+
// config (the editor + its API always; portal and checkout routes when those
|
|
3
|
+
// modules are on). Scaffolded once and yours to edit: add a Disallow here when
|
|
4
|
+
// you add a route crawlers shouldn't reach.
|
|
5
|
+
import type { APIRoute } from "astro";
|
|
6
|
+
import { astroidRobotsTxt } from "astroidjs";
|
|
7
|
+
import { env } from "cloudflare:workers";
|
|
8
|
+
import astroidConfig from "../../astroid.config.js";
|
|
9
|
+
|
|
10
|
+
export const prerender = false;
|
|
11
|
+
|
|
12
|
+
export const GET: APIRoute = async (context) => {
|
|
13
|
+
// The origin actually serving this request — never a configured domain. A
|
|
14
|
+
// preview deploy that advertises the production host invites its content to
|
|
15
|
+
// be indexed under the real domain.
|
|
16
|
+
const origin = new URL(context.request.url).origin;
|
|
17
|
+
|
|
18
|
+
// The same site-wide switch that noindexes pages also stops the crawl, rather
|
|
19
|
+
// than relying on crawlers fetching every page to find the meta tag.
|
|
20
|
+
let disableIndexing = false;
|
|
21
|
+
try {
|
|
22
|
+
const row = await env.DB.prepare("SELECT disable_indexing FROM site_settings WHERE id = 1").first<{
|
|
23
|
+
disable_indexing: number;
|
|
24
|
+
}>();
|
|
25
|
+
disableIndexing = Boolean(row?.disable_indexing);
|
|
26
|
+
} catch {
|
|
27
|
+
// No DB binding yet (pre-provision) — fall through to the crawlable default.
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return new Response(astroidRobotsTxt(astroidConfig, { origin, disableIndexing }), {
|
|
31
|
+
headers: {
|
|
32
|
+
"content-type": "text/plain; charset=utf-8",
|
|
33
|
+
"cache-control": "public, max-age=3600",
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// sitemap.xml — the published `pages` rows, plus whatever else this site serves.
|
|
2
|
+
// Scaffolded once and yours to edit: add your own routes (a product catalog, a
|
|
3
|
+
// gallery) to the `entries` array below.
|
|
4
|
+
//
|
|
5
|
+
// `astroidSitemapXml` drops anything matching the config's noindex prefixes, so
|
|
6
|
+
// this file and robots.txt can never disagree about what's crawlable.
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
import { astroidSitemapXml, type SitemapEntry } from "astroidjs";
|
|
9
|
+
import { env } from "cloudflare:workers";
|
|
10
|
+
import astroidConfig from "../../astroid.config.js";
|
|
11
|
+
|
|
12
|
+
export const prerender = false;
|
|
13
|
+
|
|
14
|
+
export const GET: APIRoute = async (context) => {
|
|
15
|
+
const origin = new URL(context.request.url).origin;
|
|
16
|
+
const entries: SitemapEntry[] = [{ path: "/" }];
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const { results } = await env.DB.prepare(
|
|
20
|
+
"SELECT slug, updated_at FROM pages WHERE status = 'published' AND noindex = 0",
|
|
21
|
+
).all<{ slug: string; updated_at: number | null }>();
|
|
22
|
+
for (const row of results ?? []) {
|
|
23
|
+
// `home` is served at "/", which is already listed — never at "/home".
|
|
24
|
+
if (row.slug === "home") continue;
|
|
25
|
+
// Drizzle stores `updated_at` as a Unix timestamp in SECONDS; <lastmod>
|
|
26
|
+
// wants a W3C datetime, so a raw epoch number would be invalid.
|
|
27
|
+
const lastmod = row.updated_at ? new Date(row.updated_at * 1000) : undefined;
|
|
28
|
+
entries.push({ path: `/${row.slug}`, lastmod });
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// No DB binding yet (pre-provision) — ship the root-only sitemap.
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return new Response(astroidSitemapXml(astroidConfig, entries, { origin }), {
|
|
35
|
+
headers: {
|
|
36
|
+
"content-type": "application/xml; charset=utf-8",
|
|
37
|
+
"cache-control": "public, max-age=3600",
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
};
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
@plugin "daisyui";
|
|
6
6
|
@import "louise-toolkit/theme/louise.css";
|
|
7
7
|
|
|
8
|
+
/* Astroid's section components (<Hero>, the section library, etc.) ship as SOURCE
|
|
9
|
+
inside node_modules, and Tailwind v4 does not scan node_modules by default. So
|
|
10
|
+
the utilities used ONLY in those files — bg-primary, text-primary-content,
|
|
11
|
+
items-center, and every other colorway/alignment class — would never be
|
|
12
|
+
generated, and the whole `_settings` token contract would silently no-op: a
|
|
13
|
+
section set to `colorway: "brand"` would render on the default background.
|
|
14
|
+
This tells Tailwind to scan them too. Keep it if you use any astroidjs section. */
|
|
15
|
+
@source "../../node_modules/astroidjs/src/components";
|
|
16
|
+
|
|
8
17
|
:root {
|
|
9
18
|
--brand: __BRAND_COLOR__;
|
|
10
19
|
}
|