create-goodfellow 0.0.0-stage → 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 +21 -0
- package/README.md +15 -2
- package/bin/create-goodfellow.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +220 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +2 -0
- package/dist/scaffold-BGxzPdN1.js +150 -0
- package/package.json +55 -3
- package/templates/next/.github/workflows/deploy.yml +86 -0
- package/templates/next/.gitlab-ci.yml +28 -0
- package/templates/next/README.md +127 -0
- package/templates/next/_gitignore +5 -0
- package/templates/next/app/(site)/[[...path]]/page.tsx +8 -0
- package/templates/next/app/(site)/layout.tsx +5 -0
- package/templates/next/app/admin/demo-content.json/route.ts +5 -0
- package/templates/next/app/admin/layout.tsx +8 -0
- package/templates/next/app/admin/page.tsx +8 -0
- package/templates/next/app/layout.tsx +11 -0
- package/templates/next/app/not-found.tsx +4 -0
- package/templates/next/app/robots.ts +4 -0
- package/templates/next/app/site.css +11 -0
- package/templates/next/app/sitemap.ts +4 -0
- package/templates/next/blocks/installed/index.ts +7 -0
- package/templates/next/components.json +24 -0
- package/templates/next/content/collections/news/_collection.json +113 -0
- package/templates/next/content/collections/news/open-house.json +9 -0
- package/templates/next/content/collections/news/welcome.json +10 -0
- package/templates/next/content/layout/footer.json +55 -0
- package/templates/next/content/layout/header.json +54 -0
- package/templates/next/content/menus.json +25 -0
- package/templates/next/content/pages/404.json +73 -0
- package/templates/next/content/pages/about.json +59 -0
- package/templates/next/content/pages/index.json +215 -0
- package/templates/next/content/pages/news.json +55 -0
- package/templates/next/content/site.json +25 -0
- package/templates/next/content/styles/custom.css +4 -0
- package/templates/next/goodfellow.config.tsx +17 -0
- package/templates/next/lib/site.ts +5 -0
- package/templates/next/next.config.ts +7 -0
- package/templates/next/package.json +40 -0
- package/templates/next/postcss.config.mjs +3 -0
- package/templates/next/public/media/favicon.svg +1 -0
- package/templates/next/public/media/logo.svg +1 -0
- package/templates/next/public/media/placeholder.svg +1 -0
- package/templates/next/tsconfig.json +27 -0
- package/templates/parish/.github/workflows/deploy.yml +84 -0
- package/templates/parish/.gitlab-ci.yml +28 -0
- package/templates/parish/README.md +128 -0
- package/templates/parish/_gitignore +2 -0
- package/templates/parish/blocks/installed/index.ts +7 -0
- package/templates/parish/blocks/mass-times.tsx +59 -0
- package/templates/parish/blocks/notice.tsx +46 -0
- package/templates/parish/blocks/scripture.tsx +40 -0
- package/templates/parish/components.json +24 -0
- package/templates/parish/content/collections/bulletins/2026-09-27.json +8 -0
- package/templates/parish/content/collections/bulletins/2026-10-04.json +8 -0
- package/templates/parish/content/collections/bulletins/_collection.json +113 -0
- package/templates/parish/content/collections/events/_collection.json +156 -0
- package/templates/parish/content/collections/events/advent-penance-service.json +14 -0
- package/templates/parish/content/collections/events/all-souls.json +14 -0
- package/templates/parish/content/collections/events/christmas-concert.json +14 -0
- package/templates/parish/content/collections/events/fall-festival.json +14 -0
- package/templates/parish/content/collections/events/youth-group.json +23 -0
- package/templates/parish/content/collections/news/_collection.json +137 -0
- package/templates/parish/content/collections/news/bell-tower-repairs.json +13 -0
- package/templates/parish/content/collections/news/choir-new-voices.json +14 -0
- package/templates/parish/content/collections/news/food-drive.json +13 -0
- package/templates/parish/content/collections/news/new-website.json +13 -0
- package/templates/parish/content/collections/news/winter-coats.json +13 -0
- package/templates/parish/content/collections/news/youth-group.json +13 -0
- package/templates/parish/content/collections/news/youth-service-day.json +14 -0
- package/templates/parish/content/collections/staff/_collection.json +34 -0
- package/templates/parish/content/collections/staff/david-okafor.json +9 -0
- package/templates/parish/content/collections/staff/fr-thomas-reed.json +9 -0
- package/templates/parish/content/collections/staff/maria-chen.json +9 -0
- package/templates/parish/content/collections/staff/sarah-lindqvist.json +9 -0
- package/templates/parish/content/layout/footer.json +142 -0
- package/templates/parish/content/layout/header.json +54 -0
- package/templates/parish/content/menus.json +67 -0
- package/templates/parish/content/pages/404.json +85 -0
- package/templates/parish/content/pages/about.json +153 -0
- package/templates/parish/content/pages/bulletins.json +64 -0
- package/templates/parish/content/pages/calendar.json +59 -0
- package/templates/parish/content/pages/contact.json +180 -0
- package/templates/parish/content/pages/events.json +107 -0
- package/templates/parish/content/pages/index.json +390 -0
- package/templates/parish/content/pages/mass-times.json +168 -0
- package/templates/parish/content/pages/news.json +68 -0
- package/templates/parish/content/pages/sacraments.json +259 -0
- package/templates/parish/content/site.json +36 -0
- package/templates/parish/content/styles/custom.css +4 -0
- package/templates/parish/goodfellow.config.tsx +24 -0
- package/templates/parish/package.json +36 -0
- package/templates/parish/public/media/bulletin-example.pdf +32 -0
- package/templates/parish/public/media/candles.svg +22 -0
- package/templates/parish/public/media/church.svg +33 -0
- package/templates/parish/public/media/favicon.svg +5 -0
- package/templates/parish/public/media/gathering.svg +18 -0
- package/templates/parish/public/media/logo.svg +5 -0
- package/templates/parish/public/media/person.svg +5 -0
- package/templates/parish/public/media/placeholder.svg +5 -0
- package/templates/parish/src/styles.css +8 -0
- package/templates/parish/tsconfig.json +14 -0
- package/templates/parish/vercel.ts +65 -0
- package/templates/registry/accordion.json +17 -0
- package/templates/registry/alert.json +16 -0
- package/templates/registry/badge.json +17 -0
- package/templates/registry/button.json +17 -0
- package/templates/registry/card.json +16 -0
- package/templates/registry/carousel.json +19 -0
- package/templates/registry/custom-html.json +31 -0
- package/templates/registry/registry.json +364 -0
- package/templates/registry/shadcn-call-to-action.json +26 -0
- package/templates/registry/shadcn-cards.json +26 -0
- package/templates/registry/shadcn-carousel.json +29 -0
- package/templates/registry/shadcn-faq.json +30 -0
- package/templates/registry/shadcn-hero.json +25 -0
- package/templates/registry/shadcn-notice.json +27 -0
- package/templates/registry/shadcn-pricing.json +28 -0
- package/templates/registry/shadcn-tabs.json +30 -0
- package/templates/registry/shadcn-testimonials.json +25 -0
- package/templates/registry/tabs.json +17 -0
- package/templates/starter/.github/workflows/deploy.yml +84 -0
- package/templates/starter/.gitlab-ci.yml +28 -0
- package/templates/starter/README.md +108 -0
- package/templates/starter/_gitignore +2 -0
- package/templates/starter/blocks/installed/index.ts +7 -0
- package/templates/starter/components.json +24 -0
- package/templates/starter/content/collections/news/_collection.json +113 -0
- package/templates/starter/content/collections/news/open-house.json +9 -0
- package/templates/starter/content/collections/news/welcome.json +10 -0
- package/templates/starter/content/layout/footer.json +55 -0
- package/templates/starter/content/layout/header.json +54 -0
- package/templates/starter/content/menus.json +25 -0
- package/templates/starter/content/pages/404.json +73 -0
- package/templates/starter/content/pages/about.json +59 -0
- package/templates/starter/content/pages/index.json +215 -0
- package/templates/starter/content/pages/news.json +55 -0
- package/templates/starter/content/site.json +25 -0
- package/templates/starter/content/styles/custom.css +4 -0
- package/templates/starter/goodfellow.config.tsx +17 -0
- package/templates/starter/package.json +36 -0
- package/templates/starter/public/media/favicon.svg +1 -0
- package/templates/starter/public/media/logo.svg +1 -0
- package/templates/starter/public/media/placeholder.svg +1 -0
- package/templates/starter/src/styles.css +8 -0
- package/templates/starter/tsconfig.json +14 -0
- package/templates/starter/vercel.ts +65 -0
- package/templates/versions.json +13 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "shadcn-hero",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Hero",
|
|
6
|
+
"description": "A large heading at the top of a page, with text, buttons and a picture.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"@goodfellow/button"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "blocks/installed/shadcn-hero/block.tsx",
|
|
13
|
+
"type": "registry:block",
|
|
14
|
+
"content": "import { classNameField, cx, mediaField, SiteImage, SiteLink } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { buttonVariants } from \"@/components/ui/button\";\n\ntype Layout = \"centered\" | \"beside\";\n\nexport interface HeroProps {\n title: string;\n text: string;\n image: string;\n imageAlt: string;\n layout: Layout;\n primaryLabel: string;\n primaryLink: string;\n secondaryLabel: string;\n secondaryLink: string;\n className: string;\n}\n\n/** A large heading at the top of a page, with text, up to two buttons and a picture. */\nconst Hero: ComponentConfig<HeroProps> = {\n label: \"Hero\",\n fields: {\n title: { type: \"text\", label: \"Heading\", contentEditable: true },\n text: { type: \"textarea\", label: \"Text\", contentEditable: true },\n image: mediaField(\"Picture\"),\n imageAlt: { type: \"text\", label: \"Description of the picture for screen readers\" },\n layout: {\n type: \"radio\",\n label: \"Layout\",\n options: [\n { label: \"Centered, picture below\", value: \"centered\" },\n { label: \"Picture beside the text\", value: \"beside\" },\n ],\n },\n primaryLabel: { type: \"text\", label: \"Main button\" },\n primaryLink: { type: \"text\", label: \"Main button links to\" },\n secondaryLabel: { type: \"text\", label: \"Second button (optional)\" },\n secondaryLink: { type: \"text\", label: \"Second button links to\" },\n className: classNameField,\n },\n defaultProps: {\n title: \"Welcome\",\n text: \"Say in a sentence or two what this site is about.\",\n image: \"\",\n imageAlt: \"\",\n layout: \"centered\",\n primaryLabel: \"Find out more\",\n primaryLink: \"/about\",\n secondaryLabel: \"\",\n secondaryLink: \"\",\n className: \"\",\n },\n render: ({\n title,\n text,\n image,\n imageAlt,\n layout,\n primaryLabel,\n primaryLink,\n secondaryLabel,\n secondaryLink,\n className,\n }) => {\n const beside = layout === \"beside\";\n return (\n <section className={cx(\"mx-auto w-full max-w-6xl px-4 py-16 md:py-24\", className)}>\n <div\n className={cx(\"flex flex-col gap-10\", beside ? \"md:flex-row md:items-center\" : \"items-center text-center\")}\n >\n <div className={cx(\"flex flex-col gap-6\", beside ? \"md:flex-1\" : \"max-w-3xl items-center\")}>\n <h1 className=\"text-4xl font-bold tracking-tight text-balance md:text-6xl\">{title}</h1>\n {text && <p className=\"text-lg text-muted-foreground md:text-xl\">{text}</p>}\n {(primaryLabel || secondaryLabel) && (\n <div className=\"flex flex-wrap gap-3\">\n {primaryLabel && (\n <SiteLink href={primaryLink || \"/\"} className={buttonVariants({ size: \"lg\" })}>\n {primaryLabel}\n </SiteLink>\n )}\n {secondaryLabel && (\n <SiteLink href={secondaryLink || \"/\"} className={buttonVariants({ size: \"lg\", variant: \"outline\" })}>\n {secondaryLabel}\n </SiteLink>\n )}\n </div>\n )}\n </div>\n {image && (\n <SiteImage\n src={image}\n alt={imageAlt}\n className={cx(\"w-full rounded-xl object-cover\", beside ? \"md:w-1/2\" : \"max-w-4xl\")}\n />\n )}\n </div>\n </section>\n );\n },\n};\n\nexport default Hero;\n"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"meta": {
|
|
18
|
+
"goodfellow": {
|
|
19
|
+
"category": "Page sections",
|
|
20
|
+
"recommended": true,
|
|
21
|
+
"version": "0.2.0"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"dependencies": []
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "shadcn-notice",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Notice",
|
|
6
|
+
"description": "A short message that stands out, such as a closure or a change of times.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"@goodfellow/alert"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"lucide-react"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "blocks/installed/shadcn-notice/block.tsx",
|
|
16
|
+
"type": "registry:block",
|
|
17
|
+
"content": "import { classNameField, cx } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig, RichText } from \"@puckeditor/core\";\nimport { CircleCheckIcon, InfoIcon, TriangleAlertIcon } from \"lucide-react\";\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\n\ntype Kind = \"info\" | \"warning\" | \"success\" | \"urgent\";\n\nexport interface NoticeProps {\n kind: Kind;\n title: string;\n text: RichText;\n className: string;\n}\n\nconst icons = { info: InfoIcon, warning: TriangleAlertIcon, success: CircleCheckIcon, urgent: TriangleAlertIcon };\n\n/** A short message that stands out, such as a closure or a change of times. */\nconst Notice: ComponentConfig<NoticeProps> = {\n label: \"Notice\",\n fields: {\n kind: {\n type: \"select\",\n label: \"Kind\",\n options: [\n { label: \"Information\", value: \"info\" },\n { label: \"Warning\", value: \"warning\" },\n { label: \"Good news\", value: \"success\" },\n { label: \"Urgent\", value: \"urgent\" },\n ],\n },\n title: { type: \"text\", label: \"Heading\", contentEditable: true },\n text: { type: \"richtext\", label: \"Text\", contentEditable: true },\n className: classNameField,\n },\n defaultProps: { kind: \"info\", title: \"Please note\", text: \"<p>Say what's changed.</p>\", className: \"\" },\n render: ({ kind, title, text, className }) => {\n const Icon = icons[kind];\n return (\n <div className={cx(\"mx-auto w-full max-w-3xl px-4 py-4\", className)}>\n <Alert variant={kind === \"urgent\" ? \"destructive\" : \"default\"}>\n <Icon aria-hidden=\"true\" />\n {title && <AlertTitle>{title}</AlertTitle>}\n <AlertDescription>\n <div className=\"gf-prose\">{text}</div>\n </AlertDescription>\n </Alert>\n </div>\n );\n },\n};\n\nexport default Notice;\n"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"meta": {
|
|
21
|
+
"goodfellow": {
|
|
22
|
+
"category": "Page sections",
|
|
23
|
+
"recommended": true,
|
|
24
|
+
"version": "0.2.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "shadcn-pricing",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Pricing table",
|
|
6
|
+
"description": "Plans side by side, with prices, what each includes and a button.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"@goodfellow/card",
|
|
9
|
+
"@goodfellow/badge",
|
|
10
|
+
"@goodfellow/button"
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "blocks/installed/shadcn-pricing/block.tsx",
|
|
18
|
+
"type": "registry:block",
|
|
19
|
+
"content": "import { classNameField, cx, SiteLink } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { CheckIcon } from \"lucide-react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from \"@/components/ui/card\";\n\ninterface Plan {\n name: string;\n price: string;\n period: string;\n description: string;\n features: string;\n buttonLabel: string;\n buttonLink: string;\n badge: string;\n}\n\nexport interface PricingProps {\n title: string;\n intro: string;\n plans: Plan[];\n className: string;\n}\n\nconst emptyPlan: Plan = {\n name: \"Plan\",\n price: \"$10\",\n period: \"a month\",\n description: \"\",\n features: \"Something included\\nSomething else included\",\n buttonLabel: \"Choose\",\n buttonLink: \"/contact\",\n badge: \"\",\n};\n\n/** Plans side by side, with prices, what each includes and a button. */\nconst Pricing: ComponentConfig<PricingProps> = {\n label: \"Pricing table\",\n fields: {\n title: { type: \"text\", label: \"Heading\" },\n intro: { type: \"textarea\", label: \"Introduction\" },\n plans: {\n type: \"array\",\n label: \"Plans\",\n arrayFields: {\n name: { type: \"text\", label: \"Name\" },\n price: { type: \"text\", label: \"Price, such as $10\" },\n period: { type: \"text\", label: \"Per, such as a month\" },\n description: { type: \"textarea\", label: \"Description\" },\n features: { type: \"textarea\", label: \"What's included, one per line\" },\n buttonLabel: { type: \"text\", label: \"Button\" },\n buttonLink: { type: \"text\", label: \"Button links to\" },\n badge: { type: \"text\", label: \"Label at the top, such as Most popular (optional)\" },\n },\n defaultItemProps: emptyPlan,\n getItemSummary: (plan) => plan.name || \"Plan\",\n },\n className: classNameField,\n },\n defaultProps: {\n title: \"Prices\",\n intro: \"\",\n plans: [\n { ...emptyPlan, name: \"Basic\" },\n { ...emptyPlan, name: \"Standard\", price: \"$25\", badge: \"Most popular\" },\n { ...emptyPlan, name: \"Premium\", price: \"$50\" },\n ],\n className: \"\",\n },\n render: ({ title, intro, plans, className }) => (\n <section className={cx(\"mx-auto w-full max-w-6xl px-4 py-12\", className)}>\n {(title || intro) && (\n <div className=\"mb-8 flex flex-col items-center gap-3 text-center\">\n {title && <h2 className=\"text-3xl font-bold tracking-tight\">{title}</h2>}\n {intro && <p className=\"max-w-2xl text-lg text-muted-foreground\">{intro}</p>}\n </div>\n )}\n <div className=\"grid gap-6 md:grid-cols-2 lg:grid-cols-3\">\n {plans.map((plan, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: plans have no ids of their own\n <Card key={index} className={cx(plan.badge && \"border-primary border-2\")}>\n <CardHeader>\n <div className=\"flex items-center justify-between gap-2\">\n <CardTitle className=\"text-xl\">{plan.name}</CardTitle>\n {plan.badge && <Badge>{plan.badge}</Badge>}\n </div>\n {plan.description && <CardDescription>{plan.description}</CardDescription>}\n </CardHeader>\n <CardContent className=\"flex flex-1 flex-col gap-6\">\n <p>\n <span className=\"text-4xl font-bold\">{plan.price}</span>\n {plan.period && <span className=\"text-muted-foreground\"> {plan.period}</span>}\n </p>\n <ul className=\"flex flex-col gap-2\">\n {plan.features\n .split(\"\\n\")\n .map((feature) => feature.trim())\n .filter(Boolean)\n .map((feature) => (\n <li key={feature} className=\"flex items-start gap-2\">\n <CheckIcon aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0 text-primary\" />\n {feature}\n </li>\n ))}\n </ul>\n </CardContent>\n {plan.buttonLabel && (\n <CardFooter>\n <SiteLink\n href={plan.buttonLink || \"/\"}\n className={cx(buttonVariants({ variant: plan.badge ? \"default\" : \"outline\" }), \"w-full\")}\n >\n {plan.buttonLabel}\n </SiteLink>\n </CardFooter>\n )}\n </Card>\n ))}\n </div>\n </section>\n ),\n};\n\nexport default Pricing;\n"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"meta": {
|
|
23
|
+
"goodfellow": {
|
|
24
|
+
"category": "Page sections",
|
|
25
|
+
"version": "0.2.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "shadcn-tabs",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Tabs",
|
|
6
|
+
"description": "Content split into tabs, one shown at a time.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"@goodfellow/tabs"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "blocks/installed/shadcn-tabs/block.tsx",
|
|
13
|
+
"type": "registry:block",
|
|
14
|
+
"content": "import { classNameField, cx } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig, RichText } from \"@puckeditor/core\";\nimport { TabsView } from \"./tabs-view\";\n\ninterface Tab {\n label: string;\n content: RichText;\n}\n\nexport interface TabsProps {\n tabs: Tab[];\n className: string;\n}\n\n/** Content split into tabs, one shown at a time. Every tab's content is in the page, so search engines see it. */\nconst Tabs: ComponentConfig<TabsProps> = {\n label: \"Tabs\",\n fields: {\n tabs: {\n type: \"array\",\n label: \"Tabs\",\n arrayFields: {\n label: { type: \"text\", label: \"Tab name\" },\n content: { type: \"richtext\", label: \"Content\" },\n },\n defaultItemProps: { label: \"Tab\", content: \"<p>What this tab shows.</p>\" },\n getItemSummary: (tab) => tab.label || \"Tab\",\n },\n className: classNameField,\n },\n defaultProps: {\n tabs: [\n { label: \"First\", content: \"<p>What the first tab shows.</p>\" },\n { label: \"Second\", content: \"<p>What the second tab shows.</p>\" },\n ],\n className: \"\",\n },\n render: ({ tabs, className }) => (\n <section className={cx(\"mx-auto w-full max-w-4xl px-4 py-8\", className)}>\n <TabsView tabs={tabs.map(({ label, content }) => ({ label, content }))} />\n </section>\n ),\n};\n\nexport default Tabs;\n"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"path": "blocks/installed/shadcn-tabs/tabs-view.tsx",
|
|
18
|
+
"type": "registry:component",
|
|
19
|
+
"content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\n\n/** The tabs, with every tab's content in the page and the others hidden. */\nexport function TabsView({ tabs }: { tabs: Array<{ label: string; content: ReactNode }> }) {\n if (tabs.length === 0) return null;\n return (\n <Tabs defaultValue=\"tab-0\">\n <TabsList className=\"flex-wrap\">\n {tabs.map((tab, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: tabs have no ids of their own\n <TabsTrigger key={index} value={`tab-${index}`}>\n {tab.label}\n </TabsTrigger>\n ))}\n </TabsList>\n {tabs.map((tab, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: tabs have no ids of their own\n <TabsContent key={index} value={`tab-${index}`} forceMount className=\"pt-4 data-[state=inactive]:hidden\">\n <div className=\"gf-prose\">{tab.content}</div>\n </TabsContent>\n ))}\n </Tabs>\n );\n}\n"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"meta": {
|
|
23
|
+
"goodfellow": {
|
|
24
|
+
"category": "Interactive",
|
|
25
|
+
"recommended": true,
|
|
26
|
+
"version": "0.2.0"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"dependencies": []
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "shadcn-testimonials",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Testimonials",
|
|
6
|
+
"description": "Quotes from people, with their names and photos.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"@goodfellow/card"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "blocks/installed/shadcn-testimonials/block.tsx",
|
|
13
|
+
"type": "registry:block",
|
|
14
|
+
"content": "import { classNameField, cx, mediaField, SiteImage } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { Card, CardContent, CardFooter } from \"@/components/ui/card\";\n\ninterface Testimonial {\n quote: string;\n name: string;\n role: string;\n photo: string;\n}\n\nexport interface TestimonialsProps {\n title: string;\n testimonials: Testimonial[];\n className: string;\n}\n\n/** Quotes from people, with their names and photos. */\nconst Testimonials: ComponentConfig<TestimonialsProps> = {\n label: \"Testimonials\",\n fields: {\n title: { type: \"text\", label: \"Heading\" },\n testimonials: {\n type: \"array\",\n label: \"Quotes\",\n arrayFields: {\n quote: { type: \"textarea\", label: \"What they said\" },\n name: { type: \"text\", label: \"Name\" },\n role: { type: \"text\", label: \"Who they are, such as a member since 2020\" },\n photo: mediaField(\"Photo\"),\n },\n defaultItemProps: { quote: \"Something kind they said.\", name: \"A name\", role: \"\", photo: \"\" },\n getItemSummary: (item) => item.name || \"Quote\",\n },\n className: classNameField,\n },\n defaultProps: {\n title: \"What people say\",\n testimonials: [\n { quote: \"Something kind they said.\", name: \"A name\", role: \"\", photo: \"\" },\n { quote: \"Something else kind they said.\", name: \"Another name\", role: \"\", photo: \"\" },\n ],\n className: \"\",\n },\n render: ({ title, testimonials, className }) => (\n <section className={cx(\"mx-auto w-full max-w-6xl px-4 py-12\", className)}>\n {title && <h2 className=\"mb-8 text-3xl font-bold tracking-tight\">{title}</h2>}\n <div className=\"grid gap-6 md:grid-cols-2 lg:grid-cols-3\">\n {testimonials.map((item, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: quotes have no ids of their own\n <Card key={index}>\n <CardContent className=\"flex-1\">\n <blockquote className=\"text-lg leading-relaxed\">“{item.quote}”</blockquote>\n </CardContent>\n <CardFooter className=\"gap-3\">\n {item.photo && (\n <SiteImage src={item.photo} alt=\"\" className=\"size-10 shrink-0 rounded-full object-cover\" />\n )}\n <div className=\"flex flex-col\">\n <span className=\"font-semibold\">{item.name}</span>\n {item.role && <span className=\"text-sm text-muted-foreground\">{item.role}</span>}\n </div>\n </CardFooter>\n </Card>\n ))}\n </div>\n </section>\n ),\n};\n\nexport default Testimonials;\n"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"meta": {
|
|
18
|
+
"goodfellow": {
|
|
19
|
+
"category": "Page sections",
|
|
20
|
+
"recommended": true,
|
|
21
|
+
"version": "0.2.0"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"dependencies": []
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "tabs",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"dependencies": [
|
|
6
|
+
"cn",
|
|
7
|
+
"radix-ui"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "components/ui/tabs.tsx",
|
|
13
|
+
"type": "registry:ui",
|
|
14
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"cn\"\nimport { Tabs as TabsPrimitive } from \"radix-ui\"\n\nfunction Tabs({\n className,\n orientation = \"horizontal\",\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n return (\n <TabsPrimitive.Root\n data-slot=\"tabs\"\n data-orientation={orientation}\n orientation={orientation}\n className={cn(\n \"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col\",\n className\n )}\n {...props}\n />\n )\n}\n\nconst tabsListVariants = cva(\n \"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none\",\n {\n variants: {\n variant: {\n default: \"bg-muted\",\n line: \"gap-1 bg-transparent\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction TabsList({\n className,\n variant = \"default\",\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.List> &\n VariantProps<typeof tabsListVariants>) {\n return (\n <TabsPrimitive.List\n data-slot=\"tabs-list\"\n data-variant={variant}\n className={cn(tabsListVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nfunction TabsTrigger({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n return (\n <TabsPrimitive.Trigger\n data-slot=\"tabs-trigger\"\n className={cn(\n \"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n \"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent\",\n \"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground\",\n \"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TabsContent({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n return (\n <TabsPrimitive.Content\n data-slot=\"tabs-content\"\n className={cn(\"flex-1 outline-none\", className)}\n {...props}\n />\n )\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }\n"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Builds the site and publishes it with GitHub Pages whenever the main branch
|
|
2
|
+
# changes (including every Publish in the admin panel), and once a night so
|
|
3
|
+
# date-based content stays current. Each night it first installs Goodfellow's
|
|
4
|
+
# newest fixes, and publishes them only if the site still builds with them,
|
|
5
|
+
# unless content/updates.json turns automatic updates off.
|
|
6
|
+
# In the repository's Settings → Pages, set "Source" to "GitHub Actions".
|
|
7
|
+
# See README.md.
|
|
8
|
+
name: Deploy to GitHub Pages
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
branches: [main]
|
|
13
|
+
schedule:
|
|
14
|
+
- cron: "17 4 * * *"
|
|
15
|
+
workflow_dispatch:
|
|
16
|
+
inputs:
|
|
17
|
+
update:
|
|
18
|
+
description: "Update Goodfellow first: fixes, latest, or a release such as 0.5.0 (an older one goes back to it). Leave empty to only build."
|
|
19
|
+
required: false
|
|
20
|
+
default: ""
|
|
21
|
+
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
pages: write
|
|
25
|
+
id-token: write
|
|
26
|
+
|
|
27
|
+
concurrency:
|
|
28
|
+
group: pages
|
|
29
|
+
cancel-in-progress: false
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
update:
|
|
33
|
+
# Each night, and when the admin panel's Updates screen asks.
|
|
34
|
+
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.update != '')
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
permissions:
|
|
37
|
+
contents: write
|
|
38
|
+
outputs:
|
|
39
|
+
sha: ${{ steps.update.outputs.sha }}
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v5
|
|
42
|
+
- uses: actions/setup-node@v5
|
|
43
|
+
with:
|
|
44
|
+
node-version: 22
|
|
45
|
+
cache: npm
|
|
46
|
+
- run: npm ci
|
|
47
|
+
- id: update
|
|
48
|
+
env:
|
|
49
|
+
TO: ${{ inputs.update || 'automatic' }}
|
|
50
|
+
run: npx goodfellow update --publish --to "$TO"
|
|
51
|
+
|
|
52
|
+
build:
|
|
53
|
+
needs: update
|
|
54
|
+
# Also when there was no update to run, or it didn't work: the site is built either way.
|
|
55
|
+
if: ${{ !cancelled() }}
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v5
|
|
59
|
+
with:
|
|
60
|
+
# The update's commit, when it published one.
|
|
61
|
+
ref: ${{ needs.update.outputs.sha || github.sha }}
|
|
62
|
+
- uses: actions/setup-node@v5
|
|
63
|
+
with:
|
|
64
|
+
node-version: 22
|
|
65
|
+
cache: npm
|
|
66
|
+
- id: pages
|
|
67
|
+
uses: actions/configure-pages@v5
|
|
68
|
+
- run: npm ci
|
|
69
|
+
# Project sites live in a subfolder (/repository-name/); user and organization sites don't.
|
|
70
|
+
- run: npx goodfellow build --base "${{ steps.pages.outputs.base_path }}/"
|
|
71
|
+
- uses: actions/upload-pages-artifact@v4
|
|
72
|
+
with:
|
|
73
|
+
path: dist
|
|
74
|
+
|
|
75
|
+
deploy:
|
|
76
|
+
needs: build
|
|
77
|
+
if: ${{ !cancelled() && needs.build.result == 'success' }}
|
|
78
|
+
runs-on: ubuntu-latest
|
|
79
|
+
environment:
|
|
80
|
+
name: github-pages
|
|
81
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
82
|
+
steps:
|
|
83
|
+
- id: deployment
|
|
84
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Builds the site and publishes it with GitLab Pages whenever the default
|
|
2
|
+
# branch changes (including every Publish in the admin panel). On scheduled
|
|
3
|
+
# pipelines, which the admin panel's Updates screen sets up to run each night,
|
|
4
|
+
# it first installs Goodfellow's newest fixes, unless content/updates.json turns
|
|
5
|
+
# automatic updates off (or the release GOODFELLOW_UPDATE names), and publishes
|
|
6
|
+
# them only if the site still builds with them. That
|
|
7
|
+
# needs "Allow Git push requests to the repository" under Settings → CI/CD →
|
|
8
|
+
# Job token permissions, which the Updates screen turns on. See README.md.
|
|
9
|
+
pages:
|
|
10
|
+
image: node:22
|
|
11
|
+
cache:
|
|
12
|
+
key:
|
|
13
|
+
files: [package-lock.json]
|
|
14
|
+
paths: [.npm/]
|
|
15
|
+
script:
|
|
16
|
+
- npm ci --cache .npm --prefer-offline
|
|
17
|
+
- |
|
|
18
|
+
if [ "$CI_PIPELINE_SOURCE" = "schedule" ] || [ -n "$GOODFELLOW_UPDATE" ]; then
|
|
19
|
+
git remote set-url origin "https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_FQDN:-$CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
|
|
20
|
+
npx goodfellow update --publish --to "${GOODFELLOW_UPDATE:-automatic}" || echo "The update wasn't published; building the site as it is."
|
|
21
|
+
fi
|
|
22
|
+
# Pages may serve the site from a subfolder, such as https://group.gitlab.io/site/.
|
|
23
|
+
- export GOODFELLOW_BASE="$(node -e 'console.log(new URL(process.env.CI_PAGES_URL).pathname)')"
|
|
24
|
+
- npx goodfellow build
|
|
25
|
+
pages:
|
|
26
|
+
publish: dist
|
|
27
|
+
rules:
|
|
28
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# My site
|
|
2
|
+
|
|
3
|
+
A website built with [Goodfellow](https://github.com/goodfellow-cms/goodfellow-cms). Pages are files in `content/`, edited in the admin panel at `/admin`, and published to a free static host.
|
|
4
|
+
|
|
5
|
+
## Working on the site
|
|
6
|
+
|
|
7
|
+
<!-- goodfellow-repository -->
|
|
8
|
+
> **In the Goodfellow repository**, this folder is part of a pnpm workspace, so `npm install` doesn't work here. From the repository's root, run `pnpm install` and `pnpm build` (get pnpm with `corepack enable`, or `npm install -g pnpm`), then `pnpm dev` in this folder. The `npm` commands below are for a site of your own, created with `npm create goodfellow`, which leaves this note out.
|
|
9
|
+
<!-- /goodfellow-repository -->
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install
|
|
13
|
+
npm run dev # site at http://localhost:4321, admin panel at http://localhost:4321/admin
|
|
14
|
+
npm run build # builds the site into dist/
|
|
15
|
+
npm run preview # serves dist/
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
On your own computer, the admin panel saves straight to the files in `content/`.
|
|
19
|
+
|
|
20
|
+
More blocks, such as an FAQ, tabs and a pricing table, can be added on the admin panel's **Blocks** screen, which publishes their code into `blocks/installed/` and the shadcn components they use into `components/ui/`. See [Add blocks](https://goodfellow-cms.github.io/goodfellow-cms/docs/add-blocks).
|
|
21
|
+
|
|
22
|
+
To add blocks of your own, write React components and add them to `blocks` in `goodfellow.config.tsx`. Parts that respond to visitors, such as a button that shows more, go in a file starting with `"use client"`, and only pages that use them load JavaScript: see [Interactive blocks](https://goodfellow-cms.github.io/goodfellow-cms/docs/interactive-blocks).
|
|
23
|
+
|
|
24
|
+
The News page and its stories are an example of a collection: a group of similar items that share one page design. Change it or delete it under **Collections** in the admin panel.
|
|
25
|
+
|
|
26
|
+
## Putting the site online
|
|
27
|
+
|
|
28
|
+
Setup takes four steps. It's done once, usually by whoever set up the site.
|
|
29
|
+
|
|
30
|
+
### 1. Store the site on GitHub or GitLab
|
|
31
|
+
|
|
32
|
+
Create an empty repository on GitHub or GitLab, without a README, and push this folder to it:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
git remote add origin https://github.com/your-name/your-site.git # unless origin is already set
|
|
36
|
+
git push -u origin main
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`create-goodfellow` makes the new site a git repository on the branch `main`, with everything, `package-lock.json` included, in its first commit, and sets `origin` when it's given the repository. Commit any changes since (`git add -A` and `git commit`) before pushing.
|
|
40
|
+
|
|
41
|
+
### 2. Tell Goodfellow where the site is stored
|
|
42
|
+
|
|
43
|
+
If you gave the repository when creating the site with `npm create goodfellow`, this is already done. Otherwise, in `goodfellow.config.tsx`, uncomment one `backend` line and its `import`, and fill in your repository:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import { github } from "@goodfellow-cms/github";
|
|
47
|
+
|
|
48
|
+
export default defineConfig({
|
|
49
|
+
blocks,
|
|
50
|
+
categories,
|
|
51
|
+
backend: github({ repo: "your-name/your-site" }),
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Builds include the admin panel at `/admin` only once a backend is set. If the live site is built from a branch other than the repository's default branch, add `branch: "name"`.
|
|
56
|
+
|
|
57
|
+
Also set the site's address under **Site settings → General → Site address**, including any subfolder (such as `https://your-name.github.io/your-site`). It's used for the sitemap and for link previews.
|
|
58
|
+
|
|
59
|
+
### 3. Choose a host
|
|
60
|
+
|
|
61
|
+
Each host's free plan has its own rules about business use. If the site is for a business, sells anything, shows ads, or is built or looked after by someone who's paid for it, check those rules first. As of October 2026, GitHub Pages and Vercel's free plan don't allow most of these, and GitLab Pages has no rule against them that we know of. See [Hosts and business sites](https://goodfellow-cms.github.io/goodfellow-cms/docs/hosts) in Goodfellow's documentation.
|
|
62
|
+
|
|
63
|
+
**GitHub Pages** (free for public repositories; not for online businesses or shops)
|
|
64
|
+
|
|
65
|
+
1. In the repository on GitHub, go to **Settings → Pages** and set **Source** to **GitHub Actions**.
|
|
66
|
+
2. Push. `.github/workflows/deploy.yml` builds and publishes the site on every change and once a night. If your main branch isn't called `main`, change it in that file.
|
|
67
|
+
|
|
68
|
+
**GitLab Pages** (free, including for private projects; no rule against business sites that we know of)
|
|
69
|
+
|
|
70
|
+
1. Push. `.gitlab-ci.yml` builds and publishes the site on every change to the default branch.
|
|
71
|
+
2. For nightly rebuilds, add a schedule under **Build → Pipeline schedules**.
|
|
72
|
+
3. The site's address is under **Deploy → Pages**.
|
|
73
|
+
|
|
74
|
+
**Vercel** (works with GitHub and GitLab; the free plan is for non-commercial sites only)
|
|
75
|
+
|
|
76
|
+
1. Import the repository in Vercel. `vercel.ts` already has the right settings, and has Vercel send the site's redirects itself.
|
|
77
|
+
2. For nightly rebuilds, create a deploy hook under **Settings → Git → Deploy Hooks**, and call it from a scheduled GitHub Action or GitLab pipeline schedule.
|
|
78
|
+
|
|
79
|
+
Delete the setup files for the hosts you don't use.
|
|
80
|
+
|
|
81
|
+
### 4. Set up sign-in
|
|
82
|
+
|
|
83
|
+
**GitHub:** nothing to set up. Editors sign in at `/admin` with an access token. The admin panel links to GitHub's token page with the right permissions already chosen. On that page, under **Repository access**, choose **Only select repositories** and pick the site.
|
|
84
|
+
|
|
85
|
+
- Editors who are collaborators on someone else's personal repository can't use that kind of token. They can use the "broader token" link instead. Alternatively, move the repository into a free GitHub organization and make editors members.
|
|
86
|
+
|
|
87
|
+
**GitLab:** editors can sign in with an access token straight away. For one-click **Sign in with GitLab**, register the admin panel with GitLab once:
|
|
88
|
+
|
|
89
|
+
1. Go to your profile's **Preferences → Applications** (or the group's **Settings → Applications**) and add an application.
|
|
90
|
+
2. Set:
|
|
91
|
+
- **Redirect URI:** the admin panel's address with a trailing slash, such as `https://your-site.example/admin/`.
|
|
92
|
+
- **Confidential:** leave this **off**.
|
|
93
|
+
- **Scopes:** `api`.
|
|
94
|
+
3. Copy the **Application ID** into the config: `gitlab({ project: "your-name/your-site", clientId: "…" })`.
|
|
95
|
+
|
|
96
|
+
Every editor needs permission to change the repository: on GitHub, write access; on GitLab, the Developer role or higher.
|
|
97
|
+
|
|
98
|
+
## Images and files
|
|
99
|
+
|
|
100
|
+
Upload images and files on the admin panel's **Media** screen, or with the **Choose image** button wherever an image goes. They're saved in `public/media/`, and large photos are made web-sized automatically.
|
|
101
|
+
|
|
102
|
+
## Writing with AI
|
|
103
|
+
|
|
104
|
+
The admin panel's **AI** tab can draft pages, rewrite blocks and fill in news stories. Each editor picks an AI service there. Claude and some others need the editor's own API key and charge for use; there are free options, including copying the request into a chat app such as Claude.ai. To offer only some services, or to turn the assistant off, see `ai` in [Configuration](https://goodfellow-cms.github.io/goodfellow-cms/docs/configuration).
|
|
105
|
+
|
|
106
|
+
## Publishing
|
|
107
|
+
|
|
108
|
+
Each **Publish** in the admin panel saves its changes as one commit to the main branch, which starts a new build. The admin panel shows when the live site has been updated, usually within a minute or two. If someone else changed the same page in the meantime, publishing stops and explains what happened, instead of overwriting their work.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Written by the admin panel when blocks are installed or removed. Don't edit it by hand.
|
|
2
|
+
import type { ComponentConfig, Config } from "@puckeditor/core";
|
|
3
|
+
|
|
4
|
+
// biome-ignore lint/suspicious/noExplicitAny: blocks have arbitrary props
|
|
5
|
+
export const installedBlocks: Record<string, ComponentConfig<any>> = {};
|
|
6
|
+
|
|
7
|
+
export const installedCategories: NonNullable<Config["categories"]> = {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/styles.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"aliases": {
|
|
15
|
+
"components": "@/components",
|
|
16
|
+
"utils": "@/lib/utils",
|
|
17
|
+
"ui": "@/components/ui",
|
|
18
|
+
"lib": "@/lib",
|
|
19
|
+
"hooks": "@/hooks"
|
|
20
|
+
},
|
|
21
|
+
"registries": {
|
|
22
|
+
"@goodfellow": "https://cdn.jsdelivr.net/npm/@goodfellow-cms/registry@latest/r/{name}.json"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"entryName": "News story",
|
|
4
|
+
"fields": [
|
|
5
|
+
{
|
|
6
|
+
"type": "text",
|
|
7
|
+
"label": "Title",
|
|
8
|
+
"name": "title",
|
|
9
|
+
"required": true
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"type": "date",
|
|
13
|
+
"label": "Date",
|
|
14
|
+
"name": "date",
|
|
15
|
+
"required": true
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"type": "textarea",
|
|
19
|
+
"hint": "One or two sentences, shown in lists of news.",
|
|
20
|
+
"label": "Summary",
|
|
21
|
+
"name": "summary"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "image",
|
|
25
|
+
"label": "Image",
|
|
26
|
+
"name": "image"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"type": "richtext",
|
|
30
|
+
"label": "Text",
|
|
31
|
+
"name": "text"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"name": "News",
|
|
35
|
+
"path": "/news/{slug}",
|
|
36
|
+
"sort": {
|
|
37
|
+
"field": "date",
|
|
38
|
+
"order": "desc"
|
|
39
|
+
},
|
|
40
|
+
"template": {
|
|
41
|
+
"content": [
|
|
42
|
+
{
|
|
43
|
+
"type": "Section",
|
|
44
|
+
"props": {
|
|
45
|
+
"id": "Section-news",
|
|
46
|
+
"background": "none",
|
|
47
|
+
"backgroundImage": "",
|
|
48
|
+
"className": "",
|
|
49
|
+
"content": [
|
|
50
|
+
{
|
|
51
|
+
"type": "EntryField",
|
|
52
|
+
"props": {
|
|
53
|
+
"id": "EntryField-date",
|
|
54
|
+
"className": "",
|
|
55
|
+
"field": "date",
|
|
56
|
+
"style": "small"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"type": "EntryField",
|
|
61
|
+
"props": {
|
|
62
|
+
"id": "EntryField-title",
|
|
63
|
+
"className": "mt-2",
|
|
64
|
+
"field": "title",
|
|
65
|
+
"style": "title"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"type": "EntryField",
|
|
70
|
+
"props": {
|
|
71
|
+
"id": "EntryField-image",
|
|
72
|
+
"className": "mt-8",
|
|
73
|
+
"field": "image",
|
|
74
|
+
"style": "text"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"type": "EntryField",
|
|
79
|
+
"props": {
|
|
80
|
+
"id": "EntryField-text",
|
|
81
|
+
"className": "mt-8",
|
|
82
|
+
"field": "text",
|
|
83
|
+
"style": "text"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"type": "Button",
|
|
88
|
+
"props": {
|
|
89
|
+
"id": "Button-news",
|
|
90
|
+
"className": "mt-10",
|
|
91
|
+
"href": "/news",
|
|
92
|
+
"label": "All news",
|
|
93
|
+
"newTab": false,
|
|
94
|
+
"size": "md",
|
|
95
|
+
"variant": "outline"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"padding": "lg",
|
|
100
|
+
"width": "narrow"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"root": {
|
|
105
|
+
"props": {
|
|
106
|
+
"className": "",
|
|
107
|
+
"description": "{summary}",
|
|
108
|
+
"image": "{image}",
|
|
109
|
+
"title": "{title}"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"fields": {
|
|
4
|
+
"date": "2026-10-01",
|
|
5
|
+
"image": "/media/placeholder.svg",
|
|
6
|
+
"summary": "Our new site is easier to keep up to date, so you'll always find the latest news here.",
|
|
7
|
+
"text": "<p>This is a news story. Add your own on the <strong>Collections</strong> screen of the admin panel, and they'll appear on the news page in date order.</p>",
|
|
8
|
+
"title": "Welcome to our new website"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"data": {
|
|
4
|
+
"content": [
|
|
5
|
+
{
|
|
6
|
+
"type": "Section",
|
|
7
|
+
"props": {
|
|
8
|
+
"id": "Section-footer",
|
|
9
|
+
"background": "muted",
|
|
10
|
+
"backgroundImage": "",
|
|
11
|
+
"className": "",
|
|
12
|
+
"content": [
|
|
13
|
+
{
|
|
14
|
+
"type": "Flex",
|
|
15
|
+
"props": {
|
|
16
|
+
"id": "Flex-footer",
|
|
17
|
+
"align": "center",
|
|
18
|
+
"className": "",
|
|
19
|
+
"direction": "row",
|
|
20
|
+
"gap": "md",
|
|
21
|
+
"items": [
|
|
22
|
+
{
|
|
23
|
+
"type": "Text",
|
|
24
|
+
"props": {
|
|
25
|
+
"id": "Text-footer",
|
|
26
|
+
"align": "left",
|
|
27
|
+
"className": "text-sm text-muted-foreground",
|
|
28
|
+
"content": "<p>© My site. Built with Goodfellow.</p>"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "Menu",
|
|
33
|
+
"props": {
|
|
34
|
+
"id": "Menu-footer",
|
|
35
|
+
"className": "text-sm",
|
|
36
|
+
"menu": "footer",
|
|
37
|
+
"orientation": "horizontal"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"justify": "between",
|
|
42
|
+
"wrap": true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"padding": "sm",
|
|
47
|
+
"width": "wide"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"root": {
|
|
52
|
+
"props": {}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|