@kdrgny/justjson 1.2.0 → 1.4.0

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.
package/dist/cli.js CHANGED
@@ -4054,6 +4054,20 @@ var coerce = {
4054
4054
  var NEVER = INVALID;
4055
4055
 
4056
4056
  // ../core/dist/index.js
4057
+ var JustJsonError = class extends Error {
4058
+ constructor(message) {
4059
+ super(message);
4060
+ this.name = new.target.name;
4061
+ }
4062
+ };
4063
+ var NotFoundError = class extends JustJsonError {
4064
+ };
4065
+ var UnsafeSlugError = class extends JustJsonError {
4066
+ };
4067
+ var PathEscapeError = class extends JustJsonError {
4068
+ };
4069
+ var SchemaError = class extends JustJsonError {
4070
+ };
4057
4071
  var fieldTypes = [
4058
4072
  "text",
4059
4073
  "richtext",
@@ -4062,23 +4076,34 @@ var fieldTypes = [
4062
4076
  "date",
4063
4077
  "select",
4064
4078
  "relation",
4065
- "image"
4079
+ "image",
4080
+ "url",
4081
+ "email",
4082
+ "list",
4083
+ "color",
4084
+ "group"
4066
4085
  ];
4067
- var zField = external_exports.object({
4068
- key: external_exports.string().min(1),
4069
- label: external_exports.string().optional(),
4070
- type: external_exports.enum(fieldTypes),
4071
- required: external_exports.boolean().optional(),
4072
- options: external_exports.array(external_exports.string()).optional(),
4073
- to: external_exports.string().optional()
4074
- }).superRefine((field, ctx) => {
4075
- if (field.type === "select" && (!field.options || field.options.length === 0)) {
4076
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: "select alan\u0131 options gerektirir" });
4077
- }
4078
- if (field.type === "relation" && !field.to) {
4079
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: 'relation alan\u0131 "to" gerektirir' });
4080
- }
4081
- });
4086
+ var zField = external_exports.lazy(
4087
+ () => external_exports.object({
4088
+ key: external_exports.string().min(1),
4089
+ label: external_exports.string().optional(),
4090
+ type: external_exports.enum(fieldTypes),
4091
+ required: external_exports.boolean().optional(),
4092
+ options: external_exports.array(external_exports.string()).optional(),
4093
+ to: external_exports.string().optional(),
4094
+ fields: external_exports.array(zField).optional()
4095
+ }).superRefine((field, ctx) => {
4096
+ if (field.type === "select" && (!field.options || field.options.length === 0)) {
4097
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: "a select field requires options" });
4098
+ }
4099
+ if (field.type === "relation" && !field.to) {
4100
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: 'a relation field requires "to"' });
4101
+ }
4102
+ if (field.type === "group" && (!field.fields || field.fields.length === 0)) {
4103
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: "a group field requires fields" });
4104
+ }
4105
+ })
4106
+ );
4082
4107
  var zCollection = external_exports.object({
4083
4108
  name: external_exports.string().min(1),
4084
4109
  label: external_exports.string().optional(),
@@ -4102,30 +4127,38 @@ var zSchema = external_exports.object({
4102
4127
  const containers = [...schema.collections, ...schema.singletons];
4103
4128
  for (const c of containers) {
4104
4129
  if (names.has(c.name)) {
4105
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `tekrar eden ad: ${c.name}` });
4130
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `duplicate name: ${c.name}` });
4106
4131
  }
4107
4132
  names.add(c.name);
4108
4133
  if (c.path.includes("..") || c.path.startsWith("/") || c.path.includes("\\")) {
4109
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `g\xFCvensiz path: ${c.path}` });
4134
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `unsafe path: ${c.path}` });
4110
4135
  }
4111
4136
  if (paths.has(c.path)) {
4112
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `tekrar eden path: ${c.path}` });
4137
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `duplicate path: ${c.path}` });
4113
4138
  }
4114
4139
  paths.add(c.path);
4115
4140
  const keys = /* @__PURE__ */ new Set();
4116
4141
  for (const f of c.fields) {
4117
4142
  if (keys.has(f.key)) {
4118
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `tekrar eden field key: ${f.key}` });
4143
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `duplicate field key: ${f.key}` });
4119
4144
  }
4120
4145
  keys.add(f.key);
4121
4146
  if (f.type === "relation" && f.to && !collectionNames.has(f.to)) {
4122
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `relation hedefi yok: ${f.to}` });
4147
+ ctx.addIssue({
4148
+ code: external_exports.ZodIssueCode.custom,
4149
+ message: `relation target does not exist: ${f.to}`
4150
+ });
4123
4151
  }
4124
4152
  }
4125
4153
  }
4126
4154
  });
4127
4155
  function parseSchema(input) {
4128
- return zSchema.parse(input);
4156
+ const result = zSchema.safeParse(input);
4157
+ if (result.success) return result.data;
4158
+ const lines = result.error.issues.map(
4159
+ (i) => i.path.length ? `${i.path.join(".")}: ${i.message}` : i.message
4160
+ );
4161
+ throw new SchemaError(lines.join("\n"));
4129
4162
  }
4130
4163
  function serializeSchema(schema) {
4131
4164
  return `${JSON.stringify(schema, null, 2)}
@@ -4149,7 +4182,7 @@ var TR = {
4149
4182
  function slugify(input) {
4150
4183
  const mapped = input.replace(/[çğıöşüÇĞİÖŞÜI]/g, (c) => TR[c] ?? c);
4151
4184
  const slug = mapped.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4152
- return slug || "icerik";
4185
+ return slug || "content";
4153
4186
  }
4154
4187
  var TITLE_KEYS = ["title", "name", "baslik", "ba\u015Fl\u0131k", "ad", "label", "heading", "isim"];
4155
4188
  function entryTitle(fields, data) {
@@ -4171,7 +4204,7 @@ function schemaPath(contentDir) {
4171
4204
  }
4172
4205
  function assertSafeSlug(slug) {
4173
4206
  if (slug.length === 0 || slug.includes("/") || slug.includes("\\") || slug.includes("..")) {
4174
- throw new Error(`G\xFCvensiz slug: ${slug}`);
4207
+ throw new UnsafeSlugError(`Unsafe slug: ${slug}`);
4175
4208
  }
4176
4209
  }
4177
4210
  async function loadSchema(adapter, contentDir = "content") {
@@ -4193,12 +4226,12 @@ var ContentStore = class {
4193
4226
  contentDir;
4194
4227
  collection(name) {
4195
4228
  const col = this.schema.collections.find((c) => c.name === name);
4196
- if (!col) throw new Error(`Bilinmeyen koleksiyon: ${name}`);
4229
+ if (!col) throw new NotFoundError(`Bilinmeyen koleksiyon: ${name}`);
4197
4230
  return col;
4198
4231
  }
4199
4232
  singleton(name) {
4200
4233
  const s = this.schema.singletons.find((x) => x.name === name);
4201
- if (!s) throw new Error(`Bilinmeyen singleton: ${name}`);
4234
+ if (!s) throw new NotFoundError(`Bilinmeyen singleton: ${name}`);
4202
4235
  return s;
4203
4236
  }
4204
4237
  entryPath(col, slug) {
@@ -4251,6 +4284,206 @@ var ContentStore = class {
4251
4284
  `);
4252
4285
  }
4253
4286
  };
4287
+ function isEmpty(value) {
4288
+ return value === void 0 || value === null || value === "";
4289
+ }
4290
+ var URL_RE = /^[a-z][a-z0-9+.-]*:\/\/.+/i;
4291
+ var EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
4292
+ var COLOR_RE = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
4293
+ function typeError(field, value) {
4294
+ switch (field.type) {
4295
+ case "text":
4296
+ case "richtext":
4297
+ case "date":
4298
+ case "image":
4299
+ return typeof value === "string" ? null : "expected text";
4300
+ case "url":
4301
+ if (typeof value !== "string") return "expected text";
4302
+ return URL_RE.test(value) ? null : "expected a valid URL";
4303
+ case "email":
4304
+ if (typeof value !== "string") return "expected text";
4305
+ return EMAIL_RE.test(value) ? null : "expected a valid email address";
4306
+ case "color":
4307
+ if (typeof value !== "string") return "expected text";
4308
+ return COLOR_RE.test(value) ? null : "expected a hex color (e.g. #ff0000)";
4309
+ case "list":
4310
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
4311
+ return "expected a list of text";
4312
+ }
4313
+ return null;
4314
+ case "relation":
4315
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
4316
+ return "expected a list of slugs";
4317
+ }
4318
+ return null;
4319
+ case "group":
4320
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
4321
+ return "expected an object";
4322
+ }
4323
+ return null;
4324
+ case "number":
4325
+ return typeof value === "number" ? null : "expected a number";
4326
+ case "boolean":
4327
+ return typeof value === "boolean" ? null : "expected true or false";
4328
+ case "select":
4329
+ if (typeof value !== "string") return "expected text";
4330
+ return field.options?.includes(value) ? null : "not one of the options";
4331
+ }
4332
+ }
4333
+ function validateEntry(fields, data) {
4334
+ const issues = [];
4335
+ const known = new Set(fields.map((f) => f.key));
4336
+ for (const field of fields) {
4337
+ const value = data[field.key];
4338
+ if (isEmpty(value)) {
4339
+ if (field.required) {
4340
+ issues.push({
4341
+ key: field.key,
4342
+ level: "warning",
4343
+ kind: "required",
4344
+ message: "required field is empty"
4345
+ });
4346
+ }
4347
+ continue;
4348
+ }
4349
+ const err = typeError(field, value);
4350
+ if (err) issues.push({ key: field.key, level: "error", kind: "type", message: err });
4351
+ }
4352
+ for (const key of Object.keys(data)) {
4353
+ if (key.startsWith("_")) continue;
4354
+ if (!known.has(key)) {
4355
+ issues.push({
4356
+ key,
4357
+ level: "warning",
4358
+ kind: "unknown-key",
4359
+ message: "key is not in the schema"
4360
+ });
4361
+ }
4362
+ }
4363
+ return { ok: issues.every((i) => i.level !== "error"), issues };
4364
+ }
4365
+ function basename(path) {
4366
+ return path.split(/[/\\]/).pop() ?? path;
4367
+ }
4368
+ function imageFields(fields) {
4369
+ return fields.filter((f) => f.type === "image");
4370
+ }
4371
+ function relationFields(fields) {
4372
+ return fields.filter((f) => f.type === "relation");
4373
+ }
4374
+ function validateProject(schema, content) {
4375
+ const issues = [];
4376
+ const mediaNames = content.media ? new Set(content.media.map(basename)) : null;
4377
+ const schemaCollectionNames = new Set(schema.collections.map((c) => c.name));
4378
+ for (const name of Object.keys(content.collections)) {
4379
+ if (!schemaCollectionNames.has(name)) {
4380
+ issues.push({
4381
+ level: "warning",
4382
+ kind: "schema-content-mismatch",
4383
+ collection: name,
4384
+ message: `Collection folder '${name}' is not in the schema`
4385
+ });
4386
+ }
4387
+ }
4388
+ const slugsByCollection = {};
4389
+ for (const [name, entries] of Object.entries(content.collections)) {
4390
+ slugsByCollection[name] = new Set(entries.map((e) => e.slug));
4391
+ }
4392
+ for (const col of schema.collections) {
4393
+ const entries = content.collections[col.name] ?? [];
4394
+ const seen = /* @__PURE__ */ new Set();
4395
+ const reported = /* @__PURE__ */ new Set();
4396
+ for (const { slug } of entries) {
4397
+ if (seen.has(slug) && !reported.has(slug)) {
4398
+ reported.add(slug);
4399
+ issues.push({
4400
+ level: "error",
4401
+ kind: "duplicate-slug",
4402
+ collection: col.name,
4403
+ slug,
4404
+ message: `Duplicate slug in '${col.name}': ${slug}`
4405
+ });
4406
+ }
4407
+ seen.add(slug);
4408
+ }
4409
+ const rels = relationFields(col.fields);
4410
+ const imgs = imageFields(col.fields);
4411
+ for (const { slug, data } of entries) {
4412
+ for (const issue of validateEntry(col.fields, data).issues) {
4413
+ issues.push({
4414
+ level: issue.level,
4415
+ kind: issue.kind,
4416
+ collection: col.name,
4417
+ slug,
4418
+ field: issue.key,
4419
+ message: issue.message
4420
+ });
4421
+ }
4422
+ for (const field of rels) {
4423
+ const value = data[field.key];
4424
+ if (!Array.isArray(value)) continue;
4425
+ const target = field.to ? slugsByCollection[field.to] : void 0;
4426
+ for (const ref of value) {
4427
+ if (typeof ref !== "string") continue;
4428
+ if (!target || !target.has(ref)) {
4429
+ issues.push({
4430
+ level: "error",
4431
+ kind: "broken-relation",
4432
+ collection: col.name,
4433
+ slug,
4434
+ field: field.key,
4435
+ message: field.to ? `Broken relation: '${ref}' does not exist in '${field.to}'` : `Relation field '${field.key}' has no target collection`
4436
+ });
4437
+ }
4438
+ }
4439
+ }
4440
+ if (mediaNames) {
4441
+ for (const field of imgs) {
4442
+ const value = data[field.key];
4443
+ if (typeof value !== "string" || value === "") continue;
4444
+ if (!mediaNames.has(basename(value))) {
4445
+ issues.push({
4446
+ level: "error",
4447
+ kind: "missing-media",
4448
+ collection: col.name,
4449
+ slug,
4450
+ field: field.key,
4451
+ message: `Missing media: ${value}`
4452
+ });
4453
+ }
4454
+ }
4455
+ }
4456
+ }
4457
+ }
4458
+ for (const s of schema.singletons) {
4459
+ const data = content.singletons[s.name] ?? {};
4460
+ for (const issue of validateEntry(s.fields, data).issues) {
4461
+ issues.push({
4462
+ level: issue.level,
4463
+ kind: issue.kind,
4464
+ singleton: s.name,
4465
+ field: issue.key,
4466
+ message: issue.message
4467
+ });
4468
+ }
4469
+ if (mediaNames) {
4470
+ for (const field of imageFields(s.fields)) {
4471
+ const value = data[field.key];
4472
+ if (typeof value !== "string" || value === "") continue;
4473
+ if (!mediaNames.has(basename(value))) {
4474
+ issues.push({
4475
+ level: "error",
4476
+ kind: "missing-media",
4477
+ singleton: s.name,
4478
+ field: field.key,
4479
+ message: `Missing media: ${value}`
4480
+ });
4481
+ }
4482
+ }
4483
+ }
4484
+ }
4485
+ return issues;
4486
+ }
4254
4487
  function pascalCase(name) {
4255
4488
  return name.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
4256
4489
  }
@@ -4263,7 +4496,14 @@ function fieldTsType(field) {
4263
4496
  case "select":
4264
4497
  return (field.options ?? []).map((o) => `'${o}'`).join(" | ") || "string";
4265
4498
  case "relation":
4499
+ case "list":
4266
4500
  return "string[]";
4501
+ case "group": {
4502
+ const subs = field.fields ?? [];
4503
+ if (subs.length === 0) return "Record<string, unknown>";
4504
+ const inner = subs.map((f) => `${f.key}${f.required ? "" : "?"}: ${fieldTsType(f)}`).join("; ");
4505
+ return `{ ${inner} }`;
4506
+ }
4267
4507
  default:
4268
4508
  return "string";
4269
4509
  }
@@ -4278,7 +4518,7 @@ ${lines.join("\n")}
4278
4518
  }`;
4279
4519
  }
4280
4520
  function generateTypes(schema) {
4281
- const blocks = ["// JustJSON taraf\u0131ndan \xFCretildi \u2014 elle d\xFCzenlemeyin."];
4521
+ const blocks = ["// Generated by JustJSON \u2014 do not edit by hand."];
4282
4522
  for (const col of schema.collections) {
4283
4523
  const name = pascalCase(col.name);
4284
4524
  blocks.push(emitInterface(name, col.fields));
@@ -4290,6 +4530,71 @@ function generateTypes(schema) {
4290
4530
  return `${blocks.join("\n\n")}
4291
4531
  `;
4292
4532
  }
4533
+ function pascalCase2(name) {
4534
+ return name.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
4535
+ }
4536
+ var IMPORTS = `import { readFile, readdir } from 'node:fs/promises'
4537
+ import { dirname, join } from 'node:path'
4538
+ import { fileURLToPath } from 'node:url'`;
4539
+ var READERS = `export type WithSlug<T> = T & { slug: string }
4540
+
4541
+ export interface LoadOptions {
4542
+ /** true ise _status: 'draft' olan kay\u0131tlar da dahil edilir (varsay\u0131lan: yaln\u0131zca yay\u0131nlananlar). */
4543
+ drafts?: boolean
4544
+ }
4545
+
4546
+ async function readCollection<T>(dir: string, opts?: LoadOptions): Promise<WithSlug<T>[]> {
4547
+ let files: string[]
4548
+ try {
4549
+ files = (await readdir(join(contentDir, dir))).filter((f) => f.endsWith('.json'))
4550
+ } catch {
4551
+ return []
4552
+ }
4553
+ const out: WithSlug<T>[] = []
4554
+ for (const file of files.sort()) {
4555
+ const raw = await readFile(join(contentDir, dir, file), 'utf8')
4556
+ const data = JSON.parse(raw) as T & { _status?: string }
4557
+ if (!opts?.drafts && data._status === 'draft') continue
4558
+ out.push({ ...data, slug: file.slice(0, -'.json'.length) })
4559
+ }
4560
+ return out
4561
+ }
4562
+
4563
+ async function readSingleton<T>(file: string): Promise<T | null> {
4564
+ try {
4565
+ return JSON.parse(await readFile(join(contentDir, file), 'utf8')) as T
4566
+ } catch {
4567
+ return null
4568
+ }
4569
+ }`;
4570
+ function generateLoader(schema, contentDir) {
4571
+ const typeNames = [
4572
+ ...schema.collections.map((c) => pascalCase2(c.name)),
4573
+ ...schema.singletons.map((s) => pascalCase2(s.name))
4574
+ ];
4575
+ const header = [
4576
+ "// Generated by JustJSON \u2014 do not edit by hand.",
4577
+ IMPORTS,
4578
+ `import type { ${typeNames.join(", ")} } from './types'`
4579
+ ].join("\n");
4580
+ const q = (s) => `'${s.replace(/'/g, "\\'")}'`;
4581
+ const contentDirLine = `const contentDir = join(dirname(fileURLToPath(import.meta.url)), ${q(contentDir)})`;
4582
+ const loaders = [];
4583
+ for (const col of schema.collections) {
4584
+ const name = pascalCase2(col.name);
4585
+ loaders.push(
4586
+ `export const load${name} = (opts?: LoadOptions): Promise<WithSlug<${name}>[]> => readCollection<${name}>(${q(col.path)}, opts)`
4587
+ );
4588
+ }
4589
+ for (const s of schema.singletons) {
4590
+ const name = pascalCase2(s.name);
4591
+ loaders.push(
4592
+ `export const load${name} = (): Promise<${name} | null> => readSingleton<${name}>(${q(s.path)})`
4593
+ );
4594
+ }
4595
+ return `${[header, contentDirLine, READERS, loaders.join("\n")].join("\n\n")}
4596
+ `;
4597
+ }
4293
4598
  function json(data) {
4294
4599
  return `${JSON.stringify(data, null, 2)}
4295
4600
  `;
@@ -4342,7 +4647,7 @@ function fieldsFromRows(rows) {
4342
4647
  return order.map((key) => ({ key, label: key, type: types.get(key) }));
4343
4648
  }
4344
4649
  function inferProject(data) {
4345
- if (!isPlainObject(data)) throw new Error("\u0130\xE7e aktar\u0131lan JSON bir nesne olmal\u0131.");
4650
+ if (!isPlainObject(data)) throw new Error("The imported JSON must be an object.");
4346
4651
  const collections = [];
4347
4652
  const singletons = [];
4348
4653
  const entries = {};
@@ -4351,7 +4656,7 @@ function inferProject(data) {
4351
4656
  const generalData = {};
4352
4657
  const usedNames = /* @__PURE__ */ new Set();
4353
4658
  const uniqueName = (base) => {
4354
- let name = base || "alan";
4659
+ let name = base || "field";
4355
4660
  let n = 2;
4356
4661
  while (usedNames.has(name)) name = `${base}-${n++}`;
4357
4662
  usedNames.add(name);
@@ -4400,8 +4705,8 @@ function inferProject(data) {
4400
4705
  }
4401
4706
  }
4402
4707
  if (generalFields.length > 0) {
4403
- const name = uniqueName("genel");
4404
- singletons.push({ name, label: "Genel", path: `${name}.json`, fields: generalFields });
4708
+ const name = uniqueName("general");
4709
+ singletons.push({ name, label: "General", path: `${name}.json`, fields: generalFields });
4405
4710
  singletonData[name] = generalData;
4406
4711
  }
4407
4712
  return { schema: { version: 1, collections, singletons }, entries, singletons: singletonData };
@@ -4422,7 +4727,7 @@ var FsAdapter = class {
4422
4727
  const rootAbs = resolve(this.root);
4423
4728
  const full = resolve(rootAbs, path);
4424
4729
  if (full !== rootAbs && !full.startsWith(rootAbs + sep)) {
4425
- throw new Error(`Yol k\xF6k d\u0131\u015F\u0131na \xE7\u0131k\u0131yor: ${path}`);
4730
+ throw new PathEscapeError(`Path escapes the project root: ${path}`);
4426
4731
  }
4427
4732
  return full;
4428
4733
  }
@@ -4481,7 +4786,7 @@ async function resolveContentDir(root2) {
4481
4786
  // src/commands/export.ts
4482
4787
  async function collectExportZip(adapter, contentDir) {
4483
4788
  const schema = await loadSchema(adapter, contentDir);
4484
- if (!schema) throw new Error("\u015Eema bulunamad\u0131. \xD6nce `justjson init` \xE7al\u0131\u015Ft\u0131r\u0131n.");
4789
+ if (!schema) throw new Error("No schema found. Run `justjson init` first.");
4485
4790
  const store = new ContentStore(adapter, schema, contentDir);
4486
4791
  const entries = {};
4487
4792
  for (const col of schema.collections) {
@@ -4518,82 +4823,142 @@ async function exportZip(root2, outFile = "justjson-export.zip") {
4518
4823
  // src/templates/blog.json
4519
4824
  var blog_default = {
4520
4825
  title: "Blog",
4521
- description: "Rich-text i\xE7erikli yaz\u0131lar ve site ad\u0131 i\xE7in bir Ayarlar.",
4826
+ description: "Posts with rich-text content, plus a Settings singleton for the site name.",
4522
4827
  schema: {
4523
4828
  version: 1,
4524
4829
  collections: [
4525
4830
  {
4526
4831
  name: "posts",
4527
- label: "Yaz\u0131lar",
4832
+ label: "Posts",
4528
4833
  path: "posts",
4529
4834
  fields: [
4530
- { key: "title", label: "Ba\u015Fl\u0131k", type: "text", required: true },
4531
- { key: "slug", label: "Slug", type: "text", required: true },
4532
- { key: "date", label: "Tarih", type: "date" },
4533
- { key: "body", label: "\u0130\xE7erik", type: "richtext" }
4835
+ {
4836
+ key: "title",
4837
+ label: "Title",
4838
+ type: "text",
4839
+ required: true
4840
+ },
4841
+ {
4842
+ key: "slug",
4843
+ label: "Slug",
4844
+ type: "text",
4845
+ required: true
4846
+ },
4847
+ {
4848
+ key: "date",
4849
+ label: "Date",
4850
+ type: "date"
4851
+ },
4852
+ {
4853
+ key: "body",
4854
+ label: "Body",
4855
+ type: "richtext"
4856
+ }
4534
4857
  ]
4535
4858
  }
4536
4859
  ],
4537
4860
  singletons: [
4538
4861
  {
4539
4862
  name: "settings",
4540
- label: "Ayarlar",
4863
+ label: "Settings",
4541
4864
  path: "settings.json",
4542
- fields: [{ key: "title", label: "Site ad\u0131", type: "text" }]
4865
+ fields: [
4866
+ {
4867
+ key: "title",
4868
+ label: "Site name",
4869
+ type: "text"
4870
+ }
4871
+ ]
4543
4872
  }
4544
4873
  ]
4545
4874
  },
4546
4875
  samples: {
4547
- posts: [{ title: "\u0130lk yaz\u0131", slug: "ilk-yazi", body: "Merhaba." }]
4876
+ posts: [
4877
+ {
4878
+ title: "First post",
4879
+ slug: "first-post",
4880
+ body: "Hello."
4881
+ }
4882
+ ]
4548
4883
  }
4549
4884
  };
4550
4885
 
4551
4886
  // src/templates/catalog.json
4552
4887
  var catalog_default = {
4553
- title: "\xDCr\xFCn Katalo\u011Fu",
4554
- description: "Fiyat, kategori ve g\xF6rselli \xFCr\xFCnler; bir de Ma\u011Faza ayarlar\u0131.",
4888
+ title: "Product catalog",
4889
+ description: "Products with price, category and an image, plus Store settings.",
4555
4890
  schema: {
4556
4891
  version: 1,
4557
4892
  collections: [
4558
4893
  {
4559
- name: "urunler",
4560
- label: "\xDCr\xFCnler",
4561
- path: "urunler",
4894
+ name: "products",
4895
+ label: "Products",
4896
+ path: "products",
4562
4897
  fields: [
4563
- { key: "title", label: "Ba\u015Fl\u0131k", type: "text", required: true },
4564
- { key: "slug", label: "Slug", type: "text", required: true },
4565
- { key: "fiyat", label: "Fiyat", type: "number" },
4566
4898
  {
4567
- key: "kategori",
4568
- label: "Kategori",
4899
+ key: "title",
4900
+ label: "Title",
4901
+ type: "text",
4902
+ required: true
4903
+ },
4904
+ {
4905
+ key: "slug",
4906
+ label: "Slug",
4907
+ type: "text",
4908
+ required: true
4909
+ },
4910
+ {
4911
+ key: "price",
4912
+ label: "Price",
4913
+ type: "number"
4914
+ },
4915
+ {
4916
+ key: "category",
4917
+ label: "Category",
4569
4918
  type: "select",
4570
- options: ["Ev", "Giyim", "Aksesuar"]
4919
+ options: ["Home", "Clothing", "Accessories"]
4920
+ },
4921
+ {
4922
+ key: "cover",
4923
+ label: "Image",
4924
+ type: "image"
4571
4925
  },
4572
- { key: "kapak", label: "G\xF6rsel", type: "image" },
4573
- { key: "aciklama", label: "A\xE7\u0131klama", type: "richtext" }
4926
+ {
4927
+ key: "description",
4928
+ label: "Description",
4929
+ type: "richtext"
4930
+ }
4574
4931
  ]
4575
4932
  }
4576
4933
  ],
4577
4934
  singletons: [
4578
4935
  {
4579
- name: "magaza",
4580
- label: "Ma\u011Faza",
4581
- path: "magaza.json",
4936
+ name: "store",
4937
+ label: "Store",
4938
+ path: "store.json",
4582
4939
  fields: [
4583
- { key: "title", label: "Ma\u011Faza ad\u0131", type: "text" },
4584
- { key: "slogan", label: "Slogan", type: "text" }
4940
+ {
4941
+ key: "title",
4942
+ label: "Store name",
4943
+ type: "text"
4944
+ },
4945
+ {
4946
+ key: "tagline",
4947
+ label: "Tagline",
4948
+ type: "text"
4949
+ }
4585
4950
  ]
4586
4951
  }
4587
4952
  ]
4588
4953
  },
4589
4954
  samples: {
4590
- urunler: [
4955
+ products: [
4591
4956
  {
4592
- title: "Seramik Kupa",
4593
- slug: "seramik-kupa",
4594
- fiyat: 180,
4595
- kategori: "Ev",
4596
- aciklama: "Elde yap\u0131m seramik kupa."
4957
+ title: "Ceramic mug",
4958
+ slug: "ceramic-mug",
4959
+ price: 18,
4960
+ category: "Home",
4961
+ description: "Hand-made ceramic mug."
4597
4962
  }
4598
4963
  ]
4599
4964
  }
@@ -4602,25 +4967,43 @@ var catalog_default = {
4602
4967
  // src/templates/changelog.json
4603
4968
  var changelog_default = {
4604
4969
  title: "Changelog",
4605
- description: "S\xFCr\xFCm notlar\u0131: versiyon, tarih, t\xFCr ve rich-text a\xE7\u0131klama.",
4970
+ description: "Release notes: version, date, type and a rich-text body.",
4606
4971
  schema: {
4607
4972
  version: 1,
4608
4973
  collections: [
4609
4974
  {
4610
4975
  name: "releases",
4611
- label: "S\xFCr\xFCmler",
4976
+ label: "Releases",
4612
4977
  path: "releases",
4613
4978
  fields: [
4614
- { key: "version", label: "Versiyon", type: "text", required: true },
4615
- { key: "slug", label: "Slug", type: "text", required: true },
4616
- { key: "date", label: "Tarih", type: "date" },
4979
+ {
4980
+ key: "version",
4981
+ label: "Version",
4982
+ type: "text",
4983
+ required: true
4984
+ },
4985
+ {
4986
+ key: "slug",
4987
+ label: "Slug",
4988
+ type: "text",
4989
+ required: true
4990
+ },
4991
+ {
4992
+ key: "date",
4993
+ label: "Date",
4994
+ type: "date"
4995
+ },
4617
4996
  {
4618
4997
  key: "type",
4619
- label: "T\xFCr",
4998
+ label: "Type",
4620
4999
  type: "select",
4621
- options: ["Eklendi", "De\u011Fi\u015Fti", "D\xFCzeltildi"]
5000
+ options: ["Added", "Changed", "Fixed"]
4622
5001
  },
4623
- { key: "body", label: "Notlar", type: "richtext" }
5002
+ {
5003
+ key: "body",
5004
+ label: "Notes",
5005
+ type: "richtext"
5006
+ }
4624
5007
  ]
4625
5008
  }
4626
5009
  ],
@@ -4631,8 +5014,8 @@ var changelog_default = {
4631
5014
  {
4632
5015
  version: "1.0.0",
4633
5016
  slug: "1-0-0",
4634
- type: "Eklendi",
4635
- body: "\u0130lk s\xFCr\xFCm."
5017
+ type: "Added",
5018
+ body: "First release."
4636
5019
  }
4637
5020
  ]
4638
5021
  }
@@ -4641,119 +5024,217 @@ var changelog_default = {
4641
5024
  // src/templates/cv.json
4642
5025
  var cv_default = {
4643
5026
  title: "CV",
4644
- description: "Deneyim koleksiyonu ve ki\u015Fisel bilgiler i\xE7in bir Profil.",
5027
+ description: "An Experience collection and a Profile singleton for your personal details.",
4645
5028
  schema: {
4646
5029
  version: 1,
4647
5030
  collections: [
4648
5031
  {
4649
5032
  name: "experience",
4650
- label: "Deneyim",
5033
+ label: "Experience",
4651
5034
  path: "experience",
4652
5035
  fields: [
4653
- { key: "role", label: "Pozisyon", type: "text", required: true },
4654
- { key: "company", label: "\u015Eirket", type: "text", required: true },
4655
- { key: "summary", label: "\xD6zet", type: "richtext" }
5036
+ {
5037
+ key: "role",
5038
+ label: "Role",
5039
+ type: "text",
5040
+ required: true
5041
+ },
5042
+ {
5043
+ key: "company",
5044
+ label: "Company",
5045
+ type: "text",
5046
+ required: true
5047
+ },
5048
+ {
5049
+ key: "summary",
5050
+ label: "Summary",
5051
+ type: "richtext"
5052
+ }
4656
5053
  ]
4657
5054
  }
4658
5055
  ],
4659
5056
  singletons: [
4660
5057
  {
4661
5058
  name: "profile",
4662
- label: "Profil",
5059
+ label: "Profile",
4663
5060
  path: "profile.json",
4664
5061
  fields: [
4665
- { key: "name", label: "Ad", type: "text", required: true },
4666
- { key: "headline", label: "Ba\u015Fl\u0131k", type: "text" }
5062
+ {
5063
+ key: "name",
5064
+ label: "Name",
5065
+ type: "text",
5066
+ required: true
5067
+ },
5068
+ {
5069
+ key: "headline",
5070
+ label: "Headline",
5071
+ type: "text"
5072
+ }
4667
5073
  ]
4668
5074
  }
4669
5075
  ]
4670
5076
  },
4671
5077
  samples: {
4672
- experience: [{ role: "Yaz\u0131l\u0131mc\u0131", company: "\xD6rnek A.\u015E.", summary: "..." }]
5078
+ experience: [
5079
+ {
5080
+ role: "Developer",
5081
+ company: "Acme Inc.",
5082
+ summary: "..."
5083
+ }
5084
+ ]
4673
5085
  }
4674
5086
  };
4675
5087
 
4676
5088
  // src/templates/docs.json
4677
5089
  var docs_default = {
4678
- title: "Dok\xFCmantasyon",
4679
- description: "S\u0131ralanabilir sayfalar ve rich-text i\xE7erik; site ba\u015Fl\u0131\u011F\u0131 i\xE7in bir Ayarlar.",
5090
+ title: "Documentation",
5091
+ description: "Orderable pages with rich-text content, plus a Settings singleton for the site title.",
4680
5092
  schema: {
4681
5093
  version: 1,
4682
5094
  collections: [
4683
5095
  {
4684
5096
  name: "pages",
4685
- label: "Sayfalar",
5097
+ label: "Pages",
4686
5098
  path: "pages",
4687
5099
  fields: [
4688
- { key: "title", label: "Ba\u015Fl\u0131k", type: "text", required: true },
4689
- { key: "slug", label: "Slug", type: "text", required: true },
4690
- { key: "order", label: "S\u0131ra", type: "number" },
4691
- { key: "body", label: "\u0130\xE7erik", type: "richtext" }
5100
+ {
5101
+ key: "title",
5102
+ label: "Title",
5103
+ type: "text",
5104
+ required: true
5105
+ },
5106
+ {
5107
+ key: "slug",
5108
+ label: "Slug",
5109
+ type: "text",
5110
+ required: true
5111
+ },
5112
+ {
5113
+ key: "order",
5114
+ label: "Order",
5115
+ type: "number"
5116
+ },
5117
+ {
5118
+ key: "body",
5119
+ label: "Body",
5120
+ type: "richtext"
5121
+ }
4692
5122
  ]
4693
5123
  }
4694
5124
  ],
4695
5125
  singletons: [
4696
5126
  {
4697
5127
  name: "settings",
4698
- label: "Ayarlar",
5128
+ label: "Settings",
4699
5129
  path: "settings.json",
4700
5130
  fields: [
4701
- { key: "title", label: "Site ad\u0131", type: "text" },
4702
- { key: "tagline", label: "Slogan", type: "text" }
5131
+ {
5132
+ key: "title",
5133
+ label: "Site name",
5134
+ type: "text"
5135
+ },
5136
+ {
5137
+ key: "tagline",
5138
+ label: "Tagline",
5139
+ type: "text"
5140
+ }
4703
5141
  ]
4704
5142
  }
4705
5143
  ]
4706
5144
  },
4707
5145
  samples: {
4708
5146
  pages: [
4709
- { title: "Ba\u015Flarken", slug: "baslarken", order: 1, body: "Kuruluma ho\u015F geldin." }
5147
+ {
5148
+ title: "Getting started",
5149
+ slug: "getting-started",
5150
+ order: 1,
5151
+ body: "Welcome aboard."
5152
+ }
4710
5153
  ]
4711
5154
  }
4712
5155
  };
4713
5156
 
4714
5157
  // src/templates/event.json
4715
5158
  var event_default = {
4716
- title: "Etkinlik Program\u0131",
4717
- description: "Tarih, saat ve konu\u015Fmac\u0131l\u0131 oturumlardan olu\u015Fan bir etkinlik ajandas\u0131.",
5159
+ title: "Event schedule",
5160
+ description: "An event agenda of sessions with date, time and speaker.",
4718
5161
  schema: {
4719
5162
  version: 1,
4720
5163
  collections: [
4721
5164
  {
4722
- name: "oturumlar",
4723
- label: "Oturumlar",
4724
- path: "oturumlar",
5165
+ name: "sessions",
5166
+ label: "Sessions",
5167
+ path: "sessions",
4725
5168
  fields: [
4726
- { key: "title", label: "Ba\u015Fl\u0131k", type: "text", required: true },
4727
- { key: "slug", label: "Slug", type: "text", required: true },
4728
- { key: "tarih", label: "Tarih", type: "date" },
4729
- { key: "saat", label: "Saat", type: "text" },
4730
- { key: "konusmaci", label: "Konu\u015Fmac\u0131", type: "text" },
4731
- { key: "yer", label: "Salon", type: "text" },
4732
- { key: "aciklama", label: "A\xE7\u0131klama", type: "richtext" }
5169
+ {
5170
+ key: "title",
5171
+ label: "Title",
5172
+ type: "text",
5173
+ required: true
5174
+ },
5175
+ {
5176
+ key: "slug",
5177
+ label: "Slug",
5178
+ type: "text",
5179
+ required: true
5180
+ },
5181
+ {
5182
+ key: "date",
5183
+ label: "Date",
5184
+ type: "date"
5185
+ },
5186
+ {
5187
+ key: "time",
5188
+ label: "Time",
5189
+ type: "text"
5190
+ },
5191
+ {
5192
+ key: "speaker",
5193
+ label: "Speaker",
5194
+ type: "text"
5195
+ },
5196
+ {
5197
+ key: "room",
5198
+ label: "Room",
5199
+ type: "text"
5200
+ },
5201
+ {
5202
+ key: "description",
5203
+ label: "Description",
5204
+ type: "richtext"
5205
+ }
4733
5206
  ]
4734
5207
  }
4735
5208
  ],
4736
5209
  singletons: [
4737
5210
  {
4738
- name: "etkinlik",
4739
- label: "Etkinlik",
4740
- path: "etkinlik.json",
5211
+ name: "event",
5212
+ label: "Event",
5213
+ path: "event.json",
4741
5214
  fields: [
4742
- { key: "title", label: "Etkinlik ad\u0131", type: "text" },
4743
- { key: "slogan", label: "Slogan", type: "text" }
5215
+ {
5216
+ key: "title",
5217
+ label: "Event name",
5218
+ type: "text"
5219
+ },
5220
+ {
5221
+ key: "tagline",
5222
+ label: "Tagline",
5223
+ type: "text"
5224
+ }
4744
5225
  ]
4745
5226
  }
4746
5227
  ]
4747
5228
  },
4748
5229
  samples: {
4749
- oturumlar: [
5230
+ sessions: [
4750
5231
  {
4751
- title: "A\xE7\u0131l\u0131\u015F Konu\u015Fmas\u0131",
4752
- slug: "acilis-konusmasi",
4753
- saat: "09:00",
4754
- konusmaci: "Ay\u015Fe Y\u0131lmaz",
4755
- yer: "Ana Salon",
4756
- aciklama: "Etkinli\u011Fe ho\u015F geldiniz."
5232
+ title: "Opening keynote",
5233
+ slug: "opening-keynote",
5234
+ time: "09:00",
5235
+ speaker: "Ada Lovelace",
5236
+ room: "Main hall",
5237
+ description: "Welcome to the event."
4757
5238
  }
4758
5239
  ]
4759
5240
  }
@@ -4761,41 +5242,84 @@ var event_default = {
4761
5242
 
4762
5243
  // src/templates/portfolio.json
4763
5244
  var portfolio_default = {
4764
- title: "Portfolyo",
4765
- description: "Kapak g\xF6rseli, etiket ve rich-text i\xE7erikli projeler; bir de Hakk\u0131nda sayfas\u0131.",
5245
+ title: "Portfolio",
5246
+ description: "Projects with a cover image, tag and rich-text body, plus an About page.",
4766
5247
  schema: {
4767
5248
  version: 1,
4768
5249
  collections: [
4769
5250
  {
4770
5251
  name: "projects",
4771
- label: "Projeler",
5252
+ label: "Projects",
4772
5253
  path: "projects",
4773
5254
  fields: [
4774
- { key: "title", label: "Ba\u015Fl\u0131k", type: "text", required: true },
4775
- { key: "slug", label: "Slug", type: "text", required: true },
4776
- { key: "summary", label: "\xD6zet", type: "text" },
4777
- { key: "cover", label: "Kapak", type: "image" },
4778
- { key: "url", label: "Ba\u011Flant\u0131", type: "text" },
5255
+ {
5256
+ key: "title",
5257
+ label: "Title",
5258
+ type: "text",
5259
+ required: true
5260
+ },
5261
+ {
5262
+ key: "slug",
5263
+ label: "Slug",
5264
+ type: "text",
5265
+ required: true
5266
+ },
5267
+ {
5268
+ key: "summary",
5269
+ label: "Summary",
5270
+ type: "text"
5271
+ },
5272
+ {
5273
+ key: "cover",
5274
+ label: "Cover",
5275
+ type: "image"
5276
+ },
5277
+ {
5278
+ key: "url",
5279
+ label: "Link",
5280
+ type: "url"
5281
+ },
4779
5282
  {
4780
5283
  key: "kind",
4781
- label: "T\xFCr",
5284
+ label: "Kind",
4782
5285
  type: "select",
4783
- options: ["Web", "Mobil", "Tasar\u0131m"]
5286
+ options: ["Web", "Mobile", "Design"]
5287
+ },
5288
+ {
5289
+ key: "body",
5290
+ label: "Body",
5291
+ type: "richtext"
4784
5292
  },
4785
- { key: "body", label: "\u0130\xE7erik", type: "richtext" },
4786
- { key: "date", label: "Tarih", type: "date" }
5293
+ {
5294
+ key: "date",
5295
+ label: "Date",
5296
+ type: "date"
5297
+ }
4787
5298
  ]
4788
5299
  }
4789
5300
  ],
4790
5301
  singletons: [
4791
5302
  {
4792
5303
  name: "about",
4793
- label: "Hakk\u0131nda",
5304
+ label: "About",
4794
5305
  path: "about.json",
4795
5306
  fields: [
4796
- { key: "name", label: "Ad", type: "text", required: true },
4797
- { key: "avatar", label: "Avatar", type: "image" },
4798
- { key: "bio", label: "Biyografi", type: "richtext" }
5307
+ {
5308
+ key: "name",
5309
+ label: "Name",
5310
+ type: "text",
5311
+ required: true
5312
+ },
5313
+ {
5314
+ key: "avatar",
5315
+ label: "Avatar",
5316
+ type: "image"
5317
+ },
5318
+ {
5319
+ key: "bio",
5320
+ label: "Bio",
5321
+ type: "richtext"
5322
+ }
4799
5323
  ]
4800
5324
  }
4801
5325
  ]
@@ -4803,11 +5327,11 @@ var portfolio_default = {
4803
5327
  samples: {
4804
5328
  projects: [
4805
5329
  {
4806
- title: "\xD6rnek Proje",
4807
- slug: "ornek-proje",
4808
- summary: "K\u0131sa bir a\xE7\u0131klama.",
5330
+ title: "Sample project",
5331
+ slug: "sample-project",
5332
+ summary: "A short description.",
4809
5333
  kind: "Web",
4810
- body: "Proje detaylar\u0131 burada."
5334
+ body: "Project details go here."
4811
5335
  }
4812
5336
  ]
4813
5337
  }
@@ -4815,44 +5339,80 @@ var portfolio_default = {
4815
5339
 
4816
5340
  // src/templates/recipe.json
4817
5341
  var recipe_default = {
4818
- title: "Tarif Defteri",
4819
- description: "Malzeme, yap\u0131l\u0131\u015F ve kapak g\xF6rselli tarifler; bir de Mutfak ayarlar\u0131.",
5342
+ title: "Recipe box",
5343
+ description: "Recipes with ingredients, steps and a cover image, plus Kitchen settings.",
4820
5344
  schema: {
4821
5345
  version: 1,
4822
5346
  collections: [
4823
5347
  {
4824
- name: "tarifler",
4825
- label: "Tarifler",
4826
- path: "tarifler",
5348
+ name: "recipes",
5349
+ label: "Recipes",
5350
+ path: "recipes",
4827
5351
  fields: [
4828
- { key: "title", label: "Ba\u015Fl\u0131k", type: "text", required: true },
4829
- { key: "slug", label: "Slug", type: "text", required: true },
4830
- { key: "sure", label: "S\xFCre", type: "text" },
4831
- { key: "porsiyon", label: "Porsiyon", type: "number" },
4832
- { key: "kapak", label: "Kapak", type: "image" },
4833
- { key: "malzemeler", label: "Malzemeler", type: "richtext" },
4834
- { key: "yapilis", label: "Yap\u0131l\u0131\u015F", type: "richtext" }
5352
+ {
5353
+ key: "title",
5354
+ label: "Title",
5355
+ type: "text",
5356
+ required: true
5357
+ },
5358
+ {
5359
+ key: "slug",
5360
+ label: "Slug",
5361
+ type: "text",
5362
+ required: true
5363
+ },
5364
+ {
5365
+ key: "time",
5366
+ label: "Time",
5367
+ type: "text"
5368
+ },
5369
+ {
5370
+ key: "servings",
5371
+ label: "Servings",
5372
+ type: "number"
5373
+ },
5374
+ {
5375
+ key: "cover",
5376
+ label: "Cover",
5377
+ type: "image"
5378
+ },
5379
+ {
5380
+ key: "ingredients",
5381
+ label: "Ingredients",
5382
+ type: "richtext"
5383
+ },
5384
+ {
5385
+ key: "steps",
5386
+ label: "Steps",
5387
+ type: "richtext"
5388
+ }
4835
5389
  ]
4836
5390
  }
4837
5391
  ],
4838
5392
  singletons: [
4839
5393
  {
4840
- name: "mutfak",
4841
- label: "Mutfak",
4842
- path: "mutfak.json",
4843
- fields: [{ key: "title", label: "Site ad\u0131", type: "text" }]
5394
+ name: "kitchen",
5395
+ label: "Kitchen",
5396
+ path: "kitchen.json",
5397
+ fields: [
5398
+ {
5399
+ key: "title",
5400
+ label: "Site name",
5401
+ type: "text"
5402
+ }
5403
+ ]
4844
5404
  }
4845
5405
  ]
4846
5406
  },
4847
5407
  samples: {
4848
- tarifler: [
5408
+ recipes: [
4849
5409
  {
4850
- title: "Ak\u015Fam Makarnas\u0131",
4851
- slug: "aksam-makarnasi",
4852
- sure: "20 dk",
4853
- porsiyon: 2,
4854
- malzemeler: "- 200g makarna\n- 2 di\u015F sar\u0131msak\n- Zeytinya\u011F\u0131",
4855
- yapilis: "Makarnay\u0131 ha\u015Fla. Sar\u0131msa\u011F\u0131 zeytinya\u011F\u0131nda kavur. Kar\u0131\u015Ft\u0131r."
5410
+ title: "Weeknight pasta",
5411
+ slug: "weeknight-pasta",
5412
+ time: "20 min",
5413
+ servings: 2,
5414
+ ingredients: "- 200g pasta\n- 2 cloves garlic\n- Olive oil",
5415
+ steps: "Boil the pasta. Fry the garlic in olive oil. Toss together."
4856
5416
  }
4857
5417
  ]
4858
5418
  }
@@ -4896,7 +5456,7 @@ async function applyTemplate(adapter, contentDir, template) {
4896
5456
  const store = new ContentStore(adapter, schema, contentDir);
4897
5457
  for (const [collection, rows] of Object.entries(template.samples)) {
4898
5458
  for (const row of rows) {
4899
- const slug = slugify(typeof row.slug === "string" ? row.slug : String(row.title ?? "icerik"));
5459
+ const slug = slugify(typeof row.slug === "string" ? row.slug : String(row.title ?? "content"));
4900
5460
  await store.writeEntry(collection, slug, row);
4901
5461
  }
4902
5462
  }
@@ -4908,26 +5468,108 @@ async function initProject(root2, templateName) {
4908
5468
  const adapter = new FsAdapter(root2);
4909
5469
  const contentDir = await resolveContentDir(root2);
4910
5470
  if (await loadSchema(adapter, contentDir)) {
4911
- throw new Error("Bu klas\xF6rde zaten bir \u015Fema var (content/_schema.json).");
5471
+ throw new Error("This folder already has a schema (content/_schema.json).");
4912
5472
  }
4913
5473
  await applyTemplate(adapter, contentDir, template);
4914
5474
  }
4915
5475
 
4916
5476
  // src/commands/types.ts
4917
5477
  import { join as join2 } from "path";
4918
- async function generateTypesFile(root2, outPath = "types.ts") {
5478
+ async function generateTypesFile(root2, outPath = "types.ts", loaderPath = "content.ts") {
4919
5479
  const adapter = new FsAdapter(root2);
4920
5480
  const contentDir = await resolveContentDir(root2);
4921
5481
  const schema = await loadSchema(adapter, contentDir);
4922
- if (!schema) throw new Error("\u015Eema bulunamad\u0131. \xD6nce `justjson init` \xE7al\u0131\u015Ft\u0131r\u0131n.");
5482
+ if (!schema) throw new Error("No schema found. Run `justjson init` first.");
4923
5483
  await adapter.write(outPath, generateTypes(schema));
5484
+ await adapter.write(loaderPath, generateLoader(schema, contentDir));
4924
5485
  return join2(root2, outPath);
4925
5486
  }
4926
5487
 
5488
+ // src/commands/validate.ts
5489
+ import { readdir as readdir2 } from "fs/promises";
5490
+ import { join as join3 } from "path";
5491
+ async function listDir(path) {
5492
+ try {
5493
+ const entries = await readdir2(path, { withFileTypes: true });
5494
+ return entries.map((e) => ({ name: e.name, isDir: e.isDirectory() }));
5495
+ } catch {
5496
+ return [];
5497
+ }
5498
+ }
5499
+ async function loadProjectContent(root2) {
5500
+ const adapter = new FsAdapter(root2);
5501
+ const contentDir = await resolveContentDir(root2);
5502
+ const schema = await loadSchema(adapter, contentDir);
5503
+ if (!schema) return null;
5504
+ const store = new ContentStore(adapter, schema, contentDir);
5505
+ const collections = {};
5506
+ for (const col of schema.collections) {
5507
+ const slugs = await store.listEntries(col.name);
5508
+ const entries = [];
5509
+ for (const slug of slugs) {
5510
+ entries.push({ slug, data: await store.readEntry(col.name, slug) ?? {} });
5511
+ }
5512
+ collections[col.name] = entries;
5513
+ }
5514
+ const known = new Set(schema.collections.map((c) => c.name));
5515
+ for (const { name, isDir } of await listDir(join3(root2, contentDir))) {
5516
+ if (!isDir || name === "media" || known.has(name) || collections[name]) continue;
5517
+ const files = await listDir(join3(root2, contentDir, name));
5518
+ collections[name] = files.filter((f) => !f.isDir && f.name.endsWith(".json")).map((f) => ({ slug: f.name.slice(0, -".json".length), data: {} }));
5519
+ }
5520
+ const singletons = {};
5521
+ for (const s of schema.singletons) {
5522
+ singletons[s.name] = await store.readSingleton(s.name);
5523
+ }
5524
+ const media = (await listDir(join3(root2, contentDir, "media"))).filter((f) => !f.isDir).map((f) => `${contentDir}/media/${f.name}`);
5525
+ return { schema, content: { collections, singletons, media } };
5526
+ }
5527
+ async function validateProjectAt(root2) {
5528
+ const loaded = await loadProjectContent(root2);
5529
+ if (!loaded) return null;
5530
+ return validateProject(loaded.schema, loaded.content);
5531
+ }
5532
+ function summarize(issues) {
5533
+ let errors = 0;
5534
+ let warnings = 0;
5535
+ for (const i of issues) {
5536
+ if (i.level === "error") errors++;
5537
+ else warnings++;
5538
+ }
5539
+ return { errors, warnings };
5540
+ }
5541
+ function shouldFail(issues, strict) {
5542
+ const { errors, warnings } = summarize(issues);
5543
+ return errors > 0 || strict && warnings > 0;
5544
+ }
5545
+ function location(issue) {
5546
+ const parts = [];
5547
+ if (issue.collection) parts.push(issue.collection);
5548
+ if (issue.singleton) parts.push(issue.singleton);
5549
+ if (issue.slug) parts.push(issue.slug);
5550
+ if (issue.field) parts.push(issue.field);
5551
+ return parts.join(":") || "(project)";
5552
+ }
5553
+ function formatText(issues) {
5554
+ if (issues.length === 0) return "\u2713 No issues \u2014 content matches the schema.";
5555
+ const lines = issues.map((i) => {
5556
+ const tag = i.level === "error" ? "ERROR" : "WARN ";
5557
+ return ` ${tag} ${location(i)} ${i.message}`;
5558
+ });
5559
+ const { errors, warnings } = summarize(issues);
5560
+ lines.push("");
5561
+ lines.push(`${errors} error(s), ${warnings} warning(s)`);
5562
+ return lines.join("\n");
5563
+ }
5564
+ function formatJson(issues) {
5565
+ const { errors, warnings } = summarize(issues);
5566
+ return JSON.stringify({ ok: errors === 0, errors, warnings, issues }, null, 2);
5567
+ }
5568
+
4927
5569
  // src/server.ts
4928
5570
  import { exec } from "child_process";
4929
5571
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
4930
- import { basename, dirname as dirname2, extname, join as join3, normalize } from "path";
5572
+ import { basename as basename2, dirname as dirname2, extname, join as join4, normalize } from "path";
4931
5573
  import { fileURLToPath } from "url";
4932
5574
  import { serve } from "@hono/node-server";
4933
5575
  import { Hono } from "hono";
@@ -4948,16 +5590,17 @@ async function callGemini(apiKey, model, system, prompt) {
4948
5590
  })
4949
5591
  });
4950
5592
  const data = await res.json();
4951
- if (!res.ok) throw new Error(data.error?.message ?? `Gemini iste\u011Fi ba\u015Far\u0131s\u0131z (${res.status})`);
5593
+ if (!res.ok) throw new Error(data.error?.message ?? `Gemini request failed (${res.status})`);
4952
5594
  const text = data.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("") ?? "";
4953
- if (!text) throw new Error("Gemini bo\u015F yan\u0131t d\xF6nd\xFC");
5595
+ if (!text) throw new Error("Gemini returned an empty response");
4954
5596
  return text;
4955
5597
  }
4956
5598
  async function listGeminiModels(apiKey) {
4957
5599
  const url = `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}&pageSize=1000`;
4958
5600
  const res = await fetch(url);
4959
5601
  const data = await res.json();
4960
- if (!res.ok) throw new Error(data.error?.message ?? `Model listesi al\u0131namad\u0131 (${res.status})`);
5602
+ if (!res.ok)
5603
+ throw new Error(data.error?.message ?? `Could not fetch the model list (${res.status})`);
4961
5604
  return (data.models ?? []).filter((m) => m.supportedGenerationMethods?.includes("generateContent")).map((m) => ({
4962
5605
  id: (m.name ?? "").replace(/^models\//, ""),
4963
5606
  label: m.displayName ?? (m.name ?? "").replace(/^models\//, "")
@@ -4966,16 +5609,17 @@ async function listGeminiModels(apiKey) {
4966
5609
  );
4967
5610
  }
4968
5611
  async function listOpenAiCompatibleModels(baseUrl, apiKey) {
4969
- if (!baseUrl) throw new Error("Bu sa\u011Flay\u0131c\u0131 i\xE7in taban URL gerekli");
5612
+ if (!baseUrl) throw new Error("This provider needs a base URL");
4970
5613
  const res = await fetch(`${baseUrl.replace(/\/$/, "")}/models`, {
4971
5614
  headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {}
4972
5615
  });
4973
5616
  const data = await res.json();
4974
- if (!res.ok) throw new Error(data.error?.message ?? `Model listesi al\u0131namad\u0131 (${res.status})`);
5617
+ if (!res.ok)
5618
+ throw new Error(data.error?.message ?? `Could not fetch the model list (${res.status})`);
4975
5619
  return (data.data ?? []).map((m) => ({ id: m.id ?? "", label: m.id ?? "" })).filter((m) => m.id);
4976
5620
  }
4977
5621
  async function callOpenAiCompatible(baseUrl, apiKey, model, system, prompt) {
4978
- if (!baseUrl) throw new Error("Bu sa\u011Flay\u0131c\u0131 i\xE7in taban URL gerekli");
5622
+ if (!baseUrl) throw new Error("This provider needs a base URL");
4979
5623
  const messages = [
4980
5624
  ...system ? [{ role: "system", content: system }] : [],
4981
5625
  { role: "user", content: prompt }
@@ -4986,9 +5630,9 @@ async function callOpenAiCompatible(baseUrl, apiKey, model, system, prompt) {
4986
5630
  body: JSON.stringify({ model, messages })
4987
5631
  });
4988
5632
  const data = await res.json();
4989
- if (!res.ok) throw new Error(data.error?.message ?? `\u0130stek ba\u015Far\u0131s\u0131z (${res.status})`);
5633
+ if (!res.ok) throw new Error(data.error?.message ?? `Request failed (${res.status})`);
4990
5634
  const text = data.choices?.[0]?.message?.content ?? "";
4991
- if (!text) throw new Error("Sa\u011Flay\u0131c\u0131 bo\u015F yan\u0131t d\xF6nd\xFC");
5635
+ if (!text) throw new Error("The provider returned an empty response");
4992
5636
  return text;
4993
5637
  }
4994
5638
  var MIME = {
@@ -5009,15 +5653,16 @@ async function createServer(root2) {
5009
5653
  let store = new ContentStore(adapter, schema, contentDir);
5010
5654
  const app = new Hono();
5011
5655
  app.onError((err, c) => {
5012
- const msg = err.message;
5013
- if (msg.startsWith("Bilinmeyen")) return c.json({ error: msg }, 404);
5014
- if (msg.startsWith("G\xFCvensiz") || msg.startsWith("Yol")) return c.json({ error: msg }, 400);
5015
- return c.json({ error: "sunucu hatas\u0131" }, 500);
5656
+ if (err instanceof NotFoundError) return c.json({ error: err.message }, 404);
5657
+ if (err instanceof UnsafeSlugError || err instanceof PathEscapeError) {
5658
+ return c.json({ error: err.message }, 400);
5659
+ }
5660
+ return c.json({ error: "server error" }, 500);
5016
5661
  });
5017
5662
  app.get(
5018
5663
  "/api/_project",
5019
5664
  (c) => c.json({
5020
- name: basename(root2) || "proje",
5665
+ name: basename2(root2) || "project",
5021
5666
  path: root2,
5022
5667
  contentDir,
5023
5668
  collections: schema.collections.length,
@@ -5028,9 +5673,9 @@ async function createServer(root2) {
5028
5673
  app.post("/api/_init", async (c) => {
5029
5674
  const { template: id } = await c.req.json();
5030
5675
  const t = id ? getTemplate(id) : void 0;
5031
- if (!t) return c.json({ error: "Bilinmeyen template" }, 404);
5676
+ if (!t) return c.json({ error: "Unknown template" }, 404);
5032
5677
  if (schema.collections.length > 0 || schema.singletons.length > 0) {
5033
- return c.json({ error: "Bu klas\xF6rde zaten bir \u015Fema var." }, 400);
5678
+ return c.json({ error: "This folder already has a schema." }, 400);
5034
5679
  }
5035
5680
  schema = await applyTemplate(adapter, contentDir, t);
5036
5681
  store = new ContentStore(adapter, schema, contentDir);
@@ -5038,7 +5683,7 @@ async function createServer(root2) {
5038
5683
  });
5039
5684
  app.post("/api/_import", async (c) => {
5040
5685
  if (schema.collections.length > 0 || schema.singletons.length > 0) {
5041
- return c.json({ error: "Bu klas\xF6rde zaten bir \u015Fema var." }, 400);
5686
+ return c.json({ error: "This folder already has a schema." }, 400);
5042
5687
  }
5043
5688
  const { raw } = await c.req.json();
5044
5689
  let next;
@@ -5053,7 +5698,7 @@ async function createServer(root2) {
5053
5698
  entries = inferred.entries;
5054
5699
  singletonData = inferred.singletons;
5055
5700
  } catch (e) {
5056
- return c.json({ error: `\u0130\xE7e aktar\u0131lamad\u0131: ${e.message}` }, 400);
5701
+ return c.json({ error: `Import failed: ${e.message}` }, 400);
5057
5702
  }
5058
5703
  }
5059
5704
  await saveSchema(adapter, next, contentDir);
@@ -5061,7 +5706,7 @@ async function createServer(root2) {
5061
5706
  store = new ContentStore(adapter, schema, contentDir);
5062
5707
  for (const [collection, rows] of Object.entries(entries)) {
5063
5708
  for (const row of rows) {
5064
- const slug = slugify(String(row.slug ?? row.title ?? "icerik")) || "icerik";
5709
+ const slug = slugify(String(row.slug ?? row.title ?? "content")) || "content";
5065
5710
  await store.writeEntry(collection, slug, row);
5066
5711
  }
5067
5712
  }
@@ -5103,11 +5748,11 @@ async function createServer(root2) {
5103
5748
  });
5104
5749
  app.post("/api/_media", async (c) => {
5105
5750
  const body = await c.req.json();
5106
- if (!body.dataBase64) return c.json({ error: "veri yok" }, 400);
5751
+ if (!body.dataBase64) return c.json({ error: "no data" }, 400);
5107
5752
  const base = slugify((body.filename ?? "gorsel").replace(/\.[^.]+$/, "")) || "gorsel";
5108
5753
  const name = `${base}-${Date.now().toString(36)}.webp`;
5109
5754
  const rel = `${contentDir}/media/${name}`;
5110
- const abs = join3(root2, rel);
5755
+ const abs = join4(root2, rel);
5111
5756
  await mkdir2(dirname2(abs), { recursive: true });
5112
5757
  await writeFile3(abs, Buffer.from(body.dataBase64, "base64"));
5113
5758
  return c.json({ path: rel });
@@ -5115,7 +5760,7 @@ async function createServer(root2) {
5115
5760
  app.post("/api/_ai/models", async (c) => {
5116
5761
  const body = await c.req.json();
5117
5762
  const { provider, apiKey } = body;
5118
- if (!provider) return c.json({ error: "provider zorunlu" }, 400);
5763
+ if (!provider) return c.json({ error: "provider is required" }, 400);
5119
5764
  try {
5120
5765
  const models = provider === "gemini" ? await listGeminiModels(apiKey ?? "") : await listOpenAiCompatibleModels(
5121
5766
  body.baseUrl || PROVIDER_BASE_URLS[provider] || "",
@@ -5130,7 +5775,7 @@ async function createServer(root2) {
5130
5775
  const body = await c.req.json();
5131
5776
  const { provider, apiKey, model, system, prompt } = body;
5132
5777
  if (!provider || !apiKey || !model || !prompt) {
5133
- return c.json({ error: "provider, model, apiKey ve prompt zorunlu" }, 400);
5778
+ return c.json({ error: "provider, model, apiKey and prompt are required" }, 400);
5134
5779
  }
5135
5780
  try {
5136
5781
  const text = provider === "gemini" ? await callGemini(apiKey, model, system, prompt) : await callOpenAiCompatible(
@@ -5151,10 +5796,10 @@ async function createServer(root2) {
5151
5796
  return c.text("forbidden", 400);
5152
5797
  }
5153
5798
  try {
5154
- const bytes = await readFile2(join3(root2, contentDir, "media", file));
5799
+ const bytes = await readFile2(join4(root2, contentDir, "media", file));
5155
5800
  return new Response(bytes, { headers: { "content-type": "image/webp" } });
5156
5801
  } catch {
5157
- return c.text("yok", 404);
5802
+ return c.text("not found", 404);
5158
5803
  }
5159
5804
  });
5160
5805
  app.get("/api/:collection", async (c) => {
@@ -5163,7 +5808,7 @@ async function createServer(root2) {
5163
5808
  });
5164
5809
  app.get("/api/:collection/:slug", async (c) => {
5165
5810
  const data = await store.readEntry(c.req.param("collection"), c.req.param("slug"));
5166
- return data ? c.json(data) : c.json({ error: "bulunamad\u0131" }, 404);
5811
+ return data ? c.json(data) : c.json({ error: "not found" }, 404);
5167
5812
  });
5168
5813
  app.put("/api/:collection/:slug", async (c) => {
5169
5814
  const slug = slugify(c.req.param("slug"));
@@ -5178,17 +5823,17 @@ async function createServer(root2) {
5178
5823
  app.get("/*", async (c) => {
5179
5824
  const urlPath = c.req.path === "/" ? "/index.html" : c.req.path;
5180
5825
  const rel = normalize(urlPath).replace(/^(\.\.[/\\])+/, "");
5181
- const file = join3(editorDir, rel);
5826
+ const file = join4(editorDir, rel);
5182
5827
  if (!file.startsWith(editorDir)) return c.text("forbidden", 403);
5183
5828
  const type = MIME[extname(file)] ?? "application/octet-stream";
5184
5829
  try {
5185
5830
  return new Response(await readFile2(file), { headers: { "content-type": type } });
5186
5831
  } catch {
5187
5832
  try {
5188
- const html = await readFile2(join3(editorDir, "index.html"));
5833
+ const html = await readFile2(join4(editorDir, "index.html"));
5189
5834
  return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } });
5190
5835
  } catch {
5191
- return c.text("Edit\xF6r aray\xFCz\xFC bulunamad\u0131 (justjson build gerekli).", 404);
5836
+ return c.text("Editor UI not found (run the justjson build first).", 404);
5192
5837
  }
5193
5838
  }
5194
5839
  });
@@ -5203,27 +5848,44 @@ async function startServer(root2, port) {
5203
5848
  const app = await createServer(root2);
5204
5849
  serve({ fetch: app.fetch, port });
5205
5850
  const url = `http://localhost:${port}`;
5206
- console.log(`JustJSON \xE7al\u0131\u015F\u0131yor: ${url}`);
5851
+ console.log(`JustJSON is running at ${url}`);
5207
5852
  openBrowser(url);
5208
5853
  }
5209
5854
 
5855
+ // src/version.ts
5856
+ import { readFileSync } from "fs";
5857
+ function readVersion() {
5858
+ const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
5859
+ return JSON.parse(raw).version;
5860
+ }
5861
+
5210
5862
  // src/cli.ts
5211
5863
  var program = new Command();
5212
5864
  var root = process.cwd();
5213
- program.name("justjson").description("Lokalde \xE7al\u0131\u015Fan, JSON \xFCreten mini CMS").version("0.0.0");
5214
- program.command("init").description("Bir template ile projeyi ba\u015Flat\u0131r").argument("[template]", `template ad\u0131 (${listTemplates().join(", ")})`, "blog").action(async (template) => {
5865
+ program.name("justjson").description("A tiny local-first CMS that keeps your content as plain JSON").version(readVersion());
5866
+ program.command("init").description("Scaffold a project from a template").argument("[template]", `template name (${listTemplates().join(", ")})`, "blog").action(async (template) => {
5215
5867
  await initProject(root, template);
5216
- console.log(`'${template}' template'i ile ba\u015Flat\u0131ld\u0131.`);
5868
+ console.log(`Scaffolded from the '${template}' template.`);
5217
5869
  });
5218
- program.command("types").description("\u015Eemadan types.ts \xFCretir").action(async () => {
5870
+ program.command("types").description("Generate types.ts and a typed content.ts loader from your schema").action(async () => {
5219
5871
  const out = await generateTypesFile(root);
5220
- console.log(`Yaz\u0131ld\u0131: ${out}`);
5872
+ console.log(`Wrote: ${out}`);
5221
5873
  });
5222
- program.command("export").description("\u0130\xE7eri\u011Fi ZIP olarak d\u0131\u015Fa aktar\u0131r").action(async () => {
5874
+ program.command("export").description("Export schema, content and types as a ZIP").action(async () => {
5223
5875
  const out = await exportZip(root);
5224
- console.log(`Yaz\u0131ld\u0131: ${out}`);
5876
+ console.log(`Wrote: ${out}`);
5877
+ });
5878
+ program.command("validate").description("Check your content against the schema (great for CI)").option("--json", "machine-readable JSON output").option("--strict", "treat warnings as errors too").action(async (opts) => {
5879
+ const issues = await validateProjectAt(root);
5880
+ if (issues === null) {
5881
+ console.error("No schema found. Run `justjson init` first.");
5882
+ process.exitCode = 1;
5883
+ return;
5884
+ }
5885
+ console.log(opts.json ? formatJson(issues) : formatText(issues));
5886
+ if (shouldFail(issues, Boolean(opts.strict))) process.exitCode = 1;
5225
5887
  });
5226
- program.command("serve", { isDefault: true }).description("Lokal edit\xF6r sunucusunu ba\u015Flat\u0131r").option("-p, --port <port>", "port", "5180").action(async (opts) => {
5888
+ program.command("serve", { isDefault: true }).description("Start the local editor server").option("-p, --port <port>", "port", "5180").action(async (opts) => {
5227
5889
  await startServer(root, Number(opts.port));
5228
5890
  });
5229
5891
  program.parseAsync();