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
14 changes: 14 additions & 0 deletions QUICK-REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
| **Radio** | `- ( )` / `- (*)` | `- (*) Option 1` |
| **Icon** | `:name:` | `:home: :user: :gear:` |
| **Nav Bar** | `[[ A \| B \| C ]]` | `[[ Home \| About \| [Login] ]]` |
| **Nav Link** | `[[ [Text](url) \| ... ]]` | `[[ [About](./about.md) \| ... ]]` |
| **Breadcrumbs** | `[[ A > B > C ]]` | `[[ Home > Products > Item ]]` |
| **Button Link** | `[[Text](url)]` | `[[About](./about.md)]` |
| **Primary Button Link** | `[[Text](url)]*` | `[[Get Started](./start.md)]*` |

## Containers

Expand Down Expand Up @@ -132,6 +135,17 @@ Message
[[ Home > Products > Category > Item ]]
```

### Multi-file Navigation

When running `wiremd --serve`, clicking a button link navigates to and renders that `.md` file:

```markdown
# Shared navbar (paste in each page)
[[ :logo: MyApp | [Home](./home.md) | [About](./about.md) | [Contact](./contact.md)* ]]
```

The dev server (`--serve <port>`) redirects `/` to the entry file and renders any `.md` on demand — no build step needed between page navigations.

## Grid Pattern

```markdown
Expand Down
17 changes: 17 additions & 0 deletions docs/guide/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,23 @@ Enterprise-grade security
Grows with your needs
```

### Button Links

Wrap a Markdown link inside button brackets to make a clickable button that navigates:

```markdown
[[Go to Docs](./docs.md)]
[[Get Started](./start.md)]*
```

The `*` suffix makes it a primary button. Attributes work too:

```markdown
[[Sign Up](./signup.md)]{.secondary}
```

When using `wiremd --serve`, clicking a button link renders the target `.md` file in the same browser tab — no build step required. This is the recommended way to wire up multi-page navigation in prototypes.

**Column spanning** — `{.col-span-N}` on a child heading spans multiple columns:

```markdown
Expand Down
16 changes: 16 additions & 0 deletions examples/gallery/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ Application dashboards for data visualization and management.

---

### 🔗 Multi-Page Navigation (1 Example)

A three-page prototype showing how button links and the dev server work together for real navigation.

| Example | Description | Key Features |
|---------|-------------|--------------|
| **Multi-Page App** | Home → About → Contact prototype | Shared navbar, button links, live navigation between pages |

**Run it:**
```bash
wiremd examples/gallery/multi-page/home.md --serve 3001
# Open http://localhost:3001 and click the nav buttons
```

---

### 🧩 Components (5 Examples)

Reusable UI component patterns and layouts.
Expand Down
45 changes: 45 additions & 0 deletions examples/gallery/multi-page/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[[ :logo: MyApp | [Home](./home.md) | *About* | [Contact](./contact.md) ]]

---

## About MyApp

wiremd is a text-first UI design tool that generates wireframes from Markdown.
Write your UI, see it instantly — no design software required.

---

## The Team {.grid-3 card}

### :user: Alice Chen
Co-founder & CEO

### :user: Ben Müller
Co-founder & CTO

### :user: Sara Kim
Head of Design

---

## Our Story

::: card

### From frustration to tool

We were tired of maintaining both a Figma file and a spec doc.
wiremd collapses those into one artifact: a `.md` file that
*is* the design.

[Read the full story](./home.md)

:::

---

::: alert info
Want to get in touch? Hit the **Contact** button in the nav.
:::

[← Home](./home.md) [Contact →](./contact.md)*
37 changes: 37 additions & 0 deletions examples/gallery/multi-page/contact.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[[ :logo: MyApp | [Home](./home.md) | [About](./about.md) | *Contact* ]]

---

::: card

## Contact Us

Name
[_____________________________]{required}

Email
[_____________________________]{type:email required}

Subject
[Select topic_____________v]
- General question
- Bug report
- Feature request
- Partnership

Message
[Your message...]{rows:5}

- [ ] Subscribe to updates

[Send Message]* [Cancel]

:::

---

::: alert success
We typically respond within one business day.
:::

[← About](./about.md)
38 changes: 38 additions & 0 deletions examples/gallery/multi-page/home.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[[ :logo: MyApp | *Home* | [About](./about.md) | [Contact](./contact.md) ]]

---

::: hero

# Welcome to MyApp

The fastest way to prototype multi-page apps in Markdown.

[[Get Started](./about.md)]* [[See Features](./about.md)]

:::

## Why MyApp {.grid-3 card}

### :rocket: Fast
From idea to prototype in minutes, not hours.

### :shield: Reliable
Battle-tested across thousands of real projects.

### :gear: Flexible
Works with any workflow, any team size.

---

## Latest Updates

| Feature | Status | Release |
|---------|--------|---------|
| Multi-file navigation | Released | v0.1.5 |
| Button links | Released | v0.1.5 |
| Grid card modifier | Released | v0.1.4 |

---

[About →](./about.md)
10 changes: 8 additions & 2 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/

import { readFileSync, writeFileSync, existsSync, statSync } from 'fs';
import { resolve, dirname, join } from 'path';
import { resolve, dirname, join, basename } from 'path';
import { pathToFileURL } from 'url';
import { parse } from '../parser/index.js';
import { renderToHTML, renderToJSON } from '../renderer/index.js';
Expand Down Expand Up @@ -288,7 +288,13 @@ export function main(): void {
// Start dev server if requested
if (options.serve) {
const port = options.serve;
startServer({ port, outputPath: options.output });
startServer({
port,
outputPath: options.output,
renderFile: (mdPath: string) => generateOutput({ ...options, input: mdPath }),
rootDir: dirname(options.input),
inputFile: basename(options.input),
});
console.log('');
}

Expand Down
86 changes: 67 additions & 19 deletions src/cli/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@
*/

import { createServer, IncomingMessage, ServerResponse } from 'http';
import { readFileSync } from 'fs';
import { readFileSync, existsSync } from 'fs';
import { createHash } from 'crypto';
import { dirname, join } from 'path';

interface ServerOptions {
port: number;
outputPath: string;
renderFile?: (mdPath: string) => string;
/** Root directory for resolving linked .md files. Defaults to dirname(outputPath). */
rootDir?: string;
/** Entry .md filename (e.g. "index.md"). When set, GET / redirects to /{inputFile}. */
inputFile?: string;
}

const liveReloadScript = `
Expand Down Expand Up @@ -356,35 +362,75 @@ const liveReloadScript = `

const wsClients: Set<any> = new Set();

export function startServer(options: ServerOptions): void {
const { port, outputPath } = options;
export function startServer(options: ServerOptions): ReturnType<typeof createServer> {
const { port, outputPath, renderFile, inputFile } = options;
const rootDir = options.rootDir || dirname(outputPath);

const injectScript = (html: string) => {
const script = liveReloadScript.replace('__PORT__', String(port));
return html.replace('</body>', `${script}\n</body>`);
};

// Simple WebSocket implementation without dependencies
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
// Handle WebSocket upgrade
if (req.url === '/__ws') {
res.writeHead(426, { 'Content-Type': 'text/plain' });
res.end('This endpoint requires WebSocket upgrade');
return;
}

// Serve the HTML file
try {
let html = readFileSync(outputPath, 'utf-8');
const urlPath = (req.url || '/').split('?')[0];
let html: string | null = null;

// Inject live-reload script before </body>
const script = liveReloadScript.replace('__PORT__', String(port));
html = html.replace('</body>', `${script}\n</body>`);
if (urlPath === '/' || urlPath === '') {
if (inputFile) {
res.writeHead(302, { Location: `/${inputFile}` });
res.end();
return;
}
try {
html = readFileSync(outputPath, 'utf-8');
} catch {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end(`Error reading: ${outputPath}`);
return;
}
} else if (renderFile) {
const requestedFile = urlPath.replace(/^\//, '');
const targetPath = join(rootDir, requestedFile);

if (targetPath.endsWith('.md') && existsSync(targetPath)) {
try {
html = renderFile(targetPath);
} catch (err: any) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end(`Error rendering ${targetPath}: ${err.message}`);
return;
}
} else if (targetPath.endsWith('.html')) {
if (existsSync(targetPath)) {
try { html = readFileSync(targetPath, 'utf-8'); } catch {}
}
if (!html) {
const mdPath = targetPath.replace(/\.html$/, '.md');
if (existsSync(mdPath)) {
try { html = renderFile(mdPath); } catch (err: any) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end(`Error rendering ${mdPath}: ${err.message}`);
return;
}
}
}
}
}

res.writeHead(200, {
'Content-Type': 'text/html',
'Cache-Control': 'no-cache, no-store, must-revalidate'
});
res.end(html);
} catch (error) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end(`Error reading file: ${outputPath}`);
if (!html) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end(`Not found: ${urlPath}`);
return;
}

res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache, no-store, must-revalidate' });
res.end(injectScript(html));
});

// Handle WebSocket upgrade manually
Expand Down Expand Up @@ -421,6 +467,8 @@ export function startServer(options: ServerOptions): void {
console.log(`📡 Live-reload enabled`);
console.log(`Press Ctrl+C to stop`);
});

return server;
}

export function notifyReload(): void {
Expand Down
12 changes: 11 additions & 1 deletion src/parser/remark-inline-containers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ import type { Plugin } from 'unified';
/**
* Remark plugin to parse wiremd inline container directives
*/
function serializeChild(c: any): string {
if (c.type === 'link') {
const text = (c.children || []).map((cc: any) => cc.value || '').join('');
return `[${text}](${c.url})`;
}
if (c.type === 'strong') return `**${(c.children || []).map(serializeChild).join('')}**`;
if (c.type === 'emphasis') return `*${(c.children || []).map(serializeChild).join('')}*`;
return c.value || '';
}

export const remarkWiremdInlineContainers: Plugin = () => {
return (tree: any) => {
const newChildren: any[] = [];
Expand All @@ -24,7 +34,7 @@ export const remarkWiremdInlineContainers: Plugin = () => {
node.children &&
node.children.length > 0
) {
const text = node.children.map((c: any) => c.value || '').join('');
const text = node.children.map(serializeChild).join('');

// Check for inline container syntax [[...]]
const match = text.match(/^\[\[\s*(.+?)\s*\]\](\{[^}]+\})?$/);
Expand Down
Loading