@rxova/brand 0.8.1 → 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.1",
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": [
@@ -35,11 +35,15 @@ interface Props {
35
35
  /**
36
36
  * The standalone surfaces to list under "Site", below Home.
37
37
  *
38
- * Defaults to Blog and Updates, which is what every surface showed when this
39
- * column was hardcoded. It is a prop because only the umbrella repo knows
40
- * which surfaces are actually *deployed* `sources.json` gates the mount and
41
- * the link together and a hardcoded list advertises a 404 for a surface
42
- * whose first build has not landed yet. Same reason `Header` takes `items`.
38
+ * It is a prop because only the umbrella repo knows which *mounted* surfaces
39
+ * are actually deployed `sources.json` gates the mount and the link
40
+ * together and a hardcoded list advertises a 404 for a surface whose first
41
+ * build has not landed yet. Same reason `Header` takes `items`.
42
+ *
43
+ * The default covers the caller that cannot know: a docs site, or this
44
+ * package's own shell. /about is safe to include there unconditionally
45
+ * because it is a page of the landing's Astro build rather than a mount —
46
+ * there is no artifact to wait for, so it exists whenever the landing does.
43
47
  */
44
48
  site?: readonly SiteLink[]
45
49
  }
@@ -51,6 +55,7 @@ const year = new Date().getFullYear()
51
55
  const surfaces: readonly SiteLink[] = site ?? [
52
56
  { label: 'Blog', href: siteUrl('/blog') },
53
57
  { label: 'Updates', href: siteUrl('/updates') },
58
+ { label: 'About', href: siteUrl('/about') },
54
59
  ]
55
60
  ---
56
61
 
@@ -1,71 +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
- const SECTIONS = [
43
- { label: 'Blog', path: '/blog' },
44
- { label: 'Updates', path: '/updates' },
45
- ]
46
-
47
- /**
48
- * A URL inside this surface.
49
- *
50
- * Anything *leaving* it goes through `siteUrl`: a bare `/updates` would resolve
51
- * against the mount and point at `/blog/updates`.
52
- */
53
- const inside = (p = '') => `${base}/${p}`.replace(/\/{2,}/g, '/')
54
28
 
55
- /**
56
- * The menu, fully resolved for this surface, in the one fixed order every surface
57
- * shows it. The current section links inside its own base; every other link is
58
- * absolute so it does not resolve against this mount. `Header` owns how it looks;
59
- * this owns where it points.
60
- */
61
- const items: HeaderItem[] = [
62
- { label: 'Projects', href: siteUrl('/') },
63
- ...SECTIONS.map((section) =>
64
- section.path === base
65
- ? { label: section.label, href: inside(), current: true }
66
- : { label: section.label, href: siteUrl(section.path) },
67
- ),
68
- ]
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)
69
33
  ---
70
34
 
71
35
  <!doctype html>
@@ -73,8 +37,6 @@ const items: HeaderItem[] = [
73
37
  <head>
74
38
  <meta charset="utf-8" />
75
39
  <meta name="viewport" content="width=device-width, initial-scale=1" />
76
- <link rel="icon" type="image/png" href={siteUrl('/rxova-logo-256.png')} />
77
- <link rel="apple-touch-icon" href={siteUrl('/rxova-logo-256.png')} />
78
40
  <link rel="canonical" href={canonical} />
79
41
  <title>{title}</title>
80
42
  <meta name="description" content={description} />
@@ -84,35 +46,18 @@ const items: HeaderItem[] = [
84
46
  <meta property="og:url" content={canonical} />
85
47
  <meta property="og:image" content={siteUrl('/og/rxova.png')} />
86
48
  <meta name="twitter:card" content="summary_large_image" />
87
- <ThemeScript />
88
49
  </head>
89
50
  <body>
90
- <Header homeHref={siteUrl('/')} logoSrc={siteUrl('/rxova-logo-256.png')} items={items} />
91
-
92
51
  <main>
93
52
  <slot />
94
53
  </main>
95
54
 
96
- <div class="foot">
97
- <SiteFooter />
98
- </div>
99
-
100
55
  <style>
101
- /* The header is `Header` (its own component, shared with the landing); the
102
- only chrome styled here is the page body and the footer's measure. */
103
56
  main {
104
57
  max-width: var(--max);
105
58
  margin: 0 auto;
106
59
  padding: clamp(2rem, 6vh, 3.5rem) 1.5rem 4rem;
107
60
  }
108
-
109
- /* The footer's own rules are unscoped (footer.css, shared with the docs
110
- sites); this only puts it on the page's measure. */
111
- .foot {
112
- max-width: var(--max);
113
- margin: 0 auto;
114
- padding: 0 1.5rem;
115
- }
116
61
  </style>
117
62
 
118
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.