@treeseed/core 0.8.9 → 0.8.11

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.
@@ -1,205 +1,76 @@
1
1
  ---
2
2
  import '../styles/global.css';
3
- import { SITE_NAV_GROUPS, isCurrentSitePath } from '../utils/routes';
4
- import { PROJECT_STAGE } from '../utils/content-status';
3
+ import PublicShell from '../components/ui/shell/PublicShell.astro';
4
+ import { SITE_NAV_GROUPS } from '../utils/routes';
5
5
  import { SITE } from '../utils/seo';
6
- import { SITE_FOOTER_MENU, SITE_THEME_CSS } from '../utils/site-config';
7
- import FooterSubscribeForm from '../components/forms/FooterSubscribeForm.astro';
8
- import DevWatchReload from '../components/DevWatchReload.astro';
9
- import ThemeScript from '../components/ui/theme/ThemeScript.astro';
6
+ import { SITE_THEME_CSS } from '../utils/site-config';
7
+ import { normalizeThemePreference } from '../utils/theme.js';
10
8
 
11
9
  const { title, description, currentPath = '/' } = Astro.props;
10
+ const appearance = normalizeThemePreference({
11
+ scheme: SITE.theme?.defaultScheme,
12
+ mode: SITE.theme?.defaultMode,
13
+ });
14
+ const navItems = SITE_NAV_GROUPS.flatMap((group) => group.items);
12
15
  ---
13
16
 
14
- <!doctype html>
15
- <html lang="en">
16
- <head>
17
- <meta charset="UTF-8" />
18
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
19
- <title>{title ? `${title} | ${SITE.name}` : SITE.name}</title>
20
- <meta name="description" content={description ?? SITE.description} />
17
+ <PublicShell
18
+ title={title ? `${title} | ${SITE.name}` : SITE.name}
19
+ description={description ?? SITE.description}
20
+ currentPath={currentPath}
21
+ appearance={appearance}
22
+ brand={{
23
+ name: SITE.name,
24
+ tag: SITE.statement,
25
+ href: '/',
26
+ logoSrc: SITE.logo.src,
27
+ logoAlt: SITE.logo.alt,
28
+ }}
29
+ navItems={navItems}
30
+ navGroups={SITE_NAV_GROUPS}
31
+ contentWidth="content"
32
+ >
33
+ <Fragment slot="head">
21
34
  <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
22
35
  <link rel="shortcut icon" href="/favicon.svg" type="image/svg+xml" />
23
- <ThemeScript includeCss={false} />
24
36
  {SITE_THEME_CSS && <style is:global>{SITE_THEME_CSS}</style>}
25
- </head>
26
- <body>
27
- <div class="flex min-h-screen flex-col">
28
- <header class="sticky top-0 z-30 mb-12 bg-[color:var(--ts-color-surface-overlay)] backdrop-blur md:mb-16">
29
- <div class="border-y border-[color:var(--ts-color-border-strong)] px-4 py-4 md:px-6">
30
- <div class="mx-auto flex max-w-[var(--ts-content-width)] flex-col gap-4 sm:px-2 lg:flex-row lg:items-center lg:justify-between lg:px-4">
31
- <a href="/" class="flex items-center gap-3">
32
- <div class="flex h-11 w-11 items-center justify-center tracking-[0.2em]">
33
- <img src={SITE.logo.src} alt={SITE.logo.alt} class="h-11 w-11" />
34
- </div>
35
- <div>
36
- <p class="text-xl font-bold text-[color:var(--ts-color-text)]">{SITE.name}</p>
37
- <p class="text-sm text-[color:var(--ts-color-text-subtle)]">{SITE.statement}</p>
38
- </div>
39
- </a>
40
- <div class="ml-auto flex w-full flex-wrap items-center justify-end gap-3 lg:w-auto lg:flex-nowrap">
41
- <nav class="flex w-full justify-end text-base text-[color:var(--ts-color-text-muted)] lg:w-auto">
42
- <ul class="js-site-nav-disclosures flex flex-wrap justify-end gap-3">
43
- {
44
- SITE_NAV_GROUPS.map((group) => {
45
- return (
46
- <li class="relative">
47
- <details class="group js-site-nav-disclosure">
48
- <summary
49
- class:list={[
50
- 'flex cursor-pointer list-none items-center gap-2 border-b-2 border-transparent px-1 py-2 font-medium transition marker:content-none hover:border-[color:var(--ts-color-info)] hover:text-[color:var(--ts-color-text)]',
51
- group.items.some((item) => isCurrentSitePath(currentPath, item.href)) &&
52
- 'border-[color:var(--ts-color-accent)] text-[color:var(--ts-color-text)]',
53
- ]}
54
- >
55
- {group.label}
56
- <span class="text-xs transition group-open:rotate-180">▾</span>
57
- </summary>
58
- <div class="absolute right-0 top-full z-40 mt-2 min-w-56 border border-[color:var(--ts-color-border-strong)] bg-[color:var(--ts-color-surface-overlay)] p-2 shadow-[var(--ts-color-shadow)] backdrop-blur">
59
- <ul class="flex flex-col gap-1">
60
- {group.items.map((item) => (
61
- <li>
62
- <a
63
- href={item.href}
64
- class:list={[
65
- 'block px-3 py-2 text-sm transition hover:bg-[color:var(--ts-color-info-soft)] hover:text-[color:var(--ts-color-text)]',
66
- isCurrentSitePath(currentPath, item.href) &&
67
- 'bg-[color:var(--ts-color-surface-muted)] text-[color:var(--ts-color-text)]',
68
- ]}
69
- >
70
- {item.label}
71
- </a>
72
- </li>
73
- ))}
74
- </ul>
75
- </div>
76
- </details>
77
- </li>
78
- );
79
- })
80
- }
81
- </ul>
82
- </nav>
83
- <a
84
- href={SITE.githubRepository}
85
- target="_blank"
86
- rel="noreferrer"
87
- aria-label={`${SITE.name} GitHub repository`}
88
- class="inline-flex h-12 w-12 shrink-0 items-center justify-center border-2 border-transparent bg-transparent text-[color:var(--ts-color-text)] transition hover:-translate-y-0.5 hover:text-[color:var(--ts-color-info)] focus-visible:border-[color:var(--ts-color-info)] focus-visible:outline-none"
89
- >
90
- <svg
91
- xmlns="http://www.w3.org/2000/svg"
92
- viewBox="0 0 16 16"
93
- aria-hidden="true"
94
- class="h-6 w-6 fill-current"
95
- >
96
- <path d="M8 0C3.58 0 0 3.67 0 8.2c0 3.63 2.29 6.7 5.47 7.78.4.08.55-.18.55-.39 0-.2-.01-.85-.01-1.54-2.01.38-2.53-.5-2.69-.95-.09-.24-.48-.95-.82-1.15-.28-.15-.68-.54-.01-.55.63-.01 1.08.59 1.23.84.72 1.24 1.87.89 2.33.68.07-.54.28-.89.5-1.09-1.78-.21-3.64-.92-3.64-4.08 0-.9.31-1.64.82-2.22-.08-.21-.36-1.06.08-2.2 0 0 .67-.22 2.2.85A7.34 7.34 0 0 1 8 4.83c.68 0 1.37.09 2.01.27 1.53-1.07 2.2-.85 2.2-.85.44 1.14.16 1.99.08 2.2.51.58.82 1.31.82 2.22 0 3.17-1.87 3.87-3.65 4.08.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.19 0 .21.15.48.55.39A8.22 8.22 0 0 0 16 8.2C16 3.67 12.42 0 8 0Z" />
97
- </svg>
98
- </a>
99
- <a
100
- href={SITE.discordLink}
101
- target="_blank"
102
- rel="noreferrer"
103
- aria-label={`${SITE.name} Discord`}
104
- class="inline-flex h-12 w-12 shrink-0 items-center justify-center border-2 border-transparent bg-transparent text-[color:var(--ts-color-text)] transition hover:-translate-y-0.5 hover:text-[color:var(--ts-color-info)] focus-visible:border-[color:var(--ts-color-info)] focus-visible:outline-none"
105
- >
106
- <svg
107
- xmlns="http://www.w3.org/2000/svg"
108
- viewBox="0 0 24 24"
109
- aria-hidden="true"
110
- class="h-6 w-6 fill-current"
111
- >
112
- <path d="M20.317 4.369A19.791 19.791 0 0 0 15.885 3c-.191.328-.403.762-.554 1.104a18.27 18.27 0 0 0-5.314 0A11.64 11.64 0 0 0 9.463 3a19.736 19.736 0 0 0-4.433 1.369C2.227 8.617 1.468 12.759 1.848 16.845a19.9 19.9 0 0 0 5.437 2.755c.438-.6.825-1.236 1.157-1.902-.637-.241-1.246-.545-1.818-.9.152-.111.3-.229.444-.347 3.507 1.648 7.316 1.648 10.782 0 .145.118.293.236.444.347-.573.355-1.183.659-1.82.9.332.666.719 1.302 1.158 1.902a19.87 19.87 0 0 0 5.438-2.755c.446-4.737-.762-8.842-3.753-12.476ZM9.954 14.379c-1.053 0-1.918-.966-1.918-2.153 0-1.188.845-2.153 1.918-2.153 1.082 0 1.938.975 1.918 2.153 0 1.187-.846 2.153-1.918 2.153Zm4.092 0c-1.053 0-1.918-.966-1.918-2.153 0-1.188.845-2.153 1.918-2.153 1.082 0 1.938.975 1.918 2.153 0 1.187-.836 2.153-1.918 2.153Z" />
113
- </svg>
114
- </a>
115
- <a
116
- href="/contact/"
117
- aria-label={`Contact ${SITE.name}`}
118
- class="inline-flex h-12 w-12 shrink-0 items-center justify-center border-2 border-transparent bg-transparent text-[color:var(--ts-color-text)] transition hover:-translate-y-0.5 hover:text-[color:var(--ts-color-info)] focus-visible:border-[color:var(--ts-color-info)] focus-visible:outline-none"
119
- >
120
- <svg
121
- xmlns="http://www.w3.org/2000/svg"
122
- viewBox="0 0 24 24"
123
- aria-hidden="true"
124
- class="h-6 w-6 fill-none stroke-current stroke-[1.8]"
125
- >
126
- <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5v10.5H3.75z" />
127
- <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 7.5 7.5 6 7.5-6" />
128
- </svg>
129
- </a>
130
- </div>
131
- </div>
132
- </div>
133
- </header>
134
-
135
- <main class="mx-auto flex-1 max-w-[var(--ts-content-width)] px-4 pb-12 sm:px-6 lg:px-8">
136
- <slot />
137
- </main>
138
-
139
- <footer class="mt-20 border-t-2 border-[color:var(--ts-color-border-strong)] pt-8">
140
- <div class="mb-15 mx-auto max-w-[var(--ts-content-width)] px-4 sm:px-6 lg:px-8">
141
- <div class="flex flex-wrap justify-center gap-8 pb-8">
142
- <div class="w-64">
143
- <p class="text-xl font-bold text-[color:var(--ts-color-text)]">{SITE.name}</p>
144
- <p class="mt-3 text-base leading-8 text-[color:var(--ts-color-text-muted)]">
145
- {SITE.summary}
146
- </p>
147
- </div>
148
- <div class="w-64">
149
- <p class="text-sm font-semibold uppercase tracking-[0.16em] text-[color:var(--ts-color-accent-strong)]">Project stage</p>
150
- <p class="mt-3 text-base font-semibold text-[color:var(--ts-color-text)]">{PROJECT_STAGE.label}</p>
151
- <p class="mt-2 text-base leading-8 text-[color:var(--ts-color-text-muted)]">{PROJECT_STAGE.description}</p>
152
- </div>
153
- {SITE_FOOTER_MENU.map((group) => (
154
- <div class="w-64">
155
- <p class="text-sm font-semibold uppercase tracking-[0.16em] text-[color:var(--ts-color-info-text)]">{group.label}</p>
156
- <div class="mt-3 flex flex-col gap-2 text-base text-[color:var(--ts-color-text-muted)]">
157
- {group.items.map((item) => (
158
- <a href={item.href} class="hover:text-[color:var(--ts-color-text)]">{item.label}</a>
159
- ))}
160
- </div>
161
- </div>
162
- ))}
163
- </div>
164
- <FooterSubscribeForm currentPath={currentPath} />
165
- </div>
166
- </footer>
167
- </div>
168
- <script>
169
- const navRoot = document.querySelector('.js-site-nav-disclosures');
170
- const navDetails = document.querySelectorAll<HTMLDetailsElement>('details.js-site-nav-disclosure');
171
-
172
- navDetails.forEach((details) => {
173
- details.addEventListener('toggle', () => {
174
- if (!details.open) return;
175
-
176
- navDetails.forEach((otherDetails) => {
177
- if (otherDetails !== details) {
178
- otherDetails.open = false;
179
- }
180
- });
181
- });
182
- });
183
-
184
- document.addEventListener('click', (event) => {
185
- if (!(navRoot instanceof HTMLElement)) return;
186
- if (event.target instanceof Node && navRoot.contains(event.target)) return;
187
-
188
- navDetails.forEach((details) => {
189
- details.open = false;
190
- });
191
- });
192
-
193
- navRoot?.addEventListener('click', (event) => {
194
- const target = event.target;
195
- if (!(target instanceof HTMLElement)) return;
196
- if (!target.closest('a')) return;
197
-
198
- navDetails.forEach((details) => {
199
- details.open = false;
200
- });
201
- });
202
- </script>
203
- <DevWatchReload />
204
- </body>
205
- </html>
37
+ </Fragment>
38
+ <Fragment slot="actions">
39
+ <a
40
+ href={SITE.githubRepository}
41
+ target="_blank"
42
+ rel="noreferrer"
43
+ aria-label={`${SITE.name} GitHub repository`}
44
+ title={`${SITE.name} GitHub repository`}
45
+ class="ts-public-shell__icon-link"
46
+ >
47
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
48
+ <path d="M8 0C3.58 0 0 3.67 0 8.2c0 3.63 2.29 6.7 5.47 7.78.4.08.55-.18.55-.39 0-.2-.01-.85-.01-1.54-2.01.38-2.53-.5-2.69-.95-.09-.24-.48-.95-.82-1.15-.28-.15-.68-.54-.01-.55.63-.01 1.08.59 1.23.84.72 1.24 1.87.89 2.33.68.07-.54.28-.89.5-1.09-1.78-.21-3.64-.92-3.64-4.08 0-.9.31-1.64.82-2.22-.08-.21-.36-1.06.08-2.2 0 0 .67-.22 2.2.85A7.34 7.34 0 0 1 8 4.83c.68 0 1.37.09 2.01.27 1.53-1.07 2.2-.85 2.2-.85.44 1.14.16 1.99.08 2.2.51.58.82 1.31.82 2.22 0 3.17-1.87 3.87-3.65 4.08.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.19 0 .21.15.48.55.39A8.22 8.22 0 0 0 16 8.2C16 3.67 12.42 0 8 0Z" />
49
+ </svg>
50
+ </a>
51
+ <a
52
+ href={SITE.discordLink}
53
+ target="_blank"
54
+ rel="noreferrer"
55
+ aria-label={`${SITE.name} Discord`}
56
+ title={`${SITE.name} Discord`}
57
+ class="ts-public-shell__icon-link"
58
+ >
59
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
60
+ <path d="M20.317 4.369A19.791 19.791 0 0 0 15.885 3c-.191.328-.403.762-.554 1.104a18.27 18.27 0 0 0-5.314 0A11.64 11.64 0 0 0 9.463 3a19.736 19.736 0 0 0-4.433 1.369C2.227 8.617 1.468 12.759 1.848 16.845a19.9 19.9 0 0 0 5.437 2.755c.438-.6.825-1.236 1.157-1.902-.637-.241-1.246-.545-1.818-.9.152-.111.3-.229.444-.347 3.507 1.648 7.316 1.648 10.782 0 .145.118.293.236.444.347-.573.355-1.183.659-1.82.9.332.666.719 1.302 1.158 1.902a19.87 19.87 0 0 0 5.438-2.755c.446-4.737-.762-8.842-3.753-12.476ZM9.954 14.379c-1.053 0-1.918-.966-1.918-2.153 0-1.188.845-2.153 1.918-2.153 1.082 0 1.938.975 1.918 2.153 0 1.187-.846 2.153-1.918 2.153Zm4.092 0c-1.053 0-1.918-.966-1.918-2.153 0-1.188.845-2.153 1.918-2.153 1.082 0 1.938.975 1.918 2.153 0 1.187-.836 2.153-1.918 2.153Z" />
61
+ </svg>
62
+ </a>
63
+ <a
64
+ href="/contact/"
65
+ aria-label={`Contact ${SITE.name}`}
66
+ title={`Contact ${SITE.name}`}
67
+ class="ts-public-shell__icon-link ts-public-shell__icon-link--stroke"
68
+ >
69
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
70
+ <path d="M3.75 6.75h16.5v10.5H3.75z" />
71
+ <path d="m4.5 7.5 7.5 6 7.5-6" />
72
+ </svg>
73
+ </a>
74
+ </Fragment>
75
+ <slot />
76
+ </PublicShell>
@@ -9,6 +9,9 @@ const copyEntry = (entry) => entry.type === "link" ? copyLink(entry) : {
9
9
  };
10
10
  const findTopLevelGroup = (sidebar, label) => sidebar.find((entry) => entry.type === "group" && entry.label === label);
11
11
  const flattenLinks = (entries) => entries.flatMap((entry) => entry.type === "link" ? [copyLink(entry)] : flattenLinks(entry.entries));
12
+ const findTopLevelGroupForPath = (sidebar, currentPath) => sidebar.find(
13
+ (entry) => entry.type === "group" && flattenLinks(entry.entries).some((link) => normalizeHref(link.href) === normalizeHref(currentPath))
14
+ );
12
15
  const buildPagination = (entries, currentHref) => {
13
16
  const flatLinks = flattenLinks(entries);
14
17
  const currentIndex = flatLinks.findIndex((link) => normalizeHref(link.href) === normalizeHref(currentHref));
@@ -25,19 +28,6 @@ const setRouteSidebar = (route, currentPath, sidebar, paginationSource) => {
25
28
  route.hasSidebar = sidebar.length > 0;
26
29
  route.pagination = paginationSource ? buildPagination(paginationSource, currentPath) : { prev: void 0, next: void 0 };
27
30
  };
28
- const defaultRuntime = {
29
- BOOKS: [],
30
- BOOKS_LINK: {
31
- label: "Books",
32
- link: TREESEED_LINKS.home
33
- },
34
- TREESEED_LIBRARY_DOWNLOAD: {
35
- downloadFileName: "treeseed-knowledge.md",
36
- downloadHref: "/books/treeseed-knowledge.md",
37
- downloadTitle: "TreeSeed Knowledge Library"
38
- },
39
- TREESEED_LINKS
40
- };
41
31
  function runtimeTenantModelRendered(modelName) {
42
32
  const featureValue = RUNTIME_TENANT.features?.[modelName];
43
33
  const siteValue = RUNTIME_TENANT.site?.[modelName];
@@ -50,7 +40,23 @@ const onRequest = defineRouteMiddleware(async (context) => {
50
40
  setRouteSidebar(route, currentPath, [], null);
51
41
  return;
52
42
  }
53
- const runtime = await loadHostedBookRuntime(context.locals) ?? defaultRuntime;
43
+ let runtime = null;
44
+ try {
45
+ runtime = await loadHostedBookRuntime(context.locals);
46
+ } catch {
47
+ runtime = null;
48
+ }
49
+ if (!runtime) {
50
+ const bookGroup = findTopLevelGroupForPath(route.sidebar, currentPath);
51
+ if (bookGroup) {
52
+ setRouteSidebar(route, currentPath, [copyEntry(bookGroup)], bookGroup.entries);
53
+ return;
54
+ }
55
+ if (currentPath === normalizeHref(TREESEED_LINKS.home)) {
56
+ setRouteSidebar(route, currentPath, [], null);
57
+ }
58
+ return;
59
+ }
54
60
  route.sidebar = buildStarlightSidebarEntriesFromRuntime(runtime, currentPath);
55
61
  const activeBook = runtime.BOOKS.find(
56
62
  (book) => currentPath.startsWith(normalizeHref(book.basePath))
@@ -11,15 +11,15 @@ import MainLayout from '../layouts/MainLayout.astro';
11
11
  <p class="text-sm font-semibold uppercase tracking-[0.16em] text-[color:var(--ts-color-accent-strong)]">404</p>
12
12
  <h1 class="font-serif text-5xl text-[color:var(--ts-color-text)]">Page not found</h1>
13
13
  <p class="text-lg leading-9 text-[color:var(--ts-color-text-muted)]">
14
- The page you requested is not available in this Treeseed. Try the homepage, the knowledge
14
+ The page you requested is not available in this Treeseed. Try the homepage, the books
15
15
  library, or the current project status page instead.
16
16
  </p>
17
17
  <div class="flex flex-wrap gap-4">
18
18
  <a href="/" class="border border-[color:var(--ts-color-accent)] bg-[color:var(--ts-color-accent)] px-5 py-3 text-base font-semibold text-[color:var(--ts-color-text)] transition hover:border-[color:var(--ts-color-info)] hover:bg-[color:var(--ts-color-info-soft)]">
19
19
  Go home
20
20
  </a>
21
- <a href="/knowledge/" class="border border-[color:var(--ts-color-border-strong)] px-5 py-3 text-base font-semibold text-[color:var(--ts-color-text)] transition hover:border-[color:var(--ts-color-info)] hover:bg-[color:var(--ts-color-info-soft)]">
22
- Open knowledge
21
+ <a href="/books/" class="border border-[color:var(--ts-color-border-strong)] px-5 py-3 text-base font-semibold text-[color:var(--ts-color-text)] transition hover:border-[color:var(--ts-color-info)] hover:bg-[color:var(--ts-color-info-soft)]">
22
+ Open books
23
23
  </a>
24
24
  <a href="/status/" class="border border-[color:var(--ts-color-border-strong)] px-5 py-3 text-base font-semibold text-[color:var(--ts-color-text)] transition hover:border-[color:var(--ts-color-info)] hover:bg-[color:var(--ts-color-info-soft)]">
25
25
  View status
@@ -8,7 +8,7 @@ import { isPublishedRuntimeContentMode, loadPublishedEntry } from '../utils/site
8
8
 
9
9
  export const prerender = false;
10
10
 
11
- const slug = String(Astro.params.slug ?? '');
11
+ const slug = String(Astro.params.slug ?? Astro.url.pathname.replace(/^\/+|\/+$/g, ''));
12
12
  const publishedRuntime = isPublishedRuntimeContentMode();
13
13
  const localEntry = publishedRuntime ? null : (await getCollection('pages')).find((candidate) => candidate.data.slug === slug) ?? null;
14
14
  const publishedEntry = publishedRuntime ? await loadPublishedEntry(Astro.locals, 'pages', slug) : null;
@@ -9,14 +9,14 @@ export const prerender = false;
9
9
 
10
10
  const slug = String(Astro.params.slug ?? '');
11
11
  const publishedRuntime = isPublishedRuntimeContentMode();
12
- const books = publishedRuntime ? [] : (await getCollection('books')).sort((a, b) => a.data.order - b.data.order);
13
- const localBook = publishedRuntime ? null : books.find((candidate) => candidate.id === slug) ?? null;
12
+ const books = (await getCollection('books')).sort((a, b) => a.data.order - b.data.order);
13
+ const localBook = books.find((candidate) => candidate.id === slug || candidate.data.slug === slug) ?? null;
14
14
  const publishedBook = publishedRuntime ? await loadPublishedEntry(Astro.locals, 'books', slug) : null;
15
- const book = publishedRuntime ? publishedBook?.entry ?? null : localBook;
15
+ const book = publishedRuntime ? publishedBook?.entry ?? localBook : localBook;
16
16
  if (!book) {
17
17
  Astro.response.status = 404;
18
18
  }
19
- const rendered = !publishedRuntime && localBook ? await render(localBook) : null;
19
+ const rendered = localBook ? await render(localBook) : null;
20
20
  const Content = rendered?.Content ?? null;
21
21
  ---
22
22
 
@@ -25,7 +25,7 @@ const Content = rendered?.Content ?? null;
25
25
  <RouteNotFound title="Book not found" description="The requested book could not be found in this Treeseed." currentPath="/books/" />
26
26
  ) : (
27
27
  <BookLayout entry={book.data} currentPath="/books/">
28
- {publishedRuntime ? <PublishedContentBody html={publishedBook?.html ?? ''} /> : <Content />}
28
+ {publishedBook?.html ? <PublishedContentBody html={publishedBook.html} /> : Content ? <Content /> : null}
29
29
  </BookLayout>
30
30
  )
31
31
  }
@@ -51,7 +51,7 @@ const download = activeBook
51
51
  <p class="text-sm font-semibold uppercase tracking-[0.16em] text-[color:var(--ts-color-info-text)]">
52
52
  {activeBook?.sectionLabel ?? 'Knowledge'}
53
53
  </p>
54
- <a href="/knowledge/" class="block text-sm font-medium text-[color:var(--ts-color-text-muted)] hover:text-[color:var(--ts-color-text)]">Knowledge home</a>
54
+ <a href={runtime?.TREESEED_LINKS.home ?? '/books/'} class="block text-sm font-medium text-[color:var(--ts-color-text-muted)] hover:text-[color:var(--ts-color-text)]">Books home</a>
55
55
  {activeBook?.landingPath && (
56
56
  <a href={activeBook.landingPath} class="block text-sm font-medium text-[color:var(--ts-color-text-muted)] hover:text-[color:var(--ts-color-text)]">Open book landing page</a>
57
57
  )}
@@ -20,10 +20,17 @@ function readNumberOption(name) {
20
20
  return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
21
21
  }
22
22
  function parseSurface(value) {
23
- if (value === 'web' || value === 'integrated') {
23
+ if (value === 'web' ||
24
+ value === 'api' ||
25
+ value === 'manager' ||
26
+ value === 'worker' ||
27
+ value === 'agents' ||
28
+ value === 'services' ||
29
+ value === 'all' ||
30
+ value === 'integrated') {
24
31
  return value;
25
32
  }
26
- return 'integrated';
33
+ return undefined;
27
34
  }
28
35
  function parseSetupMode(value) {
29
36
  if (value === 'auto' || value === 'check' || value === 'off') {
@@ -45,6 +52,7 @@ function parseOpenMode(value) {
45
52
  }
46
53
  const exitCode = await runTreeseedIntegratedDev({
47
54
  surface: parseSurface(readOption('--surface')),
55
+ surfaces: readOption('--surfaces'),
48
56
  watch: readFlag('--watch'),
49
57
  webHost: readOption('--host'),
50
58
  webPort: readNumberOption('--port'),
package/dist/site.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { defineConfig, envField } from "astro/config";
2
2
  import cloudflare from "@astrojs/cloudflare";
3
- import { existsSync, readFileSync } from "node:fs";
3
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { parse as parseYaml } from "yaml";
6
7
  import rehypeKatex from "rehype-katex";
7
8
  import remarkMath from "remark-math";
8
9
  import tailwindcss from "@tailwindcss/vite";
@@ -12,6 +13,7 @@ import { getStarlightSidebarConfigFromRuntime } from "./utils/starlight-nav.js";
12
13
  import { buildTreeseedThemeCss } from "./utils/theme.js";
13
14
  import { loadTreeseedDeployConfig } from "@treeseed/sdk/platform/deploy-config";
14
15
  import { getTreeseedContentServingMode } from "@treeseed/sdk/platform/deploy-runtime";
16
+ import { getTenantContentRoot } from "@treeseed/sdk/platform/tenant-config";
15
17
  import { loadTreeseedPluginRuntime } from "@treeseed/sdk/platform/plugins";
16
18
  import {
17
19
  buildTreeseedSiteLayers,
@@ -42,7 +44,6 @@ const PACKAGE_ROUTE_ENTRIES = [
42
44
  { pattern: "/contact", resourcePath: "pages/contact.astro" },
43
45
  { pattern: "/feed.xml", resourcePath: "pages/feed.xml", model: "notes" },
44
46
  { pattern: "/ui", resourcePath: "pages/ui/index.astro" },
45
- { pattern: "/[slug]", resourcePath: "pages/[slug].astro", model: "pages" },
46
47
  { pattern: "/agents", resourcePath: "pages/agents/index.astro", model: "agents" },
47
48
  { pattern: "/agents/[slug]", resourcePath: "pages/agents/[slug].astro", model: "agents" },
48
49
  { pattern: "/books", resourcePath: "pages/books/index.astro", model: "books" },
@@ -60,6 +61,48 @@ const PACKAGE_ROUTE_ENTRIES = [
60
61
  { pattern: "/questions", resourcePath: "pages/questions/index.astro", model: "questions" },
61
62
  { pattern: "/questions/[slug]", resourcePath: "pages/questions/[slug].astro", model: "questions" }
62
63
  ];
64
+ const DYNAMIC_PAGE_ROUTE_ENTRY = { pattern: "/[slug]", resourcePath: "pages/[slug].astro", model: "pages" };
65
+ function collectMarkdownFiles(rootPath) {
66
+ if (!existsSync(rootPath)) {
67
+ return [];
68
+ }
69
+ const stats = statSync(rootPath);
70
+ if (stats.isFile()) {
71
+ return /\.(md|mdx)$/iu.test(rootPath) ? [rootPath] : [];
72
+ }
73
+ return readdirSync(rootPath, { withFileTypes: true }).flatMap((entry) => {
74
+ const fullPath = resolve(rootPath, entry.name);
75
+ if (entry.isDirectory()) {
76
+ return collectMarkdownFiles(fullPath);
77
+ }
78
+ return entry.isFile() && /\.(md|mdx)$/iu.test(entry.name) ? [fullPath] : [];
79
+ });
80
+ }
81
+ function readFrontmatter(filePath) {
82
+ const raw = readFileSync(filePath, "utf8");
83
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
84
+ if (!match) {
85
+ return null;
86
+ }
87
+ return parseYaml(match[1]);
88
+ }
89
+ function collectLocalPageRouteEntries(tenantConfig, projectRoot) {
90
+ const pagesRoot = resolve(projectRoot, getTenantContentRoot(tenantConfig, "pages"));
91
+ const slugs = /* @__PURE__ */ new Set();
92
+ for (const filePath of collectMarkdownFiles(pagesRoot)) {
93
+ const frontmatter = readFrontmatter(filePath);
94
+ const slug = typeof frontmatter?.slug === "string" ? frontmatter.slug.replace(/^\/+|\/+$/g, "") : "";
95
+ if (!slug || slug.includes("/")) {
96
+ continue;
97
+ }
98
+ slugs.add(slug);
99
+ }
100
+ return [...slugs].sort().map((slug) => ({
101
+ pattern: `/${slug}`,
102
+ resourcePath: "pages/[slug].astro",
103
+ model: "pages"
104
+ }));
105
+ }
63
106
  function createTreeseedRoutesIntegration(tenantConfig, routes = []) {
64
107
  return {
65
108
  name: "treeseed-routes",
@@ -234,12 +277,15 @@ function createTreeseedSite(tenantConfig, { starlight }) {
234
277
  const injectedProjectRoot = JSON.stringify(projectRoot);
235
278
  const injectedSiteConfig = JSON.stringify(siteConfig);
236
279
  const injectedDeployConfig = JSON.stringify(deployConfig);
280
+ const injectedBookRuntime = JSON.stringify(bookRuntime);
237
281
  const resolvedGlobalCss = resolveTreeseedStyleEntrypoint(siteLayers, "styles/global.css");
238
282
  const serverRendered = deployConfig.surfaces?.web?.provider === "cloudflare" || deployConfig.providers.deploy === "cloudflare";
239
283
  const allowedDomains = deriveTreeseedAstroAllowedDomains(deployConfig, { siteUrl: siteConfig.site.siteUrl });
240
284
  const publishedRuntime = getTreeseedContentServingMode() === "published_runtime";
285
+ const pageRoutes = publishedRuntime ? [DYNAMIC_PAGE_ROUTE_ENTRY] : collectLocalPageRouteEntries(tenantConfig, projectRoot);
241
286
  const packageRoutes = [
242
287
  ...PACKAGE_ROUTE_ENTRIES,
288
+ ...pageRoutes,
243
289
  ...docsRendered && publishedRuntime ? [
244
290
  { pattern: "/knowledge", resourcePath: "pages/docs-runtime/index.astro", model: "docs" },
245
291
  { pattern: "/knowledge/[...slug]", resourcePath: "pages/docs-runtime/[...slug].astro", model: "docs" }
@@ -264,7 +310,8 @@ function createTreeseedSite(tenantConfig, { starlight }) {
264
310
  __TREESEED_TENANT_CONFIG__: injectedTenantConfig,
265
311
  __TREESEED_PROJECT_ROOT__: injectedProjectRoot,
266
312
  __TREESEED_SITE_CONFIG__: injectedSiteConfig,
267
- __TREESEED_DEPLOY_CONFIG__: injectedDeployConfig
313
+ __TREESEED_DEPLOY_CONFIG__: injectedDeployConfig,
314
+ __TREESEED_BOOK_RUNTIME__: injectedBookRuntime
268
315
  },
269
316
  plugins: [
270
317
  createTenantThemeVitePlugin(tenantThemeCss),