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.
@@ -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
- <Site title="Sign in">
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: { "content-type": "application/json" },
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
  }