create-ngmd 0.0.2 → 0.0.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.
- package/package.json +1 -1
- package/template/README.md +1 -1
- package/template/link-guard.plugin.ts +38 -27
- package/template/page-meta.plugin.ts +45 -29
- package/template/sitemap.plugin.ts +36 -25
- package/template/src/app/pages/[...slug].page.ts +79 -0
- package/template/src/content/welcome.md +2 -2
- package/template/src/app/pages/[...not-found].page.ts +0 -47
- package/template/src/app/pages/welcome.page.ts +0 -18
package/package.json
CHANGED
package/template/README.md
CHANGED
|
@@ -11,7 +11,7 @@ pnpm dev
|
|
|
11
11
|
|
|
12
12
|
## Add a page
|
|
13
13
|
|
|
14
|
-
Drop a `.md` file under `src/
|
|
14
|
+
Drop a `.md` file under `src/content/`. The path becomes the URL: `src/content/install.md` resolves at `/install`, `src/content/guides/auth.md` at `/guides/auth`. No `.page.ts` wrapper needed; the catch-all at `src/app/pages/[...slug].page.ts` renders every prose route.
|
|
15
15
|
|
|
16
16
|
## Build
|
|
17
17
|
|
|
@@ -10,8 +10,8 @@ import type { Plugin } from 'vite';
|
|
|
10
10
|
* - `[text](/path)` — `/path` must be a known route
|
|
11
11
|
* - `[text](/path#fragment)` — both the route and the heading slug must exist
|
|
12
12
|
*
|
|
13
|
-
* Routes are discovered by
|
|
14
|
-
*
|
|
13
|
+
* Routes are discovered by walking `src/content/**\/*.md` (each markdown
|
|
14
|
+
* file's path under content/ becomes its route) and `src/app/pages/**\/*.page.ts`.
|
|
15
15
|
* External (`http(s)://`), mail (`mailto:`), and relative (`./foo`) links are
|
|
16
16
|
* skipped; the existing externalLinkGuard covers raw HTML external anchors.
|
|
17
17
|
*
|
|
@@ -19,21 +19,6 @@ import type { Plugin } from 'vite';
|
|
|
19
19
|
* rule the rendered TOC uses, so dev-time and runtime stay in sync.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
const CONTENT_TO_ROUTE: Record<string, string> = {
|
|
23
|
-
welcome: '/welcome',
|
|
24
|
-
about: '/getting-started/about',
|
|
25
|
-
changelog: '/getting-started/changelog',
|
|
26
|
-
installation: '/getting-started/installation',
|
|
27
|
-
'quick-start': '/getting-started/quick-start',
|
|
28
|
-
theming: '/concepts/theming',
|
|
29
|
-
components: '/concepts/components',
|
|
30
|
-
'markdown-routes': '/concepts/markdown-routes',
|
|
31
|
-
'stack-overview': '/stack/overview',
|
|
32
|
-
'stack-technologies': '/stack/technologies',
|
|
33
|
-
'stack-installation': '/stack/installation',
|
|
34
|
-
support: '/support',
|
|
35
|
-
};
|
|
36
|
-
|
|
37
22
|
function slugify(s: string): string {
|
|
38
23
|
return s
|
|
39
24
|
.toLowerCase()
|
|
@@ -55,6 +40,32 @@ function walkPageFiles(dir: string, root: string, out: string[] = []): string[]
|
|
|
55
40
|
return out;
|
|
56
41
|
}
|
|
57
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Walk `src/content/**\/*.md` and return `[relativePath, route]` pairs.
|
|
45
|
+
* Route mirrors the path under `src/content/` with the .md stripped.
|
|
46
|
+
* Example: `src/content/concepts/theming.md` → `/concepts/theming`.
|
|
47
|
+
*/
|
|
48
|
+
function walkContentFiles(
|
|
49
|
+
dir: string,
|
|
50
|
+
root: string,
|
|
51
|
+
baseDir: string = dir,
|
|
52
|
+
out: Array<[string, string]> = [],
|
|
53
|
+
): Array<[string, string]> {
|
|
54
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
55
|
+
const full = join(dir, entry.name);
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
walkContentFiles(full, root, baseDir, out);
|
|
58
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
59
|
+
const rel = relative(root, full);
|
|
60
|
+
const fromContent = relative(baseDir, full)
|
|
61
|
+
.replace(/\\/g, '/')
|
|
62
|
+
.replace(/\.md$/, '');
|
|
63
|
+
out.push([rel, '/' + fromContent]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
function routeFromPagePath(rel: string): string {
|
|
59
70
|
const trimmed = rel
|
|
60
71
|
.replace(/^src\/app\/pages\//, '')
|
|
@@ -86,17 +97,17 @@ export function internalLinkGuard(): Plugin {
|
|
|
86
97
|
if (primed) return;
|
|
87
98
|
primed = true;
|
|
88
99
|
|
|
89
|
-
// .md → route
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
// .md → route (walk src/content/ tree)
|
|
101
|
+
const contentDir = join(root, 'src/content');
|
|
102
|
+
try {
|
|
103
|
+
statSync(contentDir);
|
|
104
|
+
for (const [rel, route] of walkContentFiles(contentDir, root)) {
|
|
105
|
+
const full = join(root, rel);
|
|
106
|
+
routes.set(route, rel);
|
|
107
|
+
headingsByRoute.set(route, extractHeadings(readFileSync(full, 'utf8')));
|
|
97
108
|
}
|
|
98
|
-
|
|
99
|
-
|
|
109
|
+
} catch {
|
|
110
|
+
// src/content missing — skip
|
|
100
111
|
}
|
|
101
112
|
|
|
102
113
|
// .page.ts → route (no heading scrape; just makes the route resolvable)
|
|
@@ -6,10 +6,11 @@ import type { Plugin } from 'vite';
|
|
|
6
6
|
/**
|
|
7
7
|
* Build-time map of page URL → { editUrl, lastUpdated }.
|
|
8
8
|
*
|
|
9
|
-
* Walks `src/app/pages` (for `.page.ts` routes) and `src/content
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `virtual:ngmd/page-meta`
|
|
9
|
+
* Walks `src/app/pages` (for `.page.ts` routes) and `src/content/**\/*.md`
|
|
10
|
+
* (each markdown file's path under content/ becomes its route, matching the
|
|
11
|
+
* `[...slug].page.ts` catch-all), pulls the latest commit date via `git log`,
|
|
12
|
+
* and emits a typed module under the virtual id `virtual:ngmd/page-meta`
|
|
13
|
+
* which the runtime imports.
|
|
13
14
|
*
|
|
14
15
|
* If the file is uncommitted, lastUpdated falls back to its mtime in ISO
|
|
15
16
|
* date form so dev iteration still shows something.
|
|
@@ -64,21 +65,31 @@ function routeFromPagePath(rel: string): string {
|
|
|
64
65
|
return '/' + trimmed;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Walk `src/content/**\/*.md` and return `[relativePath, route]` pairs.
|
|
70
|
+
* Route mirrors the path under `src/content/` with the .md stripped.
|
|
71
|
+
* Example: `src/content/concepts/theming.md` → `/concepts/theming`.
|
|
72
|
+
*/
|
|
73
|
+
function walkContentFiles(
|
|
74
|
+
dir: string,
|
|
75
|
+
root: string,
|
|
76
|
+
baseDir: string = dir,
|
|
77
|
+
out: Array<[string, string]> = [],
|
|
78
|
+
): Array<[string, string]> {
|
|
79
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
80
|
+
const full = join(dir, entry.name);
|
|
81
|
+
if (entry.isDirectory()) {
|
|
82
|
+
walkContentFiles(full, root, baseDir, out);
|
|
83
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
84
|
+
const rel = relative(root, full);
|
|
85
|
+
const fromContent = relative(baseDir, full)
|
|
86
|
+
.replace(/\\/g, '/')
|
|
87
|
+
.replace(/\.md$/, '');
|
|
88
|
+
out.push([rel, '/' + fromContent]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
82
93
|
|
|
83
94
|
export function pageMetaPlugin(opts: {
|
|
84
95
|
repoUrl: string;
|
|
@@ -114,16 +125,21 @@ export function pageMetaPlugin(opts: {
|
|
|
114
125
|
};
|
|
115
126
|
}
|
|
116
127
|
|
|
117
|
-
// src/content
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
128
|
+
// src/content/**/*.md → route (mirrors the [...slug] catch-all)
|
|
129
|
+
const contentDir = join(root, 'src/content');
|
|
130
|
+
try {
|
|
131
|
+
statSync(contentDir);
|
|
132
|
+
for (const [rel, route] of walkContentFiles(contentDir, root)) {
|
|
133
|
+
const date = gitDate(rel, root);
|
|
134
|
+
if (!date) continue;
|
|
135
|
+
// .md edit URL wins when present (more useful for prose pages)
|
|
136
|
+
map[route] = {
|
|
137
|
+
editUrl: `${opts.repoUrl}/edit/${branch}/${rel}`,
|
|
138
|
+
lastUpdated: date,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// src/content missing — skip
|
|
127
143
|
}
|
|
128
144
|
|
|
129
145
|
return `export const pageMeta = ${JSON.stringify(map, null, 2)};`;
|
|
@@ -7,28 +7,39 @@ import type { Plugin } from 'vite';
|
|
|
7
7
|
* Emits `sitemap.xml` and `robots.txt` into the client build output.
|
|
8
8
|
*
|
|
9
9
|
* Discovery mirrors the page-meta plugin: walks `src/app/pages/*.page.ts` and
|
|
10
|
-
* `src/content
|
|
11
|
-
* commit date via `git log -1 --format=%cs` to populate
|
|
12
|
-
* back to mtime for uncommitted files, and writes both
|
|
13
|
-
* `emitFile` so they land at the client root.
|
|
10
|
+
* `src/content/**\/*.md` (each markdown file's path becomes its route), pulls
|
|
11
|
+
* each file's last commit date via `git log -1 --format=%cs` to populate
|
|
12
|
+
* `<lastmod>`, falls back to mtime for uncommitted files, and writes both
|
|
13
|
+
* files via Rollup's `emitFile` so they land at the client root.
|
|
14
14
|
*
|
|
15
15
|
* `robots.txt` is a one-liner pointing at the sitemap.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Walk `src/content/**\/*.md` and return `[relativePath, route]` pairs.
|
|
20
|
+
* Route mirrors the path under `src/content/` with the .md stripped.
|
|
21
|
+
* Example: `src/content/concepts/theming.md` → `/concepts/theming`.
|
|
22
|
+
*/
|
|
23
|
+
function walkContentFiles(
|
|
24
|
+
dir: string,
|
|
25
|
+
root: string,
|
|
26
|
+
baseDir: string = dir,
|
|
27
|
+
out: Array<[string, string]> = [],
|
|
28
|
+
): Array<[string, string]> {
|
|
29
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
30
|
+
const full = join(dir, entry.name);
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
walkContentFiles(full, root, baseDir, out);
|
|
33
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
34
|
+
const rel = relative(root, full);
|
|
35
|
+
const fromContent = relative(baseDir, full)
|
|
36
|
+
.replace(/\\/g, '/')
|
|
37
|
+
.replace(/\.md$/, '');
|
|
38
|
+
out.push([rel, '/' + fromContent]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
32
43
|
|
|
33
44
|
function gitDate(file: string, cwd: string): string {
|
|
34
45
|
try {
|
|
@@ -107,14 +118,14 @@ export function sitemapPlugin(opts: { siteUrl: string }): Plugin {
|
|
|
107
118
|
// src/app/pages missing — fine
|
|
108
119
|
}
|
|
109
120
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
continue;
|
|
121
|
+
const contentDir = join(root, 'src/content');
|
|
122
|
+
try {
|
|
123
|
+
statSync(contentDir);
|
|
124
|
+
for (const [rel, route] of walkContentFiles(contentDir, root)) {
|
|
125
|
+
entries.set(route, gitDate(rel, root));
|
|
116
126
|
}
|
|
117
|
-
|
|
127
|
+
} catch {
|
|
128
|
+
// src/content missing — skip
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
const urls = [...entries.entries()]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { AsyncPipe } from '@angular/common';
|
|
2
|
+
import { Component, OnDestroy, computed, effect, inject } from '@angular/core';
|
|
3
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
4
|
+
import { RouterLink } from '@angular/router';
|
|
5
|
+
import { injectContent, MarkdownComponent } from '@analogjs/content';
|
|
6
|
+
import { LayoutMode } from '../layout-mode.service';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Catch-all route for every markdown page.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors the adev pattern: one shared component renders every prose route.
|
|
12
|
+
* `injectContent()` reads the `slug` route param (which the `[...slug]`
|
|
13
|
+
* convention populates with the full nested path) and looks up the matching
|
|
14
|
+
* `src/content/<slug>.md`. Pages that need bespoke layouts (the landing
|
|
15
|
+
* page, the components gallery) keep their named `.page.ts` and Angular's
|
|
16
|
+
* router prefers the more specific match.
|
|
17
|
+
*
|
|
18
|
+
* Doubles as the 404 page: when no markdown matches the requested URL,
|
|
19
|
+
* `injectContent` returns the sentinel below as the body, the docs chrome
|
|
20
|
+
* is hidden, and the 404 UI is rendered instead.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const NOT_FOUND = '__ngmd-not-found__';
|
|
24
|
+
|
|
25
|
+
@Component({
|
|
26
|
+
selector: 'app-doc',
|
|
27
|
+
imports: [AsyncPipe, MarkdownComponent, RouterLink],
|
|
28
|
+
template: `
|
|
29
|
+
@if (content$ | async; as doc) {
|
|
30
|
+
@if (doc.content === notFound) {
|
|
31
|
+
<section class="mx-auto max-w-2xl px-6 py-24 text-center">
|
|
32
|
+
<p class="text-sm font-medium tracking-[0.2em] text-zinc-400 dark:text-zinc-500">
|
|
33
|
+
404
|
|
34
|
+
</p>
|
|
35
|
+
<h1 class="mt-3 text-4xl sm:text-5xl font-bold tracking-tight">
|
|
36
|
+
Page not found
|
|
37
|
+
</h1>
|
|
38
|
+
<p class="mt-4 text-lg text-zinc-600 dark:text-zinc-400">
|
|
39
|
+
The page you're looking for doesn't exist or has moved.
|
|
40
|
+
</p>
|
|
41
|
+
<div class="mt-10 flex flex-wrap items-center justify-center gap-3">
|
|
42
|
+
<a
|
|
43
|
+
routerLink="/"
|
|
44
|
+
class="rounded-md bg-zinc-900 dark:bg-zinc-50 px-5 py-2.5 text-sm font-medium text-zinc-50 dark:text-zinc-900 hover:bg-zinc-700 dark:hover:bg-zinc-200"
|
|
45
|
+
>
|
|
46
|
+
Go home
|
|
47
|
+
</a>
|
|
48
|
+
<a
|
|
49
|
+
routerLink="/welcome"
|
|
50
|
+
class="rounded-md border border-zinc-200 dark:border-zinc-800 px-5 py-2.5 text-sm font-medium hover:bg-zinc-100 dark:hover:bg-zinc-900"
|
|
51
|
+
>
|
|
52
|
+
Read the docs
|
|
53
|
+
</a>
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
} @else {
|
|
57
|
+
<article class="max-w-3xl mx-auto p-8">
|
|
58
|
+
<analog-markdown [content]="doc.content" />
|
|
59
|
+
</article>
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
`,
|
|
63
|
+
})
|
|
64
|
+
export default class DocPage implements OnDestroy {
|
|
65
|
+
private readonly layout = inject(LayoutMode);
|
|
66
|
+
protected readonly notFound = NOT_FOUND;
|
|
67
|
+
|
|
68
|
+
readonly content$ = injectContent<{ title: string }>('slug', NOT_FOUND);
|
|
69
|
+
private readonly doc = toSignal(this.content$);
|
|
70
|
+
private readonly missing = computed(() => this.doc()?.content === NOT_FOUND);
|
|
71
|
+
|
|
72
|
+
constructor() {
|
|
73
|
+
effect(() => this.layout.chromeHidden.set(this.missing()));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ngOnDestroy(): void {
|
|
77
|
+
this.layout.chromeHidden.set(false);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -8,7 +8,7 @@ This is your first docs page, rendered from `src/content/welcome.md`.
|
|
|
8
8
|
|
|
9
9
|
## Add a page
|
|
10
10
|
|
|
11
|
-
Drop a new `.md` file under `src/content
|
|
11
|
+
Drop a new `.md` file under `src/content/`. The path becomes the URL: `src/content/install.md` resolves at `/install`, `src/content/guides/auth.md` at `/guides/auth`. The catch-all at `src/app/pages/[...slug].page.ts` renders every prose route, no wrapper required.
|
|
12
12
|
|
|
13
13
|
## Add nav
|
|
14
14
|
|
|
@@ -16,4 +16,4 @@ Edit the `nav` array in `src/ngmd.config.ts`. Sidebar, command palette, breadcru
|
|
|
16
16
|
|
|
17
17
|
## Authoring components
|
|
18
18
|
|
|
19
|
-
NgMd ships a small authoring component library under `src/app/ui/`: callouts, alerts, cards, tabs (Spartan brain), pill rows, workflows, hero, and a code block with shiki highlighting. Compose them in `.page.ts`
|
|
19
|
+
NgMd ships a small authoring component library under `src/app/ui/`: callouts, alerts, cards, tabs (Spartan brain), pill rows, workflows, hero, and a code block with shiki highlighting. Compose them in a `.page.ts` route for pages that need bespoke layout; for prose pages, stick with markdown.
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
|
|
2
|
-
import { RouterLink } from '@angular/router';
|
|
3
|
-
import { LayoutMode } from '../layout-mode.service';
|
|
4
|
-
|
|
5
|
-
@Component({
|
|
6
|
-
selector: 'app-not-found',
|
|
7
|
-
imports: [RouterLink],
|
|
8
|
-
template: `
|
|
9
|
-
<section class="mx-auto max-w-2xl px-6 py-24 text-center">
|
|
10
|
-
<p class="text-sm font-medium tracking-[0.2em] text-zinc-400 dark:text-zinc-500">
|
|
11
|
-
404
|
|
12
|
-
</p>
|
|
13
|
-
<h1 class="mt-3 text-4xl sm:text-5xl font-bold tracking-tight">
|
|
14
|
-
Page not found
|
|
15
|
-
</h1>
|
|
16
|
-
<p class="mt-4 text-lg text-zinc-600 dark:text-zinc-400">
|
|
17
|
-
The page you're looking for doesn't exist or has moved.
|
|
18
|
-
</p>
|
|
19
|
-
|
|
20
|
-
<div class="mt-10 flex flex-wrap items-center justify-center gap-3">
|
|
21
|
-
<a
|
|
22
|
-
routerLink="/"
|
|
23
|
-
class="rounded-md bg-zinc-900 dark:bg-zinc-50 px-5 py-2.5 text-sm font-medium text-zinc-50 dark:text-zinc-900 hover:bg-zinc-700 dark:hover:bg-zinc-200"
|
|
24
|
-
>
|
|
25
|
-
Go home
|
|
26
|
-
</a>
|
|
27
|
-
<a
|
|
28
|
-
routerLink="/welcome"
|
|
29
|
-
class="rounded-md border border-zinc-200 dark:border-zinc-800 px-5 py-2.5 text-sm font-medium hover:bg-zinc-100 dark:hover:bg-zinc-900"
|
|
30
|
-
>
|
|
31
|
-
Read the docs
|
|
32
|
-
</a>
|
|
33
|
-
</div>
|
|
34
|
-
</section>
|
|
35
|
-
`,
|
|
36
|
-
})
|
|
37
|
-
export default class NotFoundPage implements OnInit, OnDestroy {
|
|
38
|
-
private readonly layout = inject(LayoutMode);
|
|
39
|
-
|
|
40
|
-
ngOnInit(): void {
|
|
41
|
-
this.layout.chromeHidden.set(true);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
ngOnDestroy(): void {
|
|
45
|
-
this.layout.chromeHidden.set(false);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { AsyncPipe } from '@angular/common';
|
|
2
|
-
import { Component } from '@angular/core';
|
|
3
|
-
import { injectContent, MarkdownComponent } from '@analogjs/content';
|
|
4
|
-
|
|
5
|
-
@Component({
|
|
6
|
-
selector: 'app-welcome',
|
|
7
|
-
imports: [AsyncPipe, MarkdownComponent],
|
|
8
|
-
template: `
|
|
9
|
-
@if (content$ | async; as doc) {
|
|
10
|
-
<article class="max-w-3xl mx-auto p-8">
|
|
11
|
-
<analog-markdown [content]="doc.content" />
|
|
12
|
-
</article>
|
|
13
|
-
}
|
|
14
|
-
`,
|
|
15
|
-
})
|
|
16
|
-
export default class WelcomePage {
|
|
17
|
-
readonly content$ = injectContent<{ title: string }>({ customFilename: 'welcome' });
|
|
18
|
-
}
|