@anglefeint/astro-theme 0.1.15 → 0.1.17

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.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  doc_id: package_readme
3
3
  doc_role: package-guide
4
+ doc_purpose: Package-level install, upgrade, and manual integration reference for advanced users.
4
5
  doc_scope: [package-install, package-upgrade, package-usage]
5
6
  update_triggers: [package-change, command-change, export-change]
6
7
  source_of_truth: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anglefeint/astro-theme",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "type": "module",
5
5
  "description": "Anglefeint core theme package for Astro",
6
6
  "keywords": [
@@ -13,6 +13,9 @@
13
13
  "matrix"
14
14
  ],
15
15
  "license": "MIT",
16
+ "engines": {
17
+ "node": ">=22.12.0"
18
+ },
16
19
  "files": [
17
20
  "src/index.ts",
18
21
  "src/consts.ts",
@@ -43,6 +46,6 @@
43
46
  "./consts": "./src/consts.ts"
44
47
  },
45
48
  "peerDependencies": {
46
- "astro": "^5.0.0"
49
+ "astro": "^5.0.0 || ^6.0.0-beta.0"
47
50
  }
48
51
  }
@@ -1,54 +1,15 @@
1
1
  import { access, mkdir, writeFile } from 'node:fs/promises';
2
2
  import { constants } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import {
5
+ buildNewPageTemplate,
6
+ isValidNewPageTheme,
7
+ parseNewPageArgs,
8
+ usageNewPage,
9
+ validatePageSlug,
10
+ } from './scaffold/new-page.mjs';
4
11
 
5
- const THEMES = ['base', 'cyber', 'ai', 'hacker', 'matrix'];
6
12
  const PAGES_ROOT = path.resolve(process.cwd(), 'src/pages/[lang]');
7
- const LAYOUT_BY_THEME = {
8
- base: 'BasePageLayout',
9
- ai: 'AiPageLayout',
10
- cyber: 'CyberPageLayout',
11
- hacker: 'HackerPageLayout',
12
- matrix: 'MatrixPageLayout',
13
- };
14
-
15
- function usage() {
16
- console.error('Usage: npm run new-page -- <slug> [--theme <base|cyber|ai|hacker|matrix>]');
17
- }
18
-
19
- function parseArgs(argv) {
20
- const args = argv.slice(2);
21
- const positional = [];
22
- let theme = 'base';
23
-
24
- for (let i = 0; i < args.length; i += 1) {
25
- const token = args[i];
26
- if (token === '--theme') {
27
- theme = args[i + 1] ?? '';
28
- i += 1;
29
- continue;
30
- }
31
- positional.push(token);
32
- }
33
-
34
- return { slug: positional[0], theme };
35
- }
36
-
37
- function validateSlug(slug) {
38
- if (!slug) return false;
39
- if (slug.startsWith('/') || slug.endsWith('/')) return false;
40
- const parts = slug.split('/');
41
- return parts.every((part) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(part));
42
- }
43
-
44
- function toTitleFromSlug(slug) {
45
- const leaf = slug.split('/').pop() ?? slug;
46
- return leaf
47
- .split('-')
48
- .filter(Boolean)
49
- .map((segment) => segment[0].toUpperCase() + segment.slice(1))
50
- .join(' ');
51
- }
52
13
 
53
14
  async function exists(filePath) {
54
15
  try {
@@ -59,42 +20,20 @@ async function exists(filePath) {
59
20
  }
60
21
  }
61
22
 
62
- function templateFor({ slug, theme }) {
63
- const title = toTitleFromSlug(slug) || 'New Page';
64
- const layoutName = LAYOUT_BY_THEME[theme];
65
- return `---
66
- import type { GetStaticPaths } from 'astro';
67
- import ${layoutName} from '@anglefeint/astro-theme/layouts/${layoutName}.astro';
68
- import { SUPPORTED_LOCALES } from '@anglefeint/astro-theme/i18n/config';
69
-
70
- export const getStaticPaths = (() => SUPPORTED_LOCALES.map((lang) => ({ params: { lang } }))) satisfies GetStaticPaths;
71
-
72
- const locale = Astro.params.lang;
73
- const pageTitle = '${title}';
74
- const pageDescription = 'A custom page built from the ${theme} theme shell.';
75
- ---
76
-
77
- <${layoutName} locale={locale} title={pageTitle} description={pageDescription}>
78
- \t<h1>${title}</h1>
79
- \t<p>Replace this content with your own page content.</p>
80
- </${layoutName}>
81
- `;
82
- }
83
-
84
23
  async function main() {
85
- const { slug, theme: rawTheme } = parseArgs(process.argv);
24
+ const { slug, theme: rawTheme } = parseNewPageArgs(process.argv);
86
25
  const theme = String(rawTheme || '').toLowerCase();
87
26
 
88
27
  if (!slug) {
89
- usage();
28
+ console.error(usageNewPage());
90
29
  process.exit(1);
91
30
  }
92
- if (!validateSlug(slug)) {
31
+ if (!validatePageSlug(slug)) {
93
32
  console.error('Invalid page slug. Use lowercase letters, numbers, hyphens, and optional nested paths.');
94
33
  process.exit(1);
95
34
  }
96
- if (!THEMES.includes(theme)) {
97
- console.error(`Invalid theme "${rawTheme}". Use one of: ${THEMES.join(', ')}.`);
35
+ if (!isValidNewPageTheme(theme)) {
36
+ console.error('Invalid theme. Use one of: base, cyber, ai, hacker, matrix.');
98
37
  process.exit(1);
99
38
  }
100
39
 
@@ -109,7 +48,7 @@ async function main() {
109
48
  await mkdir(targetDir, { recursive: true });
110
49
  await writeFile(
111
50
  targetPath,
112
- templateFor({ slug, theme }),
51
+ buildNewPageTemplate({ slug, theme }),
113
52
  'utf8',
114
53
  );
115
54
 
@@ -1,47 +1,18 @@
1
- import { mkdir, writeFile, access, readdir } from 'node:fs/promises';
1
+ import { mkdir, writeFile, access } from 'node:fs/promises';
2
2
  import { constants } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { SUPPORTED_LOCALES } from './i18n/locales.mjs';
5
+ import {
6
+ buildNewPostTemplate,
7
+ loadDefaultCovers,
8
+ pickDefaultCoverBySlug,
9
+ usageNewPost,
10
+ validatePostSlug,
11
+ } from './scaffold/new-post.mjs';
5
12
 
6
13
  const CONTENT_ROOT = path.resolve(process.cwd(), 'src/content/blog');
7
14
  const DEFAULT_COVERS_ROOT = path.resolve(process.cwd(), 'src/assets/blog/default-covers');
8
15
 
9
- function toTitleFromSlug(slug) {
10
- return slug
11
- .split('-')
12
- .filter(Boolean)
13
- .map((segment) => segment[0].toUpperCase() + segment.slice(1))
14
- .join(' ');
15
- }
16
-
17
- function validateSlug(slug) {
18
- return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug);
19
- }
20
-
21
- function hashString(input) {
22
- let hash = 5381;
23
- for (let i = 0; i < input.length; i += 1) {
24
- hash = ((hash << 5) + hash + input.charCodeAt(i)) >>> 0;
25
- }
26
- return hash >>> 0;
27
- }
28
-
29
- function normalizePathForFrontmatter(filePath) {
30
- return filePath.split(path.sep).join('/');
31
- }
32
-
33
- async function loadDefaultCovers() {
34
- try {
35
- const entries = await readdir(DEFAULT_COVERS_ROOT, { withFileTypes: true });
36
- return entries
37
- .filter((entry) => entry.isFile() && /\.(webp|png|jpe?g)$/i.test(entry.name))
38
- .map((entry) => path.join(DEFAULT_COVERS_ROOT, entry.name))
39
- .sort((a, b) => a.localeCompare(b));
40
- } catch {
41
- return [];
42
- }
43
- }
44
-
45
16
  async function exists(filePath) {
46
17
  try {
47
18
  await access(filePath, constants.F_OK);
@@ -51,54 +22,20 @@ async function exists(filePath) {
51
22
  }
52
23
  }
53
24
 
54
- function templateFor(locale, slug, pubDate, heroImage) {
55
- const titleByLocale = {
56
- en: toTitleFromSlug(slug),
57
- ja: '新しい記事タイトル',
58
- ko: '새 글 제목',
59
- es: 'Título del nuevo artículo',
60
- zh: '新文章标题',
61
- };
62
- const descriptionByLocale = {
63
- en: `A short EN post scaffold for "${slug}".`,
64
- ja: `「${slug}」用の短い日本語記事テンプレートです。`,
65
- ko: `"${slug}"용 한국어 글 템플릿입니다.`,
66
- es: `Plantilla breve en español para "${slug}".`,
67
- zh: `“${slug}”的中文文章模板。`,
68
- };
69
- const bodyByLocale = {
70
- en: `Write your EN content for "${slug}" here.`,
71
- ja: `ここに「${slug}」の日本語本文を書いてください。`,
72
- ko: `여기에 "${slug}" 한국어 본문을 작성하세요.`,
73
- es: `Escribe aquí el contenido en español para "${slug}".`,
74
- zh: `请在这里填写“${slug}”的中文正文。`,
75
- };
76
- return `---
77
- title: '${titleByLocale[locale]}'
78
- subtitle: ''
79
- description: '${descriptionByLocale[locale]}'
80
- pubDate: '${pubDate}'
81
- ${heroImage ? `heroImage: '${heroImage}'` : ''}
82
- ---
83
-
84
- ${bodyByLocale[locale]}
85
- `;
86
- }
87
-
88
25
  async function main() {
89
26
  const slug = process.argv[2];
90
27
  if (!slug) {
91
- console.error('Usage: npm run new-post -- <slug>');
28
+ console.error(usageNewPost());
92
29
  process.exit(1);
93
30
  }
94
31
 
95
- if (!validateSlug(slug)) {
32
+ if (!validatePostSlug(slug)) {
96
33
  console.error('Invalid slug. Use lowercase letters, numbers, and hyphens only.');
97
34
  process.exit(1);
98
35
  }
99
36
 
100
37
  const pubDate = new Date().toISOString().slice(0, 10);
101
- const defaultCovers = await loadDefaultCovers();
38
+ const defaultCovers = await loadDefaultCovers(DEFAULT_COVERS_ROOT);
102
39
  const created = [];
103
40
  const skipped = [];
104
41
 
@@ -112,13 +49,9 @@ async function main() {
112
49
  continue;
113
50
  }
114
51
 
115
- let heroImage = '';
116
- if (defaultCovers.length > 0) {
117
- const coverPath = defaultCovers[hashString(slug) % defaultCovers.length];
118
- heroImage = normalizePathForFrontmatter(path.relative(localeDir, coverPath));
119
- }
52
+ const heroImage = pickDefaultCoverBySlug(slug, localeDir, defaultCovers);
120
53
 
121
- await writeFile(filePath, templateFor(locale, slug, pubDate, heroImage), 'utf8');
54
+ await writeFile(filePath, buildNewPostTemplate(locale, slug, pubDate, heroImage), 'utf8');
122
55
  created.push(filePath);
123
56
  }
124
57
 
@@ -1,5 +1,6 @@
1
- import { defineCollection, z } from 'astro:content';
1
+ import { defineCollection } from 'astro:content';
2
2
  import { glob } from 'astro/loaders';
3
+ import { z } from 'astro/zod';
3
4
 
4
5
  const blog = defineCollection({
5
6
  // Load Markdown and MDX files in the `src/content/blog/` directory.
@@ -26,7 +27,7 @@ const blog = defineCollection({
26
27
  author: z.string().optional(),
27
28
  tags: z.array(z.string()).optional(),
28
29
  canonicalTopic: z.string().optional(),
29
- sourceLinks: z.array(z.string().url()).optional(),
30
+ sourceLinks: z.array(z.url()).optional(),
30
31
  }),
31
32
  });
32
33
 
@@ -1,6 +1,6 @@
1
1
  import type { Locale } from './config';
2
2
 
3
- type Messages = {
3
+ export type Messages = {
4
4
  siteTitle: string;
5
5
  siteDescription: string;
6
6
  langLabel: string;
@@ -39,7 +39,7 @@ type Messages = {
39
39
  };
40
40
  };
41
41
 
42
- const MESSAGES: Record<Locale, Messages> = {
42
+ export const DEFAULT_MESSAGES: Record<Locale, Messages> = {
43
43
  en: {
44
44
  siteTitle: 'Angle Feint',
45
45
  siteDescription: 'Cinematic web interfaces and AI-era engineering essays.',
@@ -208,5 +208,5 @@ const MESSAGES: Record<Locale, Messages> = {
208
208
  };
209
209
 
210
210
  export function getMessages(locale: Locale): Messages {
211
- return MESSAGES[locale];
211
+ return DEFAULT_MESSAGES[locale];
212
212
  }