@janga/norna 0.7.1 → 0.7.3

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.
Files changed (168) hide show
  1. package/README.md +32 -18
  2. package/astro.config.mjs +2 -0
  3. package/bin/norna-cli.mjs +175 -0
  4. package/bin/norna.mjs +149 -150
  5. package/docs/README.md +41 -22
  6. package/docs/commands.md +25 -12
  7. package/docs/configuration.md +38 -135
  8. package/docs/content.md +108 -265
  9. package/docs/{command-organization.md → design/command-organization.md} +64 -60
  10. package/docs/{site-examples-structure-note.md → design/site-examples-structure.md} +19 -24
  11. package/docs/engine-development.md +35 -7
  12. package/docs/getting-started.md +55 -26
  13. package/docs/images-and-metadata.md +1 -1
  14. package/docs/local-development.md +14 -1
  15. package/docs/publishing.md +24 -1
  16. package/docs/routes.md +90 -0
  17. package/docs/site-structure.md +18 -10
  18. package/docs/theme.md +292 -0
  19. package/docs/typography.md +144 -0
  20. package/examples/dog-gallery/.astro/collections/site.schema.json +802 -0
  21. package/examples/dog-gallery/.astro/collections/theme.schema.json +714 -0
  22. package/examples/dog-gallery/.astro/content-assets.mjs +4 -0
  23. package/examples/dog-gallery/.astro/content-modules.mjs +1 -0
  24. package/examples/dog-gallery/.astro/content.d.ts +175 -0
  25. package/examples/dog-gallery/.astro/data-store.json +1 -0
  26. package/examples/dog-gallery/.astro/dev-local.json +8 -0
  27. package/examples/dog-gallery/.astro/dev.json +13 -0
  28. package/examples/dog-gallery/.astro/dev.log +49 -0
  29. package/examples/dog-gallery/.astro/settings.json +5 -0
  30. package/examples/dog-gallery/.astro/types.d.ts +2 -0
  31. package/examples/dog-gallery/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
  32. package/examples/dog-gallery/dist/_astro/SitePage.BFHZm5UG.css +1 -0
  33. package/examples/dog-gallery/dist/dog-care/index.html +15 -0
  34. package/examples/dog-gallery/dist/favicon.svg +7 -0
  35. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1080.webp +0 -0
  36. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1440.webp +0 -0
  37. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1536.webp +0 -0
  38. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-480.webp +0 -0
  39. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-768.webp +0 -0
  40. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-480.webp +0 -0
  41. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-768.webp +0 -0
  42. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-960.webp +0 -0
  43. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1080.webp +0 -0
  44. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1440.webp +0 -0
  45. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1920.webp +0 -0
  46. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-3939.webp +0 -0
  47. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-480.webp +0 -0
  48. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-768.webp +0 -0
  49. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1080.webp +0 -0
  50. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1440.webp +0 -0
  51. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1920.webp +0 -0
  52. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-2400.webp +0 -0
  53. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-480.webp +0 -0
  54. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-768.webp +0 -0
  55. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1080.webp +0 -0
  56. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1440.webp +0 -0
  57. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1600.webp +0 -0
  58. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-480.webp +0 -0
  59. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-768.webp +0 -0
  60. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1080.webp +0 -0
  61. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1440.webp +0 -0
  62. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1920.webp +0 -0
  63. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-2576.webp +0 -0
  64. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-480.webp +0 -0
  65. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-768.webp +0 -0
  66. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1080.webp +0 -0
  67. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1277.webp +0 -0
  68. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-480.webp +0 -0
  69. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-768.webp +0 -0
  70. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1080.webp +0 -0
  71. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1440.webp +0 -0
  72. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1536.webp +0 -0
  73. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-480.webp +0 -0
  74. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-768.webp +0 -0
  75. package/examples/dog-gallery/dist/index.html +43 -0
  76. package/examples/dog-gallery/dist/robots.txt +2 -0
  77. package/examples/dog-gallery/node_modules/.vite/deps/_metadata.json +74 -0
  78. package/examples/dog-gallery/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
  79. package/examples/dog-gallery/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
  80. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  81. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  82. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  83. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  84. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  85. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  86. package/examples/dog-gallery/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  87. package/examples/dog-gallery/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  88. package/examples/dog-gallery/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
  89. package/examples/dog-gallery/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
  90. package/examples/dog-gallery/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
  91. package/examples/dog-gallery/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
  92. package/examples/dog-gallery/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
  93. package/examples/dog-gallery/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
  94. package/examples/dog-gallery/node_modules/.vite/deps/package.json +3 -0
  95. package/examples/dog-gallery/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  96. package/examples/dog-gallery/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
  97. package/examples/dog-gallery/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
  98. package/examples/dog-gallery/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
  99. package/examples/dog-gallery/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
  100. package/examples/dog-gallery/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
  101. package/examples/dog-gallery/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
  102. package/examples/dog-gallery/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
  103. package/examples/dog-gallery/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
  104. package/examples/dog-gallery/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
  105. package/examples/dog-gallery/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
  106. package/examples/dog-gallery/site/.norna/generated-images.json +226 -0
  107. package/examples/dog-gallery/site/config.mjs +67 -0
  108. package/examples/dog-gallery/site/content.md +168 -0
  109. package/examples/dog-gallery/site/images/black-dogs/black-puppy-meadow.png +0 -0
  110. package/examples/dog-gallery/site/images/black-dogs/photo-of-a-black-dog.jpg +0 -0
  111. package/examples/dog-gallery/site/images/brown-dogs/brown-dog.jpg +0 -0
  112. package/examples/dog-gallery/site/images/brown-dogs/dog-accompanies-master.jpg +0 -0
  113. package/examples/dog-gallery/site/images/golden-dogs/golden-retriever.jpg +0 -0
  114. package/examples/dog-gallery/site/images/golden-dogs/toller-puppy.jpg +0 -0
  115. package/examples/dog-gallery/site/images/white-dogs/white-cute-dog.jpg +0 -0
  116. package/examples/dog-gallery/site/images/white-dogs/white-puppy-garden.png +0 -0
  117. package/examples/dog-gallery/site/public/favicon.svg +7 -0
  118. package/examples/dog-gallery/site/public/robots.txt +2 -0
  119. package/examples/dog-gallery/site/routes/dog-care/route-content.md +49 -0
  120. package/examples/dog-gallery/site/theme.md +114 -0
  121. package/fixtures/basic/site/config.mjs +3 -22
  122. package/fixtures/basic/site/content.md +1 -1
  123. package/fixtures/basic/site/theme.md +7 -3
  124. package/package.json +10 -9
  125. package/scripts/check-config.mjs +4 -3
  126. package/scripts/deploy-site.mjs +3 -3
  127. package/scripts/dev-local.mjs +64 -15
  128. package/scripts/init-site.mjs +11 -9
  129. package/scripts/lib/frontmatter-yaml.mjs +85 -0
  130. package/scripts/lib/presentation.mjs +10 -11
  131. package/scripts/lib/project-config.mjs +113 -45
  132. package/scripts/lib/site-content.mjs +21 -2
  133. package/scripts/lib/site-paths.mjs +18 -3
  134. package/scripts/lib/theme-config.mjs +34 -0
  135. package/scripts/lib/typography.mjs +103 -23
  136. package/scripts/show-typography.mjs +202 -75
  137. package/scripts/sync-content-sections.mjs +6 -6
  138. package/scripts/test-cli-discovery.mjs +124 -0
  139. package/scripts/test-content-check.mjs +3 -4
  140. package/scripts/test-engine-commands.mjs +20 -6
  141. package/scripts/test-navigation.mjs +10 -6
  142. package/scripts/test-package-check.mjs +40 -11
  143. package/scripts/test-temporary-visibility.mjs +2 -3
  144. package/src/components/SiteNavigation.astro +7 -5
  145. package/src/components/SitePage.astro +5 -3
  146. package/src/components/SiteSection.astro +63 -29
  147. package/src/content.config.ts +64 -2
  148. package/src/layouts/BaseLayout.astro +19 -1
  149. package/src/lib/basePath.ts +21 -0
  150. package/src/lib/generatedImages.ts +8 -4
  151. package/src/lib/sectionContent.ts +6 -1
  152. package/src/lib/sitePublicAssets.ts +8 -1
  153. package/src/styles/global.css +59 -55
  154. package/starters/basic/.github/workflows/deploy.yml +3 -3
  155. package/starters/basic/README.md +30 -14
  156. package/starters/basic/package.json +3 -3
  157. package/starters/basic/site/config.mjs +4 -24
  158. package/starters/basic/site/content.md +44 -9
  159. package/starters/basic/site/theme.md +113 -16
  160. package/starters/project/.github/workflows/deploy.yml +65 -0
  161. package/starters/project/README.md +72 -0
  162. package/starters/project/package-lock.json +4296 -0
  163. package/starters/project/package.json +35 -0
  164. package/starters/project/site/config.mjs +43 -0
  165. package/starters/project/site/content.md +88 -0
  166. package/starters/project/site/public/robots.txt +2 -0
  167. package/starters/project/site/routes/guide/route-content.md +58 -0
  168. package/starters/project/site/theme.md +15 -0
@@ -25,6 +25,12 @@ const cssLength = z.string().regex(
25
25
  /^(?:0|(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em|ch|lh))$/,
26
26
  'Use a CSS length such as "0", "0.8em", "1rem", or "12px".',
27
27
  );
28
+ const visualCssLengthUnit = String.raw`(?:px|rem|em|vw|vh|vmin|vmax|ch|%)`;
29
+ const visualCssLengthValue = String.raw`(?:\d+(?:\.\d+)?|\.\d+)${visualCssLengthUnit}`;
30
+ const visualCssLength = z.string().regex(
31
+ new RegExp(`^(?:0|${visualCssLengthValue}|clamp\\(\\s*${visualCssLengthValue}\\s*,\\s*${visualCssLengthValue}\\s*,\\s*${visualCssLengthValue}\\s*\\))$`),
32
+ 'Use a CSS length such as "0", "900px", "56rem", "90%", or "clamp(1rem, 4vw, 3rem)".',
33
+ );
28
34
  const inlineStyleName = z.string().regex(/^[a-z][a-z0-9-]*$/);
29
35
  const dateOnly = z.string()
30
36
  .regex(/^\d{4}-\d{2}-\d{2}$/, 'Use YYYY-MM-DD format.')
@@ -55,6 +61,12 @@ const headingPresentationOverride = z.object({
55
61
  ...commonTextPresentationOverride,
56
62
  spacing: cssLength.optional(),
57
63
  }).strict();
64
+ const headingLevelsPresentationOverride = z.object({
65
+ h1: headingPresentationOverride.optional(),
66
+ h2: headingPresentationOverride.optional(),
67
+ h3: headingPresentationOverride.optional(),
68
+ h4: headingPresentationOverride.optional(),
69
+ }).strict();
58
70
  const bodyPresentationOverride = z.object({
59
71
  ...commonTextPresentationOverride,
60
72
  paragraphSpacing: cssLength.optional(),
@@ -64,7 +76,7 @@ const captionPresentationOverride = z.object({
64
76
  spacing: cssLength.optional(),
65
77
  }).strict();
66
78
  const typographyOverrides = z.object({
67
- heading: headingPresentationOverride.optional(),
79
+ headings: headingLevelsPresentationOverride.optional(),
68
80
  body: bodyPresentationOverride.optional(),
69
81
  caption: captionPresentationOverride.optional(),
70
82
  }).strict();
@@ -75,6 +87,50 @@ const typography = z.object({
75
87
  (value) => value.preset !== undefined || value.overrides !== undefined,
76
88
  'Specify preset, overrides, or both.',
77
89
  );
90
+ const themeTypography = z.object({
91
+ fontFamily: z.string()
92
+ .min(1)
93
+ .refine((value) => !/[\n\r;{}]/.test(value), 'Do not use semicolons, braces, or line breaks.').optional(),
94
+ preset: typographyPreset.optional(),
95
+ overrides: typographyOverrides.optional(),
96
+ }).strict().refine(
97
+ (value) => value.fontFamily !== undefined || value.preset !== undefined || value.overrides !== undefined,
98
+ 'Specify fontFamily, preset, overrides, or both.',
99
+ );
100
+ const responsiveCssLength = z.union([
101
+ visualCssLength,
102
+ z.object({
103
+ desktop: visualCssLength,
104
+ mobile: visualCssLength,
105
+ }).strict(),
106
+ ]);
107
+ const responsivePercent = z.union([
108
+ z.number().positive().max(100),
109
+ z.object({
110
+ desktop: z.number().positive().max(100),
111
+ mobile: z.number().positive().max(100),
112
+ }).strict(),
113
+ ]);
114
+ const themeLayoutSpacing = z.object({
115
+ bodyToImages: responsiveCssLength.optional(),
116
+ finalSectionBottom: responsiveCssLength.optional(),
117
+ firstSectionTop: responsiveCssLength.optional(),
118
+ imageGap: responsiveCssLength.optional(),
119
+ sectionGap: responsiveCssLength.optional(),
120
+ sectionHeadingToBody: responsiveCssLength.optional(),
121
+ subheadingRuleTop: responsiveCssLength.optional(),
122
+ subheadingTop: responsiveCssLength.optional(),
123
+ }).strict();
124
+ const themeLayout = z.object({
125
+ pageWidth: visualCssLength.optional(),
126
+ gutter: responsiveCssLength.optional(),
127
+ spacing: themeLayoutSpacing.optional(),
128
+ }).strict();
129
+ const themeGallery = z.object({
130
+ width: visualCssLength.optional(),
131
+ maxAvailableWidthPercent: responsivePercent.optional(),
132
+ maxAvailableHeightPercent: responsivePercent.optional(),
133
+ }).strict();
78
134
  const sectionPresentationOverride = z.object({
79
135
  backgroundColor: colorValue.optional(),
80
136
  textColor: colorValue.optional(),
@@ -86,7 +142,6 @@ const themePresentation = z.object({
86
142
  inlineStyles: z.record(inlineStyleName, z.object({
87
143
  color: colorValue,
88
144
  }).strict()).optional(),
89
- typography: typography.optional(),
90
145
  }).strict();
91
146
  const pagePresentation = z.object({
92
147
  backgroundColor: colorValue.optional(),
@@ -108,6 +163,9 @@ const pageNavigation = z.object({
108
163
  label: z.string().optional(),
109
164
  order: z.number().int().optional(),
110
165
  }).strict();
166
+ const themeNavigation = z.object({
167
+ brand: z.string().min(1).optional(),
168
+ }).strict();
111
169
 
112
170
  const galleryImage = z.object({
113
171
  image: contentImageName,
@@ -137,6 +195,10 @@ const siteSchema = z.object({
137
195
  });
138
196
 
139
197
  const themeSchema = z.object({
198
+ navigation: themeNavigation.optional(),
199
+ layout: themeLayout.optional(),
200
+ gallery: themeGallery.optional(),
201
+ typography: themeTypography.optional(),
140
202
  presentation: themePresentation.optional(),
141
203
  frame: frame.optional(),
142
204
  }).strict();
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import projectConfig from '../../scripts/lib/project-config.mjs';
3
3
  import { getIconLinks } from '../lib/sitePublicAssets';
4
+ import { withBasePath } from '../lib/basePath';
4
5
  import '../styles/global.css';
5
6
 
6
7
  type FrameColors = {
@@ -23,17 +24,34 @@ const {
23
24
  } = Astro.props;
24
25
 
25
26
  const documentTitle = title;
26
- const canonicalUrl = new URL(pathname, projectConfig.site.url).href;
27
+ const canonicalUrl = new URL(withBasePath(projectConfig.site.basePath, pathname), projectConfig.site.url).href;
27
28
  const iconLinks = getIconLinks();
28
29
  const scrollBehavior = projectConfig.navigation.smoothScroll.enabled ? 'smooth' : 'auto';
29
30
  const formatPercent = (value: number) => `${value}%`;
30
31
  const formatViewportHeightPercent = (value: number) => `${value}svh`;
32
+ const layoutSpacing = projectConfig.layout.spacing;
31
33
  const documentStyle = [
32
34
  `scroll-behavior: ${scrollBehavior}`,
33
35
  `--font-sans: ${projectConfig.typography.fontFamily}`,
34
36
  `--page-width: ${projectConfig.layout.pageWidth}`,
35
37
  `--content-gutter-desktop: ${projectConfig.layout.gutter.desktop}`,
36
38
  `--content-gutter-mobile: ${projectConfig.layout.gutter.mobile}`,
39
+ `--space-nav-to-first-section-desktop: ${layoutSpacing.firstSectionTop.desktop}`,
40
+ `--space-nav-to-first-section-mobile: ${layoutSpacing.firstSectionTop.mobile}`,
41
+ `--space-section-title-to-body-desktop: ${layoutSpacing.sectionHeadingToBody.desktop}`,
42
+ `--space-section-title-to-body-mobile: ${layoutSpacing.sectionHeadingToBody.mobile}`,
43
+ `--space-section-body-to-gallery-desktop: ${layoutSpacing.bodyToImages.desktop}`,
44
+ `--space-section-body-to-gallery-mobile: ${layoutSpacing.bodyToImages.mobile}`,
45
+ `--space-gallery-item-desktop: ${layoutSpacing.imageGap.desktop}`,
46
+ `--space-gallery-item-mobile: ${layoutSpacing.imageGap.mobile}`,
47
+ `--space-section-to-section-desktop: ${layoutSpacing.sectionGap.desktop}`,
48
+ `--space-section-to-section-mobile: ${layoutSpacing.sectionGap.mobile}`,
49
+ `--space-final-section-bottom-desktop: ${layoutSpacing.finalSectionBottom.desktop}`,
50
+ `--space-final-section-bottom-mobile: ${layoutSpacing.finalSectionBottom.mobile}`,
51
+ `--space-subheading-top-desktop: ${layoutSpacing.subheadingTop.desktop}`,
52
+ `--space-subheading-top-mobile: ${layoutSpacing.subheadingTop.mobile}`,
53
+ `--space-subheading-rule-top-desktop: ${layoutSpacing.subheadingRuleTop.desktop}`,
54
+ `--space-subheading-rule-top-mobile: ${layoutSpacing.subheadingRuleTop.mobile}`,
37
55
  `--gallery-width: ${projectConfig.gallery.width}`,
38
56
  `--gallery-max-available-width-desktop: ${formatPercent(projectConfig.gallery.maxAvailableWidthPercent.desktop)}`,
39
57
  `--gallery-max-available-width-mobile: ${formatPercent(projectConfig.gallery.maxAvailableWidthPercent.mobile)}`,
@@ -0,0 +1,21 @@
1
+ export const withBasePath = (basePath: string, value: string) => {
2
+ if (
3
+ basePath === '/'
4
+ || value.startsWith('#')
5
+ || value.startsWith('//')
6
+ || /^[a-z][a-z0-9+.-]*:/i.test(value)
7
+ ) {
8
+ return value;
9
+ }
10
+
11
+ if (!value.startsWith('/')) {
12
+ return value;
13
+ }
14
+
15
+ return `${basePath.replace(/\/$/, '')}${value}`;
16
+ };
17
+
18
+ export const applyBasePathToHtml = (basePath: string, html: string) =>
19
+ html.replace(/\b(href|src)=(["'])\/(?!\/)([^"']*)\2/g, (_match, attribute: string, quote: string, path: string) => (
20
+ `${attribute}=${quote}${withBasePath(basePath, `/${path}`)}${quote}`
21
+ ));
@@ -1,5 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import projectConfig from '../../scripts/lib/project-config.mjs';
2
3
  import { generatedImagesManifestPath } from '../../scripts/lib/site-paths.mjs';
4
+ import { withBasePath } from './basePath';
3
5
 
4
6
  type GeneratedImage = {
5
7
  outputVersion?: number;
@@ -26,7 +28,9 @@ const fallbackDisplayImageWidth = 1440;
26
28
 
27
29
  export const getGeneratedImage = (src: string) => images[src];
28
30
 
29
- export const getLinkedImageSrc = (src: string) => getGeneratedImage(src)?.variants.at(-1)?.src ?? src;
31
+ const displaySrc = (src: string) => withBasePath(projectConfig.site.basePath, src);
32
+
33
+ export const getLinkedImageSrc = (src: string) => displaySrc(getGeneratedImage(src)?.variants.at(-1)?.src ?? src);
30
34
 
31
35
  const getDisplayVariants = (variants: GeneratedImage['variants']) => {
32
36
  const sortedVariants = [...variants].sort((a, b) => a.width - b.width);
@@ -44,7 +48,7 @@ export const getImageAttributes = (src: string, sizes: string) => {
44
48
 
45
49
  if (!image) {
46
50
  return {
47
- src,
51
+ src: displaySrc(src),
48
52
  sizes,
49
53
  };
50
54
  }
@@ -53,8 +57,8 @@ export const getImageAttributes = (src: string, sizes: string) => {
53
57
  const fallbackVariant = getFallbackVariant(displayVariants);
54
58
 
55
59
  return {
56
- src: fallbackVariant?.src ?? src,
57
- srcset: displayVariants.map((variant) => `${variant.src} ${variant.width}w`).join(', '),
60
+ src: displaySrc(fallbackVariant?.src ?? src),
61
+ srcset: displayVariants.map((variant) => `${displaySrc(variant.src)} ${variant.width}w`).join(', '),
58
62
  sizes,
59
63
  style: `aspect-ratio: ${image.width} / ${image.height};`,
60
64
  width: image.width,
@@ -1,4 +1,6 @@
1
1
  import type { CollectionEntry } from 'astro:content';
2
+ import projectConfig from '../../scripts/lib/project-config.mjs';
3
+ import { applyBasePathToHtml } from './basePath';
2
4
 
3
5
  type SiteSection = CollectionEntry<'site'>['data']['sections'][number];
4
6
  type ThemePresentation = CollectionEntry<'theme'>['data']['presentation'];
@@ -56,6 +58,9 @@ const applyInlineStyles = (html: string, inlineStyles: InlineStyles | undefined)
56
58
  return `<span class="inline-style inline-style-${styleName}" style="--inline-style-color: ${style.color}">${text}</span>`;
57
59
  });
58
60
 
61
+ const prepareContentHtml = (html: string, inlineStyles: InlineStyles | undefined) =>
62
+ applyBasePathToHtml(projectConfig.site.basePath, applyInlineStyles(html, inlineStyles));
63
+
59
64
  export const getSectionsContent = (
60
65
  html: string,
61
66
  sections: SiteSection[],
@@ -88,7 +93,7 @@ export const getSectionsContent = (
88
93
 
89
94
  contentById.set(id, {
90
95
  title,
91
- contentHtml: applyInlineStyles(content, inlineStyles),
96
+ contentHtml: prepareContentHtml(content, inlineStyles),
92
97
  });
93
98
  markdownSectionIds.push(id);
94
99
  }
@@ -1,6 +1,8 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { sitePublicDir } from '../../scripts/lib/site-paths.mjs';
4
+ import projectConfig from '../../scripts/lib/project-config.mjs';
5
+ import { withBasePath } from './basePath';
4
6
 
5
7
  type IconLink = {
6
8
  rel: 'icon' | 'apple-touch-icon';
@@ -36,4 +38,9 @@ const publicAssetExists = (href: string) => {
36
38
  return existsSync(path.join(sitePublicDir, relativePath));
37
39
  };
38
40
 
39
- export const getIconLinks = () => faviconCandidates.filter((candidate) => publicAssetExists(candidate.href));
41
+ export const getIconLinks = () => faviconCandidates
42
+ .filter((candidate) => publicAssetExists(candidate.href))
43
+ .map((candidate) => ({
44
+ ...candidate,
45
+ href: withBasePath(projectConfig.site.basePath, candidate.href),
46
+ }));
@@ -28,20 +28,30 @@
28
28
  --gallery-max-image-height: var(--gallery-max-image-height-desktop);
29
29
  --nav-top-safety-inset: 0px;
30
30
 
31
- /* Avstand fran sticky navigationsrad till forsta sektionsrubriken. */
32
- --space-nav-to-first-section: clamp(1.875rem, 3vw, 2.75rem);
33
-
34
- /* Avstand fran sektionsrubrik till sektionens forsta textstycke. */
35
- --space-section-title-to-body: clamp(0.5625rem, 1.25vw, 0.75rem);
36
-
37
- /* Avstand fran sektionens lopande text till efterfoljande galleri. */
38
- --space-section-body-to-gallery: clamp(1.25rem, 2.5vw, 2rem);
39
-
40
- /* Avstand mellan enskilda bilder/verk inom samma galleri. */
41
- --space-gallery-item: clamp(1.5rem, 3.5vw, 2.75rem);
42
-
43
- /* Avstand fran sista galleriobjektet eller textblocket till nasta sektion. */
44
- --space-section-to-section: clamp(2.55rem, 4.2vw, 3.9rem);
31
+ --space-nav-to-first-section-desktop: clamp(1.875rem, 3vw, 2.75rem);
32
+ --space-nav-to-first-section-mobile: 1.375rem;
33
+ --space-nav-to-first-section: var(--space-nav-to-first-section-desktop);
34
+ --space-section-title-to-body-desktop: clamp(0.5625rem, 1.25vw, 0.75rem);
35
+ --space-section-title-to-body-mobile: 0.4375rem;
36
+ --space-section-title-to-body: var(--space-section-title-to-body-desktop);
37
+ --space-section-body-to-gallery-desktop: clamp(1.25rem, 2.5vw, 2rem);
38
+ --space-section-body-to-gallery-mobile: 1.25rem;
39
+ --space-section-body-to-gallery: var(--space-section-body-to-gallery-desktop);
40
+ --space-gallery-item-desktop: clamp(1.5rem, 3.5vw, 2.75rem);
41
+ --space-gallery-item-mobile: 2.75rem;
42
+ --space-gallery-item: var(--space-gallery-item-desktop);
43
+ --space-section-to-section-desktop: clamp(2.55rem, 4.2vw, 3.9rem);
44
+ --space-section-to-section-mobile: 2.25rem;
45
+ --space-section-to-section: var(--space-section-to-section-desktop);
46
+ --space-final-section-bottom-desktop: var(--space-section-to-section-desktop);
47
+ --space-final-section-bottom-mobile: var(--space-section-to-section-mobile);
48
+ --space-final-section-bottom: var(--space-final-section-bottom-desktop);
49
+ --space-subheading-top-desktop: 2rem;
50
+ --space-subheading-top-mobile: 1.6rem;
51
+ --space-subheading-top: var(--space-subheading-top-desktop);
52
+ --space-subheading-rule-top-desktop: 1rem;
53
+ --space-subheading-rule-top-mobile: 0.8rem;
54
+ --space-subheading-rule-top: var(--space-subheading-rule-top-desktop);
45
55
 
46
56
  --content-gutter-desktop: clamp(1.25rem, 4vw, 3rem);
47
57
  --content-gutter-mobile: 1rem;
@@ -316,7 +326,7 @@ a {
316
326
  }
317
327
 
318
328
  .site-section:last-child {
319
- padding-bottom: var(--space-section-to-section);
329
+ padding-bottom: var(--space-final-section-bottom);
320
330
  }
321
331
 
322
332
  .section-card {
@@ -331,18 +341,18 @@ a {
331
341
  text-align: var(--section-heading-align-desktop, center);
332
342
  }
333
343
 
334
- h1,
335
- h2,
344
+ .section-header h1,
345
+ .section-header h2,
336
346
  p,
337
347
  ul {
338
348
  margin-top: 0;
339
349
  }
340
350
 
341
- h1,
342
- h2 {
351
+ .section-header h1,
352
+ .section-header h2 {
343
353
  margin-bottom: 0;
344
354
  color: inherit;
345
- font-size: var(--section-heading-font-size-desktop, clamp(1.4rem, 3.1vw, 3.2rem));
355
+ font-size: var(--section-heading-font-size-desktop, clamp(1.35rem, 1.1rem + 0.9vw, 2rem));
346
356
  font-weight: 400;
347
357
  letter-spacing: 0;
348
358
  line-height: var(--section-heading-line-height, 1.04);
@@ -350,23 +360,18 @@ h2 {
350
360
  overflow-wrap: anywhere;
351
361
  }
352
362
 
353
- .site-section-primary h1 {
354
- font-size: var(--section-heading-font-size-desktop, clamp(1.65rem, 4.6vw, 5.65rem));
355
- font-weight: 700;
356
- }
357
-
358
363
  .section-body {
359
364
  width: 100%;
360
365
  margin: 0 auto;
361
366
  color: inherit;
362
367
  font-family: var(--font-sans);
363
- font-size: clamp(1rem, 0.96rem + 0.18vw, 1.125rem);
368
+ font-size: clamp(0.96rem, 0.94rem + 0.08vw, 1rem);
364
369
  }
365
370
 
366
371
  .section-markdown {
367
372
  width: min(100%, var(--section-body-width-desktop, var(--text-width)));
368
373
  margin: 0 auto var(--space-section-body-to-gallery);
369
- font-size: var(--section-body-font-size-desktop, clamp(1rem, 0.96rem + 0.18vw, 1.125rem));
374
+ font-size: var(--section-body-font-size-desktop, clamp(0.96rem, 0.94rem + 0.08vw, 1rem));
370
375
  line-height: var(--section-body-line-height, 1.78);
371
376
  text-align: var(--section-body-align-desktop, center);
372
377
  }
@@ -389,23 +394,25 @@ h2 {
389
394
  }
390
395
 
391
396
  .section-markdown h3 {
392
- margin: 2.75rem 0 0.9rem;
397
+ margin: var(--space-subheading-top) 0 var(--section-markdown-h3-spacing, 0.9rem);
393
398
  border-top: 1px solid var(--color-soft);
394
- padding-top: 1.75rem;
399
+ padding-top: var(--space-subheading-rule-top);
395
400
  font-family: var(--font-sans);
396
- font-size: 0.82rem;
397
- letter-spacing: 0.14em;
398
- line-height: 1.35;
401
+ font-size: var(--section-markdown-h3-font-size-desktop, 0.82rem);
402
+ letter-spacing: 0;
403
+ line-height: var(--section-markdown-h3-line-height, 1.35);
404
+ text-align: var(--section-markdown-h3-align-desktop, left);
399
405
  text-transform: uppercase;
400
406
  }
401
407
 
402
408
  .section-markdown h4 {
403
- margin: 0 0 0.85rem;
409
+ margin: 0 0 var(--section-markdown-h4-spacing, 0.85rem);
404
410
  color: inherit;
405
- font-size: clamp(0.95rem, 0.9rem + 0.2vw, 1.08rem);
411
+ font-size: var(--section-markdown-h4-font-size-desktop, clamp(0.96rem, 0.94rem + 0.08vw, 1rem));
406
412
  font-weight: 400;
407
- letter-spacing: 0.02em;
408
- line-height: 1.5;
413
+ letter-spacing: 0;
414
+ line-height: var(--section-markdown-h4-line-height, 1.5);
415
+ text-align: var(--section-markdown-h4-align-desktop, left);
409
416
  }
410
417
 
411
418
  .section-markdown ul,
@@ -639,11 +646,14 @@ h2 {
639
646
 
640
647
  @media (max-width: 700px) {
641
648
  :root {
642
- --space-nav-to-first-section: 1.375rem;
643
- --space-section-title-to-body: 0.4375rem;
644
- --space-section-body-to-gallery: 1.25rem;
645
- --space-gallery-item: 2.75rem;
646
- --space-section-to-section: 2.25rem;
649
+ --space-nav-to-first-section: var(--space-nav-to-first-section-mobile);
650
+ --space-section-title-to-body: var(--space-section-title-to-body-mobile);
651
+ --space-section-body-to-gallery: var(--space-section-body-to-gallery-mobile);
652
+ --space-gallery-item: var(--space-gallery-item-mobile);
653
+ --space-section-to-section: var(--space-section-to-section-mobile);
654
+ --space-final-section-bottom: var(--space-final-section-bottom-mobile);
655
+ --space-subheading-top: var(--space-subheading-top-mobile);
656
+ --space-subheading-rule-top: var(--space-subheading-rule-top-mobile);
647
657
  --content-gutter: var(--content-gutter-mobile);
648
658
  --gallery-max-available-width: var(--gallery-max-available-width-mobile);
649
659
  --gallery-max-image-height: var(--gallery-max-image-height-mobile);
@@ -780,18 +790,11 @@ h2 {
780
790
  line-height: 1.72;
781
791
  }
782
792
 
783
- h1,
784
- h2 {
785
- font-size: var(
786
- --section-heading-font-size-mobile,
787
- var(--section-heading-font-size-desktop, clamp(1.4rem, 3.1vw, 3.2rem))
788
- );
789
- }
790
-
791
- .site-section-primary h1 {
793
+ .section-header h1,
794
+ .section-header h2 {
792
795
  font-size: var(
793
796
  --section-heading-font-size-mobile,
794
- var(--section-heading-font-size-desktop, clamp(1.9rem, 8vw, 3.2rem))
797
+ var(--section-heading-font-size-desktop, clamp(1.3rem, 1.05rem + 1.35vw, 1.75rem))
795
798
  );
796
799
  }
797
800
 
@@ -804,7 +807,7 @@ h2 {
804
807
  width: min(100%, var(--section-body-width-mobile, var(--section-body-width-desktop, var(--text-width))));
805
808
  font-size: var(
806
809
  --section-body-font-size-mobile,
807
- var(--section-body-font-size-desktop, clamp(1rem, 0.96rem + 0.18vw, 1.125rem))
810
+ var(--section-body-font-size-desktop, clamp(0.96rem, 0.94rem + 0.08vw, 1rem))
808
811
  );
809
812
  text-align: var(--section-body-align-mobile, var(--section-body-align-desktop, left));
810
813
  }
@@ -819,12 +822,13 @@ h2 {
819
822
  }
820
823
 
821
824
  .section-markdown h3 {
822
- margin-top: 2.25rem;
823
- padding-top: 1.35rem;
825
+ font-size: var(--section-markdown-h3-font-size-mobile, var(--section-markdown-h3-font-size-desktop, 0.82rem));
826
+ text-align: var(--section-markdown-h3-align-mobile, var(--section-markdown-h3-align-desktop, left));
824
827
  }
825
828
 
826
829
  .section-markdown h4 {
827
- text-align: center;
830
+ font-size: var(--section-markdown-h4-font-size-mobile, var(--section-markdown-h4-font-size-desktop, clamp(0.96rem, 0.94rem + 0.08vw, 1rem)));
831
+ text-align: var(--section-markdown-h4-align-mobile, var(--section-markdown-h4-align-desktop, left));
828
832
  }
829
833
 
830
834
  .section-markdown ul,
@@ -34,10 +34,10 @@ jobs:
34
34
  - name: Restore generated images
35
35
  uses: actions/cache@v4
36
36
  with:
37
- path: site/.cli-gallery/public/images/generated
38
- key: cli-gallery-images-${{ hashFiles('site/.cli-gallery/generated-images.json') }}
37
+ path: site/.norna/public/images/generated
38
+ key: norna-images-${{ hashFiles('site/.norna/generated-images.json') }}
39
39
  restore-keys: |
40
- cli-gallery-images-
40
+ norna-images-
41
41
 
42
42
  - name: Install dependencies
43
43
  run: npm ci
@@ -6,22 +6,35 @@ This is a minimal site repository starter for `@janga/norna`.
6
6
 
7
7
  ```sh
8
8
  npm install
9
- npm run norna:dev
9
+ npm run dev
10
10
  ```
11
11
 
12
- Edit site-wide theme defaults in `site/theme.md`, homepage content and section
13
- overrides in `site/content.md`, technical settings such as URL, layout, font,
14
- and locale labels in `site/config.mjs`, source images under
12
+ If the standard local port is blocked, start with:
13
+
14
+ ```sh
15
+ npm run dev -- --kill
16
+ ```
17
+
18
+ Edit site-wide visual defaults in `site/theme.md`, homepage content and section
19
+ overrides in `site/content.md`, technical settings such as URL and locale
20
+ labels in `site/config.mjs`, source images under
15
21
  `site/images/<section-id>/`, and static public files under `site/public/`.
16
22
 
17
- The page width is configured with `layout.pageWidth`; gallery width is
18
- configured with `gallery.width`. Side margins are configured with
19
- `layout.gutter`; image height is limited with
20
- `gallery.maxAvailableHeightPercent`.
23
+ Use `site.basePath: '/'` for a custom domain or root-hosted site. For a GitHub
24
+ Pages project site without a custom domain, set both values:
25
+
26
+ ```js
27
+ site: {
28
+ url: 'https://owner.github.io/repository-name/',
29
+ basePath: '/repository-name/',
30
+ }
31
+ ```
32
+
33
+ In GitHub repository settings, configure Pages to build from GitHub Actions.
21
34
 
22
- The site font is configured with `typography.fontFamily` in `site/config.mjs`.
23
- Site-wide text presentation is configured with typography presets and overrides
24
- in `site/theme.md`; page and section overrides live in `site/content.md`.
35
+ Page width, side gutters, vertical spacing, image width, image height limits,
36
+ font, colors, and site-wide typography are configured in `site/theme.md`. Page
37
+ and section presentation overrides live in `site/content.md`.
25
38
 
26
39
  Commit `package-lock.json` after the first install so GitHub Actions can use
27
40
  `npm ci`.
@@ -36,9 +49,9 @@ npm run norna:typography:show
36
49
  npm run norna:build
37
50
  ```
38
51
 
39
- This keeps gallery commands separate from repository-specific build or
40
- publishing commands in projects that embed a gallery inside a larger GitHub
41
- project. This pure starter also keeps `npm run build` as an alias for
52
+ This keeps Norna commands separate from repository-specific build or publishing
53
+ commands in projects that embed a Norna site inside a larger GitHub project.
54
+ This pure starter also keeps `npm run build` as an alias for
42
55
  `npm run norna:build`.
43
56
 
44
57
  Use `npm run norna:engine:version` to inspect the installed engine and
@@ -49,6 +62,9 @@ Generic documentation lives in the `norna` repository:
49
62
  - `docs/getting-started.md`
50
63
  - `docs/site-structure.md`
51
64
  - `docs/content.md`
65
+ - `docs/theme.md`
66
+ - `docs/typography.md`
67
+ - `docs/routes.md`
52
68
  - `docs/configuration.md`
53
69
  - `docs/commands.md`
54
70
  - `docs/images-and-metadata.md`
@@ -3,7 +3,7 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "scripts": {
6
- "dev": "npm run norna:dev",
6
+ "dev": "npm run norna:dev --",
7
7
  "norna:dev": "norna dev:local",
8
8
  "norna:dev:lan": "norna dev:lan",
9
9
  "norna:dev:restart": "norna dev:restart",
@@ -14,8 +14,8 @@
14
14
  "norna:config:check": "norna config:check",
15
15
  "norna:content:check": "norna content:check",
16
16
  "norna:sync": "norna content:sync",
17
- "norna:typography:presets": "norna typography:presets",
18
- "norna:typography:show": "norna typography:show",
17
+ "norna:typography:presets": "norna typography presets",
18
+ "norna:typography:show": "norna typography show",
19
19
  "norna:public": "norna site:public",
20
20
  "norna:images": "norna images",
21
21
  "norna:build": "norna build",
@@ -1,27 +1,7 @@
1
1
  export default {
2
2
  site: {
3
3
  url: 'https://example.com/',
4
- },
5
- layout: {
6
- pageWidth: '1180px',
7
- gutter: {
8
- desktop: 'clamp(1.25rem, 4vw, 3rem)',
9
- mobile: '1rem',
10
- },
11
- },
12
- gallery: {
13
- width: '900px',
14
- maxAvailableWidthPercent: {
15
- desktop: 100,
16
- mobile: 100,
17
- },
18
- maxAvailableHeightPercent: {
19
- desktop: 74,
20
- mobile: 68,
21
- },
22
- },
23
- typography: {
24
- fontFamily: "Arial, 'Helvetica Neue', Helvetica, sans-serif",
4
+ basePath: '/',
25
5
  },
26
6
  navigation: {
27
7
  smoothScroll: {
@@ -36,11 +16,11 @@ export default {
36
16
  labels: {
37
17
  skipToContent: 'Skip to content',
38
18
  sectionNavigation: 'Sections',
39
- gallery: 'Gallery',
19
+ gallery: 'Images',
40
20
  },
41
21
  },
42
22
  footer: {
43
- copyrightMessage: '(c) Example Artist.',
23
+ copyrightMessage: '(c) Example Owner.',
44
24
  buildInfo: {
45
25
  enabled: true,
46
26
  text: 'Built',
@@ -53,7 +33,7 @@ export default {
53
33
  },
54
34
  },
55
35
  github: {
56
- repo: 'owner/example-gallery',
36
+ repo: 'owner/example-site',
57
37
  branch: 'main',
58
38
  pagesWorkflow: 'Deploy to GitHub Pages',
59
39
  },