@franzzemen/brokenstock-shell 0.0.29 → 0.0.30

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.
@@ -12,6 +12,7 @@ import { Marked } from 'marked';
12
12
  import { gfmHeadingId } from 'marked-gfm-heading-id';
13
13
  import fm from 'front-matter';
14
14
  import { NgClass, CommonModule, DOCUMENT, DatePipe } from '@angular/common';
15
+ import { Title, Meta } from '@angular/platform-browser';
15
16
  import { z } from 'zod/v4';
16
17
  import * as i1 from '@angular/material/icon';
17
18
  import { MatIconModule } from '@angular/material/icon';
@@ -1236,19 +1237,38 @@ var faqAuthoringPanel = /*#__PURE__*/Object.freeze({
1236
1237
  FaqAuthoringPanel: FaqAuthoringPanel
1237
1238
  });
1238
1239
 
1240
+ /**
1241
+ * Canonical public base URL (no trailing slash) used for OpenGraph,
1242
+ * Twitter, JSON-LD canonical URLs, and sitemap entries on the
1243
+ * public FAQ pages. Default empty string — falls back to relative
1244
+ * `og:url` etc. when unset, which is fine for previewing but
1245
+ * suboptimal for production SEO. Consumer apps should override.
1246
+ */
1247
+ const FAQ_PUBLIC_SITE_BASE_URL = new InjectionToken('FAQ_PUBLIC_SITE_BASE_URL', { providedIn: 'root', factory: () => '' });
1248
+ const FAQ_PUBLIC_PUBLISHER = new InjectionToken('FAQ_PUBLIC_PUBLISHER', { providedIn: 'root', factory: () => ({ name: '' }) });
1249
+
1239
1250
  /**
1240
1251
  * Public, SEO-targeted FAQ index. Prerendered at build time on the
1241
1252
  * consumer side (e.g. /help in broken-stock). Lists every manifest
1242
1253
  * entry where `audience === 'public' && seoReady === true`, grouped
1243
1254
  * by category and ordered by `order`.
1255
+ *
1256
+ * SEO (E17): sets <title>, meta description, OG, and Twitter tags.
1257
+ * JSON-LD (E18): emits a schema.org CollectionPage referencing
1258
+ * each child entry as an ItemList.
1244
1259
  */
1245
1260
  class PublicFaqIndex {
1246
1261
  constructor() {
1247
1262
  this.loader = inject(FAQ_CONTENT_LOADER);
1263
+ this.title = inject(Title);
1264
+ this.meta = inject(Meta);
1265
+ this.siteBase = inject(FAQ_PUBLIC_SITE_BASE_URL);
1266
+ this.publisher = inject(FAQ_PUBLIC_PUBLISHER);
1248
1267
  this._entries = signal([], ...(ngDevMode ? [{ debugName: "_entries" }] : /* istanbul ignore next */ []));
1249
1268
  this.loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
1269
+ this.visibleEntries = computed(() => this._entries().filter((e) => e.audience === 'public' && e.seoReady), ...(ngDevMode ? [{ debugName: "visibleEntries" }] : /* istanbul ignore next */ []));
1250
1270
  this.groups = computed(() => {
1251
- const visible = this._entries().filter((e) => e.audience === 'public' && e.seoReady);
1271
+ const visible = this.visibleEntries();
1252
1272
  const byCategory = new Map();
1253
1273
  for (const entry of visible) {
1254
1274
  const list = byCategory.get(entry.category) ?? [];
@@ -1263,16 +1283,68 @@ class PublicFaqIndex {
1263
1283
  groups.sort((a, b) => a.category.localeCompare(b.category));
1264
1284
  return groups;
1265
1285
  }, ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
1286
+ this.jsonLd = computed(() => {
1287
+ const entries = this.visibleEntries();
1288
+ if (entries.length === 0)
1289
+ return null;
1290
+ const base = this.siteBase.replace(/\/+$/, '');
1291
+ const indexUrl = `${base}/help`;
1292
+ const ld = {
1293
+ '@context': 'https://schema.org',
1294
+ '@type': 'CollectionPage',
1295
+ name: 'Brokenstock Help',
1296
+ url: indexUrl,
1297
+ mainEntity: {
1298
+ '@type': 'ItemList',
1299
+ itemListElement: entries.map((entry, idx) => ({
1300
+ '@type': 'ListItem',
1301
+ position: idx + 1,
1302
+ url: `${base}/help/${entry.file}`,
1303
+ name: entry.title,
1304
+ })),
1305
+ },
1306
+ };
1307
+ if (this.publisher.name) {
1308
+ const pub = {
1309
+ '@type': 'Organization',
1310
+ name: this.publisher.name,
1311
+ };
1312
+ if (this.publisher.url)
1313
+ pub['url'] = this.publisher.url;
1314
+ if (this.publisher.logo)
1315
+ pub['logo'] = this.publisher.logo;
1316
+ ld['publisher'] = pub;
1317
+ }
1318
+ return serializeJsonLd$1(ld);
1319
+ }, ...(ngDevMode ? [{ debugName: "jsonLd" }] : /* istanbul ignore next */ []));
1266
1320
  }
1267
1321
  async ngOnInit() {
1268
1322
  try {
1269
1323
  const manifest = await this.loader.loadManifest();
1270
1324
  this._entries.set(manifest.entries);
1325
+ this.applyMeta();
1271
1326
  }
1272
1327
  finally {
1273
1328
  this.loading.set(false);
1274
1329
  }
1275
1330
  }
1331
+ applyMeta() {
1332
+ const base = this.siteBase.replace(/\/+$/, '');
1333
+ const url = `${base}/help`;
1334
+ const description = 'Documentation and walkthroughs for the Brokenstock trade-journal platform.';
1335
+ this.title.setTitle('Help — Brokenstock');
1336
+ this.meta.updateTag({ name: 'description', content: description });
1337
+ this.meta.updateTag({ property: 'og:type', content: 'website' });
1338
+ this.meta.updateTag({ property: 'og:title', content: 'Help — Brokenstock' });
1339
+ this.meta.updateTag({ property: 'og:description', content: description });
1340
+ this.meta.updateTag({ property: 'og:url', content: url });
1341
+ if (this.publisher.name) {
1342
+ this.meta.updateTag({ property: 'og:site_name', content: this.publisher.name });
1343
+ }
1344
+ this.meta.updateTag({ name: 'twitter:card', content: 'summary' });
1345
+ this.meta.updateTag({ name: 'twitter:title', content: 'Help — Brokenstock' });
1346
+ this.meta.updateTag({ name: 'twitter:description', content: description });
1347
+ }
1276
1348
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: PublicFaqIndex, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1277
1349
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: PublicFaqIndex, isStandalone: true, selector: "public-faq-index", ngImport: i0, template: `
1278
1350
  <article class="public-faq-index">
@@ -1301,6 +1373,9 @@ class PublicFaqIndex {
1301
1373
  </section>
1302
1374
  }
1303
1375
  }
1376
+ @if (jsonLd(); as ld) {
1377
+ <script type="application/ld+json">{{ ld }}</script>
1378
+ }
1304
1379
  </article>
1305
1380
  `, isInline: true, styles: [":host{display:block}.public-faq-index{max-width:860px;margin:0 auto;padding:2rem 1.25rem 4rem;font-family:system-ui,-apple-system,Segoe UI,sans-serif;line-height:1.55}header h1{font-size:2.25rem;margin:0 0 .5rem}.lead{color:#555;margin:0 0 2rem;font-size:1.1rem}h2{font-size:1.35rem;margin:1.75rem 0 .5rem;border-bottom:1px solid #eee;padding-bottom:.25rem}ul{list-style:none;padding:0;margin:0}li{padding:.4rem 0}a{color:#1565c0;text-decoration:none;font-weight:500}a:hover{text-decoration:underline}.desc{color:#666;font-weight:400}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }] }); }
1306
1381
  }
@@ -1333,9 +1408,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
1333
1408
  </section>
1334
1409
  }
1335
1410
  }
1411
+ @if (jsonLd(); as ld) {
1412
+ <script type="application/ld+json">{{ ld }}</script>
1413
+ }
1336
1414
  </article>
1337
1415
  `, styles: [":host{display:block}.public-faq-index{max-width:860px;margin:0 auto;padding:2rem 1.25rem 4rem;font-family:system-ui,-apple-system,Segoe UI,sans-serif;line-height:1.55}header h1{font-size:2.25rem;margin:0 0 .5rem}.lead{color:#555;margin:0 0 2rem;font-size:1.1rem}h2{font-size:1.35rem;margin:1.75rem 0 .5rem;border-bottom:1px solid #eee;padding-bottom:.25rem}ul{list-style:none;padding:0;margin:0}li{padding:.4rem 0}a{color:#1565c0;text-decoration:none;font-weight:500}a:hover{text-decoration:underline}.desc{color:#666;font-weight:400}\n"] }]
1338
1416
  }] });
1417
+ function serializeJsonLd$1(obj) {
1418
+ return JSON.stringify(obj).replace(/</g, '\\u003c');
1419
+ }
1339
1420
 
1340
1421
  /**
1341
1422
  * Public, SEO-targeted FAQ page. Prerendered at build time per
@@ -1343,12 +1424,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
1343
1424
  *
1344
1425
  * The route is `help/**` so multi-segment file paths work; this
1345
1426
  * component reads the wildcard path off `ActivatedRoute.url`.
1427
+ *
1428
+ * SEO (E17): sets <title>, meta description, OpenGraph and Twitter
1429
+ * Card tags using Angular's Title + Meta services (SSR-compatible).
1430
+ * JSON-LD (E18): emits a schema.org Article (or BlogPosting) object
1431
+ * via an inline <script type="application/ld+json"> element. The
1432
+ * raw JSON has `<` escaped to `<` so a stray `</script>` inside
1433
+ * data won't break the surrounding tag.
1346
1434
  */
1347
1435
  class PublicFaqPage {
1348
1436
  constructor() {
1349
1437
  this.route = inject(ActivatedRoute);
1350
1438
  this.loader = inject(FAQ_CONTENT_LOADER);
1351
1439
  this.renderer = inject(FaqMarkdownRendererService);
1440
+ this.title = inject(Title);
1441
+ this.meta = inject(Meta);
1442
+ this.siteBase = inject(FAQ_PUBLIC_SITE_BASE_URL);
1443
+ this.publisher = inject(FAQ_PUBLIC_PUBLISHER);
1352
1444
  this.entry = signal(null, ...(ngDevMode ? [{ debugName: "entry" }] : /* istanbul ignore next */ []));
1353
1445
  this.body = signal('', ...(ngDevMode ? [{ debugName: "body" }] : /* istanbul ignore next */ []));
1354
1446
  this.loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
@@ -1364,31 +1456,88 @@ class PublicFaqPage {
1364
1456
  : undefined,
1365
1457
  });
1366
1458
  }, ...(ngDevMode ? [{ debugName: "renderedHtml" }] : /* istanbul ignore next */ []));
1459
+ this.jsonLd = computed(() => {
1460
+ const entry = this.entry();
1461
+ if (!entry)
1462
+ return null;
1463
+ const url = this.canonicalUrl(entry);
1464
+ const ld = {
1465
+ '@context': 'https://schema.org',
1466
+ '@type': 'Article',
1467
+ headline: entry.title,
1468
+ url,
1469
+ mainEntityOfPage: url,
1470
+ };
1471
+ if (entry.description)
1472
+ ld['description'] = entry.description;
1473
+ if (this.publisher.name) {
1474
+ const pub = {
1475
+ '@type': 'Organization',
1476
+ name: this.publisher.name,
1477
+ };
1478
+ if (this.publisher.url)
1479
+ pub['url'] = this.publisher.url;
1480
+ if (this.publisher.logo)
1481
+ pub['logo'] = this.publisher.logo;
1482
+ ld['publisher'] = pub;
1483
+ ld['author'] = pub;
1484
+ }
1485
+ return serializeJsonLd(ld);
1486
+ }, ...(ngDevMode ? [{ debugName: "jsonLd" }] : /* istanbul ignore next */ []));
1367
1487
  }
1368
1488
  async ngOnInit() {
1369
1489
  try {
1370
1490
  const segments = this.route.snapshot.url.map((s) => s.path);
1371
1491
  const filePath = segments.join('/');
1372
1492
  if (!filePath) {
1493
+ this.applyNotFoundMeta();
1373
1494
  return;
1374
1495
  }
1375
1496
  const manifest = await this.loader.loadManifest();
1376
- const match = manifest.entries.find((e) => e.file === filePath &&
1377
- e.audience === 'public' &&
1378
- e.seoReady);
1379
- if (!match)
1497
+ const match = manifest.entries.find((e) => e.file === filePath && e.audience === 'public' && e.seoReady);
1498
+ if (!match) {
1499
+ this.applyNotFoundMeta();
1380
1500
  return;
1501
+ }
1381
1502
  const content = await this.loader.loadContent(match.key);
1382
- if (content === null)
1503
+ if (content === null) {
1504
+ this.applyNotFoundMeta();
1383
1505
  return;
1506
+ }
1384
1507
  const parsed = this.renderer.parse(content);
1385
1508
  this.entry.set(match);
1386
1509
  this.body.set(parsed.body);
1510
+ this.applyMeta(match);
1387
1511
  }
1388
1512
  finally {
1389
1513
  this.loading.set(false);
1390
1514
  }
1391
1515
  }
1516
+ applyMeta(entry) {
1517
+ const url = this.canonicalUrl(entry);
1518
+ const description = entry.description ?? '';
1519
+ const title = `${entry.title} — Help`;
1520
+ this.title.setTitle(title);
1521
+ this.meta.updateTag({ name: 'description', content: description });
1522
+ this.meta.updateTag({ property: 'og:type', content: 'article' });
1523
+ this.meta.updateTag({ property: 'og:title', content: entry.title });
1524
+ this.meta.updateTag({ property: 'og:description', content: description });
1525
+ this.meta.updateTag({ property: 'og:url', content: url });
1526
+ if (this.publisher.name) {
1527
+ this.meta.updateTag({ property: 'og:site_name', content: this.publisher.name });
1528
+ }
1529
+ this.meta.updateTag({ name: 'twitter:card', content: 'summary' });
1530
+ this.meta.updateTag({ name: 'twitter:title', content: entry.title });
1531
+ this.meta.updateTag({ name: 'twitter:description', content: description });
1532
+ }
1533
+ applyNotFoundMeta() {
1534
+ this.title.setTitle('Page not found — Help');
1535
+ this.meta.updateTag({ name: 'robots', content: 'noindex' });
1536
+ }
1537
+ canonicalUrl(entry) {
1538
+ const base = this.siteBase.replace(/\/+$/, '');
1539
+ return `${base}/help/${entry.file}`;
1540
+ }
1392
1541
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: PublicFaqPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1393
1542
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: PublicFaqPage, isStandalone: true, selector: "public-faq-page", ngImport: i0, template: `
1394
1543
  <article class="public-faq-page">
@@ -1410,6 +1559,9 @@ class PublicFaqPage {
1410
1559
  }
1411
1560
  </header>
1412
1561
  <div class="body" [innerHTML]="renderedHtml()"></div>
1562
+ @if (jsonLd(); as ld) {
1563
+ <script type="application/ld+json">{{ ld }}</script>
1564
+ }
1413
1565
  }
1414
1566
  </article>
1415
1567
  `, isInline: true, styles: [":host{display:block}.public-faq-page{max-width:860px;margin:0 auto;padding:2rem 1.25rem 4rem;font-family:system-ui,-apple-system,Segoe UI,sans-serif;line-height:1.6}.crumbs{margin-bottom:1.5rem;font-size:.9rem}.crumbs a{color:#1565c0;text-decoration:none}.crumbs a:hover{text-decoration:underline}header h1{font-size:2.1rem;margin:0 0 .5rem}.lead{color:#555;font-size:1.1rem;margin:0 0 1.5rem}.body :is(h1,h2,h3,h4){margin-top:2rem}.body a{color:#1565c0}.body img{max-width:100%;height:auto}.body pre{background:#f6f8fa;padding:.75rem 1rem;border-radius:6px;overflow-x:auto}.body code{background:#f6f8fa;padding:.1rem .3rem;border-radius:3px;font-size:.9em}.body pre code{background:transparent;padding:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }] }); }
@@ -1436,10 +1588,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
1436
1588
  }
1437
1589
  </header>
1438
1590
  <div class="body" [innerHTML]="renderedHtml()"></div>
1591
+ @if (jsonLd(); as ld) {
1592
+ <script type="application/ld+json">{{ ld }}</script>
1593
+ }
1439
1594
  }
1440
1595
  </article>
1441
1596
  `, styles: [":host{display:block}.public-faq-page{max-width:860px;margin:0 auto;padding:2rem 1.25rem 4rem;font-family:system-ui,-apple-system,Segoe UI,sans-serif;line-height:1.6}.crumbs{margin-bottom:1.5rem;font-size:.9rem}.crumbs a{color:#1565c0;text-decoration:none}.crumbs a:hover{text-decoration:underline}header h1{font-size:2.1rem;margin:0 0 .5rem}.lead{color:#555;font-size:1.1rem;margin:0 0 1.5rem}.body :is(h1,h2,h3,h4){margin-top:2rem}.body a{color:#1565c0}.body img{max-width:100%;height:auto}.body pre{background:#f6f8fa;padding:.75rem 1rem;border-radius:6px;overflow-x:auto}.body code{background:#f6f8fa;padding:.1rem .3rem;border-radius:3px;font-size:.9em}.body pre code{background:transparent;padding:0}\n"] }]
1442
1597
  }] });
1598
+ /**
1599
+ * Serializes a JSON-LD object for safe inclusion in a
1600
+ * `<script type="application/ld+json">` element. Escapes `<` so a
1601
+ * stray `</script>` inside any string field cannot break out of the
1602
+ * script tag. (Browsers and crawlers parse the resulting `<`
1603
+ * back to `<` per JSON spec.)
1604
+ */
1605
+ function serializeJsonLd(obj) {
1606
+ return JSON.stringify(obj).replace(/</g, '\\u003c');
1607
+ }
1443
1608
 
1444
1609
  class ThemeService {
1445
1610
  constructor() {
@@ -2253,5 +2418,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
2253
2418
  * Generated bundle index. Do not edit.
2254
2419
  */
2255
2420
 
2256
- export { AuthenticationService, CLEAR_USAGE, CommandRegistryService, CommandService, CommandTerminal, ConfirmDialog, FAQ_CONTENT_LOADER, FAQ_DEV_EDITOR, FAQ_DEV_MODE, FAQ_DEV_WORKSPACE_ROOT, FaqAuthoringPanel, FaqHistoryService, FaqMarkdownRendererService, FaqModal, FaqService, HELP_USAGE, HttpFaqContentLoader, KeyboardShortcutService, Login, MainContent, PublicFaqIndex, PublicFaqPage, RegexValidatorDirective, SHELL_APP_NAME, SHELL_AUTH_URLS, SHELL_COMMANDS, SHELL_MENU_ITEMS, SHELL_POST_LOGIN_ROUTE, Shell, SideMenu, StatusBar, TerminalFocusService, ThemeService, ThinkingService, audienceAllowed, authGuard, authInterceptor, buildFallbackChain, guestGuard, provideShellConfig, tokenize };
2421
+ export { AuthenticationService, CLEAR_USAGE, CommandRegistryService, CommandService, CommandTerminal, ConfirmDialog, FAQ_CONTENT_LOADER, FAQ_DEV_EDITOR, FAQ_DEV_MODE, FAQ_DEV_WORKSPACE_ROOT, FAQ_PUBLIC_PUBLISHER, FAQ_PUBLIC_SITE_BASE_URL, FaqAuthoringPanel, FaqHistoryService, FaqMarkdownRendererService, FaqModal, FaqService, HELP_USAGE, HttpFaqContentLoader, KeyboardShortcutService, Login, MainContent, PublicFaqIndex, PublicFaqPage, RegexValidatorDirective, SHELL_APP_NAME, SHELL_AUTH_URLS, SHELL_COMMANDS, SHELL_MENU_ITEMS, SHELL_POST_LOGIN_ROUTE, Shell, SideMenu, StatusBar, TerminalFocusService, ThemeService, ThinkingService, audienceAllowed, authGuard, authInterceptor, buildFallbackChain, guestGuard, provideShellConfig, tokenize };
2257
2422
  //# sourceMappingURL=franzzemen-brokenstock-shell.mjs.map