Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8130653
ft: Implement blog setup
descholar-ceo May 11, 2026
8534c1b
Complete keystatic setup
descholar-ceo May 11, 2026
db47b39
Test the build
descholar-ceo May 11, 2026
d9a064e
Fix text readability
descholar-ceo May 11, 2026
35eea4a
Fix fonts
descholar-ceo May 11, 2026
199649d
bg: Fix seo centralization
descholar-ceo May 14, 2026
3d771ed
Fix synthax
descholar-ceo May 14, 2026
555044b
Fix llm file generation
descholar-ceo May 14, 2026
7d74d84
Fix the site
descholar-ceo May 14, 2026
94c5f78
Fix the light theme
descholar-ceo May 14, 2026
f4ecc13
Fix titles
descholar-ceo May 14, 2026
1d73924
Fix article hearder
descholar-ceo May 14, 2026
9875a35
Fix keystatic
descholar-ceo May 14, 2026
c74f2f3
Fix the first article
descholar-ceo May 14, 2026
b72249a
FIx code renderer
descholar-ceo May 14, 2026
d27a412
Fix wording
descholar-ceo May 14, 2026
0f910db
Update readme file
descholar-ceo May 15, 2026
b2ae8d7
Improve the whitepaper
descholar-ceo May 16, 2026
3a9f822
Improve SEO
descholar-ceo May 17, 2026
5b748b6
Add missing pages
descholar-ceo May 17, 2026
e45445d
Fill all gaps regarding the docs
descholar-ceo May 17, 2026
8ee63ce
Fill out the SEO gaps
descholar-ceo May 17, 2026
3042f8a
Remove verification requirements
descholar-ceo May 17, 2026
e784be7
Fix deps
descholar-ceo May 17, 2026
345db71
Document the refactor command
descholar-ceo May 18, 2026
174ece6
Fix synthax
descholar-ceo May 18, 2026
0947a45
Fix inconsistncy in docs
descholar-ceo May 18, 2026
d491d46
Complete documentation
descholar-ceo May 18, 2026
4d93f61
Fix inconsistencies about related pages
descholar-ceo May 18, 2026
f55c4d6
Complete blogs
descholar-ceo Jul 2, 2026
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,14 @@ The home page hits target. The doc-page performance gap is dominated by Nextra v
- **`color-contrast` audit on the landing's ambient code spans** (`gofasta-code-breath`). The spans are decorative atmosphere set inside an `aria-hidden="true"` parent, with each span individually marked `aria-hidden="true"` as well. Lighthouse / axe-core still flags them because contrast checks run on visually-rendered text regardless of `aria-hidden` (the rule is about low-vision sighted users, not screen readers). The contrast is design-intentional. Score impact: −4 on accessibility for the home page.
- **`unused-javascript`** opportunities (~450ms on home, ~600ms on docs). Most of this is Next.js / Nextra runtime code that is genuinely loaded; some is React Compiler emit. Aggressively code-splitting beyond what Next does by default usually trades one Lighthouse number for another.


## Maintenance and sustainability

Gofasta is currently maintained by one person; sustainability planning — release cadence, security SLOs, the solo-to-team transition, and the automation arc that retires manual steps as the project matures — is documented in the [release coordination repo](https://github.com/gofastadev/release), specifically in [`CADENCE.md`](https://github.com/gofastadev/release/blob/main/CADENCE.md), [`RELEASING.md`](https://github.com/gofastadev/release/blob/main/RELEASING.md), and [`COMMUNITY.md`](https://github.com/gofastadev/release/blob/main/COMMUNITY.md). Read those three together for the full picture.

## Contributing

Contributions are welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md).

## License

MIT
90 changes: 90 additions & 0 deletions data/blog/hello-gofasta.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
title: Hello, Gofasta!
description: >-
What the Gofasta toolkit ships, what a freshly scaffolded project actually
contains, and why every line of generated code is plain Go you own.
publishedAt: 2026-05-10T10:00:00.000Z
author: Gofasta Team
authorUrl: https://gofasta.dev
tags:
- golang
- toolkit
- agent-native
cover: /blog/covers/hello-gofasta/cover.png
---
# Hello, Gofasta!

Starting a production Go backend is mostly assembly work. You pick a router, wire structured logging, choose a database client, hook up migrations, write CRUD layers for every new resource, set up authentication, prepare a Dockerfile, decide where models and handlers live. None of it is business logic. All of it has to exist before the first feature ships. Most teams pay that cost from scratch on every project.

Gofasta is what we built so that cost gets paid once.

## What gofasta is

Gofasta is a Go backend toolkit. Two pieces, both open source under the MIT license:

* **A CLI** at [`github.com/gofastadev/cli`](https://github.com/gofastadev/cli). Install it with `go install github.com/gofastadev/cli/cmd/gofasta@latest`. It scaffolds new projects, generates full CRUD layers from a single command, runs migrations, regenerates dependency injection, and reports drift in your tree. It never imports the library at runtime — it only writes files to disk.
* **A library** at [`github.com/gofastadev/gofasta`](https://github.com/gofastadev/gofasta). Packages under `pkg/*` covering the concerns every backend ends up needing: configuration loading, JWT authentication, caching, structured errors, HTTP middleware, mailer, multi-channel notifications, WebSockets, scheduled jobs, async task queues, observability, feature flags, file storage, and a few more. Each package is independently importable; there is no umbrella that pulls everything in. The few cross-package dependencies are functional, not structural — `pkg/health` pings the database and `pkg/cache` because that is the job of a health probe.

A project produced by `gofasta new` is plain, idiomatic Go that you own. The HTTP server in `cmd/serve.go` is a standard `net/http` listener with [`chi`](https://github.com/go-chi/chi) for routing. Handlers return errors. Models are GORM structs. Dependency injection is resolved at compile time by [Google Wire](https://github.com/google/wire). You can read every line top to bottom and trace exactly what runs.

## What gofasta isn't

It isn't a framework. There is no central application object you register handlers into, no inversion-of-control runtime, no lifecycle that owns your code. The CLI emits standard Go files that compile and run on their own — `go build`, `go test`, `golangci-lint`, Delve, and your IDE all work the same way they do on any other Go project, because nothing is custom.

It isn't lock-in. The scaffold imports `pkg/*` packages as **opt-out defaults** — the same way any Go service imports `chi`, `go-redis`, or `gorm.io/gorm`. Prefer a different JWT library? Delete the `pkg/auth` import and `go get` your alternative. Nothing else in the project has to change. Every package is independently importable; every default is swappable.

It isn't a clone of anything. The shape of the library is informed by what real Go services need in production — config loading, structured logging, RBAC, request middleware, async tasks, observability — not by mapping conventions from elsewhere onto Go.

## How it actually works

Run `gofasta new myapp` and you get a complete project: HTTP server, database layer, migration tooling, JWT auth with [Casbin](https://casbin.org) RBAC, structured logging via `slog`, Swagger generation, Docker setup, GitHub Actions workflow. Roughly 80 files, all readable, all yours to modify.

Run `gofasta g scaffold User name:string email:string` and you get a model, migration, repository, service, DTOs, controller, route registration, and a Wire provider — all for one resource, in one command. The output is the same code a senior Go engineer would write by hand. The CLI doesn't watch it, doesn't reconcile it, doesn't sync it. After generation, it is your code.

Run `gofasta dev` and the full local stack comes up: Docker services, migrations applied, [Air](https://github.com/air-verse/air) hot reload, optional debug dashboard. One terminal, no orchestration scripts.

The CLI runs once per generation and then gets out of the way. No daemon, no continuous reconciliation, no hidden state.

## Agent-native by default

Most backend tooling treats AI coding agents as an afterthought. Gofasta treats them as a first-class user.

Every CLI command emits structured JSON output via `--json`. Every error carries a stable machine-readable code, a one-line message, a remediation hint, and a documentation URL. Every resource is inspectable as structured data via `gofasta inspect`. Every scaffolded project ships with an [`AGENTS.md`](https://gofasta.dev/docs/cli-reference/ai) briefing — the cross-tool file format that Claude Code, OpenAI Codex, Cursor, Aider, and Windsurf all read at startup. The `gofasta ai <agent>` command installs per-agent configuration (permission allowlists, hooks, slash commands) without cluttering projects that don't use that particular agent.

The documentation site also publishes [`/llms.txt`](https://gofasta.dev/llms.txt) and [`/llms-full.txt`](https://gofasta.dev/llms-full.txt) so agents reason about current features instead of stale training data.

The goal is straightforward: an AI agent working in a Gofasta project should be measurably faster and more accurate than one working on a hand-rolled Go project. That is a design constraint on every feature, not a separate product layered on top.

## What you get on day one

A single `gofasta new` gives you:

* HTTP routing via [chi](https://github.com/go-chi/chi)
* Database support for PostgreSQL, MySQL, SQLite, SQL Server, and ClickHouse — driver chosen from `config.yaml`
* Migrations via [`golang-migrate`](https://github.com/golang-migrate/migrate)
* JWT authentication plus Casbin RBAC
* Structured logging via `slog`
* Caching abstraction (in-memory or Redis)
* File storage abstraction (local or S3-compatible)
* Mailer with SMTP, SendGrid, and Brevo drivers
* WebSocket hub for real-time channels
* Scheduled jobs (`pkg/scheduler`) and async tasks (`pkg/queue`, backed by [asynq](https://github.com/hibiken/asynq))
* Resilience patterns: circuit breaker and retry with exponential backoff
* Prometheus metrics and OpenTelemetry tracing
* Feature flags via the [OpenFeature](https://openfeature.dev) Go SDK
* Swagger/OpenAPI generation from code comments
* Docker setup plus a `gofasta deploy` command for VPS-via-SSH deployment
* A debug dashboard, build-tag-gated so it costs nothing in production
* AGENTS.md, `llms.txt` awareness, and structured CLI output across the board

Pick what you use; delete what you don't.

## Where this is going

The roadmap splits across three surfaces:

1. **CLI ergonomics.** Faster scaffolding, broader generator coverage, more `gofasta do` workflows that compose existing commands.
2. **Library depth.** More production-tested defaults in the packages that already exist — especially around observability, resilience, and authorization.
3. **Documentation.** Guides that go beyond "here's the API" into "here's the trade-off you should think about before you wire this up." This blog is part of that effort: when a `pkg/*` package changes in a non-trivial way, the why often deserves more than a CHANGELOG line.

Start with the [Quick Start](https://gofasta.dev/docs/getting-started/quick-start) for a five-minute walkthrough, or read the full [white paper](https://gofasta.dev/docs/white-paper) for the design rationale behind every choice. Issues, ideas, and contributions are welcome on [GitHub](https://github.com/gofastadev) — we read every one, and many turn into posts here.
116 changes: 116 additions & 0 deletions keystatic.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { config, fields, collection } from "@keystatic/core";

// Keystatic editor config for the Gofasta blog.
//
// Storage mode is capability-conditional, not environment-conditional:
// if every GitHub OAuth env var is present, we use `github` (PR mode
// with a `keystatic/` branch prefix); otherwise we fall back to
// `local` so builds without secrets (CI, dev, preview deploys
// without the OAuth app linked) still succeed.
//
// Required env vars for github mode (Vercel Production scope):
// KEYSTATIC_GITHUB_CLIENT_ID
// KEYSTATIC_GITHUB_CLIENT_SECRET
// KEYSTATIC_SECRET
// NEXT_PUBLIC_KEYSTATIC_GITHUB_APP_SLUG
//
// All Keystatic-flavored MDX disallows `import` and raw HTML tags;
// custom components must be passed at render time (see
// `src/lib/blog-mdx-components.tsx`).

const hasGithubCredentials =
Boolean(process.env.KEYSTATIC_GITHUB_CLIENT_ID) &&
Boolean(process.env.KEYSTATIC_GITHUB_CLIENT_SECRET) &&
Boolean(process.env.KEYSTATIC_SECRET);

export default config({
storage: hasGithubCredentials
? {
kind: "github",
repo: { owner: "gofastadev", name: "website" },
// PR mode — saves open a Pull Request on a dedicated branch
// instead of committing to main. Editorial review + preview
// deploy per post.
branchPrefix: "keystatic/",
}
: { kind: "local" },
ui: {
brand: { name: "Gofasta" },
},
collections: {
posts: collection({
label: "Blog posts",
slugField: "title",
path: "data/blog/*",
format: { contentField: "body" },
entryLayout: "content",
schema: {
draft: fields.checkbox({
label: "Draft",
description:
"Drafts are excluded from the public blog, sitemap, RSS, JSON Feed, tag pages, and on-site search. Pair with a future Published date to schedule — the post will stay hidden until both Draft is unchecked AND the scheduled date passes.",
defaultValue: false,
}),
title: fields.slug({
name: { label: "Title" },
slug: {
label: "URL slug",
description:
"Auto-derived from the title — only edit if the URL needs to differ.",
},
}),
description: fields.text({
label: "Description",
description:
"1–2 sentences shown under the title and used as the meta description.",
multiline: true,
validation: { length: { min: 50, max: 200 } },
}),
publishedAt: fields.datetime({
label: "Published at",
description:
"Posts dated in the future are hidden until the next deploy after that timestamp.",
}),
updatedAt: fields.datetime({
label: "Updated at (optional)",
}),
author: fields.text({
label: "Author name",
defaultValue: "Gofasta Team",
}),
authorUrl: fields.url({
label: "Author URL (optional)",
}),
tags: fields.array(
fields.text({ label: "Tag" }),
{
label: "Tags",
itemLabel: (props) => props.value || "Tag",
},
),
cover: fields.image({
label: "Cover image",
description: "1200×630 recommended. Saved under public/blog/covers/.",
directory: "public/blog/covers",
publicPath: "/blog/covers/",
validation: { isRequired: true },
}),
body: fields.mdx({
label: "Body",
description:
"Start at H2 — the post title (H1) is rendered above the body by the article header. Medium and Hashnode follow the same convention.",
options: {
// H1 is reserved for the post title, which the article header
// renders from frontmatter. Allowing H1 in the body produces
// a duplicate title on the rendered page.
heading: [2, 3, 4, 5, 6],
image: {
directory: "public/blog/inline",
publicPath: "/blog/inline/",
},
},
}),
},
}),
},
});
26 changes: 26 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ export default withNextra({
output: "standalone",
turbopack: {
root: __dirname,
// ── react-aria duplicate-context workaround ──────────────────────
// Keystatic 0.5.50 imports `@react-aria/interactions` while its
// button components transitively load the `react-aria` umbrella.
// Each ships its own bundled PressResponderContext, so toolbar
// PressResponders never find their pressable children → toolbar
// buttons render but do not fire. The shim below re-exports the
// subpackage's surface from the umbrella's private paths so both
// worlds share one set of context instances. See
// shims/react-aria-interactions-shim.mjs for the full rationale,
// and adobe/react-spectrum#5647 for the upstream bug.
//
// Remove this once Keystatic ships a version that uses the
// `react-aria` monopackage directly.
resolveAlias: {
"@react-aria/interactions": "./shims/react-aria-interactions-shim.mjs",
},
},
// Image optimization: serve modern formats by default. Next.js will
// negotiate AVIF first (best compression), then WebP, then fall
Expand All @@ -24,5 +40,15 @@ export default withNextra({
images: {
formats: ["image/avif", "image/webp"],
minimumCacheTTL: 60 * 60 * 24 * 30,
// SVG covers are allowed for blog posts. Authoring goes through
// Keystatic (an authenticated admin UI gated behind GitHub OAuth)
// OR via direct PRs we review — never via untrusted user upload.
// The strict CSP below sandboxes the rendered SVG so scripts and
// foreign-origin assets inside the file are blocked, eliminating
// the XSS vector that makes user-uploaded SVGs risky on hosts
// that mirror unmodified user input.
dangerouslyAllowSVG: true,
contentSecurityPolicy:
"default-src 'self'; script-src 'none'; sandbox;",
},
});
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "next dev",
"prebuild": "node scripts/generate-llms-txt.mjs",
"prebuild": "node scripts/generate-llms-txt.mjs && node scripts/check-blog-images.mjs",
"build": "next build",
"postbuild": "pagefind --site .next/server/app --output-path public/_pagefind",
"start": "next start",
Expand All @@ -15,17 +15,27 @@
"generate:llms": "node scripts/generate-llms-txt.mjs"
},
"dependencies": {
"@giscus/react": "^3.1.0",
"@keystatic/core": "^0.5.50",
"@keystatic/next": "^5.0.4",
"@mdx-js/mdx": "^3.1.1",
"clsx": "^2.1.1",
"image-size": "^2.0.2",
"next": "16.2.2",
"next-mdx-remote": "^6.0.0",
"next-themes": "^0.4.6",
"nextra": "^4.6.1",
"nextra-theme-docs": "^4.6.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"tailwind-merge": "^3.5.0"
"reading-time": "^1.5.0",
"rehype-pretty-code": "^0.14.3",
"tailwind-merge": "^3.5.0",
"yaml": "^2.8.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@tailwindcss/typography": "^0.5.19",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
Expand Down
Binary file added public/blog/covers/hello-gofasta/cover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 94 additions & 0 deletions scripts/check-blog-images.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env node
//
// Image-budget guard for `public/blog/`.
//
// Walks every file under `public/blog/` and fails the build if any
// raster image (.jpg, .jpeg, .png, .webp, .avif, .gif) is larger than
// the configured ceiling. SVGs are exempt because they're vector and
// typically a few kilobytes — the budget applies to bitmap formats
// only.
//
// The threshold is intentionally modest. A 1200×630 JPEG at ~80%
// quality lands in the 100–200 KB range; an editor who commits a
// 4 MB straight-from-camera screenshot triggers the budget and gets
// a clear failure with the file path + actual size before the PR
// lands in main.
//
// Adjust MAX_BYTES if real cover art legitimately needs more room.

import { readdir, stat } from "node:fs/promises";
import { join, resolve, extname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = fileURLToPath(new URL(".", import.meta.url));
const ROOT = resolve(__dirname, "..", "public", "blog");
const MAX_BYTES = 250_000;
const RASTER_EXTENSIONS = new Set([
".jpg",
".jpeg",
".png",
".webp",
".avif",
".gif",
]);

async function walk(dir) {
const out = [];
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch (err) {
if (err && err.code === "ENOENT") return out;
throw err;
}
for (const entry of entries) {
const full = join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...(await walk(full)));
} else if (entry.isFile()) {
out.push(full);
}
}
return out;
}

async function main() {
const files = await walk(ROOT);
const offenders = [];
for (const file of files) {
const ext = extname(file).toLowerCase();
if (!RASTER_EXTENSIONS.has(ext)) continue;
const info = await stat(file);
if (info.size > MAX_BYTES) {
offenders.push({ file, size: info.size });
}
}

if (offenders.length === 0) {
console.log(
`✓ image-budget check passed (${files.length} files under public/blog/, limit ${MAX_BYTES.toLocaleString()} bytes per raster image).`,
);
return;
}

console.error(
`✗ image-budget check failed — ${offenders.length} file(s) exceed the ${MAX_BYTES.toLocaleString()}-byte limit:`,
);
for (const { file, size } of offenders) {
const rel = file.slice(ROOT.length + 1);
console.error(
` - public/blog/${rel} (${size.toLocaleString()} bytes, +${(
size - MAX_BYTES
).toLocaleString()} over)`,
);
}
console.error(
"\nCompress the file(s) or raise MAX_BYTES in scripts/check-blog-images.mjs if the larger size is intentional.",
);
process.exit(1);
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
Loading
Loading