create-daniworks-app 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HJ-company
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # create-daniworks-app
2
+
3
+ Scaffold a new [Daniworks Builder](https://github.com/HJ-company/daniworks-builder-v2)
4
+ project — a Strapi v5 CMS + a Next.js front end that renders CMS block content.
5
+
6
+ ```bash
7
+ npx create-daniworks-app my-site
8
+ cd my-site
9
+ pnpm dev
10
+ ```
11
+
12
+ The scaffold wires the front end (`apps/web`) — `@dani-builder/blocks` provider,
13
+ Tailwind `@theme` + `@source`, and a `home` page renderer — and generates all
14
+ secrets. It also creates the Strapi CMS (`apps/cms`), installs the Builder plugin
15
+ and its custom-field peer plugins, enables Builder, and configures sqlite's
16
+ default database filename.
17
+
18
+ ## License
19
+
20
+ MIT © HJ-company
package/bin/cms.js ADDED
@@ -0,0 +1,101 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+
5
+ export const CMS_PACKAGES = [
6
+ ["@dani-builder/strapi-plugin-builder", "latest"],
7
+ ["@strapi/plugin-color-picker", "^5.0.0"],
8
+ ["@d3levvv/strapi-react-icons-plugin", "^1.0.0"],
9
+ ];
10
+
11
+ export function runCommand(command, args, options = {}) {
12
+ const result = spawnSync(command, args, { stdio: "inherit", ...options });
13
+
14
+ if (result.error) {
15
+ throw result.error;
16
+ }
17
+ if (result.status !== 0) {
18
+ throw new Error(
19
+ `Command failed (${result.status}): ${command} ${args.join(" ")}`,
20
+ );
21
+ }
22
+
23
+ return result;
24
+ }
25
+
26
+ export function enableBuilder(cmsDir) {
27
+ const pluginsFile = join(cmsDir, "config", "plugins.ts");
28
+ const config = readFileSync(pluginsFile, "utf-8");
29
+ const objectStart = /=> \(\{\r?\n/;
30
+
31
+ if (!objectStart.test(config)) {
32
+ throw new Error(`Cannot enable Builder in ${pluginsFile}`);
33
+ }
34
+
35
+ writeFileSync(
36
+ pluginsFile,
37
+ config.replace(
38
+ objectStart,
39
+ (match) => `${match} builder: { enabled: true },\n`,
40
+ ),
41
+ );
42
+ }
43
+
44
+ export function addCmsDependencies(cmsDir) {
45
+ const packageFile = join(cmsDir, "package.json");
46
+ const packageJson = JSON.parse(readFileSync(packageFile, "utf-8"));
47
+
48
+ packageJson.dependencies ??= {};
49
+ for (const [name, version] of CMS_PACKAGES) {
50
+ packageJson.dependencies[name] = version;
51
+ }
52
+
53
+ writeFileSync(packageFile, `${JSON.stringify(packageJson, null, 2)}\n`);
54
+ }
55
+
56
+ export function fixSqliteDatabaseFilename(cmsDir) {
57
+ const envFile = join(cmsDir, ".env");
58
+
59
+ if (!existsSync(envFile)) return;
60
+
61
+ const env = readFileSync(envFile, "utf-8");
62
+ const fixed = /^DATABASE_FILENAME=[ \t]*$/m.test(env)
63
+ ? env.replace(
64
+ /^DATABASE_FILENAME=[ \t]*$/m,
65
+ "DATABASE_FILENAME=.tmp/data.db",
66
+ )
67
+ : env;
68
+
69
+ if (fixed !== env) writeFileSync(envFile, fixed);
70
+ }
71
+
72
+ export function createCms(projectDir, execute = runCommand) {
73
+ const cmsDir = join(projectDir, "apps", "cms");
74
+
75
+ execute(
76
+ "pnpm",
77
+ [
78
+ "dlx",
79
+ "create-strapi-app@^5",
80
+ "apps/cms",
81
+ "--ts",
82
+ "--use-pnpm",
83
+ "--skip-cloud",
84
+ "--no-example",
85
+ "--no-git-init",
86
+ "--no-run",
87
+ "--no-install",
88
+ "--non-interactive",
89
+ "--dbclient",
90
+ "sqlite",
91
+ "--dbfile",
92
+ ".tmp/data.db",
93
+ ],
94
+ { cwd: projectDir },
95
+ );
96
+
97
+ addCmsDependencies(cmsDir);
98
+ enableBuilder(cmsDir);
99
+ fixSqliteDatabaseFilename(cmsDir);
100
+ execute("pnpm", ["install"], { cwd: projectDir });
101
+ }
package/bin/index.js ADDED
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ import {
5
+ cpSync,
6
+ existsSync,
7
+ mkdirSync,
8
+ rmSync,
9
+ readdirSync,
10
+ readFileSync,
11
+ renameSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { basename, dirname, join } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { randomBytes } from "node:crypto";
17
+
18
+ import { createCms, runCommand } from "./cms.js";
19
+
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ const TEMPLATE_DIR = join(__dirname, "..", "template");
22
+
23
+ const c = {
24
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
25
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
26
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
27
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
28
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
29
+ };
30
+
31
+ function secret(bytes = 16) {
32
+ return randomBytes(bytes).toString("base64");
33
+ }
34
+
35
+ function die(msg) {
36
+ console.error(c.red(`\n✗ ${msg}\n`));
37
+ process.exit(1);
38
+ }
39
+
40
+ // ── Args ──
41
+ const args = process.argv.slice(2);
42
+ const input = args.find((a) => !a.startsWith("-"));
43
+ if (!input) {
44
+ die("Usage: create-daniworks-app <project-name>");
45
+ }
46
+ const target = join(process.cwd(), input);
47
+ // The workspace package name must be a valid npm name — derive it from the
48
+ // target directory, not the raw arg (which may be a path like "./my-site").
49
+ const pkgName = basename(target)
50
+ .toLowerCase()
51
+ .replace(/[^a-z0-9._-]+/g, "-")
52
+ .replace(/^[._-]+|[._-]+$/g, "");
53
+ if (!pkgName) {
54
+ die(`Cannot derive a valid package name from "${input}".`);
55
+ }
56
+ if (existsSync(target) && readdirSync(target).length > 0) {
57
+ die(`Directory "${input}" already exists and is not empty.`);
58
+ }
59
+
60
+ try {
61
+ const result = runCommand("pnpm", ["--version"], {
62
+ stdio: "pipe",
63
+ encoding: "utf-8",
64
+ });
65
+ const [major, minor] = result.stdout.trim().split(".").map(Number);
66
+ if (
67
+ !Number.isInteger(major) ||
68
+ !Number.isInteger(minor) ||
69
+ major < 9 ||
70
+ (major === 9 && minor < 4)
71
+ ) {
72
+ throw new Error("create-daniworks-app requires pnpm >=9.4 on PATH.");
73
+ }
74
+ } catch (error) {
75
+ die(error instanceof Error ? error.message : String(error));
76
+ }
77
+
78
+ console.log(
79
+ `\n${c.bold("create-daniworks-app")} ${c.dim("· scaffolding a Strapi + Next.js block site")}\n`,
80
+ );
81
+ console.log(` ${c.cyan("→")} ${target}`);
82
+ console.log(` ${c.dim("package name:")} ${pkgName}`);
83
+
84
+ const targetExisted = existsSync(target);
85
+
86
+ try {
87
+ // ── Copy template ──
88
+ mkdirSync(target, { recursive: true });
89
+ cpSync(TEMPLATE_DIR, target, { recursive: true });
90
+
91
+ // Rename dotfile stand-ins that npm would otherwise strip on publish.
92
+ for (const [from, to] of [
93
+ ["_gitignore", ".gitignore"],
94
+ ["_env.example", ".env.example"],
95
+ ]) {
96
+ walk(target, (file) => {
97
+ if (basename(file) === from) renameSync(file, join(dirname(file), to));
98
+ });
99
+ }
100
+
101
+ // ── Interpolate {{PROJECT_NAME}} ──
102
+ walk(target, (file) => {
103
+ if (/\.(json|ts|tsx|js|md|css|yaml)$/.test(file)) {
104
+ const content = readFileSync(file, "utf-8");
105
+ if (content.includes("{{PROJECT_NAME}}")) {
106
+ writeFileSync(file, content.split("{{PROJECT_NAME}}").join(pkgName));
107
+ }
108
+ }
109
+ });
110
+
111
+ // ── Generate real secrets into a gitignored .env.local ──
112
+ // .env.example stays committed with blank values; the runtime secrets live in
113
+ // .env.local (already covered by the template's .gitignore).
114
+ const webEnvExample = join(target, "apps", "web", ".env.example");
115
+ if (existsSync(webEnvExample)) {
116
+ const local = readFileSync(webEnvExample, "utf-8")
117
+ .replace(/^REVALIDATE_SECRET=.*$/m, `REVALIDATE_SECRET=${secret()}`)
118
+ .replace(/^PREVIEW_SECRET=.*$/m, `PREVIEW_SECRET=${secret()}`);
119
+ writeFileSync(join(target, "apps", "web", ".env.local"), local);
120
+ }
121
+
122
+ // ── Create and configure the Strapi CMS ──
123
+ console.log(` ${c.cyan("→")} Creating Strapi CMS`);
124
+ createCms(target);
125
+ } catch (error) {
126
+ rmSync(target, { recursive: true, force: true });
127
+ if (targetExisted) mkdirSync(target, { recursive: true });
128
+ die(error instanceof Error ? error.message : String(error));
129
+ }
130
+
131
+ // ── Done ──
132
+ console.log(c.green("\n✓ Project created.\n"));
133
+ console.log(c.bold("Next steps:"));
134
+ console.log(` ${c.cyan("cd")} ${input}`);
135
+ console.log(
136
+ ` ${c.cyan("pnpm --filter cms strapi admin:create-user")} ${c.dim("# create an admin + Full-access API token")}`,
137
+ );
138
+ console.log(` ${c.dim("# set STRAPI_TOKEN in apps/web/.env.local, then:")}`);
139
+ console.log(` ${c.cyan("pnpm dev")}\n`);
140
+
141
+ function walk(dir, fn) {
142
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
143
+ const full = join(dir, entry.name);
144
+ if (entry.isDirectory()) walk(full, fn);
145
+ else fn(full);
146
+ }
147
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "create-daniworks-app",
3
+ "version": "0.2.0",
4
+ "description": "Scaffold a new Daniworks Builder project (Strapi v5 CMS + Next.js block-rendering front end)",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "create-daniworks-app": "bin/index.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=22.13",
12
+ "pnpm": ">=9.4"
13
+ },
14
+ "files": [
15
+ "bin",
16
+ "template",
17
+ "LICENSE",
18
+ "README.md"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/HJ-company/daniworks-builder-v2.git",
26
+ "directory": "packages/create-daniworks-app"
27
+ },
28
+ "keywords": [
29
+ "strapi",
30
+ "strapi-v5",
31
+ "nextjs",
32
+ "daniworks-builder",
33
+ "scaffold",
34
+ "create-app",
35
+ "cli"
36
+ ],
37
+ "homepage": "https://github.com/HJ-company/daniworks-builder-v2/tree/main/packages/create-daniworks-app#readme",
38
+ "author": "HJ-company",
39
+ "scripts": {
40
+ "test": "node --test",
41
+ "sync:skills": "rm -rf template/.claude/skills/builder-extender && mkdir -p template/.claude/skills && cp -R ../../.claude/skills/builder-extender template/.claude/skills/"
42
+ }
43
+ }
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: builder-extender
3
+ description: "Extend @dani-builder/strapi-plugin-builder from a consumer Strapi
4
+ project without touching the plugin. Actions: add a custom block to
5
+ Page.blocks, add fields to a plugin component (hero, features, cta, ...),
6
+ register populate for a custom block, install the plugin with its peer
7
+ plugins, verify an extension boots. Triggers: 'add a block', 'custom
8
+ component', 'new hero variant', 'add field to hero', 'extend the builder',
9
+ 'dynamic zone', 'plugin::builder', 'strapi-server.ts', 'registerBlock'."
10
+ ---
11
+
12
+ # Builder Extender
13
+
14
+ Adds project-specific blocks and fields to a Strapi project that consumes
15
+ `@dani-builder/strapi-plugin-builder` from npm. Every change lands in the
16
+ consumer's `src/`; the plugin package is never edited.
17
+
18
+ Long-form rationale and reproduced failure cases:
19
+ `docs/guides/extending-builder.md` (in the builder monorepo). This skill is the
20
+ executable version.
21
+
22
+ ## 1. When to apply
23
+
24
+ - The user wants a block that the plugin does not ship (a project-specific
25
+ hero, a product grid, an embed) selectable in `Page.blocks`.
26
+ - The user wants extra fields on a plugin block or shared component
27
+ (`blocks.hero`, `shared.link`, …).
28
+ - The user is installing the plugin into a fresh Strapi app.
29
+ - `cms-content-populator` was asked for a block that does not exist.
30
+
31
+ Do **not** apply when the working directory is the builder monorepo itself and
32
+ the request is to change the plugin for everyone — edit
33
+ `packages/strapi-plugin-builder/server/src/components/**` there instead and
34
+ run `pnpm docs:gen`.
35
+
36
+ ## 2. Locate the consumer project
37
+
38
+ The consumer is the directory whose `package.json` lists
39
+ `@dani-builder/strapi-plugin-builder` in `dependencies` and has
40
+ `config/plugins.ts`. In a scaffolded customer repo that is the CMS app; in the
41
+ builder monorepo it is `apps/demo-cms`. All paths below are relative to it.
42
+
43
+ ## 3. Procedure
44
+
45
+ ### 3.1 Install (only if the plugin is not yet a dependency)
46
+
47
+ ```bash
48
+ pnpm add @dani-builder/strapi-plugin-builder @strapi/plugin-color-picker @d3levvv/strapi-react-icons-plugin
49
+ ```
50
+
51
+ All three are required: the plugin's components use the two peers' custom
52
+ fields, and Strapi only loads plugins listed in the consumer's own
53
+ `package.json`. Then ensure `config/plugins.ts` contains
54
+ `builder: { enabled: true }`.
55
+
56
+ ### 3.2 Add a custom block
57
+
58
+ 1. Create `src/components/<category>/<name>.json`. Use a project category
59
+ (`custom`, or the project name) — never `blocks`, `elements`, or `shared`.
60
+ Reuse plugin sub-components (`elements.section`, `shared.link`,
61
+ `shared.image`, `shared.slider-item`, …) for anything the front end already
62
+ knows how to render. Follow Strapi component JSON: `collectionName`,
63
+ `info.displayName`, `attributes`.
64
+ 2. Create or update `src/extensions/builder/strapi-server.ts` from
65
+ `references/strapi-server.template.ts`:
66
+ - push the uid (`<category>.<name>`) onto
67
+ `plugin.contentTypes.page.schema.attributes.blocks.components`;
68
+ - inside the wrapped `register`, call
69
+ `strapi.plugin('builder').service('populate').registerBlock(uid, populate)`
70
+ with a populate object that covers every nested component, media, and
71
+ relation of the new block. Scalars need nothing.
72
+ 3. Verify (§3.4).
73
+ 4. Remind the user that `BlockRenderer` from `@dani-builder/blocks` will not
74
+ render the new block: the front end needs a project renderer keyed on the
75
+ uid (see guide §4).
76
+
77
+ ### 3.3 Add fields to a plugin component
78
+
79
+ 1. In `src/extensions/builder/strapi-server.ts`, inside the wrapped
80
+ `register` **after** `await baseRegister(ctx)`:
81
+ ```ts
82
+ const target = ctx.strapi.get('components').get('blocks.hero');
83
+ Object.assign(target.attributes, EXTRA);
84
+ Object.assign(target.__schema__.attributes, EXTRA);
85
+ ```
86
+ Update both objects. Additive changes only — never delete or rename a
87
+ plugin attribute (the DB sync drops the column and its data).
88
+ 2. Verify (§3.4). The new columns appear on the component table; the Content
89
+ Manager layout updates itself; the smart API returns scalars without a
90
+ populate change. A nested component or media added this way also needs
91
+ the block's populate extended via `registerBlock` with the full populate
92
+ for that block (it replaces the plugin's entry for that uid).
93
+ 3. Remind the user the front-end block component must be wrapped or replaced
94
+ to show the new fields.
95
+
96
+ ### 3.4 Verify
97
+
98
+ ```bash
99
+ rm -rf dist # TypeScript consumers: stale compiled JSON is loaded as current
100
+ node <path-to>/references/verify-boot.cjs
101
+ ```
102
+
103
+ Copy `references/verify-boot.cjs` into the consumer (e.g. `scripts/`) so it
104
+ resolves the consumer's `@strapi/strapi` and `.env`. It boots Strapi headlessly
105
+ and prints the Page dynamic-zone list, the attributes and DB columns of any
106
+ component named in `VERIFY_COMPONENTS`, and the registered populate keys.
107
+ Confirm all three show the change, then start the server normally.
108
+
109
+ If the boot fails with `unable to open database file` on sqlite, set
110
+ `DATABASE_FILENAME=.tmp/data.db` in `.env` and create `.tmp/`.
111
+
112
+ ## 4. Hard rules
113
+
114
+ | Never | Because |
115
+ |---|---|
116
+ | Write a partial `src/extensions/builder/content-types/<ct>/schema.json` | Strapi shallow-merges it: `attributes` is replaced wholesale, the other fields vanish, and the DB sync drops their columns without an error. |
117
+ | Create `src/components/blocks/*.json`, `elements/*.json`, or `shared/*.json` with a plugin uid | Boot fails: `Component <uid> has already been registered.` |
118
+ | Edit a plugin component in the Content-Type Builder UI | Save returns 400 (`Error writing schema files … "path" argument must be of type string`) and the edit is lost. Plugin content types are read-only there by design. |
119
+ | Copy plugin content-type schemas into `src/api/` | Duplicate `collectionName`s prevent boot (see plugin README). |
120
+ | Register populate at the top level of `strapi-server.ts` | Plugin services do not exist until the plugin loads; do it inside the wrapped `register`. |
121
+
122
+ ## 5. Reference
123
+
124
+ - `references/strapi-server.template.ts` — complete extension file to copy.
125
+ - `references/verify-boot.cjs` — headless boot verifier.
126
+ - Guide with reproduced failure cases: `docs/guides/extending-builder.md`
127
+ (builder monorepo).
128
+ - Plugin populate hook: `packages/strapi-plugin-builder/server/src/services/populate.ts`
129
+ (`registerBlock`, `getPageBlocksPopulate`, `getGlobalPopulate`).
@@ -0,0 +1,71 @@
1
+ /**
2
+ * src/extensions/builder/strapi-server.ts
3
+ *
4
+ * Project-level extension of @dani-builder/strapi-plugin-builder. Strapi loads
5
+ * this file after the plugin package and before the plugin registers, passes
6
+ * the plugin object in, and uses whatever is returned.
7
+ *
8
+ * Copy into the consumer project and edit the three marked sections. Delete
9
+ * any section you do not need. Everything is additive: never remove or rename
10
+ * a plugin attribute here (the DB sync would drop the column and its data).
11
+ */
12
+ import type { Core } from '@strapi/strapi';
13
+
14
+ type AnyRecord = Record<string, any>;
15
+
16
+ interface BuilderPlugin extends AnyRecord {
17
+ register?: (ctx: { strapi: Core.Strapi }) => void | Promise<void>;
18
+ contentTypes: Record<string, { schema: AnyRecord }>;
19
+ }
20
+
21
+ // ── 1. Project blocks to make selectable in Page.blocks ──────────────────────
22
+ // Each entry is a component defined in src/components/<category>/<name>.json
23
+ // with its smart-API populate. Cover every nested component / media / relation;
24
+ // scalars need nothing. `true` populates a flat component or media field.
25
+ const PROJECT_BLOCKS: Record<string, AnyRecord> = {
26
+ 'custom.dynamic-hero': {
27
+ populate: {
28
+ section: { populate: { backgroundImage: true } },
29
+ slides: { populate: { link: true, image: { populate: { media: true } } } },
30
+ ctaLink: true,
31
+ },
32
+ },
33
+ };
34
+
35
+ // ── 2. Extra attributes on plugin components ─────────────────────────────────
36
+ // Keyed by component uid. Plain Strapi attribute definitions.
37
+ const COMPONENT_EXTENSIONS: Record<string, AnyRecord> = {
38
+ 'blocks.hero': {
39
+ badgeText: { type: 'string' },
40
+ videoUrl: { type: 'string' },
41
+ },
42
+ };
43
+
44
+ export default (plugin: BuilderPlugin): BuilderPlugin => {
45
+ const pageBlocks: string[] = plugin.contentTypes.page.schema.attributes.blocks.components;
46
+ for (const uid of Object.keys(PROJECT_BLOCKS)) {
47
+ if (!pageBlocks.includes(uid)) pageBlocks.push(uid);
48
+ }
49
+
50
+ const baseRegister = plugin.register;
51
+ plugin.register = async (ctx) => {
52
+ // Plugin components are registered here; extend them only afterwards.
53
+ await baseRegister?.(ctx);
54
+
55
+ const components: AnyRecord = ctx.strapi.get('components');
56
+ for (const [uid, attributes] of Object.entries(COMPONENT_EXTENSIONS)) {
57
+ const component = components.get(uid);
58
+ if (!component) throw new Error(`[builder extension] unknown plugin component ${uid}`);
59
+ Object.assign(component.attributes, attributes);
60
+ Object.assign(component.__schema__.attributes, attributes);
61
+ }
62
+
63
+ // ── 3. Populate registration (must run after the plugin loaded) ──────────
64
+ const populate = ctx.strapi.plugin('builder').service('populate');
65
+ for (const [uid, config] of Object.entries(PROJECT_BLOCKS)) {
66
+ populate.registerBlock(uid, config);
67
+ }
68
+ };
69
+
70
+ return plugin;
71
+ };
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Headless boot verifier for builder extensions.
4
+ *
5
+ * Copy into the consumer Strapi project (e.g. scripts/verify-boot.cjs) and run
6
+ * from the project root so it resolves the project's @strapi/strapi and .env:
7
+ *
8
+ * rm -rf dist && node scripts/verify-boot.cjs
9
+ *
10
+ * Boots Strapi (register + bootstrap + DB sync, no HTTP listener) and prints:
11
+ * - the Page.blocks dynamic-zone component list
12
+ * - attributes and DB columns for each component in VERIFY_COMPONENTS
13
+ * - the populate keys the smart API will use for Page.blocks
14
+ *
15
+ * Environment:
16
+ * VERIFY_COMPONENTS comma-separated uids to inspect (default: blocks.hero)
17
+ */
18
+ const { createStrapi, compileStrapi } = require('@strapi/strapi');
19
+
20
+ const componentsToInspect = (process.env.VERIFY_COMPONENTS || 'blocks.hero')
21
+ .split(',')
22
+ .map((s) => s.trim())
23
+ .filter(Boolean);
24
+
25
+ async function columnsOf(app, table) {
26
+ const client = app.db.connection.client.config.client;
27
+ if (client === 'sqlite' || client === 'better-sqlite3') {
28
+ const rows = await app.db.connection.raw(`pragma table_info('${table}')`);
29
+ return rows.map((r) => r.name);
30
+ }
31
+ const rows = await app.db.connection('information_schema.columns')
32
+ .select('column_name')
33
+ .where({ table_name: table });
34
+ return rows.map((r) => r.column_name);
35
+ }
36
+
37
+ (async () => {
38
+ const app = await createStrapi(await compileStrapi()).load();
39
+ try {
40
+ const page = app.contentTypes['plugin::builder.page'];
41
+ if (!page) throw new Error('plugin::builder.page is not registered — is the builder plugin enabled?');
42
+
43
+ const report = {
44
+ pageBlocks: page.attributes.blocks.components,
45
+ components: {},
46
+ populateKeys: Object.keys(app.plugin('builder').service('populate').getPageBlocksPopulate().on),
47
+ };
48
+
49
+ for (const uid of componentsToInspect) {
50
+ const component = app.components[uid];
51
+ if (!component) {
52
+ report.components[uid] = 'NOT REGISTERED';
53
+ continue;
54
+ }
55
+ report.components[uid] = {
56
+ attributes: Object.keys(component.attributes),
57
+ ctbAttributes: Object.keys(component.__schema__?.attributes || {}),
58
+ columns: await columnsOf(app, component.collectionName),
59
+ };
60
+ }
61
+
62
+ console.log(JSON.stringify(report, null, 2));
63
+ } finally {
64
+ await app.destroy();
65
+ }
66
+ process.exit(0);
67
+ })().catch((error) => {
68
+ console.error('BOOT FAILED:', error.message);
69
+ process.exit(1);
70
+ });
@@ -0,0 +1,46 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ A [Daniworks Builder](https://github.com/HJ-company/daniworks-builder-v2) site:
4
+ a Strapi v5 CMS + a Next.js front end that renders the CMS's block content.
5
+
6
+ This scaffold ships the front end (`apps/web`) and Strapi CMS (`apps/cms`) fully
7
+ wired.
8
+
9
+ The generator installed the workspace and created `apps/cms` with Builder and
10
+ its two custom-field peer plugins as direct dependencies. It also enabled
11
+ Builder in `apps/cms/config/plugins.ts` and configured sqlite's database
12
+ filename.
13
+
14
+ The plugin registers page/article/etc. and the write-only inquiry router when
15
+ Strapi starts. Do not copy these schemas into `apps/cms/src/api/`.
16
+
17
+ To add your own blocks or extra fields on the builder's components, use
18
+ `apps/cms/src/extensions/builder/strapi-server.ts` — the `builder-extender`
19
+ skill in `.claude/skills/` walks an agent through it, and the full guide is
20
+ [docs/guides/extending-builder.md](https://github.com/HJ-company/daniworks-builder-v2/blob/main/docs/guides/extending-builder.md).
21
+
22
+ Create an admin and a **Full-access API token** (Strapi Admin → Settings →
23
+ API Tokens), then set it as `STRAPI_TOKEN` in `apps/web/.env.local` (this file
24
+ was generated for you from `.env.example`, with `REVALIDATE_SECRET` /
25
+ `PREVIEW_SECRET` already filled in).
26
+
27
+ ## Run
28
+
29
+ ```bash
30
+ pnpm dev
31
+ ```
32
+
33
+ - CMS admin: http://localhost:1337/admin — create a `page` with slug `home`.
34
+ - Front end: http://localhost:3000
35
+
36
+ ## What's wired for you
37
+
38
+ - `apps/web/components/blocks-provider.tsx` — injects `next/image` / `next/link`
39
+ and a token-free `resolveAssetUrl` into `@dani-builder/blocks`.
40
+ - `apps/web/app/globals.css` — Tailwind v4 `@theme` tokens + `@source` for the
41
+ block package + `@dani-builder/blocks/styles.css` import in the layout.
42
+ - `apps/web/app/page.tsx` — fetches the `home` page and renders it with
43
+ `<BlockRenderer>`.
44
+
45
+ Runtime secrets were generated into `apps/web/.env.local` (gitignored). The
46
+ committed `.env.example` keeps blank placeholders.
@@ -0,0 +1,10 @@
1
+ node_modules
2
+ .next
3
+ dist
4
+ .turbo
5
+ .tmp
6
+ .env
7
+ .env.local
8
+ !.env.example
9
+ *.log
10
+ .DS_Store
@@ -0,0 +1,6 @@
1
+ STRAPI_URL=http://localhost:1337
2
+ STRAPI_MEDIA_HOST=http://localhost:1337
3
+ STRAPI_TOKEN=
4
+ SITE_URL=http://localhost:3000
5
+ REVALIDATE_SECRET=
6
+ PREVIEW_SECRET=
@@ -0,0 +1,14 @@
1
+ @import "tailwindcss";
2
+ @source "../node_modules/@dani-builder/blocks/dist";
3
+
4
+ @theme {
5
+ --color-primary: #6366F1;
6
+ --color-primary-light: #818CF8;
7
+ --color-cta: #047857;
8
+ --color-cta-hover: #065F46;
9
+ --color-bg: #F5F3FF;
10
+ --color-text: #1E1B4B;
11
+ --color-text-muted: #4B5563;
12
+ }
13
+
14
+ body { background-color: #fff; color: var(--color-text); }
@@ -0,0 +1,18 @@
1
+ import type { ReactNode } from "react";
2
+ import { BlocksProvider } from "@/components/blocks-provider";
3
+ import { mediaHost } from "@/lib/strapi";
4
+ import "./globals.css";
5
+ import "@dani-builder/blocks/styles.css";
6
+
7
+ export default function RootLayout({ children }: { children: ReactNode }) {
8
+ return (
9
+ <html lang="en">
10
+ <head>
11
+ <script dangerouslySetInnerHTML={{ __html: "document.documentElement.classList.add('js')" }} />
12
+ </head>
13
+ <body>
14
+ <BlocksProvider mediaHost={mediaHost}>{children}</BlocksProvider>
15
+ </body>
16
+ </html>
17
+ );
18
+ }
@@ -0,0 +1,11 @@
1
+ import { notFound } from "next/navigation";
2
+ import { BlockRenderer } from "@dani-builder/blocks";
3
+ import { fetchPageBySlug } from "@/lib/strapi";
4
+
5
+ export const revalidate = 60;
6
+
7
+ export default async function HomePage() {
8
+ const page = await fetchPageBySlug("home");
9
+ if (!page) notFound();
10
+ return <BlockRenderer blocks={page.blocks} />;
11
+ }
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import NextImage from "next/image";
4
+ import NextLink from "next/link";
5
+ import type { ReactNode } from "react";
6
+ import { getAssetUrl } from "@dani-builder/strapi-client/utils";
7
+ import type { StrapiMedia } from "@dani-builder/strapi-client/types";
8
+ import { BlocksProvider as Provider, type BlocksImageProps, type BlocksLinkProps } from "@dani-builder/blocks";
9
+
10
+ const Image = (p: BlocksImageProps) => <NextImage {...p} />;
11
+ const Link = ({ href, children, ...r }: BlocksLinkProps) => (
12
+ <NextLink href={href} {...r}>{children}</NextLink>
13
+ );
14
+
15
+ export function BlocksProvider({ mediaHost, children }: { mediaHost: string; children: ReactNode }) {
16
+ const resolveAssetUrl = (a: StrapiMedia | null | undefined, f?: "thumbnail" | "small" | "medium" | "large") =>
17
+ getAssetUrl(a, f, mediaHost);
18
+ return <Provider value={{ Image, Link, resolveAssetUrl }}>{children}</Provider>;
19
+ }
@@ -0,0 +1,17 @@
1
+ import { cache } from "react";
2
+ import { createStrapiClient } from "@dani-builder/strapi-client/client";
3
+ import { getPageBySlug } from "@dani-builder/strapi-client/api";
4
+
5
+ const strapiUrl = process.env.STRAPI_URL ?? "http://localhost:1337";
6
+ export const mediaHost = process.env.STRAPI_MEDIA_HOST ?? strapiUrl;
7
+
8
+ const strapi = createStrapiClient({
9
+ baseUrl: strapiUrl,
10
+ apiToken: process.env.STRAPI_TOKEN,
11
+ mediaHost,
12
+ });
13
+
14
+ // getPageBySlug returns null for a genuinely missing slug (200 + empty result),
15
+ // which the page turns into a 404. Real failures (network/auth/5xx) are left to
16
+ // throw so they surface in the error boundary instead of silently 404-ing.
17
+ export const fetchPageBySlug = cache((slug: string) => getPageBySlug(strapi, slug));
@@ -0,0 +1,19 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const withScheme = (v: string) => (/^https?:\/\//.test(v) ? v : `https://${v}`);
4
+ const mediaHost = withScheme(process.env.STRAPI_MEDIA_HOST ?? process.env.STRAPI_URL ?? "http://localhost:1337");
5
+
6
+ // Next 16 refuses to optimize images from local/loopback IPs (SSRF guard) even
7
+ // when remotePatterns matches — /_next/image 400s. Local dev serves Strapi media
8
+ // from localhost, so opt in ONLY when the media host is loopback.
9
+ const mediaHostname = new URL(mediaHost).hostname;
10
+ const allowLocalIP = /^(localhost|127\.|0\.0\.0\.0|::1|\[::1\])/.test(mediaHostname);
11
+
12
+ const nextConfig: NextConfig = {
13
+ images: {
14
+ remotePatterns: [new URL(`${mediaHost}/**`)],
15
+ ...(allowLocalIP ? { dangerouslyAllowLocalIP: true } : {}),
16
+ },
17
+ transpilePackages: ["@dani-builder/strapi-client", "@dani-builder/blocks"],
18
+ };
19
+ export default nextConfig;
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "web",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start"
9
+ },
10
+ "dependencies": {
11
+ "@dani-builder/blocks": "latest",
12
+ "@dani-builder/strapi-client": "latest",
13
+ "next": "^16",
14
+ "react": "^19",
15
+ "react-dom": "^19"
16
+ },
17
+ "devDependencies": {
18
+ "@tailwindcss/postcss": "^4",
19
+ "@types/node": "^26",
20
+ "@types/react": "^19",
21
+ "@types/react-dom": "^19",
22
+ "tailwindcss": "^4",
23
+ "typescript": "^6.0.3"
24
+ }
25
+ }
@@ -0,0 +1 @@
1
+ export default { plugins: { "@tailwindcss/postcss": {} } };
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022", "lib": ["dom", "dom.iterable", "esnext"],
4
+ "module": "esnext", "moduleResolution": "bundler", "jsx": "preserve",
5
+ "strict": true, "noEmit": true, "esModuleInterop": true, "skipLibCheck": true,
6
+ "incremental": true, "plugins": [{ "name": "next" }],
7
+ "paths": { "@/*": ["./*"] }
8
+ },
9
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
10
+ "exclude": ["node_modules"]
11
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "private": true,
4
+ "packageManager": "pnpm@11.21.0",
5
+ "engines": {
6
+ "node": ">=22.13"
7
+ },
8
+ "scripts": {
9
+ "dev": "turbo run dev",
10
+ "build": "turbo run build"
11
+ },
12
+ "devDependencies": {
13
+ "turbo": "^2"
14
+ }
15
+ }
@@ -0,0 +1,8 @@
1
+ packages:
2
+ - "apps/*"
3
+
4
+ allowBuilds:
5
+ "@swc/core": true
6
+ better-sqlite3: true
7
+ core-js-pure: true
8
+ esbuild: true
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "https://turborepo.dev/schema.json",
3
+ "tasks": {
4
+ "build": { "dependsOn": ["^build"], "outputs": [".next/**", "!.next/cache/**"] },
5
+ "dev": { "cache": false, "persistent": true }
6
+ }
7
+ }