diff --git a/.github/actions/setup-ci/action.yml b/.github/actions/setup-ci/action.yml index 17d058f6e..3458a35e7 100644 --- a/.github/actions/setup-ci/action.yml +++ b/.github/actions/setup-ci/action.yml @@ -13,6 +13,10 @@ runs: node-version-file: './.nvmrc' registry-url: 'https://registry.npmjs.org' cache: 'pnpm' + - uses: actions/setup-go@v6 + with: + go-version: '1.26.x' + cache: false - name: Install dependencies shell: bash run: pnpm install --frozen-lockfile diff --git a/.prettierignore b/.prettierignore index 0eed97a90..591537c96 100644 --- a/.prettierignore +++ b/.prettierignore @@ -30,6 +30,7 @@ projects/starters/bundles/src/index.html projects/starters/eleventy/src/_layouts/index.11ty.js projects/starters/eleventy/src/index.11ty.js projects/starters/go/src/index.html +projects/starters/go-htmx/src/index.html projects/starters/hugo/**/* projects/starters/importmaps/src/index.html projects/starters/mpa/src/**/*.html @@ -39,4 +40,4 @@ projects/starters/solidjs/src/App.tsx projects/starters/typescript/src/index.html projects/starters/vue/src/**/* projects/starters/nuxt/.nuxt/**/* -projects/starters/nuxt/app/**/* \ No newline at end of file +projects/starters/nuxt/app/**/* diff --git a/README.md b/README.md index 45bf52f11..5fd22edd3 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,14 @@ Examples of projects include: To set up repository dependencies and run the full build, run the following commands at the **root** of the repository: +The CI pipeline also builds Go starters. Install [Go 1.26.x](https://go.dev/doc/install) before running the full local CI pipeline. + ```shell # install required dependencies brew install git-lfs git lfs install git lfs pull +go version curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash . ~/.nvm/nvm.sh nvm install diff --git a/package.json b/package.json index 851af8886..eddb7b5e5 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,8 @@ "./projects/starters/bundles:ci", "./projects/starters/eleventy:ci", "./projects/starters/eleventy-ssr:ci", + "./projects/starters/go:ci", + "./projects/starters/go-htmx:ci", "./projects/starters/hugo:ci", "./projects/starters/importmaps:ci", "./projects/starters/lit-library:ci", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8139cac7..88ef268e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1480,6 +1480,8 @@ importers: projects/starters/go: {} + projects/starters/go-htmx: {} + projects/starters/hugo: dependencies: '@nvidia-elements/code': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4b7f185e7..5aea0f347 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,6 +6,7 @@ packages: - projects/starters/eleventy - projects/starters/eleventy-ssr - projects/starters/go + - projects/starters/go-htmx - projects/starters/hugo - projects/starters/importmaps - projects/starters/lit-library diff --git a/projects/internals/tools/src/project/service.test.ts b/projects/internals/tools/src/project/service.test.ts index 6b7db11c4..d77075a67 100644 --- a/projects/internals/tools/src/project/service.test.ts +++ b/projects/internals/tools/src/project/service.test.ts @@ -15,8 +15,8 @@ vi.mock('./setup-agent.js', () => ({ vi.mock('./starters.js', () => ({ startersData: { typescript: { cli: true, zip: 'typescript.zip' }, - react: { cli: true, zip: 'react.zip' }, - importmaps: { cli: false, zip: 'importmaps.zip' } + go: { cli: true, zip: 'go.zip', setupElementsDependencies: false }, + lit: { cli: false, zip: null } }, createStarter: vi.fn(), startStarter: vi.fn() @@ -60,6 +60,25 @@ describe('ProjectService', () => { expect(updateProject).toHaveBeenCalled(); }); + it('should skip dependency setup for starters that opt out of project setup', async () => { + const { createStarter } = await import('./starters.js'); + const { setupAgent } = await import('./setup-agent.js'); + const { setupProject } = await import('./setup.js'); + const { updateProject } = await import('./update.js'); + vi.mocked(createStarter).mockResolvedValue({ create: { message: 'created', status: 'success' } }); + vi.mocked(setupAgent).mockResolvedValue({ agent: { message: 'configured', status: 'success' } }); + + const { ProjectService } = await import('./service.js'); + const result = await ProjectService.create({ type: 'go', cwd: '/test', start: false }); + + expect(result).toHaveProperty('create'); + expect(result).toHaveProperty('agent'); + expect(createStarter).toHaveBeenCalledWith('go', '/test'); + expect(setupAgent).toHaveBeenCalled(); + expect(setupProject).not.toHaveBeenCalled(); + expect(updateProject).not.toHaveBeenCalled(); + }); + it('should return failed report when a step fails', async () => { const { createStarter } = await import('./starters.js'); const { setupAgent } = await import('./setup-agent.js'); diff --git a/projects/internals/tools/src/project/service.ts b/projects/internals/tools/src/project/service.ts index 2317e5274..b62850f53 100644 --- a/projects/internals/tools/src/project/service.ts +++ b/projects/internals/tools/src/project/service.ts @@ -16,6 +16,11 @@ const starters = Object.keys(startersData).filter( starter => startersData[starter as keyof typeof startersData]?.cli ) as Starter[]; +function starterShouldSetupElementsDependencies(type: Starter): boolean { + const starterData = startersData[type]; + return !('setupElementsDependencies' in starterData) || starterData.setupElementsDependencies; +} + @service() export class ProjectService { @tool({ @@ -56,9 +61,12 @@ export class ProjectService { const createReport = await createStarter(type, dir); const agentReport = await setupAgent(projectDir, 'all'); - const setupProjectReport = await setupProject(projectDir); - const updateReport = await updateProject(projectDir); - const reports = [createReport, agentReport, setupProjectReport, updateReport]; + const reports = [createReport, agentReport]; + if (starterShouldSetupElementsDependencies(type)) { + const setupProjectReport = setupProject(projectDir); + const updateProjectReport = await updateProject(projectDir); + reports.push(setupProjectReport, updateProjectReport); + } const failedReport = reports.find(report => Object.values(report).some(value => value.status === 'danger')); if (failedReport) { @@ -69,12 +77,7 @@ export class ProjectService { await startStarter(projectDir); } - return { - ...createReport, - ...agentReport, - ...setupProjectReport, - ...updateReport - }; + return Object.assign({}, ...reports); } @tool({ diff --git a/projects/internals/tools/src/project/starters.test.ts b/projects/internals/tools/src/project/starters.test.ts index c15eca7d8..b1b6c4519 100644 --- a/projects/internals/tools/src/project/starters.test.ts +++ b/projects/internals/tools/src/project/starters.test.ts @@ -5,11 +5,13 @@ import type * as childProcess from 'node:child_process'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { startersData, + createStarterCDNUrl, createGitInitProcess, createStarterPaths, execPackageManager, getDependencyInstallFailureMessage, getRequiredNPMClient, + stampStarterCDNVersions, removeWireitScripts, startStarter } from './starters.js'; @@ -48,6 +50,7 @@ describe('startersData', () => { expect(startersData.nextjs.cli).toBe(true); expect(startersData.solidjs.cli).toBe(true); expect(startersData.go.cli).toBe(true); + expect(startersData['go-htmx'].cli).toBe(true); expect(startersData.hugo.cli).toBe(true); expect(startersData.eleventy.cli).toBe(true); expect(startersData.bundles.cli).toBe(true); @@ -66,6 +69,7 @@ describe('startersData', () => { expect(startersData.nextjs.zip).toContain('nextjs.zip'); expect(startersData.solidjs.zip).toContain('solidjs.zip'); expect(startersData.go.zip).toContain('go.zip'); + expect(startersData['go-htmx'].zip).toContain('go-htmx.zip'); expect(startersData.eleventy.zip).toContain('eleventy.zip'); expect(startersData.importmaps.zip).toContain('importmaps.zip'); expect(startersData.bundles.zip).toContain('bundles.zip'); @@ -198,6 +202,96 @@ describe('getDependencyInstallFailureMessage', () => { }); }); +describe('starter CDN URLs', () => { + type StarterCDNPackageName = Parameters[0]; + type StarterCDNAsset = { packageName: StarterCDNPackageName; filePath: string }; + + const versions: Record = { + '@nvidia-elements/core': '1.2.3', + '@nvidia-elements/styles': '4.5.6', + '@nvidia-elements/themes': '7.8.9' + }; + + const coreAsset: StarterCDNAsset = { packageName: '@nvidia-elements/core', filePath: 'dist/bundles/index.min.js' }; + const stylesAsset: StarterCDNAsset = { packageName: '@nvidia-elements/styles', filePath: 'dist/bundles/index.css' }; + const themesAsset: StarterCDNAsset = { packageName: '@nvidia-elements/themes', filePath: 'dist/bundles/index.css' }; + const themeFontsAsset: StarterCDNAsset = { packageName: '@nvidia-elements/themes', filePath: 'dist/fonts/inter.css' }; + const starterCDNAssets = [stylesAsset, themesAsset, themeFontsAsset, coreAsset]; + + function createVersionedStarterCDNUrl(asset: StarterCDNAsset, version = versions[asset.packageName]) { + return createStarterCDNUrl(asset.packageName, version, asset.filePath); + } + + function createUnversionedStarterCDNUrl(asset: StarterCDNAsset) { + return createVersionedStarterCDNUrl(asset).replace( + `${asset.packageName}@${versions[asset.packageName]}`, + asset.packageName + ); + } + + it('should create versioned CDN URLs', () => { + const url = createVersionedStarterCDNUrl(coreAsset); + + expect(new URL(url).protocol).toBe('https:'); + expect(url).toContain(`${coreAsset.packageName}@${versions[coreAsset.packageName]}/${coreAsset.filePath}`); + }); + + it('should stamp unversioned Elements CDN URLs with package versions', () => { + const content = ` + + + + + `; + + const result = stampStarterCDNVersions(content, versions); + + for (const asset of starterCDNAssets) { + expect(result).toContain(createVersionedStarterCDNUrl(asset)); + } + }); + + it('should replace existing Elements CDN versions', () => { + const content = ` + + + + + `; + + const result = stampStarterCDNVersions(content, versions); + + for (const asset of starterCDNAssets) { + expect(result).toContain(createVersionedStarterCDNUrl(asset)); + } + expect(result).not.toContain('@0.0.1/'); + }); + + it('should leave unrelated CDN URLs unchanged', () => { + const unrelatedElementsPackageUrl = createVersionedStarterCDNUrl(coreAsset).replace( + `${coreAsset.packageName}@${versions[coreAsset.packageName]}/${coreAsset.filePath}`, + '@nvidia-elements/monaco/dist/bundles/index.css' + ); + const htmxUrl = createVersionedStarterCDNUrl(coreAsset).replace( + `${coreAsset.packageName}@${versions[coreAsset.packageName]}/${coreAsset.filePath}`, + 'htmx.org@2.0.10/dist/htmx.min.js' + ); + const content = ` + + + `; + + const result = stampStarterCDNVersions(content, versions); + + expect(result).toContain(unrelatedElementsPackageUrl); + expect(result).toContain(htmxUrl); + }); +}); + describe('getNPMClient', () => { it('should return the npm client', async () => { expect(await getNPMClient()).toBe('pnpm'); diff --git a/projects/internals/tools/src/project/starters.ts b/projects/internals/tools/src/project/starters.ts index cabcaa90d..a0ef95dd9 100644 --- a/projects/internals/tools/src/project/starters.ts +++ b/projects/internals/tools/src/project/starters.ts @@ -4,9 +4,10 @@ import { basename, dirname, join, parse, resolve } from 'node:path'; import { execFile, execFileSync } from 'node:child_process'; import { cwd } from 'node:process'; +import { fileURLToPath } from 'node:url'; -import { writeFile } from 'fs/promises'; -import { existsSync, unlinkSync, writeFileSync, cpSync, createWriteStream, rmSync } from 'fs'; +import { readFile, writeFile } from 'fs/promises'; +import { existsSync, unlinkSync, writeFileSync, cpSync, createWriteStream, rmSync, readFileSync } from 'fs'; import { readWorkspaceManifest } from '@pnpm/workspace.read-manifest'; import { getCatalogsFromWorkspaceManifest } from '@pnpm/catalogs.config'; import { createExportableManifest } from '@pnpm/exportable-manifest'; @@ -18,12 +19,47 @@ import type { Report } from '../internal/types.js'; import { writeAllAgentConfigs } from './setup-agent.js'; const ELEMENTS_PAGES_BASE_URL = 'https://nvidia.github.io/elements'; +const ELEMENTS_CDN_BASE_URL = 'https://cdn.jsdelivr.net/npm'; +function findWorkspaceRoot(startDir: string) { + let dir = startDir; + while (dir !== dirname(dir)) { + if (existsSync(join(dir, 'pnpm-workspace.yaml'))) { + return dir; + } + dir = dirname(dir); + } + throw new Error(`Unable to find pnpm-workspace.yaml from ${startDir}`); +} + +const REPO_WORKSPACE_DIR = findWorkspaceRoot(dirname(fileURLToPath(import.meta.url))); + +type StarterCDNPackageName = '@nvidia-elements/core' | '@nvidia-elements/styles' | '@nvidia-elements/themes'; + +const starterCDNPackagePaths: Record = { + '@nvidia-elements/core': 'projects/core/package.json', + '@nvidia-elements/styles': 'projects/styles/package.json', + '@nvidia-elements/themes': 'projects/themes/package.json' +}; + +const starterCDNAssets: { packageName: StarterCDNPackageName; filePath: string }[] = [ + { packageName: '@nvidia-elements/core', filePath: 'dist/bundles/index.min.js' }, + { packageName: '@nvidia-elements/styles', filePath: 'dist/bundles/index.css' }, + { packageName: '@nvidia-elements/themes', filePath: 'dist/bundles/index.css' }, + { packageName: '@nvidia-elements/themes', filePath: 'dist/fonts/inter.css' } +]; + +const cdnStampTargets = new Map([ + ['go', 'src/index.html'], + ['go-htmx', 'src/index.html'] +]); export type Starter = | 'angular' | 'bundles' | 'eleventy' | 'go' + | 'go-htmx' + | 'hugo' | 'importmaps' | 'lit-library' | 'lit' @@ -54,7 +90,13 @@ export const startersData = { }, go: { zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/go.zip`, - cli: true + cli: true, + setupElementsDependencies: false + }, + 'go-htmx': { + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/go-htmx.zip`, + cli: true, + setupElementsDependencies: false }, hugo: { zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/hugo.zip`, @@ -113,7 +155,8 @@ export const startersData = { /* istanbul ignore next -- @preserve */ export async function archiveStarter(projectDir: string, outDir: string) { const dist = join(outDir, projectDir); - await copyProject(projectDir); + await copyProject(projectDir, dist); + await stampStarterCDNVersionFiles(projectDir, dist); writeAllAgentConfigs(dist); const packageJSON = await exportPackageFromWorkspace(projectDir); await writeFile(join(dist, 'package.json'), JSON.stringify(packageJSON, undefined, 2)); @@ -135,17 +178,64 @@ async function zipProject(outDir: string) { } /* istanbul ignore next -- @preserve */ -function copyProject(projectDir: string) { - const ignoreDirs = new Set(['dist', 'node_modules', '.wireit', '.eslintcache']); - cpSync(projectDir, join('dist', projectDir), { +function copyProject(projectDir: string, dist: string) { + const ignoreDirs = new Set(['dist', 'node_modules', '.wireit', '.eslintcache', 'bin']); + cpSync(projectDir, dist, { recursive: true, filter: src => !ignoreDirs.has(basename(src)) }); } +function escapeRegExp(value: string) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function getPackageVersion(repoRoot: string, packageName: StarterCDNPackageName): string { + const packageJsonPath = join(repoRoot, starterCDNPackagePaths[packageName]); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { version?: unknown }; + if (typeof packageJson.version !== 'string' || !packageJson.version) { + throw new Error(`No version found for ${packageName} in ${packageJsonPath}`); + } + return packageJson.version; +} + +function getStarterCDNPackageVersions(repoRoot: string): Record { + return { + '@nvidia-elements/core': getPackageVersion(repoRoot, '@nvidia-elements/core'), + '@nvidia-elements/styles': getPackageVersion(repoRoot, '@nvidia-elements/styles'), + '@nvidia-elements/themes': getPackageVersion(repoRoot, '@nvidia-elements/themes') + }; +} + +export function createStarterCDNUrl(packageName: StarterCDNPackageName, version: string, filePath: string) { + return `${ELEMENTS_CDN_BASE_URL}/${packageName}@${version}/${filePath}`; +} + +export function stampStarterCDNVersions(content: string, versions: Record) { + return starterCDNAssets.reduce((updatedContent, asset) => { + const versionedUrl = createStarterCDNUrl(asset.packageName, versions[asset.packageName], asset.filePath); + const urlPattern = new RegExp( + `${escapeRegExp(ELEMENTS_CDN_BASE_URL)}/${escapeRegExp(asset.packageName)}(?:@[^/"']+)?/${escapeRegExp(asset.filePath)}`, + 'g' + ); + return updatedContent.replace(urlPattern, versionedUrl); + }, content); +} + +async function stampStarterCDNVersionFiles(projectDir: string, dist: string) { + const stampTarget = cdnStampTargets.get(projectDir); + if (!stampTarget) { + return; + } + + const indexPath = join(dist, stampTarget); + const versions = getStarterCDNPackageVersions(REPO_WORKSPACE_DIR); + const content = await readFile(indexPath, 'utf8'); + await writeFile(indexPath, stampStarterCDNVersions(content, versions)); +} + /* istanbul ignore next -- @preserve */ async function exportPackageFromWorkspace(projectDir: string) { - const REPO_WORKSPACE_DIR = '../../'; const workspace = await readWorkspaceManifest(REPO_WORKSPACE_DIR); const catalogs = getCatalogsFromWorkspaceManifest(workspace); const manifest = await readProjectManifestOnly(projectDir); diff --git a/projects/internals/tools/src/skills/about.md b/projects/internals/tools/src/skills/about.md index 13763d92a..13f76cbbe 100644 --- a/projects/internals/tools/src/skills/about.md +++ b/projects/internals/tools/src/skills/about.md @@ -21,11 +21,11 @@ Give a high level overview of the NVIDIA Elements Design System. ```bash # create a new project -nve project.create # typescript, angular, react, lit, preact, solidjs, vue, nextjs, go +nve project.create # angular, bundles, eleventy, go, go-htmx, hugo, nextjs, nuxt, react, solidjs, svelte, typescript, vue ``` ### Resources for Users - [Documentation](https://NVIDIA.github.io/elements/) -- [Gitlab Repo](https://github.com/NVIDIA/elements) +- [GitHub Repo](https://github.com/NVIDIA/elements) - [Changelog](https://NVIDIA.github.io/elements/docs/changelog/) diff --git a/projects/site/public/static/images/integrations/NOTICE b/projects/site/public/static/images/integrations/NOTICE index 1d361b917..dfcd420e2 100644 --- a/projects/site/public/static/images/integrations/NOTICE +++ b/projects/site/public/static/images/integrations/NOTICE @@ -12,6 +12,7 @@ These logos are **not** covered by this project's Apache 2.0 license. | cursor.svg | Anysphere Inc. | | eleventy.svg | Zach Leatherman | | go.svg | Google LLC | +| htmx.svg | Big Sky Software | | hugo.svg | The Hugo Authors | | javascript.svg | Oracle Corporation (pending community usage) | | nextjs.svg | Vercel Inc. | diff --git a/projects/site/public/static/images/integrations/htmx.svg b/projects/site/public/static/images/integrations/htmx.svg new file mode 100644 index 000000000..25b0a3008 --- /dev/null +++ b/projects/site/public/static/images/integrations/htmx.svg @@ -0,0 +1 @@ + diff --git a/projects/site/src/_11ty/layouts/common.js b/projects/site/src/_11ty/layouts/common.js index 3a462f571..71de1bb19 100644 --- a/projects/site/src/_11ty/layouts/common.js +++ b/projects/site/src/_11ty/layouts/common.js @@ -149,6 +149,7 @@ export const renderDocsNav = data => /* html */ ` CDN Custom Elements Golang + HTMX + Go Hugo Import Maps Lit diff --git a/projects/site/src/_11ty/shortcodes/svg-logo.js b/projects/site/src/_11ty/shortcodes/svg-logo.js index af6eaca3c..63210f101 100644 --- a/projects/site/src/_11ty/shortcodes/svg-logo.js +++ b/projects/site/src/_11ty/shortcodes/svg-logo.js @@ -31,6 +31,7 @@ const svgs = { nuxt: ``, typescript: ``, go: ``, + htmx: ``, hugo: ``, w3c: `` }; diff --git a/projects/site/src/docs/integrations/go-htmx.md b/projects/site/src/docs/integrations/go-htmx.md new file mode 100644 index 000000000..610920021 --- /dev/null +++ b/projects/site/src/docs/integrations/go-htmx.md @@ -0,0 +1,25 @@ +--- +{ + title: 'HTMX + Go', + description: 'Use NVIDIA Elements with HTMX and Go: render full pages and fragment responses from Go templates.', + layout: 'docs.11ty.js' +} +--- + +# {{ title }} + +{% integration 'go-htmx' %} + +{% installation 'go-htmx' %} + +The HTMX + Go starter extends the Go starter with one server-rendered fragment endpoint for HTMX swaps. + +The starter uses the pre-built Elements CSS and JavaScript bundles. It loads HTMX in the base HTML page and renders `/` as the full page. The `/fragment/time` endpoint serves a fragment response that returns only the refresh button's swap target. + +Use this path when you want Go templates to own rendering while HTMX updates small regions of the page without adding a JavaScript build step. + +For upstream framework details, see the [Go documentation](https://go.dev/doc/) and [HTMX documentation](https://htmx.org/docs/). + +## Registry Usage Guidelines + +{% artifactory-usage %} diff --git a/projects/site/src/docs/integrations/go.md b/projects/site/src/docs/integrations/go.md index 4231a9eca..cfbfca474 100644 --- a/projects/site/src/docs/integrations/go.md +++ b/projects/site/src/docs/integrations/go.md @@ -12,14 +12,14 @@ {% installation 'go' %} -Elements is agnostic to any frontend or backend tooling. To leverage elements in Go based templating two paths are available. +Elements is agnostic to any frontend or backend tooling. To use Elements in Go-based templating, two paths are available. -1. Static bundles with little to no JavScript ecosystem tooling -2. Build time tooling with NodeJS and npm packages +1. Static bundles with little to no JavaScript ecosystem tooling +2. Build-time tooling with Node.js and npm packages -The current simple [Go starter]({{ELEMENTS_REPO_BASE_URL}}/tree/main/projects/starters/go) provides an example of a basic Go web server leveraging the pre-built JS and CSS bundles. This enables Go generated HTML pages with minimal NodeJS/JavaScript ecosystem tooling. +The [Go starter]({{ELEMENTS_REPO_BASE_URL}}/tree/main/projects/starters/go) provides a basic Go web server that leverages the pre-built JavaScript and CSS bundles. The [HTMX + Go integration](../go-htmx/) extends that setup with fragment responses for HTMX interactions. Both starters enable Go-generated HTML pages with minimal JavaScript ecosystem tooling. -But, if you would like to integrate advanced tooling such as TypeScript, treeshaking or other JavaScript ecosystem tools and packages consider leveraging tools like [Vite](https://vite.dev/) and [Vite Go](https://olivere.github.io/vite/) +If you would like to integrate advanced tooling such as TypeScript, tree-shaking, or other JavaScript ecosystem tools and packages, consider leveraging tools like [Vite](https://vite.dev/) and [Vite Go](https://olivere.github.io/vite/). ## Registry Usage Guidelines diff --git a/projects/site/src/docs/integrations/index.11ty.js b/projects/site/src/docs/integrations/index.11ty.js index 270e23932..9de0edaed 100644 --- a/projects/site/src/docs/integrations/index.11ty.js +++ b/projects/site/src/docs/integrations/index.11ty.js @@ -35,6 +35,14 @@ const integrations = [ title: 'Golang', description: 'Use Elements with Go-backed web applications.' }, + { + href: 'docs/integrations/go-htmx/', + icon: 'htmx.svg', + iconHeight: '32px', + iconWidth: '48px', + title: 'HTMX + Go', + description: 'Use Elements with HTMX and Go template fragments.' + }, { href: 'docs/integrations/hugo/', icon: 'hugo.svg', @@ -118,10 +126,10 @@ const integrations = [ } ]; -const renderLogo = ({ icon, iconSize = '36px', nveIcon, title, color = 'gray-denim' }) => { +const renderLogo = ({ icon, iconHeight, iconSize = '36px', iconWidth, nveIcon, title, color = 'gray-denim' }) => { if (icon) { return /* html */ ` - ${title.toLowerCase()} logo + ${title.toLowerCase()} logo `; } diff --git a/projects/site/src/index.11tydata.js b/projects/site/src/index.11tydata.js index fe050da80..4b656d818 100644 --- a/projects/site/src/index.11tydata.js +++ b/projects/site/src/index.11tydata.js @@ -67,7 +67,15 @@ const integrations = { starterDemo: null, starterDownload: `${ELEMENTS_PAGES_BASE_URL}/starters/download/go.zip`, starterSource: `${ELEMENTS_REPO_BASE_URL}/tree/main/projects/starters/go`, - documentation: 'https://go.dev', + documentation: 'https://go.dev/doc/', + playgroundURL: null + }, + 'go-htmx': { + logo: 'htmx', + starterDemo: null, + starterDownload: `${ELEMENTS_PAGES_BASE_URL}/starters/download/go-htmx.zip`, + starterSource: `${ELEMENTS_REPO_BASE_URL}/tree/main/projects/starters/go-htmx`, + documentation: 'https://htmx.org/docs/', playgroundURL: null }, hugo: { diff --git a/projects/starters/README.md b/projects/starters/README.md index bc17bcbbf..4508cee81 100644 --- a/projects/starters/README.md +++ b/projects/starters/README.md @@ -7,6 +7,7 @@ This directory contains a suite of standardized starter apps for kickstarting an - `/eleventy` - Static site generator starter using Eleventy and Elements. - `/eleventy-ssr` - Server-rendered Eleventy starter using Lit SSR. - `/go` - Minimal Go web server leveraging Elements pre-built bundles. +- `/go-htmx` - Minimal HTMX starter using a Go web server for fragment updates. - `/hugo` - Static site generator starter using Hugo and Elements. - `/importmaps` - Plain HTML with import maps, no build step required. - `/lit-library` - Build setup for reusable Lit Web Component libraries. diff --git a/projects/starters/go-htmx/.gitignore b/projects/starters/go-htmx/.gitignore new file mode 100644 index 000000000..8fd3b0c4f --- /dev/null +++ b/projects/starters/go-htmx/.gitignore @@ -0,0 +1,3 @@ +node_modules +bin +.wireit diff --git a/projects/starters/go-htmx/AGENTS.md b/projects/starters/go-htmx/AGENTS.md new file mode 100644 index 000000000..2cdecc751 --- /dev/null +++ b/projects/starters/go-htmx/AGENTS.md @@ -0,0 +1,35 @@ +# Elements HTMX + Go Starter + +This file only covers how this starter wires Elements into Go templates and HTMX fragment responses. For component APIs, template validation, and project setup commands, use the Elements CLI/MCP documentation instead. + +## Integration Points + +- Render Elements markup through `html/template` in `src/index.html`. +- Render the full page through the root route. +- Keep HTMX swap content in fragment templates under `src/`. +- Keep dynamic values in Go data structs and pass them into templates; do not concatenate HTML strings in handlers. +- Use static bundles or a separate frontend build pipeline. This starter is the static-bundle path. + +## Static Assets + +- Use pre-built browser-loadable bundles when keeping the starter free of frontend build tooling. +- If serving local bundles instead, add explicit static file handling in `main.go` and point CSS and module imports at that route. +- Keep theme attributes on the root HTML template, not in Go handler code. + +## Go Template Constraints + +- Use escaped template output for data values. +- Only use trusted template HTML for checked Elements markup and HTMX attributes. +- Keep routing and rendering small; move repeated page shell or fragment code into templates before adding more routes. + +## HTMX Constraints + +- Keep fragment endpoints under `/fragment/`. +- Return only the target fragment from fragment routes. +- Keep full-page templates and fragment templates separate so fragment routes do not render the page shell. +- Keep HTMX request behavior in templates, not generated handler strings. + +## Verification + +- Run `pnpm run build` in `projects/starters/go-htmx` after Go or template changes. +- Run `pnpm run dev` for local rendering checks on `http://localhost:8080`. diff --git a/projects/starters/go-htmx/README.md b/projects/starters/go-htmx/README.md new file mode 100644 index 000000000..812093fce --- /dev/null +++ b/projects/starters/go-htmx/README.md @@ -0,0 +1,13 @@ +# HTMX + Go + Elements Starter + +This starter shows a minimal Go web server using Elements and HTMX. The server uses `html/template`, pre-built Elements bundles, and one fragment endpoint that returns only the HTMX swap target. + +## Commands / npm scripts + +- `dev`: `go run main.go` +- `build`: `wireit` runs `go build -o bin main.go` +- `preview`: `go build -o bin main.go && ./bin` + +## Advanced Usage + +This starter does not use Node.js or JavaScript build tooling. If you need TypeScript, tree-shaking, or other JavaScript ecosystem tooling, use a build tool such as [Vite](https://vite.dev/) with [Vite Go](https://olivere.github.io/vite/). diff --git a/projects/starters/go-htmx/main.go b/projects/starters/go-htmx/main.go new file mode 100644 index 000000000..ec271aabd --- /dev/null +++ b/projects/starters/go-htmx/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "fmt" + "html/template" + "log" + "net/http" + "os" + "path/filepath" + "time" +) + +type PageData struct { + Title string + Description string + Time TimeData +} + +type TimeData struct { + Timestamp string +} + +func main() { + port := envOrDefault("PORT", "8080") + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + renderTemplate(w, "src/index.html", PageData{ + Title: "HTMX + Go", + Description: "A simple starter using Elements, HTMX fragment updates, and Go templates.", + Time: currentTime(), + }, "src/time.html") + }) + http.HandleFunc("/fragment/time", func(w http.ResponseWriter, r *http.Request) { + renderTemplate(w, "src/time.html", currentTime()) + }) + + fmt.Printf("Server is running at http://localhost:%s\n", port) + if err := http.ListenAndServe(":"+port, nil); err != nil { + fmt.Println(err) + } +} + +func currentTime() TimeData { + return TimeData{Timestamp: time.Now().Format("2006-01-02 15:04:05 MST")} +} + +func envOrDefault(name string, defaultValue string) string { + if value := os.Getenv(name); value != "" { + return value + } + + return defaultValue +} + +func renderTemplate(w http.ResponseWriter, templatePath string, data interface{}, templatePaths ...string) { + paths := append([]string{templatePath}, templatePaths...) + tmpl, templateError := template.ParseFiles(paths...) + if templateError != nil { + log.Printf("template parse error path=%s err=%v", templatePath, templateError) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + templateError = tmpl.ExecuteTemplate(w, filepath.Base(templatePath), data) + if templateError != nil { + log.Printf("template execute error path=%s err=%v", templatePath, templateError) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } +} diff --git a/projects/starters/go-htmx/package.json b/projects/starters/go-htmx/package.json new file mode 100644 index 000000000..388be677e --- /dev/null +++ b/projects/starters/go-htmx/package.json @@ -0,0 +1,30 @@ +{ + "name": "go-htmx-starter", + "version": "0.0.0", + "private": true, + "description": "HTMX + Go starter", + "scripts": { + "ci": "wireit", + "dev": "go run main.go", + "build": "wireit", + "preview": "go build -o bin main.go && ./bin" + }, + "wireit": { + "ci": { + "dependencies": [ + "build" + ] + }, + "build": { + "command": "go build -o bin main.go", + "files": [ + "main.go", + "src/**", + "package.json" + ], + "output": [ + "bin" + ] + } + } +} diff --git a/projects/starters/go-htmx/src/index.html b/projects/starters/go-htmx/src/index.html new file mode 100644 index 000000000..badc69ae2 --- /dev/null +++ b/projects/starters/go-htmx/src/index.html @@ -0,0 +1,86 @@ + + + + Elements + HTMX + Go + + + + + + + + + + + + + + NV + Elements + Documentation + Starters + GitHub + + + + + Documentation + Source + Download + + + +
+
+ htmx logo +

{{.Title}}

+
+

{{.Description}}

+
+

Server time

+ {{template "time.html" .Time}} + Refresh +
+ +
+
+ + diff --git a/projects/starters/go-htmx/src/time.html b/projects/starters/go-htmx/src/time.html new file mode 100644 index 000000000..0a2cb571f --- /dev/null +++ b/projects/starters/go-htmx/src/time.html @@ -0,0 +1 @@ +{{.Timestamp}} diff --git a/projects/starters/go/.gitignore b/projects/starters/go/.gitignore index ed4598e7f..8fd3b0c4f 100644 --- a/projects/starters/go/.gitignore +++ b/projects/starters/go/.gitignore @@ -1,2 +1,3 @@ node_modules bin +.wireit diff --git a/projects/starters/go/AGENTS.md b/projects/starters/go/AGENTS.md index 5b4bcca48..e6e76f0d2 100644 --- a/projects/starters/go/AGENTS.md +++ b/projects/starters/go/AGENTS.md @@ -10,7 +10,7 @@ This file only covers how this starter wires Elements into Go templates. For com ## Static Assets -- Treat the current CDN comments in `src/index.html` as placeholders until a real Elements CDN path exists. +- Use pre-built browser-loadable bundles when keeping the starter free of frontend build tooling. - If serving local bundles instead, add explicit static file handling in `main.go` and point CSS and module imports at that route. - Keep theme attributes on the root HTML template, not in Go handler code. diff --git a/projects/starters/go/README.md b/projects/starters/go/README.md index 0901ca5a4..9c31be3fc 100644 --- a/projects/starters/go/README.md +++ b/projects/starters/go/README.md @@ -1,13 +1,13 @@ # Go + Elements Starter -This starter shows a minimal example of a Go based web server using Elements. The web server is using basic templating and static JS/CSS bundles provided by the Elements package. +This starter shows a minimal Go web server using Elements. The server uses `html/template` and pre-built Elements bundles. ## Commands / npm scripts -- `dev`: `cd src && go run main.go` -- `build`: `cd src && go build -o bin main.go` -- `preview`: `cd src && go build -o bin main.go && ./bin` +- `dev`: `go run main.go` +- `build`: `wireit` runs `go build -o bin main.go` +- `preview`: `go build -o bin main.go && ./bin` -## Advance Usage +## Advanced Usage -This demo demonstrates a minimal Go web server with no NodeJS/JavaScript build tooling. If use of TypeScript or other JavaScript ecosystem tools are desired it is recommended to leverage tools like [Vite](https://vite.dev/) and [Vite Go](https://olivere.github.io/vite/) +This starter does not use Node.js or JavaScript build tooling. If you need TypeScript, tree-shaking, or other JavaScript ecosystem tooling, use a build tool such as [Vite](https://vite.dev/) with [Vite Go](https://olivere.github.io/vite/). diff --git a/projects/starters/go/main.go b/projects/starters/go/main.go index 71b5d7a45..cdabfd411 100644 --- a/projects/starters/go/main.go +++ b/projects/starters/go/main.go @@ -3,15 +3,13 @@ package main import ( "fmt" "html/template" + "log" "net/http" + "os" + "path/filepath" "time" ) -type Page[T any] struct { - Template string - Data T -} - type PageData struct { Title string Description string @@ -19,27 +17,48 @@ type PageData struct { } func main() { + port := envOrDefault("PORT", "8080") + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - renderPage(w, Page[PageData]{ - Template: "src/index.html", - Data: PageData{ - Title: "Go", - Description: "A simple starter using Elements and Go.", - Date: time.Now().Format("02-01-2006 15:04:05"), - }, + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + renderTemplate(w, "src/index.html", PageData{ + Title: "Go", + Description: "A simple starter using Elements and Go.", + Date: time.Now().Format("02-01-2006 15:04:05"), }) }) - fmt.Println("Server is running at http://localhost:8080") - http.ListenAndServe(":8080", nil) + fmt.Printf("Server is running at http://localhost:%s\n", port) + if err := http.ListenAndServe(":"+port, nil); err != nil { + fmt.Println(err) + } +} + +func envOrDefault(name string, defaultValue string) string { + if value := os.Getenv(name); value != "" { + return value + } + + return defaultValue } -func renderPage[T any](w http.ResponseWriter, page Page[T]) { - template := template.Must(template.ParseFiles(page.Template)) - templateError := template.Execute(w, page) +func renderTemplate(w http.ResponseWriter, templatePath string, data interface{}, templatePaths ...string) { + paths := append([]string{templatePath}, templatePaths...) + tmpl, templateError := template.ParseFiles(paths...) if templateError != nil { - http.Error(w, templateError.Error(), http.StatusInternalServerError) + log.Printf("template parse error path=%s err=%v", templatePath, templateError) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } - w.WriteHeader(http.StatusOK) + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + templateError = tmpl.ExecuteTemplate(w, filepath.Base(templatePath), data) + if templateError != nil { + log.Printf("template execute error path=%s err=%v", templatePath, templateError) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } } diff --git a/projects/starters/go/package.json b/projects/starters/go/package.json index 169c6b343..8d308c20b 100644 --- a/projects/starters/go/package.json +++ b/projects/starters/go/package.json @@ -4,8 +4,9 @@ "private": true, "description": "Go starter", "scripts": { + "ci": "wireit", "dev": "go run main.go", - "build": "go build -o bin main.go", + "build": "wireit", "preview": "go build -o bin main.go && ./bin" }, "wireit": { @@ -13,6 +14,17 @@ "dependencies": [ "build" ] + }, + "build": { + "command": "go build -o bin main.go", + "files": [ + "main.go", + "src/**", + "package.json" + ], + "output": [ + "bin" + ] } } } diff --git a/projects/starters/go/src/index.html b/projects/starters/go/src/index.html index c01b774f6..32298e79a 100644 --- a/projects/starters/go/src/index.html +++ b/projects/starters/go/src/index.html @@ -4,45 +4,44 @@ Elements + Go - + - - - - + + + NV - Elements - Catalog - Starters - GitHub + Elements + Documentation + Starters + GitHub - Documentation - Source - Download + Documentation + Source + Download
- go logo -

{{.Data.Title}}

+ go logo +

{{.Title}}

-

{{.Data.Description}}

-

Last updated: {{.Data.Date}}

+

{{.Description}}

+

Last updated: {{.Date}}

diff --git a/projects/starters/package.json b/projects/starters/package.json index 4edb8d95f..371a50f45 100644 --- a/projects/starters/package.json +++ b/projects/starters/package.json @@ -25,6 +25,18 @@ "./importmaps/dist/**/*.js", "./eleventy/dist/**/*.js", "./eleventy-ssr/dist/**/*.js", + "./go/*.go", + "./go/.gitignore", + "./go/AGENTS.md", + "./go/package.json", + "./go/README.md", + "./go/src/**", + "./go-htmx/*.go", + "./go-htmx/.gitignore", + "./go-htmx/AGENTS.md", + "./go-htmx/package.json", + "./go-htmx/README.md", + "./go-htmx/src/**", "./lit-library/dist/**/*.js", "./mcp-app/dist/**", "./mpa/dist/**/*.js", @@ -65,6 +77,14 @@ "script": "./eleventy-ssr:build", "cascade": false }, + { + "script": "./go:build", + "cascade": false + }, + { + "script": "./go-htmx:build", + "cascade": false + }, { "script": "./hugo:build", "cascade": false