diff --git a/.gitignore b/.gitignore
index 9ffc58c4..bc2bcdc7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ dist
*.tgz
tsconfig.bundle.generated.json
PureGate
+purestack-setup
*.vsix
.codex/*
/.tmp
diff --git a/frontend/purestack.studio/guides/_nav.json b/frontend/purestack.studio/guides/_nav.json
index 90bedd33..b943bb6f 100644
--- a/frontend/purestack.studio/guides/_nav.json
+++ b/frontend/purestack.studio/guides/_nav.json
@@ -5,6 +5,7 @@
"components",
"purestack-cli.mdx",
"site-config.mdx",
+ "links.mdx",
"vscode-extension.mdx",
"regor.mdx",
"typography.mdx",
diff --git a/frontend/purestack.studio/guides/index.mdx b/frontend/purestack.studio/guides/index.mdx
index 7d6a0728..4feb75ab 100644
--- a/frontend/purestack.studio/guides/index.mdx
+++ b/frontend/purestack.studio/guides/index.mdx
@@ -1,6 +1,6 @@
---
title: Guides
-description: Learn how to build and style a PureStack site with the CLI, site configuration, VS Code extension, Regor, typography, utility classes, semantic tones, icons, and themes.
+description: Learn how to build and style a PureStack site with the CLI, site configuration, links, VS Code extension, Regor, typography, utility classes, semantic tones, icons, and themes.
template: doc
layout:
showNav: true
@@ -26,6 +26,11 @@ These guides follow the path from a content folder to a finished interface. Star
Set output paths, navigation, themes, search, metadata, languages, and optional scripts.
Configure a site
+
+
Links
+
Link pages and sections relative to your file, and let every build check them.
+ Write links
+
VS Code extension
Find component source, complete props and frontmatter, and format markup on save.
diff --git a/frontend/purestack.studio/guides/links.mdx b/frontend/purestack.studio/guides/links.mdx
new file mode 100644
index 00000000..97619c58
--- /dev/null
+++ b/frontend/purestack.studio/guides/links.mdx
@@ -0,0 +1,124 @@
+---
+title: Links
+description: Link to pages, sections, and other sites. Write links relative to your file, and every build checks that they still work.
+template: doc
+layout:
+ showNav: true
+ showToc: true
+ showFooter: true
+nav:
+ order: 16
+ icon: tabler:link
+---
+
+# Links
+
+Write a link the way you see your files. PureStack turns it into the right page address and checks it every time the site builds, so a renamed or deleted page never leaves a broken link behind.
+
+## Link to another page
+
+Start from the file you are writing in, just like in your editor's file tree. Take this content folder:
+
+```text
+index.mdx
+guides/
+ index.mdx
+ semantic-tones.mdx
+ themes.mdx
+ components/
+ index.mdx
+ buttons.mdx
+```
+
+From `guides/semantic-tones.mdx`, these links work:
+
+| Write | Opens |
+| --- | --- |
+| `[Themes](./themes)` | `/guides/themes/`, a page in the same folder |
+| `[Buttons](./components/buttons)` | `/guides/components/buttons/`, a page in a subfolder |
+| `[Components](./components/)` | `/guides/components/`, the main page of a subfolder |
+| `[All guides](./)` | `/guides/`, the main page of this folder |
+| `[Home](../)` | `/`, the main page one folder up |
+
+The file extension is optional. `./themes` and `./themes.mdx` open the same page.
+
+A folder's main page is its `index.mdx`, or a file named after the folder, such as `components/components.mdx`. Link to the folder, like `./components/`, and you reach it either way.
+
+## Jump to a section
+
+Add `#` and the section name after the page:
+
+```md
+[Create a skin](./themes#create-a-skin)
+[Back to the top](#links)
+```
+
+A section name is its heading in lowercase, with hyphens between the words. The heading "Create a skin" becomes `#create-a-skin`. Try it: [create a skin](./themes#create-a-skin).
+
+PureStack checks that the page exists, not the section, so double-check section names after you rename a heading.
+
+## Links in components
+
+The same rule works for `href` on any tag, including components and links they build from a value:
+
+```mdx
+Read about themes
+
+Next
+
+Back to home
+```
+
+Links in a shared header or footer start from that header or footer file, so they work on every page that shows them.
+
+## Link outside your content
+
+Anything that starts with `/` or a protocol is used as written:
+
+| Write | Use it for |
+| --- | --- |
+| `/blog/` | Pages served from the same domain but built separately |
+| `https://github.com/PureStackStudio/PureStack` | Other websites |
+| `mailto:hello@example.com` | Email |
+
+PureStack doesn't check these links, because the pages behind them aren't part of your content folder.
+
+You can link your own pages this way too, as in `/guides/themes/`, but a relative link is the better habit: if that page moves, the build tells you.
+
+If the site lives under a base path, such as `/docs`, PureStack adds it to every link for you. Set it once in the [site configuration](./site-config).
+
+## When a link is broken
+
+The build stops and names the file and the link:
+
+```text
+Content link "./themse" in "guides/semantic-tones.mdx" does not match any page.
+```
+
+While `purestack serve` runs, only that page shows the error, and it updates as soon as you fix the link. Most fixes are one of these:
+
+- **A typo or different capitals.** `./Themes` doesn't find `themes.mdx`.
+- **The page moved.** Write the path again, starting from your file.
+- **The page is built elsewhere.** Start the path at the site root, such as `/blog/`.
+
+## Translated sites
+
+Link pages inside your language folder as usual. If a page isn't translated yet, the link opens the default language's version, so you can translate at your own pace.
+
+## Quick reference
+
+| To reach | Write |
+| --- | --- |
+| A page in the same folder | `./themes` |
+| A page in a subfolder | `./components/buttons` |
+| A folder's main page | `./components/` |
+| The page one folder up | `../` |
+| A section of another page | `./themes#create-a-skin` |
+| A section of this page | `#quick-reference` |
+| A page built separately | `/blog/` |
+| Another website | `https://example.com` |
+
+
+ Link your own pages relative to your file. Use a full path or address for
+ everything else.
+
diff --git a/frontend/purestack.studio/guides/semantic-tones.mdx b/frontend/purestack.studio/guides/semantic-tones.mdx
index acfc5558..22332215 100644
--- a/frontend/purestack.studio/guides/semantic-tones.mdx
+++ b/frontend/purestack.studio/guides/semantic-tones.mdx
@@ -468,4 +468,4 @@ root.select('.my-action:focus-visible').css({
The symbolic reference keeps the style responsive to the nearest tone and theme. Apply `tone--danger` to that action and its focus ring follows the danger button role without another selector.
-Continue with [themes and skins](/guides/themes) for palette authoring and [utility classes](/guides/utility-css-classes) for geometry and layout.
+Continue with [themes and skins](./themes) for palette authoring and [utility classes](./utility-css-classes) for geometry and layout.
diff --git a/package.json b/package.json
index fb02492a..d33dc7b1 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "purestack-repo",
- "version": "1.1.1",
+ "version": "1.1.2",
"packageManager": "yarn@4.18.1",
"type": "module",
"sideEffects": false,
diff --git a/packages/purestack/package.json b/packages/purestack/package.json
index df21fa6e..68ead2e9 100644
--- a/packages/purestack/package.json
+++ b/packages/purestack/package.json
@@ -1,6 +1,6 @@
{
"name": "purestack",
- "version": "1.1.1",
+ "version": "1.1.2",
"packageManager": "yarn@4.9.2",
"module": "./dist/purestack.mjs",
"types": "./dist/purestack.d.mts",
diff --git a/packages/purestack/src/index.ts b/packages/purestack/src/index.ts
index 09e9b440..5598cd9f 100644
--- a/packages/purestack/src/index.ts
+++ b/packages/purestack/src/index.ts
@@ -1,3 +1,3 @@
-export const version: string = '1.1.1'
+export const version: string = '1.1.2'
export * from '@purestack/ts-ssg'
diff --git a/packages/ts-common/package.json b/packages/ts-common/package.json
index b4b370db..92373060 100644
--- a/packages/ts-common/package.json
+++ b/packages/ts-common/package.json
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-common",
- "version": "1.1.1",
+ "version": "1.1.2",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-common.mjs",
"types": "./dist/ts-common.d.mts",
diff --git a/packages/ts-components/package.json b/packages/ts-components/package.json
index cf7a7a90..91cf9d66 100644
--- a/packages/ts-components/package.json
+++ b/packages/ts-components/package.json
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-components",
- "version": "1.1.1",
+ "version": "1.1.2",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-components.mjs",
"types": "./dist/ts-components.d.mts",
diff --git a/packages/ts-components/src/standard/btn/btn.test.ts b/packages/ts-components/src/standard/btn/btn.test.ts
index 19b409c4..1add61e4 100644
--- a/packages/ts-components/src/standard/btn/btn.test.ts
+++ b/packages/ts-components/src/standard/btn/btn.test.ts
@@ -112,7 +112,8 @@ describe('Button rendering', () => {
cleanup()
expect(html).toContain('Read docs')
diff --git a/packages/ts-components/src/standard/btn/btn.ts b/packages/ts-components/src/standard/btn/btn.ts
index 14c2836d..dbd58e94 100644
--- a/packages/ts-components/src/standard/btn/btn.ts
+++ b/packages/ts-components/src/standard/btn/btn.ts
@@ -1,6 +1,5 @@
import { type TsSsgContext, tryResolveTsSsgContext } from '@purestack/ts-common'
import type { SemanticTone } from '@purestack/ts-style'
-import { urlNormalizer } from '@purestack/ts-util'
import {
type ComputedRef,
computed,
@@ -178,10 +177,9 @@ function resolveShowEndIcon(props: BtnBase) {
}
function resolveButtonHref(props: BtnLink, context: TsSsgContext | undefined) {
- const normalized = urlNormalizer.normalizeHref(unref(props.href))
- return normalized
- ? (context?.resolvePublicHref(normalized) ?? normalized)
- : undefined
+ const value = unref(props.href)
+ const href = typeof value === 'string' ? value.trim() : undefined
+ return href ? (context?.resolvePublicHref(href) ?? href) : undefined
}
function resolveButtonRel(props: BtnLink) {
diff --git a/packages/ts-components/src/standard/btnGroup/btnGroup.test.ts b/packages/ts-components/src/standard/btnGroup/btnGroup.test.ts
index 143d6f37..8b00f4b8 100644
--- a/packages/ts-components/src/standard/btnGroup/btnGroup.test.ts
+++ b/packages/ts-components/src/standard/btnGroup/btnGroup.test.ts
@@ -32,7 +32,7 @@ describe('Button group rendering', () => {
expect(html).toContain('