Skip to content

Commit ae25b60

Browse files
Copilotmikebarkmin
andauthored
Add page layout options: "wide" and "standalone" layouts for full-width and iframe embedding with automatic detection (#1017)
* Initial plan * Add layout option for pages (default and wide) Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com> * Add wide layout demo pages in English and German Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com> * Add standalone layout for iframe embedding with URL parameter support Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com> * Fix standalone layout content cutoff by adjusting grid template rows Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com> * Reorganize layout documentation into new layouts section Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com> * Consolidate layout docs into single layouts.md page in advanced features Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com> * Add automatic iframe detection for standalone mode and hide TOC/QR buttons Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com> * Add changeset and update changelog for v0.61.0 with layout options Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com> * fix wide and standalone styling --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com> Co-authored-by: Mike Barkmin <mike@barkmin.eu>
1 parent 7590ad2 commit ae25b60

16 files changed

Lines changed: 807 additions & 7 deletions

File tree

.changeset/add-layout-options.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"hyperbook": minor
3+
"@hyperbook/markdown": minor
4+
"@hyperbook/types": minor
5+
---
6+
7+
Add page layout options with automatic iframe detection
8+
9+
- Added three layout options: default, wide, and standalone
10+
- Wide layout provides full-width content with drawer-only navigation, ideal for tables, galleries, and code examples
11+
- Standalone layout displays content only (no header, sidebar, footer) for clean iframe embedding
12+
- Standalone mode can be activated via frontmatter (`layout: standalone`), URL parameter (`?standalone=true`), or automatic iframe detection
13+
- Automatically hides TOC toggle and QR code buttons when in standalone mode
14+
- Zero-configuration embedding: pages automatically switch to standalone mode when embedded in iframes
15+
- Added comprehensive documentation in Advanced Features section with usage examples and demos
16+
- All changes are backward compatible with existing pages

packages/markdown/assets/client.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,38 @@ var hyperbook = (function () {
274274
initBookmarks(root);
275275
}
276276

277+
// Check for standalone layout URL parameter or iframe context
278+
function checkStandaloneMode() {
279+
// Check if explicitly requested via URL parameter
280+
const urlParams = new URLSearchParams(window.location.search);
281+
const standaloneParam = urlParams.get('standalone') === 'true';
282+
283+
// Check if page is inside an iframe
284+
const isInIframe = window.self !== window.top;
285+
286+
if (standaloneParam || isInIframe) {
287+
const mainGrid = document.querySelector('.main-grid');
288+
if (mainGrid && !mainGrid.classList.contains('layout-standalone')) {
289+
mainGrid.classList.add('layout-standalone');
290+
}
291+
292+
// Hide TOC and QR code buttons when in standalone mode
293+
const tocToggle = document.getElementById('toc-toggle');
294+
if (tocToggle) {
295+
tocToggle.style.display = 'none';
296+
}
297+
298+
const qrcodeOpen = document.getElementById('qrcode-open');
299+
if (qrcodeOpen) {
300+
qrcodeOpen.style.display = 'none';
301+
}
302+
}
303+
}
304+
277305
// Initialize existing elements on document load
278306
document.addEventListener("DOMContentLoaded", () => {
279307
init(document);
308+
checkStandaloneMode();
280309
});
281310

282311
// Observe for new elements added to the DOM

packages/markdown/assets/shell.css

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,3 +807,62 @@ nav.toc li.level-3 {
807807
flex-shrink: 0;
808808
/* Prevent caption from shrinking */
809809
}
810+
811+
/* Wide layout: full width content with drawer-only navigation */
812+
.main-grid.layout-wide {
813+
grid-template-columns: 1fr;
814+
grid-template-areas:
815+
"header"
816+
"article";
817+
818+
819+
--main-width: 100%;
820+
}
821+
822+
.main-grid.layout-wide .sidebar {
823+
display: none;
824+
}
825+
826+
.main-grid.layout-wide .mobile-nav {
827+
display: flex;
828+
align-items: center;
829+
justify-content: center;
830+
}
831+
832+
.main-grid.layout-wide main {
833+
padding: 20px 40px;
834+
max-width: 100%;
835+
}
836+
837+
/* Standalone layout: content only, no navigation or header (for iframe embedding) */
838+
.main-grid.layout-standalone {
839+
grid-template-columns: 1fr;
840+
grid-template-rows: 1fr;
841+
grid-template-areas: "article";
842+
843+
--main-width: 100%;
844+
845+
#toc-toggle {
846+
top: inherit;
847+
}
848+
849+
#qrcode-open {
850+
top: 90px;
851+
}
852+
}
853+
854+
.main-grid.layout-standalone header,
855+
.main-grid.layout-standalone .sidebar {
856+
display: none;
857+
}
858+
859+
.main-grid.layout-standalone main {
860+
padding: 20px 40px;
861+
max-width: 100%;
862+
}
863+
864+
.main-grid.layout-standalone .jump-container,
865+
.main-grid.layout-standalone .meta,
866+
.main-grid.layout-standalone #custom-links-footer {
867+
display: none;
868+
}

packages/markdown/src/rehypeShell.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -981,12 +981,20 @@ const makeCustomLinksFooter = (ctx: HyperbookContext) => {
981981
export default (ctx: HyperbookContext) => () => {
982982
return (tree: Root, file: VFile) => {
983983
const originalChildren = tree.children as ElementContent[];
984+
const layout = ctx.navigation.current?.layout || "default";
985+
let mainGridClass = "main-grid";
986+
if (layout === "wide") {
987+
mainGridClass = "main-grid layout-wide";
988+
} else if (layout === "standalone") {
989+
mainGridClass = "main-grid layout-standalone";
990+
}
991+
984992
tree.children = [
985993
{
986994
type: "element",
987995
tagName: "div",
988996
properties: {
989-
class: "main-grid",
997+
class: mainGridClass,
990998
},
991999
children: [
9921000
...makeHeaderElements(ctx),

packages/markdown/tests/rehypeShell.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,68 @@ describe("rehypeShell", () => {
4747
expect(value).toContain("__I_AM_HERE");
4848
expect(value).not.toContain("__I_AM_NOT_HERE");
4949
});
50+
51+
it("should apply default layout class when no layout is specified", () => {
52+
const defaultCtx: HyperbookContext = {
53+
...ctx,
54+
navigation: {
55+
...ctx.navigation,
56+
current: {
57+
...ctx.navigation.current!,
58+
layout: undefined,
59+
},
60+
},
61+
};
62+
const value = toHtml("", defaultCtx).value;
63+
expect(value).toContain('class="main-grid"');
64+
expect(value).not.toContain("layout-wide");
65+
expect(value).not.toContain("layout-standalone");
66+
});
67+
68+
it("should apply wide layout class when layout is set to wide", () => {
69+
const wideCtx: HyperbookContext = {
70+
...ctx,
71+
navigation: {
72+
...ctx.navigation,
73+
current: {
74+
...ctx.navigation.current!,
75+
layout: "wide",
76+
},
77+
},
78+
};
79+
const value = toHtml("", wideCtx).value;
80+
expect(value).toContain('class="main-grid layout-wide"');
81+
});
82+
83+
it("should apply default layout class when layout is explicitly set to default", () => {
84+
const defaultExplicitCtx: HyperbookContext = {
85+
...ctx,
86+
navigation: {
87+
...ctx.navigation,
88+
current: {
89+
...ctx.navigation.current!,
90+
layout: "default",
91+
},
92+
},
93+
};
94+
const value = toHtml("", defaultExplicitCtx).value;
95+
expect(value).toContain('class="main-grid"');
96+
expect(value).not.toContain("layout-wide");
97+
expect(value).not.toContain("layout-standalone");
98+
});
99+
100+
it("should apply standalone layout class when layout is set to standalone", () => {
101+
const standaloneCtx: HyperbookContext = {
102+
...ctx,
103+
navigation: {
104+
...ctx.navigation,
105+
current: {
106+
...ctx.navigation.current!,
107+
layout: "standalone",
108+
},
109+
},
110+
};
111+
const value = toHtml("", standaloneCtx).value;
112+
expect(value).toContain('class="main-grid layout-standalone"');
113+
});
50114
});

packages/types/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export type Glossary = Record<string, Term[]>;
3434

3535
export type Language = "de" | "en" | "fr" | "es" | "it" | "pt" | "nl";
3636

37+
export type Layout = "default" | "wide" | "standalone";
38+
3739
export type HyperbookPageFrontmatter = {
3840
name: string;
3941
permaid?: string;
@@ -48,6 +50,7 @@ export type HyperbookPageFrontmatter = {
4850
toc?: boolean;
4951
next?: string;
5052
prev?: string;
53+
layout?: Layout;
5154
};
5255

5356
export type HyperbookSectionFrontmatter = HyperbookPageFrontmatter & {

pnpm-workspace.yaml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
packages:
2-
# all packages in subdirs of packages/ and components/
3-
- "packages/*"
4-
- "templates/*"
5-
- "platforms/*"
6-
# exclude packages that are inside test directories
7-
- "!**/test/**"
2+
- packages/*
3+
- templates/*
4+
- platforms/*
5+
- '!**/test/**'
6+
7+
onlyBuiltDependencies:
8+
- sharp
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
---
2+
name: Layouts
3+
index: 8
4+
lang: de
5+
---
6+
7+
# Seiten-Layouts
8+
9+
Hyperbook bietet drei Layout-Optionen für unterschiedliche Anforderungen an die Inhaltsdarstellung. Sie können ein Layout wählen, indem Sie die `layout` Eigenschaft im Frontmatter Ihrer Seite hinzufügen.
10+
11+
## Verfügbare Layouts
12+
13+
### Standard-Layout
14+
15+
Das Standardlayout mit sichtbarer Seitenleiste auf Desktop-Bildschirmen. Dies ist das Standardlayout, wenn kein Layout angegeben wird.
16+
17+
**Merkmale:**
18+
- Seitenleisten-Navigation auf Desktop immer sichtbar
19+
- Inhaltsbereich mit optimaler Lesebreite
20+
- Responsives Design, das auf Mobilgeräten zur Drawer-Navigation wechselt
21+
22+
**Verwendung:**
23+
```md
24+
---
25+
name: Meine Seite
26+
layout: default
27+
---
28+
```
29+
30+
Oder lassen Sie die `layout` Eigenschaft einfach weg.
31+
32+
**Am besten für:** Standard-Dokumentationsseiten, Tutorials und Artikel.
33+
34+
---
35+
36+
### Wide Layout
37+
38+
Inhalt in voller Breite mit Navigation immer im Drawer-Modus, bietet maximalen horizontalen Platz.
39+
40+
**Merkmale:**
41+
- Inhalt erstreckt sich über volle Breite mit Padding
42+
- Seitenleiste auf allen Bildschirmgrößen versteckt
43+
- Navigation über Hamburger-Menü zugänglich
44+
- Ideal für Inhalte, die horizontalen Platz benötigen
45+
46+
**Verwendung:**
47+
```md
48+
---
49+
name: Meine Breite Seite
50+
layout: wide
51+
---
52+
```
53+
54+
**Am besten für:**
55+
- Datentabellen mit vielen Spalten
56+
- Lange Code-Beispiele
57+
- Bildergalerien
58+
- Interaktive eingebettete Inhalte
59+
- Präsentations-Seiten
60+
61+
**[Wide Layout Demo ansehen →](/de/advanced/wide-layout-demo)**
62+
63+
---
64+
65+
### Standalone Layout
66+
67+
Nur-Inhalts-Anzeige mit allen versteckten Navigations- und UI-Elementen, perfekt für iframe-Einbettung.
68+
69+
**Merkmale:**
70+
- Keine Header-, Seitenleisten- oder Footer-Elemente
71+
- Saubere, ablenkungsfreie Inhalte
72+
- Kann über Frontmatter, URL-Parameter oder automatisch in iframes aktiviert werden
73+
- Entwickelt für die Einbettung in externe Seiten
74+
- Versteckt automatisch TOC- und QR-Code-Buttons
75+
76+
**Verwendungsmethode 1: Frontmatter**
77+
```md
78+
---
79+
name: Meine Standalone-Seite
80+
layout: standalone
81+
---
82+
```
83+
84+
**Verwendungsmethode 2: URL-Parameter** (funktioniert auf jeder Seite)
85+
```html
86+
<iframe src="https://ihr-hyperbook.com/beliebige-seite?standalone=true"></iframe>
87+
```
88+
89+
**Verwendungsmethode 3: Automatische Erkennung** (iframe-Einbettung)
90+
91+
Wenn eine Hyperbook-Seite in einem iframe eingebettet wird, wechselt sie automatisch in den Standalone-Modus - keine Konfiguration erforderlich! Dies ermöglicht eine nahtlose Einbettung ohne URL-Parameter oder Frontmatter-Änderungen.
92+
93+
```html
94+
<!-- Betten Sie einfach eine beliebige Seite ein - Standalone-Modus wird automatisch aktiviert -->
95+
<iframe src="https://ihr-hyperbook.com/beliebige-seite"></iframe>
96+
```
97+
98+
Die automatische Erkennung sorgt für saubere, ablenkungsfreie Inhalte bei iframe-Einbettungen und behält gleichzeitig die volle Funktionalität bei, wenn Seiten direkt aufgerufen werden.
99+
100+
**Am besten für:**
101+
- Lernmanagementsystem (LMS) Integration
102+
- Einbettung in Dokumentationsportale
103+
- Mobile App Webviews
104+
- Widget-Integration
105+
- Präsentationen
106+
107+
**[Standalone Layout Demo ansehen →](/de/advanced/standalone-layout-demo)**
108+
109+
---
110+
111+
## Konfiguration
112+
113+
Die Layout-Eigenschaft ist optional im Frontmatter Ihrer Seite:
114+
115+
```md
116+
---
117+
name: Seitentitel
118+
layout: wide # oder 'default', 'standalone'
119+
---
120+
121+
# Ihr Inhalt hier
122+
```
123+
124+
## Layout-Vergleich
125+
126+
| Merkmal | Standard | Wide | Standalone |
127+
|---------|---------|------|------------|
128+
| Seitenleisten-Sichtbarkeit | Sichtbar auf Desktop | Immer versteckt | Immer versteckt |
129+
| Inhaltsbreite | Begrenzt für Lesbarkeit | Volle Breite | Volle Breite |
130+
| Navigationszugriff | Seitenleiste / Drawer | Nur Drawer | Keine (versteckt) |
131+
| Header | Sichtbar | Sichtbar | Versteckt |
132+
| Footer | Sichtbar | Sichtbar | Versteckt |
133+
| Bester Anwendungsfall | Dokumentation | Tabellen, Galerien | iframe-Einbettung |
134+
135+
---
136+
137+
## Tipps
138+
139+
- **Standard-Layout**: Verwenden Sie es für die meisten Dokumentationsseiten, um eine konsistente Navigation zu gewährleisten
140+
- **Wide Layout**: Wechseln Sie zu wide, wenn Inhalte horizontalen Platz benötigen (Tabellen, Code, Galerien)
141+
- **Standalone Layout**: Verwenden Sie den URL-Parameter (`?standalone=true`) für flexible iframe-Einbettung ohne Änderung der Seitenquelle
142+
- Sie können verschiedene Layouts über Seiten im selben Hyperbook-Projekt mischen

0 commit comments

Comments
 (0)