@opnpress/opnpress-cli 0.1.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/README.md +72 -0
- package/dist/build.js +518 -0
- package/dist/cli.js +68 -0
- package/dist/config.js +210 -0
- package/dist/content.js +109 -0
- package/dist/init.js +45 -0
- package/dist/render.js +1975 -0
- package/dist/server.js +97 -0
- package/dist/utils.js +45 -0
- package/package.json +51 -0
- package/templates/.github/workflows/build-pages.yml +37 -0
- package/templates/.skills/README.md +6 -0
- package/templates/.skills/add-shortcode.md +18 -0
- package/templates/.skills/create-page.md +17 -0
- package/templates/.skills/deployment-checks.md +75 -0
- package/templates/.skills/integrations.md +6 -0
- package/templates/.skills/link-audit.md +17 -0
- package/templates/.skills/setup-integrations.md +84 -0
- package/templates/.skills/shortcodes.md +152 -0
- package/templates/.skills/site-audit.md +20 -0
- package/templates/.skills/theme-customization.md +17 -0
- package/templates/.skills/update-header-footer.md +15 -0
- package/templates/.skills/update-navigation.md +16 -0
- package/templates/.skills/update-page.md +16 -0
- package/templates/README.md +32 -0
- package/templates/content/pages/index.md +8 -0
- package/templates/navigation.yaml +20 -0
- package/templates/site.config.yaml +80 -0
- package/templates/theme.config.yaml +42 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
const NavLinkSchema = z.object({
|
|
7
|
+
title: z.string().min(1),
|
|
8
|
+
url: z.string().min(1)
|
|
9
|
+
});
|
|
10
|
+
const NavigationSchema = z.object({
|
|
11
|
+
main: z.array(NavLinkSchema).default([]),
|
|
12
|
+
footer: z.array(NavLinkSchema).default([])
|
|
13
|
+
});
|
|
14
|
+
const ThemeSchema = z.object({
|
|
15
|
+
colors: z
|
|
16
|
+
.object({
|
|
17
|
+
primary: z.string().default('#1c3557'),
|
|
18
|
+
accent: z.string().default('#c86b3c'),
|
|
19
|
+
background: z.string().default('#f5efe6'),
|
|
20
|
+
surface: z.string().default('#fffaf3'),
|
|
21
|
+
text: z.string().default('#14212f'),
|
|
22
|
+
muted: z.string().default('#5e6b76'),
|
|
23
|
+
border: z.string().default('rgba(20, 33, 47, 0.12)')
|
|
24
|
+
})
|
|
25
|
+
.default({}),
|
|
26
|
+
backgrounds: z
|
|
27
|
+
.object({
|
|
28
|
+
page: z.string().default('radial-gradient(circle at top, rgba(200, 107, 60, 0.12), transparent 34%), linear-gradient(180deg, #f5efe6 0%, #fffaf3 100%)')
|
|
29
|
+
})
|
|
30
|
+
.default({}),
|
|
31
|
+
fonts: z
|
|
32
|
+
.object({
|
|
33
|
+
heading: z.string().default('Georgia, serif'),
|
|
34
|
+
body: z.string().default('Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'),
|
|
35
|
+
mono: z.string().default('ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace')
|
|
36
|
+
})
|
|
37
|
+
.default({}),
|
|
38
|
+
sizing: z
|
|
39
|
+
.object({
|
|
40
|
+
contentWidth: z.string().default('72rem'),
|
|
41
|
+
radius: z.string().default('24px'),
|
|
42
|
+
spacing: z.string().default('1rem')
|
|
43
|
+
})
|
|
44
|
+
.default({})
|
|
45
|
+
});
|
|
46
|
+
const ContactFormIntegrationSchema = z.object({
|
|
47
|
+
provider: z.enum(['formspree', 'basin', 'custom']).default('formspree'),
|
|
48
|
+
action: z.string().url().optional(),
|
|
49
|
+
buttonLabel: z.string().min(1).default('Send message')
|
|
50
|
+
});
|
|
51
|
+
const ShareableLinksIntegrationSchema = z.object({
|
|
52
|
+
enabled: z.boolean().default(true)
|
|
53
|
+
});
|
|
54
|
+
const ContactLinksIntegrationSchema = z.object({
|
|
55
|
+
email: z.string().optional(),
|
|
56
|
+
phone: z.string().optional(),
|
|
57
|
+
website: z.string().optional(),
|
|
58
|
+
mapUrl: z.string().url().optional(),
|
|
59
|
+
address: z.string().optional(),
|
|
60
|
+
hours: z.string().optional(),
|
|
61
|
+
showIcons: z.boolean().default(true),
|
|
62
|
+
showLabels: z.boolean().default(true),
|
|
63
|
+
showInHeader: z.boolean().default(false),
|
|
64
|
+
showInFooter: z.boolean().default(false)
|
|
65
|
+
});
|
|
66
|
+
const CompanyInfoIntegrationSchema = z.object({
|
|
67
|
+
name: z.string().optional(),
|
|
68
|
+
tagline: z.string().optional(),
|
|
69
|
+
logo: z.string().optional(),
|
|
70
|
+
address: z.string().optional(),
|
|
71
|
+
mapUrl: z.string().url().optional(),
|
|
72
|
+
phone: z.string().optional(),
|
|
73
|
+
email: z.string().optional(),
|
|
74
|
+
hours: z.string().optional(),
|
|
75
|
+
website: z.string().optional(),
|
|
76
|
+
showInHeader: z.boolean().default(false),
|
|
77
|
+
showInFooter: z.boolean().default(false)
|
|
78
|
+
});
|
|
79
|
+
const BookingCalendarIntegrationSchema = z.object({
|
|
80
|
+
provider: z.enum(['calendly', 'calcom']).default('calendly'),
|
|
81
|
+
embedUrl: z.string().url().optional(),
|
|
82
|
+
buttonLabel: z.string().min(1).default('Book a call')
|
|
83
|
+
});
|
|
84
|
+
const AnalyticsIntegrationSchema = z.object({
|
|
85
|
+
provider: z.enum(['google-analytics']).default('google-analytics'),
|
|
86
|
+
measurementId: z.string().min(1).optional()
|
|
87
|
+
});
|
|
88
|
+
const MapsIntegrationSchema = z.object({
|
|
89
|
+
provider: z.enum(['google-maps']).default('google-maps'),
|
|
90
|
+
embedUrl: z.string().url().optional()
|
|
91
|
+
});
|
|
92
|
+
const IntegrationsSchema = z.object({
|
|
93
|
+
contactForm: ContactFormIntegrationSchema.default({
|
|
94
|
+
provider: 'formspree',
|
|
95
|
+
buttonLabel: 'Send message'
|
|
96
|
+
}),
|
|
97
|
+
shareableLinks: ShareableLinksIntegrationSchema.default({
|
|
98
|
+
enabled: true
|
|
99
|
+
}),
|
|
100
|
+
contactLinks: ContactLinksIntegrationSchema.default({}),
|
|
101
|
+
companyInfo: CompanyInfoIntegrationSchema.default({}),
|
|
102
|
+
bookingCalendar: BookingCalendarIntegrationSchema.default({
|
|
103
|
+
provider: 'calendly',
|
|
104
|
+
buttonLabel: 'Book a call'
|
|
105
|
+
}),
|
|
106
|
+
analytics: AnalyticsIntegrationSchema.default({
|
|
107
|
+
provider: 'google-analytics'
|
|
108
|
+
}),
|
|
109
|
+
maps: MapsIntegrationSchema.default({
|
|
110
|
+
provider: 'google-maps'
|
|
111
|
+
})
|
|
112
|
+
});
|
|
113
|
+
const DeploymentSchema = z
|
|
114
|
+
.object({
|
|
115
|
+
provider: z.enum(['github-pages', 'cloudflare-pages']).default('cloudflare-pages'),
|
|
116
|
+
cloudflare: z
|
|
117
|
+
.object({
|
|
118
|
+
projectName: z.string().min(1).optional(),
|
|
119
|
+
productionBranch: z.string().min(1).default('master')
|
|
120
|
+
})
|
|
121
|
+
.default({
|
|
122
|
+
productionBranch: 'master'
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
.default({
|
|
126
|
+
provider: 'cloudflare-pages',
|
|
127
|
+
cloudflare: {
|
|
128
|
+
productionBranch: 'master'
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
const FrontmatterSchema = z.object({
|
|
132
|
+
title: z.string().min(1).optional(),
|
|
133
|
+
description: z.string().optional(),
|
|
134
|
+
slug: z.string().optional(),
|
|
135
|
+
tags: z.array(z.string()).optional(),
|
|
136
|
+
categories: z.array(z.string()).optional(),
|
|
137
|
+
published: z.boolean().optional(),
|
|
138
|
+
draft: z.boolean().optional(),
|
|
139
|
+
image: z.string().optional(),
|
|
140
|
+
canonical: z.string().optional(),
|
|
141
|
+
date: z.union([z.string(), z.date()]).optional(),
|
|
142
|
+
updated: z.union([z.string(), z.date()]).optional(),
|
|
143
|
+
author: z.string().optional(),
|
|
144
|
+
schema: z.any().optional(),
|
|
145
|
+
aiVisibility: z.boolean().optional(),
|
|
146
|
+
llmVisibility: z.boolean().optional(),
|
|
147
|
+
customMetadata: z.record(z.any()).optional()
|
|
148
|
+
});
|
|
149
|
+
const SiteSchema = z.object({
|
|
150
|
+
site: z.object({
|
|
151
|
+
name: z.string().min(1),
|
|
152
|
+
domain: z.string().min(1),
|
|
153
|
+
description: z.string().optional().default(''),
|
|
154
|
+
language: z.string().min(1).default('en'),
|
|
155
|
+
theme: z.string().min(1).default('default'),
|
|
156
|
+
logo: z.string().optional(),
|
|
157
|
+
favicon: z.string().optional()
|
|
158
|
+
}),
|
|
159
|
+
seo: z
|
|
160
|
+
.object({
|
|
161
|
+
defaultImage: z.string().optional()
|
|
162
|
+
})
|
|
163
|
+
.default({}),
|
|
164
|
+
socials: z
|
|
165
|
+
.object({
|
|
166
|
+
twitter: z.string().optional(),
|
|
167
|
+
x: z.string().optional(),
|
|
168
|
+
github: z.string().optional(),
|
|
169
|
+
facebook: z.string().optional(),
|
|
170
|
+
instagram: z.string().optional(),
|
|
171
|
+
linkedin: z.string().optional(),
|
|
172
|
+
youtube: z.string().optional(),
|
|
173
|
+
tiktok: z.string().optional(),
|
|
174
|
+
threads: z.string().optional(),
|
|
175
|
+
mastodon: z.string().optional(),
|
|
176
|
+
bluesky: z.string().optional(),
|
|
177
|
+
website: z.string().optional(),
|
|
178
|
+
email: z.string().optional()
|
|
179
|
+
})
|
|
180
|
+
.default({}),
|
|
181
|
+
branding: z
|
|
182
|
+
.object({
|
|
183
|
+
poweredByOpnPress: z.boolean().default(true)
|
|
184
|
+
})
|
|
185
|
+
.default({}),
|
|
186
|
+
deployment: DeploymentSchema,
|
|
187
|
+
integrations: IntegrationsSchema.default({})
|
|
188
|
+
});
|
|
189
|
+
export async function readYamlFile(filePath, schema) {
|
|
190
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
191
|
+
const parsed = YAML.parse(raw);
|
|
192
|
+
return schema.parse(parsed);
|
|
193
|
+
}
|
|
194
|
+
export async function loadSiteConfig(rootDir) {
|
|
195
|
+
return (await readYamlFile(path.join(rootDir, 'site.config.yaml'), SiteSchema));
|
|
196
|
+
}
|
|
197
|
+
export async function loadThemeConfig(rootDir) {
|
|
198
|
+
return (await readYamlFile(path.join(rootDir, 'theme.config.yaml'), ThemeSchema));
|
|
199
|
+
}
|
|
200
|
+
export async function loadNavigationConfig(rootDir) {
|
|
201
|
+
return (await readYamlFile(path.join(rootDir, 'navigation.yaml'), NavigationSchema));
|
|
202
|
+
}
|
|
203
|
+
export async function parseFrontmatter(filePath) {
|
|
204
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
205
|
+
const parsed = matter(raw);
|
|
206
|
+
return {
|
|
207
|
+
data: FrontmatterSchema.parse(parsed.data),
|
|
208
|
+
content: parsed.content.trim()
|
|
209
|
+
};
|
|
210
|
+
}
|
package/dist/content.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { parseFrontmatter } from './config.js';
|
|
4
|
+
import { slugify, toPosix, walkFiles } from './utils.js';
|
|
5
|
+
const SECTION_PREFIX = {
|
|
6
|
+
pages: '',
|
|
7
|
+
services: 'services',
|
|
8
|
+
locations: 'locations'
|
|
9
|
+
};
|
|
10
|
+
function normalizeSegments(relativePath) {
|
|
11
|
+
const withoutExt = relativePath.replace(/\.(md|html)$/i, '');
|
|
12
|
+
const segments = withoutExt.split('/').filter(Boolean);
|
|
13
|
+
if (segments.at(-1) === 'index') {
|
|
14
|
+
segments.pop();
|
|
15
|
+
}
|
|
16
|
+
return segments;
|
|
17
|
+
}
|
|
18
|
+
function resolveRoutePath(section, relativePath, frontmatterSlug) {
|
|
19
|
+
const prefix = SECTION_PREFIX[section];
|
|
20
|
+
const segments = normalizeSegments(relativePath);
|
|
21
|
+
if (frontmatterSlug) {
|
|
22
|
+
if (segments.length === 0) {
|
|
23
|
+
segments.push(frontmatterSlug);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
segments[segments.length - 1] = frontmatterSlug;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const routeSegments = prefix ? [prefix, ...segments] : segments;
|
|
30
|
+
const joined = routeSegments.join('/');
|
|
31
|
+
if (!joined) {
|
|
32
|
+
return '/';
|
|
33
|
+
}
|
|
34
|
+
return `/${joined}/`;
|
|
35
|
+
}
|
|
36
|
+
function resolveOutputPath(routePath) {
|
|
37
|
+
if (routePath === '/') {
|
|
38
|
+
return 'dist/index.html';
|
|
39
|
+
}
|
|
40
|
+
return path.join('dist', routePath.replace(/^\//, ''), 'index.html');
|
|
41
|
+
}
|
|
42
|
+
function inferSection(filePath) {
|
|
43
|
+
const normalized = toPosix(filePath);
|
|
44
|
+
if (normalized.includes('/content/pages/'))
|
|
45
|
+
return 'pages';
|
|
46
|
+
if (normalized.includes('/content/services/'))
|
|
47
|
+
return 'services';
|
|
48
|
+
if (normalized.includes('/content/locations/'))
|
|
49
|
+
return 'locations';
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
export async function loadContentSources(rootDir) {
|
|
53
|
+
const contentRoot = path.join(rootDir, 'content');
|
|
54
|
+
const files = await walkFiles(contentRoot);
|
|
55
|
+
const sources = [];
|
|
56
|
+
for (const sourcePath of files) {
|
|
57
|
+
const section = inferSection(sourcePath);
|
|
58
|
+
if (!section) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!/\.(md|html)$/i.test(sourcePath)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const relativePath = toPosix(path.relative(path.join(contentRoot, section), sourcePath));
|
|
65
|
+
if (relativePath.startsWith('..')) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
let frontmatter = {};
|
|
69
|
+
let body = '';
|
|
70
|
+
if (sourcePath.endsWith('.md')) {
|
|
71
|
+
const parsed = await parseFrontmatter(sourcePath);
|
|
72
|
+
frontmatter = parsed.data;
|
|
73
|
+
body = parsed.content;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const raw = await fs.readFile(sourcePath, 'utf8');
|
|
77
|
+
body = raw.trim();
|
|
78
|
+
}
|
|
79
|
+
const routePath = resolveRoutePath(section, relativePath, frontmatter.slug ?? undefined);
|
|
80
|
+
const outputPath = resolveOutputPath(routePath);
|
|
81
|
+
sources.push({
|
|
82
|
+
section,
|
|
83
|
+
sourcePath,
|
|
84
|
+
relativePath,
|
|
85
|
+
routePath,
|
|
86
|
+
outputPath,
|
|
87
|
+
kind: sourcePath.endsWith('.md') ? 'markdown' : 'html',
|
|
88
|
+
frontmatter,
|
|
89
|
+
body
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return sources;
|
|
93
|
+
}
|
|
94
|
+
export function buildPageTitle(source, siteName) {
|
|
95
|
+
const title = source.frontmatter.title ?? slugify(path.basename(source.sourcePath, path.extname(source.sourcePath))).replace(/-/g, ' ');
|
|
96
|
+
if (source.routePath === '/') {
|
|
97
|
+
return title || siteName;
|
|
98
|
+
}
|
|
99
|
+
return title;
|
|
100
|
+
}
|
|
101
|
+
export function isPublished(source) {
|
|
102
|
+
if (source.frontmatter.draft === true) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (source.frontmatter.published === false) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { ensureDir, walkFiles, writeFileEnsured } from './utils.js';
|
|
5
|
+
const INIT_MARKER_PATH = '.opnpress/initialized.json';
|
|
6
|
+
const TEMPLATE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'templates');
|
|
7
|
+
export async function isInitialized(rootDir) {
|
|
8
|
+
try {
|
|
9
|
+
await fs.access(path.join(rootDir, INIT_MARKER_PATH));
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function copyTemplateTree(rootDir) {
|
|
17
|
+
try {
|
|
18
|
+
await fs.access(TEMPLATE_ROOT);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new Error(`Template directory not found: ${TEMPLATE_ROOT}`);
|
|
22
|
+
}
|
|
23
|
+
const entries = await walkFiles(TEMPLATE_ROOT);
|
|
24
|
+
if (!entries.length) {
|
|
25
|
+
throw new Error(`Template directory is empty: ${TEMPLATE_ROOT}`);
|
|
26
|
+
}
|
|
27
|
+
for (const templatePath of entries) {
|
|
28
|
+
const relativePath = path.relative(TEMPLATE_ROOT, templatePath);
|
|
29
|
+
const destination = path.join(rootDir, relativePath);
|
|
30
|
+
const content = await fs.readFile(templatePath, 'utf8');
|
|
31
|
+
await writeFileEnsured(destination, content);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function initProject(rootDir) {
|
|
35
|
+
if (await isInitialized(rootDir)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
await ensureDir(rootDir);
|
|
39
|
+
await copyTemplateTree(rootDir);
|
|
40
|
+
await writeFileEnsured(path.join(rootDir, INIT_MARKER_PATH), JSON.stringify({
|
|
41
|
+
name: 'OpnPress',
|
|
42
|
+
initializedAt: new Date().toISOString()
|
|
43
|
+
}, null, 2) + '\n');
|
|
44
|
+
return true;
|
|
45
|
+
}
|