create-opendocs 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 ADDED
@@ -0,0 +1,78 @@
1
+ # create-opendocs
2
+
3
+ `create-opendocs` scaffolds a new OpenDocs-powered documentation site.
4
+
5
+ ## What It Creates
6
+
7
+ The generated project is a Next.js docs app with:
8
+
9
+ - File-based content in `content/`
10
+ - Sidebar navigation configured in `content/config.json`
11
+ - Markdown and MDX page support
12
+ - A footer CTA block driven by config
13
+ - Static page generation and sitemap support
14
+
15
+ ## Usage
16
+
17
+ Create a new project:
18
+
19
+ ```bash
20
+ npx create-opendocs my-docs
21
+ ```
22
+
23
+ Create a project with custom defaults:
24
+
25
+ ```bash
26
+ npx create-opendocs my-docs --site-name "My Docs" --support-email docs@example.com --site-url https://docs.example.com
27
+ ```
28
+
29
+ Then run:
30
+
31
+ ```bash
32
+ cd my-docs
33
+ npm run dev
34
+ ```
35
+
36
+ ## Options
37
+
38
+ - `--site-name <name>`: visible docs/site name
39
+ - `--description <text>`: metadata description
40
+ - `--support-email <email>`: footer support email
41
+ - `--site-url <url>`: default public site URL used by the starter
42
+ - `--no-install`: skip `npm install`
43
+ - `--force`: allow copying into a non-empty directory
44
+
45
+ ## Repository Layout
46
+
47
+ - `src/create-opendocs.js`: the CLI entrypoint
48
+ - `template/`: the Next.js starter app that gets copied into new projects
49
+
50
+ ## Local Development
51
+
52
+ Dry-run the published package contents:
53
+
54
+ ```bash
55
+ npm run pack:dry-run
56
+ ```
57
+
58
+ Install and validate the template app:
59
+
60
+ ```bash
61
+ npm run template:install
62
+ npm run template:lint
63
+ npm run template:build
64
+ ```
65
+
66
+ ## Publishing
67
+
68
+ When you're ready to publish:
69
+
70
+ ```bash
71
+ npm publish --access public
72
+ ```
73
+
74
+ Before publishing, it's worth running:
75
+
76
+ ```bash
77
+ npm run pack:dry-run
78
+ ```
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "create-opendocs",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a new OpenDocs documentation site.",
5
+ "license": "MIT",
6
+ "preferGlobal": true,
7
+ "bin": {
8
+ "create-opendocs": "./src/create-opendocs.js"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "template"
13
+ ],
14
+ "scripts": {
15
+ "pack:dry-run": "npm pack --dry-run",
16
+ "template:install": "npm --prefix template install",
17
+ "template:lint": "npm --prefix template run lint",
18
+ "template:build": "npm --prefix template run build"
19
+ },
20
+ "engines": {
21
+ "node": ">=18.17"
22
+ },
23
+ "keywords": [
24
+ "docs",
25
+ "nextjs",
26
+ "starter",
27
+ "scaffold"
28
+ ]
29
+ }
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { spawnSync } = require("child_process");
6
+
7
+ const TEMPLATE_DIR = path.join(__dirname, "..", "template");
8
+ const TEXT_FILE_EXTENSIONS = new Set([
9
+ ".css",
10
+ ".gitignore",
11
+ ".js",
12
+ ".json",
13
+ ".md",
14
+ ".mdx",
15
+ ".mjs",
16
+ ".ts",
17
+ ".tsx",
18
+ ".txt",
19
+ ]);
20
+
21
+ function printUsage() {
22
+ console.log(`Usage: create-opendocs <project-directory> [options]
23
+
24
+ Options:
25
+ --site-name <name> Override the visible docs/site name
26
+ --description <text> Override the site description
27
+ --support-email <email> Set the footer contact email
28
+ --site-url <url> Set NEXT_PUBLIC_SITE_URL in the starter content
29
+ --no-install Skip npm install in the generated project
30
+ --force Allow copying into a non-empty directory
31
+ `);
32
+ }
33
+
34
+ function parseArgs(argv) {
35
+ const options = {
36
+ install: true,
37
+ force: false,
38
+ };
39
+ let targetDir = null;
40
+
41
+ for (let i = 0; i < argv.length; i += 1) {
42
+ const arg = argv[i];
43
+
44
+ if (!arg.startsWith("--")) {
45
+ targetDir = targetDir || arg;
46
+ continue;
47
+ }
48
+
49
+ if (arg === "--no-install") {
50
+ options.install = false;
51
+ continue;
52
+ }
53
+
54
+ if (arg === "--force") {
55
+ options.force = true;
56
+ continue;
57
+ }
58
+
59
+ const nextValue = argv[i + 1];
60
+ if (!nextValue || nextValue.startsWith("--")) {
61
+ throw new Error(`Missing value for ${arg}`);
62
+ }
63
+
64
+ if (arg === "--site-name") {
65
+ options.siteName = nextValue;
66
+ } else if (arg === "--description") {
67
+ options.description = nextValue;
68
+ } else if (arg === "--support-email") {
69
+ options.supportEmail = nextValue;
70
+ } else if (arg === "--site-url") {
71
+ options.siteUrl = nextValue;
72
+ } else {
73
+ throw new Error(`Unknown option: ${arg}`);
74
+ }
75
+
76
+ i += 1;
77
+ }
78
+
79
+ return { options, targetDir };
80
+ }
81
+
82
+ function toTitleCase(value) {
83
+ return value
84
+ .replace(/[-_]+/g, " ")
85
+ .trim()
86
+ .split(/\s+/)
87
+ .filter(Boolean)
88
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
89
+ .join(" ");
90
+ }
91
+
92
+ function toPackageName(value) {
93
+ return value
94
+ .toLowerCase()
95
+ .replace(/[^a-z0-9-_]/g, "-")
96
+ .replace(/-{2,}/g, "-")
97
+ .replace(/^-+|-+$/g, "") || "my-docs";
98
+ }
99
+
100
+ function ensureEmptyTarget(targetPath, force) {
101
+ if (!fs.existsSync(targetPath)) {
102
+ fs.mkdirSync(targetPath, { recursive: true });
103
+ return;
104
+ }
105
+
106
+ const files = fs.readdirSync(targetPath);
107
+ if (files.length === 0 || force) {
108
+ return;
109
+ }
110
+
111
+ throw new Error(`Target directory is not empty: ${targetPath}`);
112
+ }
113
+
114
+ function copyDirectory(sourceDir, destinationDir) {
115
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
116
+
117
+ for (const entry of entries) {
118
+ if (entry.name === "node_modules" || entry.name === ".next") {
119
+ continue;
120
+ }
121
+
122
+ const sourcePath = path.join(sourceDir, entry.name);
123
+ const destinationPath = path.join(destinationDir, entry.name);
124
+
125
+ if (entry.isDirectory()) {
126
+ fs.mkdirSync(destinationPath, { recursive: true });
127
+ copyDirectory(sourcePath, destinationPath);
128
+ continue;
129
+ }
130
+
131
+ fs.copyFileSync(sourcePath, destinationPath);
132
+ }
133
+ }
134
+
135
+ function isTextFile(filePath) {
136
+ const extension = path.extname(filePath);
137
+ return TEXT_FILE_EXTENSIONS.has(extension) || path.basename(filePath) === ".gitignore";
138
+ }
139
+
140
+ function replaceTokensInDirectory(directoryPath, replacements) {
141
+ const entries = fs.readdirSync(directoryPath, { withFileTypes: true });
142
+
143
+ for (const entry of entries) {
144
+ const fullPath = path.join(directoryPath, entry.name);
145
+
146
+ if (entry.isDirectory()) {
147
+ replaceTokensInDirectory(fullPath, replacements);
148
+ continue;
149
+ }
150
+
151
+ if (!isTextFile(fullPath)) {
152
+ continue;
153
+ }
154
+
155
+ let contents = fs.readFileSync(fullPath, "utf8");
156
+ for (const [token, value] of Object.entries(replacements)) {
157
+ contents = contents.split(token).join(value);
158
+ }
159
+ fs.writeFileSync(fullPath, contents);
160
+ }
161
+ }
162
+
163
+ function runInstall(targetPath) {
164
+ const result = spawnSync("npm", ["install"], {
165
+ cwd: targetPath,
166
+ stdio: "inherit",
167
+ shell: true,
168
+ });
169
+
170
+ if (result.status !== 0) {
171
+ throw new Error("npm install failed");
172
+ }
173
+ }
174
+
175
+ function main() {
176
+ try {
177
+ const { options, targetDir } = parseArgs(process.argv.slice(2));
178
+
179
+ if (!targetDir) {
180
+ printUsage();
181
+ process.exit(1);
182
+ }
183
+
184
+ const targetPath = path.resolve(process.cwd(), targetDir);
185
+ const projectDirectoryName = path.basename(targetPath);
186
+ const projectName = toPackageName(projectDirectoryName);
187
+ const siteName = options.siteName || toTitleCase(projectDirectoryName);
188
+ const description = options.description || `${siteName} documentation`;
189
+ const supportEmail = options.supportEmail || "support@example.com";
190
+ const siteUrl = options.siteUrl || "https://docs.example.com";
191
+
192
+ ensureEmptyTarget(targetPath, options.force);
193
+ copyDirectory(TEMPLATE_DIR, targetPath);
194
+ replaceTokensInDirectory(targetPath, {
195
+ "__PROJECT_NAME__": projectName,
196
+ "__SITE_NAME__": siteName,
197
+ "__SITE_DESCRIPTION__": description,
198
+ "__SUPPORT_EMAIL__": supportEmail,
199
+ "__SITE_URL__": siteUrl,
200
+ });
201
+
202
+ if (options.install) {
203
+ console.log(`Installing dependencies in ${targetPath}...`);
204
+ runInstall(targetPath);
205
+ }
206
+
207
+ console.log("");
208
+ console.log(`OpenDocs project created at ${targetPath}`);
209
+ console.log("");
210
+ console.log("Next steps:");
211
+ console.log(` cd ${targetDir}`);
212
+ if (!options.install) {
213
+ console.log(" npm install");
214
+ }
215
+ console.log(" npm run dev");
216
+ } catch (error) {
217
+ console.error(error.message);
218
+ process.exit(1);
219
+ }
220
+ }
221
+
222
+ main();
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": ["next/core-web-vitals", "next/typescript"]
3
+ }
@@ -0,0 +1,48 @@
1
+ FROM node:20-alpine AS base
2
+
3
+ # Install dependencies only when needed
4
+ FROM base AS deps
5
+ RUN apk add --no-cache libc6-compat
6
+ WORKDIR /app
7
+
8
+ # Install dependencies based on the preferred package manager
9
+ COPY package.json package-lock.json* ./
10
+ RUN npm ci
11
+
12
+ # Rebuild the source code only when needed
13
+ FROM base AS builder
14
+ WORKDIR /app
15
+ COPY --from=deps /app/node_modules ./node_modules
16
+ COPY . .
17
+
18
+ # Next.js telemetry is disabled
19
+ ENV NEXT_TELEMETRY_DISABLED 1
20
+
21
+ RUN npm run build
22
+
23
+ # Production image, copy all the files and run next
24
+ FROM base AS runner
25
+ WORKDIR /app
26
+
27
+ ENV NODE_ENV production
28
+ ENV NEXT_TELEMETRY_DISABLED 1
29
+
30
+ RUN addgroup --system --gid 1001 nodejs
31
+ RUN adduser --system --uid 1001 nextjs
32
+
33
+ # Set the correct permission for prerender cache
34
+ RUN mkdir .next
35
+ RUN chown nextjs:nodejs .next
36
+
37
+ # Leverage output traces to reduce image size
38
+ # https://nextjs.org/docs/advanced-features/output-file-tracing
39
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
40
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
41
+
42
+ USER nextjs
43
+
44
+ EXPOSE 3000
45
+ ENV PORT 3000
46
+ ENV HOSTNAME "0.0.0.0"
47
+
48
+ CMD ["node", "server.js"]
@@ -0,0 +1,86 @@
1
+ # __SITE_NAME__
2
+
3
+ __SITE_NAME__ is a lightweight Next.js documentation site powered by OpenDocs.
4
+
5
+ ## Setup
6
+
7
+ 1. Install dependencies:
8
+
9
+ ```bash
10
+ npm install
11
+ ```
12
+
13
+ 2. Start the development server:
14
+
15
+ ```bash
16
+ npm run dev
17
+ ```
18
+
19
+ 3. Open `http://localhost:3000`.
20
+
21
+ 4. Optional: set your production site URL for sitemap generation:
22
+
23
+ ```bash
24
+ $env:NEXT_PUBLIC_SITE_URL="__SITE_URL__"
25
+ ```
26
+
27
+ ## What The Project Does
28
+
29
+ - Loads docs from the `content/` folder
30
+ - Builds the sidebar from `content/config.json`
31
+ - Renders Markdown and MDX pages automatically from file paths
32
+ - Shows an optional footer CTA from config
33
+ - Generates a sitemap from your docs pages
34
+
35
+ ## Navigation
36
+
37
+ Navigation is configured in `content/config.json`.
38
+
39
+ Example:
40
+
41
+ ```json
42
+ {
43
+ "homepage": "index.md",
44
+ "sidebar": [
45
+ {
46
+ "title": "Getting Started",
47
+ "items": [
48
+ "index.md",
49
+ "guides/getting-started.md"
50
+ ]
51
+ }
52
+ ]
53
+ }
54
+ ```
55
+
56
+ How it works:
57
+
58
+ - `homepage` controls which file renders at `/`
59
+ - each `sidebar` group becomes a visible section in the left navigation
60
+ - each `items` entry should point to a file inside `content/`
61
+ - sidebar order also controls the "Up next" order at the bottom of pages
62
+
63
+ ## Footer Config
64
+
65
+ The footer CTA lives under `contactSupport` in `content/config.json`.
66
+
67
+ Example:
68
+
69
+ ```json
70
+ {
71
+ "contactSupport": {
72
+ "title": "Need help?",
73
+ "description": "Contact the docs team if anything is unclear.",
74
+ "buttonText": "Email us",
75
+ "buttonLink": "mailto:docs@example.com"
76
+ }
77
+ }
78
+ ```
79
+
80
+ ## Useful Files
81
+
82
+ - `content/config.json`: homepage, sidebar, and footer configuration
83
+ - `content/index.md`: homepage content
84
+ - `src/lib/docs.ts`: file loading and sidebar generation
85
+ - `src/app/[[...slug]]/page.tsx`: page rendering and metadata
86
+ - `src/components/Sidebar.tsx`: sidebar UI
@@ -0,0 +1,18 @@
1
+ {
2
+ "homepage": "index.md",
3
+ "sidebar": [
4
+ {
5
+ "title": "Getting Started",
6
+ "items": [
7
+ "index.md",
8
+ "guides/getting-started.md"
9
+ ]
10
+ }
11
+ ],
12
+ "contactSupport": {
13
+ "title": "Was this helpful?",
14
+ "description": "Reach out to support if you need assistance or are stuck.",
15
+ "buttonText": "Contact Support",
16
+ "buttonLink": "mailto:__SUPPORT_EMAIL__"
17
+ }
18
+ }
@@ -0,0 +1,21 @@
1
+ # Getting Started
2
+
3
+ ## Run Locally
4
+
5
+ Install dependencies:
6
+
7
+ ```bash
8
+ npm install
9
+ ```
10
+
11
+ Start the dev server:
12
+
13
+ ```bash
14
+ npm run dev
15
+ ```
16
+
17
+ Open `http://localhost:3000` to view your docs site.
18
+
19
+ ## Add Pages
20
+
21
+ Create Markdown or MDX files in `content/`, then list them in `content/config.json` if you want them to appear in the sidebar.
@@ -0,0 +1,11 @@
1
+ # Welcome to __SITE_NAME__
2
+
3
+ __SITE_NAME__ is your new documentation site powered by OpenDocs.
4
+
5
+ ## Start Here
6
+
7
+ - Edit this homepage in `content/index.md`
8
+ - Update the sidebar in `content/config.json`
9
+ - Add new Markdown or MDX pages under `content/`
10
+
11
+ When you're ready, publish the site with your own branding, content, and links.
@@ -0,0 +1,5 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ output: "standalone",
4
+ };
5
+ export default nextConfig;