@shevky/core 0.0.4 → 0.0.6

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.
@@ -517,7 +517,9 @@ export class RenderEngine {
517
517
  return;
518
518
  }
519
519
 
520
- const configKeys = Object.keys(collectionsConfig);
520
+ const configKeys = Object.keys(collectionsConfig).filter(
521
+ (key) => key !== "includeContentFile",
522
+ );
521
523
  for (const configKey of configKeys) {
522
524
  const config = collectionsConfig[configKey];
523
525
  if (!config || typeof config !== "object") {
@@ -40,6 +40,10 @@ export class ContentFile {
40
40
  return this._header.status;
41
41
  }
42
42
 
43
+ get fragment() {
44
+ return this._header.fragment;
45
+ }
46
+
43
47
  get id() {
44
48
  return this._header.id;
45
49
  }
@@ -167,4 +171,16 @@ export class ContentFile {
167
171
  get isPostTemplate() {
168
172
  return this.template === "post";
169
173
  }
174
+
175
+ toObject() {
176
+ return {
177
+ header: this._header.raw,
178
+ body: {
179
+ content: this.content,
180
+ },
181
+ content: this.content,
182
+ isValid: this.isValid,
183
+ sourcePath: this.sourcePath,
184
+ };
185
+ }
170
186
  }
@@ -18,6 +18,10 @@ export class ContentHeader {
18
18
  : "";
19
19
  }
20
20
 
21
+ get fragment() {
22
+ return _fmt.boolean(this._frontMatter.fragment);
23
+ }
24
+
21
25
  get id() {
22
26
  return typeof this._frontMatter.id === "string"
23
27
  ? this._frontMatter.id.trim()
@@ -12,6 +12,10 @@ export class ContentSummary {
12
12
  return this._file.id;
13
13
  }
14
14
 
15
+ get fragment() {
16
+ return this._file.fragment;
17
+ }
18
+
15
19
  get title() {
16
20
  return this._file.title;
17
21
  }
@@ -67,6 +71,7 @@ export class ContentSummary {
67
71
  toObject() {
68
72
  return {
69
73
  id: this.id,
74
+ fragment: this.fragment,
70
75
  title: this.title,
71
76
  slug: this.slug,
72
77
  lang: this.lang,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shevky/core",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "A minimal, dependency-light static site generator.",
5
5
  "type": "module",
6
6
  "main": "shevky.js",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "homepage": "https://github.com/shevky/core#readme",
38
38
  "dependencies": {
39
- "@shevky/base": "^0.0.2",
39
+ "@shevky/base": "^0.0.3",
40
40
  "@types/node": "^20.11.30",
41
41
  "@types/mustache": "^4.2.6",
42
42
  "@types/html-minifier-terser": "^7.0.2",
@@ -1,4 +1,4 @@
1
- import { io as _io } from "@shevky/base";
1
+ import { io as _io, config as _cfg } from "@shevky/base";
2
2
  import matter from "gray-matter";
3
3
 
4
4
  import { ContentFile } from "../lib/contentFile.js";
@@ -202,6 +202,9 @@ export class ContentRegistry {
202
202
  return this.#_collectionsCache;
203
203
  }
204
204
 
205
+ const includeContentFile = Boolean(
206
+ _cfg?.content?.collections?.includeContentFile,
207
+ );
205
208
  /** @type {import("../types/index.d.ts").CollectionsByLang} */
206
209
  const pagesByLang = {};
207
210
  const contentFiles = this.files;
@@ -211,8 +214,10 @@ export class ContentRegistry {
211
214
  }
212
215
 
213
216
  const contentSummary = new ContentSummary(file);
217
+ const summaryBase = contentSummary.toObject();
214
218
  const summary = /** @type {import("../types/index.d.ts").CollectionEntry} */ ({
215
- ...contentSummary.toObject(),
219
+ ...summaryBase,
220
+ ...(includeContentFile ? file.toObject() : {}),
216
221
  canonical: this.#_metaEngine.buildContentUrl(
217
222
  file.canonical,
218
223
  file.lang,
package/scripts/build.js CHANGED
@@ -48,6 +48,7 @@ const TEMPLATES_DIR = _prj.templatesDir;
48
48
  const ASSETS_DIR = _prj.assetsDir;
49
49
  const SITE_CONFIG_PATH = _prj.siteConfig;
50
50
  const I18N_CONFIG_PATH = _prj.i18nConfig;
51
+ const FRAGMENTS_DIR = "fragments";
51
52
 
52
53
  const pluginRegistry = new PluginRegistry();
53
54
  const templateRegistry = new TemplateRegistry();
@@ -461,6 +462,100 @@ async function renderContentTemplate(
461
462
  listingOverride,
462
463
  ) {
463
464
  const template = templateRegistry.getTemplate(TYPE_TEMPLATE, templateName);
465
+ const {
466
+ normalizedFront,
467
+ listing,
468
+ collectionFlags,
469
+ site,
470
+ languageFlags,
471
+ resolvedDictionary,
472
+ } = buildContentRenderContext(front, lang, dictionary, listingOverride);
473
+
474
+ return Mustache.render(
475
+ template.content,
476
+ {
477
+ content: { html: decorateHtml(contentHtml, templateName) },
478
+ front: normalizedFront,
479
+ lang,
480
+ listing,
481
+ site,
482
+ locale: languageFlags.locale,
483
+ isEnglish: languageFlags.isEnglish,
484
+ isTurkish: languageFlags.isTurkish,
485
+ i18n: resolvedDictionary,
486
+ ...collectionFlags,
487
+ },
488
+ {
489
+ ...templateRegistry.getFiles(TYPE_PARTIAL),
490
+ ...templateRegistry.getFiles(TYPE_COMPONENT),
491
+ },
492
+ );
493
+ }
494
+
495
+ /**
496
+ * @param {string} contentHtml
497
+ * @param {FrontMatter} front
498
+ * @param {string} lang
499
+ * @param {Record<string, any>} dictionary
500
+ * @param {string} [templateName]
501
+ * @param {any} [listingOverride]
502
+ */
503
+ async function renderFragmentTemplate(
504
+ contentHtml,
505
+ front,
506
+ lang,
507
+ dictionary,
508
+ templateName,
509
+ listingOverride,
510
+ ) {
511
+ const template =
512
+ typeof templateName === "string" && templateName.trim().length > 0
513
+ ? templateRegistry.getTemplate(TYPE_TEMPLATE, templateName.trim())
514
+ : null;
515
+ if (templateName && !template) {
516
+ _log.warn(`[build] Fragment template not found: ${templateName}`);
517
+ }
518
+ const {
519
+ normalizedFront,
520
+ listing,
521
+ collectionFlags,
522
+ site,
523
+ languageFlags,
524
+ resolvedDictionary,
525
+ } = buildContentRenderContext(front, lang, dictionary, listingOverride);
526
+ const decorated = decorateHtml(contentHtml, templateName ?? "");
527
+ const templateContent = template?.content ?? "{{{content.html}}}";
528
+
529
+ return Mustache.render(
530
+ templateContent,
531
+ {
532
+ content: { html: decorated },
533
+ contentHtml: decorated,
534
+ contentObject: { html: decorated },
535
+ front: normalizedFront,
536
+ lang,
537
+ listing,
538
+ site,
539
+ locale: languageFlags.locale,
540
+ isEnglish: languageFlags.isEnglish,
541
+ isTurkish: languageFlags.isTurkish,
542
+ i18n: resolvedDictionary,
543
+ ...collectionFlags,
544
+ },
545
+ {
546
+ ...templateRegistry.getFiles(TYPE_PARTIAL),
547
+ ...templateRegistry.getFiles(TYPE_COMPONENT),
548
+ },
549
+ );
550
+ }
551
+
552
+ /**
553
+ * @param {FrontMatter} front
554
+ * @param {string} lang
555
+ * @param {Record<string, any>} dictionary
556
+ * @param {any} [listingOverride]
557
+ */
558
+ function buildContentRenderContext(front, lang, dictionary, listingOverride) {
464
559
  const baseFront = normalizeFrontMatter(front);
465
560
  /** @type {string[]} */
466
561
  const normalizedTags = Array.isArray(front.tags)
@@ -513,25 +608,14 @@ async function renderContentTemplate(
513
608
  const site = metaEngine.buildSiteData(lang);
514
609
  const languageFlags = _i18n.flags(lang);
515
610
 
516
- return Mustache.render(
517
- template.content,
518
- {
519
- content: { html: decorateHtml(contentHtml, templateName) },
520
- front: normalizedFront,
521
- lang,
522
- listing,
523
- site,
524
- locale: languageFlags.locale,
525
- isEnglish: languageFlags.isEnglish,
526
- isTurkish: languageFlags.isTurkish,
527
- i18n: resolvedDictionary,
528
- ...collectionFlags,
529
- },
530
- {
531
- ...templateRegistry.getFiles(TYPE_PARTIAL),
532
- ...templateRegistry.getFiles(TYPE_COMPONENT),
533
- },
534
- );
611
+ return {
612
+ normalizedFront,
613
+ listing,
614
+ collectionFlags,
615
+ site,
616
+ languageFlags,
617
+ resolvedDictionary,
618
+ };
535
619
  }
536
620
 
537
621
  /**
@@ -597,6 +681,18 @@ function buildOutputPath(front, lang, slug) {
597
681
  return _io.path.combine(...segments.filter(Boolean), "index.html");
598
682
  }
599
683
 
684
+ /** @param {unknown} value */
685
+ function resolveFragmentId(value) {
686
+ if (typeof value !== "string") {
687
+ return "";
688
+ }
689
+ const trimmed = value.trim();
690
+ if (!trimmed) {
691
+ return "";
692
+ }
693
+ return trimmed.replace(/[\\/]/g, "-");
694
+ }
695
+
600
696
  /** @param {string} value */
601
697
  function toPosixPath(value) {
602
698
  return value.split(_io.path.separator).join("/");
@@ -833,6 +929,13 @@ async function buildContentPages() {
833
929
  markdownHtml ?? "",
834
930
  placeholders,
835
931
  );
932
+ const rawFront = normalizeFrontMatter(file.header);
933
+ const shouldBuildFragment = _fmt.boolean(rawFront.fragment);
934
+ const fragmentTemplateName =
935
+ typeof rawFront.fragmentTemplate === "string" &&
936
+ rawFront.fragmentTemplate.trim().length > 0
937
+ ? rawFront.fragmentTemplate.trim()
938
+ : file.template;
836
939
 
837
940
  if (file.template === "collection" || file.template === "home") {
838
941
  await renderEngine.buildPaginatedCollectionPages({
@@ -918,6 +1021,48 @@ async function buildContentPages() {
918
1021
  inputBytes: byteLength(file.content),
919
1022
  },
920
1023
  });
1024
+
1025
+ if (shouldBuildFragment) {
1026
+ const fragmentId = resolveFragmentId(rawFront.id ?? file.id);
1027
+ if (!fragmentId) {
1028
+ _log.warn(
1029
+ `[build] Fragment skipped for ${normalizeLogPath(file.sourcePath)} (missing id).`,
1030
+ );
1031
+ } else {
1032
+ const fragmentHtml = await renderFragmentTemplate(
1033
+ hydratedHtml,
1034
+ file.header,
1035
+ file.lang,
1036
+ dictionary,
1037
+ fragmentTemplateName,
1038
+ );
1039
+ const transformedFragment = await renderEngine.transformHtml(
1040
+ fragmentHtml,
1041
+ {
1042
+ versionToken,
1043
+ minifyHtml,
1044
+ },
1045
+ );
1046
+ const fragmentLang =
1047
+ typeof file.lang === "string" && file.lang.trim().length > 0
1048
+ ? file.lang.trim()
1049
+ : _i18n.default;
1050
+ const fragmentPath = _io.path.combine(
1051
+ FRAGMENTS_DIR,
1052
+ fragmentLang,
1053
+ `frag_${fragmentId}.html`,
1054
+ );
1055
+ GENERATED_PAGES.add(toPosixPath(fragmentPath));
1056
+ await writeHtmlFile(fragmentPath, transformedFragment, {
1057
+ action: "BUILD_FRAGMENT",
1058
+ type: "fragment",
1059
+ source: file.sourcePath,
1060
+ lang: file.lang,
1061
+ template: fragmentTemplateName,
1062
+ inputBytes: byteLength(file.content),
1063
+ });
1064
+ }
1065
+ }
921
1066
  }
922
1067
 
923
1068
  await renderEngine.buildDynamicCollectionPages({
package/scripts/main.js CHANGED
@@ -4,7 +4,7 @@ import _cli from "./cli.js";
4
4
  import _build from "./build.js";
5
5
  import _init from "./init.js";
6
6
 
7
- const VERSION = "0.0.4";
7
+ const VERSION = "0.0.6";
8
8
 
9
9
  const __filename = _io.url.toPath(import.meta.url);
10
10
  const __dirname = _io.path.name(__filename);
package/scripts/social.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import { config as _cfg } from "@shevky/base";
2
2
 
3
3
  function getRss() {
4
+ if (!_cfg.identity.social.rss) {
5
+ return;
6
+ }
7
+
4
8
  return {
5
9
  key: "rss",
6
10
  tone: "rss",
package/types/index.d.ts CHANGED
@@ -91,6 +91,11 @@ export type CollectionEntry = ContentSummaryLike & {
91
91
  type?: string;
92
92
  seriesTitle?: string;
93
93
  canonical?: string;
94
+ header?: ContentHeaderLike;
95
+ body?: ContentBodyLike;
96
+ content?: string;
97
+ isValid?: boolean;
98
+ sourcePath?: string;
94
99
  };
95
100
 
96
101
  export type CollectionsByLang = Record<string, Record<string, CollectionEntry[]>>;