Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,9 @@ You can now use the `--advanced` flag when running the `create` command to get a
- [Tailwind](https://tailwindcss.com/) css framework
- Docker configuration for go project
- [React](https://react.dev/) frontend written in TypeScript, including an example fetch request to the backend
- [Next.js](https://nextjs.org/) frontend (App Router + TypeScript) with [Tailwind CSS](https://tailwindcss.com/) and [shadcn/ui](https://ui.shadcn.com/) pre-wired, including an example fetch to the Go backend

Note: Selecting Tailwind option will automatically select HTMX unless React is explicitly selected
Note: Selecting Tailwind option will automatically select HTMX unless React or Next.js is explicitly selected. React, Next.js, and HTMX are mutually exclusive — only one frontend option can be chosen at a time.

<a id="blueprint-ui"></a>

Expand Down Expand Up @@ -214,10 +215,18 @@ React:
go-blueprint create --advanced --feature react
```

Or all features at once:
Next.js (App Router, TypeScript, Tailwind, shadcn/ui — all bundled):

```bash
go-blueprint create --name my-project --framework chi --driver mysql --advanced --feature htmx --feature githubaction --feature websocket --feature tailwind --feature docker --git commit --feature react
go-blueprint create --advanced --feature nextjs
```

The Next.js scaffold runs `create-next-app@latest` with `--ts --app --eslint --src-dir --tailwind --use-npm --import-alias "@/*"`, then initializes shadcn/ui (`init -d`) and adds the `button` and `card` components. A `frontend/.env.local` is created with `NEXT_PUBLIC_API_URL` pointing at the Go backend, and `next.config.mjs` is configured with `output: "standalone"` plus an `/api/*` → Go rewrite so server-side fetches can hit the backend without CORS. When combined with `--feature docker`, the frontend runs as its own compose service on port `3000`.

Or all features at once (picking Next.js as the frontend):

```bash
go-blueprint create --name my-project --framework chi --driver mysql --advanced --feature nextjs --feature githubaction --feature websocket --feature docker --git commit
```

<p align="center">
Expand Down
9 changes: 9 additions & 0 deletions cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,15 @@ var createCmd = &cobra.Command{
fmt.Println(endingMsgStyle.Render("\nNext steps:"))
fmt.Println(endingMsgStyle.Render(fmt.Sprintf("• cd into the newly created project with: `cd %s`\n", utils.GetRootDir(project.ProjectName))))

if options.Advanced.Choices["Nextjs"] {
options.Advanced.Choices["React"] = false
options.Advanced.Choices["Htmx"] = false
options.Advanced.Choices["Tailwind"] = false
fmt.Println(endingMsgStyle.Render("• cd into frontend\n"))
fmt.Println(endingMsgStyle.Render("• npm install\n"))
fmt.Println(endingMsgStyle.Render("• npm run dev\n"))
}

if options.Advanced.Choices["React"] {
options.Advanced.Choices["Htmx"] = false
options.Advanced.Choices["Tailwind"] = false
Expand Down
3 changes: 2 additions & 1 deletion cmd/flags/advancedFeatures.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ const (
Websocket string = "websocket"
Tailwind string = "tailwind"
React string = "react"
Nextjs string = "nextjs"
Docker string = "docker"
)

var AllowedAdvancedFeatures = []string{string(React), string(Htmx), string(GoProjectWorkflow), string(Websocket), string(Tailwind), string(Docker)}
var AllowedAdvancedFeatures = []string{string(React), string(Nextjs), string(Htmx), string(GoProjectWorkflow), string(Websocket), string(Tailwind), string(Docker)}

func (f AdvancedFeatures) String() string {
return strings.Join(f, ",")
Expand Down
105 changes: 105 additions & 0 deletions cmd/program/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,17 @@ func (p *Project) CreateMainFile() error {
return err
}

if p.AdvancedOptions[string(flags.Nextjs)] {
// deselect htmx/react since nextjs is selected
p.AdvancedOptions[string(flags.Htmx)] = false
p.AdvancedOptions[string(flags.React)] = false
if err := p.CreateNextJSProject(projectPath); err != nil {
return fmt.Errorf("failed to set up Next.js project: %w", err)
}
// tailwind is baked into the Next scaffold; skip downstream tailwind-for-htmx block
p.AdvancedOptions[string(flags.Tailwind)] = false
}

if p.AdvancedOptions[string(flags.React)] {
// deselect htmx option automatically since react is selected
p.AdvancedOptions[string(flags.Htmx)] = false
Expand Down Expand Up @@ -932,6 +943,100 @@ func (p *Project) CreateViteReactProject(projectPath string) error {
return nil
}

func (p *Project) CreateNextJSProject(projectPath string) error {
if err := checkNpmInstalled(); err != nil {
return err
}

originalDir, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %w", err)
}
defer func() {
if err := os.Chdir(originalDir); err != nil {
fmt.Fprintf(os.Stderr, "failed to change back to original directory: %v\n", err)
}
}()

if err := os.Chdir(projectPath); err != nil {
return fmt.Errorf("failed to change into project directory: %w", err)
}

fmt.Println("Scaffolding Next.js app with create-next-app...")
cmd := exec.Command("npx", "--yes", "create-next-app@latest", "frontend",
"--ts",
"--app",
"--eslint",
"--src-dir",
"--tailwind",
"--use-npm",
"--import-alias", "@/*",
"--no-turbopack",
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to run create-next-app: %w", err)
}

frontendPath := filepath.Join(projectPath, "frontend")
if err := os.Chdir(frontendPath); err != nil {
return fmt.Errorf("failed to change to frontend directory: %w", err)
}

// Create global .env so we can read the backend PORT.
if err := p.CreateFileWithInjection("", projectPath, ".env", "env"); err != nil {
return fmt.Errorf("failed to create global .env file: %w", err)
}

backendPort := "8080"
if data, readErr := os.ReadFile(filepath.Join(projectPath, ".env")); readErr == nil {
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "PORT=") {
backendPort = strings.SplitN(line, "=", 2)[1]
break
}
}
}

frontendEnvContent := fmt.Sprintf("NEXT_PUBLIC_API_URL=http://localhost:%s\n", backendPort)
if err := os.WriteFile(filepath.Join(frontendPath, ".env.local"), []byte(frontendEnvContent), 0644); err != nil {
return fmt.Errorf("failed to create frontend .env.local: %w", err)
}

// Overwrite next.config with one that proxies /api/* to the Go backend and enables standalone output.
nextConfigPath := filepath.Join(frontendPath, "next.config.mjs")
if err := os.WriteFile(nextConfigPath, advanced.NextJSConfigFile(), 0644); err != nil {
return fmt.Errorf("failed to write next.config.mjs: %w", err)
}
// create-next-app may emit next.config.ts as well — remove it so .mjs wins.
_ = os.Remove(filepath.Join(frontendPath, "next.config.ts"))

// Initialize shadcn/ui with defaults and add baseline components.
fmt.Println("Initializing shadcn/ui...")
initCmd := exec.Command("npx", "--yes", "shadcn@latest", "init", "-d", "-y")
initCmd.Stdout = os.Stdout
initCmd.Stderr = os.Stderr
if err := initCmd.Run(); err != nil {
return fmt.Errorf("failed to init shadcn/ui: %w", err)
}

addCmd := exec.Command("npx", "--yes", "shadcn@latest", "add", "button", "card", "-y")
addCmd.Stdout = os.Stdout
addCmd.Stderr = os.Stderr
if err := addCmd.Run(); err != nil {
return fmt.Errorf("failed to add shadcn components: %w", err)
}

// Overwrite the starter page with one that uses our shadcn Button + Card and fetches from Go.
pagePath := filepath.Join(frontendPath, "src", "app", "page.tsx")
if err := os.WriteFile(pagePath, advanced.NextJSPageFile(), 0644); err != nil {
return fmt.Errorf("failed to write page.tsx template: %w", err)
}

return nil
}

func (p *Project) CreateHtmxTemplates() {
routesPlaceHolder := ""
importsPlaceHolder := ""
Expand Down
7 changes: 6 additions & 1 deletion cmd/steps/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,12 @@ func InitSteps(projectType flags.Framework, databaseType flags.Database) *Steps
{
Flag: "React",
Title: "React",
Desc: "Use Vite to spin up a React project in TypeScript. This disables selecting HTMX/Templ",
Desc: "Use Vite to spin up a React project in TypeScript. This disables selecting HTMX/Templ and Next.js",
},
{
Flag: "Nextjs",
Title: "Next.js",
Desc: "Scaffold a Next.js app (App Router, TypeScript, Tailwind, shadcn/ui). This disables selecting HTMX/Templ and React",
},
{
Flag: "Htmx",
Expand Down
14 changes: 14 additions & 0 deletions cmd/template/advanced/files/docker/docker_compose.yml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ services:
depends_on:
- app
{{- end }}
{{- if .AdvancedOptions.nextjs }}
frontend:
build:
context: .
dockerfile: Dockerfile
target: frontend
restart: unless-stopped
ports:
- 3000:3000
environment:
NEXT_PUBLIC_API_URL: http://app:${PORT}
depends_on:
- app
{{- end }}

{{- if and (.AdvancedOptions.docker) (eq .DBDriver "sqlite") }}
volumes:
Expand Down
20 changes: 20 additions & 0 deletions cmd/template/advanced/files/docker/dockerfile.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,23 @@ COPY --from=frontend_builder /frontend/dist /app/dist
EXPOSE 5173
CMD ["serve", "-s", "/app/dist", "-l", "5173"]
{{- end}}

{{ if .AdvancedOptions.nextjs}}
FROM node:20 AS frontend_builder
WORKDIR /frontend

COPY frontend/package*.json ./
RUN npm ci
COPY frontend/. .
RUN npm run build

FROM node:20-slim AS frontend
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
COPY --from=frontend_builder /frontend/.next/standalone ./
COPY --from=frontend_builder /frontend/.next/static ./.next/static
COPY --from=frontend_builder /frontend/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
{{- end}}
15 changes: 15 additions & 0 deletions cmd/template/advanced/files/nextjs/next.config.mjs.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
async rewrites() {
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8080"
return [
{
source: "/api/:path*",
destination: `${apiUrl}/:path*`,
},
]
},
}

export default nextConfig
56 changes: 56 additions & 0 deletions cmd/template/advanced/files/nextjs/page.tsx.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"

export default function Home() {
const [message, setMessage] = useState<string>("")
const [loading, setLoading] = useState<boolean>(false)

const apiUrl =
process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8080"

const fetchData = async () => {
setLoading(true)
try {
const res = await fetch(`${apiUrl}/`)
setMessage(await res.text())
} catch (err) {
console.error("Error fetching data:", err)
setMessage("Failed to reach Go backend")
} finally {
setLoading(false)
}
}

return (
<main className="flex min-h-screen flex-col items-center justify-center gap-6 p-8">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Next.js + Go Blueprint</CardTitle>
<CardDescription>
Talking to the Go backend at {apiUrl}
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Button onClick={fetchData} disabled={loading}>
{loading ? "Fetching…" : "Fetch from Go server"}
</Button>
{message && (
<div className="rounded-md bg-muted p-3 text-sm">
<p className="font-semibold">Server response:</p>
<p className="whitespace-pre-wrap">{message}</p>
</div>
)}
</CardContent>
</Card>
</main>
)
}
14 changes: 14 additions & 0 deletions cmd/template/advanced/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ var reactTailwindAppFile []byte
//go:embed files/react/app.tsx.tmpl
var reactAppFile []byte

//go:embed files/nextjs/page.tsx.tmpl
var nextjsPageFile []byte

//go:embed files/nextjs/next.config.mjs.tmpl
var nextjsConfigFile []byte

//go:embed files/tailwind/input.css.tmpl
var inputCssTemplate []byte

Expand Down Expand Up @@ -127,6 +133,14 @@ func ReactAppfile() []byte {
return reactAppFile
}

func NextJSPageFile() []byte {
return nextjsPageFile
}

func NextJSConfigFile() []byte {
return nextjsConfigFile
}

func InputCssTemplateReact() []byte {
return inputCssTemplateReact
}
Expand Down
8 changes: 4 additions & 4 deletions cmd/template/framework/files/makefile.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ tailwind-install:
@if not exist tailwindcss.exe powershell -ExecutionPolicy Bypass -Command "Invoke-WebRequest -Uri 'https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-windows-x64.exe' -OutFile 'tailwindcss.exe'"{{- end }}
{{- end }}

build:{{- if and .AdvancedOptions.tailwind (not .AdvancedOptions.react) }} tailwind-install{{- end }}{{- if and (or .AdvancedOptions.htmx .AdvancedOptions.tailwind) (not .AdvancedOptions.react) }} templ-install{{- end }}
build:{{- if and .AdvancedOptions.tailwind (not .AdvancedOptions.react) (not .AdvancedOptions.nextjs) }} tailwind-install{{- end }}{{- if and (or .AdvancedOptions.htmx .AdvancedOptions.tailwind) (not .AdvancedOptions.react) (not .AdvancedOptions.nextjs) }} templ-install{{- end }}
@echo "Building..."
{{ if and (or .AdvancedOptions.htmx .AdvancedOptions.tailwind) (not .AdvancedOptions.react) }}@templ generate{{- end }}
{{ if and .AdvancedOptions.tailwind (not .AdvancedOptions.react) }}@{{ if .OSCheck.UnixBased }}./tailwindcss{{ else }}.\tailwindcss.exe{{ end }} -i cmd/web/styles/input.css -o cmd/web/assets/css/output.css{{ end }}
{{ if and (or .AdvancedOptions.htmx .AdvancedOptions.tailwind) (not .AdvancedOptions.react) (not .AdvancedOptions.nextjs) }}@templ generate{{- end }}
{{ if and .AdvancedOptions.tailwind (not .AdvancedOptions.react) (not .AdvancedOptions.nextjs) }}@{{ if .OSCheck.UnixBased }}./tailwindcss{{ else }}.\tailwindcss.exe{{ end }} -i cmd/web/styles/input.css -o cmd/web/assets/css/output.css{{ end }}
{{ if .OSCheck.UnixBased }}@{{- if and (.AdvancedOptions.docker) (eq .DBDriver "sqlite") }}CGO_ENABLED=1 GOOS=linux {{ end }}go build -o main cmd/api/main.go{{- else }}@go build -o main.exe cmd/api/main.go{{- end }}

# Run the application
run:
@go run cmd/api/main.go{{- if .AdvancedOptions.react }} &
@go run cmd/api/main.go{{- if or .AdvancedOptions.react .AdvancedOptions.nextjs }} &
@npm install --prefer-offline --no-fund --prefix ./frontend
@npm run dev --prefix ./frontend
{{- end }}
Expand Down
Loading
Loading