create-ampless 0.2.0-alpha.9 → 1.0.0-alpha.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +77 -0
- package/README.md +4 -1
- package/dist/index.js +99 -47
- package/dist/templates/_shared/AGENTS.ja.md +104 -0
- package/dist/templates/_shared/AGENTS.md +104 -0
- package/dist/templates/_shared/README.ja.md +175 -0
- package/dist/templates/_shared/README.md +175 -0
- package/dist/templates/_shared/RUNBOOK.ja.md +120 -0
- package/dist/templates/_shared/RUNBOOK.md +24 -51
- package/dist/templates/_shared/amplify/auth/post-confirmation/resource.ts +6 -0
- package/dist/templates/_shared/amplify/backend.ts +2 -0
- package/dist/templates/_shared/amplify/data/get-published-post.js +9 -12
- package/dist/templates/_shared/amplify/data/list-posts-by-tag.js +6 -9
- package/dist/templates/_shared/amplify/data/list-published-posts.js +10 -11
- package/dist/templates/_shared/amplify/data/resource.custom.ts +1 -2
- package/dist/templates/_shared/amplify/data/resource.ts +22 -5
- package/dist/templates/_shared/amplify/events/processor-untrusted/resource.ts +4 -0
- package/dist/templates/_shared/amplify/functions/mcp-handler/handler.ts +1 -0
- package/dist/templates/_shared/amplify/functions/mcp-handler/resource.ts +11 -0
- package/dist/templates/_shared/amplify/functions/user-admin/resource.ts +6 -0
- package/dist/templates/_shared/app/(admin)/admin/mcp-tokens/page.tsx +5 -0
- package/dist/templates/_shared/app/globals.css +55 -39
- package/dist/templates/_shared/app/layout.tsx +24 -11
- package/dist/templates/_shared/app/site/[siteId]/r/[slug]/[[...path]]/route.ts +17 -0
- package/dist/templates/_shared/cms.config.ts +1 -23
- package/dist/templates/_shared/components/site-chrome/site-sidebar.tsx +2 -3
- package/dist/templates/_shared/lib/admin.ts +9 -7
- package/dist/templates/_shared/lib/posts-public.ts +1 -1
- package/dist/templates/_shared/package.json +11 -12
- package/dist/templates/_shared/proxy.ts +3 -3
- package/dist/templates/blog/README.ja.md +22 -0
- package/dist/templates/blog/README.md +17 -47
- package/dist/templates/blog/manifest.ts +1 -1
- package/dist/templates/blog/pages/feed.ts +3 -4
- package/dist/templates/blog/pages/home.tsx +5 -8
- package/dist/templates/blog/pages/post.tsx +7 -7
- package/dist/templates/blog/pages/sitemap.ts +3 -4
- package/dist/templates/blog/pages/tag.tsx +4 -4
- package/dist/templates/blog/tokens.css +26 -40
- package/dist/templates/corporate/README.ja.md +18 -0
- package/dist/templates/corporate/README.md +12 -14
- package/dist/templates/corporate/pages/feed.ts +2 -3
- package/dist/templates/corporate/pages/home.tsx +5 -8
- package/dist/templates/corporate/pages/post.tsx +7 -7
- package/dist/templates/corporate/pages/sitemap.ts +2 -3
- package/dist/templates/corporate/pages/tag.tsx +4 -4
- package/dist/templates/corporate/tokens.css +17 -39
- package/dist/templates/dads/README.ja.md +31 -0
- package/dist/templates/dads/README.md +13 -17
- package/dist/templates/dads/pages/feed.ts +2 -3
- package/dist/templates/dads/pages/home.tsx +5 -8
- package/dist/templates/dads/pages/post.tsx +7 -7
- package/dist/templates/dads/pages/sitemap.ts +2 -3
- package/dist/templates/dads/pages/tag.tsx +4 -4
- package/dist/templates/dads/tokens.css +22 -42
- package/dist/templates/docs/README.ja.md +24 -0
- package/dist/templates/docs/README.md +10 -13
- package/dist/templates/docs/pages/feed.ts +2 -3
- package/dist/templates/docs/pages/home.tsx +5 -6
- package/dist/templates/docs/pages/post.tsx +8 -8
- package/dist/templates/docs/pages/sitemap.ts +2 -3
- package/dist/templates/docs/pages/tag.tsx +5 -5
- package/dist/templates/docs/tokens.css +17 -39
- package/dist/templates/landing/README.ja.md +20 -0
- package/dist/templates/landing/README.md +14 -19
- package/dist/templates/landing/pages/feed.ts +3 -4
- package/dist/templates/landing/pages/home.tsx +5 -8
- package/dist/templates/landing/pages/post.tsx +7 -7
- package/dist/templates/landing/pages/sitemap.ts +2 -3
- package/dist/templates/landing/pages/tag.tsx +4 -4
- package/dist/templates/landing/tokens.css +17 -39
- package/dist/templates/minimal/README.ja.md +14 -0
- package/dist/templates/minimal/README.md +9 -47
- package/dist/templates/minimal/pages/feed.ts +3 -4
- package/dist/templates/minimal/pages/home.tsx +3 -4
- package/dist/templates/minimal/pages/post.tsx +6 -6
- package/dist/templates/minimal/pages/sitemap.ts +3 -4
- package/dist/templates/minimal/pages/tag.tsx +3 -3
- package/dist/templates/minimal/tokens.css +17 -39
- package/package.json +1 -1
- package/dist/templates/_shared/app/(admin)/admin/sites/page.tsx +0 -5
- package/dist/templates/_shared/app/site/[siteId]/raw/[slug]/route.ts +0 -5
- package/dist/templates/_shared/lib/admin-site-client.ts +0 -10
- package/dist/templates/_shared/lib/admin-site.ts +0 -12
|
@@ -2,23 +2,21 @@ import { util } from '@aws-appsync/utils'
|
|
|
2
2
|
|
|
3
3
|
// AppSync JS resolver: list published posts for a given tag, newest first.
|
|
4
4
|
// Reads the denormalized PostTag table where:
|
|
5
|
-
// PK =
|
|
5
|
+
// PK = tag
|
|
6
6
|
// SK = `${publishedAt}#${postId}` (so descending SK = newest first)
|
|
7
7
|
//
|
|
8
8
|
// Authorization is enforced by AppSync; the resolver itself only encodes
|
|
9
|
-
// the tag
|
|
9
|
+
// the tag partition condition. Drafts never appear here because the
|
|
10
10
|
// admin client only writes PostTag rows for posts whose status is
|
|
11
11
|
// 'published'.
|
|
12
12
|
export function request(ctx) {
|
|
13
|
-
const {
|
|
13
|
+
const { tag, limit, nextToken } = ctx.args
|
|
14
14
|
return {
|
|
15
15
|
operation: 'Query',
|
|
16
16
|
query: {
|
|
17
|
-
expression: '#
|
|
18
|
-
expressionNames: { '#
|
|
19
|
-
expressionValues: util.dynamodb.toMapValues({
|
|
20
|
-
':siteIdTag': `${siteId}#${tag}`,
|
|
21
|
-
}),
|
|
17
|
+
expression: '#tag = :tag',
|
|
18
|
+
expressionNames: { '#tag': 'tag' },
|
|
19
|
+
expressionValues: util.dynamodb.toMapValues({ ':tag': tag }),
|
|
22
20
|
},
|
|
23
21
|
scanIndexForward: false, // newest first (SK descends)
|
|
24
22
|
limit: limit ?? 20,
|
|
@@ -33,7 +31,6 @@ export function response(ctx) {
|
|
|
33
31
|
// PublicPost shape `listPublishedPosts` returns; the detail view should
|
|
34
32
|
// call `getPublishedPost(slug)` for the full body.
|
|
35
33
|
const items = (ctx.result.items ?? []).map((row) => ({
|
|
36
|
-
siteId: row.siteId,
|
|
37
34
|
postId: row.postId,
|
|
38
35
|
slug: row.slug,
|
|
39
36
|
title: row.title,
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import { util } from '@aws-appsync/utils'
|
|
2
2
|
|
|
3
|
-
// AppSync JS resolver: list
|
|
3
|
+
// AppSync JS resolver: list published posts, newest first.
|
|
4
4
|
//
|
|
5
|
-
// Reads the `
|
|
6
|
-
// PK =
|
|
5
|
+
// Reads the `byStatus` GSI:
|
|
6
|
+
// PK = status (denormalized field set by writers)
|
|
7
7
|
// SK = publishedAt
|
|
8
|
-
// so a single Query reads only
|
|
9
|
-
//
|
|
8
|
+
// so a single Query reads only the `published` partition. Drafts never
|
|
9
|
+
// appear because the PK condition pins status='published'.
|
|
10
10
|
//
|
|
11
11
|
// Date-range filtering (`from`, `to`) is pushed into the SK condition,
|
|
12
12
|
// so DynamoDB only reads the matching range. `nextToken` paginates
|
|
13
13
|
// without re-issuing a fresh query.
|
|
14
14
|
export function request(ctx) {
|
|
15
|
-
const {
|
|
15
|
+
const { from, to, limit, nextToken } = ctx.args
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const expressionValueMap = { ':siteIdStatus': partition }
|
|
17
|
+
let keyExpression = '#status = :status'
|
|
18
|
+
const expressionNames = { '#status': 'status' }
|
|
19
|
+
const expressionValueMap = { ':status': 'published' }
|
|
21
20
|
|
|
22
21
|
if (from && to) {
|
|
23
22
|
keyExpression += ' AND #publishedAt BETWEEN :from AND :to'
|
|
@@ -36,7 +35,7 @@ export function request(ctx) {
|
|
|
36
35
|
|
|
37
36
|
return {
|
|
38
37
|
operation: 'Query',
|
|
39
|
-
index: '
|
|
38
|
+
index: 'byStatus',
|
|
40
39
|
query: {
|
|
41
40
|
expression: keyExpression,
|
|
42
41
|
expressionNames,
|
|
@@ -14,12 +14,11 @@
|
|
|
14
14
|
// return {
|
|
15
15
|
// Bookmark: a
|
|
16
16
|
// .model({
|
|
17
|
-
// siteId: a.string().required(),
|
|
18
17
|
// bookmarkId: a.id().required(),
|
|
19
18
|
// url: a.string().required(),
|
|
20
19
|
// title: a.string(),
|
|
21
20
|
// })
|
|
22
|
-
// .identifier(['
|
|
21
|
+
// .identifier(['bookmarkId'])
|
|
23
22
|
// .authorization((allow: any) => [allow.groups(['ampless-admin', 'ampless-editor'])]),
|
|
24
23
|
// }
|
|
25
24
|
// }
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url'
|
|
2
2
|
import { dirname, resolve } from 'node:path'
|
|
3
3
|
import { a, defineData, type ClientSchema } from '@aws-amplify/backend'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
amplessSchemaModels,
|
|
6
|
+
amplessSchemaAuthorization,
|
|
7
|
+
defaultAuthorizationModes,
|
|
8
|
+
} from '@ampless/backend'
|
|
5
9
|
import { userAdmin } from '../functions/user-admin/resource.js'
|
|
10
|
+
import { mcpHandler } from '../functions/mcp-handler/resource.js'
|
|
6
11
|
import { customSchemaModels } from './resource.custom.js'
|
|
7
12
|
|
|
8
13
|
// AppSync's `a.handler.custom({ entry })` paths are resolved by CDK
|
|
@@ -26,10 +31,22 @@ const resolverPaths = {
|
|
|
26
31
|
listPostsByTag: resolve(__dirname, 'list-posts-by-tag.js'),
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
const schema = a
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
const schema = a
|
|
35
|
+
.schema({
|
|
36
|
+
...amplessSchemaModels(a, {
|
|
37
|
+
resolverPaths,
|
|
38
|
+
userAdminFunction: userAdmin,
|
|
39
|
+
}),
|
|
40
|
+
...customSchemaModels(a),
|
|
41
|
+
})
|
|
42
|
+
// Schema-level `.authorization()` is the only place resource auth is
|
|
43
|
+
// honoured in `@aws-amplify/data-schema` — model-level callbacks have
|
|
44
|
+
// `resource` destructured out of `allow`. The helper returns an empty
|
|
45
|
+
// array when no Lambda refs are supplied, so the schema stays
|
|
46
|
+
// unaffected if MCP is later removed.
|
|
47
|
+
.authorization((allow) => amplessSchemaAuthorization(allow, {
|
|
48
|
+
mcpHandlerFunction: mcpHandler,
|
|
49
|
+
}))
|
|
33
50
|
|
|
34
51
|
export type Schema = ClientSchema<typeof schema>
|
|
35
52
|
export const data = defineData({
|
|
@@ -6,4 +6,8 @@ export const processorUntrusted = defineFunction({
|
|
|
6
6
|
// Untrusted plugins do pure JS work; modest memory.
|
|
7
7
|
memoryMB: 256,
|
|
8
8
|
timeoutSeconds: 30,
|
|
9
|
+
// Pin to the data stack so all event-system Lambdas live together
|
|
10
|
+
// (dispatcher / processor-trusted are also `data`) and the function
|
|
11
|
+
// stack doesn't bridge data ↔ storage ↔ auth in a CFN cycle.
|
|
12
|
+
resourceGroupName: 'data',
|
|
9
13
|
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { handler } from '@ampless/backend/functions/mcp-handler'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineFunction } from '@aws-amplify/backend'
|
|
2
|
+
|
|
3
|
+
export const mcpHandler = defineFunction({
|
|
4
|
+
name: 'mcp-handler',
|
|
5
|
+
entry: './handler.ts',
|
|
6
|
+
// Co-locate with the data stack so the function has the KvStore
|
|
7
|
+
// table ARN available without cross-stack references at synth time.
|
|
8
|
+
resourceGroupName: 'data',
|
|
9
|
+
memoryMB: 512,
|
|
10
|
+
timeoutSeconds: 30,
|
|
11
|
+
})
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { defineFunction } from '@aws-amplify/backend'
|
|
2
2
|
|
|
3
|
+
// `resourceGroupName: 'data'` co-locates this Lambda with the AppSync
|
|
4
|
+
// stack. Required because ampless wires userAdmin as a custom GraphQL
|
|
5
|
+
// resolver (data → function) while api-key-renewer reads graphqlApi
|
|
6
|
+
// (function → data); leaving userAdmin in the default `function` stack
|
|
7
|
+
// turns those two arrows into a CloudFormation circular dependency.
|
|
3
8
|
export const userAdmin = defineFunction({
|
|
4
9
|
name: 'user-admin',
|
|
5
10
|
entry: './handler.ts',
|
|
11
|
+
resourceGroupName: 'data',
|
|
6
12
|
})
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
@import 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Tailwind v4 doesn't scan `node_modules/` by default. The admin UI
|
|
5
|
+
* (`@ampless/admin`) and runtime UI bits (`@ampless/runtime`) ship
|
|
6
|
+
* compiled JS that uses Tailwind utility classes; without these
|
|
7
|
+
* explicit sources, every utility used only inside those packages
|
|
8
|
+
* (e.g. the admin sidebar's `flex`, `fixed`, `md:sticky`, ...) gets
|
|
9
|
+
* tree-shaken out of the final CSS, and the admin layout falls back
|
|
10
|
+
* to browser defaults — sidebar overlaps main content, etc.
|
|
11
|
+
*/
|
|
12
|
+
@source '../node_modules/@ampless/admin/dist';
|
|
13
|
+
@source '../node_modules/@ampless/runtime/dist';
|
|
14
|
+
|
|
2
15
|
@plugin '@tailwindcss/typography';
|
|
3
16
|
/* DADS theme plugin from Digital Agency. Loaded globally so the
|
|
4
17
|
* `templates/dads/` theme can use DADS-conformant color variables
|
|
@@ -36,6 +49,31 @@
|
|
|
36
49
|
--radius-sm: calc(var(--radius) - 4px);
|
|
37
50
|
}
|
|
38
51
|
|
|
52
|
+
/*
|
|
53
|
+
* Color scheme:
|
|
54
|
+
*
|
|
55
|
+
* - `:root { color-scheme: light dark }` opts the page into both modes
|
|
56
|
+
* and asks the browser to pick based on the visitor's
|
|
57
|
+
* `prefers-color-scheme` setting.
|
|
58
|
+
* - `html[data-color-scheme='light' | 'dark']` lets the site admin pin
|
|
59
|
+
* one mode regardless of system preference. The attribute is written
|
|
60
|
+
* by `app/layout.tsx` from the per-site setting
|
|
61
|
+
* (`theme.colorScheme`, default `'auto'` = no attribute).
|
|
62
|
+
* - Token values below (and in each theme's `tokens.css`) use the CSS
|
|
63
|
+
* `light-dark(L, D)` function so a single declaration covers both
|
|
64
|
+
* modes — the active `color-scheme` selects between L and D at
|
|
65
|
+
* paint time.
|
|
66
|
+
*/
|
|
67
|
+
:root {
|
|
68
|
+
color-scheme: light dark;
|
|
69
|
+
}
|
|
70
|
+
html[data-color-scheme='light'] {
|
|
71
|
+
color-scheme: light;
|
|
72
|
+
}
|
|
73
|
+
html[data-color-scheme='dark'] {
|
|
74
|
+
color-scheme: dark;
|
|
75
|
+
}
|
|
76
|
+
|
|
39
77
|
/*
|
|
40
78
|
* Default tokens — used as fallback when no theme's `[data-theme=...]`
|
|
41
79
|
* selector matches (e.g. body has no data-theme attribute, or admin
|
|
@@ -45,49 +83,27 @@
|
|
|
45
83
|
* `<body data-theme="...">` attribute makes the scoped selector match.
|
|
46
84
|
*/
|
|
47
85
|
: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);
|
|
86
|
+
--background: light-dark(oklch(1 0 0), oklch(0.145 0 0));
|
|
87
|
+
--foreground: light-dark(oklch(0.145 0 0), oklch(0.985 0 0));
|
|
88
|
+
--card: light-dark(oklch(1 0 0), oklch(0.145 0 0));
|
|
89
|
+
--card-foreground: light-dark(oklch(0.145 0 0), oklch(0.985 0 0));
|
|
90
|
+
--primary: light-dark(oklch(0.205 0 0), oklch(0.985 0 0));
|
|
91
|
+
--primary-foreground: light-dark(oklch(0.985 0 0), oklch(0.205 0 0));
|
|
92
|
+
--secondary: light-dark(oklch(0.97 0 0), oklch(0.269 0 0));
|
|
93
|
+
--secondary-foreground: light-dark(oklch(0.205 0 0), oklch(0.985 0 0));
|
|
94
|
+
--muted: light-dark(oklch(0.97 0 0), oklch(0.269 0 0));
|
|
95
|
+
--muted-foreground: light-dark(oklch(0.556 0 0), oklch(0.708 0 0));
|
|
96
|
+
--accent: light-dark(oklch(0.97 0 0), oklch(0.269 0 0));
|
|
97
|
+
--accent-foreground: light-dark(oklch(0.205 0 0), oklch(0.985 0 0));
|
|
98
|
+
--destructive: light-dark(oklch(0.577 0.245 27.325), oklch(0.396 0.141 25.723));
|
|
99
|
+
--destructive-foreground: light-dark(oklch(0.985 0 0), oklch(0.985 0 0));
|
|
100
|
+
--border: light-dark(oklch(0.922 0 0), oklch(0.269 0 0));
|
|
101
|
+
--input: light-dark(oklch(0.922 0 0), oklch(0.269 0 0));
|
|
102
|
+
--ring: light-dark(oklch(0.708 0 0), oklch(0.439 0 0));
|
|
65
103
|
--radius: 0.5rem;
|
|
66
104
|
--ampless-body-font: system-ui, -apple-system, sans-serif;
|
|
67
105
|
}
|
|
68
106
|
|
|
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
107
|
body {
|
|
92
108
|
font-family: var(--ampless-body-font, system-ui, -apple-system, sans-serif);
|
|
93
109
|
background: var(--background);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
import { headers } from 'next/headers'
|
|
3
|
-
import {
|
|
3
|
+
import { validateColorScheme } from '@ampless/runtime'
|
|
4
4
|
import { Providers } from './providers'
|
|
5
5
|
import { siteMetadata } from '@/lib/seo'
|
|
6
6
|
import { loadThemeConfig, renderThemeCss } from '@/lib/theme-config'
|
|
@@ -8,25 +8,38 @@ import { getLocale, getDictionary } from '@/lib/i18n'
|
|
|
8
8
|
import { I18nProvider } from '@/components/i18n-provider'
|
|
9
9
|
import './globals.css'
|
|
10
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
11
|
export async function generateMetadata(): Promise<Metadata> {
|
|
16
|
-
|
|
17
|
-
const siteId = h.get('x-site-id') ?? DEFAULT_SITE_ID
|
|
18
|
-
return siteMetadata(siteId)
|
|
12
|
+
return siteMetadata()
|
|
19
13
|
}
|
|
20
14
|
|
|
21
15
|
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
22
16
|
const h = await headers()
|
|
23
|
-
const
|
|
24
|
-
const theme = await loadThemeConfig(siteId)
|
|
17
|
+
const theme = await loadThemeConfig()
|
|
25
18
|
const themeCss = renderThemeCss(theme.cssVars)
|
|
26
19
|
const locale = getLocale()
|
|
27
20
|
const dict = getDictionary(locale)
|
|
21
|
+
// `data-color-scheme` pins the visitor to light or dark regardless
|
|
22
|
+
// of their system `prefers-color-scheme`. `'auto'` (the default)
|
|
23
|
+
// means we don't emit the attribute at all, so `globals.css`'s
|
|
24
|
+
// `:root { color-scheme: light dark }` lets the browser follow the
|
|
25
|
+
// system setting. Themes use `light-dark()` in their tokens.css so a
|
|
26
|
+
// single declaration covers both modes; the active `color-scheme`
|
|
27
|
+
// selects which value is rendered.
|
|
28
|
+
//
|
|
29
|
+
// The admin's theme-settings iframe preview passes the unsaved
|
|
30
|
+
// selection via `?previewColorScheme=<mode>`, which middleware
|
|
31
|
+
// forwards as `x-preview-color-scheme`. When that header is present
|
|
32
|
+
// it overrides the stored site setting so the preview updates live.
|
|
33
|
+
const previewHeader = h.get('x-preview-color-scheme')
|
|
34
|
+
const effectiveColorScheme = previewHeader
|
|
35
|
+
? validateColorScheme(previewHeader)
|
|
36
|
+
: theme.colorScheme
|
|
37
|
+
const htmlProps: { lang: string; 'data-color-scheme'?: 'light' | 'dark' } = { lang: locale }
|
|
38
|
+
if (effectiveColorScheme !== 'auto') {
|
|
39
|
+
htmlProps['data-color-scheme'] = effectiveColorScheme
|
|
40
|
+
}
|
|
28
41
|
return (
|
|
29
|
-
<html
|
|
42
|
+
<html {...htmlProps}>
|
|
30
43
|
<head>
|
|
31
44
|
{/* Inline `:root` overrides come AFTER globals.css so they win
|
|
32
45
|
against the static defaults. Validated values only — see
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ampless } from '@/lib/ampless'
|
|
2
|
+
import { createUnderscoreRouteHandler } from '@ampless/runtime/routes'
|
|
3
|
+
|
|
4
|
+
// Unified handler for the public `/_/<slug>(/...)` URL family.
|
|
5
|
+
// Covers two cases that both bypass the theme's post page:
|
|
6
|
+
//
|
|
7
|
+
// - `format: 'html'` posts with `metadata.no_layout === true`
|
|
8
|
+
// → bare HTML response, no Next.js root layout, no theme chrome
|
|
9
|
+
// - `format: 'static'` posts
|
|
10
|
+
// → S3 presigned URL redirect for the entrypoint and every bundle file
|
|
11
|
+
//
|
|
12
|
+
// File location uses the literal folder name `r/` (not `_/`) because
|
|
13
|
+
// Next.js's App Router excludes any path part starting with `_` from
|
|
14
|
+
// route discovery. The middleware rewrites the public `/_/` prefix
|
|
15
|
+
// to `/r/` internally; the browser URL stays `/_/<slug>(/...)`.
|
|
16
|
+
export const dynamic = 'force-dynamic'
|
|
17
|
+
export const GET = createUnderscoreRouteHandler(ampless)
|
|
@@ -27,29 +27,7 @@ export default defineConfig({
|
|
|
27
27
|
losslessForPng: true,
|
|
28
28
|
},
|
|
29
29
|
},
|
|
30
|
-
//
|
|
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
|
-
|
|
30
|
+
// Date display format.
|
|
53
31
|
// 'iso' — YYYY-MM-DD (default; SSR-safe, locale-neutral)
|
|
54
32
|
// 'long' — "April 27, 2026" (en-US)
|
|
55
33
|
// 'locale' — browser/server locale
|
|
@@ -4,7 +4,6 @@ import { listPostsByTag } from '@/lib/posts-public'
|
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
links: string | undefined
|
|
7
|
-
siteId: string
|
|
8
7
|
className?: string
|
|
9
8
|
}
|
|
10
9
|
|
|
@@ -19,7 +18,7 @@ interface Props {
|
|
|
19
18
|
* paired with `force-dynamic` on the page so fresh content shows up
|
|
20
19
|
* after publish events.
|
|
21
20
|
*/
|
|
22
|
-
export async function SiteSidebar({ links,
|
|
21
|
+
export async function SiteSidebar({ links, className }: Props) {
|
|
23
22
|
const items = parseLinkList(links)
|
|
24
23
|
if (items.length === 0) return null
|
|
25
24
|
|
|
@@ -28,7 +27,7 @@ export async function SiteSidebar({ links, siteId, className }: Props) {
|
|
|
28
27
|
items.map(async (item) => {
|
|
29
28
|
const tagRef = isTagListUrl(item.url)
|
|
30
29
|
if (!tagRef) return { type: 'link' as const, label: item.label, url: item.url }
|
|
31
|
-
const { items: posts } = await listPostsByTag(tagRef.tag, {
|
|
30
|
+
const { items: posts } = await listPostsByTag(tagRef.tag, { limit: 50 })
|
|
32
31
|
return {
|
|
33
32
|
type: 'tagSection' as const,
|
|
34
33
|
label: item.label,
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
// `amplify_outputs.json` and `cms.config` into a single `Admin`
|
|
8
8
|
// instance.
|
|
9
9
|
//
|
|
10
|
-
// NOTE:
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// ReferenceError on `ampless` at module init.
|
|
15
|
-
//
|
|
16
|
-
//
|
|
10
|
+
// NOTE: we pass `ampless` as a thunk (not the resolved instance) so we
|
|
11
|
+
// don't have a static `import './ampless'` at the top of this file.
|
|
12
|
+
// A static import would form the cycle
|
|
13
|
+
// `lib/admin.ts → lib/ampless.ts → themes-registry → themes → lib/i18n.ts → lib/admin.ts`
|
|
14
|
+
// and crash with a TDZ ReferenceError on `ampless` at module init.
|
|
15
|
+
// The thunk uses dynamic `import()` so `lib/ampless.ts` only loads on
|
|
16
|
+
// the first `loadSiteSettings` / `loadThemeConfig` call (request
|
|
17
|
+
// time), by which point every module has finished initialising.
|
|
17
18
|
|
|
18
19
|
import outputs from '../amplify_outputs.json'
|
|
19
20
|
import cmsConfig from '@/cms.config'
|
|
@@ -22,6 +23,7 @@ import { createAdmin } from '@ampless/admin'
|
|
|
22
23
|
export const admin = createAdmin({
|
|
23
24
|
outputs,
|
|
24
25
|
cmsConfig,
|
|
26
|
+
ampless: async () => (await import('./ampless')).ampless,
|
|
25
27
|
locale: (cmsConfig as { locale?: string }).locale ?? 'en',
|
|
26
28
|
})
|
|
27
29
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//
|
|
5
5
|
// New code should import from `@/lib/ampless` directly:
|
|
6
6
|
// import { ampless } from '@/lib/ampless'
|
|
7
|
-
// const posts = await ampless.listPublishedPosts(
|
|
7
|
+
// const posts = await ampless.listPublishedPosts()
|
|
8
8
|
|
|
9
9
|
import { ampless } from './ampless'
|
|
10
10
|
|
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
"build": "next build",
|
|
9
9
|
"start": "next start",
|
|
10
10
|
"lint": "next lint",
|
|
11
|
-
"sandbox": "ampx sandbox",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"copy-theme": "npx create-ampless@alpha copy-theme"
|
|
11
|
+
"sandbox": "ampx sandbox --once && next dev",
|
|
12
|
+
"update-ampless": "npx create-ampless@latest upgrade",
|
|
13
|
+
"copy-theme": "npx create-ampless@latest copy-theme"
|
|
15
14
|
},
|
|
16
15
|
"dependencies": {
|
|
17
16
|
"@aws-amplify/adapter-nextjs": "^1.6.0",
|
|
@@ -23,15 +22,15 @@
|
|
|
23
22
|
"@tiptap/pm": "^3.23.4",
|
|
24
23
|
"@tiptap/react": "^3.23.4",
|
|
25
24
|
"@tiptap/starter-kit": "^3.23.4",
|
|
26
|
-
"@ampless/plugin-og-image": "^0.2.0-alpha.
|
|
27
|
-
"@ampless/plugin-rss": "^0.2.0-alpha.
|
|
28
|
-
"@ampless/plugin-seo": "^0.2.0-alpha.
|
|
29
|
-
"@ampless/plugin-webhook": "^0.2.0-alpha.
|
|
30
|
-
"@ampless/admin": "^0.
|
|
31
|
-
"@ampless/backend": "^0.
|
|
32
|
-
"@ampless/runtime": "^0.
|
|
25
|
+
"@ampless/plugin-og-image": "^0.2.0-alpha.9",
|
|
26
|
+
"@ampless/plugin-rss": "^0.2.0-alpha.9",
|
|
27
|
+
"@ampless/plugin-seo": "^0.2.0-alpha.9",
|
|
28
|
+
"@ampless/plugin-webhook": "^0.2.0-alpha.9",
|
|
29
|
+
"@ampless/admin": "^1.0.0-alpha.26",
|
|
30
|
+
"@ampless/backend": "^1.0.0-alpha.19",
|
|
31
|
+
"@ampless/runtime": "^1.0.0-alpha.13",
|
|
33
32
|
"@digital-go-jp/tailwind-theme-plugin": "^0.3.4",
|
|
34
|
-
"ampless": "^0.
|
|
33
|
+
"ampless": "^1.0.0-alpha.9",
|
|
35
34
|
"aws-amplify": "^6.10.0",
|
|
36
35
|
"class-variance-authority": "^0.7.1",
|
|
37
36
|
"clsx": "^2.1.1",
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// for API stability; only the user-side file convention (proxy.ts +
|
|
6
6
|
// `export const proxy`) follows Next 16's rename.
|
|
7
7
|
//
|
|
8
|
-
// See `@ampless/runtime/middleware` for behaviour details:
|
|
9
|
-
//
|
|
10
|
-
// forwarding
|
|
8
|
+
// See `@ampless/runtime/middleware` for behaviour details:
|
|
9
|
+
// `/path` → `/site/default/path` rewrite, `?previewTheme=` header
|
|
10
|
+
// forwarding.
|
|
11
11
|
|
|
12
12
|
import cmsConfig from './cms.config'
|
|
13
13
|
import { createAmplessMiddleware } from '@ampless/runtime/middleware'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
> English: [README.md](./README.md)
|
|
2
|
+
>
|
|
3
|
+
# Blog テーマ
|
|
4
|
+
|
|
5
|
+
shadcn/ui のデフォルトに準じたニュートラル系モノクロ。時系列の投稿フィード、個別投稿ページ、タグ別一覧を提供します。
|
|
6
|
+
|
|
7
|
+
## カスタマイズ
|
|
8
|
+
|
|
9
|
+
`/admin/sites/<siteId>/theme` で設定:
|
|
10
|
+
|
|
11
|
+
- **プライマリカラー** — ボタン、リンク、強調表示の背景色
|
|
12
|
+
- **アクセントカラー**
|
|
13
|
+
- **フォーカスリング**
|
|
14
|
+
- **破壊的操作** — 削除等の操作色
|
|
15
|
+
- **角丸**
|
|
16
|
+
- **本文フォント** — システムサンセリフ / セリフ (Georgia) / セリフ (Iowan) / 等幅
|
|
17
|
+
- **トップに固定する記事のスラッグ** — 公開済み投稿を 1 件ホームの先頭に固定
|
|
18
|
+
- **ロゴ画像 URL**
|
|
19
|
+
- **ヘッダーナビ** — ラベル + URL のペア
|
|
20
|
+
- **フッターリンク** — ラベル + URL のペア
|
|
21
|
+
|
|
22
|
+
サイトごとのライト/ダークカラースキームは ampless の標準テーマトークン経由で反映されます。
|
|
@@ -1,52 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
> 日本語版: [README.ja.md](./README.ja.md)
|
|
2
|
+
>
|
|
3
|
+
# Blog theme
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
Neutral monochrome with shadcn/ui defaults. Chronological post feed, single-post page, and tag listing.
|
|
4
6
|
|
|
5
|
-
##
|
|
7
|
+
## Customizing
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
In `/admin/sites/<siteId>/theme`:
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
- **Primary color** — buttons, links, accent fills
|
|
12
|
+
- **Accent color**
|
|
13
|
+
- **Focus ring**
|
|
14
|
+
- **Destructive** — color used for destructive actions
|
|
15
|
+
- **Corner radius**
|
|
16
|
+
- **Body font** — System sans / Serif (Georgia) / Serif (Iowan) / Monospace
|
|
17
|
+
- **Featured post slug** — pin one published post to the top of the home page
|
|
18
|
+
- **Logo image URL**
|
|
19
|
+
- **Header navigation** — label + URL pairs
|
|
20
|
+
- **Footer links** — label + URL pairs
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
# Requires AWS credentials configured (`aws configure`).
|
|
15
|
-
# First run takes ~5–10 min to provision resources.
|
|
16
|
-
# Generates amplify_outputs.json when ready.
|
|
17
|
-
npx ampx sandbox
|
|
18
|
-
|
|
19
|
-
# 3. Start the Next.js dev server (terminal 2)
|
|
20
|
-
npm run dev
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Then open [http://localhost:3000](http://localhost:3000).
|
|
24
|
-
|
|
25
|
-
## First admin user
|
|
26
|
-
|
|
27
|
-
Open [http://localhost:3000/login](http://localhost:3000/login) and click **Create admin account**. The first user to register is automatically added to the `ampless-admin` Cognito group.
|
|
28
|
-
|
|
29
|
-
After that, manage content from `/admin`:
|
|
30
|
-
|
|
31
|
-
- `/admin` — dashboard
|
|
32
|
-
- `/admin/posts` — list / create / edit posts (tiptap editor)
|
|
33
|
-
- `/admin/media` — upload images to S3
|
|
34
|
-
|
|
35
|
-
## Production deploy
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
git init && git add . && git commit -m "init"
|
|
39
|
-
git remote add origin <your-repo>
|
|
40
|
-
git push
|
|
41
|
-
# Then connect the repo to AWS Amplify Hosting in the AWS console.
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Customize
|
|
45
|
-
|
|
46
|
-
- `cms.config.ts` — site name, media delivery mode, plugins
|
|
47
|
-
- `app/` — Next.js App Router pages (`(public)/` for the blog, `(admin)/` for the CMS)
|
|
48
|
-
- `amplify/` — Amplify Gen 2 backend definitions (auth / data / storage)
|
|
49
|
-
|
|
50
|
-
## Plugins
|
|
51
|
-
|
|
52
|
-
Enabled: {{plugins}}
|
|
22
|
+
Per-site light/dark color scheme is honored through the standard ampless theme tokens.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineTheme } from 'ampless'
|
|
2
2
|
|
|
3
3
|
// Customizable fields for the Blog theme. Edit values in
|
|
4
|
-
// `/admin/sites
|
|
4
|
+
// `/admin/sites/default/theme` — they're stored in KvStore and applied
|
|
5
5
|
// at render time as CSS variables on every public page.
|
|
6
6
|
//
|
|
7
7
|
// Labels / descriptions / groups accept either a plain string (works in
|