@rm-industries/create-forge 0.4.0-beta.1 → 0.4.0-beta.3

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 (27) hide show
  1. package/dist/template/.github/workflows/automation.yml +20 -0
  2. package/dist/template/.github/workflows/project.yml +27 -0
  3. package/dist/template/README.md +38 -0
  4. package/dist/template/astro.config.ts +5 -1
  5. package/dist/template/docs/github-pages.md +56 -0
  6. package/dist/template/docs/guides/content-and-cms.md +123 -0
  7. package/dist/template/docs/guides/getting-started.md +96 -0
  8. package/dist/template/docs/guides/quality-and-troubleshooting.md +118 -0
  9. package/dist/template/docs/guides/themes-and-accessibility.md +85 -0
  10. package/dist/template/package-lock.json +420 -479
  11. package/dist/template/package.json +3 -3
  12. package/dist/template/src/components/navigation/Footer.astro +2 -1
  13. package/dist/template/src/components/navigation/Navbar.astro +9 -5
  14. package/dist/template/src/components/navigation/Pagination.astro +4 -2
  15. package/dist/template/src/components/seo/SeoHead.astro +7 -6
  16. package/dist/template/src/components/ui/Card.astro +5 -2
  17. package/dist/template/src/config/deployment.test.ts +16 -0
  18. package/dist/template/src/config/deployment.ts +14 -0
  19. package/dist/template/src/integrations/sveltia/config.ts +3 -2
  20. package/dist/template/src/lib/paths.test.ts +26 -0
  21. package/dist/template/src/lib/paths.ts +17 -0
  22. package/dist/template/src/pages/404.astro +2 -1
  23. package/dist/template/src/pages/about.astro +5 -1
  24. package/dist/template/src/pages/index.astro +4 -3
  25. package/dist/template/src/pages/rss.xml.ts +2 -1
  26. package/dist/template/src/pages/site.webmanifest.ts +3 -2
  27. package/package.json +3 -3
@@ -41,7 +41,7 @@
41
41
  "@fontsource/fira-sans": "^5.3.0",
42
42
  "@rm-industries/content-model": "0.2.0-alpha.0",
43
43
  "@sveltia/cms": "0.203.2",
44
- "astro": "^7.2.3"
44
+ "astro": "^7.2.9"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@astrojs/check": "^0.9.10",
@@ -55,10 +55,10 @@
55
55
  "@vitest/coverage-v8": "^4.1.11",
56
56
  "audit-ci": "7.1.0",
57
57
  "cspell": "^10.0.1",
58
- "daisyui": "^5.7.18",
58
+ "daisyui": "^5.7.22",
59
59
  "knip": "^6.33.0",
60
60
  "markdownlint-cli": "^0.49.0",
61
- "oxfmt": "^0.63.0",
61
+ "oxfmt": "^0.65.0",
62
62
  "oxlint": "^1.72.0",
63
63
  "stylelint": "^17.14.1",
64
64
  "stylelint-config-standard": "^40.0.0",
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import { site } from '../../config/site';
3
3
  import { isExternalHref } from '../../lib/navigation';
4
+ import { resolveSiteHref } from '../../lib/paths';
4
5
  import Container from '../ui/Container.astro';
5
6
  ---
6
7
 
@@ -18,7 +19,7 @@ import Container from '../ui/Container.astro';
18
19
  <h2>Elsewhere</h2>
19
20
  {site.socialLinks.map((item) => (
20
21
  <a
21
- href={item.href}
22
+ href={resolveSiteHref(item.href)}
22
23
  class="link link-hover"
23
24
  target={isExternalHref(item.href) ? '_blank' : undefined}
24
25
  rel={isExternalHref(item.href) ? 'noopener noreferrer' : undefined}
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import type { SiteLink } from '../../config/site';
3
3
  import { isCurrentPath } from '../../lib/navigation';
4
+ import { resolveSiteHref } from '../../lib/paths';
4
5
 
5
6
  interface Props {
6
7
  brand: string;
@@ -10,22 +11,24 @@ interface Props {
10
11
  }
11
12
 
12
13
  const { brand, brandHref = '/', links, currentPath } = Astro.props;
14
+ const resolvedBrandHref = resolveSiteHref(brandHref);
13
15
  ---
14
16
 
15
17
  <div class="navbar rounded-box bg-base-200 shadow-sm">
16
18
  <div class="navbar-start">
17
- <a href={brandHref} class="btn btn-ghost text-xl">{brand}</a>
19
+ <a href={resolvedBrandHref} class="btn btn-ghost text-xl">{brand}</a>
18
20
  </div>
19
21
 
20
22
  <nav class="navbar-end hidden sm:flex" aria-label="Primary navigation">
21
23
  <ul class="menu menu-horizontal" role="list">
22
24
  {
23
25
  links.map((item) => {
24
- const isCurrent = isCurrentPath(currentPath, item.href);
26
+ const href = resolveSiteHref(item.href);
27
+ const isCurrent = isCurrentPath(currentPath, href);
25
28
 
26
29
  return (
27
30
  <li>
28
- <a href={item.href} aria-current={isCurrent ? 'page' : undefined} class:list={[isCurrent && 'menu-active']}>
31
+ <a {href} aria-current={isCurrent ? 'page' : undefined} class:list={[isCurrent && 'menu-active']}>
29
32
  {item.label}
30
33
  </a>
31
34
  </li>
@@ -42,11 +45,12 @@ const { brand, brandHref = '/', links, currentPath } = Astro.props;
42
45
  <ul class="menu" role="list">
43
46
  {
44
47
  links.map((item) => {
45
- const isCurrent = isCurrentPath(currentPath, item.href);
48
+ const href = resolveSiteHref(item.href);
49
+ const isCurrent = isCurrentPath(currentPath, href);
46
50
 
47
51
  return (
48
52
  <li>
49
- <a href={item.href} aria-current={isCurrent ? 'page' : undefined} class:list={[isCurrent && 'menu-active']}>
53
+ <a {href} aria-current={isCurrent ? 'page' : undefined} class:list={[isCurrent && 'menu-active']}>
50
54
  {item.label}
51
55
  </a>
52
56
  </li>
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { resolveSiteHref } from '../../lib/paths';
3
+
2
4
  interface PaginationLink {
3
5
  href: string;
4
6
  label: string;
@@ -16,7 +18,7 @@ const { previous, next, label = 'Pagination' } = Astro.props;
16
18
  <nav aria-label={label} class="join grid grid-cols-2">
17
19
  {
18
20
  previous ? (
19
- <a href={previous.href} rel="prev" class="btn btn-outline join-item">
21
+ <a href={resolveSiteHref(previous.href)} rel="prev" class="btn btn-outline join-item">
20
22
  <span aria-hidden="true">←</span>
21
23
  <span>{previous.label}</span>
22
24
  </a>
@@ -26,7 +28,7 @@ const { previous, next, label = 'Pagination' } = Astro.props;
26
28
  }
27
29
  {
28
30
  next ? (
29
- <a href={next.href} rel="next" class="btn btn-outline join-item">
31
+ <a href={resolveSiteHref(next.href)} rel="next" class="btn btn-outline join-item">
30
32
  <span>{next.label}</span>
31
33
  <span aria-hidden="true">→</span>
32
34
  </a>
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import { site } from '../../config/site';
3
+ import { resolveSiteHref } from '../../lib/paths';
3
4
 
4
5
  interface Props {
5
6
  title?: string;
@@ -21,18 +22,18 @@ const normalizedPath = (() => {
21
22
  const path = `/${canonicalPath}`.replace(/\/{2,}/g, '/');
22
23
  return path === '/' ? path : `${path.replace(/\/$/, '')}/`;
23
24
  })();
24
- const canonicalUrl = new URL(normalizedPath, site.url);
25
+ const canonicalUrl = new URL(resolveSiteHref(normalizedPath), new URL(site.url).origin);
25
26
  const pageTitle = title ? `${title} | ${site.name}` : site.name;
26
- const socialImageUrl = new URL(site.socialImage, site.url);
27
+ const socialImageUrl = new URL(resolveSiteHref(site.socialImage), new URL(site.url).origin);
27
28
  ---
28
29
 
29
30
  <title>{pageTitle}</title>
30
31
  <meta name="description" content={description} />
31
32
  <link rel="canonical" href={canonicalUrl} />
32
- <link rel="sitemap" href="/sitemap-index.xml" />
33
- <link rel="alternate" type="application/rss+xml" title={`${site.name} RSS feed`} href="/rss.xml" />
34
- <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
35
- <link rel="manifest" href="/site.webmanifest" />
33
+ <link rel="sitemap" href={resolveSiteHref('/sitemap-index.xml')} />
34
+ <link rel="alternate" type="application/rss+xml" title={`${site.name} RSS feed`} href={resolveSiteHref('/rss.xml')} />
35
+ <link rel="icon" href={resolveSiteHref('/favicon.svg')} type="image/svg+xml" />
36
+ <link rel="manifest" href={resolveSiteHref('/site.webmanifest')} />
36
37
 
37
38
  <meta property="og:site_name" content={site.name} />
38
39
  <meta property="og:title" content={pageTitle} />
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { resolveSiteHref } from '../../lib/paths';
3
+
2
4
  interface Props {
3
5
  class?: string;
4
6
  href?: string;
@@ -7,11 +9,12 @@ interface Props {
7
9
 
8
10
  const { class: className, href, size = 'md' } = Astro.props;
9
11
  const classes = ['card bg-base-200', `card-${size}`, className];
12
+ const resolvedHref = href ? resolveSiteHref(href) : undefined;
10
13
  ---
11
14
 
12
15
  {
13
- href ? (
14
- <a href={href} class:list={classes}>
16
+ resolvedHref ? (
17
+ <a href={resolvedHref} class:list={classes}>
15
18
  <div class="card-body">
16
19
  <slot />
17
20
  </div>
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { getDeploymentConfig } from './deployment';
4
+
5
+ describe('getDeploymentConfig', () => {
6
+ it('configures a root deployment from an origin-only canonical URL', () => {
7
+ expect(getDeploymentConfig('https://example.com/')).toEqual({ base: undefined, site: 'https://example.com' });
8
+ });
9
+
10
+ it('configures a project deployment from a canonical URL pathname', () => {
11
+ expect(getDeploymentConfig('https://owner.github.io/project/')).toEqual({
12
+ base: '/project',
13
+ site: 'https://owner.github.io',
14
+ });
15
+ });
16
+ });
@@ -0,0 +1,14 @@
1
+ export interface DeploymentConfig {
2
+ base: string | undefined;
3
+ site: string;
4
+ }
5
+
6
+ /** Derive Astro's origin and optional project path from the canonical site URL. */
7
+ export const getDeploymentConfig = (siteUrl: string): DeploymentConfig => {
8
+ const canonicalUrl = new URL(siteUrl);
9
+
10
+ return {
11
+ base: canonicalUrl.pathname === '/' ? undefined : canonicalUrl.pathname.replace(/\/$/, ''),
12
+ site: canonicalUrl.origin,
13
+ };
14
+ };
@@ -3,11 +3,12 @@ import type { CmsConfig } from '@sveltia/cms';
3
3
 
4
4
  import { contentModels } from '../../config/content-models/registry.ts';
5
5
  import { cmsBranding } from '../../config/site.ts';
6
+ import { resolveSiteHref } from '../../lib/paths.ts';
6
7
 
7
8
  export const sveltiaConfig = {
8
9
  load_config_file: false,
9
10
  app_title: cmsBranding.appTitle,
10
- logo: { src: '/favicon.svg' },
11
+ logo: { src: resolveSiteHref('/favicon.svg') },
11
12
  backend: {
12
13
  name: 'github',
13
14
  repo: 'your-github-user/your-repository',
@@ -22,7 +23,7 @@ export const sveltiaConfig = {
22
23
  },
23
24
  },
24
25
  media_folder: 'public/assets',
25
- public_folder: '/assets',
26
+ public_folder: resolveSiteHref('/assets'),
26
27
  output: { omit_empty_optional_fields: true },
27
28
  collections: contentModels.map(createSveltiaCollection),
28
29
  } satisfies CmsConfig;
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { resolveSiteHref } from './paths';
4
+
5
+ describe('resolveSiteHref', () => {
6
+ it('keeps root-relative URLs unchanged for root deployments', () => {
7
+ expect(resolveSiteHref('/articles/', '/')).toBe('/articles/');
8
+ });
9
+
10
+ it('prefixes root-relative URLs for project deployments', () => {
11
+ expect(resolveSiteHref('/articles/', '/forge/')).toBe('/forge/articles/');
12
+ expect(resolveSiteHref('/articles/', 'forge')).toBe('/forge/articles/');
13
+ });
14
+
15
+ it('does not prefix an existing deployment base', () => {
16
+ expect(resolveSiteHref('/forge/articles/', '/forge/')).toBe('/forge/articles/');
17
+ expect(resolveSiteHref('/forge', '/forge/')).toBe('/forge');
18
+ });
19
+
20
+ it.each(['https://example.com', '//cdn.example.com/file.svg', '#content', 'mailto:hello@example.com'])(
21
+ 'keeps non-local URL %s unchanged',
22
+ (href) => {
23
+ expect(resolveSiteHref(href, '/forge/')).toBe(href);
24
+ },
25
+ );
26
+ });
@@ -0,0 +1,17 @@
1
+ const normalizeBase = (base: string): string => {
2
+ const withLeadingSlash = base.startsWith('/') ? base : `/${base}`;
3
+
4
+ if (withLeadingSlash === '/') return withLeadingSlash;
5
+
6
+ return withLeadingSlash.replace(/\/+$/, '');
7
+ };
8
+
9
+ /** Prefix a root-relative URL with Astro's deployment base path. */
10
+ export const resolveSiteHref = (href: string, base = import.meta.env.BASE_URL): string => {
11
+ if (!href.startsWith('/') || href.startsWith('//')) return href;
12
+
13
+ const normalizedBase = normalizeBase(base);
14
+ if (normalizedBase === '/' || href === normalizedBase || href.startsWith(`${normalizedBase}/`)) return href;
15
+
16
+ return `${normalizedBase}${href}`;
17
+ };
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import Container from '../components/ui/Container.astro';
3
3
  import BaseLayout from '../layouts/BaseLayout.astro';
4
+ import { resolveSiteHref } from '../lib/paths';
4
5
  ---
5
6
 
6
7
  <BaseLayout title="Page not found" description="The requested page could not be found." canonicalPath="/404/">
@@ -10,7 +11,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
10
11
  <div class="badge badge-outline">404</div>
11
12
  <h1 class="text-4xl font-bold">That page is not here.</h1>
12
13
  <p>The address may have changed, or the page may no longer exist.</p>
13
- <a href="/" class="btn btn-primary">Return home</a>
14
+ <a href={resolveSiteHref('/')} class="btn btn-primary">Return home</a>
14
15
  </div>
15
16
  </section>
16
17
  </Container>
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import Container from '../components/ui/Container.astro';
3
3
  import BaseLayout from '../layouts/BaseLayout.astro';
4
+ import { resolveSiteHref } from '../lib/paths';
4
5
  ---
5
6
 
6
7
  <BaseLayout title="About" description="Learn how this generic Forge starter is structured and how to make it your own.">
@@ -16,7 +17,10 @@ import BaseLayout from '../layouts/BaseLayout.astro';
16
17
  <h2>What is already connected</h2>
17
18
  <p>Semantic navigation, responsive layout primitives, local fonts, theme-aware styling, canonical metadata, a sitemap, an RSS feed, and automated accessibility checks are ready to extend.</p>
18
19
 
19
- <p><a href="/articles/">Explore the example articles</a> or return to the <a href="/">homepage</a>.</p>
20
+ <p>
21
+ <a href={resolveSiteHref('/articles/')}>Explore the example articles</a> or return to the
22
+ <a href={resolveSiteHref('/')}>homepage</a>.
23
+ </p>
20
24
  </article>
21
25
  </Container>
22
26
  </BaseLayout>
@@ -3,6 +3,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
3
3
  import { site } from '../config/site';
4
4
  import Card from '../components/ui/Card.astro';
5
5
  import Container from '../components/ui/Container.astro';
6
+ import { resolveSiteHref } from '../lib/paths';
6
7
  ---
7
8
 
8
9
  <BaseLayout>
@@ -14,8 +15,8 @@ import Container from '../components/ui/Container.astro';
14
15
  <h1 class="text-5xl font-bold tracking-tight sm:text-6xl">Build something worth publishing.</h1>
15
16
  <p class="text-lg">{site.description} Start with accessible components, typed configuration, and a theme system that works without client JavaScript.</p>
16
17
  <div class="flex flex-wrap justify-center gap-3">
17
- <a href="/articles/" class="btn btn-primary">Read the examples</a>
18
- <a href="/about/" class="btn btn-ghost">About this starter</a>
18
+ <a href={resolveSiteHref('/articles/')} class="btn btn-primary">Read the examples</a>
19
+ <a href={resolveSiteHref('/about/')} class="btn btn-ghost">About this starter</a>
19
20
  </div>
20
21
  </div>
21
22
  </div>
@@ -56,7 +57,7 @@ import Container from '../components/ui/Container.astro';
56
57
  <h2 id="start-title" class="card-title text-3xl">Replace the examples with your own story.</h2>
57
58
  <p>Update the typed site configuration, choose your content model, and grow the template one reusable piece at a time.</p>
58
59
  <div class="card-actions">
59
- <a href="/articles/" class="btn">Browse the articles</a>
60
+ <a href={resolveSiteHref('/articles/')} class="btn">Browse the articles</a>
60
61
  </div>
61
62
  </div>
62
63
  </section>
@@ -2,6 +2,7 @@ import rss from '@astrojs/rss';
2
2
 
3
3
  import { site } from '../config/site';
4
4
  import { getArticles } from '../lib/articles';
5
+ import { resolveSiteHref } from '../lib/paths';
5
6
 
6
7
  export const GET = async () =>
7
8
  rss({
@@ -11,7 +12,7 @@ export const GET = async () =>
11
12
  items: (await getArticles()).map((article) => ({
12
13
  title: article.data.title,
13
14
  description: article.data.description,
14
- link: `/articles/${article.id}/`,
15
+ link: resolveSiteHref(`/articles/${article.id}/`),
15
16
  pubDate: article.data.publishedAt,
16
17
  })),
17
18
  });
@@ -1,6 +1,7 @@
1
1
  import type { APIRoute } from 'astro';
2
2
 
3
3
  import { site } from '../config/site';
4
+ import { resolveSiteHref } from '../lib/paths';
4
5
  import { darkThemeColor } from '../themes/site-theme';
5
6
 
6
7
  export const GET: APIRoute = () =>
@@ -9,11 +10,11 @@ export const GET: APIRoute = () =>
9
10
  name: site.name,
10
11
  short_name: site.name,
11
12
  description: site.description,
12
- start_url: '/',
13
+ start_url: resolveSiteHref('/'),
13
14
  display: 'standalone',
14
15
  background_color: darkThemeColor,
15
16
  theme_color: darkThemeColor,
16
- icons: [{ src: '/favicon.svg', sizes: 'any', type: 'image/svg+xml' }],
17
+ icons: [{ src: resolveSiteHref('/favicon.svg'), sizes: 'any', type: 'image/svg+xml' }],
17
18
  }),
18
19
  { headers: { 'Content-Type': 'application/manifest+json' } },
19
20
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rm-industries/create-forge",
3
- "version": "0.4.0-beta.1",
3
+ "version": "0.4.0-beta.3",
4
4
  "description": "Create a content-driven Forge website",
5
5
  "keywords": [
6
6
  "astro",
@@ -19,7 +19,7 @@
19
19
  "directory": "packages/create-forge"
20
20
  },
21
21
  "bin": {
22
- "create-forge": "./dist/index.mjs"
22
+ "create-forge": "dist/index.mjs"
23
23
  },
24
24
  "files": [
25
25
  "dist"
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@clack/prompts": "^1.7.0",
42
- "commander": "^14.0.3",
42
+ "commander": "^15.0.0",
43
43
  "validate-npm-package-name": "^8.0.0"
44
44
  },
45
45
  "devDependencies": {