Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ dist
*.tgz
tsconfig.bundle.generated.json
PureGate
purestack-setup
*.vsix
.codex/*
/.tmp
Expand Down
1 change: 1 addition & 0 deletions frontend/purestack.studio/guides/_nav.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"components",
"purestack-cli.mdx",
"site-config.mdx",
"links.mdx",
"vscode-extension.mdx",
"regor.mdx",
"typography.mdx",
Expand Down
7 changes: 6 additions & 1 deletion frontend/purestack.studio/guides/index.mdx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -26,6 +26,11 @@ These guides follow the path from a content folder to a finished interface. Star
<p>Set output paths, navigation, themes, search, metadata, languages, and optional scripts.</p>
<BtnLink href="/guides/site-config/" tone="accent" variant="link">Configure a site</BtnLink>
</Panel>
<Panel tone="neutral" bodyClass="p-3">
<h2 class="mt-0">Links</h2>
<p>Link pages and sections relative to your file, and let every build check them.</p>
<BtnLink href="./links" tone="accent" variant="link">Write links</BtnLink>
</Panel>
<Panel tone="neutral" bodyClass="p-3">
<h2 class="mt-0">VS Code extension</h2>
<p>Find component source, complete props and frontmatter, and format markup on save.</p>
Expand Down
124 changes: 124 additions & 0 deletions frontend/purestack.studio/guides/links.mdx
Original file line number Diff line number Diff line change
@@ -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
<BtnLink href="./themes" tone="accent">Read about themes</BtnLink>

<BtnLink :href="nextPage">Next</BtnLink>

<a href="../">Back to home</a>
```

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` |

<AlertBox tone="accent" icon="tabler:bolt" title="Rule of thumb">
Link your own pages relative to your file. Use a full path or address for
everything else.
</AlertBox>
2 changes: 1 addition & 1 deletion frontend/purestack.studio/guides/semantic-tones.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/purestack/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/purestack/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const version: string = '1.1.1'
export const version: string = '1.1.2'

export * from '@purestack/ts-ssg'
2 changes: 1 addition & 1 deletion packages/ts-common/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-components/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
3 changes: 2 additions & 1 deletion packages/ts-components/src/standard/btn/btn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ describe('Button rendering', () => {
cleanup()

expect(html).toContain('<a')
expect(html).toContain('href="/getting-started"')
// The page build resolves relative links from the file that wrote them.
expect(html).toContain('href="./getting-started"')
expect(html).toContain('tone--neutral')
expect(html).toContain('btn--lg')
expect(html).toContain('<span class="btn__label">Read docs</span>')
Expand Down
8 changes: 3 additions & 5 deletions packages/ts-components/src/standard/btn/btn.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions packages/ts-components/src/standard/btnGroup/btnGroup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('Button group rendering', () => {
expect(html).toContain('<button')
expect(html).toContain('<a')
expect(html).toContain('<span class="btn__label">Save</span>')
expect(html).toContain('href="/docs"')
expect(html).toContain('href="./docs"')
})

it('renders a native dropdown menu with default trigger affordance', () => {
Expand Down Expand Up @@ -103,7 +103,7 @@ describe('Button group rendering', () => {
</BtnGroup>`)

expect(html).toContain('<span class="btn__label">Default</span>')
expect(html).toContain('href="/settings"')
expect(html).toContain('href="./settings"')
expect(html).toContain('<span class="btn__label">Settings</span>')
expect(html).toContain('<section class="menu-extra">')
expect(html).toContain('<strong>Advanced</strong>')
Expand Down
10 changes: 5 additions & 5 deletions packages/ts-components/src/standard/form/form.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { tryResolveTsSsgContext } from '@purestack/ts-common'
import type { SemanticTone } from '@purestack/ts-style'
import { urlNormalizer } from '@purestack/ts-util'
import {
type ComponentHead,
type ComputedRef,
Expand Down Expand Up @@ -164,10 +163,7 @@ function resolveFormAssistLink(
return {
...head.props,
normalizedHref: computed(() =>
resolvePublicHref(
urlNormalizer.normalizeHref(unref(head.props.href)),
head,
),
resolvePublicHref(toTrimmedHref(unref(head.props.href)), head),
),
resolvedRel: computed(
() =>
Expand All @@ -179,6 +175,10 @@ function resolveFormAssistLink(
}
}

function toTrimmedHref(value: unknown) {
return typeof value === 'string' ? value.trim() : undefined
}

function resolvePublicHref(
href: string | undefined,
head: ComponentHead<FormAssistLink>,
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-css/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-css",
"version": "1.1.1",
"version": "1.1.2",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-css.mjs",
"types": "./dist/ts-css.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-html/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-html",
"version": "1.1.1",
"version": "1.1.2",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-html.mjs",
"types": "./dist/ts-html.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-minidom/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-minidom",
"version": "1.1.1",
"version": "1.1.2",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-minidom.mjs",
"types": "./dist/ts-minidom.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-page-scripts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-page-scripts",
"version": "1.1.1",
"version": "1.1.2",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-page-scripts.mjs",
"types": "./dist/ts-page-scripts.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-render/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-render",
"version": "1.1.1",
"version": "1.1.2",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-render.mjs",
"types": "./dist/ts-render.d.mts",
Expand Down
3 changes: 3 additions & 0 deletions packages/ts-render/src/renderApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { componentRegistry } from './componentRegistry'
export interface RenderAppOptions<TContext extends TsSsgContext> {
components: unknown
context: TContext
/** Runs on the rendered document, before it becomes HTML. */
onRendered?: (document: Document) => void
}

export const renderApp = <TContext extends TsSsgContext>(
Expand Down Expand Up @@ -50,6 +52,7 @@ export const renderApp = <TContext extends TsSsgContext>(
},
)
appendEmbeddedScriptsToDom(runtimeEmbeds, tsSsgContext)
options.onRendered?.(document)
if (isDocument) {
const documentHtml = document.documentElement?.outerHTML ?? ''
return `<!DOCTYPE html>${documentHtml}`
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-ssg-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "purestack-component-navigation",
"displayName": "PureStack Component Tools",
"description": "Component navigation, prop IntelliSense, lit-html syntax highlighting, and template formatting for Markdown, Regor MDX, and TypeScript templates.",
"version": "1.1.1",
"version": "1.1.2",
"private": true,
"publisher": "purestack",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-ssg/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-ssg",
"version": "1.1.1",
"version": "1.1.2",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-ssg.mjs",
"types": "./dist/ts-ssg.d.mts",
Expand Down
Loading
Loading