@half-built/astro 0.9.0 → 0.10.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 CHANGED
@@ -57,20 +57,37 @@ consumer config itself.
57
57
 
58
58
  ## WhenPublished and PostLink
59
59
 
60
- `content/WhenPublished.astro` renders its children only when the post
61
- at `slug` is visible, so a published post can tease one still in
62
- draft and the passage appears on its own when the target ships.
63
60
  `content/PostLink.astro` links to a post by slug and resolves the
64
61
  href at build time through `postPath`, so a re-dated post does not
65
- strand the links pointing at it. Both take the consumer's collection
66
- as the `posts` prop (the full collection, drafts included, so an
67
- unknown slug can throw instead of hiding as "still a draft");
68
- `WhenPublished` also takes the consumer's draft gate as `showDrafts`,
69
- and `PostLink` accepts an optional `tip` carried as `data-tooltip` for
70
- the link-tip island. A consumer wraps each in a one-line site
71
- component that injects `getCollection` and its own gate, the way the
72
- blog does. The resolvers live in `lib/drafts.ts` for consumers that
73
- want the logic without the components.
62
+ strand the links pointing at it. While the target is hidden (a draft, with the
63
+ draft gate off) it renders its text alone, with no element and no
64
+ styling, and becomes a link on its own when the post ships. That makes
65
+ it the right tool for a draft's name in published prose.
66
+
67
+ `content/WhenPublished.astro` renders its children only when the post
68
+ at `slug` is visible. Use it when a whole phrase only makes sense once
69
+ the post exists ("its own post", "see it here"). An optional `fallback`
70
+ slot renders while the target is hidden:
71
+
72
+ ```mdx
73
+ <WhenPublished slug="my-draft">
74
+ It has <PostLink slug="my-draft">its own post</PostLink> now.
75
+ <Fragment slot="fallback">A post on it is coming.</Fragment>
76
+ </WhenPublished>
77
+ ```
78
+
79
+ Both take the consumer's collection as the `posts` prop (the full
80
+ collection, drafts included, so an unknown slug can throw instead of
81
+ hiding as "still a draft") and the consumer's draft gate as
82
+ `showDrafts`. `showDrafts` defaults to false, so a preview build must
83
+ pass its own draft gate through to `PostLink` to keep draft links
84
+ live. `PostLink` also accepts an optional `tip` carried as
85
+ `data-tooltip` for the link-tip island; a hidden `PostLink` drops it. A
86
+ consumer wraps each in a one-line site component that injects
87
+ `getCollection` and its own gate, the way the blog does. The resolvers
88
+ (`postLinkHref`, `resolvePostHref`, `forwardLinkVisible`) live in
89
+ `lib/drafts.ts` for consumers that want the logic without the
90
+ components.
74
91
 
75
92
  ## Derived excerpts
76
93
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@half-built/astro",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Astro components, islands, and pure helpers for the half-built design system.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,15 +1,22 @@
1
1
  ---
2
- import { resolvePostHref, type Linkable } from "../../lib/drafts";
2
+ import { postLinkHref, type Linkable } from "../../lib/drafts";
3
3
 
4
4
  interface Props {
5
5
  slug: string;
6
6
  posts: Linkable[];
7
+ showDrafts?: boolean;
7
8
  tip?: string;
8
9
  class?: string;
9
10
  }
10
11
 
11
- const { slug, posts, tip, class: className } = Astro.props;
12
- const href = resolvePostHref(slug, posts);
12
+ const { slug, posts, showDrafts = false, tip, class: className } = Astro.props;
13
+ const href = postLinkHref(slug, posts, showDrafts);
13
14
  ---
14
15
 
15
- <a href={href} data-tooltip={tip} class={className}><slot /></a>
16
+ {href ? (
17
+ <a href={href} data-tooltip={tip} class={className}>
18
+ <slot />
19
+ </a>
20
+ ) : (
21
+ <slot />
22
+ )}
@@ -11,4 +11,4 @@ const { slug, posts, showDrafts = false } = Astro.props;
11
11
  const visible = forwardLinkVisible(slug, posts, showDrafts);
12
12
  ---
13
13
 
14
- {visible && <slot />}
14
+ {visible ? <slot /> : <slot name="fallback" />}
package/src/lib/drafts.ts CHANGED
@@ -43,17 +43,21 @@ export function forwardLinkVisible(
43
43
  /* Post links by slug. A hand-typed post URL bakes in the target's date,
44
44
  so a re-dated draft silently breaks every earlier link to it. The
45
45
  PostLink content component resolves the slug through postPath() at
46
- build time instead. It resolves drafts too, and must: Astro renders
47
- MDX slot children eagerly, so a PostLink inside a hidden WhenPublished
48
- still runs (learned the hard way 2026-08-24). The deploy-build guard
49
- against a bare link to a draft is therefore a test, not a throw here:
50
- test/post-link.test.ts asserts no draft slug appears in any built
51
- page. Unknown slugs throw for the same reason forwardLinkVisible's do. */
46
+ build time instead. postLinkHref() is its policy: a visible target
47
+ gets its href, a hidden one gets null and the component renders its
48
+ text with no link (owner decision 2026-09-26), so a draft's name can
49
+ sit in published prose and becomes a link on its own when the draft
50
+ ships. resolvePostHref() still resolves drafts: consumers use it for
51
+ lookups keyed by href, and Astro renders MDX slot children eagerly,
52
+ so a PostLink inside a hidden WhenPublished still runs. The
53
+ deploy-build guard against a link to a draft stays a consumer test
54
+ (no draft slug in any built page). Unknown slugs throw for the same
55
+ reason forwardLinkVisible's do. */
52
56
 
53
57
  import { postPath } from "./slug";
54
58
 
55
59
  export interface Linkable {
56
- data: { slug: string; date: Date };
60
+ data: { slug: string; date: Date; draft?: boolean };
57
61
  }
58
62
 
59
63
  export function resolvePostHref(slug: string, posts: Linkable[]): string {
@@ -61,3 +65,14 @@ export function resolvePostHref(slug: string, posts: Linkable[]): string {
61
65
  if (!target) throw new Error(`PostLink: no post has slug "${slug}"`);
62
66
  return postPath(target);
63
67
  }
68
+
69
+ export function postLinkHref(
70
+ slug: string,
71
+ posts: Linkable[],
72
+ showDrafts: boolean,
73
+ ): string | null {
74
+ const target = posts.find((p) => p.data.slug === slug);
75
+ if (!target) throw new Error(`PostLink: no post has slug "${slug}"`);
76
+ if (target.data.draft && !showDrafts) return null;
77
+ return resolvePostHref(slug, posts);
78
+ }