@hutusi/amytis 1.5.5

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 (179) hide show
  1. package/.github/workflows/ci.yml +33 -0
  2. package/.github/workflows/publish.yml +53 -0
  3. package/AGENTS.md +41 -0
  4. package/CLAUDE.md +200 -0
  5. package/GEMINI.md +84 -0
  6. package/README.md +172 -0
  7. package/TODO.md +76 -0
  8. package/bun.lock +1530 -0
  9. package/content/about.mdx +23 -0
  10. package/content/books/sample-book/index.mdx +24 -0
  11. package/content/books/sample-book/introduction.mdx +34 -0
  12. package/content/books/sample-book/setup.mdx +48 -0
  13. package/content/books/sample-book/writing-content.mdx +49 -0
  14. package/content/flows/2026/02/05.md +8 -0
  15. package/content/flows/2026/02/10.mdx +8 -0
  16. package/content/flows/2026/02/15.md +8 -0
  17. package/content/flows/2026/02/18.mdx +14 -0
  18. package/content/posts/2026-01-12-the-art-of-algorithms.mdx +49 -0
  19. package/content/posts/2026-01-15-nested-image-test/images/test.svg +5 -0
  20. package/content/posts/2026-01-15-nested-image-test/index.mdx +27 -0
  21. package/content/posts/2026-01-21-kitchen-sink/assets/test.svg +5 -0
  22. package/content/posts/2026-01-21-kitchen-sink/index.mdx +169 -0
  23. package/content/posts/asynchronous-javascript.mdx +49 -0
  24. package/content/posts/draft-post.mdx +13 -0
  25. package/content/posts/future-post.mdx +12 -0
  26. package/content/posts/legacy-markdown.md +60 -0
  27. package/content/posts/markdown-features.mdx +78 -0
  28. package/content/posts/modern-css-layouts.mdx +45 -0
  29. package/content/posts/multilingual-test.mdx +124 -0
  30. package/content/posts/syntax-highlighting-showcase.mdx +528 -0
  31. package/content/posts/understanding-react-hooks.mdx +48 -0
  32. package/content/posts/welcome-to-amytis.mdx +21 -0
  33. package/content/posts//344/270/255/346/226/207/346/265/213/350/257/225/346/226/207/347/253/240.mdx +54 -0
  34. package/content/series/ai-nexus-weekly/index.mdx +10 -0
  35. package/content/series/ai-nexus-weekly/week-1.mdx +20 -0
  36. package/content/series/ai-nexus-weekly/week-10.mdx +20 -0
  37. package/content/series/ai-nexus-weekly/week-11.mdx +20 -0
  38. package/content/series/ai-nexus-weekly/week-12.mdx +20 -0
  39. package/content/series/ai-nexus-weekly/week-2.mdx +20 -0
  40. package/content/series/ai-nexus-weekly/week-3.mdx +20 -0
  41. package/content/series/ai-nexus-weekly/week-4.mdx +20 -0
  42. package/content/series/ai-nexus-weekly/week-5.mdx +20 -0
  43. package/content/series/ai-nexus-weekly/week-6.mdx +20 -0
  44. package/content/series/ai-nexus-weekly/week-7.mdx +20 -0
  45. package/content/series/ai-nexus-weekly/week-8.mdx +20 -0
  46. package/content/series/ai-nexus-weekly/week-9.mdx +20 -0
  47. package/content/series/digital-garden/01-philosophy/index.mdx +23 -0
  48. package/content/series/digital-garden/01-philosophy.mdx +30 -0
  49. package/content/series/digital-garden/02-architecture.mdx +19 -0
  50. package/content/series/digital-garden/index.mdx +11 -0
  51. package/content/series/markdown-showcase/index.mdx +11 -0
  52. package/content/series/markdown-showcase/mathematical-notation.mdx +32 -0
  53. package/content/series/markdown-showcase/syntax-highlighting.mdx +119 -0
  54. package/content/series/markdown-showcase/visuals-and-diagrams.mdx +27 -0
  55. package/content/series/nextjs-deep-dive/01-getting-started.mdx +66 -0
  56. package/content/series/nextjs-deep-dive/02-routing-mastery/assets/diagram.svg +8 -0
  57. package/content/series/nextjs-deep-dive/02-routing-mastery/assets/m-p-model.png +0 -0
  58. package/content/series/nextjs-deep-dive/02-routing-mastery/index.mdx +138 -0
  59. package/content/series/nextjs-deep-dive/index.mdx +12 -0
  60. package/docs/ARCHITECTURE.md +103 -0
  61. package/docs/CONTRIBUTING.md +86 -0
  62. package/docs/deployment.md +319 -0
  63. package/eslint.config.mjs +18 -0
  64. package/next.config.ts +25 -0
  65. package/package.json +81 -0
  66. package/postcss.config.mjs +7 -0
  67. package/public/file.svg +1 -0
  68. package/public/globe.svg +1 -0
  69. package/public/icon.svg +9 -0
  70. package/public/logo.svg +11 -0
  71. package/public/next-image-export-optimizer-hashes.json +7 -0
  72. package/public/next.svg +1 -0
  73. package/public/screenshot.png +0 -0
  74. package/public/vercel.svg +1 -0
  75. package/public/window.svg +1 -0
  76. package/scripts/copy-assets.ts +211 -0
  77. package/scripts/new-flow.ts +47 -0
  78. package/scripts/new-from-images.ts +141 -0
  79. package/scripts/new-from-pdf.ts +105 -0
  80. package/scripts/new-post.ts +98 -0
  81. package/scripts/new-series.ts +40 -0
  82. package/scripts/series-draft.ts +136 -0
  83. package/site.config.ts +91 -0
  84. package/src/app/[slug]/page.tsx +67 -0
  85. package/src/app/archive/page.tsx +147 -0
  86. package/src/app/authors/[author]/page.tsx +210 -0
  87. package/src/app/books/[slug]/[chapter]/page.tsx +54 -0
  88. package/src/app/books/[slug]/page.tsx +156 -0
  89. package/src/app/books/page.tsx +63 -0
  90. package/src/app/favicon.ico +0 -0
  91. package/src/app/feed.xml/route.ts +44 -0
  92. package/src/app/flows/[year]/[month]/[day]/page.tsx +105 -0
  93. package/src/app/flows/[year]/[month]/page.tsx +72 -0
  94. package/src/app/flows/[year]/page.tsx +82 -0
  95. package/src/app/flows/page/[page]/page.tsx +63 -0
  96. package/src/app/flows/page.tsx +38 -0
  97. package/src/app/globals.css +406 -0
  98. package/src/app/layout.tsx +114 -0
  99. package/src/app/page/[page]/page.tsx +60 -0
  100. package/src/app/page.tsx +110 -0
  101. package/src/app/posts/[slug]/page.tsx +119 -0
  102. package/src/app/posts/page/[page]/page.tsx +58 -0
  103. package/src/app/posts/page.tsx +40 -0
  104. package/src/app/search.json/route.ts +49 -0
  105. package/src/app/series/[slug]/page/[page]/page.tsx +141 -0
  106. package/src/app/series/[slug]/page.tsx +139 -0
  107. package/src/app/series/page.tsx +96 -0
  108. package/src/app/sitemap.ts +112 -0
  109. package/src/app/tags/[tag]/page.tsx +76 -0
  110. package/src/app/tags/page.tsx +37 -0
  111. package/src/components/Analytics.tsx +49 -0
  112. package/src/components/AuthorStats.tsx +34 -0
  113. package/src/components/BookMobileNav.tsx +171 -0
  114. package/src/components/BookSidebar.tsx +275 -0
  115. package/src/components/CodeBlock.tsx +110 -0
  116. package/src/components/Comments.tsx +63 -0
  117. package/src/components/CoverImage.tsx +93 -0
  118. package/src/components/CuratedSeriesSection.tsx +124 -0
  119. package/src/components/ExternalLinks.tsx +45 -0
  120. package/src/components/FeaturedStoriesSection.tsx +106 -0
  121. package/src/components/FlowCalendarSidebar.tsx +249 -0
  122. package/src/components/FlowContent.tsx +96 -0
  123. package/src/components/FlowTimelineEntry.tsx +34 -0
  124. package/src/components/Footer.tsx +104 -0
  125. package/src/components/Hero.tsx +126 -0
  126. package/src/components/HorizontalScroll.tsx +128 -0
  127. package/src/components/LanguageProvider.tsx +80 -0
  128. package/src/components/LanguageSwitch.tsx +17 -0
  129. package/src/components/LatestWritingSection.tsx +45 -0
  130. package/src/components/MarkdownRenderer.tsx +135 -0
  131. package/src/components/Mermaid.tsx +89 -0
  132. package/src/components/Navbar.tsx +243 -0
  133. package/src/components/PageHeader.tsx +39 -0
  134. package/src/components/Pagination.tsx +120 -0
  135. package/src/components/PostCard.tsx +30 -0
  136. package/src/components/PostList.tsx +104 -0
  137. package/src/components/PostSidebar.tsx +225 -0
  138. package/src/components/ReadingProgressBar.tsx +37 -0
  139. package/src/components/RecentNotesSection.tsx +56 -0
  140. package/src/components/RelatedPosts.tsx +34 -0
  141. package/src/components/Search.tsx +151 -0
  142. package/src/components/SelectedBooksSection.tsx +80 -0
  143. package/src/components/SeriesCatalog.tsx +112 -0
  144. package/src/components/SeriesList.tsx +167 -0
  145. package/src/components/SeriesSidebar.tsx +132 -0
  146. package/src/components/SimpleLayoutHeader.tsx +38 -0
  147. package/src/components/Skeleton.tsx +131 -0
  148. package/src/components/TableOfContents.tsx +158 -0
  149. package/src/components/Tag.tsx +47 -0
  150. package/src/components/TagPageHeader.tsx +38 -0
  151. package/src/components/ThemeProvider.tsx +12 -0
  152. package/src/components/ThemeToggle.tsx +68 -0
  153. package/src/components/TranslatedText.tsx +13 -0
  154. package/src/fonts/Inter-Bold.woff2 +0 -0
  155. package/src/fonts/Inter-Regular.woff2 +0 -0
  156. package/src/fonts/LibreBaskerville-Bold.ttf +0 -0
  157. package/src/fonts/LibreBaskerville-Italic.ttf +0 -0
  158. package/src/fonts/LibreBaskerville-Regular.ttf +0 -0
  159. package/src/i18n/translations.ts +135 -0
  160. package/src/layouts/BookLayout.tsx +109 -0
  161. package/src/layouts/PostLayout.tsx +118 -0
  162. package/src/layouts/SimpleLayout.tsx +31 -0
  163. package/src/lib/i18n.ts +35 -0
  164. package/src/lib/markdown.test.ts +127 -0
  165. package/src/lib/markdown.ts +1067 -0
  166. package/src/lib/rehype-image-metadata.ts +54 -0
  167. package/src/lib/shuffle.ts +11 -0
  168. package/templates/default.mdx +13 -0
  169. package/tests/e2e/navigation.test.ts +51 -0
  170. package/tests/e2e/series-routes.test.ts +63 -0
  171. package/tests/e2e/smoke.test.ts +19 -0
  172. package/tests/integration/markdown-features.test.ts +54 -0
  173. package/tests/integration/posts.test.ts +57 -0
  174. package/tests/integration/reading-time-headings.test.ts +79 -0
  175. package/tests/integration/series-draft.test.ts +46 -0
  176. package/tests/integration/series.test.ts +79 -0
  177. package/tests/tooling/new-from-images.test.ts +173 -0
  178. package/tests/tooling/new-post.test.ts +72 -0
  179. package/tsconfig.json +34 -0
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@hutusi/amytis",
3
+ "version": "1.5.5",
4
+ "description": "A high-performance digital garden and blog engine with Next.js 16 and Tailwind CSS v4",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/hutusi/amytis.git"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/hutusi/amytis/issues"
11
+ },
12
+ "homepage": "https://github.com/hutusi/amytis#readme",
13
+ "private": false,
14
+ "packageManager": "bun@1.3.4",
15
+ "scripts": {
16
+ "dev": "next dev",
17
+ "build": "bun scripts/copy-assets.ts && next build && next-image-export-optimizer",
18
+ "build:dev": "bun scripts/copy-assets.ts && next build",
19
+ "validate": "bun run lint && bun run test && bun run build:dev",
20
+ "clean": "rm -rf .next out public/posts public/books public/flows",
21
+ "new": "bun scripts/new-post.ts",
22
+ "new-weekly": "bun scripts/new-post.ts --series ai-nexus-weekly --md --folder --prefix weekly",
23
+ "new-series": "bun scripts/new-series.ts",
24
+ "new-from-pdf": "bun scripts/new-from-pdf.ts",
25
+ "new-from-images": "bun scripts/new-from-images.ts",
26
+ "new-flow": "bun scripts/new-flow.ts",
27
+ "series-draft": "bun scripts/series-draft.ts",
28
+ "start": "next start",
29
+ "lint": "eslint",
30
+ "test": "bun test",
31
+ "test:unit": "bun test src",
32
+ "test:int": "bun test tests/integration",
33
+ "test:e2e": "bun test tests/e2e"
34
+ },
35
+ "dependencies": {
36
+ "@giscus/react": "^3.1.0",
37
+ "@tailwindcss/typography": "^0.5.19",
38
+ "fuse.js": "^7.1.0",
39
+ "github-slugger": "^2.0.0",
40
+ "gray-matter": "^4.0.3",
41
+ "image-size": "^2.0.2",
42
+ "mermaid": "^11.12.3",
43
+ "next": "16.1.6",
44
+ "next-image-export-optimizer": "^1.20.1",
45
+ "next-themes": "^0.4.6",
46
+ "react": "19.2.4",
47
+ "react-dom": "19.2.4",
48
+ "react-markdown": "^10.1.0",
49
+ "react-syntax-highlighter": "^16.1.0",
50
+ "rehype-katex": "^7.0.1",
51
+ "rehype-raw": "^7.0.0",
52
+ "rehype-slug": "^6.0.0",
53
+ "remark-gfm": "^4.0.1",
54
+ "remark-math": "^6.0.0",
55
+ "unist-util-visit": "^5.1.0",
56
+ "zod": "^4.3.6"
57
+ },
58
+ "devDependencies": {
59
+ "@tailwindcss/postcss": "^4.1.18",
60
+ "@types/bun": "^1.3.9",
61
+ "@types/image-size": "^0.8.0",
62
+ "@types/node": "^24.10.13",
63
+ "@types/react": "^19.2.14",
64
+ "@types/react-dom": "^19.2.3",
65
+ "@types/react-syntax-highlighter": "^15.5.13",
66
+ "babel-plugin-react-compiler": "1.0.0",
67
+ "eslint": "^9.0.0",
68
+ "eslint-config-next": "16.1.6",
69
+ "pdf-to-img": "^5.0.0",
70
+ "tailwindcss": "^4.1.18",
71
+ "typescript": "^5.9.3"
72
+ },
73
+ "ignoreScripts": [
74
+ "sharp",
75
+ "unrs-resolver"
76
+ ],
77
+ "trustedDependencies": [
78
+ "sharp",
79
+ "unrs-resolver"
80
+ ]
81
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="#059669" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <!-- Letter A structure -->
3
+ <path d="M16 4 L7 28" />
4
+ <path d="M16 4 L25 28" />
5
+ <!-- Crossbar / Leaf stem -->
6
+ <path d="M11.5 18 H 20.5" />
7
+ <!-- Leaf accent -->
8
+ <path d="M20.5 18 Q 26 14 26 8 Q 23 12 20.5 18" fill="#059669" stroke="none" />
9
+ </svg>
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 32" fill="none">
2
+ <!-- Icon Part -->
3
+ <g>
4
+ <path d="M16 4 L7 28" stroke="#059669" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
5
+ <path d="M16 4 L25 28" stroke="#059669" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
6
+ <path d="M11.5 18 H 20.5" stroke="#059669" stroke-width="2" stroke-linecap="round" />
7
+ <path d="M20.5 18 Q 26 14 26 8 Q 23 12 20.5 18" fill="#059669" />
8
+ </g>
9
+ <!-- Text Part -->
10
+ <text x="36" y="24" font-family="serif" font-size="22" font-weight="bold" fill="#1c1917" letter-spacing="0.5">Amytis</text>
11
+ </svg>
@@ -0,0 +1,7 @@
1
+ {
2
+ "posts/02-routing-mastery/assets/m-p-model.png": "fDmvlEkZnE-UCvPK4gmDkJD7SU8coOTl4iw5hpsmcWI=",
3
+ "posts/advanced-markdown/images/m-p-model.png": "BdYG3Wq+FcOnoSrZeJeb6x2V-LsB-gwvms3WQJFq+Wg=",
4
+ "posts/part-1/images/m-p-model.png": "94OaqjvkCmDcz+Iuhw14vHXYUzYl3Y8+wwoU1CQZ6l0=",
5
+ "posts/part-2-rich-content/images/m-p-model.png": "Vp5bOs9N2OHHjMM2PFntkkCl1uY7DleLymwrSHbecOA=",
6
+ "/screenshot.png": "dGCNrjp1oMIL3NrVh1VDW0K+7Pp-cI5KH6tW4-6o9zg="
7
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
Binary file
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -0,0 +1,211 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { siteConfig } from '../site.config';
4
+
5
+ const srcDir = path.join(process.cwd(), 'content', 'posts');
6
+ const seriesSrcDir = path.join(process.cwd(), 'content', 'series');
7
+ const booksSrcDir = path.join(process.cwd(), 'content', 'books');
8
+ const flowsSrcDir = path.join(process.cwd(), 'content', 'flows');
9
+ const destDir = path.join(process.cwd(), 'public', 'posts');
10
+ const booksDestDir = path.join(process.cwd(), 'public', 'books');
11
+ const flowsDestDir = path.join(process.cwd(), 'public', 'flows');
12
+
13
+ function copyRecursive(src: string, dest: string) {
14
+ if (!fs.existsSync(src)) return;
15
+
16
+ if (!fs.existsSync(dest)) {
17
+ fs.mkdirSync(dest, { recursive: true });
18
+ }
19
+
20
+ const entries = fs.readdirSync(src, { withFileTypes: true });
21
+
22
+ for (const entry of entries) {
23
+ const srcPath = path.join(src, entry.name);
24
+ const destPath = path.join(dest, entry.name);
25
+
26
+ if (entry.isDirectory()) {
27
+ copyRecursive(srcPath, destPath);
28
+ } else {
29
+ // Copy all files except markdown source
30
+ if (!entry.name.endsWith('.md') && !entry.name.endsWith('.mdx')) {
31
+ let shouldCopy = true;
32
+ if (fs.existsSync(destPath)) {
33
+ const srcStat = fs.statSync(srcPath);
34
+ const destStat = fs.statSync(destPath);
35
+ if (srcStat.mtimeMs <= destStat.mtimeMs) {
36
+ shouldCopy = false;
37
+ }
38
+ }
39
+
40
+ if (shouldCopy) {
41
+ fs.copyFileSync(srcPath, destPath);
42
+ // console.log(`Copied: ${entry.name} -> ${destPath}`);
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ function getSlugFromFilename(filename: string): string {
50
+ const nameWithoutExt = filename.replace(/\.mdx?$/, '');
51
+ const dateRegex = /^(\d{4}-\d{2}-\d{2})-(.*)$/;
52
+ const match = nameWithoutExt.match(dateRegex);
53
+
54
+ if (match && !siteConfig.includeDateInUrl) {
55
+ return match[2];
56
+ }
57
+ return nameWithoutExt;
58
+ }
59
+
60
+ function processPosts() {
61
+ if (fs.existsSync(srcDir)) {
62
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
63
+
64
+ entries.forEach((entry) => {
65
+ if (entry.isDirectory()) {
66
+ const targetName = getSlugFromFilename(entry.name);
67
+ const srcPostDir = path.join(srcDir, entry.name);
68
+ const destPostDir = path.join(destDir, targetName);
69
+
70
+ console.log(`Processing Post: ${entry.name} -> ${targetName}`);
71
+ copyRecursive(srcPostDir, destPostDir);
72
+ }
73
+ });
74
+ }
75
+ }
76
+
77
+ // Check if a directory is a post folder (contains index.md or index.mdx)
78
+ function isPostFolder(dirPath: string): boolean {
79
+ return fs.existsSync(path.join(dirPath, 'index.md')) ||
80
+ fs.existsSync(path.join(dirPath, 'index.mdx'));
81
+ }
82
+
83
+ // Check if a directory is an asset folder (not a post folder)
84
+ function isAssetFolder(dirPath: string): boolean {
85
+ return !isPostFolder(dirPath);
86
+ }
87
+
88
+ function processSeries() {
89
+ if (!fs.existsSync(seriesSrcDir)) return;
90
+
91
+ const seriesEntries = fs.readdirSync(seriesSrcDir, { withFileTypes: true });
92
+
93
+ seriesEntries.forEach((seriesEntry) => {
94
+ if (seriesEntry.isDirectory()) {
95
+ const seriesPath = path.join(seriesSrcDir, seriesEntry.name);
96
+ const items = fs.readdirSync(seriesPath, { withFileTypes: true });
97
+
98
+ // First pass: identify shared asset folders at series level (folders that are NOT posts)
99
+ const sharedAssetFolders = items
100
+ .filter(item => item.isDirectory() && isAssetFolder(path.join(seriesPath, item.name)))
101
+ .map(item => item.name);
102
+
103
+ // Process items in series folder
104
+ items.forEach(item => {
105
+ if (item.isFile() && (item.name.endsWith('.md') || item.name.endsWith('.mdx'))) {
106
+ // File-based post or series index
107
+ const targetSlug = item.name.startsWith('index.') ? seriesEntry.name : getSlugFromFilename(item.name);
108
+ const destPostDir = path.join(destDir, targetSlug);
109
+
110
+ console.log(`Processing Series File: ${item.name} -> ${targetSlug}`);
111
+
112
+ // Only copy shared asset folders (like images/, assets/), not sibling post folders
113
+ sharedAssetFolders.forEach(folderName => {
114
+ const srcPath = path.join(seriesPath, folderName);
115
+ const destPath = path.join(destPostDir, folderName);
116
+ copyRecursive(srcPath, destPath);
117
+ });
118
+
119
+ // Copy any non-markdown files at the series root level
120
+ items.filter(sub => sub.isFile() && !sub.name.endsWith('.md') && !sub.name.endsWith('.mdx'))
121
+ .forEach(sub => {
122
+ const srcPath = path.join(seriesPath, sub.name);
123
+ const destPath = path.join(destPostDir, sub.name);
124
+ if (!fs.existsSync(destPostDir)) {
125
+ fs.mkdirSync(destPostDir, { recursive: true });
126
+ }
127
+ fs.copyFileSync(srcPath, destPath);
128
+ });
129
+
130
+ } else if (item.isDirectory() && isPostFolder(path.join(seriesPath, item.name))) {
131
+ // Folder-based post: copy only its own assets
132
+ const targetSlug = getSlugFromFilename(item.name);
133
+ const itemSrcPath = path.join(seriesPath, item.name);
134
+ const destPostDir = path.join(destDir, targetSlug);
135
+
136
+ console.log(`Processing Series Post Folder: ${item.name} -> ${targetSlug}`);
137
+
138
+ // Copy everything from the post folder EXCEPT markdown files
139
+ const subItems = fs.readdirSync(itemSrcPath, { withFileTypes: true });
140
+ subItems.forEach(sub => {
141
+ const srcPath = path.join(itemSrcPath, sub.name);
142
+ const destPath = path.join(destPostDir, sub.name);
143
+
144
+ if (sub.isDirectory()) {
145
+ copyRecursive(srcPath, destPath);
146
+ } else if (!sub.name.endsWith('.md') && !sub.name.endsWith('.mdx')) {
147
+ if (!fs.existsSync(destPostDir)) {
148
+ fs.mkdirSync(destPostDir, { recursive: true });
149
+ }
150
+ fs.copyFileSync(srcPath, destPath);
151
+ }
152
+ });
153
+ }
154
+ });
155
+ }
156
+ });
157
+ }
158
+
159
+ function processBooks() {
160
+ if (!fs.existsSync(booksSrcDir)) return;
161
+
162
+ const entries = fs.readdirSync(booksSrcDir, { withFileTypes: true });
163
+
164
+ entries.forEach((entry) => {
165
+ if (entry.isDirectory()) {
166
+ const srcBookDir = path.join(booksSrcDir, entry.name);
167
+ const destBookDir = path.join(booksDestDir, entry.name);
168
+
169
+ console.log(`Processing Book: ${entry.name}`);
170
+ copyRecursive(srcBookDir, destBookDir);
171
+ }
172
+ });
173
+ }
174
+
175
+ function processFlows() {
176
+ if (!fs.existsSync(flowsSrcDir)) return;
177
+
178
+ // Walk content/flows/YYYY/MM/ structure for folder-based flows with co-located assets
179
+ const yearDirs = fs.readdirSync(flowsSrcDir, { withFileTypes: true });
180
+ for (const yearEntry of yearDirs) {
181
+ if (!yearEntry.isDirectory() || !/^\d{4}$/.test(yearEntry.name)) continue;
182
+ const yearPath = path.join(flowsSrcDir, yearEntry.name);
183
+
184
+ const monthDirs = fs.readdirSync(yearPath, { withFileTypes: true });
185
+ for (const monthEntry of monthDirs) {
186
+ if (!monthEntry.isDirectory() || !/^\d{2}$/.test(monthEntry.name)) continue;
187
+ const monthPath = path.join(yearPath, monthEntry.name);
188
+
189
+ const dayItems = fs.readdirSync(monthPath, { withFileTypes: true });
190
+ for (const dayItem of dayItems) {
191
+ // Only process folder-based flows (DD/ directories with index.mdx)
192
+ if (!dayItem.isDirectory()) continue;
193
+ const rawName = dayItem.name;
194
+ if (!/^\d{2}$/.test(rawName)) continue;
195
+
196
+ const srcFlowDir = path.join(monthPath, rawName);
197
+ const destFlowDir = path.join(flowsDestDir, yearEntry.name, monthEntry.name, rawName);
198
+
199
+ console.log(`Processing Flow: ${yearEntry.name}/${monthEntry.name}/${rawName}`);
200
+ copyRecursive(srcFlowDir, destFlowDir);
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ console.log('Copying assets...');
207
+ processPosts();
208
+ processSeries();
209
+ processBooks();
210
+ processFlows();
211
+ console.log('Assets copied successfully.');
@@ -0,0 +1,47 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const args = process.argv.slice(2);
5
+ const title = args.filter(arg => !arg.startsWith('--'))[0];
6
+ const useMdx = args.includes('--mdx');
7
+
8
+ const now = new Date();
9
+ const year = String(now.getFullYear());
10
+ const month = String(now.getMonth() + 1).padStart(2, '0');
11
+ const day = String(now.getDate()).padStart(2, '0');
12
+ const dateStr = `${year}-${month}-${day}`;
13
+
14
+ const ext = useMdx ? '.mdx' : '.md';
15
+ const dirPath = path.join(process.cwd(), 'content', 'flows', year, month);
16
+ const targetPath = path.join(dirPath, `${day}${ext}`);
17
+
18
+ // Check if today's flow already exists (either .md or .mdx)
19
+ const altExt = useMdx ? '.md' : '.mdx';
20
+ const altPath = path.join(dirPath, `${day}${altExt}`);
21
+
22
+ if (fs.existsSync(targetPath)) {
23
+ console.error(`Error: Flow already exists at ${targetPath}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ if (fs.existsSync(altPath)) {
28
+ console.error(`Error: Flow already exists at ${altPath}`);
29
+ process.exit(1);
30
+ }
31
+
32
+ // Create parent directories if needed
33
+ if (!fs.existsSync(dirPath)) {
34
+ fs.mkdirSync(dirPath, { recursive: true });
35
+ }
36
+
37
+ const flowTitle = title || dateStr;
38
+
39
+ const content = `---
40
+ title: "${flowTitle}"
41
+ tags: []
42
+ ---
43
+
44
+ `;
45
+
46
+ fs.writeFileSync(targetPath, content);
47
+ console.log(`Created new flow: ${targetPath}`);
@@ -0,0 +1,141 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const args = process.argv.slice(2);
5
+
6
+ // Parse arguments
7
+ const folderPath = args.find(arg => !arg.startsWith('--'));
8
+ const titleArgIndex = args.indexOf('--title');
9
+ const title = titleArgIndex > -1 ? args[titleArgIndex + 1] : '';
10
+ const sortArg = args.indexOf('--sort');
11
+ const sortOrder = sortArg > -1 ? args[sortArg + 1] : 'name'; // name, date, or none
12
+ const copyImages = !args.includes('--no-copy');
13
+
14
+ // Supported image extensions
15
+ const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.avif'];
16
+
17
+ function isImageFile(filename: string): boolean {
18
+ const ext = path.extname(filename).toLowerCase();
19
+ return IMAGE_EXTENSIONS.includes(ext);
20
+ }
21
+
22
+ function getImageFiles(dir: string, sort: string): string[] {
23
+ const files = fs.readdirSync(dir)
24
+ .filter(f => {
25
+ const filePath = path.join(dir, f);
26
+ return fs.statSync(filePath).isFile() && isImageFile(f);
27
+ });
28
+
29
+ switch (sort) {
30
+ case 'name':
31
+ return files.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
32
+ case 'date':
33
+ return files.sort((a, b) => {
34
+ const statA = fs.statSync(path.join(dir, a));
35
+ const statB = fs.statSync(path.join(dir, b));
36
+ return statA.mtimeMs - statB.mtimeMs;
37
+ });
38
+ case 'none':
39
+ default:
40
+ return files;
41
+ }
42
+ }
43
+
44
+ if (!folderPath) {
45
+ console.error('Please provide an image folder path.');
46
+ console.error('Usage: bun run new-from-images <folder> [--title "Post Title"] [--sort name|date|none] [--no-copy]');
47
+ console.error('');
48
+ console.error('Options:');
49
+ console.error(' --title Custom title for the post (default: folder name)');
50
+ console.error(' --sort Sort images by: name (default), date, or none');
51
+ console.error(' --no-copy Reference images in place instead of copying');
52
+ process.exit(1);
53
+ }
54
+
55
+ if (!fs.existsSync(folderPath) || !fs.statSync(folderPath).isDirectory()) {
56
+ console.error(`Error: "${folderPath}" is not a valid directory.`);
57
+ process.exit(1);
58
+ }
59
+
60
+ // Get image files
61
+ const imageFiles = getImageFiles(folderPath, sortOrder);
62
+
63
+ if (imageFiles.length === 0) {
64
+ console.error(`Error: No image files found in "${folderPath}".`);
65
+ console.error(`Supported formats: ${IMAGE_EXTENSIONS.join(', ')}`);
66
+ process.exit(1);
67
+ }
68
+
69
+ console.log(`Found ${imageFiles.length} image(s) in folder.`);
70
+
71
+ // Generate slug from title or folder name
72
+ const folderName = path.basename(folderPath);
73
+ const postTitle = title || folderName.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
74
+ const slug = (title || folderName)
75
+ .toLowerCase()
76
+ .replace(/[^a-z0-9]+/g, '-')
77
+ .replace(/(^-|-$)+/g, '');
78
+
79
+ const date = new Date().toISOString().split('T')[0];
80
+ const dirName = `${date}-${slug}`;
81
+ const dirPath = path.join(process.cwd(), 'content', 'posts', dirName);
82
+ const imagesDir = path.join(dirPath, 'images');
83
+
84
+ // Check if post already exists
85
+ if (fs.existsSync(dirPath)) {
86
+ console.error(`Error: Post already exists at ${dirPath}`);
87
+ process.exit(1);
88
+ }
89
+
90
+ // Create directories
91
+ fs.mkdirSync(imagesDir, { recursive: true });
92
+ console.log(`Created directory: ${dirPath}`);
93
+
94
+ // Copy or reference images
95
+ const images: { filename: string; relativePath: string }[] = [];
96
+
97
+ for (const file of imageFiles) {
98
+ const srcPath = path.join(folderPath, file);
99
+
100
+ if (copyImages) {
101
+ const destPath = path.join(imagesDir, file);
102
+ fs.copyFileSync(srcPath, destPath);
103
+ images.push({ filename: file, relativePath: `./images/${file}` });
104
+ console.log(` Copied: ${file}`);
105
+ } else {
106
+ // Use absolute path from project root
107
+ const absoluteSrcPath = path.resolve(srcPath);
108
+ const relativePath = path.relative(dirPath, absoluteSrcPath);
109
+ images.push({ filename: file, relativePath });
110
+ console.log(` Referenced: ${file}`);
111
+ }
112
+ }
113
+
114
+ // Generate markdown content
115
+ const imageMarkdown = images
116
+ .map((img, index) => `![Image ${index + 1}](${img.relativePath})`)
117
+ .join('\n\n');
118
+
119
+ const content = `---
120
+ title: "${postTitle}"
121
+ date: "${date}"
122
+ excerpt: "A collection of ${images.length} images."
123
+ category: "Gallery"
124
+ tags: ["images", "gallery"]
125
+ authors: ["Amytis"]
126
+ layout: "post"
127
+ draft: false
128
+ latex: false
129
+ ---
130
+
131
+ ${imageMarkdown}
132
+ `;
133
+
134
+ // Write markdown file
135
+ const targetPath = path.join(dirPath, 'index.mdx');
136
+ fs.writeFileSync(targetPath, content);
137
+
138
+ console.log(`\nCreated new post from images:`);
139
+ console.log(` Post: ${targetPath}`);
140
+ console.log(` Images: ${images.length} file(s)${copyImages ? ' (copied)' : ' (referenced)'}`);
141
+ console.log(`\nYou can edit the post at: ${dirPath}`);
@@ -0,0 +1,105 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { pdf } from 'pdf-to-img';
4
+
5
+ const args = process.argv.slice(2);
6
+
7
+ // Parse arguments
8
+ const pdfPath = args.find(arg => arg.endsWith('.pdf') || (!arg.startsWith('--') && args.indexOf(arg) === 0));
9
+ const titleArgIndex = args.indexOf('--title');
10
+ const title = titleArgIndex > -1 ? args[titleArgIndex + 1] : '';
11
+ const scaleArg = args.indexOf('--scale');
12
+ const scale = scaleArg > -1 ? parseFloat(args[scaleArg + 1]) || 2.0 : 2.0;
13
+
14
+ if (!pdfPath || !fs.existsSync(pdfPath)) {
15
+ console.error('Please provide a valid PDF file path.');
16
+ console.error('Usage: bun run new-from-pdf <pdf-file> [--title "Post Title"] [--scale 2.0]');
17
+ console.error('');
18
+ console.error('Options:');
19
+ console.error(' --title Custom title for the post (default: PDF filename)');
20
+ console.error(' --scale Image scale factor (default: 2.0 for good quality)');
21
+ process.exit(1);
22
+ }
23
+
24
+ // Generate slug from title or filename
25
+ const pdfBasename = path.basename(pdfPath, '.pdf');
26
+ const postTitle = title || pdfBasename.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
27
+ const slug = (title || pdfBasename)
28
+ .toLowerCase()
29
+ .replace(/[^a-z0-9]+/g, '-')
30
+ .replace(/(^-|-$)+/g, '');
31
+
32
+ const date = new Date().toISOString().split('T')[0];
33
+ const dirName = `${date}-${slug}`;
34
+ const dirPath = path.join(process.cwd(), 'content', 'posts', dirName);
35
+ const imagesDir = path.join(dirPath, 'images');
36
+
37
+ // Check if post already exists
38
+ if (fs.existsSync(dirPath)) {
39
+ console.error(`Error: Post already exists at ${dirPath}`);
40
+ process.exit(1);
41
+ }
42
+
43
+ // Create directories
44
+ fs.mkdirSync(imagesDir, { recursive: true });
45
+ console.log(`Created directory: ${dirPath}`);
46
+
47
+ // Convert PDF to images
48
+ console.log(`Converting PDF (scale: ${scale})...`);
49
+
50
+ const images: string[] = [];
51
+ let pageNum = 0;
52
+
53
+ try {
54
+ const pdfDocument = await pdf(pdfPath, { scale });
55
+
56
+ for await (const image of pdfDocument) {
57
+ pageNum++;
58
+ const filename = `page-${String(pageNum).padStart(3, '0')}.png`;
59
+ const imagePath = path.join(imagesDir, filename);
60
+
61
+ fs.writeFileSync(imagePath, image);
62
+ images.push(filename);
63
+ console.log(` Generated: ${filename}`);
64
+ }
65
+ } catch (error) {
66
+ const errorMessage = error instanceof Error ? error.message : String(error);
67
+ console.error(`Error converting PDF: ${errorMessage}`);
68
+ fs.rmSync(dirPath, { recursive: true });
69
+ process.exit(1);
70
+ }
71
+
72
+ if (images.length === 0) {
73
+ console.error('Error: No images were generated from the PDF.');
74
+ fs.rmSync(dirPath, { recursive: true });
75
+ process.exit(1);
76
+ }
77
+
78
+ // Generate markdown content
79
+ const imageMarkdown = images
80
+ .map((img, index) => `![Page ${index + 1}](./images/${img})`)
81
+ .join('\n\n');
82
+
83
+ const content = `---
84
+ title: "${postTitle}"
85
+ date: "${date}"
86
+ excerpt: "Content extracted from PDF document."
87
+ category: "Document"
88
+ tags: ["pdf", "document"]
89
+ authors: ["Amytis"]
90
+ layout: "post"
91
+ draft: false
92
+ latex: false
93
+ ---
94
+
95
+ ${imageMarkdown}
96
+ `;
97
+
98
+ // Write markdown file
99
+ const targetPath = path.join(dirPath, 'index.mdx');
100
+ fs.writeFileSync(targetPath, content);
101
+
102
+ console.log(`\nCreated new post from PDF:`);
103
+ console.log(` Post: ${targetPath}`);
104
+ console.log(` Images: ${images.length} page(s)`);
105
+ console.log(`\nYou can edit the post at: ${dirPath}`);