create-lacspace-app 1.5.0 → 1.6.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/dist/index.js +323 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -43,8 +43,8 @@ var pkgJson = (ctx) => JSON.stringify({
|
|
|
43
43
|
"@lacspace/ui": "^1.0.0",
|
|
44
44
|
"@lacspace/form": "^1.0.0",
|
|
45
45
|
"@lacspace/validate": "^1.0.0",
|
|
46
|
-
// The blog
|
|
47
|
-
...ctx.template.key === "blog" ? { "@lacspace/markdown": "^1.0.0" } : {}
|
|
46
|
+
// The blog & docs templates render Markdown with @lacspace/markdown.
|
|
47
|
+
...ctx.template.key === "blog" || ctx.template.key === "docs" ? { "@lacspace/markdown": "^1.0.0" } : {}
|
|
48
48
|
},
|
|
49
49
|
devDependencies: {
|
|
50
50
|
typescript: "^5.7.0",
|
|
@@ -134,7 +134,7 @@ body {
|
|
|
134
134
|
@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
|
|
135
135
|
main > section, main > * { animation: rise 0.6s cubic-bezier(0.22, 1, 0.36, 1) both; }
|
|
136
136
|
@media (prefers-reduced-motion: reduce) { *, ::before, ::after { animation: none !important; scroll-behavior: auto; } }
|
|
137
|
-
${ctx.template.key === "blog" ? BLOG_PROSE_CSS : ""}`;
|
|
137
|
+
${ctx.template.key === "blog" || ctx.template.key === "docs" ? BLOG_PROSE_CSS : ""}`;
|
|
138
138
|
var BLOG_PROSE_CSS = `
|
|
139
139
|
/* article typography for Markdown posts */
|
|
140
140
|
.prose { line-height: 1.75; color: rgb(229 231 235 / 0.9); }
|
|
@@ -678,7 +678,8 @@ export function CommandMenu() {
|
|
|
678
678
|
items={[
|
|
679
679
|
{ id: "home", label: "Home", group: "Navigate", shortcut: "G H", onSelect: () => router.push("/") },
|
|
680
680
|
{ id: "about", label: "About", group: "Navigate", onSelect: () => router.push("/about") },${ctx.template.key === "blog" ? `
|
|
681
|
-
{ id: "blog", label: "Blog", group: "Navigate", onSelect: () => router.push("/blog") },` : ""}
|
|
681
|
+
{ id: "blog", label: "Blog", group: "Navigate", onSelect: () => router.push("/blog") },` : ""}${ctx.template.key === "docs" ? `
|
|
682
|
+
{ id: "docs", label: "Docs", group: "Navigate", onSelect: () => router.push("/docs") },` : ""}
|
|
682
683
|
{ id: "contact", label: "Contact", group: "Navigate", onSelect: () => router.push("/contact") },
|
|
683
684
|
{ id: "packages", label: "Lacspace packages", group: "Links", onSelect: () => window.open("https://lacspace.com/packages", "_blank") },
|
|
684
685
|
]}
|
|
@@ -793,6 +794,312 @@ export default function ContactPage() {
|
|
|
793
794
|
);
|
|
794
795
|
}
|
|
795
796
|
`;
|
|
797
|
+
var docsLib = () => `import fs from "node:fs";
|
|
798
|
+
import path from "node:path";
|
|
799
|
+
import { markdownToHtml, extractHeadings, type Heading } from "@lacspace/markdown";
|
|
800
|
+
|
|
801
|
+
const DIR = path.join(process.cwd(), "content/docs");
|
|
802
|
+
|
|
803
|
+
export interface DocMeta {
|
|
804
|
+
slug: string;
|
|
805
|
+
title: string;
|
|
806
|
+
group: string;
|
|
807
|
+
order: number;
|
|
808
|
+
description: string;
|
|
809
|
+
}
|
|
810
|
+
export interface Doc extends DocMeta {
|
|
811
|
+
html: string;
|
|
812
|
+
toc: Heading[];
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function parse(raw: string): { data: Record<string, string>; body: string } {
|
|
816
|
+
const m = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/.exec(raw);
|
|
817
|
+
if (!m) return { data: {}, body: raw };
|
|
818
|
+
const data: Record<string, string> = {};
|
|
819
|
+
for (const line of m[1].split(/\\r?\\n/)) {
|
|
820
|
+
const i = line.indexOf(":");
|
|
821
|
+
if (i === -1) continue;
|
|
822
|
+
data[line.slice(0, i).trim()] = line.slice(i + 1).trim().replace(/^["']|["']$/g, "");
|
|
823
|
+
}
|
|
824
|
+
return { data, body: m[2] };
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export function getAllDocs(): DocMeta[] {
|
|
828
|
+
if (!fs.existsSync(DIR)) return [];
|
|
829
|
+
return fs
|
|
830
|
+
.readdirSync(DIR)
|
|
831
|
+
.filter((f) => f.endsWith(".md"))
|
|
832
|
+
.map((f) => {
|
|
833
|
+
const { data } = parse(fs.readFileSync(path.join(DIR, f), "utf8"));
|
|
834
|
+
return {
|
|
835
|
+
slug: f.replace(/\\.md$/, ""),
|
|
836
|
+
title: data.title ?? f,
|
|
837
|
+
group: data.group ?? "Docs",
|
|
838
|
+
order: Number(data.order ?? "99"),
|
|
839
|
+
description: data.description ?? "",
|
|
840
|
+
};
|
|
841
|
+
})
|
|
842
|
+
.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
export function getDoc(slug: string): Doc | null {
|
|
846
|
+
const file = path.join(DIR, \`\${slug}.md\`);
|
|
847
|
+
if (!fs.existsSync(file)) return null;
|
|
848
|
+
const { data, body } = parse(fs.readFileSync(file, "utf8"));
|
|
849
|
+
return {
|
|
850
|
+
slug,
|
|
851
|
+
title: data.title ?? slug,
|
|
852
|
+
group: data.group ?? "Docs",
|
|
853
|
+
order: Number(data.order ?? "99"),
|
|
854
|
+
description: data.description ?? "",
|
|
855
|
+
html: markdownToHtml(body, { headingOffset: 1 }),
|
|
856
|
+
toc: extractHeadings(body),
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export function getDocNav(): { group: string; items: DocMeta[] }[] {
|
|
861
|
+
const groups: { group: string; items: DocMeta[] }[] = [];
|
|
862
|
+
for (const doc of getAllDocs()) {
|
|
863
|
+
let g = groups.find((x) => x.group === doc.group);
|
|
864
|
+
if (!g) { g = { group: doc.group, items: [] }; groups.push(g); }
|
|
865
|
+
g.items.push(doc);
|
|
866
|
+
}
|
|
867
|
+
return groups;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
export function adjacentDocs(slug: string): { prev: DocMeta | null; next: DocMeta | null } {
|
|
871
|
+
const all = getAllDocs();
|
|
872
|
+
const i = all.findIndex((d) => d.slug === slug);
|
|
873
|
+
return { prev: i > 0 ? all[i - 1]! : null, next: i >= 0 && i < all.length - 1 ? all[i + 1]! : null };
|
|
874
|
+
}
|
|
875
|
+
`;
|
|
876
|
+
var docsSidebar = () => `"use client";
|
|
877
|
+
import Link from "next/link";
|
|
878
|
+
import { usePathname } from "next/navigation";
|
|
879
|
+
|
|
880
|
+
interface DocMeta { slug: string; title: string; group: string; order: number; description: string; }
|
|
881
|
+
|
|
882
|
+
export function DocsSidebar({ nav }: { nav: { group: string; items: DocMeta[] }[] }) {
|
|
883
|
+
const path = usePathname();
|
|
884
|
+
return (
|
|
885
|
+
<nav className="flex flex-col gap-6 text-sm">
|
|
886
|
+
{nav.map((group) => (
|
|
887
|
+
<div key={group.group}>
|
|
888
|
+
<p className="mb-2 text-xs font-semibold uppercase tracking-widest text-white/40">{group.group}</p>
|
|
889
|
+
<ul className="flex flex-col gap-1">
|
|
890
|
+
{group.items.map((d) => {
|
|
891
|
+
const href = \`/docs/\${d.slug}\`;
|
|
892
|
+
const active = path === href;
|
|
893
|
+
return (
|
|
894
|
+
<li key={d.slug}>
|
|
895
|
+
<Link
|
|
896
|
+
href={href}
|
|
897
|
+
className={\`block rounded-lg px-3 py-1.5 transition \${active ? "gradient-bg font-semibold text-black" : "text-white/70 hover:bg-white/5 hover:text-white"}\`}
|
|
898
|
+
>
|
|
899
|
+
{d.title}
|
|
900
|
+
</Link>
|
|
901
|
+
</li>
|
|
902
|
+
);
|
|
903
|
+
})}
|
|
904
|
+
</ul>
|
|
905
|
+
</div>
|
|
906
|
+
))}
|
|
907
|
+
</nav>
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
`;
|
|
911
|
+
var docsLayout = () => `import Link from "next/link";
|
|
912
|
+
import { getDocNav } from "@/lib/docs";
|
|
913
|
+
import { DocsSidebar } from "@/components/docs-sidebar";
|
|
914
|
+
|
|
915
|
+
export default function DocsLayout({ children }: { children: React.ReactNode }) {
|
|
916
|
+
const nav = getDocNav();
|
|
917
|
+
return (
|
|
918
|
+
<div className="mx-auto grid max-w-6xl gap-10 px-6 py-16 md:grid-cols-[220px_1fr]">
|
|
919
|
+
<aside className="md:sticky md:top-24 md:h-fit">
|
|
920
|
+
<Link href="/docs" className="mb-6 block text-lg font-black gradient-text">Docs</Link>
|
|
921
|
+
<DocsSidebar nav={nav} />
|
|
922
|
+
</aside>
|
|
923
|
+
<div className="min-w-0">{children}</div>
|
|
924
|
+
</div>
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
`;
|
|
928
|
+
var docsIndexPage = (ctx) => `import Link from "next/link";
|
|
929
|
+
import { site } from "@/lib/site";
|
|
930
|
+
import { getAllDocs } from "@/lib/docs";
|
|
931
|
+
|
|
932
|
+
export const metadata = site.meta({
|
|
933
|
+
title: "Documentation",
|
|
934
|
+
path: "/docs",
|
|
935
|
+
description: "Documentation for ${ctx.template.siteName}.",
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
export default function DocsIndex() {
|
|
939
|
+
const docs = getAllDocs();
|
|
940
|
+
return (
|
|
941
|
+
<div>
|
|
942
|
+
<h1 className="text-4xl font-black gradient-text">Documentation</h1>
|
|
943
|
+
<p className="mt-4 text-white/60">Everything you need to get started and go deep.</p>
|
|
944
|
+
<div className="mt-10 grid gap-4 sm:grid-cols-2">
|
|
945
|
+
{docs.map((d) => (
|
|
946
|
+
<Link key={d.slug} href={\`/docs/\${d.slug}\`} className="rounded-2xl border border-white/10 bg-white/5 p-5 transition hover:border-white/25">
|
|
947
|
+
<span className="text-xs font-semibold uppercase tracking-widest text-white/40">{d.group}</span>
|
|
948
|
+
<h2 className="mt-1 text-lg font-bold">{d.title}</h2>
|
|
949
|
+
{d.description && <p className="mt-1 text-sm text-white/60">{d.description}</p>}
|
|
950
|
+
</Link>
|
|
951
|
+
))}
|
|
952
|
+
</div>
|
|
953
|
+
</div>
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
`;
|
|
957
|
+
var docsPage = () => `import Link from "next/link";
|
|
958
|
+
import { notFound } from "next/navigation";
|
|
959
|
+
import { site } from "@/lib/site";
|
|
960
|
+
import { getAllDocs, getDoc, adjacentDocs } from "@/lib/docs";
|
|
961
|
+
|
|
962
|
+
export function generateStaticParams() {
|
|
963
|
+
return getAllDocs().map((d) => ({ slug: d.slug }));
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
|
|
967
|
+
const { slug } = await params;
|
|
968
|
+
const doc = getDoc(slug);
|
|
969
|
+
if (!doc) return {};
|
|
970
|
+
return site.meta({ title: doc.title, path: \`/docs/\${slug}\`, description: doc.description });
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
export default async function DocPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
974
|
+
const { slug } = await params;
|
|
975
|
+
const doc = getDoc(slug);
|
|
976
|
+
if (!doc) notFound();
|
|
977
|
+
const { prev, next } = adjacentDocs(slug);
|
|
978
|
+
|
|
979
|
+
return (
|
|
980
|
+
<article>
|
|
981
|
+
<span className="text-xs font-semibold uppercase tracking-widest text-white/40">{doc.group}</span>
|
|
982
|
+
<h1 className="mt-1 text-4xl font-black leading-tight">{doc.title}</h1>
|
|
983
|
+
{doc.description && <p className="mt-3 text-lg text-white/60">{doc.description}</p>}
|
|
984
|
+
|
|
985
|
+
{doc.toc.length > 2 && (
|
|
986
|
+
<div className="mt-8 rounded-xl border border-white/10 bg-white/5 p-4">
|
|
987
|
+
<p className="mb-2 text-xs font-semibold uppercase tracking-widest text-white/40">On this page</p>
|
|
988
|
+
<ul className="flex flex-col gap-1 text-sm">
|
|
989
|
+
{doc.toc.map((h) => (
|
|
990
|
+
<li key={h.id} style={{ paddingLeft: (h.level - 1) * 12 }}>
|
|
991
|
+
<a href={\`#\${h.id}\`} className="text-white/60 hover:text-white">{h.text}</a>
|
|
992
|
+
</li>
|
|
993
|
+
))}
|
|
994
|
+
</ul>
|
|
995
|
+
</div>
|
|
996
|
+
)}
|
|
997
|
+
|
|
998
|
+
<div className="prose mt-10" dangerouslySetInnerHTML={{ __html: doc.html }} />
|
|
999
|
+
|
|
1000
|
+
<nav className="mt-14 flex justify-between gap-4 border-t border-white/10 pt-8 text-sm">
|
|
1001
|
+
{prev ? <Link href={\`/docs/\${prev.slug}\`} className="text-white/70 hover:text-white">← {prev.title}</Link> : <span />}
|
|
1002
|
+
{next ? <Link href={\`/docs/\${next.slug}\`} className="text-right text-white/70 hover:text-white">{next.title} →</Link> : <span />}
|
|
1003
|
+
</nav>
|
|
1004
|
+
</article>
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
`;
|
|
1008
|
+
var docsSitemapTs = () => `import { sitemapForSite } from "@lacspace/sitemap";
|
|
1009
|
+
import { site } from "@/lib/site";
|
|
1010
|
+
import { getAllDocs } from "@/lib/docs";
|
|
1011
|
+
|
|
1012
|
+
export function GET() {
|
|
1013
|
+
const paths = ["/", "/about", "/contact", "/docs", ...getAllDocs().map((d) => \`/docs/\${d.slug}\`)];
|
|
1014
|
+
const xml = sitemapForSite(site.config, paths);
|
|
1015
|
+
return new Response(xml, { headers: { "content-type": "application/xml" } });
|
|
1016
|
+
}
|
|
1017
|
+
`;
|
|
1018
|
+
var sampleDocIntro = (ctx) => `---
|
|
1019
|
+
title: Introduction
|
|
1020
|
+
group: Getting Started
|
|
1021
|
+
order: 1
|
|
1022
|
+
description: What ${ctx.template.siteName} is and how these docs work.
|
|
1023
|
+
---
|
|
1024
|
+
|
|
1025
|
+
# Introduction
|
|
1026
|
+
|
|
1027
|
+
Welcome to the **${ctx.template.siteName}** documentation. These pages are plain
|
|
1028
|
+
**Markdown files** in \`content/docs/\`, rendered to static pages with
|
|
1029
|
+
[\`@lacspace/markdown\`](https://www.npmjs.com/package/@lacspace/markdown).
|
|
1030
|
+
|
|
1031
|
+
## How the docs are organised
|
|
1032
|
+
|
|
1033
|
+
- Each \`.md\` file becomes a page at \`/docs/<filename>\`.
|
|
1034
|
+
- Front-matter sets the **title**, **group** (sidebar heading) and **order**.
|
|
1035
|
+
- The sidebar, the on-this-page table of contents and prev/next links are all
|
|
1036
|
+
generated for you.
|
|
1037
|
+
|
|
1038
|
+
> Edit these files, add your own, and the navigation updates automatically.
|
|
1039
|
+
`;
|
|
1040
|
+
var sampleDocInstall = () => `---
|
|
1041
|
+
title: Installation
|
|
1042
|
+
group: Getting Started
|
|
1043
|
+
order: 2
|
|
1044
|
+
description: Add a new documentation page in under a minute.
|
|
1045
|
+
---
|
|
1046
|
+
|
|
1047
|
+
# Installation & setup
|
|
1048
|
+
|
|
1049
|
+
Create a Markdown file in \`content/docs/\` with front-matter at the top:
|
|
1050
|
+
|
|
1051
|
+
\`\`\`md
|
|
1052
|
+
---
|
|
1053
|
+
title: My page
|
|
1054
|
+
group: Guides
|
|
1055
|
+
order: 1
|
|
1056
|
+
description: A short summary for SEO and the index.
|
|
1057
|
+
---
|
|
1058
|
+
|
|
1059
|
+
# My page
|
|
1060
|
+
|
|
1061
|
+
Write **Markdown** here \u2014 headings, lists, tables and code all work.
|
|
1062
|
+
\`\`\`
|
|
1063
|
+
|
|
1064
|
+
That's it \u2014 the page appears at \`/docs/my-page\` and in the sidebar under "Guides".
|
|
1065
|
+
`;
|
|
1066
|
+
var sampleDocWriting = () => `---
|
|
1067
|
+
title: Writing content
|
|
1068
|
+
group: Guides
|
|
1069
|
+
order: 1
|
|
1070
|
+
description: Everything the Markdown renderer supports.
|
|
1071
|
+
---
|
|
1072
|
+
|
|
1073
|
+
# Writing content
|
|
1074
|
+
|
|
1075
|
+
The renderer supports the essentials \u2014 and a bit more.
|
|
1076
|
+
|
|
1077
|
+
## Text & lists
|
|
1078
|
+
|
|
1079
|
+
**Bold**, *italic*, \`inline code\`, and:
|
|
1080
|
+
|
|
1081
|
+
- bullet lists
|
|
1082
|
+
- that nest
|
|
1083
|
+
- [x] task lists
|
|
1084
|
+
|
|
1085
|
+
## Tables
|
|
1086
|
+
|
|
1087
|
+
| Feature | Supported |
|
|
1088
|
+
| ------- | :-------: |
|
|
1089
|
+
| Headings + anchors | \u2705 |
|
|
1090
|
+
| Code blocks | \u2705 |
|
|
1091
|
+
| Tables | \u2705 |
|
|
1092
|
+
|
|
1093
|
+
## Code
|
|
1094
|
+
|
|
1095
|
+
\`\`\`ts
|
|
1096
|
+
export function greet(name: string) {
|
|
1097
|
+
return \`Hello, \${name}!\`;
|
|
1098
|
+
}
|
|
1099
|
+
\`\`\`
|
|
1100
|
+
|
|
1101
|
+
Every heading gets an anchor id, so the on-this-page menu links straight to it.
|
|
1102
|
+
`;
|
|
796
1103
|
var seoWorkflow = () => `name: SEO
|
|
797
1104
|
|
|
798
1105
|
on:
|
|
@@ -1045,6 +1352,7 @@ Check the [first post](/blog/welcome) for the full Markdown reference.
|
|
|
1045
1352
|
`;
|
|
1046
1353
|
function buildFiles(ctx) {
|
|
1047
1354
|
const isBlog = ctx.template.key === "blog";
|
|
1355
|
+
const isDocs = ctx.template.key === "docs";
|
|
1048
1356
|
const files = {
|
|
1049
1357
|
"package.json": pkgJson(ctx),
|
|
1050
1358
|
"tsconfig.json": tsconfig(),
|
|
@@ -1063,7 +1371,7 @@ function buildFiles(ctx) {
|
|
|
1063
1371
|
"app/apple-icon.tsx": appleIconTsx(ctx),
|
|
1064
1372
|
"app/manifest.ts": manifestTs(ctx),
|
|
1065
1373
|
"app/robots.txt/route.ts": robotsTs(),
|
|
1066
|
-
"app/sitemap.xml/route.ts": isBlog ? blogSitemapTs() : sitemapTs(),
|
|
1374
|
+
"app/sitemap.xml/route.ts": isBlog ? blogSitemapTs() : isDocs ? docsSitemapTs() : sitemapTs(),
|
|
1067
1375
|
"app/contact/page.tsx": contactPage(ctx),
|
|
1068
1376
|
"app/actions.ts": actionsTs(),
|
|
1069
1377
|
"components/command-menu.tsx": commandMenu(ctx),
|
|
@@ -1079,6 +1387,16 @@ function buildFiles(ctx) {
|
|
|
1079
1387
|
files["content/posts/welcome.md"] = samplePostWelcome(ctx);
|
|
1080
1388
|
files["content/posts/building-in-the-open.md"] = samplePostSecond(ctx);
|
|
1081
1389
|
}
|
|
1390
|
+
if (isDocs) {
|
|
1391
|
+
files["lib/docs.ts"] = docsLib();
|
|
1392
|
+
files["components/docs-sidebar.tsx"] = docsSidebar();
|
|
1393
|
+
files["app/docs/layout.tsx"] = docsLayout();
|
|
1394
|
+
files["app/docs/page.tsx"] = docsIndexPage(ctx);
|
|
1395
|
+
files["app/docs/[slug]/page.tsx"] = docsPage();
|
|
1396
|
+
files["content/docs/introduction.md"] = sampleDocIntro(ctx);
|
|
1397
|
+
files["content/docs/installation.md"] = sampleDocInstall();
|
|
1398
|
+
files["content/docs/writing-content.md"] = sampleDocWriting();
|
|
1399
|
+
}
|
|
1082
1400
|
return files;
|
|
1083
1401
|
}
|
|
1084
1402
|
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.6.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": {
|