@franzzemen/brokenstock-shell 0.0.29 → 0.0.31

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';
@@ -883,9 +884,10 @@ class FaqModal {
883
884
  const entry = this.faq.activeEntry();
884
885
  if (!content)
885
886
  return '';
886
- return this.renderer.render(content, {
887
+ const parsed = this.renderer.parse(content);
888
+ return this.renderer.render(parsed.body, {
887
889
  context: 'modal',
888
- imagePrefix: entry?.imagePrefix,
890
+ imagePrefix: entry?.imagePrefix ?? parsed.frontmatter['image-prefix'],
889
891
  });
890
892
  }, ...(ngDevMode ? [{ debugName: "renderedHtml" }] : /* istanbul ignore next */ []));
891
893
  this.toc = computed(() => {
@@ -1236,19 +1238,38 @@ var faqAuthoringPanel = /*#__PURE__*/Object.freeze({
1236
1238
  FaqAuthoringPanel: FaqAuthoringPanel
1237
1239
  });
1238
1240
 
1241
+ /**
1242
+ * Canonical public base URL (no trailing slash) used for OpenGraph,
1243
+ * Twitter, JSON-LD canonical URLs, and sitemap entries on the
1244
+ * public FAQ pages. Default empty string — falls back to relative
1245
+ * `og:url` etc. when unset, which is fine for previewing but
1246
+ * suboptimal for production SEO. Consumer apps should override.
1247
+ */
1248
+ const FAQ_PUBLIC_SITE_BASE_URL = new InjectionToken('FAQ_PUBLIC_SITE_BASE_URL', { providedIn: 'root', factory: () => '' });
1249
+ const FAQ_PUBLIC_PUBLISHER = new InjectionToken('FAQ_PUBLIC_PUBLISHER', { providedIn: 'root', factory: () => ({ name: '' }) });
1250
+
1239
1251
  /**
1240
1252
  * Public, SEO-targeted FAQ index. Prerendered at build time on the
1241
1253
  * consumer side (e.g. /help in broken-stock). Lists every manifest
1242
1254
  * entry where `audience === 'public' && seoReady === true`, grouped
1243
1255
  * by category and ordered by `order`.
1256
+ *
1257
+ * SEO (E17): sets <title>, meta description, OG, and Twitter tags.
1258
+ * JSON-LD (E18): emits a schema.org CollectionPage referencing
1259
+ * each child entry as an ItemList.
1244
1260
  */
1245
1261
  class PublicFaqIndex {
1246
1262
  constructor() {
1247
1263
  this.loader = inject(FAQ_CONTENT_LOADER);
1264
+ this.title = inject(Title);
1265
+ this.meta = inject(Meta);
1266
+ this.siteBase = inject(FAQ_PUBLIC_SITE_BASE_URL);
1267
+ this.publisher = inject(FAQ_PUBLIC_PUBLISHER);
1248
1268
  this._entries = signal([], ...(ngDevMode ? [{ debugName: "_entries" }] : /* istanbul ignore next */ []));
1249
1269
  this.loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
1270
+ this.visibleEntries = computed(() => this._entries().filter((e) => e.audience === 'public' && e.seoReady), ...(ngDevMode ? [{ debugName: "visibleEntries" }] : /* istanbul ignore next */ []));
1250
1271
  this.groups = computed(() => {
1251
- const visible = this._entries().filter((e) => e.audience === 'public' && e.seoReady);
1272
+ const visible = this.visibleEntries();
1252
1273
  const byCategory = new Map();
1253
1274
  for (const entry of visible) {
1254
1275
  const list = byCategory.get(entry.category) ?? [];
@@ -1263,16 +1284,68 @@ class PublicFaqIndex {
1263
1284
  groups.sort((a, b) => a.category.localeCompare(b.category));
1264
1285
  return groups;
1265
1286
  }, ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
1287
+ this.jsonLd = computed(() => {
1288
+ const entries = this.visibleEntries();
1289
+ if (entries.length === 0)
1290
+ return null;
1291
+ const base = this.siteBase.replace(/\/+$/, '');
1292
+ const indexUrl = `${base}/help`;
1293
+ const ld = {
1294
+ '@context': 'https://schema.org',
1295
+ '@type': 'CollectionPage',
1296
+ name: 'Brokenstock Help',
1297
+ url: indexUrl,
1298
+ mainEntity: {
1299
+ '@type': 'ItemList',
1300
+ itemListElement: entries.map((entry, idx) => ({
1301
+ '@type': 'ListItem',
1302
+ position: idx + 1,
1303
+ url: `${base}/help/${entry.file}`,
1304
+ name: entry.title,
1305
+ })),
1306
+ },
1307
+ };
1308
+ if (this.publisher.name) {
1309
+ const pub = {
1310
+ '@type': 'Organization',
1311
+ name: this.publisher.name,
1312
+ };
1313
+ if (this.publisher.url)
1314
+ pub['url'] = this.publisher.url;
1315
+ if (this.publisher.logo)
1316
+ pub['logo'] = this.publisher.logo;
1317
+ ld['publisher'] = pub;
1318
+ }
1319
+ return serializeJsonLd$1(ld);
1320
+ }, ...(ngDevMode ? [{ debugName: "jsonLd" }] : /* istanbul ignore next */ []));
1266
1321
  }
1267
1322
  async ngOnInit() {
1268
1323
  try {
1269
1324
  const manifest = await this.loader.loadManifest();
1270
1325
  this._entries.set(manifest.entries);
1326
+ this.applyMeta();
1271
1327
  }
1272
1328
  finally {
1273
1329
  this.loading.set(false);
1274
1330
  }
1275
1331
  }
1332
+ applyMeta() {
1333
+ const base = this.siteBase.replace(/\/+$/, '');
1334
+ const url = `${base}/help`;
1335
+ const description = 'Documentation and walkthroughs for the Brokenstock trade-journal platform.';
1336
+ this.title.setTitle('Help — Brokenstock');
1337
+ this.meta.updateTag({ name: 'description', content: description });
1338
+ this.meta.updateTag({ property: 'og:type', content: 'website' });
1339
+ this.meta.updateTag({ property: 'og:title', content: 'Help — Brokenstock' });
1340
+ this.meta.updateTag({ property: 'og:description', content: description });
1341
+ this.meta.updateTag({ property: 'og:url', content: url });
1342
+ if (this.publisher.name) {
1343
+ this.meta.updateTag({ property: 'og:site_name', content: this.publisher.name });
1344
+ }
1345
+ this.meta.updateTag({ name: 'twitter:card', content: 'summary' });
1346
+ this.meta.updateTag({ name: 'twitter:title', content: 'Help — Brokenstock' });
1347
+ this.meta.updateTag({ name: 'twitter:description', content: description });
1348
+ }
1276
1349
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: PublicFaqIndex, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1277
1350
  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
1351
  <article class="public-faq-index">
@@ -1301,6 +1374,9 @@ class PublicFaqIndex {
1301
1374
  </section>
1302
1375
  }
1303
1376
  }
1377
+ @if (jsonLd(); as ld) {
1378
+ <script type="application/ld+json">{{ ld }}</script>
1379
+ }
1304
1380
  </article>
1305
1381
  `, 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
1382
  }
@@ -1333,9 +1409,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
1333
1409
  </section>
1334
1410
  }
1335
1411
  }
1412
+ @if (jsonLd(); as ld) {
1413
+ <script type="application/ld+json">{{ ld }}</script>
1414
+ }
1336
1415
  </article>
1337
1416
  `, 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
1417
  }] });
1418
+ function serializeJsonLd$1(obj) {
1419
+ return JSON.stringify(obj).replace(/</g, '\\u003c');
1420
+ }
1339
1421
 
1340
1422
  /**
1341
1423
  * Public, SEO-targeted FAQ page. Prerendered at build time per
@@ -1343,12 +1425,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
1343
1425
  *
1344
1426
  * The route is `help/**` so multi-segment file paths work; this
1345
1427
  * component reads the wildcard path off `ActivatedRoute.url`.
1428
+ *
1429
+ * SEO (E17): sets <title>, meta description, OpenGraph and Twitter
1430
+ * Card tags using Angular's Title + Meta services (SSR-compatible).
1431
+ * JSON-LD (E18): emits a schema.org Article (or BlogPosting) object
1432
+ * via an inline <script type="application/ld+json"> element. The
1433
+ * raw JSON has `<` escaped to `<` so a stray `</script>` inside
1434
+ * data won't break the surrounding tag.
1346
1435
  */
1347
1436
  class PublicFaqPage {
1348
1437
  constructor() {
1349
1438
  this.route = inject(ActivatedRoute);
1350
1439
  this.loader = inject(FAQ_CONTENT_LOADER);
1351
1440
  this.renderer = inject(FaqMarkdownRendererService);
1441
+ this.title = inject(Title);
1442
+ this.meta = inject(Meta);
1443
+ this.siteBase = inject(FAQ_PUBLIC_SITE_BASE_URL);
1444
+ this.publisher = inject(FAQ_PUBLIC_PUBLISHER);
1352
1445
  this.entry = signal(null, ...(ngDevMode ? [{ debugName: "entry" }] : /* istanbul ignore next */ []));
1353
1446
  this.body = signal('', ...(ngDevMode ? [{ debugName: "body" }] : /* istanbul ignore next */ []));
1354
1447
  this.loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
@@ -1364,31 +1457,88 @@ class PublicFaqPage {
1364
1457
  : undefined,
1365
1458
  });
1366
1459
  }, ...(ngDevMode ? [{ debugName: "renderedHtml" }] : /* istanbul ignore next */ []));
1460
+ this.jsonLd = computed(() => {
1461
+ const entry = this.entry();
1462
+ if (!entry)
1463
+ return null;
1464
+ const url = this.canonicalUrl(entry);
1465
+ const ld = {
1466
+ '@context': 'https://schema.org',
1467
+ '@type': 'Article',
1468
+ headline: entry.title,
1469
+ url,
1470
+ mainEntityOfPage: url,
1471
+ };
1472
+ if (entry.description)
1473
+ ld['description'] = entry.description;
1474
+ if (this.publisher.name) {
1475
+ const pub = {
1476
+ '@type': 'Organization',
1477
+ name: this.publisher.name,
1478
+ };
1479
+ if (this.publisher.url)
1480
+ pub['url'] = this.publisher.url;
1481
+ if (this.publisher.logo)
1482
+ pub['logo'] = this.publisher.logo;
1483
+ ld['publisher'] = pub;
1484
+ ld['author'] = pub;
1485
+ }
1486
+ return serializeJsonLd(ld);
1487
+ }, ...(ngDevMode ? [{ debugName: "jsonLd" }] : /* istanbul ignore next */ []));
1367
1488
  }
1368
1489
  async ngOnInit() {
1369
1490
  try {
1370
1491
  const segments = this.route.snapshot.url.map((s) => s.path);
1371
1492
  const filePath = segments.join('/');
1372
1493
  if (!filePath) {
1494
+ this.applyNotFoundMeta();
1373
1495
  return;
1374
1496
  }
1375
1497
  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)
1498
+ const match = manifest.entries.find((e) => e.file === filePath && e.audience === 'public' && e.seoReady);
1499
+ if (!match) {
1500
+ this.applyNotFoundMeta();
1380
1501
  return;
1502
+ }
1381
1503
  const content = await this.loader.loadContent(match.key);
1382
- if (content === null)
1504
+ if (content === null) {
1505
+ this.applyNotFoundMeta();
1383
1506
  return;
1507
+ }
1384
1508
  const parsed = this.renderer.parse(content);
1385
1509
  this.entry.set(match);
1386
1510
  this.body.set(parsed.body);
1511
+ this.applyMeta(match);
1387
1512
  }
1388
1513
  finally {
1389
1514
  this.loading.set(false);
1390
1515
  }
1391
1516
  }
1517
+ applyMeta(entry) {
1518
+ const url = this.canonicalUrl(entry);
1519
+ const description = entry.description ?? '';
1520
+ const title = `${entry.title} — Help`;
1521
+ this.title.setTitle(title);
1522
+ this.meta.updateTag({ name: 'description', content: description });
1523
+ this.meta.updateTag({ property: 'og:type', content: 'article' });
1524
+ this.meta.updateTag({ property: 'og:title', content: entry.title });
1525
+ this.meta.updateTag({ property: 'og:description', content: description });
1526
+ this.meta.updateTag({ property: 'og:url', content: url });
1527
+ if (this.publisher.name) {
1528
+ this.meta.updateTag({ property: 'og:site_name', content: this.publisher.name });
1529
+ }
1530
+ this.meta.updateTag({ name: 'twitter:card', content: 'summary' });
1531
+ this.meta.updateTag({ name: 'twitter:title', content: entry.title });
1532
+ this.meta.updateTag({ name: 'twitter:description', content: description });
1533
+ }
1534
+ applyNotFoundMeta() {
1535
+ this.title.setTitle('Page not found — Help');
1536
+ this.meta.updateTag({ name: 'robots', content: 'noindex' });
1537
+ }
1538
+ canonicalUrl(entry) {
1539
+ const base = this.siteBase.replace(/\/+$/, '');
1540
+ return `${base}/help/${entry.file}`;
1541
+ }
1392
1542
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: PublicFaqPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1393
1543
  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
1544
  <article class="public-faq-page">
@@ -1410,6 +1560,9 @@ class PublicFaqPage {
1410
1560
  }
1411
1561
  </header>
1412
1562
  <div class="body" [innerHTML]="renderedHtml()"></div>
1563
+ @if (jsonLd(); as ld) {
1564
+ <script type="application/ld+json">{{ ld }}</script>
1565
+ }
1413
1566
  }
1414
1567
  </article>
1415
1568
  `, 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 +1589,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
1436
1589
  }
1437
1590
  </header>
1438
1591
  <div class="body" [innerHTML]="renderedHtml()"></div>
1592
+ @if (jsonLd(); as ld) {
1593
+ <script type="application/ld+json">{{ ld }}</script>
1594
+ }
1439
1595
  }
1440
1596
  </article>
1441
1597
  `, 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
1598
  }] });
1599
+ /**
1600
+ * Serializes a JSON-LD object for safe inclusion in a
1601
+ * `<script type="application/ld+json">` element. Escapes `<` so a
1602
+ * stray `</script>` inside any string field cannot break out of the
1603
+ * script tag. (Browsers and crawlers parse the resulting `<`
1604
+ * back to `<` per JSON spec.)
1605
+ */
1606
+ function serializeJsonLd(obj) {
1607
+ return JSON.stringify(obj).replace(/</g, '\\u003c');
1608
+ }
1443
1609
 
1444
1610
  class ThemeService {
1445
1611
  constructor() {
@@ -2253,5 +2419,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
2253
2419
  * Generated bundle index. Do not edit.
2254
2420
  */
2255
2421
 
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 };
2422
+ 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
2423
  //# sourceMappingURL=franzzemen-brokenstock-shell.mjs.map