create-ampless 0.2.0-alpha.9 → 1.0.0-alpha.100
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 +6 -3
- package/dist/index.js +743 -84
- package/dist/templates/_shared/AGENTS.ja.md +94 -0
- package/dist/templates/_shared/AGENTS.md +94 -0
- package/dist/templates/_shared/README.ja.md +239 -0
- package/dist/templates/_shared/README.md +239 -0
- package/dist/templates/_shared/RUNBOOK.ja.md +120 -0
- package/dist/templates/_shared/RUNBOOK.md +32 -58
- package/dist/templates/_shared/THEMES.ja.md +495 -0
- package/dist/templates/_shared/THEMES.md +523 -0
- package/dist/templates/_shared/amplify/auth/post-confirmation/resource.ts +7 -0
- package/dist/templates/_shared/amplify/backend.ts +10 -0
- package/dist/templates/_shared/amplify/data/get-media-by-src.js +45 -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 +28 -7
- package/dist/templates/_shared/amplify/events/dispatcher/resource.ts +1 -0
- package/dist/templates/_shared/amplify/events/processor-trusted/resource.ts +1 -0
- package/dist/templates/_shared/amplify/events/processor-untrusted/resource.ts +5 -0
- package/dist/templates/_shared/amplify/functions/api-key-renewer/resource.ts +1 -0
- package/dist/templates/_shared/amplify/functions/mcp-handler/handler.ts +1 -0
- package/dist/templates/_shared/amplify/functions/mcp-handler/resource.ts +12 -0
- package/dist/templates/_shared/amplify/functions/plugin-secret-handler/handler.ts +11 -0
- package/dist/templates/_shared/amplify/functions/plugin-secret-handler/resource.ts +12 -0
- package/dist/templates/_shared/amplify/functions/user-admin/resource.ts +7 -0
- package/dist/templates/_shared/amplify/secrets/.gitkeep +0 -0
- package/dist/templates/_shared/amplify/secrets/encryption-key.ts +9 -0
- package/dist/templates/_shared/app/(admin)/admin/mcp-tokens/page.tsx +5 -0
- package/dist/templates/_shared/app/(admin)/admin/plugins/page.tsx +5 -0
- package/dist/templates/_shared/app/globals.css +55 -39
- package/dist/templates/_shared/app/layout.tsx +51 -16
- package/dist/templates/_shared/app/providers.tsx +0 -2
- package/dist/templates/_shared/app/raw/[slug]/route.ts +10 -0
- package/dist/templates/_shared/app/static/[slug]/[[...path]]/route.ts +14 -0
- package/dist/templates/_shared/cms.config.ts +64 -23
- package/dist/templates/_shared/components/site-chrome/site-sidebar.tsx +3 -4
- package/dist/templates/_shared/components/tag-list.tsx +9 -2
- package/dist/templates/_shared/components.json +1 -1
- package/dist/templates/_shared/docs/plugin-author-guide.ja.md +934 -0
- package/dist/templates/_shared/docs/plugin-author-guide.md +1336 -0
- package/dist/templates/_shared/lib/admin.ts +12 -12
- package/dist/templates/_shared/lib/ampless.ts +2 -8
- package/dist/templates/_shared/lib/amplify.ts +0 -5
- package/dist/templates/_shared/package.json +51 -43
- package/dist/templates/_shared/plugins/README.ja.md +139 -0
- package/dist/templates/_shared/plugins/README.md +143 -0
- package/dist/templates/_shared/proxy.ts +23 -8
- 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 +4 -5
- package/dist/templates/blog/pages/home.tsx +10 -15
- package/dist/templates/blog/pages/post.tsx +42 -14
- package/dist/templates/blog/pages/sitemap.ts +4 -5
- package/dist/templates/blog/pages/tag.tsx +8 -10
- 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 +3 -4
- package/dist/templates/corporate/pages/home.tsx +7 -12
- package/dist/templates/corporate/pages/post.tsx +20 -14
- package/dist/templates/corporate/pages/sitemap.ts +3 -4
- package/dist/templates/corporate/pages/tag.tsx +8 -10
- 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 +3 -4
- package/dist/templates/dads/pages/home.tsx +7 -12
- package/dist/templates/dads/pages/post.tsx +20 -14
- package/dist/templates/dads/pages/sitemap.ts +3 -4
- package/dist/templates/dads/pages/tag.tsx +8 -10
- 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 +3 -4
- package/dist/templates/docs/pages/home.tsx +6 -9
- package/dist/templates/docs/pages/post.tsx +19 -13
- package/dist/templates/docs/pages/sitemap.ts +3 -4
- package/dist/templates/docs/pages/tag.tsx +8 -10
- 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 +4 -5
- package/dist/templates/landing/pages/home.tsx +7 -12
- package/dist/templates/landing/pages/post.tsx +20 -14
- package/dist/templates/landing/pages/sitemap.ts +3 -4
- package/dist/templates/landing/pages/tag.tsx +8 -10
- 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 +4 -5
- package/dist/templates/minimal/pages/home.tsx +6 -8
- package/dist/templates/minimal/pages/post.tsx +19 -12
- package/dist/templates/minimal/pages/sitemap.ts +4 -5
- package/dist/templates/minimal/pages/tag.tsx +7 -8
- package/dist/templates/minimal/tokens.css +17 -39
- package/dist/templates/plugin-local/README.md +34 -0
- package/dist/templates/plugin-local/index.ts +39 -0
- package/dist/templates/plugin-standalone/CHANGELOG.md +1 -0
- package/dist/templates/plugin-standalone/README.ja.md +43 -0
- package/dist/templates/plugin-standalone/README.md +43 -0
- package/dist/templates/plugin-standalone/package.json +47 -0
- package/dist/templates/plugin-standalone/src/index.test.ts +16 -0
- package/dist/templates/plugin-standalone/src/index.ts +29 -0
- package/dist/templates/plugin-standalone/tsconfig.json +16 -0
- package/dist/templates/plugin-standalone/tsup.config.ts +8 -0
- 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/components/i18n-provider.tsx +0 -15
- package/dist/templates/_shared/lib/admin-site-client.ts +0 -10
- package/dist/templates/_shared/lib/admin-site.ts +0 -12
- package/dist/templates/_shared/lib/amplify-server.ts +0 -7
- package/dist/templates/_shared/lib/auth-server.ts +0 -15
- package/dist/templates/_shared/lib/cn.ts +0 -5
- package/dist/templates/_shared/lib/i18n.ts +0 -35
- package/dist/templates/_shared/lib/kv-provider.ts +0 -7
- package/dist/templates/_shared/lib/media.ts +0 -6
- package/dist/templates/_shared/lib/posts-provider.ts +0 -7
- package/dist/templates/_shared/lib/posts-public.ts +0 -27
- package/dist/templates/_shared/lib/posts.ts +0 -12
- package/dist/templates/_shared/lib/seo.ts +0 -11
- package/dist/templates/_shared/lib/site-settings.ts +0 -11
- package/dist/templates/_shared/lib/storage.ts +0 -10
- package/dist/templates/_shared/lib/theme-actions.ts +0 -5
- package/dist/templates/_shared/lib/theme-active.ts +0 -10
- package/dist/templates/_shared/lib/theme-config.ts +0 -12
- package/dist/templates/_shared/lib/upload.ts +0 -6
- /package/dist/templates/_shared/app/{site/[siteId]/[slug] → [slug]}/page.tsx +0 -0
- /package/dist/templates/_shared/app/{site/[siteId]/feed.xml → feed.xml}/route.ts +0 -0
- /package/dist/templates/_shared/app/{site/[siteId]/og → og}/[slug]/route.ts +0 -0
- /package/dist/templates/_shared/app/{site/[siteId]/page.tsx → page.tsx} +0 -0
- /package/dist/templates/_shared/app/{site/[siteId]/sitemap.xml → sitemap.xml}/route.ts +0 -0
- /package/dist/templates/_shared/app/{site/[siteId]/tag → tag}/[tag]/page.tsx +0 -0
|
@@ -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,14 @@
|
|
|
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'
|
|
11
|
+
import { pluginSecretHandler } from '../functions/plugin-secret-handler/resource.js'
|
|
6
12
|
import { customSchemaModels } from './resource.custom.js'
|
|
7
13
|
|
|
8
14
|
// AppSync's `a.handler.custom({ entry })` paths are resolved by CDK
|
|
@@ -15,8 +21,9 @@ import { customSchemaModels } from './resource.custom.js'
|
|
|
15
21
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
16
22
|
|
|
17
23
|
// Ampless's built-in models (Post / Page / Media / Taxonomy / PostTag /
|
|
18
|
-
// KvStore) plus the
|
|
19
|
-
// (listPublishedPosts / getPublishedPost / listPostsByTag
|
|
24
|
+
// KvStore) plus the public-read custom queries
|
|
25
|
+
// (listPublishedPosts / getPublishedPost / listPostsByTag /
|
|
26
|
+
// getMediaBySrc).
|
|
20
27
|
//
|
|
21
28
|
// Add project-specific models in `amplify/data/resource.custom.ts` —
|
|
22
29
|
// that file is never overwritten by `create-ampless upgrade`.
|
|
@@ -24,12 +31,26 @@ const resolverPaths = {
|
|
|
24
31
|
listPublishedPosts: resolve(__dirname, 'list-published-posts.js'),
|
|
25
32
|
getPublishedPost: resolve(__dirname, 'get-published-post.js'),
|
|
26
33
|
listPostsByTag: resolve(__dirname, 'list-posts-by-tag.js'),
|
|
34
|
+
getMediaBySrc: resolve(__dirname, 'get-media-by-src.js'),
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
const schema = a
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
const schema = a
|
|
38
|
+
.schema({
|
|
39
|
+
...amplessSchemaModels(a, {
|
|
40
|
+
resolverPaths,
|
|
41
|
+
userAdminFunction: userAdmin,
|
|
42
|
+
pluginSecretHandlerFunction: pluginSecretHandler,
|
|
43
|
+
}),
|
|
44
|
+
...customSchemaModels(a),
|
|
45
|
+
})
|
|
46
|
+
// Schema-level `.authorization()` is the only place resource auth is
|
|
47
|
+
// honoured in `@aws-amplify/data-schema` — model-level callbacks have
|
|
48
|
+
// `resource` destructured out of `allow`. The helper returns an empty
|
|
49
|
+
// array when no Lambda refs are supplied, so the schema stays
|
|
50
|
+
// unaffected if MCP is later removed.
|
|
51
|
+
.authorization((allow) => amplessSchemaAuthorization(allow, {
|
|
52
|
+
mcpHandlerFunction: mcpHandler,
|
|
53
|
+
}))
|
|
33
54
|
|
|
34
55
|
export type Schema = ClientSchema<typeof schema>
|
|
35
56
|
export const data = defineData({
|
|
@@ -3,6 +3,7 @@ import { defineFunction } from '@aws-amplify/backend'
|
|
|
3
3
|
export const eventDispatcher = defineFunction({
|
|
4
4
|
name: 'event-dispatcher',
|
|
5
5
|
entry: './handler.ts',
|
|
6
|
+
runtime: 22,
|
|
6
7
|
// Co-locate with the data stack — the function reads the Post table's
|
|
7
8
|
// DynamoDB Stream, so being in the same stack avoids a CloudFormation
|
|
8
9
|
// circular dependency between data, function, and auth.
|
|
@@ -3,6 +3,7 @@ import { defineFunction } from '@aws-amplify/backend'
|
|
|
3
3
|
export const processorTrusted = defineFunction({
|
|
4
4
|
name: 'processor-trusted',
|
|
5
5
|
entry: './handler.ts',
|
|
6
|
+
runtime: 22,
|
|
6
7
|
// Co-locate with data — this Lambda reads the Post table to assemble
|
|
7
8
|
// sitemap/RSS, so we keep the dependency intra-stack and avoid a
|
|
8
9
|
// function → data → auth → function cycle.
|
|
@@ -3,7 +3,12 @@ import { defineFunction } from '@aws-amplify/backend'
|
|
|
3
3
|
export const processorUntrusted = defineFunction({
|
|
4
4
|
name: 'processor-untrusted',
|
|
5
5
|
entry: './handler.ts',
|
|
6
|
+
runtime: 22,
|
|
6
7
|
// Untrusted plugins do pure JS work; modest memory.
|
|
7
8
|
memoryMB: 256,
|
|
8
9
|
timeoutSeconds: 30,
|
|
10
|
+
// Pin to the data stack so all event-system Lambdas live together
|
|
11
|
+
// (dispatcher / processor-trusted are also `data`) and the function
|
|
12
|
+
// stack doesn't bridge data ↔ storage ↔ auth in a CFN cycle.
|
|
13
|
+
resourceGroupName: 'data',
|
|
9
14
|
})
|
|
@@ -3,6 +3,7 @@ import { defineFunction } from '@aws-amplify/backend'
|
|
|
3
3
|
export const apiKeyRenewer = defineFunction({
|
|
4
4
|
name: 'api-key-renewer',
|
|
5
5
|
entry: './handler.ts',
|
|
6
|
+
runtime: 22,
|
|
6
7
|
// Co-locate with the data stack — the function reads/updates the
|
|
7
8
|
// AppSync API's API key, so being in the same stack avoids a CFN
|
|
8
9
|
// dependency cycle between data, function, and auth.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { handler } from '@ampless/backend/functions/mcp-handler'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineFunction } from '@aws-amplify/backend'
|
|
2
|
+
|
|
3
|
+
export const mcpHandler = defineFunction({
|
|
4
|
+
name: 'mcp-handler',
|
|
5
|
+
entry: './handler.ts',
|
|
6
|
+
runtime: 22,
|
|
7
|
+
// Co-locate with the data stack so the function has the KvStore
|
|
8
|
+
// table ARN available without cross-stack references at synth time.
|
|
9
|
+
resourceGroupName: 'data',
|
|
10
|
+
memoryMB: 512,
|
|
11
|
+
timeoutSeconds: 30,
|
|
12
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
//
|
|
5
|
+
// The handler validates Cognito group membership (admin/editor),
|
|
6
|
+
// enforces a hard 10,000-char server cap on the value (field-level
|
|
7
|
+
// `pattern` / `maxLength` come from the admin client and are NOT
|
|
8
|
+
// re-enforced here), encrypts with the AES-256-GCM key from
|
|
9
|
+
// `process.env.PLUGIN_SECRET_ENCRYPTION_KEY`, and dual-writes to
|
|
10
|
+
// PluginSecret + PluginSecretIndicator.
|
|
11
|
+
export { handler } from '@ampless/backend/functions/plugin-secret-handler'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineFunction } from '@aws-amplify/backend'
|
|
2
|
+
|
|
3
|
+
// `resourceGroupName: 'data'` co-locates this Lambda with the AppSync
|
|
4
|
+
// stack. Required because `setPluginSecret` / `clearPluginSecret` are
|
|
5
|
+
// wired as custom GraphQL resolvers (data → function), matching the
|
|
6
|
+
// same circular-dependency avoidance used for user-admin.
|
|
7
|
+
export const pluginSecretHandler = defineFunction({
|
|
8
|
+
name: 'plugin-secret-handler',
|
|
9
|
+
entry: './handler.ts',
|
|
10
|
+
runtime: 22,
|
|
11
|
+
resourceGroupName: 'data',
|
|
12
|
+
})
|
|
@@ -1,6 +1,13 @@
|
|
|
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
|
+
runtime: 22,
|
|
12
|
+
resourceGroupName: 'data',
|
|
6
13
|
})
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Generated by `npx create-ampless setup-encryption-key`.
|
|
2
|
+
// AES-256-GCM key (32 bytes, base64) for plugin secret storage.
|
|
3
|
+
// Rotate via the same CLI command. Existing ciphertext becomes unreadable on rotation.
|
|
4
|
+
//
|
|
5
|
+
// IMPORTANT: This is a placeholder. Replace the value below with a real key
|
|
6
|
+
// by running: npx create-ampless setup-encryption-key
|
|
7
|
+
// (from your project root, after scaffolding).
|
|
8
|
+
|
|
9
|
+
export const PLUGIN_SECRET_ENCRYPTION_KEY = ''
|
|
@@ -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,37 +1,67 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
import { headers } from 'next/headers'
|
|
3
|
-
import {
|
|
3
|
+
import { renderThemeCss, validateColorScheme } from '@ampless/runtime'
|
|
4
|
+
import { getDictionary } from '@ampless/admin'
|
|
5
|
+
import { I18nProvider } from '@ampless/admin/components'
|
|
6
|
+
import { ampless } from '@/lib/ampless'
|
|
7
|
+
import { admin } from '@/lib/admin'
|
|
4
8
|
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
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 ampless.siteMetadata()
|
|
19
13
|
}
|
|
20
14
|
|
|
21
15
|
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
22
16
|
const h = await headers()
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
// Resolve theme, plugin head + body in parallel. `publicHead` and
|
|
18
|
+
// `publicBodyEnd` both fetch the S3 site-settings cache to bind
|
|
19
|
+
// `ctx.setting()`; Next.js fetch dedupe collapses that into a
|
|
20
|
+
// single round trip when the two run on the same request.
|
|
21
|
+
const [theme, pluginHead, pluginBodyEnd] = await Promise.all([
|
|
22
|
+
ampless.loadThemeConfig(),
|
|
23
|
+
ampless.publicHead(),
|
|
24
|
+
ampless.publicBodyEnd(),
|
|
25
|
+
])
|
|
25
26
|
const themeCss = renderThemeCss(theme.cssVars)
|
|
26
|
-
const locale =
|
|
27
|
+
const locale = admin.locale
|
|
27
28
|
const dict = getDictionary(locale)
|
|
29
|
+
// `data-color-scheme` pins the visitor to light or dark regardless
|
|
30
|
+
// of their system `prefers-color-scheme`. `'auto'` (the default)
|
|
31
|
+
// means we don't emit the attribute at all, so `globals.css`'s
|
|
32
|
+
// `:root { color-scheme: light dark }` lets the browser follow the
|
|
33
|
+
// system setting. Themes use `light-dark()` in their tokens.css so a
|
|
34
|
+
// single declaration covers both modes; the active `color-scheme`
|
|
35
|
+
// selects which value is rendered.
|
|
36
|
+
//
|
|
37
|
+
// The admin's theme-settings iframe preview passes the unsaved
|
|
38
|
+
// selection via `?previewColorScheme=<mode>`, which middleware
|
|
39
|
+
// forwards as `x-preview-color-scheme`. When that header is present
|
|
40
|
+
// it overrides the stored site setting so the preview updates live.
|
|
41
|
+
const previewHeader = h.get('x-preview-color-scheme')
|
|
42
|
+
const effectiveColorScheme = previewHeader
|
|
43
|
+
? validateColorScheme(previewHeader)
|
|
44
|
+
: theme.colorScheme
|
|
45
|
+
const htmlProps: { lang: string; 'data-color-scheme'?: 'light' | 'dark' } = { lang: locale }
|
|
46
|
+
if (effectiveColorScheme !== 'auto') {
|
|
47
|
+
htmlProps['data-color-scheme'] = effectiveColorScheme
|
|
48
|
+
}
|
|
28
49
|
return (
|
|
29
|
-
<html
|
|
50
|
+
<html {...htmlProps}>
|
|
30
51
|
<head>
|
|
31
52
|
{/* Inline `:root` overrides come AFTER globals.css so they win
|
|
32
53
|
against the static defaults. Validated values only — see
|
|
33
54
|
ampless `validateThemeValue`. */}
|
|
34
55
|
{themeCss && <style dangerouslySetInnerHTML={{ __html: themeCss }} />}
|
|
56
|
+
{/* Descriptor-based plugin head injection (Phase 1+2). Each
|
|
57
|
+
active plugin's `publicHead()` runs validation here; the
|
|
58
|
+
output is a `<Fragment>` of `<script>` / `<meta>` / `<link>`
|
|
59
|
+
/ `<noscript>` elements. Placed after the theme style so
|
|
60
|
+
plugin-emitted overrides win on the rare collision.
|
|
61
|
+
Awaited via Promise.all above so admin-managed
|
|
62
|
+
`settings.public` values flow into ctx.setting() before
|
|
63
|
+
render. */}
|
|
64
|
+
{pluginHead}
|
|
35
65
|
</head>
|
|
36
66
|
{/* `data-theme` selects which theme's `tokens.css` block matches.
|
|
37
67
|
The active theme is resolved from `theme.active` site setting,
|
|
@@ -42,6 +72,11 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|
|
42
72
|
{children}
|
|
43
73
|
</I18nProvider>
|
|
44
74
|
</Providers>
|
|
75
|
+
{/* Descriptor-based body-end injection (Phase 1+2). GTM
|
|
76
|
+
no-script iframe / chat widgets / analytics tail snippets
|
|
77
|
+
land here. Awaited above so the same ctx.setting()
|
|
78
|
+
snapshot powers both head + body. */}
|
|
79
|
+
{pluginBodyEnd}
|
|
45
80
|
</body>
|
|
46
81
|
</html>
|
|
47
82
|
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ampless } from '@/lib/ampless'
|
|
2
|
+
import { createRawRouteHandler } from '@ampless/runtime/routes'
|
|
3
|
+
|
|
4
|
+
// Internal handler for `format: 'html'` posts with
|
|
5
|
+
// `metadata.no_layout === true`. Reached via middleware rewrite from
|
|
6
|
+
// `/<slug>` — never directly. The browser URL stays `/<slug>`; the
|
|
7
|
+
// `/raw/` prefix is an implementation detail (folder names starting
|
|
8
|
+
// with `_` are excluded by the App Router, hence the explicit `raw/`).
|
|
9
|
+
export const dynamic = 'force-dynamic'
|
|
10
|
+
export const GET = createRawRouteHandler(ampless)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ampless } from '@/lib/ampless'
|
|
2
|
+
import { createStaticRouteHandler } from '@ampless/runtime/routes'
|
|
3
|
+
|
|
4
|
+
// Internal handler for `format: 'static'` posts — the body is a
|
|
5
|
+
// manifest pointing at a bundle of files in S3 at
|
|
6
|
+
// `public/static/<slug>/`. Reached via middleware rewrite from
|
|
7
|
+
// `/<slug>(/<path>)` — never directly. Each request becomes a short-
|
|
8
|
+
// lived S3 presigned redirect (302) for the entrypoint or the
|
|
9
|
+
// requested bundle file. The browser URL stays `/<slug>(/<path>)`;
|
|
10
|
+
// the `/static/` prefix is an implementation detail (folder names
|
|
11
|
+
// starting with `_` are excluded by the App Router, hence the
|
|
12
|
+
// explicit `static/`).
|
|
13
|
+
export const dynamic = 'force-dynamic'
|
|
14
|
+
export const GET = createStaticRouteHandler(ampless)
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { defineConfig } from 'ampless'
|
|
2
2
|
import seoPlugin from '@ampless/plugin-seo'
|
|
3
3
|
import rssPlugin from '@ampless/plugin-rss'
|
|
4
|
+
// import schemaJsonLdPlugin from '@ampless/plugin-schema-jsonld'
|
|
5
|
+
// import readingTimePlugin from '@ampless/plugin-reading-time'
|
|
6
|
+
// import cookieConsentPlugin from '@ampless/plugin-cookie-consent'
|
|
7
|
+
// import analyticsGa4Plugin from '@ampless/plugin-analytics-ga4'
|
|
8
|
+
// import gtmPlugin from '@ampless/plugin-gtm'
|
|
9
|
+
// import plausiblePlugin from '@ampless/plugin-plausible'
|
|
4
10
|
// import webhookPlugin from '@ampless/plugin-webhook'
|
|
5
11
|
// import ogImagePlugin, { loadFontFromUrl } from '@ampless/plugin-og-image'
|
|
6
12
|
|
|
@@ -27,29 +33,7 @@ export default defineConfig({
|
|
|
27
33
|
losslessForPng: true,
|
|
28
34
|
},
|
|
29
35
|
},
|
|
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
|
-
|
|
36
|
+
// Date display format.
|
|
53
37
|
// 'iso' — YYYY-MM-DD (default; SSR-safe, locale-neutral)
|
|
54
38
|
// 'long' — "April 27, 2026" (en-US)
|
|
55
39
|
// 'locale' — browser/server locale
|
|
@@ -90,6 +74,63 @@ export default defineConfig({
|
|
|
90
74
|
// ],
|
|
91
75
|
// }),
|
|
92
76
|
//
|
|
77
|
+
// Cookie consent banner. Installs `window.amplessConsent` so other
|
|
78
|
+
// plugins can gate themselves on user consent (see the Consent
|
|
79
|
+
// Convention in `docs/architecture/08-plugin-architecture.md`).
|
|
80
|
+
//
|
|
81
|
+
// IMPORTANT: cookieConsentPlugin() must come BEFORE any analytics
|
|
82
|
+
// plugin in this array — both run `afterInteractive` and the consent
|
|
83
|
+
// API must be installed before analytics scripts read it. (Analytics
|
|
84
|
+
// plugins also wait for `ampless:consent-ready`, so out-of-order
|
|
85
|
+
// registration still works, but in-order is faster.)
|
|
86
|
+
//
|
|
87
|
+
// Categories (analytics / marketing / etc.) are managed live from
|
|
88
|
+
// `/admin/plugins`. Banner text and button labels are also admin-editable.
|
|
89
|
+
//
|
|
90
|
+
// cookieConsentPlugin(),
|
|
91
|
+
//
|
|
92
|
+
// Reading-time badge. Estimates read time from the post body and injects
|
|
93
|
+
// a configurable label (default: "3 min read") before or after the post
|
|
94
|
+
// content. Supports English word count + CJK character count (÷ 2).
|
|
95
|
+
// Label template and position are editable from `/admin/plugins`.
|
|
96
|
+
//
|
|
97
|
+
// readingTimePlugin(),
|
|
98
|
+
//
|
|
99
|
+
// Google Analytics 4. Once registered here, the measurement ID can be
|
|
100
|
+
// edited from `/admin/plugins` without a redeploy — the constructor
|
|
101
|
+
// value below is just the initial default. Pass an empty string to
|
|
102
|
+
// disable the GA tag entirely. See packages/ampless/docs/plugin-author-guide.md.
|
|
103
|
+
//
|
|
104
|
+
// analyticsGa4Plugin({
|
|
105
|
+
// measurementId: '', // 'G-XXXXXXXX' to enable
|
|
106
|
+
// }),
|
|
107
|
+
//
|
|
108
|
+
// Google Tag Manager. Like GA4, the container ID is editable from
|
|
109
|
+
// `/admin/plugins` after deploy. Tags fire via GTM's own trigger model.
|
|
110
|
+
//
|
|
111
|
+
// gtmPlugin({
|
|
112
|
+
// containerId: '', // 'GTM-XXXXXXX' to enable
|
|
113
|
+
// }),
|
|
114
|
+
//
|
|
115
|
+
// Plausible Analytics (privacy-focused, typically no cookie consent
|
|
116
|
+
// required). `domain` matches the registered Plausible site.
|
|
117
|
+
// `scriptUrl` defaults to plausible.io; override for self-hosted.
|
|
118
|
+
//
|
|
119
|
+
// plausiblePlugin({
|
|
120
|
+
// domain: '', // 'example.com' to enable
|
|
121
|
+
// }),
|
|
122
|
+
//
|
|
123
|
+
// JSON-LD structured data (Schema.org). Injects <script type="application/ld+json">
|
|
124
|
+
// into post pages for Article / BlogPosting markup. articleType, authorName and
|
|
125
|
+
// publisherName fall back to the site name when left empty.
|
|
126
|
+
//
|
|
127
|
+
// schemaJsonLdPlugin({
|
|
128
|
+
// articleType: 'Article', // 'Article' | 'BlogPosting' | 'NewsArticle'
|
|
129
|
+
// authorName: '', // empty falls back to site.name
|
|
130
|
+
// publisherName: '', // empty falls back to site.name
|
|
131
|
+
// publisherLogo: '', // optional absolute URL to publisher logo
|
|
132
|
+
// }),
|
|
133
|
+
//
|
|
93
134
|
// Per-post OG images: SNS crawlers hit `/og/<slug>` and we render
|
|
94
135
|
// a JSX card → PNG via Next.js `ImageResponse`. Requires at least one
|
|
95
136
|
// font — ship a .ttf from your CDN or `/public` directory.
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import Link from 'next/link'
|
|
2
2
|
import { parseLinkList, isTagListUrl } from 'ampless'
|
|
3
|
-
import {
|
|
3
|
+
import { ampless } from '@/lib/ampless'
|
|
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 ampless.listPostsByTag(tagRef.tag, { limit: 50 })
|
|
32
31
|
return {
|
|
33
32
|
type: 'tagSection' as const,
|
|
34
33
|
label: item.label,
|