create-ampless 0.2.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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +229 -0
  5. package/dist/templates/_shared/RUNBOOK.md +178 -0
  6. package/dist/templates/_shared/amplify/auth/post-confirmation/handler.ts +4 -0
  7. package/dist/templates/_shared/amplify/auth/post-confirmation/resource.ts +6 -0
  8. package/dist/templates/_shared/amplify/auth/resource.ts +8 -0
  9. package/dist/templates/_shared/amplify/backend.ts +29 -0
  10. package/dist/templates/_shared/amplify/data/get-published-post.js +33 -0
  11. package/dist/templates/_shared/amplify/data/list-posts-by-tag.js +52 -0
  12. package/dist/templates/_shared/amplify/data/list-published-posts.js +57 -0
  13. package/dist/templates/_shared/amplify/data/resource.ts +30 -0
  14. package/dist/templates/_shared/amplify/events/dispatcher/handler.ts +4 -0
  15. package/dist/templates/_shared/amplify/events/dispatcher/resource.ts +12 -0
  16. package/dist/templates/_shared/amplify/events/processor-trusted/handler.ts +12 -0
  17. package/dist/templates/_shared/amplify/events/processor-trusted/resource.ts +14 -0
  18. package/dist/templates/_shared/amplify/events/processor-untrusted/handler.ts +10 -0
  19. package/dist/templates/_shared/amplify/events/processor-untrusted/resource.ts +9 -0
  20. package/dist/templates/_shared/amplify/functions/api-key-renewer/handler.ts +4 -0
  21. package/dist/templates/_shared/amplify/functions/api-key-renewer/resource.ts +12 -0
  22. package/dist/templates/_shared/amplify/storage/resource.ts +7 -0
  23. package/dist/templates/_shared/app/(admin)/admin/layout.tsx +4 -0
  24. package/dist/templates/_shared/app/(admin)/admin/media/page.tsx +4 -0
  25. package/dist/templates/_shared/app/(admin)/admin/page.tsx +4 -0
  26. package/dist/templates/_shared/app/(admin)/admin/posts/[postId]/page.tsx +4 -0
  27. package/dist/templates/_shared/app/(admin)/admin/posts/new/page.tsx +4 -0
  28. package/dist/templates/_shared/app/(admin)/admin/posts/page.tsx +4 -0
  29. package/dist/templates/_shared/app/(admin)/admin/sites/[siteId]/page.tsx +5 -0
  30. package/dist/templates/_shared/app/(admin)/admin/sites/[siteId]/theme/page.tsx +6 -0
  31. package/dist/templates/_shared/app/(admin)/admin/sites/page.tsx +5 -0
  32. package/dist/templates/_shared/app/api/media/[...path]/route.ts +5 -0
  33. package/dist/templates/_shared/app/globals.css +114 -0
  34. package/dist/templates/_shared/app/layout.tsx +48 -0
  35. package/dist/templates/_shared/app/login/page.tsx +4 -0
  36. package/dist/templates/_shared/app/providers.tsx +13 -0
  37. package/dist/templates/_shared/app/site/[siteId]/[slug]/page.tsx +10 -0
  38. package/dist/templates/_shared/app/site/[siteId]/feed.xml/route.ts +5 -0
  39. package/dist/templates/_shared/app/site/[siteId]/og/[slug]/route.ts +6 -0
  40. package/dist/templates/_shared/app/site/[siteId]/page.tsx +10 -0
  41. package/dist/templates/_shared/app/site/[siteId]/raw/[slug]/route.ts +5 -0
  42. package/dist/templates/_shared/app/site/[siteId]/sitemap.xml/route.ts +5 -0
  43. package/dist/templates/_shared/app/site/[siteId]/tag/[tag]/page.tsx +10 -0
  44. package/dist/templates/_shared/cms.config.ts +110 -0
  45. package/dist/templates/_shared/components/i18n-provider.tsx +7 -0
  46. package/dist/templates/_shared/components/lightbox-content.tsx +69 -0
  47. package/dist/templates/_shared/components/site-chrome/collapsible-sidebar.tsx +54 -0
  48. package/dist/templates/_shared/components/site-chrome/mobile-menu.tsx +68 -0
  49. package/dist/templates/_shared/components/site-chrome/site-footer.tsx +43 -0
  50. package/dist/templates/_shared/components/site-chrome/site-header.tsx +94 -0
  51. package/dist/templates/_shared/components/site-chrome/site-sidebar.tsx +81 -0
  52. package/dist/templates/_shared/components/tag-list.tsx +25 -0
  53. package/dist/templates/_shared/components.json +21 -0
  54. package/dist/templates/_shared/lib/admin-site-client.ts +10 -0
  55. package/dist/templates/_shared/lib/admin-site.ts +8 -0
  56. package/dist/templates/_shared/lib/admin.ts +24 -0
  57. package/dist/templates/_shared/lib/ampless.ts +23 -0
  58. package/dist/templates/_shared/lib/amplify-server.ts +7 -0
  59. package/dist/templates/_shared/lib/amplify.ts +9 -0
  60. package/dist/templates/_shared/lib/auth-server.ts +11 -0
  61. package/dist/templates/_shared/lib/cn.ts +5 -0
  62. package/dist/templates/_shared/lib/i18n.ts +31 -0
  63. package/dist/templates/_shared/lib/kv-provider.ts +7 -0
  64. package/dist/templates/_shared/lib/media.ts +6 -0
  65. package/dist/templates/_shared/lib/posts-provider.ts +7 -0
  66. package/dist/templates/_shared/lib/posts-public.ts +19 -0
  67. package/dist/templates/_shared/lib/posts.ts +12 -0
  68. package/dist/templates/_shared/lib/seo.ts +8 -0
  69. package/dist/templates/_shared/lib/site-settings.ts +8 -0
  70. package/dist/templates/_shared/lib/storage.ts +7 -0
  71. package/dist/templates/_shared/lib/theme-actions.ts +5 -0
  72. package/dist/templates/_shared/lib/theme-active.ts +8 -0
  73. package/dist/templates/_shared/lib/theme-config.ts +10 -0
  74. package/dist/templates/_shared/lib/upload.ts +6 -0
  75. package/dist/templates/_shared/middleware.ts +13 -0
  76. package/dist/templates/_shared/next.config.mjs +11 -0
  77. package/dist/templates/_shared/package.json +63 -0
  78. package/dist/templates/_shared/postcss.config.mjs +5 -0
  79. package/dist/templates/_shared/themes-registry.ts +38 -0
  80. package/dist/templates/_shared/tsconfig.json +23 -0
  81. package/dist/templates/blog/README.md +52 -0
  82. package/dist/templates/blog/index.ts +29 -0
  83. package/dist/templates/blog/manifest.ts +144 -0
  84. package/dist/templates/blog/pages/feed.ts +31 -0
  85. package/dist/templates/blog/pages/home.tsx +108 -0
  86. package/dist/templates/blog/pages/post.tsx +94 -0
  87. package/dist/templates/blog/pages/sitemap.ts +30 -0
  88. package/dist/templates/blog/pages/tag.tsx +76 -0
  89. package/dist/templates/blog/tokens.css +54 -0
  90. package/dist/templates/corporate/README.md +20 -0
  91. package/dist/templates/corporate/index.ts +25 -0
  92. package/dist/templates/corporate/manifest.ts +94 -0
  93. package/dist/templates/corporate/pages/feed.ts +29 -0
  94. package/dist/templates/corporate/pages/home.tsx +130 -0
  95. package/dist/templates/corporate/pages/post.tsx +96 -0
  96. package/dist/templates/corporate/pages/sitemap.ts +28 -0
  97. package/dist/templates/corporate/pages/tag.tsx +81 -0
  98. package/dist/templates/corporate/tokens.css +47 -0
  99. package/dist/templates/dads/README.md +35 -0
  100. package/dist/templates/dads/index.ts +25 -0
  101. package/dist/templates/dads/manifest.ts +84 -0
  102. package/dist/templates/dads/pages/feed.ts +29 -0
  103. package/dist/templates/dads/pages/home.tsx +126 -0
  104. package/dist/templates/dads/pages/post.tsx +102 -0
  105. package/dist/templates/dads/pages/sitemap.ts +28 -0
  106. package/dist/templates/dads/pages/tag.tsx +86 -0
  107. package/dist/templates/dads/tokens.css +67 -0
  108. package/dist/templates/docs/README.md +27 -0
  109. package/dist/templates/docs/index.ts +25 -0
  110. package/dist/templates/docs/manifest.ts +89 -0
  111. package/dist/templates/docs/pages/feed.ts +29 -0
  112. package/dist/templates/docs/pages/home.tsx +88 -0
  113. package/dist/templates/docs/pages/post.tsx +96 -0
  114. package/dist/templates/docs/pages/sitemap.ts +28 -0
  115. package/dist/templates/docs/pages/tag.tsx +79 -0
  116. package/dist/templates/docs/tokens.css +55 -0
  117. package/dist/templates/landing/README.md +25 -0
  118. package/dist/templates/landing/index.ts +25 -0
  119. package/dist/templates/landing/manifest.ts +118 -0
  120. package/dist/templates/landing/pages/feed.ts +31 -0
  121. package/dist/templates/landing/pages/home.tsx +123 -0
  122. package/dist/templates/landing/pages/post.tsx +95 -0
  123. package/dist/templates/landing/pages/sitemap.ts +28 -0
  124. package/dist/templates/landing/pages/tag.tsx +85 -0
  125. package/dist/templates/landing/tokens.css +47 -0
  126. package/dist/templates/minimal/README.md +52 -0
  127. package/dist/templates/minimal/index.ts +25 -0
  128. package/dist/templates/minimal/manifest.ts +35 -0
  129. package/dist/templates/minimal/pages/feed.ts +31 -0
  130. package/dist/templates/minimal/pages/home.tsx +44 -0
  131. package/dist/templates/minimal/pages/post.tsx +65 -0
  132. package/dist/templates/minimal/pages/sitemap.ts +30 -0
  133. package/dist/templates/minimal/pages/tag.tsx +46 -0
  134. package/dist/templates/minimal/tokens.css +46 -0
  135. package/package.json +41 -0
@@ -0,0 +1,30 @@
1
+ import { a, defineData, type ClientSchema } from '@aws-amplify/backend'
2
+ import { amplessSchemaModels, defaultAuthorizationModes } from '@ampless/backend'
3
+
4
+ // Ampless's built-in models (Post / Page / Media / Taxonomy / PostTag /
5
+ // KvStore) plus the three public-read custom queries
6
+ // (listPublishedPosts / getPublishedPost / listPostsByTag).
7
+ //
8
+ // Add project-specific models alongside the spread:
9
+ //
10
+ // const schema = a.schema({
11
+ // ...amplessSchemaModels(a),
12
+ // MyCustomModel: a
13
+ // .model({ siteId: a.string().required(), foo: a.string() })
14
+ // .identifier(['siteId', 'foo'])
15
+ // .authorization((allow) => [allow.groups(['ampless-admin'])]),
16
+ // })
17
+ //
18
+ // The three AppSync JS resolvers (`*.js` in this directory) stay
19
+ // user-owned — AppSync resolves `entry: './...'` paths at synth time
20
+ // relative to this file. If you relocate them, pass new paths via
21
+ // `amplessSchemaModels(a, { resolverPaths: { ... } })`.
22
+ const schema = a.schema({
23
+ ...amplessSchemaModels(a),
24
+ })
25
+
26
+ export type Schema = ClientSchema<typeof schema>
27
+ export const data = defineData({
28
+ schema,
29
+ authorizationModes: defaultAuthorizationModes,
30
+ })
@@ -0,0 +1,4 @@
1
+ // Re-exported from @ampless/backend so the package can ship Lambda
2
+ // handler updates via `npm update`. Amplify's esbuild follows this
3
+ // import and bundles the real handler into the Lambda artifact.
4
+ export { handler } from '@ampless/backend/events/dispatcher'
@@ -0,0 +1,12 @@
1
+ import { defineFunction } from '@aws-amplify/backend'
2
+
3
+ export const eventDispatcher = defineFunction({
4
+ name: 'event-dispatcher',
5
+ entry: './handler.ts',
6
+ // Co-locate with the data stack — the function reads the Post table's
7
+ // DynamoDB Stream, so being in the same stack avoids a CloudFormation
8
+ // circular dependency between data, function, and auth.
9
+ resourceGroupName: 'data',
10
+ memoryMB: 256,
11
+ timeoutSeconds: 30,
12
+ })
@@ -0,0 +1,12 @@
1
+ import config from '../../../cms.config'
2
+ import { createProcessorTrustedHandler } from '@ampless/backend/events/processor-trusted'
3
+
4
+ // Plugins + site come from the user-side `cms.config`. The factory
5
+ // filters down to `trust_level === 'trusted'` plugins, wires the
6
+ // runtime context (listPublishedPosts / writePublicAsset), and runs
7
+ // the built-in site-settings cache rebuild on
8
+ // `site.settings.updated` events.
9
+ export const handler = createProcessorTrustedHandler({
10
+ plugins: config.plugins,
11
+ site: config.site,
12
+ })
@@ -0,0 +1,14 @@
1
+ import { defineFunction } from '@aws-amplify/backend'
2
+
3
+ export const processorTrusted = defineFunction({
4
+ name: 'processor-trusted',
5
+ entry: './handler.ts',
6
+ // Co-locate with data — this Lambda reads the Post table to assemble
7
+ // sitemap/RSS, so we keep the dependency intra-stack and avoid a
8
+ // function → data → auth → function cycle.
9
+ resourceGroupName: 'data',
10
+ // Higher than dispatcher because plugins (sitemap/RSS) load all
11
+ // published posts and serialize XML.
12
+ memoryMB: 512,
13
+ timeoutSeconds: 60,
14
+ })
@@ -0,0 +1,10 @@
1
+ import config from '../../../cms.config'
2
+ import { createProcessorUntrustedHandler } from '@ampless/backend/events/processor-untrusted'
3
+
4
+ // Untrusted plugins get a runtime context with NO AWS-touching
5
+ // capabilities — listPublishedPosts / writePublicAsset throw. The
6
+ // factory filters `trust_level === 'untrusted'` from cms.config.
7
+ export const handler = createProcessorUntrustedHandler({
8
+ plugins: config.plugins,
9
+ site: config.site,
10
+ })
@@ -0,0 +1,9 @@
1
+ import { defineFunction } from '@aws-amplify/backend'
2
+
3
+ export const processorUntrusted = defineFunction({
4
+ name: 'processor-untrusted',
5
+ entry: './handler.ts',
6
+ // Untrusted plugins do pure JS work; modest memory.
7
+ memoryMB: 256,
8
+ timeoutSeconds: 30,
9
+ })
@@ -0,0 +1,4 @@
1
+ // Re-exported from @ampless/backend so the package can ship Lambda
2
+ // handler updates via `npm update`. Amplify's esbuild follows this
3
+ // import and bundles the real handler into the Lambda artifact.
4
+ export { handler } from '@ampless/backend/functions/api-key-renewer'
@@ -0,0 +1,12 @@
1
+ import { defineFunction } from '@aws-amplify/backend'
2
+
3
+ export const apiKeyRenewer = defineFunction({
4
+ name: 'api-key-renewer',
5
+ entry: './handler.ts',
6
+ // Co-locate with the data stack — the function reads/updates the
7
+ // AppSync API's API key, so being in the same stack avoids a CFN
8
+ // dependency cycle between data, function, and auth.
9
+ resourceGroupName: 'data',
10
+ memoryMB: 256,
11
+ timeoutSeconds: 30,
12
+ })
@@ -0,0 +1,7 @@
1
+ import { defineAmplessStorage } from '@ampless/backend'
2
+
3
+ // Provisions the ampless media + plugins bucket with guest read on
4
+ // `public/*` and admin/editor write on the matched prefixes. CORS and
5
+ // the bucket-level public-access policy are applied by
6
+ // defineAmplessBackend.
7
+ export const storage = defineAmplessStorage()
@@ -0,0 +1,4 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createAdminLayout } from '@ampless/admin/pages'
3
+
4
+ export default createAdminLayout(admin)
@@ -0,0 +1,4 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createMediaPage } from '@ampless/admin/pages'
3
+
4
+ export default createMediaPage(admin)
@@ -0,0 +1,4 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createAdminDashboardPage } from '@ampless/admin/pages'
3
+
4
+ export default createAdminDashboardPage(admin)
@@ -0,0 +1,4 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createEditPostPage } from '@ampless/admin/pages'
3
+
4
+ export default createEditPostPage(admin)
@@ -0,0 +1,4 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createNewPostPage } from '@ampless/admin/pages'
3
+
4
+ export default createNewPostPage(admin)
@@ -0,0 +1,4 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createPostsListPage } from '@ampless/admin/pages'
3
+
4
+ export default createPostsListPage(admin)
@@ -0,0 +1,5 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createSiteEditPage } from '@ampless/admin/pages'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+ export default createSiteEditPage(admin)
@@ -0,0 +1,6 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createSiteThemePage } from '@ampless/admin/pages'
3
+ import { themeList } from '@/themes-registry'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+ export default createSiteThemePage(admin, themeList)
@@ -0,0 +1,5 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createSitesListPage } from '@ampless/admin/pages'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+ export default createSitesListPage(admin)
@@ -0,0 +1,5 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createMediaProxyRoute } from '@ampless/admin/api'
3
+
4
+ export const { GET } = createMediaProxyRoute(admin)
5
+ export const runtime = 'nodejs'
@@ -0,0 +1,114 @@
1
+ @import 'tailwindcss';
2
+ @plugin '@tailwindcss/typography';
3
+ /* DADS theme plugin from Digital Agency. Loaded globally so the
4
+ * `templates/dads/` theme can use DADS-conformant color variables
5
+ * (`--color-blue-900` etc., the canonical solidBlue). Other themes
6
+ * don't reference DADS-specific classes, so the plugin's tokens
7
+ * sit unused for them — small CSS overhead, no behavior change. */
8
+ @plugin '@digital-go-jp/tailwind-theme-plugin';
9
+
10
+ /*
11
+ * Tailwind v4 inline theme: every `--color-*` and `--radius-*` token
12
+ * the framework knows about reads from a CSS variable, so theme
13
+ * tokens.css files can override them at runtime by changing the right
14
+ * data-theme block.
15
+ */
16
+ @theme inline {
17
+ --color-background: var(--background);
18
+ --color-foreground: var(--foreground);
19
+ --color-card: var(--card);
20
+ --color-card-foreground: var(--card-foreground);
21
+ --color-primary: var(--primary);
22
+ --color-primary-foreground: var(--primary-foreground);
23
+ --color-secondary: var(--secondary);
24
+ --color-secondary-foreground: var(--secondary-foreground);
25
+ --color-muted: var(--muted);
26
+ --color-muted-foreground: var(--muted-foreground);
27
+ --color-accent: var(--accent);
28
+ --color-accent-foreground: var(--accent-foreground);
29
+ --color-destructive: var(--destructive);
30
+ --color-destructive-foreground: var(--destructive-foreground);
31
+ --color-border: var(--border);
32
+ --color-input: var(--input);
33
+ --color-ring: var(--ring);
34
+ --radius-lg: var(--radius);
35
+ --radius-md: calc(var(--radius) - 2px);
36
+ --radius-sm: calc(var(--radius) - 4px);
37
+ }
38
+
39
+ /*
40
+ * Default tokens — used as fallback when no theme's `[data-theme=...]`
41
+ * selector matches (e.g. body has no data-theme attribute, or admin
42
+ * pages that aren't theme-scoped). Each installed theme ships its own
43
+ * `themes/<name>/tokens.css` overriding these under
44
+ * `[data-theme='<name>']`. The active theme's tokens win because the
45
+ * `<body data-theme="...">` attribute makes the scoped selector match.
46
+ */
47
+ :root {
48
+ --background: oklch(1 0 0);
49
+ --foreground: oklch(0.145 0 0);
50
+ --card: oklch(1 0 0);
51
+ --card-foreground: oklch(0.145 0 0);
52
+ --primary: oklch(0.205 0 0);
53
+ --primary-foreground: oklch(0.985 0 0);
54
+ --secondary: oklch(0.97 0 0);
55
+ --secondary-foreground: oklch(0.205 0 0);
56
+ --muted: oklch(0.97 0 0);
57
+ --muted-foreground: oklch(0.556 0 0);
58
+ --accent: oklch(0.97 0 0);
59
+ --accent-foreground: oklch(0.205 0 0);
60
+ --destructive: oklch(0.577 0.245 27.325);
61
+ --destructive-foreground: oklch(0.985 0 0);
62
+ --border: oklch(0.922 0 0);
63
+ --input: oklch(0.922 0 0);
64
+ --ring: oklch(0.708 0 0);
65
+ --radius: 0.5rem;
66
+ --ampless-body-font: system-ui, -apple-system, sans-serif;
67
+ }
68
+
69
+ @media (prefers-color-scheme: dark) {
70
+ :root {
71
+ --background: oklch(0.145 0 0);
72
+ --foreground: oklch(0.985 0 0);
73
+ --card: oklch(0.145 0 0);
74
+ --card-foreground: oklch(0.985 0 0);
75
+ --primary: oklch(0.985 0 0);
76
+ --primary-foreground: oklch(0.205 0 0);
77
+ --secondary: oklch(0.269 0 0);
78
+ --secondary-foreground: oklch(0.985 0 0);
79
+ --muted: oklch(0.269 0 0);
80
+ --muted-foreground: oklch(0.708 0 0);
81
+ --accent: oklch(0.269 0 0);
82
+ --accent-foreground: oklch(0.985 0 0);
83
+ --destructive: oklch(0.396 0.141 25.723);
84
+ --destructive-foreground: oklch(0.985 0 0);
85
+ --border: oklch(0.269 0 0);
86
+ --input: oklch(0.269 0 0);
87
+ --ring: oklch(0.439 0 0);
88
+ }
89
+ }
90
+
91
+ body {
92
+ font-family: var(--ampless-body-font, system-ui, -apple-system, sans-serif);
93
+ background: var(--background);
94
+ color: var(--foreground);
95
+ }
96
+
97
+ * {
98
+ border-color: var(--border);
99
+ }
100
+
101
+ /*
102
+ Tighten list spacing inside `.prose`. The Typography plugin's default
103
+ treats every <li> as roughly a paragraph's worth of vertical rhythm,
104
+ and tiptap wraps each list item in <p> — so we get double margins
105
+ and the list looks like loose paragraphs rather than a list.
106
+ */
107
+ .prose :where(li):not(:where([class~='not-prose'] *)) {
108
+ margin-top: 0.25em;
109
+ margin-bottom: 0.25em;
110
+ }
111
+ .prose :where(li > p):not(:where([class~='not-prose'] *)) {
112
+ margin-top: 0;
113
+ margin-bottom: 0;
114
+ }
@@ -0,0 +1,48 @@
1
+ import type { Metadata } from 'next'
2
+ import { headers } from 'next/headers'
3
+ import { DEFAULT_SITE_ID } from 'ampless'
4
+ import { Providers } from './providers'
5
+ import { siteMetadata } from '@/lib/seo'
6
+ import { loadThemeConfig, renderThemeCss } from '@/lib/theme-config'
7
+ import { getLocale, getDictionary } from '@/lib/i18n'
8
+ import { I18nProvider } from '@/components/i18n-provider'
9
+ import './globals.css'
10
+
11
+ // Resolve metadata per site at request time. The middleware sets
12
+ // `x-site-id` so we can pick the right merged settings; falls back to
13
+ // DEFAULT_SITE_ID for admin / API routes that don't go through the
14
+ // public middleware path.
15
+ export async function generateMetadata(): Promise<Metadata> {
16
+ const h = await headers()
17
+ const siteId = h.get('x-site-id') ?? DEFAULT_SITE_ID
18
+ return siteMetadata(siteId)
19
+ }
20
+
21
+ export default async function RootLayout({ children }: { children: React.ReactNode }) {
22
+ const h = await headers()
23
+ const siteId = h.get('x-site-id') ?? DEFAULT_SITE_ID
24
+ const theme = await loadThemeConfig(siteId)
25
+ const themeCss = renderThemeCss(theme.cssVars)
26
+ const locale = getLocale()
27
+ const dict = getDictionary(locale)
28
+ return (
29
+ <html lang={locale}>
30
+ <head>
31
+ {/* Inline `:root` overrides come AFTER globals.css so they win
32
+ against the static defaults. Validated values only — see
33
+ ampless `validateThemeValue`. */}
34
+ {themeCss && <style dangerouslySetInnerHTML={{ __html: themeCss }} />}
35
+ </head>
36
+ {/* `data-theme` selects which theme's `tokens.css` block matches.
37
+ The active theme is resolved from `theme.active` site setting,
38
+ falling back to DEFAULT_THEME — see `resolveActiveTheme`. */}
39
+ <body className="min-h-screen" data-theme={theme.activeTheme}>
40
+ <Providers>
41
+ <I18nProvider locale={locale} dict={dict}>
42
+ {children}
43
+ </I18nProvider>
44
+ </Providers>
45
+ </body>
46
+ </html>
47
+ )
48
+ }
@@ -0,0 +1,4 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createLoginPage } from '@ampless/admin/pages'
3
+
4
+ export default createLoginPage(admin)
@@ -0,0 +1,13 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+ import '@/lib/amplify'
5
+ import '@/lib/posts-provider'
6
+ import '@/lib/kv-provider'
7
+
8
+ export function Providers({ children }: { children: React.ReactNode }) {
9
+ useEffect(() => {
10
+ // Amplify is configured via the side-effect import above
11
+ }, [])
12
+ return <>{children}</>
13
+ }
@@ -0,0 +1,10 @@
1
+ import { ampless } from '@/lib/ampless'
2
+ import {
3
+ createThemePostDispatcher,
4
+ createThemePostMetadata,
5
+ } from '@ampless/runtime/dispatchers'
6
+
7
+ export const dynamic = 'force-dynamic'
8
+
9
+ export const generateMetadata = createThemePostMetadata(ampless)
10
+ export default createThemePostDispatcher(ampless)
@@ -0,0 +1,5 @@
1
+ import { ampless } from '@/lib/ampless'
2
+ import { createFeedRouteHandler } from '@ampless/runtime/routes'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+ export const GET = createFeedRouteHandler(ampless)
@@ -0,0 +1,6 @@
1
+ import { ampless } from '@/lib/ampless'
2
+ import { createOgRouteHandler } from '@ampless/runtime/routes'
3
+
4
+ export const runtime = 'nodejs'
5
+ export const dynamic = 'force-dynamic'
6
+ export const GET = createOgRouteHandler(ampless)
@@ -0,0 +1,10 @@
1
+ import { ampless } from '@/lib/ampless'
2
+ import {
3
+ createThemeHomeDispatcher,
4
+ createThemeHomeMetadata,
5
+ } from '@ampless/runtime/dispatchers'
6
+
7
+ export const dynamic = 'force-dynamic'
8
+
9
+ export const generateMetadata = createThemeHomeMetadata(ampless)
10
+ export default createThemeHomeDispatcher(ampless)
@@ -0,0 +1,5 @@
1
+ import { ampless } from '@/lib/ampless'
2
+ import { createRawRouteHandler } from '@ampless/runtime/routes'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+ export const GET = createRawRouteHandler(ampless)
@@ -0,0 +1,5 @@
1
+ import { ampless } from '@/lib/ampless'
2
+ import { createSitemapRouteHandler } from '@ampless/runtime/routes'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+ export const GET = createSitemapRouteHandler(ampless)
@@ -0,0 +1,10 @@
1
+ import { ampless } from '@/lib/ampless'
2
+ import {
3
+ createThemeTagDispatcher,
4
+ createThemeTagMetadata,
5
+ } from '@ampless/runtime/dispatchers'
6
+
7
+ export const dynamic = 'force-dynamic'
8
+
9
+ export const generateMetadata = createThemeTagMetadata(ampless)
10
+ export default createThemeTagDispatcher(ampless)
@@ -0,0 +1,110 @@
1
+ import { defineConfig } from 'ampless'
2
+ import seoPlugin from '@ampless/plugin-seo'
3
+ import rssPlugin from '@ampless/plugin-rss'
4
+ // import webhookPlugin from '@ampless/plugin-webhook'
5
+ // import ogImagePlugin, { loadFontFromUrl } from '@ampless/plugin-og-image'
6
+
7
+ export default defineConfig({
8
+ site: {
9
+ name: '{{siteName}}',
10
+ url: 'http://localhost:3000',
11
+ },
12
+ // Admin UI language. Built-in dictionaries: 'ja', 'en'. Add more by
13
+ // dropping `locales/<code>.json` and registering it in `lib/i18n.ts`.
14
+ locale: 'ja',
15
+ media: {
16
+ delivery: 'nextjs',
17
+ // 'inline' — images flow inline at imageMaxWidth (default)
18
+ // 'lightbox' — click an image to enlarge in a fullscreen overlay
19
+ imageDisplay: 'inline',
20
+ imageMaxWidth: '100%',
21
+ // Defaults for the upload-time image processing UI.
22
+ // Per-upload values override these in the dialog.
23
+ processing: {
24
+ maxDimension: 2400,
25
+ format: 'webp',
26
+ quality: 0.85,
27
+ losslessForPng: true,
28
+ },
29
+ },
30
+ // Multi-site config. Leave undefined for single-site operation; the
31
+ // runtime then serves every request as siteId='default'. To run
32
+ // multiple sites in one Amplify environment, declare two or more
33
+ // entries here:
34
+ //
35
+ // sites: {
36
+ // blog: {
37
+ // domains: ['blog.example.com', 'www.example.com'],
38
+ // name: 'My Blog',
39
+ // url: 'https://blog.example.com',
40
+ // },
41
+ // docs: {
42
+ // domains: ['docs.example.com'],
43
+ // name: 'Docs',
44
+ // url: 'https://docs.example.com',
45
+ // },
46
+ // },
47
+ //
48
+ // In multi-site mode the middleware forces `Cache-Control: private,
49
+ // no-store` on public responses (Amplify Hosting's CloudFront cache
50
+ // key cannot disambiguate by Host). Single-site mode leaves caching
51
+ // to the default page directives.
52
+
53
+ // 'iso' — YYYY-MM-DD (default; SSR-safe, locale-neutral)
54
+ // 'long' — "April 27, 2026" (en-US)
55
+ // 'locale' — browser/server locale
56
+ dateFormat: 'iso',
57
+ // IANA timezone used for date rendering. Pin this so SSR and CSR
58
+ // always produce the same string. Examples: 'Asia/Tokyo', 'America/New_York'.
59
+ timezone: 'UTC',
60
+ // Active plugins. Order doesn't matter; the runtime aggregates metadata
61
+ // and runs hooks for events each plugin subscribes to.
62
+ //
63
+ // Plugin authors:
64
+ // - Plugin factories must return a plain `AmplessPlugin` object. Do NOT
65
+ // perform side effects at module top level (network calls, FS writes,
66
+ // global state) — both trusted and untrusted Lambdas import this file,
67
+ // so module-level work runs in every trust context regardless of which
68
+ // Lambda actually invokes the plugin's hooks.
69
+ // - Hooks must be idempotent. SQS guarantees at-least-once delivery and
70
+ // the dispatcher fans out to both queues; a single source MODIFY can
71
+ // trigger your hook more than once.
72
+ // - Use `ctx.writePublicAsset(key, ...)` for any S3 write — the runtime
73
+ // automatically namespaces under `public/plugins/{your-plugin-name}/`.
74
+ plugins: [
75
+ seoPlugin({
76
+ // defaultOgImage: '/og.png',
77
+ // twitterSite: '@example',
78
+ }),
79
+ rssPlugin({
80
+ limit: 20,
81
+ // language: 'ja',
82
+ }),
83
+ // webhookPlugin({
84
+ // endpoints: [
85
+ // {
86
+ // url: 'https://example.com/webhook',
87
+ // secret: process.env.WEBHOOK_SECRET,
88
+ // events: ['content.published', 'content.unpublished', 'content.deleted'],
89
+ // },
90
+ // ],
91
+ // }),
92
+ //
93
+ // Per-post OG images: SNS crawlers hit `/og/<slug>` and we render
94
+ // a JSX card → PNG via Next.js `ImageResponse`. Requires at least one
95
+ // font — ship a .ttf from your CDN or `/public` directory.
96
+ //
97
+ // ogImagePlugin({
98
+ // fonts: [
99
+ // {
100
+ // name: 'Inter',
101
+ // data: loadFontFromUrl('https://example.com/fonts/Inter-Regular.ttf'),
102
+ // weight: 400,
103
+ // },
104
+ // ],
105
+ // // 'content' picks the first image in the post body. Use 'theme'
106
+ // // + themeImageUrl for a fixed banner, or 'none' for text-only.
107
+ // image: 'content',
108
+ // }),
109
+ ],
110
+ })
@@ -0,0 +1,7 @@
1
+ // Back-compat shim. I18n provider + hooks moved to `@ampless/admin`
2
+ // (L2 extraction). The component tree mounts the provider inside the
3
+ // admin layout factory; the root layout in `app/layout.tsx` still
4
+ // wraps the public site in this same provider to keep client-side
5
+ // `useT()` calls working from theme-side components too.
6
+
7
+ export { I18nProvider, useT, useLocale } from '@ampless/admin/components'
@@ -0,0 +1,69 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+
5
+ interface LightboxBinderProps {
6
+ scopeSelector: string
7
+ /** Site default. Per-image data-display always wins over this. */
8
+ defaultLightbox?: boolean
9
+ }
10
+
11
+ // Wires click-to-enlarge for images inside any element. An image opts in
12
+ // when either its `data-display="lightbox"` attribute is set or the site
13
+ // default enables it (and the image hasn't opted out via data-display="inline").
14
+ export function LightboxBinder({ scopeSelector, defaultLightbox = false }: LightboxBinderProps) {
15
+ const [src, setSrc] = useState<string | null>(null)
16
+ const [alt, setAlt] = useState<string>('')
17
+
18
+ useEffect(() => {
19
+ const root = document.querySelector(scopeSelector)
20
+ if (!root) return
21
+ const imgs = root.querySelectorAll<HTMLImageElement>('img')
22
+ const cleanup: Array<() => void> = []
23
+ imgs.forEach((img) => {
24
+ const display = img.dataset.display
25
+ const enabled = display === 'lightbox' || (display !== 'inline' && defaultLightbox)
26
+ if (!enabled) return
27
+ img.style.cursor = 'zoom-in'
28
+ const onClick = () => {
29
+ setSrc(img.src)
30
+ setAlt(img.alt)
31
+ }
32
+ img.addEventListener('click', onClick)
33
+ cleanup.push(() => img.removeEventListener('click', onClick))
34
+ })
35
+ return () => {
36
+ cleanup.forEach((fn) => fn())
37
+ }
38
+ }, [scopeSelector, defaultLightbox])
39
+
40
+ useEffect(() => {
41
+ function onKey(e: KeyboardEvent) {
42
+ if (e.key === 'Escape') setSrc(null)
43
+ }
44
+ if (src) {
45
+ document.addEventListener('keydown', onKey)
46
+ return () => document.removeEventListener('keydown', onKey)
47
+ }
48
+ }, [src])
49
+
50
+ if (!src) return null
51
+
52
+ return (
53
+ <div
54
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/85 p-4"
55
+ onClick={() => setSrc(null)}
56
+ role="dialog"
57
+ aria-modal="true"
58
+ >
59
+ {/* eslint-disable-next-line @next/next/no-img-element */}
60
+ <img
61
+ src={src}
62
+ alt={alt}
63
+ className="max-h-full max-w-full object-contain"
64
+ style={{ cursor: 'zoom-out' }}
65
+ onClick={(e) => e.stopPropagation()}
66
+ />
67
+ </div>
68
+ )
69
+ }