@rxova/brand 0.8.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rxova/brand",
3
- "version": "0.8.2",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "Design tokens, Starlight theme and shared site chrome for rxova.org",
6
6
  "keywords": [
@@ -1,83 +1,35 @@
1
1
  ---
2
2
  /**
3
- * The shell for rxova.org's non-Starlight surfaces — /blog and /updates.
3
+ * Artifact document for rxova.org's non-Starlight page components.
4
4
  *
5
5
  * It lived in both packages as a 244-line file that differed on four lines, which
6
6
  * is exactly how the menu came to render in a different order on each: a fix has to
7
7
  * be made twice, and once was enough to forget.
8
8
  *
9
- * Not for the docs sites those are Starlight and take their chrome from
10
- * `sharedStarlightConfig`. This is for the surfaces that are plain Astro.
9
+ * This deliberately owns no site header, footer, analytics or theme bootstrap.
10
+ * It gives an independently-built page component a standards-compliant document
11
+ * for its metadata and assets; rxova-website consumes its body and supplies the
12
+ * public shell at deploy time.
11
13
  */
12
14
  import '../fonts.css'
13
15
  import '../astro.css'
14
16
 
15
- import { RXOVA_ORIGIN, siteUrl } from '../sites.ts'
16
-
17
- import Header, { type HeaderItem } from './Header.astro'
18
- import SiteFooter from './SiteFooter.astro'
19
- import ThemeScript from './ThemeScript.astro'
17
+ import { canonicalUrl, siteUrl } from '../sites.ts'
20
18
 
21
19
  interface Props {
22
20
  title: string
23
21
  description: string
24
- /** Canonical path, e.g. `/blog/why-rxova-has-a-blog`. */
22
+ /** Canonical path, e.g. `/blog/why-rxova-has-a-blog`. Normalised below. */
25
23
  path: string
26
24
  ogType?: 'website' | 'article'
27
25
  }
28
26
 
29
27
  const { title, description, path, ogType = 'website' } = Astro.props
30
- const canonical = `${RXOVA_ORIGIN}${path}`
31
-
32
- /**
33
- * The menu, in one fixed order for every surface.
34
- *
35
- * Which item is current is *derived* from the base the surface was built for, not
36
- * declared per package — declaring it is what let the two drift into different
37
- * orders. `BASE_URL` is substituted by the consuming app's build, so this is
38
- * `/blog` inside @rxova/blog and `/updates` inside @rxova/updates.
39
- */
40
- const base = import.meta.env.BASE_URL.replace(/\/$/, '')
41
-
42
- /**
43
- * The surfaces that can be the *current* one here — the mounted ones this shell
44
- * is ever built for. /about is not among them: it is a page of the landing's own
45
- * Astro build rather than a mount, so it can never match `base`, and it is added
46
- * as a fixed trailing item below.
47
- */
48
- const SECTIONS = [
49
- { label: 'Blog', path: '/blog' },
50
- { label: 'Updates', path: '/updates' },
51
- ]
52
-
53
- /**
54
- * A URL inside this surface.
55
- *
56
- * Anything *leaving* it goes through `siteUrl`: a bare `/updates` would resolve
57
- * against the mount and point at `/blog/updates`.
58
- */
59
- const inside = (p = '') => `${base}/${p}`.replace(/\/{2,}/g, '/')
60
28
 
61
- /**
62
- * The menu, fully resolved for this surface, in the one fixed order every surface
63
- * shows it. The current section links inside its own base; every other link is
64
- * absolute so it does not resolve against this mount. `Header` owns how it looks;
65
- * this owns where it points.
66
- */
67
- const items: HeaderItem[] = [
68
- { label: 'Projects', href: siteUrl('/') },
69
- ...SECTIONS.map((section) =>
70
- section.path === base
71
- ? { label: section.label, href: inside(), current: true }
72
- : { label: section.label, href: siteUrl(section.path) },
73
- ),
74
- // Fixed trailing item, like Projects is a fixed leading one, and for the same
75
- // reason: it belongs to the landing's build, so it is always deployed and can
76
- // never be the current surface here. The landing grew an About entry and these
77
- // two did not, which left a reader on a post with no way to reach it — the
78
- // failure mode this shell exists to prevent.
79
- { label: 'About', href: siteUrl('/about') },
80
- ]
29
+ // Normalised here rather than at the four call sites: every one of them had
30
+ // dropped the trailing slash, so every blog and updates page was declaring itself
31
+ // canonical at a URL that 301s. A caller can forget again; this cannot.
32
+ const canonical = canonicalUrl(path)
81
33
  ---
82
34
 
83
35
  <!doctype html>
@@ -85,8 +37,6 @@ const items: HeaderItem[] = [
85
37
  <head>
86
38
  <meta charset="utf-8" />
87
39
  <meta name="viewport" content="width=device-width, initial-scale=1" />
88
- <link rel="icon" type="image/png" href={siteUrl('/rxova-logo-256.png')} />
89
- <link rel="apple-touch-icon" href={siteUrl('/rxova-logo-256.png')} />
90
40
  <link rel="canonical" href={canonical} />
91
41
  <title>{title}</title>
92
42
  <meta name="description" content={description} />
@@ -96,35 +46,18 @@ const items: HeaderItem[] = [
96
46
  <meta property="og:url" content={canonical} />
97
47
  <meta property="og:image" content={siteUrl('/og/rxova.png')} />
98
48
  <meta name="twitter:card" content="summary_large_image" />
99
- <ThemeScript />
100
49
  </head>
101
50
  <body>
102
- <Header homeHref={siteUrl('/')} logoSrc={siteUrl('/rxova-logo-256.png')} items={items} />
103
-
104
51
  <main>
105
52
  <slot />
106
53
  </main>
107
54
 
108
- <div class="foot">
109
- <SiteFooter />
110
- </div>
111
-
112
55
  <style>
113
- /* The header is `Header` (its own component, shared with the landing); the
114
- only chrome styled here is the page body and the footer's measure. */
115
56
  main {
116
57
  max-width: var(--max);
117
58
  margin: 0 auto;
118
59
  padding: clamp(2rem, 6vh, 3.5rem) 1.5rem 4rem;
119
60
  }
120
-
121
- /* The footer's own rules are unscoped (footer.css, shared with the docs
122
- sites); this only puts it on the page's measure. */
123
- .foot {
124
- max-width: var(--max);
125
- margin: 0 auto;
126
- padding: 0 1.5rem;
127
- }
128
61
  </style>
129
62
 
130
63
  <!-- Prose styling for rendered markdown. Global rather than scoped: the
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  getRepo,
16
16
  docsUrl,
17
17
  siteUrl,
18
+ canonicalUrl,
18
19
  type Project,
19
20
  type ProjectId,
20
21
  type RepoId,
package/src/sites.test.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  getRepo,
19
19
  docsUrl,
20
20
  siteUrl,
21
+ canonicalUrl,
21
22
  projectFromBase,
22
23
  type ProjectId,
23
24
  } from './sites.ts'
@@ -111,6 +112,31 @@ describe('urls', () => {
111
112
  })
112
113
  })
113
114
 
115
+ describe('canonicalUrl', () => {
116
+ // The bug this exists to prevent: /blog answers 301 and only /blog/ answers
117
+ // 200, so a canonical without the slash points a crawler at a redirect.
118
+ it('always ends in a slash, whatever the caller passed', () => {
119
+ expect(canonicalUrl('/blog')).toBe(`${RXOVA_ORIGIN}/blog/`)
120
+ expect(canonicalUrl('/blog/')).toBe(`${RXOVA_ORIGIN}/blog/`)
121
+ expect(canonicalUrl('/updates/repos/journey')).toBe(`${RXOVA_ORIGIN}/updates/repos/journey/`)
122
+ })
123
+
124
+ it('roots a path that arrives without a leading slash', () => {
125
+ expect(canonicalUrl('blog')).toBe(`${RXOVA_ORIGIN}/blog/`)
126
+ })
127
+
128
+ it('defaults to the origin root', () => {
129
+ expect(canonicalUrl()).toBe(`${RXOVA_ORIGIN}/`)
130
+ })
131
+
132
+ // siteUrl builds nav hrefs, where following a redirect costs nothing. Keeping
133
+ // them different is the point; collapsing them would reintroduce the bug.
134
+ it('differs from siteUrl, which does not normalise', () => {
135
+ expect(siteUrl('/blog')).toBe(`${RXOVA_ORIGIN}/blog`)
136
+ expect(canonicalUrl('/blog')).not.toBe(siteUrl('/blog'))
137
+ })
138
+ })
139
+
114
140
  describe('projectFromBase', () => {
115
141
  it('recognises a mount with and without its trailing slash', () => {
116
142
  expect(projectFromBase('/packages/journey/')).toBe('journey')
package/src/sites.ts CHANGED
@@ -121,6 +121,23 @@ export function siteUrl(path = '/'): string {
121
121
  return `${RXOVA_ORIGIN}${path.startsWith('/') ? path : `/${path}`}`
122
122
  }
123
123
 
124
+ /**
125
+ * Absolute canonical URL for a *page* on the umbrella site.
126
+ *
127
+ * Trailing slash, always — which is the whole reason this is not `siteUrl`.
128
+ * rxova.org is published as a directory-style tree on GitHub Pages, so `/blog`
129
+ * answers 301 and only `/blog/` answers 200. A self-referencing canonical naming
130
+ * the redirecting form points a crawler at a URL that does not serve the page,
131
+ * which is the one thing a canonical must never do. `siteUrl` stays as it is:
132
+ * a nav link may follow a redirect, a canonical may not.
133
+ *
134
+ * Pages only. Assets keep their exact path and want `siteUrl`.
135
+ */
136
+ export function canonicalUrl(path = '/'): string {
137
+ const rooted = path.startsWith('/') ? path : `/${path}`
138
+ return `${RXOVA_ORIGIN}${rooted.endsWith('/') ? rooted : `${rooted}/`}`
139
+ }
140
+
124
141
  /**
125
142
  * Which project a page belongs to, inferred from Astro's `BASE_URL`.
126
143
  *
package/src/starlight.css CHANGED
@@ -78,6 +78,44 @@ header.header {
78
78
  background: var(--sl-color-bg-nav);
79
79
  }
80
80
 
81
+ /* In a schema-2 page bundle the umbrella header is rendered by rxova-website
82
+ outside Starlight's `.page`. Keep Starlight's own sticky documentation bar
83
+ below it instead of letting the two sticky headers occupy the same top edge. */
84
+ html[data-rxova-shell] {
85
+ --rx-shell-header-height: 3.5rem;
86
+ }
87
+
88
+ html[data-rxova-shell] .page > header.header {
89
+ inset-block-start: var(--rx-shell-header-height);
90
+ }
91
+
92
+ html[data-rxova-shell] .sidebar-pane {
93
+ inset-block-start: calc(var(--rx-shell-header-height) + var(--sl-nav-height));
94
+ }
95
+
96
+ html[data-rxova-shell] mobile-starlight-toc nav {
97
+ top: calc(var(--rx-shell-header-height) + var(--sl-nav-height) - 1px);
98
+ }
99
+
100
+ html[data-rxova-shell] starlight-menu-button button {
101
+ top: calc(
102
+ var(--rx-shell-header-height) + (var(--sl-nav-height) - var(--sl-menu-button-size)) / 2
103
+ );
104
+ }
105
+
106
+ @media (min-width: 72rem) {
107
+ html[data-rxova-shell] .right-sidebar {
108
+ top: var(--rx-shell-header-height);
109
+ height: calc(100vh - var(--rx-shell-header-height));
110
+ }
111
+ }
112
+
113
+ @media (max-width: 34rem) {
114
+ html[data-rxova-shell] {
115
+ --rx-shell-header-height: 3.25rem;
116
+ }
117
+ }
118
+
81
119
  /* Site title: the rxova mark sits beside the project wordmark. */
82
120
  .rx-site-title {
83
121
  display: flex;
package/src/starlight.ts CHANGED
@@ -27,6 +27,12 @@ export interface SharedStarlightOptions {
27
27
  components?: Record<string, string>
28
28
  /** Path under the repo root that holds the docs site, for the edit link. */
29
29
  editLinkBase?: string
30
+ /**
31
+ * Build body-only documentation for rxova-website to place in its global
32
+ * shell. Starlight's search, sidebar and page navigation remain page UI; only
33
+ * the umbrella footer is omitted.
34
+ */
35
+ pageComponent?: boolean
30
36
  }
31
37
 
32
38
  export function sharedStarlightConfig({
@@ -35,6 +41,7 @@ export function sharedStarlightConfig({
35
41
  customCss = [],
36
42
  components = {},
37
43
  editLinkBase = 'apps/docs',
44
+ pageComponent = false,
38
45
  }: SharedStarlightOptions) {
39
46
  const self = getProject(project)
40
47
 
@@ -72,7 +79,7 @@ export function sharedStarlightConfig({
72
79
  SocialIcons: '@rxova/brand/components/SocialIcons.astro',
73
80
  // Starlight's default footer (pagination, edit link, last updated) plus
74
81
  // the shared four-column site footer beneath it.
75
- Footer: '@rxova/brand/components/Footer.astro',
82
+ ...(!pageComponent ? { Footer: '@rxova/brand/components/Footer.astro' } : {}),
76
83
  // Starlight's own picker, plus a resync when a page is restored from the
77
84
  // back/forward cache — without it, changing the theme on one rxova.org
78
85
  // surface and pressing Back leaves the restored page on the old theme.