@rm-industries/create-forge 0.3.0-alpha.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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/dist/index.mjs +511 -0
  4. package/dist/index.mjs.map +1 -0
  5. package/dist/template/.editorconfig +10 -0
  6. package/dist/template/.markdownlint.json +10 -0
  7. package/dist/template/README.md +169 -0
  8. package/dist/template/astro.config.ts +15 -0
  9. package/dist/template/cspell.config.ts +6 -0
  10. package/dist/template/oxfmt.config.ts +9 -0
  11. package/dist/template/package-lock.json +10896 -0
  12. package/dist/template/package.json +61 -0
  13. package/dist/template/playwright.config.ts +24 -0
  14. package/dist/template/public/favicon.svg +4 -0
  15. package/dist/template/public/social-card.svg +8 -0
  16. package/dist/template/src/components/navigation/Footer.astro +33 -0
  17. package/dist/template/src/components/navigation/Header.astro +11 -0
  18. package/dist/template/src/components/navigation/Navbar.astro +60 -0
  19. package/dist/template/src/components/navigation/Pagination.astro +37 -0
  20. package/dist/template/src/components/seo/SeoHead.astro +48 -0
  21. package/dist/template/src/components/theme/ThemeHead.astro +6 -0
  22. package/dist/template/src/components/ui/Card.astro +26 -0
  23. package/dist/template/src/components/ui/Container.astro +17 -0
  24. package/dist/template/src/config/content-models/articles.ts +39 -0
  25. package/dist/template/src/config/content-models/registry.ts +5 -0
  26. package/dist/template/src/config/site.test.ts +37 -0
  27. package/dist/template/src/config/site.ts +95 -0
  28. package/dist/template/src/content/articles/accessible-by-default.md +22 -0
  29. package/dist/template/src/content/articles/content-that-travels-well.md +17 -0
  30. package/dist/template/src/content/articles/designing-a-calm-starting-point.md +22 -0
  31. package/dist/template/src/content/articles/future-draft.md +11 -0
  32. package/dist/template/src/content.config.ts +5 -0
  33. package/dist/template/src/integrations/sveltia/config.test.ts +35 -0
  34. package/dist/template/src/integrations/sveltia/config.ts +28 -0
  35. package/dist/template/src/integrations/sveltia/preview-collections.test.ts +8 -0
  36. package/dist/template/src/integrations/sveltia/preview-collections.ts +3 -0
  37. package/dist/template/src/integrations/sveltia/previews.ts +13 -0
  38. package/dist/template/src/layouts/BaseLayout.astro +41 -0
  39. package/dist/template/src/lib/articles.ts +20 -0
  40. package/dist/template/src/lib/navigation.test.ts +20 -0
  41. package/dist/template/src/lib/navigation.ts +22 -0
  42. package/dist/template/src/pages/404.astro +17 -0
  43. package/dist/template/src/pages/about.astro +22 -0
  44. package/dist/template/src/pages/admin/index.astro +24 -0
  45. package/dist/template/src/pages/articles/[slug].astro +58 -0
  46. package/dist/template/src/pages/articles/index.astro +34 -0
  47. package/dist/template/src/pages/index.astro +64 -0
  48. package/dist/template/src/pages/robots.txt.ts +8 -0
  49. package/dist/template/src/pages/rss.xml.ts +17 -0
  50. package/dist/template/src/pages/site.webmanifest.ts +19 -0
  51. package/dist/template/src/styles/global.css +73 -0
  52. package/dist/template/src/styles/print.css +34 -0
  53. package/dist/template/src/themes/catppuccinTheme.frappe.ts +5 -0
  54. package/dist/template/src/themes/catppuccinTheme.latte.ts +5 -0
  55. package/dist/template/src/themes/catppuccinTheme.macchiato.ts +5 -0
  56. package/dist/template/src/themes/catppuccinTheme.mocha.ts +5 -0
  57. package/dist/template/src/themes/site-theme.test.ts +19 -0
  58. package/dist/template/src/themes/site-theme.ts +13 -0
  59. package/dist/template/stylelint.config.ts +19 -0
  60. package/dist/template/tests/accessibility.spec.ts +47 -0
  61. package/dist/template/tests/routes.spec.ts +87 -0
  62. package/dist/template/tests/theme.spec.ts +47 -0
  63. package/dist/template/tsconfig.json +5 -0
  64. package/dist/template/vitest.config.ts +8 -0
  65. package/package.json +52 -0
@@ -0,0 +1,47 @@
1
+ import AxeBuilder from '@axe-core/playwright';
2
+ import { expect, test } from '@playwright/test';
3
+
4
+ test('has no serious or critical accessibility violations', async ({ page }) => {
5
+ await page.goto('/');
6
+
7
+ const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze();
8
+ const violations = results.violations.filter(({ impact }) => impact === 'serious' || impact === 'critical');
9
+
10
+ expect(violations).toEqual([]);
11
+ });
12
+
13
+ test('exposes landmarks and identifies the current navigation item', async ({ page }) => {
14
+ await page.goto('/');
15
+
16
+ await expect(page.getByRole('banner')).toBeVisible();
17
+ await expect(page.getByRole('navigation', { name: 'Primary navigation' })).toBeVisible();
18
+ await expect(page.getByRole('link', { name: 'Home' })).toHaveAttribute('aria-current', 'page');
19
+ await expect(page.getByRole('main')).toBeVisible();
20
+ await expect(page.getByRole('contentinfo')).toBeVisible();
21
+ });
22
+
23
+ test('moves keyboard users directly to the main content', async ({ page }) => {
24
+ await page.goto('/');
25
+ await page.keyboard.press('Tab');
26
+
27
+ const skipLink = page.getByRole('link', { name: 'Skip to content' });
28
+ await expect(skipLink).toBeFocused();
29
+ await expect(skipLink).toBeVisible();
30
+
31
+ await page.keyboard.press('Enter');
32
+ await expect(page).toHaveURL(/#main-content$/u);
33
+ await expect(page.getByRole('main')).toBeFocused();
34
+ });
35
+
36
+ test('keeps navigation available without horizontal overflow on small screens', async ({ page }) => {
37
+ await page.setViewportSize({ width: 320, height: 640 });
38
+ await page.goto('/');
39
+
40
+ await page.locator('summary').click();
41
+ await expect(page.getByRole('navigation', { name: 'Mobile navigation' })).toBeVisible();
42
+ const hasHorizontalOverflow = await page.evaluate(
43
+ () => document.documentElement.scrollWidth > document.documentElement.clientWidth,
44
+ );
45
+
46
+ expect(hasHorizontalOverflow).toBe(false);
47
+ });
@@ -0,0 +1,87 @@
1
+ import { expect, test } from '@playwright/test';
2
+
3
+ const routes = ['/', '/about/', '/articles/', '/articles/designing-a-calm-starting-point/', '/admin/', '/404/'];
4
+
5
+ test('serves every baseline page with configured canonical and social metadata', async ({ page }) => {
6
+ for (const route of routes) {
7
+ await page.goto(route);
8
+
9
+ await expect(page.locator('link[rel="canonical"]')).toHaveAttribute(
10
+ 'href',
11
+ new URL(route, 'https://example.com').href,
12
+ );
13
+ await expect(page.locator('meta[property="og:site_name"]')).toHaveAttribute('content', 'Forge');
14
+ await expect(page.locator('meta[property="og:url"]')).toHaveAttribute(
15
+ 'content',
16
+ new URL(route, 'https://example.com').href,
17
+ );
18
+ await expect(page.locator('meta[name="twitter:card"]')).toHaveAttribute('content', 'summary_large_image');
19
+ }
20
+ });
21
+
22
+ test('serves the content manager without allowing search indexing', async ({ page }) => {
23
+ await page.goto('/admin/');
24
+
25
+ await expect(page.locator('meta[name="robots"][content="noindex, nofollow"]')).toHaveCount(1);
26
+ await expect(page).toHaveTitle(/Content Manager/u);
27
+ });
28
+
29
+ test('renders generic article tags on listings and detail pages', async ({ page }) => {
30
+ await page.goto('/articles/');
31
+ await expect(
32
+ page.locator('a[href="/articles/designing-a-calm-starting-point/"]').getByLabel('Article tags'),
33
+ ).toContainText('Design');
34
+
35
+ await page.goto('/articles/designing-a-calm-starting-point/');
36
+ await expect(page.getByLabel('Article tags')).toContainText('Defaults');
37
+ });
38
+
39
+ test('resolves every internal page link', async ({ page, request }) => {
40
+ await page.goto('/');
41
+
42
+ const pending = ['/'];
43
+ const visited = new Set<string>();
44
+
45
+ while (pending.length > 0) {
46
+ const path = pending.shift();
47
+ if (!path || visited.has(path)) continue;
48
+
49
+ visited.add(path);
50
+ await page.goto(path);
51
+
52
+ const links = await page
53
+ .locator('a[href]')
54
+ .evaluateAll((elements) => elements.map((element) => (element as HTMLAnchorElement).href));
55
+
56
+ for (const href of links) {
57
+ const url = new URL(href);
58
+ if (url.origin !== 'http://127.0.0.1:4321') continue;
59
+
60
+ const linkedPath = url.pathname;
61
+ const response = await request.get(linkedPath);
62
+ expect(response.status(), `${path} links to ${linkedPath}`).toBeLessThan(400);
63
+
64
+ if (!visited.has(linkedPath)) pending.push(linkedPath);
65
+ }
66
+ }
67
+ });
68
+
69
+ test('serves feed, crawler, manifest, and not-found metadata', async ({ request }) => {
70
+ const feed = await request.get('/rss.xml');
71
+ expect(feed.ok()).toBe(true);
72
+ expect(await feed.text()).toContain('https://example.com/articles/designing-a-calm-starting-point/');
73
+ expect(await feed.text()).not.toContain('future-draft');
74
+
75
+ const robots = await request.get('/robots.txt');
76
+ expect(robots.ok()).toBe(true);
77
+ expect(await robots.text()).toContain('Sitemap: https://example.com/sitemap-index.xml');
78
+
79
+ const manifest = await request.get('/site.webmanifest');
80
+ expect(manifest.ok()).toBe(true);
81
+ expect(manifest.headers()['content-type']).toMatch(/application\/manifest\+json/u);
82
+ expect(await manifest.json()).toMatchObject({ name: 'Forge', start_url: '/' });
83
+
84
+ const missing = await request.get('/does-not-exist/');
85
+ expect(missing.status()).toBe(404);
86
+ expect(await missing.text()).toContain('That page is not here.');
87
+ });
@@ -0,0 +1,47 @@
1
+ import { flavors } from '@catppuccin/palette';
2
+ import { expect, test } from '@playwright/test';
3
+ import type { Page } from '@playwright/test';
4
+
5
+ const toRgb = (hex: string) => {
6
+ const value = Number.parseInt(hex.slice(1), 16);
7
+
8
+ return `rgb(${value >> 16}, ${(value >> 8) & 255}, ${value & 255})`;
9
+ };
10
+
11
+ const pageBackground = async (page: Page) =>
12
+ page.locator('html').evaluate((element) => getComputedStyle(element).backgroundColor);
13
+
14
+ test('uses Latte for light mode and Mocha for dark mode', async ({ page }) => {
15
+ await page.emulateMedia({ colorScheme: 'light' });
16
+ await page.goto('/');
17
+ await expect(page.locator('html')).toHaveCSS('background-color', toRgb(flavors.latte.colors.base.hex));
18
+ await expect(page.locator('main h1')).toHaveCSS('color', toRgb(flavors.latte.colors.text.hex));
19
+
20
+ await page.emulateMedia({ colorScheme: 'dark' });
21
+ await expect(page.locator('html')).toHaveCSS('background-color', toRgb(flavors.mocha.colors.base.hex));
22
+ await expect(page.locator('main h1')).toHaveCSS('color', toRgb(flavors.mocha.colors.text.hex));
23
+ });
24
+
25
+ test('makes every Catppuccin flavor available explicitly', async ({ page }) => {
26
+ await page.goto('/');
27
+
28
+ for (const flavor of ['latte', 'frappe', 'macchiato', 'mocha'] as const) {
29
+ await page.locator('html').evaluate((element, theme) => element.setAttribute('data-theme', theme), flavor);
30
+ expect(await pageBackground(page)).toBe(toRgb(flavors[flavor].colors.base.hex));
31
+ }
32
+ });
33
+
34
+ test('serves fonts locally and presents a visible keyboard focus indicator', async ({ page }) => {
35
+ const fontRequests: string[] = [];
36
+ page.on('request', (request) => {
37
+ if (request.resourceType() === 'font') fontRequests.push(request.url());
38
+ });
39
+
40
+ await page.goto('/');
41
+ await page.keyboard.press('Tab');
42
+
43
+ await expect(page.locator('a').first()).toBeFocused();
44
+ await expect(page.locator('a').first()).not.toHaveCSS('outline-style', 'none');
45
+ expect(fontRequests.length).toBeGreaterThan(0);
46
+ expect(fontRequests.every((url) => new URL(url).origin === 'http://127.0.0.1:4321')).toBe(true);
47
+ });
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "astro/tsconfigs/strict",
3
+ "include": [".astro/types.d.ts", "**/*"],
4
+ "exclude": ["dist", "coverage", "playwright-report", "test-results"]
5
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference types="vitest/config" />
2
+ import { getViteConfig } from 'astro/config';
3
+
4
+ export default getViteConfig({
5
+ test: {
6
+ include: ['src/**/*.test.ts'],
7
+ },
8
+ });
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@rm-industries/create-forge",
3
+ "version": "0.3.0-alpha.0",
4
+ "description": "Create a content-driven Forge website",
5
+ "keywords": [
6
+ "astro",
7
+ "create-forge",
8
+ "generator",
9
+ "sveltia"
10
+ ],
11
+ "homepage": "https://github.com/rm-industries/forge/tree/main/packages/create-forge#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/rm-industries/forge/issues"
14
+ },
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/rm-industries/forge.git",
19
+ "directory": "packages/create-forge"
20
+ },
21
+ "bin": {
22
+ "create-forge": "./dist/index.mjs"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "type": "module",
28
+ "publishConfig": {
29
+ "access": "public",
30
+ "tag": "next"
31
+ },
32
+ "scripts": {
33
+ "build": "tsdown && node scripts/copy-template.ts",
34
+ "prepack": "npm run typecheck && npm test && npm run build",
35
+ "test": "vitest run",
36
+ "test:e2e": "npm run build && node scripts/test-generator-e2e.ts",
37
+ "typecheck": "tsc --project tsconfig.json --noEmit",
38
+ "verify:package": "npm run build && node scripts/verify-package.ts"
39
+ },
40
+ "dependencies": {
41
+ "@clack/prompts": "^1.7.0",
42
+ "commander": "^14.0.3",
43
+ "validate-npm-package-name": "^8.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/validate-npm-package-name": "^4.0.2"
47
+ },
48
+ "engines": {
49
+ "node": "^22.22.2 || ^24.0.0 || ^26.0.0",
50
+ "npm": "^10.9.0 || ^11.0.0"
51
+ }
52
+ }