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
405 changes: 90 additions & 315 deletions README.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
/src/assets/plug-stack.yml
/src/assets/plug-k8s.yaml
/src/assets/plug-service.yml
# generated at build by scripts/gen-sitemap.mjs (the pages of app.routes.ts)
/src/root/sitemap.xml
*.log
.DS_Store
/.frames
Expand Down
14 changes: 9 additions & 5 deletions docs/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"polyfills": [],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": ["src/assets"],
"assets": ["src/assets", { "glob": "*", "input": "src/root", "output": "/" }],
"styles": ["src/styles.scss"],
"scripts": [],
"allowedCommonJsDependencies": [
Expand All @@ -28,9 +28,8 @@
"prismjs/components/prism-typescript",
"prismjs/components/prism-yaml"
],
"security": {
"autoCsp": true
}
"server": "src/main.server.ts",
"outputMode": "static"
},
"configurations": {
"production": {
Expand All @@ -41,7 +40,12 @@
"maximumError": "1.5mb"
}
],
"outputHashing": "all"
"outputHashing": "all",
"optimization": {
"scripts": true,
"styles": { "minify": true, "inlineCritical": false },
"fonts": true
}
},
"development": {
"optimization": false,
Expand Down
68 changes: 52 additions & 16 deletions docs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"private": true,
"scripts": {
"ng": "ng",
"start": "node scripts/gen-version.mjs && ng serve",
"build": "node scripts/gen-version.mjs && ng build",
"start": "node scripts/gen-version.mjs && node scripts/gen-sitemap.mjs && ng serve",
"build": "node scripts/gen-version.mjs && node scripts/gen-sitemap.mjs && ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test && vitest run --config vitest.scripts.config.mjs",
"lint": "ng lint"
Expand All @@ -18,7 +18,9 @@
"@angular/forms": "^22.2.3",
"@angular/material": "^22.2.3",
"@angular/platform-browser": "^22.2.3",
"@angular/platform-server": "^22.2.3",
"@angular/router": "^22.2.3",
"@angular/ssr": "^22.2.3",
"@softwarity/projects": "^1.0.30",
"prismjs": "^1.30.0",
"rxjs": "~7.8.0",
Expand Down
35 changes: 35 additions & 0 deletions docs/scripts/gen-sitemap.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Writes src/root/sitemap.xml from the router's own list of pages, so a page
// added to app.routes.ts is in the sitemap without anyone remembering it. Run
// by the `build`/`start` npm scripts, before Angular builds; the file is served
// at the site's root beside robots.txt (angular.json, assets).
//
// A page is a route with a component. Redirects (an old address kept working)
// and the catch-all are not pages, and listing them would hand a crawler
// addresses that only bounce. Each address ends with a slash: the pages are
// pre-rendered into <page>/index.html, and GitHub Pages answers /page with a
// redirect to /page/.
import { readFileSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

// The same origin the pages name as canonical (src/app/page-meta.ts).
export const ORIGIN = 'https://plug.softwarity.dev';

export function pagesOf(routesSource) {
const found = [...routesSource.matchAll(/\bpath: '([^']*)'/g)];
return found
.filter((m, i) => routesSource.slice(m.index, found[i + 1]?.index).includes('loadComponent'))
.map((m) => m[1]);
}

export function sitemap(origin, pages) {
const urls = pages.map((p) => ` <url><loc>${origin}/${p ? p + '/' : ''}</loc></url>`).join('\n');
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`;
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
const docs = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const pages = pagesOf(readFileSync(resolve(docs, 'src/app/app.routes.ts'), 'utf8'));
writeFileSync(resolve(docs, 'src/root/sitemap.xml'), sitemap(ORIGIN, pages));
console.log(`sitemap.xml: ${pages.length} pages`);
}
34 changes: 34 additions & 0 deletions docs/scripts/gen-sitemap.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// The sitemap is read from the real routes file: what is proven is that the
// pages a reader can open are the ones a crawler is given, and nothing else.
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { ORIGIN, pagesOf, sitemap } from './gen-sitemap.mjs';

const here = dirname(fileURLToPath(import.meta.url));
const routes = readFileSync(resolve(here, '../src/app/app.routes.ts'), 'utf8');
const meta = readFileSync(resolve(here, '../src/app/page-meta.ts'), 'utf8');

describe('gen-sitemap', () => {
it('lists every page with a component, and no redirect or catch-all', () => {
const pages = pagesOf(routes);
expect(pages).toContain('');
expect(pages).toContain('kubernetes');
expect(pages).toContain('security');
expect(pages).not.toContain('agent');
expect(pages).not.toContain('**');
expect(pages.length).toBe((routes.match(/loadComponent/g) ?? []).length);
});

it('gives each page its address with a trailing slash, the home page bare', () => {
const xml = sitemap(ORIGIN, ['', 'cli']);
expect(xml).toContain('<loc>https://plug.softwarity.dev/</loc>');
expect(xml).toContain('<loc>https://plug.softwarity.dev/cli/</loc>');
expect(xml.startsWith('<?xml')).toBe(true);
});

it('names the same origin as the canonical links', () => {
expect(meta).toContain(`siteOrigin = '${ORIGIN}'`);
});
});
13 changes: 10 additions & 3 deletions docs/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, inject, viewChild } from '@angular/core';
import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, inject, PLATFORM_ID, viewChild } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { NavigationEnd, Router, RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
import { filter } from 'rxjs/operators';
import { MatIconModule, MatIconRegistry } from '@angular/material/icon';
import { PageMeta } from './page-meta';

interface DocLink {
path: string;
Expand Down Expand Up @@ -30,6 +32,10 @@ export class AppComponent {
const router = inject(Router);
// Use Material Symbols (loaded in index.html) as the default glyph set for every <mat-icon>.
iconRegistry.setDefaultFontSetClass('material-symbols-outlined');
inject(PageMeta).start();
// Focus and scrolling belong to a reader's browser, not to the build that
// renders the pages ahead of time.
if (!isPlatformBrowser(inject(PLATFORM_ID))) return;

// Move focus into the page after every navigation, and scroll it to the top.
//
Expand All @@ -51,14 +57,15 @@ export class AppComponent {
{ path: '/', label: 'About', icon: 'sync_alt' },
{ path: '/comparison', label: 'Compared to others', icon: 'compare_arrows' },
{ path: '/getting-started', label: 'Getting started', icon: 'rocket_launch' },
{ path: '/install', label: 'Install', icon: 'download' },
{ path: '/use-cases', label: 'Use cases', icon: 'lightbulb' },
{ path: '/cli', label: 'CLI reference', icon: 'terminal' },
{ path: '/build-tools', label: 'From your build tool', icon: 'integration_instructions' },
{ path: '/how-it-works', label: 'How it works', icon: 'account_tree' },
{ path: '/under-the-hood', label: 'Under the hood', icon: 'build' },
{ path: '/profiles', label: 'Profiles & versions', icon: 'settings' },
{ path: '/swarm', label: 'Swarm', icon: 'dns' },
{ path: '/kubernetes', label: 'Kubernetes', icon: 'hub' },
{ path: '/swarm', label: 'Docker, Compose & Swarm', icon: 'dns' },
{ path: '/kubernetes', label: 'Kubernetes & OpenShift', icon: 'hub' },
{ path: '/mcp', label: 'MCP for AI agents', icon: 'smart_toy' },
{ path: '/security', label: 'Security model', icon: 'shield' },
{ path: '/meerkat', label: 'Meerkat', icon: 'shield_person', logo: 'meerkat' },
Expand Down
8 changes: 8 additions & 0 deletions docs/src/app/app.config.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';

export const config: ApplicationConfig = mergeApplicationConfig(appConfig, {
providers: [provideServerRendering(withRoutes(serverRoutes))],
});
Loading
Loading