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.
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +229 -0
- package/dist/templates/_shared/RUNBOOK.md +178 -0
- package/dist/templates/_shared/amplify/auth/post-confirmation/handler.ts +4 -0
- package/dist/templates/_shared/amplify/auth/post-confirmation/resource.ts +6 -0
- package/dist/templates/_shared/amplify/auth/resource.ts +8 -0
- package/dist/templates/_shared/amplify/backend.ts +29 -0
- package/dist/templates/_shared/amplify/data/get-published-post.js +33 -0
- package/dist/templates/_shared/amplify/data/list-posts-by-tag.js +52 -0
- package/dist/templates/_shared/amplify/data/list-published-posts.js +57 -0
- package/dist/templates/_shared/amplify/data/resource.ts +30 -0
- package/dist/templates/_shared/amplify/events/dispatcher/handler.ts +4 -0
- package/dist/templates/_shared/amplify/events/dispatcher/resource.ts +12 -0
- package/dist/templates/_shared/amplify/events/processor-trusted/handler.ts +12 -0
- package/dist/templates/_shared/amplify/events/processor-trusted/resource.ts +14 -0
- package/dist/templates/_shared/amplify/events/processor-untrusted/handler.ts +10 -0
- package/dist/templates/_shared/amplify/events/processor-untrusted/resource.ts +9 -0
- package/dist/templates/_shared/amplify/functions/api-key-renewer/handler.ts +4 -0
- package/dist/templates/_shared/amplify/functions/api-key-renewer/resource.ts +12 -0
- package/dist/templates/_shared/amplify/storage/resource.ts +7 -0
- package/dist/templates/_shared/app/(admin)/admin/layout.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/media/page.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/page.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/posts/[postId]/page.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/posts/new/page.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/posts/page.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/sites/[siteId]/page.tsx +5 -0
- package/dist/templates/_shared/app/(admin)/admin/sites/[siteId]/theme/page.tsx +6 -0
- package/dist/templates/_shared/app/(admin)/admin/sites/page.tsx +5 -0
- package/dist/templates/_shared/app/api/media/[...path]/route.ts +5 -0
- package/dist/templates/_shared/app/globals.css +114 -0
- package/dist/templates/_shared/app/layout.tsx +48 -0
- package/dist/templates/_shared/app/login/page.tsx +4 -0
- package/dist/templates/_shared/app/providers.tsx +13 -0
- package/dist/templates/_shared/app/site/[siteId]/[slug]/page.tsx +10 -0
- package/dist/templates/_shared/app/site/[siteId]/feed.xml/route.ts +5 -0
- package/dist/templates/_shared/app/site/[siteId]/og/[slug]/route.ts +6 -0
- package/dist/templates/_shared/app/site/[siteId]/page.tsx +10 -0
- package/dist/templates/_shared/app/site/[siteId]/raw/[slug]/route.ts +5 -0
- package/dist/templates/_shared/app/site/[siteId]/sitemap.xml/route.ts +5 -0
- package/dist/templates/_shared/app/site/[siteId]/tag/[tag]/page.tsx +10 -0
- package/dist/templates/_shared/cms.config.ts +110 -0
- package/dist/templates/_shared/components/i18n-provider.tsx +7 -0
- package/dist/templates/_shared/components/lightbox-content.tsx +69 -0
- package/dist/templates/_shared/components/site-chrome/collapsible-sidebar.tsx +54 -0
- package/dist/templates/_shared/components/site-chrome/mobile-menu.tsx +68 -0
- package/dist/templates/_shared/components/site-chrome/site-footer.tsx +43 -0
- package/dist/templates/_shared/components/site-chrome/site-header.tsx +94 -0
- package/dist/templates/_shared/components/site-chrome/site-sidebar.tsx +81 -0
- package/dist/templates/_shared/components/tag-list.tsx +25 -0
- package/dist/templates/_shared/components.json +21 -0
- package/dist/templates/_shared/lib/admin-site-client.ts +10 -0
- package/dist/templates/_shared/lib/admin-site.ts +8 -0
- package/dist/templates/_shared/lib/admin.ts +24 -0
- package/dist/templates/_shared/lib/ampless.ts +23 -0
- package/dist/templates/_shared/lib/amplify-server.ts +7 -0
- package/dist/templates/_shared/lib/amplify.ts +9 -0
- package/dist/templates/_shared/lib/auth-server.ts +11 -0
- package/dist/templates/_shared/lib/cn.ts +5 -0
- package/dist/templates/_shared/lib/i18n.ts +31 -0
- package/dist/templates/_shared/lib/kv-provider.ts +7 -0
- package/dist/templates/_shared/lib/media.ts +6 -0
- package/dist/templates/_shared/lib/posts-provider.ts +7 -0
- package/dist/templates/_shared/lib/posts-public.ts +19 -0
- package/dist/templates/_shared/lib/posts.ts +12 -0
- package/dist/templates/_shared/lib/seo.ts +8 -0
- package/dist/templates/_shared/lib/site-settings.ts +8 -0
- package/dist/templates/_shared/lib/storage.ts +7 -0
- package/dist/templates/_shared/lib/theme-actions.ts +5 -0
- package/dist/templates/_shared/lib/theme-active.ts +8 -0
- package/dist/templates/_shared/lib/theme-config.ts +10 -0
- package/dist/templates/_shared/lib/upload.ts +6 -0
- package/dist/templates/_shared/middleware.ts +13 -0
- package/dist/templates/_shared/next.config.mjs +11 -0
- package/dist/templates/_shared/package.json +63 -0
- package/dist/templates/_shared/postcss.config.mjs +5 -0
- package/dist/templates/_shared/themes-registry.ts +38 -0
- package/dist/templates/_shared/tsconfig.json +23 -0
- package/dist/templates/blog/README.md +52 -0
- package/dist/templates/blog/index.ts +29 -0
- package/dist/templates/blog/manifest.ts +144 -0
- package/dist/templates/blog/pages/feed.ts +31 -0
- package/dist/templates/blog/pages/home.tsx +108 -0
- package/dist/templates/blog/pages/post.tsx +94 -0
- package/dist/templates/blog/pages/sitemap.ts +30 -0
- package/dist/templates/blog/pages/tag.tsx +76 -0
- package/dist/templates/blog/tokens.css +54 -0
- package/dist/templates/corporate/README.md +20 -0
- package/dist/templates/corporate/index.ts +25 -0
- package/dist/templates/corporate/manifest.ts +94 -0
- package/dist/templates/corporate/pages/feed.ts +29 -0
- package/dist/templates/corporate/pages/home.tsx +130 -0
- package/dist/templates/corporate/pages/post.tsx +96 -0
- package/dist/templates/corporate/pages/sitemap.ts +28 -0
- package/dist/templates/corporate/pages/tag.tsx +81 -0
- package/dist/templates/corporate/tokens.css +47 -0
- package/dist/templates/dads/README.md +35 -0
- package/dist/templates/dads/index.ts +25 -0
- package/dist/templates/dads/manifest.ts +84 -0
- package/dist/templates/dads/pages/feed.ts +29 -0
- package/dist/templates/dads/pages/home.tsx +126 -0
- package/dist/templates/dads/pages/post.tsx +102 -0
- package/dist/templates/dads/pages/sitemap.ts +28 -0
- package/dist/templates/dads/pages/tag.tsx +86 -0
- package/dist/templates/dads/tokens.css +67 -0
- package/dist/templates/docs/README.md +27 -0
- package/dist/templates/docs/index.ts +25 -0
- package/dist/templates/docs/manifest.ts +89 -0
- package/dist/templates/docs/pages/feed.ts +29 -0
- package/dist/templates/docs/pages/home.tsx +88 -0
- package/dist/templates/docs/pages/post.tsx +96 -0
- package/dist/templates/docs/pages/sitemap.ts +28 -0
- package/dist/templates/docs/pages/tag.tsx +79 -0
- package/dist/templates/docs/tokens.css +55 -0
- package/dist/templates/landing/README.md +25 -0
- package/dist/templates/landing/index.ts +25 -0
- package/dist/templates/landing/manifest.ts +118 -0
- package/dist/templates/landing/pages/feed.ts +31 -0
- package/dist/templates/landing/pages/home.tsx +123 -0
- package/dist/templates/landing/pages/post.tsx +95 -0
- package/dist/templates/landing/pages/sitemap.ts +28 -0
- package/dist/templates/landing/pages/tag.tsx +85 -0
- package/dist/templates/landing/tokens.css +47 -0
- package/dist/templates/minimal/README.md +52 -0
- package/dist/templates/minimal/index.ts +25 -0
- package/dist/templates/minimal/manifest.ts +35 -0
- package/dist/templates/minimal/pages/feed.ts +31 -0
- package/dist/templates/minimal/pages/home.tsx +44 -0
- package/dist/templates/minimal/pages/post.tsx +65 -0
- package/dist/templates/minimal/pages/sitemap.ts +30 -0
- package/dist/templates/minimal/pages/tag.tsx +46 -0
- package/dist/templates/minimal/tokens.css +46 -0
- 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,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,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,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,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,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,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
|
+
}
|