mechanica 2.0.0-alpha.7 → 2.0.0-alpha.8

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.
@@ -658,8 +658,40 @@ function listPages(mechDir, options = {}) {
658
658
  }
659
659
  items.push(item);
660
660
  }
661
+ if (options.generated?.length) items.push(...generatedPageItems(options.generated, options, config));
661
662
  return items.sort(comparePages);
662
663
  }
664
+ /** File-less pages → list rows: one per logical path, with a `locales` union. */
665
+ function generatedPageItems(generated, options, config) {
666
+ const groups = /* @__PURE__ */ new Map();
667
+ for (const vp of generated) {
668
+ const arr = groups.get(vp.path);
669
+ if (arr) arr.push(vp);
670
+ else groups.set(vp.path, [vp]);
671
+ }
672
+ const items = [];
673
+ for (const [path, vps] of groups) {
674
+ const locales = config ? vps.map((v) => v.locale).filter(Boolean) : [];
675
+ if (options.locale && !locales.includes(options.locale)) continue;
676
+ const source = (options.locale ? vps.find((v) => v.locale === options.locale) : void 0) ?? (config ? vps.find((v) => v.locale === config.default) : void 0) ?? vps[0];
677
+ const embedded = {};
678
+ for (const dataEntry of options.data ?? []) embedded[dataEntry.id] = source.data?.[dataEntry.id] ?? {};
679
+ const dir = path.slice(1).split("/").slice(0, -1).join("/");
680
+ const item = {
681
+ path,
682
+ name: (typeof source.meta?.title === "string" ? source.meta.title : void 0) ?? path.split("/").pop() ?? path,
683
+ folderPath: dir === "" ? null : dir,
684
+ order: 0,
685
+ orderAfter: null,
686
+ draft: false,
687
+ generated: true,
688
+ ...embedded
689
+ };
690
+ if (config) item.locales = config.all.filter((code) => locales.includes(code));
691
+ items.push(item);
692
+ }
693
+ return items;
694
+ }
663
695
  /** Ordering: by folder, then explicit `orderAfter`, then `order`, then path. */
664
696
  function comparePages(a, b) {
665
697
  if (a.folderPath === b.folderPath) {
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { B as readImageManifest, C as getPagePath, I as UPLOADS_PREFIX, J as hasSharp, L as assetFileOf, N as setPageCodec, R as isDerivedAsset, T as listPages, U as updateImageManifest, Y as isRasterImage, a as toBlockMeta, d as readFolderData, f as readFoldersData, i as composedBlockMeta, m as readSiteLocaleOverride, n as blockAssetLinks, p as readSiteData, q as analyzeImageBuffer, r as buildRichTextCodec, t as BLOCKS_MANIFEST_FILE } from "./chunks/page-assets-BjIm0jws.js";
1
+ import { B as readImageManifest, C as getPagePath, I as UPLOADS_PREFIX, J as hasSharp, L as assetFileOf, N as setPageCodec, R as isDerivedAsset, T as listPages, U as updateImageManifest, Y as isRasterImage, a as toBlockMeta, d as readFolderData, f as readFoldersData, i as composedBlockMeta, m as readSiteLocaleOverride, n as blockAssetLinks, p as readSiteData, q as analyzeImageBuffer, r as buildRichTextCodec, t as BLOCKS_MANIFEST_FILE } from "./chunks/page-assets-BCUAuOlo.js";
2
2
  import fs from "node:fs";
3
3
  import { dirname, join, parse, relative } from "node:path";
4
4
  import { applySeoTags, auditPageHtml, buildRobotsTxt, buildSitemap, findUnknownBlocks, generateProject, isPaginatedQuery, localePath, mergeTranslation, migrateContent, paginationVariantPath, registerFieldSchemas, resolveQueryKey, templateBlockIds, validateLinks, walkSchema, walkTree } from "mechanica-shared";
@@ -367,7 +367,10 @@ async function exportProject(cwd, ssr, options = {}) {
367
367
  }
368
368
  setPageCodec(richText);
369
369
  const querySource = {
370
- listPages: (opts) => listPages(join(cwd, ".mech"), opts),
370
+ listPages: (opts) => listPages(join(cwd, ".mech"), {
371
+ ...opts,
372
+ generated: ssr.generatedPages
373
+ }),
371
374
  fetchJson: async ({ url, ...init }) => {
372
375
  const res = await fetch(url, init);
373
376
  if (!res.ok) throw new Error(`${url} responded ${res.status}`);
package/dist/editor.css CHANGED
@@ -4802,7 +4802,7 @@ from {
4802
4802
  justify-content: flex-end;
4803
4803
  gap: 8px;
4804
4804
  margin-top: 4px;
4805
- }.mech-pages[data-v-e750b5bb] {
4805
+ }.mech-pages[data-v-5f70933a] {
4806
4806
  display: flex;
4807
4807
  align-items: stretch;
4808
4808
  justify-content: center;
@@ -4811,7 +4811,7 @@ from {
4811
4811
  max-width: 1200px;
4812
4812
  height: min(620px, 82vh);
4813
4813
  }
4814
- .mech-pages__card[data-v-e750b5bb] {
4814
+ .mech-pages__card[data-v-5f70933a] {
4815
4815
  display: flex;
4816
4816
  flex-direction: column;
4817
4817
  flex: 0 1 840px;
@@ -4821,20 +4821,20 @@ from {
4821
4821
  box-shadow: var(--mech-shadow-dialog);
4822
4822
  overflow: hidden;
4823
4823
  }
4824
- .mech-pages__header[data-v-e750b5bb] {
4824
+ .mech-pages__header[data-v-5f70933a] {
4825
4825
  display: flex;
4826
4826
  align-items: center;
4827
4827
  gap: 10px;
4828
4828
  padding: 18px 14px 12px 24px;
4829
4829
  flex: none;
4830
4830
  }
4831
- .mech-pages__title[data-v-e750b5bb] {
4831
+ .mech-pages__title[data-v-5f70933a] {
4832
4832
  margin: 0;
4833
4833
  font-size: 16px;
4834
4834
  font-weight: 600;
4835
4835
  letter-spacing: -0.01em;
4836
4836
  }
4837
- .mech-pages__count[data-v-e750b5bb] {
4837
+ .mech-pages__count[data-v-5f70933a] {
4838
4838
  padding: 2px 8px;
4839
4839
  border-radius: var(--mech-radius-pill);
4840
4840
  background: var(--mech-active);
@@ -4842,7 +4842,7 @@ from {
4842
4842
  font-size: 11.5px;
4843
4843
  font-weight: 600;
4844
4844
  }
4845
- .mech-pages__close[data-v-e750b5bb] {
4845
+ .mech-pages__close[data-v-5f70933a] {
4846
4846
  display: flex;
4847
4847
  align-items: center;
4848
4848
  justify-content: center;
@@ -4855,25 +4855,25 @@ from {
4855
4855
  border-radius: var(--mech-radius-pill);
4856
4856
  cursor: pointer;
4857
4857
  }
4858
- .mech-pages__close .vicon[data-v-e750b5bb] {
4858
+ .mech-pages__close .vicon[data-v-5f70933a] {
4859
4859
  width: 18px;
4860
4860
  height: 18px;
4861
4861
  }
4862
- .mech-pages__close[data-v-e750b5bb]:hover {
4862
+ .mech-pages__close[data-v-5f70933a]:hover {
4863
4863
  background: var(--mech-hover);
4864
4864
  color: var(--mech-fg);
4865
4865
  }
4866
- .mech-pages__toolbar[data-v-e750b5bb] {
4866
+ .mech-pages__toolbar[data-v-5f70933a] {
4867
4867
  display: flex;
4868
4868
  gap: 8px;
4869
4869
  padding: 0 24px 14px;
4870
4870
  flex: none;
4871
4871
  }
4872
- .mech-pages__search-wrap[data-v-e750b5bb] {
4872
+ .mech-pages__search-wrap[data-v-5f70933a] {
4873
4873
  position: relative;
4874
4874
  flex: 1;
4875
4875
  }
4876
- .mech-pages__search-icon[data-v-e750b5bb] {
4876
+ .mech-pages__search-icon[data-v-5f70933a] {
4877
4877
  position: absolute;
4878
4878
  left: 12px;
4879
4879
  top: 50%;
@@ -4883,18 +4883,18 @@ from {
4883
4883
  color: var(--mech-muted);
4884
4884
  pointer-events: none;
4885
4885
  }
4886
- .mech-pages__search[data-v-e750b5bb] {
4886
+ .mech-pages__search[data-v-5f70933a] {
4887
4887
  width: 100%;
4888
4888
  padding-left: 36px;
4889
4889
  }
4890
- .mech-pages__content[data-v-e750b5bb] {
4890
+ .mech-pages__content[data-v-5f70933a] {
4891
4891
  display: flex;
4892
4892
  gap: 14px;
4893
4893
  flex: 1;
4894
4894
  min-height: 0;
4895
4895
  padding: 0 24px 20px;
4896
4896
  }
4897
- .mech-pages__rail[data-v-e750b5bb] {
4897
+ .mech-pages__rail[data-v-5f70933a] {
4898
4898
  flex: none;
4899
4899
  width: 172px;
4900
4900
  display: flex;
@@ -4903,7 +4903,7 @@ from {
4903
4903
  overflow-y: auto;
4904
4904
  padding-right: 2px;
4905
4905
  }
4906
- .mech-pages__rail-item[data-v-e750b5bb] {
4906
+ .mech-pages__rail-item[data-v-5f70933a] {
4907
4907
  display: flex;
4908
4908
  align-items: center;
4909
4909
  gap: 8px;
@@ -4917,51 +4917,51 @@ from {
4917
4917
  color: var(--mech-fg-alt);
4918
4918
  cursor: pointer;
4919
4919
  }
4920
- .mech-pages__rail-item .vicon[data-v-e750b5bb] {
4920
+ .mech-pages__rail-item .vicon[data-v-5f70933a] {
4921
4921
  flex: none;
4922
4922
  width: 15px;
4923
4923
  height: 15px;
4924
4924
  color: var(--mech-muted);
4925
4925
  }
4926
- .mech-pages__rail-item[data-v-e750b5bb]:hover {
4926
+ .mech-pages__rail-item[data-v-5f70933a]:hover {
4927
4927
  background: var(--mech-hover);
4928
4928
  }
4929
- .mech-pages__rail-item.is-active[data-v-e750b5bb] {
4929
+ .mech-pages__rail-item.is-active[data-v-5f70933a] {
4930
4930
  background: var(--mech-accent-soft);
4931
4931
  color: var(--mech-accent);
4932
4932
  font-weight: 500;
4933
4933
  }
4934
- .mech-pages__rail-item.is-active .vicon[data-v-e750b5bb] {
4934
+ .mech-pages__rail-item.is-active .vicon[data-v-5f70933a] {
4935
4935
  color: var(--mech-accent);
4936
4936
  }
4937
- .mech-pages__rail-name[data-v-e750b5bb] {
4937
+ .mech-pages__rail-name[data-v-5f70933a] {
4938
4938
  flex: 1;
4939
4939
  min-width: 0;
4940
4940
  overflow: hidden;
4941
4941
  text-overflow: ellipsis;
4942
4942
  white-space: nowrap;
4943
4943
  }
4944
- .mech-pages__rail-count[data-v-e750b5bb] {
4944
+ .mech-pages__rail-count[data-v-5f70933a] {
4945
4945
  flex: none;
4946
4946
  font-size: 11px;
4947
4947
  color: var(--mech-placeholder);
4948
4948
  }
4949
- .mech-pages__rail-item.is-active .mech-pages__rail-count[data-v-e750b5bb] {
4949
+ .mech-pages__rail-item.is-active .mech-pages__rail-count[data-v-5f70933a] {
4950
4950
  color: var(--mech-accent);
4951
4951
  opacity: 0.7;
4952
4952
  }
4953
- .mech-pages__list[data-v-e750b5bb] {
4953
+ .mech-pages__list[data-v-5f70933a] {
4954
4954
  flex: 1;
4955
4955
  min-width: 0;
4956
4956
  overflow-y: auto;
4957
4957
  }
4958
- .mech-pages__row[data-v-e750b5bb], .mech-pages__thead[data-v-e750b5bb] {
4958
+ .mech-pages__row[data-v-5f70933a], .mech-pages__thead[data-v-5f70933a] {
4959
4959
  display: grid;
4960
4960
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr) 96px;
4961
4961
  align-items: center;
4962
4962
  column-gap: 12px;
4963
4963
  }
4964
- .mech-pages__thead[data-v-e750b5bb] {
4964
+ .mech-pages__thead[data-v-5f70933a] {
4965
4965
  position: sticky;
4966
4966
  top: 0;
4967
4967
  z-index: 1;
@@ -4969,14 +4969,14 @@ from {
4969
4969
  background: var(--mech-bg);
4970
4970
  box-shadow: 0 1px 0 var(--mech-border);
4971
4971
  }
4972
- .mech-pages__thead span[data-v-e750b5bb] {
4972
+ .mech-pages__thead span[data-v-5f70933a] {
4973
4973
  font-size: 10.5px;
4974
4974
  font-weight: 600;
4975
4975
  text-transform: uppercase;
4976
4976
  letter-spacing: 0.05em;
4977
4977
  color: var(--mech-muted);
4978
4978
  }
4979
- .mech-pages__group[data-v-e750b5bb] {
4979
+ .mech-pages__group[data-v-5f70933a] {
4980
4980
  display: flex;
4981
4981
  align-items: center;
4982
4982
  gap: 7px;
@@ -4995,36 +4995,36 @@ from {
4995
4995
  border-radius: var(--mech-radius-sm);
4996
4996
  cursor: pointer;
4997
4997
  }
4998
- .mech-pages__group .vicon[data-v-e750b5bb] {
4998
+ .mech-pages__group .vicon[data-v-5f70933a] {
4999
4999
  width: 13px;
5000
5000
  height: 13px;
5001
5001
  }
5002
- .mech-pages__group[data-v-e750b5bb]:hover {
5002
+ .mech-pages__group[data-v-5f70933a]:hover {
5003
5003
  color: var(--mech-fg);
5004
5004
  }
5005
- .mech-pages__row[data-v-e750b5bb] {
5005
+ .mech-pages__row[data-v-5f70933a] {
5006
5006
  padding: 0 10px;
5007
5007
  height: 38px;
5008
5008
  border-radius: var(--mech-radius-sm);
5009
5009
  cursor: pointer;
5010
5010
  }
5011
- .mech-pages__row.is-active[data-v-e750b5bb] {
5011
+ .mech-pages__row.is-active[data-v-5f70933a] {
5012
5012
  background: var(--mech-accent-soft);
5013
5013
  }
5014
- .mech-pages__cell-name[data-v-e750b5bb] {
5014
+ .mech-pages__cell-name[data-v-5f70933a] {
5015
5015
  display: flex;
5016
5016
  align-items: center;
5017
5017
  gap: 8px;
5018
5018
  min-width: 0;
5019
5019
  }
5020
- .mech-pages__name[data-v-e750b5bb] {
5020
+ .mech-pages__name[data-v-5f70933a] {
5021
5021
  font-size: 13.5px;
5022
5022
  font-weight: 500;
5023
5023
  overflow: hidden;
5024
5024
  text-overflow: ellipsis;
5025
5025
  white-space: nowrap;
5026
5026
  }
5027
- .mech-pages__badge[data-v-e750b5bb] {
5027
+ .mech-pages__badge[data-v-5f70933a] {
5028
5028
  flex: none;
5029
5029
  font-size: 10px;
5030
5030
  font-weight: 600;
@@ -5036,17 +5036,17 @@ from {
5036
5036
  border-radius: var(--mech-radius-pill);
5037
5037
  padding: 1px 7px;
5038
5038
  }
5039
- .mech-pages__badge.is-draft[data-v-e750b5bb] {
5039
+ .mech-pages__badge.is-draft[data-v-5f70933a] {
5040
5040
  color: var(--mech-muted);
5041
5041
  }
5042
- .mech-pages__locales[data-v-e750b5bb] {
5042
+ .mech-pages__locales[data-v-5f70933a] {
5043
5043
  flex: none;
5044
5044
  display: inline-flex;
5045
5045
  align-items: center;
5046
5046
  gap: 3px;
5047
5047
  margin-left: 2px;
5048
5048
  }
5049
- .mech-pages__loc[data-v-e750b5bb] {
5049
+ .mech-pages__loc[data-v-5f70933a] {
5050
5050
  min-width: 15px;
5051
5051
  padding: 1px 4px;
5052
5052
  border-radius: var(--mech-radius-sm);
@@ -5060,12 +5060,12 @@ from {
5060
5060
  letter-spacing: 0.02em;
5061
5061
  opacity: 0.5;
5062
5062
  }
5063
- .mech-pages__loc.is-on[data-v-e750b5bb] {
5063
+ .mech-pages__loc.is-on[data-v-5f70933a] {
5064
5064
  background: var(--mech-accent-soft);
5065
5065
  color: var(--mech-accent);
5066
5066
  opacity: 1;
5067
5067
  }
5068
- .mech-pages__cell-path[data-v-e750b5bb] {
5068
+ .mech-pages__cell-path[data-v-5f70933a] {
5069
5069
  min-width: 0;
5070
5070
  overflow: hidden;
5071
5071
  text-overflow: ellipsis;
@@ -5073,16 +5073,16 @@ from {
5073
5073
  font-size: 12.5px;
5074
5074
  color: var(--mech-muted);
5075
5075
  }
5076
- .mech-pages__actions[data-v-e750b5bb] {
5076
+ .mech-pages__actions[data-v-5f70933a] {
5077
5077
  display: flex;
5078
5078
  justify-content: flex-end;
5079
5079
  gap: 1px;
5080
5080
  opacity: 0;
5081
5081
  }
5082
- .mech-pages__row.is-active .mech-pages__actions[data-v-e750b5bb], .mech-pages__row:hover .mech-pages__actions[data-v-e750b5bb] {
5082
+ .mech-pages__row.is-active .mech-pages__actions[data-v-5f70933a], .mech-pages__row:hover .mech-pages__actions[data-v-5f70933a] {
5083
5083
  opacity: 1;
5084
5084
  }
5085
- .mech-pages__actions button[data-v-e750b5bb] {
5085
+ .mech-pages__actions button[data-v-5f70933a] {
5086
5086
  display: flex;
5087
5087
  align-items: center;
5088
5088
  justify-content: center;
@@ -5095,24 +5095,24 @@ from {
5095
5095
  cursor: pointer;
5096
5096
  line-height: 1;
5097
5097
  }
5098
- .mech-pages__actions button .vicon[data-v-e750b5bb] {
5098
+ .mech-pages__actions button .vicon[data-v-5f70933a] {
5099
5099
  width: 15px;
5100
5100
  height: 15px;
5101
5101
  }
5102
- .mech-pages__actions button[data-v-e750b5bb]:hover {
5102
+ .mech-pages__actions button[data-v-5f70933a]:hover {
5103
5103
  background: var(--mech-active);
5104
5104
  color: var(--mech-fg);
5105
5105
  }
5106
- .mech-pages__actions button.is-danger[data-v-e750b5bb]:hover {
5106
+ .mech-pages__actions button.is-danger[data-v-5f70933a]:hover {
5107
5107
  color: var(--mech-error);
5108
5108
  }
5109
- .mech-pages__empty[data-v-e750b5bb] {
5109
+ .mech-pages__empty[data-v-5f70933a] {
5110
5110
  padding: 24px 10px;
5111
5111
  margin: 0;
5112
5112
  color: var(--mech-muted);
5113
5113
  font-size: 13px;
5114
5114
  }
5115
- .mech-pages__preview[data-v-e750b5bb] {
5115
+ .mech-pages__preview[data-v-5f70933a] {
5116
5116
  flex: none;
5117
5117
  align-self: flex-start;
5118
5118
  max-height: 100%;
@@ -5124,18 +5124,18 @@ from {
5124
5124
  box-shadow: var(--mech-shadow-dialog);
5125
5125
  overflow: hidden;
5126
5126
  }
5127
- .mech-pages__preview-frame[data-v-e750b5bb] {
5127
+ .mech-pages__preview-frame[data-v-5f70933a] {
5128
5128
  flex: 0 1 auto;
5129
5129
  min-height: 0;
5130
5130
  overflow: hidden;
5131
5131
  background: var(--mech-app-bg);
5132
5132
  }
5133
- .mech-pages__preview-frame img[data-v-e750b5bb] {
5133
+ .mech-pages__preview-frame img[data-v-5f70933a] {
5134
5134
  display: block;
5135
5135
  width: 100%;
5136
5136
  height: auto;
5137
5137
  }
5138
- .mech-pages__preview-empty[data-v-e750b5bb] {
5138
+ .mech-pages__preview-empty[data-v-5f70933a] {
5139
5139
  display: flex;
5140
5140
  flex-direction: column;
5141
5141
  align-items: center;
@@ -5144,16 +5144,16 @@ from {
5144
5144
  padding: 48px 16px;
5145
5145
  color: var(--mech-muted);
5146
5146
  }
5147
- .mech-pages__preview-empty p[data-v-e750b5bb] {
5147
+ .mech-pages__preview-empty p[data-v-5f70933a] {
5148
5148
  margin: 8px 0 0;
5149
5149
  font-size: 13px;
5150
5150
  font-weight: 500;
5151
5151
  }
5152
- .mech-pages__preview-empty code[data-v-e750b5bb] {
5152
+ .mech-pages__preview-empty code[data-v-5f70933a] {
5153
5153
  font-size: 11.5px;
5154
5154
  color: var(--mech-placeholder);
5155
5155
  }
5156
- .mech-pages__preview-monogram[data-v-e750b5bb] {
5156
+ .mech-pages__preview-monogram[data-v-5f70933a] {
5157
5157
  display: flex;
5158
5158
  align-items: center;
5159
5159
  justify-content: center;
@@ -5164,7 +5164,7 @@ from {
5164
5164
  font-size: 22px;
5165
5165
  font-weight: 600;
5166
5166
  }
5167
- .mech-pages__preview-caption[data-v-e750b5bb] {
5167
+ .mech-pages__preview-caption[data-v-5f70933a] {
5168
5168
  flex: none;
5169
5169
  display: flex;
5170
5170
  flex-direction: column;
@@ -5172,14 +5172,14 @@ from {
5172
5172
  padding: 12px 16px 14px;
5173
5173
  border-top: 1px solid var(--mech-border);
5174
5174
  }
5175
- .mech-pages__preview-name[data-v-e750b5bb] {
5175
+ .mech-pages__preview-name[data-v-5f70933a] {
5176
5176
  font-size: 13px;
5177
5177
  font-weight: 600;
5178
5178
  overflow: hidden;
5179
5179
  text-overflow: ellipsis;
5180
5180
  white-space: nowrap;
5181
5181
  }
5182
- .mech-pages__preview-path[data-v-e750b5bb] {
5182
+ .mech-pages__preview-path[data-v-5f70933a] {
5183
5183
  font-size: 11.5px;
5184
5184
  color: var(--mech-muted);
5185
5185
  overflow: hidden;
@@ -5187,7 +5187,7 @@ from {
5187
5187
  white-space: nowrap;
5188
5188
  }
5189
5189
  @media (max-width: 1160px) {
5190
- .mech-pages__preview[data-v-e750b5bb] {
5190
+ .mech-pages__preview[data-v-5f70933a] {
5191
5191
  display: none;
5192
5192
  }
5193
5193
  }.mech-pagebar[data-v-a0f85ad0] {
package/dist/editor.js CHANGED
@@ -1913,31 +1913,35 @@ var _hoisted_18 = {
1913
1913
  };
1914
1914
  var _hoisted_19 = {
1915
1915
  key: 2,
1916
+ class: "mech-pages__badge"
1917
+ };
1918
+ var _hoisted_20 = {
1919
+ key: 3,
1916
1920
  class: "mech-pages__locales"
1917
1921
  };
1918
- var _hoisted_20 = ["title"];
1919
- var _hoisted_21 = { class: "mech-pages__cell-path" };
1920
- var _hoisted_22 = ["onClick"];
1922
+ var _hoisted_21 = ["title"];
1923
+ var _hoisted_22 = { class: "mech-pages__cell-path" };
1921
1924
  var _hoisted_23 = ["onClick"];
1922
1925
  var _hoisted_24 = ["onClick"];
1923
- var _hoisted_25 = {
1926
+ var _hoisted_25 = ["onClick"];
1927
+ var _hoisted_26 = {
1924
1928
  key: 0,
1925
1929
  class: "mech-pages__empty"
1926
1930
  };
1927
- var _hoisted_26 = { class: "mech-pages__preview" };
1928
- var _hoisted_27 = { class: "mech-pages__preview-frame" };
1929
- var _hoisted_28 = ["src", "data-path"];
1930
- var _hoisted_29 = {
1931
+ var _hoisted_27 = { class: "mech-pages__preview" };
1932
+ var _hoisted_28 = { class: "mech-pages__preview-frame" };
1933
+ var _hoisted_29 = ["src", "data-path"];
1934
+ var _hoisted_30 = {
1931
1935
  key: 1,
1932
1936
  class: "mech-pages__preview-empty"
1933
1937
  };
1934
- var _hoisted_30 = { class: "mech-pages__preview-monogram" };
1935
- var _hoisted_31 = {
1938
+ var _hoisted_31 = { class: "mech-pages__preview-monogram" };
1939
+ var _hoisted_32 = {
1936
1940
  key: 0,
1937
1941
  class: "mech-pages__preview-caption"
1938
1942
  };
1939
- var _hoisted_32 = { class: "mech-pages__preview-name" };
1940
- var _hoisted_33 = { class: "mech-pages__preview-path" };
1943
+ var _hoisted_33 = { class: "mech-pages__preview-name" };
1944
+ var _hoisted_34 = { class: "mech-pages__preview-path" };
1941
1945
  //#endregion
1942
1946
  //#region src/editor/dialogs/PagesDialog.vue
1943
1947
  var PagesDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
@@ -2035,6 +2039,13 @@ var PagesDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__
2035
2039
  function onRowMenu(event, page) {
2036
2040
  if (!contextMenu) return;
2037
2041
  activePath.value = page.path;
2042
+ if (page.generated) {
2043
+ contextMenu.openAt(event, [{
2044
+ label: "Open",
2045
+ onClick: () => open(page)
2046
+ }]);
2047
+ return;
2048
+ }
2038
2049
  contextMenu.openAt(event, [
2039
2050
  {
2040
2051
  label: "Open",
@@ -2199,17 +2210,19 @@ var PagesDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__
2199
2210
  createElementVNode("span", _hoisted_15, [
2200
2211
  createElementVNode("span", _hoisted_16, toDisplayString(page.name || page.path), 1),
2201
2212
  page.draft ? (openBlock(), createElementBlock("span", _hoisted_17, "Draft")) : createCommentVNode("", true),
2202
- page.path === currentLogical.value ? (openBlock(), createElementBlock("span", _hoisted_18, "Current")) : createCommentVNode("", true),
2203
- unref(localeConfig) && page.locales ? (openBlock(), createElementBlock("span", _hoisted_19, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(localeCodes), (code) => {
2213
+ page.generated ? (openBlock(), createElementBlock("span", _hoisted_18, "Generated")) : createCommentVNode("", true),
2214
+ page.path === currentLogical.value ? (openBlock(), createElementBlock("span", _hoisted_19, "Current")) : createCommentVNode("", true),
2215
+ unref(localeConfig) && page.locales ? (openBlock(), createElementBlock("span", _hoisted_20, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(localeCodes), (code) => {
2204
2216
  return openBlock(), createElementBlock("span", {
2205
2217
  key: code,
2206
2218
  class: normalizeClass(["mech-pages__loc", { "is-on": page.locales.includes(code) }]),
2207
2219
  title: `${localeName(code)}: ${page.locales.includes(code) ? "translated" : "not translated"}`
2208
- }, toDisplayString(code), 11, _hoisted_20);
2220
+ }, toDisplayString(code), 11, _hoisted_21);
2209
2221
  }), 128))])) : createCommentVNode("", true)
2210
2222
  ]),
2211
- createElementVNode("span", _hoisted_21, toDisplayString(page.path), 1),
2212
- createElementVNode("span", {
2223
+ createElementVNode("span", _hoisted_22, toDisplayString(page.path), 1),
2224
+ !page.generated ? (openBlock(), createElementBlock("span", {
2225
+ key: 0,
2213
2226
  class: "mech-pages__actions",
2214
2227
  onClick: _cache[3] || (_cache[3] = withModifiers(() => {}, ["stop"]))
2215
2228
  }, [
@@ -2217,34 +2230,34 @@ var PagesDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__
2217
2230
  type: "button",
2218
2231
  title: "Rename",
2219
2232
  onClick: ($event) => startEdit(page)
2220
- }, [createVNode(VIcon_default, { name: "pencil" })], 8, _hoisted_22),
2233
+ }, [createVNode(VIcon_default, { name: "pencil" })], 8, _hoisted_23),
2221
2234
  createElementVNode("button", {
2222
2235
  type: "button",
2223
2236
  title: "Duplicate",
2224
2237
  onClick: ($event) => startDuplicate(page)
2225
- }, [createVNode(VIcon_default, { name: "copy" })], 8, _hoisted_23),
2238
+ }, [createVNode(VIcon_default, { name: "copy" })], 8, _hoisted_24),
2226
2239
  createElementVNode("button", {
2227
2240
  type: "button",
2228
2241
  title: "Delete",
2229
2242
  class: "is-danger",
2230
2243
  onClick: ($event) => confirmRemove(page)
2231
- }, [createVNode(VIcon_default, { name: "trash" })], 8, _hoisted_24)
2232
- ])
2244
+ }, [createVNode(VIcon_default, { name: "trash" })], 8, _hoisted_25)
2245
+ ])) : createCommentVNode("", true)
2233
2246
  ], 42, _hoisted_14);
2234
2247
  }), 128))], 64);
2235
2248
  }), 128)),
2236
- !rows.value.length ? (openBlock(), createElementBlock("p", _hoisted_25, toDisplayString(query.value ? `No pages match “${query.value}”.` : "No pages yet."), 1)) : createCommentVNode("", true)
2249
+ !rows.value.length ? (openBlock(), createElementBlock("p", _hoisted_26, toDisplayString(query.value ? `No pages match “${query.value}”.` : "No pages yet."), 1)) : createCommentVNode("", true)
2237
2250
  ], 512)])
2238
- ]), createElementVNode("aside", _hoisted_26, [createElementVNode("div", _hoisted_27, [previewPage.value && !thumbFailed.has(previewPage.value.path) ? (openBlock(), createElementBlock("img", {
2251
+ ]), createElementVNode("aside", _hoisted_27, [createElementVNode("div", _hoisted_28, [previewPage.value && !thumbFailed.has(previewPage.value.path) ? (openBlock(), createElementBlock("img", {
2239
2252
  key: previewPage.value.path,
2240
2253
  src: thumbSrc(previewPage.value),
2241
2254
  "data-path": previewPage.value.path,
2242
2255
  alt: "",
2243
2256
  onError: onThumbError
2244
- }, null, 40, _hoisted_28)) : (openBlock(), createElementBlock("div", _hoisted_29, [createElementVNode("span", _hoisted_30, toDisplayString(monogram.value), 1), previewPage.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [_cache[8] || (_cache[8] = createElementVNode("p", null, "No preview yet", -1)), _cache[9] || (_cache[9] = createElementVNode("code", null, "mechanica thumbs", -1))], 64)) : createCommentVNode("", true)]))]), previewPage.value ? (openBlock(), createElementBlock("div", _hoisted_31, [createElementVNode("span", _hoisted_32, toDisplayString(previewPage.value.name || previewPage.value.path), 1), createElementVNode("span", _hoisted_33, toDisplayString(previewPage.value.path), 1)])) : createCommentVNode("", true)])]);
2257
+ }, null, 40, _hoisted_29)) : (openBlock(), createElementBlock("div", _hoisted_30, [createElementVNode("span", _hoisted_31, toDisplayString(monogram.value), 1), previewPage.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [_cache[8] || (_cache[8] = createElementVNode("p", null, "No preview yet", -1)), _cache[9] || (_cache[9] = createElementVNode("code", null, "mechanica thumbs", -1))], 64)) : createCommentVNode("", true)]))]), previewPage.value ? (openBlock(), createElementBlock("div", _hoisted_32, [createElementVNode("span", _hoisted_33, toDisplayString(previewPage.value.name || previewPage.value.path), 1), createElementVNode("span", _hoisted_34, toDisplayString(previewPage.value.path), 1)])) : createCommentVNode("", true)])]);
2245
2258
  };
2246
2259
  }
2247
- }), [["__scopeId", "data-v-e750b5bb"]]);
2260
+ }), [["__scopeId", "data-v-5f70933a"]]);
2248
2261
  //#endregion
2249
2262
  //#region src/editor/components/PageBar.vue?vue&type=script&setup=true&lang.ts
2250
2263
  var _hoisted_1$3 = { class: "mech-pagebar__icon" };
package/dist/plugin.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as renamePage, D as pageUrlOf, E as movePage, F as translationsOf, G as wasRecentlyMutated, H as saveUpload, K as writeFileAtomic, M as setPageBlocks, N as setPageCodec, O as pageVersion, P as setPageDraft, S as fillImageMeta, T as listPages, V as saveDerivedAsset, W as markMutated, _ as createTranslation, a as toBlockMeta, b as duplicatePage, c as mergeLocaleFolderData, d as readFolderData, g as createPage, h as PageExistsError, i as composedBlockMeta, j as savePage, k as readPage, l as mergeLocaleSiteData, o as folderOf, p as readSiteData, r as buildRichTextCodec, s as mergeFolderData, t as BLOCKS_MANIFEST_FILE, u as mergeSiteData, v as deletePage, w as listFolders, x as fillContentDefaults, y as deleteTranslation, z as listImages } from "./chunks/page-assets-BjIm0jws.js";
1
+ import { A as renamePage, D as pageUrlOf, E as movePage, F as translationsOf, G as wasRecentlyMutated, H as saveUpload, K as writeFileAtomic, M as setPageBlocks, N as setPageCodec, O as pageVersion, P as setPageDraft, S as fillImageMeta, T as listPages, V as saveDerivedAsset, W as markMutated, _ as createTranslation, a as toBlockMeta, b as duplicatePage, c as mergeLocaleFolderData, d as readFolderData, g as createPage, h as PageExistsError, i as composedBlockMeta, j as savePage, k as readPage, l as mergeLocaleSiteData, o as folderOf, p as readSiteData, r as buildRichTextCodec, s as mergeFolderData, t as BLOCKS_MANIFEST_FILE, u as mergeSiteData, v as deletePage, w as listFolders, x as fillContentDefaults, y as deleteTranslation, z as listImages } from "./chunks/page-assets-BCUAuOlo.js";
2
2
  import fs, { existsSync, readFileSync, readdirSync } from "node:fs";
3
3
  import { basename, join, relative, resolve, sep } from "node:path";
4
4
  import { parseVueRequest } from "@vitejs/plugin-vue";
@@ -796,11 +796,15 @@ function generateSsrEntry(options) {
796
796
  /**
797
797
  * The dev server's {@link QuerySource}: pages come live from the `.mech`
798
798
  * store; `useFetch` URLs are fetched server-side (no CORS, matching how the
799
- * export resolves them at build time).
799
+ * export resolves them at build time). `generated` pages (plugin `generatePages`)
800
+ * are merged into the page listing so `usePages()` sees them, like the export.
800
801
  */
801
- function devQuerySource(mechDir) {
802
+ function devQuerySource(mechDir, generated) {
802
803
  return {
803
- listPages: (options) => listPages(mechDir, options),
804
+ listPages: (options) => listPages(mechDir, {
805
+ ...options,
806
+ generated
807
+ }),
804
808
  fetchJson: async ({ url, ...init }) => {
805
809
  const res = await fetch(url, init);
806
810
  if (!res.ok) throw new Error(`${url} responded ${res.status}`);
@@ -813,8 +817,8 @@ function devQuerySource(mechDir) {
813
817
  * form `"<type>.<json-args>"` (the same encoding the runtime composables use);
814
818
  * `context.page` selects the chunk of a paginated query.
815
819
  */
816
- function resolveDevQuery(mechDir, key, context = {}) {
817
- return resolveQueryKey(devQuerySource(mechDir), key, context);
820
+ function resolveDevQuery(mechDir, key, context = {}, generated) {
821
+ return resolveQueryKey(devQuerySource(mechDir, generated), key, context);
818
822
  }
819
823
  //#endregion
820
824
  //#region src/vite/dev/page-state.ts
@@ -1082,6 +1086,15 @@ function createDevMiddleware(mechDir, options = {}) {
1082
1086
  return fs.createReadStream(file).pipe(res);
1083
1087
  };
1084
1088
  try {
1089
+ const gen = options.generated ? await options.generated() : null;
1090
+ /** Reject a mutation targeting a file-less generated page. */
1091
+ const blockGenerated = (path) => {
1092
+ if (path && gen?.logical.has(path)) {
1093
+ json({ error: "This page is generated and read-only" }, 409);
1094
+ return true;
1095
+ }
1096
+ return false;
1097
+ };
1085
1098
  if (pathname.startsWith("/assets/")) return serveFrom("assets", "/assets/");
1086
1099
  if (pathname.startsWith("/thumbs/")) {
1087
1100
  res.setHeader("cache-control", "no-cache");
@@ -1095,7 +1108,7 @@ function createDevMiddleware(mechDir, options = {}) {
1095
1108
  return json(await resolveDevQuery(mechDir, key, {
1096
1109
  page: Number(query.get("page") ?? "") || void 0,
1097
1110
  locale: variantCode(query.get("locale"))
1098
- }));
1111
+ }, gen?.pages));
1099
1112
  }
1100
1113
  if (pathname === "/upload" && req.method === "POST") {
1101
1114
  const body = await readBody(req);
@@ -1111,11 +1124,14 @@ function createDevMiddleware(mechDir, options = {}) {
1111
1124
  const pathParam = query.get("path");
1112
1125
  if (!pathParam) return json({ error: "Missing path" }, 400);
1113
1126
  await options.ready?.();
1114
- const generated = (await options.generated?.())?.byServed.get(pathParam);
1127
+ const generated = gen?.byServed.get(pathParam);
1115
1128
  if (generated) return json(buildGeneratedState(mechDir, generated, config));
1116
1129
  return json(buildPageState(mechDir, pathParam, config));
1117
1130
  }
1118
- if (pathname === "/pages" && req.method === "GET") return json(listPages(mechDir, config ? { locales: config } : {}));
1131
+ if (pathname === "/pages" && req.method === "GET") return json(listPages(mechDir, {
1132
+ ...config ? { locales: config } : {},
1133
+ generated: gen?.pages
1134
+ }));
1119
1135
  if (pathname === "/blocks" && req.method === "GET") {
1120
1136
  if (!options.blocks) return json({ error: "Block listing unavailable" }, 503);
1121
1137
  return json(await options.blocks());
@@ -1123,11 +1139,13 @@ function createDevMiddleware(mechDir, options = {}) {
1123
1139
  if (pathname === "/pages" && req.method === "DELETE") {
1124
1140
  const pathParam = query.get("path");
1125
1141
  if (!pathParam) return json({ error: "Missing path" }, 400);
1142
+ if (blockGenerated(pathParam)) return;
1126
1143
  return json({ success: deletePage(mechDir, pathParam) });
1127
1144
  }
1128
1145
  if (pathname === "/pages/draft" && req.method === "POST") {
1129
1146
  const pathParam = query.get("path");
1130
1147
  if (!pathParam) return json({ error: "Missing path" }, 400);
1148
+ if (blockGenerated(pathParam)) return;
1131
1149
  const body = JSON.parse((await readBody(req)).toString("utf-8"));
1132
1150
  setPageDraft(mechDir, pathParam, body.draft === true, variantCode(query.get("locale")));
1133
1151
  return json({
@@ -1139,6 +1157,7 @@ function createDevMiddleware(mechDir, options = {}) {
1139
1157
  const pathParam = query.get("path");
1140
1158
  const locale = query.get("locale");
1141
1159
  if (!pathParam || !locale) return json({ error: "Missing path or locale" }, 400);
1160
+ if (blockGenerated(pathParam)) return;
1142
1161
  if (!config || locale === config.default || !config.all.includes(locale)) return json({ error: "Unknown locale" }, 400);
1143
1162
  try {
1144
1163
  createTranslation(mechDir, pathParam, locale);
@@ -1152,11 +1171,13 @@ function createDevMiddleware(mechDir, options = {}) {
1152
1171
  const pathParam = query.get("path");
1153
1172
  const locale = query.get("locale");
1154
1173
  if (!pathParam || !locale) return json({ error: "Missing path or locale" }, 400);
1174
+ if (blockGenerated(pathParam)) return;
1155
1175
  return json({ success: deleteTranslation(mechDir, pathParam, locale) });
1156
1176
  }
1157
1177
  if (pathname === "/pages/duplicate" && req.method === "POST") {
1158
1178
  const pathParam = query.get("path");
1159
1179
  if (!pathParam) return json({ error: "Missing path" }, 400);
1180
+ if (blockGenerated(pathParam)) return;
1160
1181
  const body = JSON.parse((await readBody(req)).toString("utf-8"));
1161
1182
  if (!body.path || !body.name) return json({ error: "path and name are required" }, 400);
1162
1183
  try {
@@ -1169,16 +1190,19 @@ function createDevMiddleware(mechDir, options = {}) {
1169
1190
  if (pathname === "/pages" && req.method === "POST") {
1170
1191
  const body = JSON.parse((await readBody(req)).toString("utf-8"));
1171
1192
  const pathParam = query.get("path");
1172
- if (pathParam) try {
1173
- const path = typeof body.path === "string" && body.path.trim() && body.path.trim() !== pathParam ? movePage(mechDir, pathParam, body.path).path : pathParam;
1174
- if (body.name) renamePage(mechDir, path, body.name);
1175
- return json({
1176
- success: true,
1177
- path
1178
- });
1179
- } catch (error) {
1180
- if (error instanceof PageExistsError) return json({ error: { path: "Page already exists" } }, 400);
1181
- throw error;
1193
+ if (pathParam) {
1194
+ if (blockGenerated(pathParam)) return;
1195
+ try {
1196
+ const path = typeof body.path === "string" && body.path.trim() && body.path.trim() !== pathParam ? movePage(mechDir, pathParam, body.path).path : pathParam;
1197
+ if (body.name) renamePage(mechDir, path, body.name);
1198
+ return json({
1199
+ success: true,
1200
+ path
1201
+ });
1202
+ } catch (error) {
1203
+ if (error instanceof PageExistsError) return json({ error: { path: "Page already exists" } }, 400);
1204
+ throw error;
1205
+ }
1182
1206
  }
1183
1207
  if (!body.path || !body.name) return json({ error: "path and name are required" }, 400);
1184
1208
  if (config) {
@@ -1195,7 +1219,7 @@ function createDevMiddleware(mechDir, options = {}) {
1195
1219
  if (pathname === "/save" && req.method === "POST") {
1196
1220
  const pathParam = query.get("path");
1197
1221
  if (!pathParam) return json({ error: "Missing path" }, 400);
1198
- if ((await options.generated?.())?.logical.has(pathParam)) return json({ error: "This page is generated and read-only" }, 409);
1222
+ if (blockGenerated(pathParam)) return;
1199
1223
  const locale = variantCode(query.get("locale"));
1200
1224
  const body = JSON.parse((await readBody(req)).toString("utf-8"));
1201
1225
  if (!body.force && typeof body.version === "string") {
@@ -1473,6 +1497,7 @@ function mechanica(options = {}) {
1473
1497
  logical.add(page.path);
1474
1498
  }
1475
1499
  return {
1500
+ pages,
1476
1501
  byServed,
1477
1502
  logical
1478
1503
  };
@@ -10,6 +10,8 @@ export interface PageItem {
10
10
  * sites — for the language coverage badges. Absent when i18n is off.
11
11
  */
12
12
  locales?: string[];
13
+ /** A programmatically generated page (plugin `generatePages`) — read-only, no file. */
14
+ generated?: boolean;
13
15
  }
14
16
  /**
15
17
  * Fetch the project's pages from the dev server, memoized for the session so
@@ -14,11 +14,13 @@ export interface DevMiddlewareOptions {
14
14
  /** The site's locale config (multi-language sites); null/absent = i18n off. */
15
15
  locales?: LocalesConfig | null;
16
16
  /**
17
- * Generated routes (plugin `generatePages`): a served-path → page map (for
18
- * `/state`) and the set of their logical paths (for the read-only `/save`
19
- * guard). Absent when no providers are configured.
17
+ * Generated routes (plugin `generatePages`): the baked page list (for the
18
+ * `/pages` + `/query` listings), a served-path → page map (for `/state`), and
19
+ * the set of their logical paths (for the read-only mutation guards). Absent
20
+ * when no providers are configured.
20
21
  */
21
22
  generated?: () => Promise<{
23
+ pages: VirtualPage[];
22
24
  byServed: Map<string, VirtualPage>;
23
25
  logical: Set<string>;
24
26
  }>;
@@ -1,4 +1,4 @@
1
- import { type Block, type ContentBlock, type LocalesConfig } from 'mechanica-shared';
1
+ import { type Block, type ContentBlock, type LocalesConfig, type VirtualPage } from 'mechanica-shared';
2
2
  import { type PageDoc, type RichTextCodec } from 'mechanica-shared/page-format';
3
3
  /** Shape of a page file under `<mech>/pages` (the parsed `.page.md` document). */
4
4
  export type PageFile = PageDoc;
@@ -49,6 +49,12 @@ export interface PageListItem {
49
49
  * i18n is off. Translation files never get their own list row.
50
50
  */
51
51
  locales?: string[];
52
+ /**
53
+ * True for a programmatically generated page ({@link VirtualPage} from the
54
+ * plugin's `generatePages`): it has no file, so the editor lists it read-only
55
+ * (no rename / duplicate / delete / translate).
56
+ */
57
+ generated?: boolean;
52
58
  [key: string]: unknown;
53
59
  }
54
60
  /** Thrown by {@link createPage} when the target page already exists. */
@@ -150,4 +156,5 @@ export declare function listPages(mechDir: string, options?: {
150
156
  }[];
151
157
  locales?: LocalesConfig;
152
158
  locale?: string;
159
+ generated?: VirtualPage[];
153
160
  }): PageListItem[];
@@ -1,13 +1,14 @@
1
- import { type QueryContext, type QuerySource } from 'mechanica-shared';
1
+ import { type QueryContext, type QuerySource, type VirtualPage } from 'mechanica-shared';
2
2
  /**
3
3
  * The dev server's {@link QuerySource}: pages come live from the `.mech`
4
4
  * store; `useFetch` URLs are fetched server-side (no CORS, matching how the
5
- * export resolves them at build time).
5
+ * export resolves them at build time). `generated` pages (plugin `generatePages`)
6
+ * are merged into the page listing so `usePages()` sees them, like the export.
6
7
  */
7
- export declare function devQuerySource(mechDir: string): QuerySource;
8
+ export declare function devQuerySource(mechDir: string, generated?: VirtualPage[]): QuerySource;
8
9
  /**
9
10
  * Resolve a dev-mode query key against the local `.mech` store. Keys are of the
10
11
  * form `"<type>.<json-args>"` (the same encoding the runtime composables use);
11
12
  * `context.page` selects the chunk of a paginated query.
12
13
  */
13
- export declare function resolveDevQuery(mechDir: string, key: string, context?: QueryContext): Promise<unknown>;
14
+ export declare function resolveDevQuery(mechDir: string, key: string, context?: QueryContext, generated?: VirtualPage[]): Promise<unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mechanica",
3
- "version": "2.0.0-alpha.7",
3
+ "version": "2.0.0-alpha.8",
4
4
  "description": "Build Vue 3 websites with a visual block editor",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -63,7 +63,7 @@
63
63
  "prepublishOnly": "bun run build"
64
64
  },
65
65
  "dependencies": {
66
- "mechanica-shared": "2.0.0-alpha.4",
66
+ "mechanica-shared": "2.0.0-alpha.8",
67
67
  "@vue/compiler-sfc": "^3.5.38",
68
68
  "compact-json-schema": "^0.1.5",
69
69
  "estree-walker": "^3.0.3",
package/src/cli/export.ts CHANGED
@@ -474,7 +474,7 @@ export async function exportProject(
474
474
  // installed so page data read by `listPages` matches what pages render with.
475
475
  setPageCodec(richText)
476
476
  const querySource: QuerySource = {
477
- listPages: (opts) => listPages(join(cwd, '.mech'), opts),
477
+ listPages: (opts) => listPages(join(cwd, '.mech'), { ...opts, generated: ssr.generatedPages }),
478
478
  fetchJson: async ({ url, ...init }) => {
479
479
  const res = await fetch(url, init as RequestInit)
480
480
  if (!res.ok) throw new Error(`${url} responded ${res.status}`)
@@ -83,6 +83,7 @@
83
83
  <span class="mech-pages__cell-name">
84
84
  <span class="mech-pages__name">{{ page.name || page.path }}</span>
85
85
  <span v-if="page.draft" class="mech-pages__badge is-draft">Draft</span>
86
+ <span v-if="page.generated" class="mech-pages__badge">Generated</span>
86
87
  <span v-if="page.path === currentLogical" class="mech-pages__badge">Current</span>
87
88
  <span v-if="localeConfig && page.locales" class="mech-pages__locales">
88
89
  <span
@@ -96,7 +97,8 @@
96
97
  </span>
97
98
  </span>
98
99
  <span class="mech-pages__cell-path">{{ page.path }}</span>
99
- <span class="mech-pages__actions" @click.stop>
100
+ <!-- Generated pages have no file — they're read-only (no edit actions). -->
101
+ <span v-if="!page.generated" class="mech-pages__actions" @click.stop>
100
102
  <button type="button" title="Rename" @click="startEdit(page)"><VIcon name="pencil" /></button>
101
103
  <button type="button" title="Duplicate" @click="startDuplicate(page)"><VIcon name="copy" /></button>
102
104
  <button type="button" title="Delete" class="is-danger" @click="confirmRemove(page)">
@@ -275,6 +277,11 @@ const open = (page: PageItem) => openPath(page.path)
275
277
  function onRowMenu(event: MouseEvent, page: PageItem) {
276
278
  if (!contextMenu) return
277
279
  activePath.value = page.path
280
+ // Generated pages have no file — offer only "Open" (read-only view).
281
+ if (page.generated) {
282
+ contextMenu.openAt(event, [{ label: 'Open', onClick: () => open(page) }])
283
+ return
284
+ }
278
285
  contextMenu.openAt(event, [
279
286
  { label: 'Open', onClick: () => open(page) },
280
287
  { label: 'Rename', onClick: () => startEdit(page) },
@@ -10,6 +10,8 @@ export interface PageItem {
10
10
  * sites — for the language coverage badges. Absent when i18n is off.
11
11
  */
12
12
  locales?: string[]
13
+ /** A programmatically generated page (plugin `generatePages`) — read-only, no file. */
14
+ generated?: boolean
13
15
  }
14
16
 
15
17
  let pagesPromise: Promise<PageItem[]> | null = null
@@ -53,11 +53,12 @@ export interface DevMiddlewareOptions {
53
53
  /** The site's locale config (multi-language sites); null/absent = i18n off. */
54
54
  locales?: LocalesConfig | null
55
55
  /**
56
- * Generated routes (plugin `generatePages`): a served-path → page map (for
57
- * `/state`) and the set of their logical paths (for the read-only `/save`
58
- * guard). Absent when no providers are configured.
56
+ * Generated routes (plugin `generatePages`): the baked page list (for the
57
+ * `/pages` + `/query` listings), a served-path → page map (for `/state`), and
58
+ * the set of their logical paths (for the read-only mutation guards). Absent
59
+ * when no providers are configured.
59
60
  */
60
- generated?: () => Promise<{ byServed: Map<string, VirtualPage>; logical: Set<string> }>
61
+ generated?: () => Promise<{ pages: VirtualPage[]; byServed: Map<string, VirtualPage>; logical: Set<string> }>
61
62
  }
62
63
 
63
64
  /**
@@ -105,6 +106,18 @@ export function createDevMiddleware(
105
106
  }
106
107
 
107
108
  try {
109
+ // Generated pages (plugin `generatePages`), resolved once (cached
110
+ // upstream): used to serve their state, list them, and reject writes.
111
+ const gen = options.generated ? await options.generated() : null
112
+ /** Reject a mutation targeting a file-less generated page. */
113
+ const blockGenerated = (path: string | null): boolean => {
114
+ if (path && gen?.logical.has(path)) {
115
+ json({ error: 'This page is generated and read-only' }, 409)
116
+ return true
117
+ }
118
+ return false
119
+ }
120
+
108
121
  if (pathname.startsWith('/assets/')) return serveFrom('assets', '/assets/')
109
122
  if (pathname.startsWith('/thumbs/')) {
110
123
  // Regenerated in place by `mechanica thumbs` — always revalidate.
@@ -121,7 +134,9 @@ export function createDevMiddleware(
121
134
  // `page` selects the chunk of a paginated query (variant URLs like /blog/2);
122
135
  // `locale` scopes a `getPages` listing to a translation.
123
136
  const page = Number(query.get('page') ?? '') || undefined
124
- return json(await resolveDevQuery(mechDir, key, { page, locale: variantCode(query.get('locale')) }))
137
+ return json(
138
+ await resolveDevQuery(mechDir, key, { page, locale: variantCode(query.get('locale')) }, gen?.pages),
139
+ )
125
140
  }
126
141
 
127
142
  if (pathname === '/upload' && req.method === 'POST') {
@@ -144,13 +159,13 @@ export function createDevMiddleware(
144
159
  const pathParam = query.get('path')
145
160
  if (!pathParam) return json({ error: 'Missing path' }, 400)
146
161
  await options.ready?.()
147
- const generated = (await options.generated?.())?.byServed.get(pathParam)
162
+ const generated = gen?.byServed.get(pathParam)
148
163
  if (generated) return json(buildGeneratedState(mechDir, generated, config))
149
164
  return json(buildPageState(mechDir, pathParam, config))
150
165
  }
151
166
 
152
167
  if (pathname === '/pages' && req.method === 'GET') {
153
- return json(listPages(mechDir, config ? { locales: config } : {}))
168
+ return json(listPages(mechDir, { ...(config ? { locales: config } : {}), generated: gen?.pages }))
154
169
  }
155
170
 
156
171
  if (pathname === '/blocks' && req.method === 'GET') {
@@ -161,12 +176,14 @@ export function createDevMiddleware(
161
176
  if (pathname === '/pages' && req.method === 'DELETE') {
162
177
  const pathParam = query.get('path')
163
178
  if (!pathParam) return json({ error: 'Missing path' }, 400)
179
+ if (blockGenerated(pathParam)) return
164
180
  return json({ success: deletePage(mechDir, pathParam) })
165
181
  }
166
182
 
167
183
  if (pathname === '/pages/draft' && req.method === 'POST') {
168
184
  const pathParam = query.get('path')
169
185
  if (!pathParam) return json({ error: 'Missing path' }, 400)
186
+ if (blockGenerated(pathParam)) return
170
187
  const body = JSON.parse((await readBody(req)).toString('utf-8'))
171
188
  setPageDraft(mechDir, pathParam, body.draft === true, variantCode(query.get('locale')))
172
189
  return json({ success: true, draft: body.draft === true })
@@ -178,6 +195,7 @@ export function createDevMiddleware(
178
195
  const pathParam = query.get('path')
179
196
  const locale = query.get('locale')
180
197
  if (!pathParam || !locale) return json({ error: 'Missing path or locale' }, 400)
198
+ if (blockGenerated(pathParam)) return
181
199
  if (!config || locale === config.default || !config.all.includes(locale)) {
182
200
  return json({ error: 'Unknown locale' }, 400)
183
201
  }
@@ -194,12 +212,14 @@ export function createDevMiddleware(
194
212
  const pathParam = query.get('path')
195
213
  const locale = query.get('locale')
196
214
  if (!pathParam || !locale) return json({ error: 'Missing path or locale' }, 400)
215
+ if (blockGenerated(pathParam)) return
197
216
  return json({ success: deleteTranslation(mechDir, pathParam, locale) })
198
217
  }
199
218
 
200
219
  if (pathname === '/pages/duplicate' && req.method === 'POST') {
201
220
  const pathParam = query.get('path')
202
221
  if (!pathParam) return json({ error: 'Missing path' }, 400)
222
+ if (blockGenerated(pathParam)) return
203
223
  const body = JSON.parse((await readBody(req)).toString('utf-8'))
204
224
  if (!body.path || !body.name) return json({ error: 'path and name are required' }, 400)
205
225
  try {
@@ -214,6 +234,7 @@ export function createDevMiddleware(
214
234
  const body = JSON.parse((await readBody(req)).toString('utf-8'))
215
235
  const pathParam = query.get('path')
216
236
  if (pathParam) {
237
+ if (blockGenerated(pathParam)) return
217
238
  // Editing an existing page: optionally move it (new path) and/or rename it.
218
239
  try {
219
240
  const wantsMove = typeof body.path === 'string' && body.path.trim() && body.path.trim() !== pathParam
@@ -248,9 +269,7 @@ export function createDevMiddleware(
248
269
  if (!pathParam) return json({ error: 'Missing path' }, 400)
249
270
  // Generated pages (plugin `generatePages`) have no file — reject a save
250
271
  // rather than writing a stray `.page.md` at their path.
251
- if ((await options.generated?.())?.logical.has(pathParam)) {
252
- return json({ error: 'This page is generated and read-only' }, 409)
253
- }
272
+ if (blockGenerated(pathParam)) return
254
273
  // A non-default `locale` writes to that translation's variant file; the
255
274
  // default locale (or i18n off) writes the base page.
256
275
  const locale = variantCode(query.get('locale'))
@@ -9,6 +9,7 @@ import {
9
9
  type Block,
10
10
  type ContentBlock,
11
11
  type LocalesConfig,
12
+ type VirtualPage,
12
13
  } from 'mechanica-shared'
13
14
  import { parsePage, serializePage, type PageDoc, type RichTextCodec } from 'mechanica-shared/page-format'
14
15
  import { applyImageManifest, harvestImageMeta, readImageManifest, updateImageManifest } from './assets-store'
@@ -128,6 +129,12 @@ export interface PageListItem {
128
129
  * i18n is off. Translation files never get their own list row.
129
130
  */
130
131
  locales?: string[]
132
+ /**
133
+ * True for a programmatically generated page ({@link VirtualPage} from the
134
+ * plugin's `generatePages`): it has no file, so the editor lists it read-only
135
+ * (no rename / duplicate / delete / translate).
136
+ */
137
+ generated?: boolean
131
138
  [key: string]: unknown
132
139
  }
133
140
 
@@ -451,7 +458,7 @@ export function listFolders(mechDir: string): { id: string; path: string; name:
451
458
  */
452
459
  export function listPages(
453
460
  mechDir: string,
454
- options: { data?: { id: string }[]; locales?: LocalesConfig; locale?: string } = {},
461
+ options: { data?: { id: string }[]; locales?: LocalesConfig; locale?: string; generated?: VirtualPage[] } = {},
455
462
  ): PageListItem[] {
456
463
  const pagesDir = join(mechDir, 'pages')
457
464
  if (!fs.existsSync(pagesDir)) return []
@@ -509,9 +516,57 @@ export function listPages(
509
516
  items.push(item)
510
517
  }
511
518
 
519
+ // Programmatically generated pages (plugin `generatePages`) join the listing —
520
+ // one read-only row per logical path — so they show in the editor's page
521
+ // browser, resolve in `usePages()` queries, and pass `mechanica shot`'s
522
+ // page-exists check, just like file pages.
523
+ if (options.generated?.length) items.push(...generatedPageItems(options.generated, options, config))
524
+
512
525
  return items.sort(comparePages)
513
526
  }
514
527
 
528
+ /** File-less pages → list rows: one per logical path, with a `locales` union. */
529
+ function generatedPageItems(
530
+ generated: VirtualPage[],
531
+ options: { data?: { id: string }[]; locale?: string },
532
+ config: LocalesConfig | null,
533
+ ): PageListItem[] {
534
+ const groups = new Map<string, VirtualPage[]>()
535
+ for (const vp of generated) {
536
+ const arr = groups.get(vp.path)
537
+ if (arr) arr.push(vp)
538
+ else groups.set(vp.path, [vp])
539
+ }
540
+
541
+ const items: PageListItem[] = []
542
+ for (const [path, vps] of groups) {
543
+ const locales = config ? (vps.map((v) => v.locale).filter(Boolean) as string[]) : []
544
+ // Locale-scoped listing (a translated query): skip a page absent in it.
545
+ if (options.locale && !locales.includes(options.locale)) continue
546
+ const source =
547
+ (options.locale ? vps.find((v) => v.locale === options.locale) : undefined) ??
548
+ (config ? vps.find((v) => v.locale === config.default) : undefined) ??
549
+ vps[0]!
550
+ const embedded: Record<string, unknown> = {}
551
+ for (const dataEntry of options.data ?? []) embedded[dataEntry.id] = source.data?.[dataEntry.id] ?? {}
552
+
553
+ const dir = path.slice(1).split('/').slice(0, -1).join('/')
554
+ const item: PageListItem = {
555
+ path,
556
+ name: (typeof source.meta?.title === 'string' ? source.meta.title : undefined) ?? path.split('/').pop() ?? path,
557
+ folderPath: dir === '' ? null : dir,
558
+ order: 0,
559
+ orderAfter: null,
560
+ draft: false,
561
+ generated: true,
562
+ ...embedded,
563
+ }
564
+ if (config) item.locales = config.all.filter((code) => locales.includes(code))
565
+ items.push(item)
566
+ }
567
+ return items
568
+ }
569
+
515
570
  /** Ordering: by folder, then explicit `orderAfter`, then `order`, then path. */
516
571
  function comparePages(a: PageListItem, b: PageListItem): number {
517
572
  if (a.folderPath === b.folderPath) {
@@ -1,14 +1,15 @@
1
- import { resolveQueryKey, type QueryContext, type QuerySource } from 'mechanica-shared'
1
+ import { resolveQueryKey, type QueryContext, type QuerySource, type VirtualPage } from 'mechanica-shared'
2
2
  import { listPages } from './pages-store'
3
3
 
4
4
  /**
5
5
  * The dev server's {@link QuerySource}: pages come live from the `.mech`
6
6
  * store; `useFetch` URLs are fetched server-side (no CORS, matching how the
7
- * export resolves them at build time).
7
+ * export resolves them at build time). `generated` pages (plugin `generatePages`)
8
+ * are merged into the page listing so `usePages()` sees them, like the export.
8
9
  */
9
- export function devQuerySource(mechDir: string): QuerySource {
10
+ export function devQuerySource(mechDir: string, generated?: VirtualPage[]): QuerySource {
10
11
  return {
11
- listPages: (options) => listPages(mechDir, options),
12
+ listPages: (options) => listPages(mechDir, { ...options, generated }),
12
13
  fetchJson: async ({ url, ...init }) => {
13
14
  const res = await fetch(url, init as RequestInit)
14
15
  if (!res.ok) throw new Error(`${url} responded ${res.status}`)
@@ -22,6 +23,11 @@ export function devQuerySource(mechDir: string): QuerySource {
22
23
  * form `"<type>.<json-args>"` (the same encoding the runtime composables use);
23
24
  * `context.page` selects the chunk of a paginated query.
24
25
  */
25
- export function resolveDevQuery(mechDir: string, key: string, context: QueryContext = {}): Promise<unknown> {
26
- return resolveQueryKey(devQuerySource(mechDir), key, context)
26
+ export function resolveDevQuery(
27
+ mechDir: string,
28
+ key: string,
29
+ context: QueryContext = {},
30
+ generated?: VirtualPage[],
31
+ ): Promise<unknown> {
32
+ return resolveQueryKey(devQuerySource(mechDir, generated), key, context)
27
33
  }
@@ -165,8 +165,13 @@ export function mechanica(options: MechanicaPluginOptions = {}): Plugin {
165
165
  }
166
166
  return generatedPagesCache
167
167
  }
168
- let generatedIndexCache: Promise<{ byServed: Map<string, VirtualPage>; logical: Set<string> }> | null = null
169
- const loadGeneratedIndex = (): Promise<{ byServed: Map<string, VirtualPage>; logical: Set<string> }> => {
168
+ interface GeneratedIndex {
169
+ pages: VirtualPage[]
170
+ byServed: Map<string, VirtualPage>
171
+ logical: Set<string>
172
+ }
173
+ let generatedIndexCache: Promise<GeneratedIndex> | null = null
174
+ const loadGeneratedIndex = (): Promise<GeneratedIndex> => {
170
175
  if (!generatedIndexCache) {
171
176
  generatedIndexCache = loadGeneratedPages().then((pages) => {
172
177
  const byServed = new Map<string, VirtualPage>()
@@ -175,7 +180,7 @@ export function mechanica(options: MechanicaPluginOptions = {}): Plugin {
175
180
  byServed.set(generatedServedPath(page, locales), page)
176
181
  logical.add(page.path)
177
182
  }
178
- return { byServed, logical }
183
+ return { pages, byServed, logical }
179
184
  })
180
185
  }
181
186
  return generatedIndexCache