create-lacspace-app 1.1.0 → 1.3.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 +10 -4
- package/dist/index.js +203 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,11 +36,17 @@ It asks for a project name and a template, scaffolds the project, installs depen
|
|
|
36
36
|
|
|
37
37
|
Every template is a real Next.js 15 App Router + Tailwind v4 project — dark, modern, responsive, with a gradient accent you can theme.
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## A complete, polished project — not a blank page
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
41
|
+
Every generated app arrives finished, with the boring-but-essential things done:
|
|
42
|
+
|
|
43
|
+
- 🎨 **Beautiful home page** — Tailwind v4, the Inter font, dark theme + a gradient accent
|
|
44
|
+
- 🔎 **SEO** — one [`@lacspace/seo`](https://www.npmjs.com/package/@lacspace/seo) `defineSite()` config drives your `<title>`, canonical, Open Graph, Twitter card and JSON-LD
|
|
45
|
+
- ✨ **Dynamic OG images** — every page auto-generates a social-share card at `/og` (no design tool)
|
|
46
|
+
- 🛡️ **Security headers** — HSTS, CSP, X-Frame-Options and more via [`@lacspace/headers`](https://www.npmjs.com/package/@lacspace/headers)
|
|
47
|
+
- 🗺️ **robots.txt + sitemap.xml** — generated from your config with [`@lacspace/robots`](https://www.npmjs.com/package/@lacspace/robots) & [`@lacspace/sitemap`](https://www.npmjs.com/package/@lacspace/sitemap)
|
|
48
|
+
- 🧩 **A styled 404, a PWA manifest, and `.env.example`** — the finishing touches most starters skip
|
|
49
|
+
- 🎁 **A `WELCOME.md`** that walks you through everything and shows the bonus packages you can drop in
|
|
44
50
|
|
|
45
51
|
Edit `lib/site.ts` and `app/page.tsx` and you're off.
|
|
46
52
|
|
package/dist/index.js
CHANGED
|
@@ -100,38 +100,59 @@ var globalsCss = (ctx) => `@import "tailwindcss";
|
|
|
100
100
|
:root {
|
|
101
101
|
--accent-from: ${ctx.template.accent[0]};
|
|
102
102
|
--accent-to: ${ctx.template.accent[1]};
|
|
103
|
+
--bg: #0a0a0f;
|
|
104
|
+
--fg: #e5e7eb;
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
* { border-color: rgb(255 255 255 / 0.08); }
|
|
105
108
|
html { scroll-behavior: smooth; }
|
|
106
|
-
body {
|
|
109
|
+
body {
|
|
110
|
+
background: var(--bg);
|
|
111
|
+
color: var(--fg);
|
|
112
|
+
-webkit-font-smoothing: antialiased;
|
|
113
|
+
text-rendering: optimizeLegibility;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
::selection { background: var(--accent-to); color: #0a0a0f; }
|
|
117
|
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
118
|
+
::-webkit-scrollbar-thumb { background: rgb(255 255 255 / 0.12); border-radius: 8px; }
|
|
119
|
+
:focus-visible { outline: 2px solid var(--accent-to); outline-offset: 2px; border-radius: 4px; }
|
|
107
120
|
|
|
108
121
|
.gradient-text {
|
|
109
122
|
background: linear-gradient(120deg, var(--accent-from), var(--accent-to));
|
|
110
123
|
-webkit-background-clip: text; background-clip: text; color: transparent;
|
|
111
124
|
}
|
|
112
125
|
.gradient-bg { background: linear-gradient(120deg, var(--accent-from), var(--accent-to)); }
|
|
126
|
+
|
|
127
|
+
/* soft entrance for content */
|
|
128
|
+
@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
|
|
129
|
+
main > section, main > * { animation: rise 0.6s cubic-bezier(0.22, 1, 0.36, 1) both; }
|
|
130
|
+
@media (prefers-reduced-motion: reduce) { *, ::before, ::after { animation: none !important; scroll-behavior: auto; } }
|
|
113
131
|
`;
|
|
114
132
|
var siteTs = (ctx) => `import { defineSite } from "@lacspace/seo";
|
|
115
133
|
|
|
116
134
|
/** Your site's SEO configuration \u2014 set once, used everywhere. */
|
|
117
135
|
export const site = defineSite({
|
|
118
136
|
name: ${JSON.stringify(ctx.template.siteName)},
|
|
119
|
-
url: "https://example.com",
|
|
137
|
+
url: process.env.NEXT_PUBLIC_SITE_URL ?? "https://example.com",
|
|
120
138
|
description: ${JSON.stringify(ctx.template.siteDescription)},
|
|
121
139
|
// twitter: "yourhandle",
|
|
122
|
-
|
|
140
|
+
ogImage: "/og", // \u2728 auto social-share images \u2014 see app/og/route.tsx
|
|
123
141
|
});
|
|
124
142
|
`;
|
|
125
143
|
var layout = (ctx) => `import type { Metadata } from "next";
|
|
144
|
+
import { Inter } from "next/font/google";
|
|
126
145
|
import { site } from "@/lib/site";
|
|
127
146
|
import "./globals.css";
|
|
128
147
|
|
|
148
|
+
const inter = Inter({ subsets: ["latin"], display: "swap" });
|
|
149
|
+
|
|
129
150
|
export const metadata: Metadata = site.meta({ title: ${JSON.stringify(ctx.template.siteName)} });
|
|
130
151
|
|
|
131
152
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
132
153
|
return (
|
|
133
|
-
<html lang="en">
|
|
134
|
-
<body>
|
|
154
|
+
<html lang="en" className={inter.className}>
|
|
155
|
+
<body className="antialiased">
|
|
135
156
|
{children}
|
|
136
157
|
<script
|
|
137
158
|
type="application/ld+json"
|
|
@@ -142,6 +163,113 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
142
163
|
);
|
|
143
164
|
}
|
|
144
165
|
`;
|
|
166
|
+
var ogRoute = (ctx) => `import { ImageResponse } from "next/og";
|
|
167
|
+
import { site } from "@/lib/site";
|
|
168
|
+
|
|
169
|
+
// \u2728 Dynamic Open Graph images \u2014 every page gets a gorgeous social card,
|
|
170
|
+
// generated on the fly at /og?title=Your+Page+Title. No design tool needed.
|
|
171
|
+
export const runtime = "edge";
|
|
172
|
+
|
|
173
|
+
export function GET(req: Request) {
|
|
174
|
+
const title = new URL(req.url).searchParams.get("title") ?? site.config.name;
|
|
175
|
+
return new ImageResponse(
|
|
176
|
+
(
|
|
177
|
+
<div
|
|
178
|
+
style={{
|
|
179
|
+
width: "100%", height: "100%", display: "flex", flexDirection: "column",
|
|
180
|
+
justifyContent: "center", padding: "90px",
|
|
181
|
+
background: "linear-gradient(135deg, ${ctx.template.accent[0]}, ${ctx.template.accent[1]})",
|
|
182
|
+
color: "white", fontFamily: "sans-serif",
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
<div style={{ fontSize: 72, fontWeight: 800, lineHeight: 1.05, letterSpacing: "-0.02em" }}>{title}</div>
|
|
186
|
+
<div style={{ marginTop: 28, fontSize: 34, opacity: 0.85 }}>{site.config.name}</div>
|
|
187
|
+
</div>
|
|
188
|
+
),
|
|
189
|
+
{ width: 1200, height: 630 },
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
`;
|
|
193
|
+
var notFound = () => `import Link from "next/link";
|
|
194
|
+
|
|
195
|
+
export default function NotFound() {
|
|
196
|
+
return (
|
|
197
|
+
<main className="flex min-h-screen flex-col items-center justify-center gap-6 px-6 text-center">
|
|
198
|
+
<div className="text-8xl font-black gradient-text">404</div>
|
|
199
|
+
<p className="text-lg text-white/60">This page wandered off.</p>
|
|
200
|
+
<Link href="/" className="gradient-bg rounded-full px-6 py-3 font-semibold text-black">Back home</Link>
|
|
201
|
+
</main>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
`;
|
|
205
|
+
var manifestTs = (ctx) => `import type { MetadataRoute } from "next";
|
|
206
|
+
import { site } from "@/lib/site";
|
|
207
|
+
|
|
208
|
+
export default function manifest(): MetadataRoute.Manifest {
|
|
209
|
+
return {
|
|
210
|
+
name: site.config.name,
|
|
211
|
+
short_name: site.config.name,
|
|
212
|
+
description: site.config.description,
|
|
213
|
+
start_url: "/",
|
|
214
|
+
display: "standalone",
|
|
215
|
+
background_color: "#0a0a0f",
|
|
216
|
+
theme_color: "${ctx.template.accent[0]}",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
`;
|
|
220
|
+
var envExample = () => `# Your production URL \u2014 powers canonical URLs, sitemap, robots and OG images.
|
|
221
|
+
NEXT_PUBLIC_SITE_URL=https://example.com
|
|
222
|
+
`;
|
|
223
|
+
var welcomeMd = (ctx) => `# \u{1F381} Welcome to ${ctx.name}
|
|
224
|
+
|
|
225
|
+
You didn't get a blank page \u2014 you got a running, good-looking **${ctx.template.label}** with the
|
|
226
|
+
boring-but-essential stuff already done. Here's what's in the box.
|
|
227
|
+
|
|
228
|
+
## \u2705 Already set up for you
|
|
229
|
+
|
|
230
|
+
- **Beautiful home page** \u2014 styled with Tailwind v4, Inter font, dark theme + gradient accent.
|
|
231
|
+
- **SEO** \u2014 metadata, Open Graph, Twitter cards & JSON-LD, all from one file (\`lib/site.ts\`).
|
|
232
|
+
- **\u2728 Dynamic OG images** \u2014 every page auto-generates a social-share card at \`/og\`. Share a link and see.
|
|
233
|
+
- **Security headers** \u2014 HSTS, CSP, X-Frame-Options and more, via \`next.config.mjs\`.
|
|
234
|
+
- **robots.txt + sitemap.xml** \u2014 generated from your site config. No hand-editing.
|
|
235
|
+
- **A styled 404** and a **PWA manifest** \u2014 the finishing touches most starters skip.
|
|
236
|
+
|
|
237
|
+
## \u{1F680} Run it
|
|
238
|
+
|
|
239
|
+
\`\`\`bash
|
|
240
|
+
npm run dev # http://localhost:3000
|
|
241
|
+
\`\`\`
|
|
242
|
+
|
|
243
|
+
## \u{1F3A8} Make it yours (start here)
|
|
244
|
+
|
|
245
|
+
1. Edit **\`lib/site.ts\`** \u2014 your name, URL and description flow into SEO, sitemap, robots and OG.
|
|
246
|
+
2. Edit **\`app/page.tsx\`** \u2014 your home page.
|
|
247
|
+
3. Set **\`NEXT_PUBLIC_SITE_URL\`** in \`.env\` before deploying (copy \`.env.example\`).
|
|
248
|
+
|
|
249
|
+
## \u{1F389} Surprise: 49 more Lacspace packages, one install away
|
|
250
|
+
|
|
251
|
+
Your app is wired for the whole ecosystem. Drop any of these in \u2014 all zero-dependency:
|
|
252
|
+
|
|
253
|
+
\`\`\`bash
|
|
254
|
+
npm i @lacspace/id # uuidv7, nanoid, short ids
|
|
255
|
+
npm i @lacspace/pdf # invoices & receipts, no headless browser
|
|
256
|
+
npm i @lacspace/signed-url # magic-login & expiring download links
|
|
257
|
+
npm i @lacspace/webhooks # sign / verify / deliver webhooks
|
|
258
|
+
npm i @lacspace/flags # feature flags & A/B, no SaaS
|
|
259
|
+
npm i @lacspace/humanize # "1.5 KB", "3 hours ago", "1.2M"
|
|
260
|
+
\`\`\`
|
|
261
|
+
|
|
262
|
+
Browse them all \u2192 **https://lacspace.com/packages**
|
|
263
|
+
|
|
264
|
+
## \u2601\uFE0F Deploy
|
|
265
|
+
|
|
266
|
+
Push to GitHub and import on **[Vercel](https://vercel.com/new)** \u2014 it just works. Remember to set
|
|
267
|
+
\`NEXT_PUBLIC_SITE_URL\` to your real domain.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
Built with \u2764\uFE0F using [Lacspace](https://lacspace.com/packages). This app is yours under the Lacspace Free Licence.
|
|
272
|
+
`;
|
|
145
273
|
var robotsTs = () => `import { robotsForSite } from "@lacspace/robots";
|
|
146
274
|
import { site } from "@/lib/site";
|
|
147
275
|
|
|
@@ -438,6 +566,67 @@ function homePage(ctx) {
|
|
|
438
566
|
</section>
|
|
439
567
|
${FOOTER(n)}`);
|
|
440
568
|
}
|
|
569
|
+
var glyph = (ctx) => (ctx.template.siteName.trim()[0] ?? "A").toUpperCase();
|
|
570
|
+
var iconTsx = (ctx) => `import { ImageResponse } from "next/og";
|
|
571
|
+
|
|
572
|
+
// \u2728 Auto-generated favicon \u2014 a branded icon from your accent, no design file.
|
|
573
|
+
export const size = { width: 64, height: 64 };
|
|
574
|
+
export const contentType = "image/png";
|
|
575
|
+
|
|
576
|
+
export default function Icon() {
|
|
577
|
+
return new ImageResponse(
|
|
578
|
+
(
|
|
579
|
+
<div style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", background: "linear-gradient(135deg, ${ctx.template.accent[0]}, ${ctx.template.accent[1]})", color: "white", fontSize: 40, fontWeight: 800, fontFamily: "sans-serif", borderRadius: 14 }}>
|
|
580
|
+
${glyph(ctx)}
|
|
581
|
+
</div>
|
|
582
|
+
),
|
|
583
|
+
{ ...size },
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
`;
|
|
587
|
+
var appleIconTsx = (ctx) => `import { ImageResponse } from "next/og";
|
|
588
|
+
|
|
589
|
+
// \u2728 Auto-generated Apple touch icon.
|
|
590
|
+
export const size = { width: 180, height: 180 };
|
|
591
|
+
export const contentType = "image/png";
|
|
592
|
+
|
|
593
|
+
export default function AppleIcon() {
|
|
594
|
+
return new ImageResponse(
|
|
595
|
+
(
|
|
596
|
+
<div style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", background: "linear-gradient(135deg, ${ctx.template.accent[0]}, ${ctx.template.accent[1]})", color: "white", fontSize: 104, fontWeight: 800, fontFamily: "sans-serif" }}>
|
|
597
|
+
${glyph(ctx)}
|
|
598
|
+
</div>
|
|
599
|
+
),
|
|
600
|
+
{ ...size },
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
`;
|
|
604
|
+
var aboutPage = (ctx) => `import type { Metadata } from "next";
|
|
605
|
+
import Link from "next/link";
|
|
606
|
+
import { site } from "@/lib/site";
|
|
607
|
+
|
|
608
|
+
// \u2728 Per-page SEO, auto-generated: title template, canonical, Open Graph,
|
|
609
|
+
// Twitter card and its OWN dynamic OG image \u2014 all from one line.
|
|
610
|
+
export const metadata: Metadata = site.meta({
|
|
611
|
+
title: "About",
|
|
612
|
+
path: "/about",
|
|
613
|
+
description: "Learn more about ${ctx.template.siteName}.",
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
export default function AboutPage() {
|
|
617
|
+
return (
|
|
618
|
+
<main className="mx-auto max-w-3xl px-6 py-28">
|
|
619
|
+
<Link href="/" className="text-sm text-white/50 hover:text-white">\u2190 Back home</Link>
|
|
620
|
+
<h1 className="mt-6 text-4xl font-black sm:text-5xl gradient-text">About</h1>
|
|
621
|
+
<p className="mt-6 text-lg leading-relaxed text-white/60">
|
|
622
|
+
This page already has its own SEO \u2014 a unique title, canonical URL, Open Graph tags and a
|
|
623
|
+
generated social image \u2014 from a single <code>site.meta()</code> call. Duplicate this file for
|
|
624
|
+
any new route and it just works.
|
|
625
|
+
</p>
|
|
626
|
+
</main>
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
`;
|
|
441
630
|
function buildFiles(ctx) {
|
|
442
631
|
return {
|
|
443
632
|
"package.json": pkgJson(ctx),
|
|
@@ -450,8 +639,16 @@ function buildFiles(ctx) {
|
|
|
450
639
|
"app/layout.tsx": layout(ctx),
|
|
451
640
|
"app/globals.css": globalsCss(ctx),
|
|
452
641
|
"app/page.tsx": homePage(ctx),
|
|
642
|
+
"app/about/page.tsx": aboutPage(ctx),
|
|
643
|
+
"app/not-found.tsx": notFound(),
|
|
644
|
+
"app/og/route.tsx": ogRoute(ctx),
|
|
645
|
+
"app/icon.tsx": iconTsx(ctx),
|
|
646
|
+
"app/apple-icon.tsx": appleIconTsx(ctx),
|
|
647
|
+
"app/manifest.ts": manifestTs(ctx),
|
|
453
648
|
"app/robots.txt/route.ts": robotsTs(),
|
|
454
|
-
"app/sitemap.xml/route.ts": sitemapTs()
|
|
649
|
+
"app/sitemap.xml/route.ts": sitemapTs(),
|
|
650
|
+
".env.example": envExample(),
|
|
651
|
+
"WELCOME.md": welcomeMd(ctx)
|
|
455
652
|
};
|
|
456
653
|
}
|
|
457
654
|
function parseArgs(list) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-lacspace-app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Scaffold a beautiful, production-ready Next.js app from a Lacspace template — portfolio, business, e-commerce, SaaS, blog, docs, dashboard or restaurant — pre-wired with SEO, security headers, sitemap and robots. Like create-next-app, but you start gorgeous.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|