contentbase 0.0.1

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.
Files changed (89) hide show
  1. package/README.md +460 -0
  2. package/bun.lock +473 -0
  3. package/examples/sdlc-queries.ts +161 -0
  4. package/package.json +41 -0
  5. package/showcases/national-parks/models.ts +74 -0
  6. package/showcases/national-parks/parks/acadia.mdx +40 -0
  7. package/showcases/national-parks/parks/yosemite.mdx +44 -0
  8. package/showcases/national-parks/parks/zion.mdx +44 -0
  9. package/showcases/national-parks/queries.ts +103 -0
  10. package/showcases/national-parks/trails/angels-landing.mdx +19 -0
  11. package/showcases/national-parks/trails/cathedral-lakes.mdx +19 -0
  12. package/showcases/national-parks/trails/half-dome.mdx +19 -0
  13. package/showcases/national-parks/trails/jordan-pond-path.mdx +19 -0
  14. package/showcases/national-parks/trails/mist-trail.mdx +19 -0
  15. package/showcases/national-parks/trails/observation-point.mdx +19 -0
  16. package/showcases/national-parks/trails/precipice-trail.mdx +19 -0
  17. package/showcases/national-parks/trails/the-narrows.mdx +19 -0
  18. package/showcases/recipes/cuisines/chinese.mdx +28 -0
  19. package/showcases/recipes/cuisines/italian.mdx +32 -0
  20. package/showcases/recipes/cuisines/mexican.mdx +28 -0
  21. package/showcases/recipes/models.ts +77 -0
  22. package/showcases/recipes/queries.ts +89 -0
  23. package/showcases/recipes/recipes/chinese/egg-fried-rice.mdx +43 -0
  24. package/showcases/recipes/recipes/chinese/mapo-tofu.mdx +47 -0
  25. package/showcases/recipes/recipes/italian/bruschetta.mdx +38 -0
  26. package/showcases/recipes/recipes/italian/cacio-e-pepe.mdx +39 -0
  27. package/showcases/recipes/recipes/italian/tiramisu.mdx +43 -0
  28. package/showcases/recipes/recipes/mexican/chicken-tinga.mdx +44 -0
  29. package/showcases/recipes/recipes/mexican/guacamole.mdx +39 -0
  30. package/showcases/vinyl-collection/albums/bitches-brew.mdx +36 -0
  31. package/showcases/vinyl-collection/albums/i-put-a-spell-on-you.mdx +35 -0
  32. package/showcases/vinyl-collection/albums/in-rainbows.mdx +35 -0
  33. package/showcases/vinyl-collection/albums/kind-of-blue.mdx +32 -0
  34. package/showcases/vinyl-collection/albums/ok-computer.mdx +37 -0
  35. package/showcases/vinyl-collection/albums/wild-is-the-wind.mdx +35 -0
  36. package/showcases/vinyl-collection/artists/miles-davis.mdx +27 -0
  37. package/showcases/vinyl-collection/artists/nina-simone.mdx +26 -0
  38. package/showcases/vinyl-collection/artists/radiohead.mdx +27 -0
  39. package/showcases/vinyl-collection/models.ts +73 -0
  40. package/showcases/vinyl-collection/queries.ts +87 -0
  41. package/src/ast-query.ts +132 -0
  42. package/src/cli/commands/action.ts +44 -0
  43. package/src/cli/commands/create.ts +59 -0
  44. package/src/cli/commands/export.ts +24 -0
  45. package/src/cli/commands/init.ts +75 -0
  46. package/src/cli/commands/inspect.ts +46 -0
  47. package/src/cli/commands/validate.ts +75 -0
  48. package/src/cli/index.ts +20 -0
  49. package/src/cli/load-collection.ts +53 -0
  50. package/src/collection.ts +399 -0
  51. package/src/define-model.ts +80 -0
  52. package/src/document.ts +468 -0
  53. package/src/index.ts +47 -0
  54. package/src/model-instance.ts +227 -0
  55. package/src/node-shortcuts.ts +87 -0
  56. package/src/parse.ts +123 -0
  57. package/src/query/collection-query.ts +149 -0
  58. package/src/query/index.ts +5 -0
  59. package/src/query/operators.ts +37 -0
  60. package/src/query/query-builder.ts +109 -0
  61. package/src/relationships/belongs-to.ts +50 -0
  62. package/src/relationships/has-many.ts +136 -0
  63. package/src/relationships/index.ts +57 -0
  64. package/src/relationships/types.ts +7 -0
  65. package/src/section.ts +29 -0
  66. package/src/types.ts +221 -0
  67. package/src/utils/index.ts +11 -0
  68. package/src/utils/inflect.ts +82 -0
  69. package/src/utils/normalize-headings.ts +31 -0
  70. package/src/utils/parse-table.ts +30 -0
  71. package/src/utils/read-directory.ts +35 -0
  72. package/src/utils/stringify-ast.ts +9 -0
  73. package/src/validator.ts +52 -0
  74. package/test/ast-query.test.ts +128 -0
  75. package/test/collection.test.ts +99 -0
  76. package/test/define-model.test.ts +78 -0
  77. package/test/document.test.ts +225 -0
  78. package/test/fixtures/sdlc/epics/authentication.mdx +42 -0
  79. package/test/fixtures/sdlc/epics/searching-and-browsing.mdx +21 -0
  80. package/test/fixtures/sdlc/models.ts +89 -0
  81. package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-register.mdx +20 -0
  82. package/test/helpers.ts +21 -0
  83. package/test/model-instance.test.ts +197 -0
  84. package/test/query.test.ts +167 -0
  85. package/test/relationships.test.ts +84 -0
  86. package/test/section.test.ts +99 -0
  87. package/test/validator.test.ts +62 -0
  88. package/tsconfig.json +18 -0
  89. package/vitest.config.ts +11 -0
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Contentbase Example: Querying an SDLC Content Collection
3
+ *
4
+ * This script demonstrates loading a collection of Epics and Stories
5
+ * from markdown files and querying them using the Contentbase API.
6
+ *
7
+ * Run with: bun run examples/sdlc-queries.ts
8
+ */
9
+ import path from "path";
10
+ import { fileURLToPath } from "url";
11
+ import { Collection, type InferModelInstance } from "../src/index";
12
+ import { Epic, Story, type StoryDef } from "../test/fixtures/sdlc/models";
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const basePath = path.resolve(__dirname, "../test/fixtures/sdlc");
16
+
17
+ async function main() {
18
+ // 1. Create and load the collection
19
+ const collection = new Collection({
20
+ rootPath: basePath,
21
+ name: "sdlc",
22
+ });
23
+
24
+ collection.register(Epic);
25
+ collection.register(Story);
26
+ await collection.load();
27
+
28
+ console.log("Available documents:", collection.available);
29
+
30
+ // -------------------------------------------------------
31
+ // 2. Get a single model instance by path ID
32
+ // -------------------------------------------------------
33
+ const authEpic = collection.getModel("epics/authentication", Epic);
34
+
35
+ console.log("\n--- Epic: Authentication ---");
36
+ console.log("Title:", authEpic.title);
37
+ console.log("Slug:", authEpic.slug);
38
+ console.log("Status:", authEpic.meta.status);
39
+ console.log("Priority:", authEpic.meta.priority);
40
+ console.log("Is complete?", authEpic.computed.isComplete);
41
+
42
+ // -------------------------------------------------------
43
+ // 3. Query all epics
44
+ // -------------------------------------------------------
45
+ const allEpics = await collection.query(Epic).fetchAll();
46
+
47
+ console.log("\n--- All Epics ---");
48
+ for (const epic of allEpics) {
49
+ console.log(` ${epic.title} (${epic.meta.status})`);
50
+ }
51
+
52
+ // -------------------------------------------------------
53
+ // 4. Filter with where clauses
54
+ // -------------------------------------------------------
55
+ const highPriority = await collection
56
+ .query(Epic)
57
+ .where("meta.priority", "high")
58
+ .fetchAll();
59
+
60
+ console.log("\n--- High Priority Epics ---");
61
+ for (const epic of highPriority) {
62
+ console.log(` ${epic.title} — priority: ${epic.meta.priority}`);
63
+ }
64
+
65
+ // -------------------------------------------------------
66
+ // 5. Query helpers: first, last, count
67
+ // -------------------------------------------------------
68
+ const firstEpic = await collection.query(Epic).first();
69
+ const lastEpic = await collection.query(Epic).last();
70
+ const epicCount = await collection.query(Epic).count();
71
+
72
+ console.log("\n--- Query Helpers ---");
73
+ console.log("First epic:", firstEpic?.title);
74
+ console.log("Last epic:", lastEpic?.title);
75
+ console.log("Total epics:", epicCount);
76
+
77
+ // -------------------------------------------------------
78
+ // 6. Chained where clauses (AND logic)
79
+ // -------------------------------------------------------
80
+ const filtered = await collection
81
+ .query(Epic)
82
+ .where("meta.status", "created")
83
+ .whereExists("meta.priority")
84
+ .fetchAll();
85
+
86
+ console.log("\n--- Created Epics with Priority Set ---");
87
+ for (const epic of filtered) {
88
+ console.log(` ${epic.title} — ${epic.meta.priority}`);
89
+ }
90
+
91
+ // -------------------------------------------------------
92
+ // 7. HasMany relationships — Epic -> Stories
93
+ // -------------------------------------------------------
94
+ const stories = authEpic.relationships.stories.fetchAll();
95
+
96
+ console.log("\n--- Stories under Authentication Epic ---");
97
+ for (const story of stories) {
98
+ console.log(` ${story.title}`);
99
+ }
100
+
101
+ console.log("First story:", authEpic.relationships.stories.first()?.title);
102
+ console.log("Last story:", authEpic.relationships.stories.last()?.title);
103
+
104
+ // -------------------------------------------------------
105
+ // 8. BelongsTo relationships — Story -> Epic
106
+ // -------------------------------------------------------
107
+ const registerStory: InferModelInstance<StoryDef> = collection.getModel(
108
+ "stories/authentication/a-user-should-be-able-to-register",
109
+ Story
110
+ );
111
+
112
+ const parentEpic = registerStory.relationships.epic.fetch();
113
+
114
+ console.log("\n--- Story -> Epic (belongsTo) ---");
115
+ console.log(`"${registerStory.title}" belongs to "${parentEpic.title}"`);
116
+
117
+ // -------------------------------------------------------
118
+ // 9. Sections — structured data extracted from headings
119
+ // -------------------------------------------------------
120
+ console.log("\n--- Sections: Acceptance Criteria ---");
121
+ for (const criterion of registerStory.sections.acceptanceCriteria) {
122
+ console.log(` • ${criterion}`);
123
+ }
124
+
125
+ console.log("\n--- Sections: Mockups ---");
126
+ for (const [label, url] of Object.entries(registerStory.sections.mockups)) {
127
+ console.log(` ${label}: ${url}`);
128
+ }
129
+
130
+ // -------------------------------------------------------
131
+ // 10. Validation
132
+ // -------------------------------------------------------
133
+ const result = await registerStory.validate();
134
+
135
+ console.log("\n--- Validation ---");
136
+ console.log("Valid?", result.valid);
137
+ console.log("Error count:", result.errors.length);
138
+
139
+ // -------------------------------------------------------
140
+ // 11. Serialization
141
+ // -------------------------------------------------------
142
+ const json = authEpic.toJSON({
143
+ computed: ["isComplete"],
144
+ related: ["stories"],
145
+ });
146
+
147
+ console.log("\n--- toJSON ---");
148
+ console.log(JSON.stringify(json, null, 2));
149
+
150
+ // -------------------------------------------------------
151
+ // 12. Working with the raw Document
152
+ // -------------------------------------------------------
153
+ const doc = authEpic.document;
154
+
155
+ console.log("\n--- Raw Document ---");
156
+ console.log("Headings:", doc.nodes.headings.length);
157
+ console.log("Links:", doc.nodes.links.length);
158
+ console.log("Lists:", doc.nodes.lists.length);
159
+ }
160
+
161
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "contentbase",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "bin": {
8
+ "contentbase": "./src/cli/index.ts"
9
+ },
10
+ "scripts": {
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "typecheck": "tsc --noEmit"
14
+ },
15
+ "dependencies": {
16
+ "citty": "^0.1.6",
17
+ "gray-matter": "^4.0.3",
18
+ "js-yaml": "^4.1.0",
19
+ "mdast-util-mdxjs-esm": "^2.0.1",
20
+ "mdast-util-to-markdown": "^2.1.2",
21
+ "mdast-util-to-string": "^4.0.0",
22
+ "remark-gfm": "^4.0.0",
23
+ "remark-parse": "^11.0.0",
24
+ "remark-stringify": "^11.0.0",
25
+ "unified": "^11.0.5",
26
+ "unist-util-find-after": "^5.0.0",
27
+ "unist-util-find-all-after": "^5.0.0",
28
+ "unist-util-find-all-before": "^5.0.0",
29
+ "unist-util-find-before": "^4.0.0",
30
+ "unist-util-select": "^5.1.0",
31
+ "unist-util-visit": "^5.0.0",
32
+ "zod": "^3.23.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/js-yaml": "^4.0.9",
36
+ "@types/mdast": "^4.0.4",
37
+ "bun-types": "^1.1.0",
38
+ "typescript": "^5.4.0",
39
+ "vitest": "^1.6.0"
40
+ }
41
+ }
@@ -0,0 +1,74 @@
1
+ import {
2
+ defineModel,
3
+ section,
4
+ hasMany,
5
+ belongsTo,
6
+ z,
7
+ } from "../../src/index";
8
+ import { toString } from "mdast-util-to-string";
9
+ import { parseTable } from "../../src/utils/parse-table";
10
+
11
+ // ─── Park (parent) ───
12
+
13
+ export const Park = defineModel("Park", {
14
+ prefix: "parks",
15
+ meta: z.object({
16
+ state: z.string(),
17
+ region: z.enum(["west", "southwest", "southeast", "northeast", "midwest"]),
18
+ established: z.number(),
19
+ area: z.string(),
20
+ visitors: z.number(),
21
+ fee: z.number(),
22
+ }),
23
+ sections: {
24
+ wildlife: section("Wildlife", {
25
+ extract: (q) => q.selectAll("listItem").map((n) => toString(n)),
26
+ schema: z.array(z.string()),
27
+ }),
28
+ seasons: section("Best Seasons", {
29
+ extract: (q) => {
30
+ const tables = q.selectAll("table");
31
+ if (tables.length > 0) {
32
+ return parseTable(tables[0]);
33
+ }
34
+ return [];
35
+ },
36
+ schema: z.array(z.record(z.string(), z.string())),
37
+ }),
38
+ },
39
+ relationships: {
40
+ trails: hasMany(() => Trail, { heading: "Trails" }),
41
+ },
42
+ computed: {
43
+ isPopular: (self: any) => self.meta.visitors > 3_000_000,
44
+ ageYears: (self: any) => new Date().getFullYear() - self.meta.established,
45
+ },
46
+ });
47
+
48
+ // ─── Trail ───
49
+
50
+ export const Trail = defineModel("Trail", {
51
+ prefix: "trails",
52
+ meta: z.object({
53
+ park: z.string(),
54
+ distance: z.string(),
55
+ elevationGain: z.string(),
56
+ difficulty: z.enum(["easy", "moderate", "strenuous"]),
57
+ type: z.enum(["out-and-back", "loop", "point-to-point"]),
58
+ dogs: z.boolean().default(false),
59
+ }),
60
+ sections: {
61
+ highlights: section("Highlights", {
62
+ extract: (q) => q.selectAll("listItem").map((n) => toString(n)),
63
+ schema: z.array(z.string()),
64
+ }),
65
+ },
66
+ relationships: {
67
+ park: belongsTo(() => Park, {
68
+ foreignKey: (doc) => doc.meta.park as string,
69
+ }),
70
+ },
71
+ computed: {
72
+ isLong: (self: any) => parseFloat(self.meta.distance) > 10,
73
+ },
74
+ });
@@ -0,0 +1,40 @@
1
+ ---
2
+ state: Maine
3
+ region: northeast
4
+ established: 1919
5
+ area: "49,071 acres"
6
+ visitors: 4069000
7
+ fee: 30
8
+ ---
9
+
10
+ # Acadia
11
+
12
+ The only national park in the northeastern United States. Rocky coastline, granite peaks, and boreal forest crammed onto an island off the coast of Maine. Small in acreage but staggering in density of beauty.
13
+
14
+ ## Wildlife
15
+
16
+ - Moose
17
+ - Harbor seal
18
+ - Bald eagle
19
+ - Puffin (offshore)
20
+ - Snowshoe hare
21
+ - Peregrine falcon
22
+
23
+ ## Best Seasons
24
+
25
+ | Season | Conditions | Crowds |
26
+ | --- | --- | --- |
27
+ | Spring | Mud season, wildflowers begin | Light |
28
+ | Summer | Warm, long days, all carriage roads open | Heavy |
29
+ | Fall | Peak foliage, crisp air, perfect hiking | Heavy |
30
+ | Winter | Snow, cross-country skiing, solitude | Very light |
31
+
32
+ ## Trails
33
+
34
+ ### Precipice Trail
35
+
36
+ Iron rungs and ladders bolted into a cliff face. More climb than hike. Seasonal closures for nesting peregrines.
37
+
38
+ ### Jordan Pond Path
39
+
40
+ A flat, easy loop around the clearest lake in the park. End with popovers at Jordan Pond House.
@@ -0,0 +1,44 @@
1
+ ---
2
+ state: California
3
+ region: west
4
+ established: 1890
5
+ area: "748,436 acres"
6
+ visitors: 3690000
7
+ fee: 35
8
+ ---
9
+
10
+ # Yosemite
11
+
12
+ Granite cliffs, ancient sequoias, and waterfalls that drop thousands of feet. Yosemite Valley alone is worth the trip, but the backcountry — Tuolumne Meadows, the high Sierra — is where the park reveals its full scale.
13
+
14
+ ## Wildlife
15
+
16
+ - Black bear
17
+ - Mule deer
18
+ - Peregrine falcon
19
+ - Sierra Nevada bighorn sheep
20
+ - Great gray owl
21
+ - Pacific fisher
22
+
23
+ ## Best Seasons
24
+
25
+ | Season | Conditions | Crowds |
26
+ | --- | --- | --- |
27
+ | Spring | Waterfalls at peak flow, some roads closed | Moderate |
28
+ | Summer | Warm, all facilities open | Heavy |
29
+ | Fall | Cool, fewer crowds, fall color | Light |
30
+ | Winter | Snow, Tioga Road closed, skiing | Very light |
31
+
32
+ ## Trails
33
+
34
+ ### Mist Trail
35
+
36
+ The park's most iconic hike — steep granite stairs through the spray of Vernal Fall and on to Nevada Fall.
37
+
38
+ ### Half Dome
39
+
40
+ The bucket-list summit. Cables, permits, and 14+ miles of exertion for a view you'll never forget.
41
+
42
+ ### Cathedral Lakes
43
+
44
+ A high-country gem in Tuolumne Meadows — alpine lakes ringed by granite peaks.
@@ -0,0 +1,44 @@
1
+ ---
2
+ state: Utah
3
+ region: southwest
4
+ established: 1919
5
+ area: "147,242 acres"
6
+ visitors: 4692000
7
+ fee: 35
8
+ ---
9
+
10
+ # Zion
11
+
12
+ Massive sandstone walls in shades of red, pink, and cream tower above the Virgin River. The canyon is narrow enough to feel intimate and vast enough to feel humbling. Zion manages to be both.
13
+
14
+ ## Wildlife
15
+
16
+ - Desert bighorn sheep
17
+ - California condor
18
+ - Ringtail cat
19
+ - Collared lizard
20
+ - Mexican spotted owl
21
+ - Canyon tree frog
22
+
23
+ ## Best Seasons
24
+
25
+ | Season | Conditions | Crowds |
26
+ | --- | --- | --- |
27
+ | Spring | Wildflowers, moderate temps, some flash flood risk | Moderate |
28
+ | Summer | Hot (100°F+), best for The Narrows | Very heavy |
29
+ | Fall | Perfect temps, fall color in the canyon | Moderate |
30
+ | Winter | Cool, snow on rims, peaceful | Light |
31
+
32
+ ## Trails
33
+
34
+ ### Angels Landing
35
+
36
+ A chain-assisted climb along a narrow ridge with 1,500-foot dropoffs on either side. Not for the faint-hearted.
37
+
38
+ ### The Narrows
39
+
40
+ Wade upstream through the Virgin River between thousand-foot canyon walls. The ultimate slot canyon experience.
41
+
42
+ ### Observation Point
43
+
44
+ The highest viewpoint in the main canyon — looks down on Angels Landing from above.
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Example queries for the National Parks showcase.
3
+ *
4
+ * Run with: bun showcases/national-parks/queries.ts
5
+ */
6
+ import { Collection } from "../../src/index";
7
+ import { Park, Trail } from "./models";
8
+
9
+ const collection = new Collection({
10
+ rootPath: new URL(".", import.meta.url).pathname,
11
+ });
12
+
13
+ collection.register(Park);
14
+ collection.register(Trail);
15
+ await collection.load();
16
+
17
+ // ── All parks ──
18
+ const allParks = await collection.query(Park).fetchAll();
19
+ console.log(`Total parks: ${allParks.length}`);
20
+ for (const park of allParks) {
21
+ console.log(
22
+ ` ${park.title} (${park.meta.state}) — ${park.meta.area}, est. ${park.meta.established}`
23
+ );
24
+ }
25
+
26
+ // ── Filter by region ──
27
+ const western = await collection
28
+ .query(Park)
29
+ .whereIn("meta.region", ["west", "southwest"])
30
+ .fetchAll();
31
+ console.log(
32
+ `\nWestern parks: ${western.map((p) => p.title).join(", ")}`
33
+ );
34
+
35
+ // ── Popular parks (>3M visitors) ──
36
+ console.log(`\nPopular parks (>3M visitors):`);
37
+ for (const park of allParks) {
38
+ if (park.computed.isPopular) {
39
+ console.log(
40
+ ` ${park.title} — ${park.meta.visitors.toLocaleString()} visitors`
41
+ );
42
+ }
43
+ }
44
+
45
+ // ── Park age ──
46
+ console.log(`\nPark ages:`);
47
+ for (const park of allParks) {
48
+ console.log(` ${park.title}: ${park.computed.ageYears} years old`);
49
+ }
50
+
51
+ // ── All trails ──
52
+ const allTrails = await collection.query(Trail).fetchAll();
53
+ console.log(`\nTotal trails: ${allTrails.length}`);
54
+
55
+ // ── Filter by difficulty ──
56
+ const strenuous = await collection
57
+ .query(Trail)
58
+ .where("meta.difficulty", "strenuous")
59
+ .fetchAll();
60
+ console.log(
61
+ `\nStrenuous trails: ${strenuous.map((t) => t.title).join(", ")}`
62
+ );
63
+
64
+ const easy = await collection
65
+ .query(Trail)
66
+ .where("meta.difficulty", "easy")
67
+ .fetchAll();
68
+ console.log(`Easy trails: ${easy.map((t) => t.title).join(", ")}`);
69
+
70
+ // ── Dog-friendly trails ──
71
+ const dogFriendly = await collection
72
+ .query(Trail)
73
+ .where("meta.dogs", true)
74
+ .fetchAll();
75
+ console.log(
76
+ `\nDog-friendly trails: ${dogFriendly.map((t) => t.title).join(", ")}`
77
+ );
78
+
79
+ // ── Sections as structured data ──
80
+ const yosemite = collection.getModel("parks/yosemite", Park);
81
+ console.log(`\n--- ${yosemite.title} ---`);
82
+ console.log("Wildlife:", yosemite.sections.wildlife);
83
+ console.log("Best seasons:", yosemite.sections.seasons);
84
+
85
+ // ── Park → Trails relationship (hasMany) ──
86
+ const zion = collection.getModel("parks/zion", Park);
87
+ const zionTrails = zion.relationships.trails.fetchAll();
88
+ console.log(
89
+ `\n${zion.title} trails: ${zionTrails.map((t) => t.title).join(", ")}`
90
+ );
91
+
92
+ // ── Trail → Park relationship (belongsTo) ──
93
+ const narrows = collection.getModel("trails/the-narrows", Trail);
94
+ const parentPark = narrows.relationships.park.fetch();
95
+ console.log(`\n${narrows.title} is in ${parentPark.title} (${parentPark.meta.state})`);
96
+ console.log("Highlights:", narrows.sections.highlights);
97
+
98
+ // ── Serialize ──
99
+ const json = yosemite.toJSON({
100
+ sections: ["wildlife", "seasons"],
101
+ computed: ["isPopular", "ageYears"],
102
+ });
103
+ console.log(`\nYosemite as JSON:`, JSON.stringify(json, null, 2));
@@ -0,0 +1,19 @@
1
+ ---
2
+ park: zion
3
+ distance: "5.4 mi"
4
+ elevationGain: "1,488 ft"
5
+ difficulty: strenuous
6
+ type: out-and-back
7
+ dogs: false
8
+ ---
9
+
10
+ # Angels Landing
11
+
12
+ Zion's most famous trail — and one of the most exposed hikes in any national park. The final half-mile follows a knife-edge ridge with chains for handholds and sheer dropoffs on both sides. Permit required.
13
+
14
+ ## Highlights
15
+
16
+ - Walter's Wiggles — 21 tight switchbacks carved into the cliff
17
+ - Scout Lookout — a wide saddle with stunning views even if you turn back here
18
+ - Chain section along the narrow ridge
19
+ - Panoramic views of Zion Canyon from the 5,790 ft summit
@@ -0,0 +1,19 @@
1
+ ---
2
+ park: yosemite
3
+ distance: "7.0 mi"
4
+ elevationGain: "1,000 ft"
5
+ difficulty: moderate
6
+ type: out-and-back
7
+ dogs: false
8
+ ---
9
+
10
+ # Cathedral Lakes
11
+
12
+ A high-country classic in Tuolumne Meadows. Two pristine alpine lakes sit in granite bowls below Cathedral Peak. The trail winds through subalpine meadows dotted with wildflowers in midsummer.
13
+
14
+ ## Highlights
15
+
16
+ - Lower Cathedral Lake — wide and open with Cathedral Peak reflected in the water
17
+ - Upper Cathedral Lake — smaller, more secluded, tucked under the peak
18
+ - Wildflower meadows in July and August
19
+ - Views of Cathedral Peak and Eichorn Pinnacle
@@ -0,0 +1,19 @@
1
+ ---
2
+ park: yosemite
3
+ distance: "14.2 mi"
4
+ elevationGain: "4,800 ft"
5
+ difficulty: strenuous
6
+ type: out-and-back
7
+ dogs: false
8
+ ---
9
+
10
+ # Half Dome
11
+
12
+ The defining Yosemite summit. A full-day endurance test that follows the Mist Trail, then climbs through the sub-dome before ascending the final 400 feet on steel cables bolted into granite. Permits required.
13
+
14
+ ## Highlights
15
+
16
+ - Cable section on the final ascent — thrilling and exposed
17
+ - Summit views of Tenaya Canyon, Clouds Rest, and the entire valley
18
+ - Passes both Vernal and Nevada Falls on the way up
19
+ - Sub-dome scramble over smooth granite slabs
@@ -0,0 +1,19 @@
1
+ ---
2
+ park: acadia
3
+ distance: "3.3 mi"
4
+ elevationGain: "105 ft"
5
+ difficulty: easy
6
+ type: loop
7
+ dogs: true
8
+ ---
9
+
10
+ # Jordan Pond Path
11
+
12
+ A flat, family-friendly loop around the clearest lake in Acadia. The path hugs the shoreline through mixed forest, with iconic views of the Bubbles — two rounded granite peaks — reflected in the water.
13
+
14
+ ## Highlights
15
+
16
+ - Mirror-like reflections of the Bubbles on calm mornings
17
+ - Boardwalk sections over the boggy north shore
18
+ - Jordan Pond House for tea and popovers after the hike
19
+ - Accessible for all skill levels and ages
@@ -0,0 +1,19 @@
1
+ ---
2
+ park: yosemite
3
+ distance: "5.4 mi"
4
+ elevationGain: "1,000 ft"
5
+ difficulty: strenuous
6
+ type: out-and-back
7
+ dogs: false
8
+ ---
9
+
10
+ # Mist Trail
11
+
12
+ Yosemite's most iconic day hike. Steep granite stairs climb through the spray of Vernal Fall, then continue along the Merced River to the top of Nevada Fall. You will get wet.
13
+
14
+ ## Highlights
15
+
16
+ - Vernal Fall (317 ft) — close enough to feel the mist
17
+ - Nevada Fall (594 ft) — a massive cascade visible from the bridge above
18
+ - Emerald Pool between the two falls
19
+ - Granite staircase carved into the cliff face
@@ -0,0 +1,19 @@
1
+ ---
2
+ park: zion
3
+ distance: "8.0 mi"
4
+ elevationGain: "2,148 ft"
5
+ difficulty: strenuous
6
+ type: out-and-back
7
+ dogs: false
8
+ ---
9
+
10
+ # Observation Point
11
+
12
+ The highest viewpoint accessible by trail in Zion Canyon. From the top, you look directly down on Angels Landing — a perspective that makes you appreciate how exposed that ridge really is.
13
+
14
+ ## Highlights
15
+
16
+ - Echo Canyon section — narrow side canyon with weeping rock walls
17
+ - Views down onto Angels Landing and the Great White Throne
18
+ - Cable Mountain and the historic cable works ruins nearby
19
+ - Slickrock terrain near the summit
@@ -0,0 +1,19 @@
1
+ ---
2
+ park: acadia
3
+ distance: "1.6 mi"
4
+ elevationGain: "1,058 ft"
5
+ difficulty: strenuous
6
+ type: out-and-back
7
+ dogs: false
8
+ ---
9
+
10
+ # Precipice Trail
11
+
12
+ Acadia's most thrilling route — iron rungs, ladders, and narrow ledges bolted into the cliff face of Champlain Mountain. More via ferrata than hiking trail. Closed spring through midsummer for peregrine falcon nesting.
13
+
14
+ ## Highlights
15
+
16
+ - Iron rung ladders on near-vertical rock faces
17
+ - Exposed traverses with ocean views
18
+ - Summit views of Frenchman Bay and the Porcupine Islands
19
+ - Peregrine falcons nesting on the cliff (visible from below during closures)
@@ -0,0 +1,19 @@
1
+ ---
2
+ park: zion
3
+ distance: "9.4 mi"
4
+ elevationGain: "334 ft"
5
+ difficulty: strenuous
6
+ type: out-and-back
7
+ dogs: false
8
+ ---
9
+
10
+ # The Narrows
11
+
12
+ A river hike through the narrowest section of Zion Canyon. You wade — and occasionally swim — upstream through the Virgin River between sandstone walls up to 1,000 feet high. Entirely unlike any other trail in the park system.
13
+
14
+ ## Highlights
15
+
16
+ - Wall Street section — walls narrow to 20 feet with the river filling the entire canyon floor
17
+ - Orderville Canyon junction — a side canyon worth exploring
18
+ - Hanging gardens of ferns and columbine on the canyon walls
19
+ - The interplay of light and shadow in the narrow slot