create-ampless 0.2.0-alpha.8 → 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.
Files changed (140) hide show
  1. package/README.ja.md +77 -0
  2. package/README.md +6 -3
  3. package/dist/index.js +1282 -93
  4. package/dist/templates/_shared/AGENTS.ja.md +94 -0
  5. package/dist/templates/_shared/AGENTS.md +94 -0
  6. package/dist/templates/_shared/README.ja.md +239 -0
  7. package/dist/templates/_shared/README.md +239 -0
  8. package/dist/templates/_shared/RUNBOOK.ja.md +120 -0
  9. package/dist/templates/_shared/RUNBOOK.md +32 -58
  10. package/dist/templates/_shared/THEMES.ja.md +495 -0
  11. package/dist/templates/_shared/THEMES.md +523 -0
  12. package/dist/templates/_shared/amplify/auth/post-confirmation/resource.ts +7 -0
  13. package/dist/templates/_shared/amplify/backend.custom.ts +41 -0
  14. package/dist/templates/_shared/amplify/backend.ts +20 -2
  15. package/dist/templates/_shared/amplify/data/get-media-by-src.js +45 -0
  16. package/dist/templates/_shared/amplify/data/get-published-post.js +9 -12
  17. package/dist/templates/_shared/amplify/data/list-posts-by-tag.js +6 -9
  18. package/dist/templates/_shared/amplify/data/list-published-posts.js +10 -11
  19. package/dist/templates/_shared/amplify/data/resource.custom.ts +32 -0
  20. package/dist/templates/_shared/amplify/data/resource.ts +32 -15
  21. package/dist/templates/_shared/amplify/events/dispatcher/resource.ts +1 -0
  22. package/dist/templates/_shared/amplify/events/processor-trusted/resource.ts +1 -0
  23. package/dist/templates/_shared/amplify/events/processor-untrusted/resource.ts +5 -0
  24. package/dist/templates/_shared/amplify/functions/api-key-renewer/resource.ts +1 -0
  25. package/dist/templates/_shared/amplify/functions/mcp-handler/handler.ts +1 -0
  26. package/dist/templates/_shared/amplify/functions/mcp-handler/resource.ts +12 -0
  27. package/dist/templates/_shared/amplify/functions/plugin-secret-handler/handler.ts +11 -0
  28. package/dist/templates/_shared/amplify/functions/plugin-secret-handler/resource.ts +12 -0
  29. package/dist/templates/_shared/amplify/functions/user-admin/handler.ts +4 -0
  30. package/dist/templates/_shared/amplify/functions/user-admin/resource.ts +13 -0
  31. package/dist/templates/_shared/amplify/secrets/.gitkeep +0 -0
  32. package/dist/templates/_shared/amplify/secrets/encryption-key.ts +9 -0
  33. package/dist/templates/_shared/app/(admin)/admin/mcp-tokens/page.tsx +5 -0
  34. package/dist/templates/_shared/app/(admin)/admin/plugins/page.tsx +5 -0
  35. package/dist/templates/_shared/app/(admin)/admin/users/page.tsx +5 -0
  36. package/dist/templates/_shared/app/globals.css +55 -39
  37. package/dist/templates/_shared/app/layout.tsx +51 -16
  38. package/dist/templates/_shared/app/providers.tsx +0 -2
  39. package/dist/templates/_shared/app/raw/[slug]/route.ts +10 -0
  40. package/dist/templates/_shared/app/static/[slug]/[[...path]]/route.ts +14 -0
  41. package/dist/templates/_shared/cms.config.ts +64 -23
  42. package/dist/templates/_shared/components/site-chrome/site-sidebar.tsx +3 -4
  43. package/dist/templates/_shared/components/tag-list.tsx +9 -2
  44. package/dist/templates/_shared/components.json +1 -1
  45. package/dist/templates/_shared/docs/plugin-author-guide.ja.md +934 -0
  46. package/dist/templates/_shared/docs/plugin-author-guide.md +1336 -0
  47. package/dist/templates/_shared/lib/admin.ts +12 -12
  48. package/dist/templates/_shared/lib/ampless.ts +2 -8
  49. package/dist/templates/_shared/lib/amplify.ts +0 -5
  50. package/dist/templates/_shared/package.json +51 -40
  51. package/dist/templates/_shared/plugins/README.ja.md +139 -0
  52. package/dist/templates/_shared/plugins/README.md +143 -0
  53. package/dist/templates/_shared/proxy.ts +23 -8
  54. package/dist/templates/blog/README.ja.md +22 -0
  55. package/dist/templates/blog/README.md +17 -47
  56. package/dist/templates/blog/manifest.ts +1 -1
  57. package/dist/templates/blog/pages/feed.ts +4 -5
  58. package/dist/templates/blog/pages/home.tsx +10 -15
  59. package/dist/templates/blog/pages/post.tsx +42 -14
  60. package/dist/templates/blog/pages/sitemap.ts +4 -5
  61. package/dist/templates/blog/pages/tag.tsx +8 -10
  62. package/dist/templates/blog/tokens.css +26 -40
  63. package/dist/templates/corporate/README.ja.md +18 -0
  64. package/dist/templates/corporate/README.md +12 -14
  65. package/dist/templates/corporate/pages/feed.ts +3 -4
  66. package/dist/templates/corporate/pages/home.tsx +7 -12
  67. package/dist/templates/corporate/pages/post.tsx +20 -14
  68. package/dist/templates/corporate/pages/sitemap.ts +3 -4
  69. package/dist/templates/corporate/pages/tag.tsx +8 -10
  70. package/dist/templates/corporate/tokens.css +17 -39
  71. package/dist/templates/dads/README.ja.md +31 -0
  72. package/dist/templates/dads/README.md +13 -17
  73. package/dist/templates/dads/pages/feed.ts +3 -4
  74. package/dist/templates/dads/pages/home.tsx +7 -12
  75. package/dist/templates/dads/pages/post.tsx +20 -14
  76. package/dist/templates/dads/pages/sitemap.ts +3 -4
  77. package/dist/templates/dads/pages/tag.tsx +8 -10
  78. package/dist/templates/dads/tokens.css +22 -42
  79. package/dist/templates/docs/README.ja.md +24 -0
  80. package/dist/templates/docs/README.md +10 -13
  81. package/dist/templates/docs/pages/feed.ts +3 -4
  82. package/dist/templates/docs/pages/home.tsx +6 -9
  83. package/dist/templates/docs/pages/post.tsx +19 -13
  84. package/dist/templates/docs/pages/sitemap.ts +3 -4
  85. package/dist/templates/docs/pages/tag.tsx +8 -10
  86. package/dist/templates/docs/tokens.css +17 -39
  87. package/dist/templates/landing/README.ja.md +20 -0
  88. package/dist/templates/landing/README.md +14 -19
  89. package/dist/templates/landing/pages/feed.ts +4 -5
  90. package/dist/templates/landing/pages/home.tsx +7 -12
  91. package/dist/templates/landing/pages/post.tsx +20 -14
  92. package/dist/templates/landing/pages/sitemap.ts +3 -4
  93. package/dist/templates/landing/pages/tag.tsx +8 -10
  94. package/dist/templates/landing/tokens.css +17 -39
  95. package/dist/templates/minimal/README.ja.md +14 -0
  96. package/dist/templates/minimal/README.md +9 -47
  97. package/dist/templates/minimal/pages/feed.ts +4 -5
  98. package/dist/templates/minimal/pages/home.tsx +6 -8
  99. package/dist/templates/minimal/pages/post.tsx +19 -12
  100. package/dist/templates/minimal/pages/sitemap.ts +4 -5
  101. package/dist/templates/minimal/pages/tag.tsx +7 -8
  102. package/dist/templates/minimal/tokens.css +17 -39
  103. package/dist/templates/plugin-local/README.md +34 -0
  104. package/dist/templates/plugin-local/index.ts +39 -0
  105. package/dist/templates/plugin-standalone/CHANGELOG.md +1 -0
  106. package/dist/templates/plugin-standalone/README.ja.md +43 -0
  107. package/dist/templates/plugin-standalone/README.md +43 -0
  108. package/dist/templates/plugin-standalone/package.json +47 -0
  109. package/dist/templates/plugin-standalone/src/index.test.ts +16 -0
  110. package/dist/templates/plugin-standalone/src/index.ts +29 -0
  111. package/dist/templates/plugin-standalone/tsconfig.json +16 -0
  112. package/dist/templates/plugin-standalone/tsup.config.ts +8 -0
  113. package/package.json +1 -1
  114. package/dist/templates/_shared/app/(admin)/admin/sites/page.tsx +0 -5
  115. package/dist/templates/_shared/app/site/[siteId]/raw/[slug]/route.ts +0 -5
  116. package/dist/templates/_shared/components/i18n-provider.tsx +0 -15
  117. package/dist/templates/_shared/lib/admin-site-client.ts +0 -10
  118. package/dist/templates/_shared/lib/admin-site.ts +0 -12
  119. package/dist/templates/_shared/lib/amplify-server.ts +0 -7
  120. package/dist/templates/_shared/lib/auth-server.ts +0 -15
  121. package/dist/templates/_shared/lib/cn.ts +0 -5
  122. package/dist/templates/_shared/lib/i18n.ts +0 -35
  123. package/dist/templates/_shared/lib/kv-provider.ts +0 -7
  124. package/dist/templates/_shared/lib/media.ts +0 -6
  125. package/dist/templates/_shared/lib/posts-provider.ts +0 -7
  126. package/dist/templates/_shared/lib/posts-public.ts +0 -27
  127. package/dist/templates/_shared/lib/posts.ts +0 -12
  128. package/dist/templates/_shared/lib/seo.ts +0 -11
  129. package/dist/templates/_shared/lib/site-settings.ts +0 -11
  130. package/dist/templates/_shared/lib/storage.ts +0 -10
  131. package/dist/templates/_shared/lib/theme-actions.ts +0 -5
  132. package/dist/templates/_shared/lib/theme-active.ts +0 -10
  133. package/dist/templates/_shared/lib/theme-config.ts +0 -12
  134. package/dist/templates/_shared/lib/upload.ts +0 -6
  135. /package/dist/templates/_shared/app/{site/[siteId]/[slug] → [slug]}/page.tsx +0 -0
  136. /package/dist/templates/_shared/app/{site/[siteId]/feed.xml → feed.xml}/route.ts +0 -0
  137. /package/dist/templates/_shared/app/{site/[siteId]/og → og}/[slug]/route.ts +0 -0
  138. /package/dist/templates/_shared/app/{site/[siteId]/page.tsx → page.tsx} +0 -0
  139. /package/dist/templates/_shared/app/{site/[siteId]/sitemap.xml → sitemap.xml}/route.ts +0 -0
  140. /package/dist/templates/_shared/app/{site/[siteId]/tag → tag}/[tag]/page.tsx +0 -0
@@ -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 = `${siteId}#${tag}`
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/site partition condition. Drafts never appear here because the
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 { siteId = 'default', tag, limit, nextToken } = ctx.args
13
+ const { tag, limit, nextToken } = ctx.args
14
14
  return {
15
15
  operation: 'Query',
16
16
  query: {
17
- expression: '#siteIdTag = :siteIdTag',
18
- expressionNames: { '#siteIdTag': 'siteIdTag' },
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 a site's published posts, newest first.
3
+ // AppSync JS resolver: list published posts, newest first.
4
4
  //
5
- // Reads the `bySiteIdStatus` GSI:
6
- // PK = `${siteId}#${status}` (denormalized field set by writers)
5
+ // Reads the `byStatus` GSI:
6
+ // PK = status (denormalized field set by writers)
7
7
  // SK = publishedAt
8
- // so a single Query reads only one site's published partition. Drafts
9
- // never appear because the PK condition pins status='published'.
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 { siteId = 'default', from, to, limit, nextToken } = ctx.args
15
+ const { from, to, limit, nextToken } = ctx.args
16
16
 
17
- const partition = `${siteId}#published`
18
- let keyExpression = '#siteIdStatus = :siteIdStatus'
19
- const expressionNames = { '#siteIdStatus': 'siteIdStatus' }
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: 'bySiteIdStatus',
38
+ index: 'byStatus',
40
39
  query: {
41
40
  expression: keyExpression,
42
41
  expressionNames,
@@ -0,0 +1,32 @@
1
+ // Custom data schema models for this project.
2
+ //
3
+ // `amplify/data/resource.ts` calls `customSchemaModels(a)` and spreads
4
+ // the result alongside ampless's built-in models in the same
5
+ // `a.schema({...})` call, which is the only way Amplify Gen 2's
6
+ // `ClientSchema<typeof schema>` inference picks up extra models.
7
+ //
8
+ // This file is NEVER overwritten by `create-ampless upgrade` —
9
+ // `amplify/data/resource.ts` is, so keep your custom models here.
10
+ //
11
+ // Example:
12
+ //
13
+ // export function customSchemaModels(a: any) {
14
+ // return {
15
+ // Bookmark: a
16
+ // .model({
17
+ // bookmarkId: a.id().required(),
18
+ // url: a.string().required(),
19
+ // title: a.string(),
20
+ // })
21
+ // .identifier(['bookmarkId'])
22
+ // .authorization((allow: any) => [allow.groups(['ampless-admin', 'ampless-editor'])]),
23
+ // }
24
+ // }
25
+ //
26
+ // `a` is the Amplify Gen 2 schema builder; see Amplify Data docs for
27
+ // the full DSL. Return an empty object if no customizations are needed.
28
+
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ export function customSchemaModels(_a: any): Record<string, unknown> {
31
+ return {}
32
+ }
@@ -1,7 +1,15 @@
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 { amplessSchemaModels, defaultAuthorizationModes } from '@ampless/backend'
4
+ import {
5
+ amplessSchemaModels,
6
+ amplessSchemaAuthorization,
7
+ defaultAuthorizationModes,
8
+ } from '@ampless/backend'
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'
12
+ import { customSchemaModels } from './resource.custom.js'
5
13
 
6
14
  // AppSync's `a.handler.custom({ entry })` paths are resolved by CDK
7
15
  // relative to the file that called `a.handler.custom`. When the call
@@ -13,27 +21,36 @@ import { amplessSchemaModels, defaultAuthorizationModes } from '@ampless/backend
13
21
  const __dirname = dirname(fileURLToPath(import.meta.url))
14
22
 
15
23
  // Ampless's built-in models (Post / Page / Media / Taxonomy / PostTag /
16
- // KvStore) plus the three public-read custom queries
17
- // (listPublishedPosts / getPublishedPost / listPostsByTag).
24
+ // KvStore) plus the public-read custom queries
25
+ // (listPublishedPosts / getPublishedPost / listPostsByTag /
26
+ // getMediaBySrc).
18
27
  //
19
- // Add project-specific models alongside the spread:
20
- //
21
- // const schema = a.schema({
22
- // ...amplessSchemaModels(a, { resolverPaths }),
23
- // MyCustomModel: a
24
- // .model({ siteId: a.string().required(), foo: a.string() })
25
- // .identifier(['siteId', 'foo'])
26
- // .authorization((allow) => [allow.groups(['ampless-admin'])]),
27
- // })
28
+ // Add project-specific models in `amplify/data/resource.custom.ts`
29
+ // that file is never overwritten by `create-ampless upgrade`.
28
30
  const resolverPaths = {
29
31
  listPublishedPosts: resolve(__dirname, 'list-published-posts.js'),
30
32
  getPublishedPost: resolve(__dirname, 'get-published-post.js'),
31
33
  listPostsByTag: resolve(__dirname, 'list-posts-by-tag.js'),
34
+ getMediaBySrc: resolve(__dirname, 'get-media-by-src.js'),
32
35
  }
33
36
 
34
- const schema = a.schema({
35
- ...amplessSchemaModels(a, { resolverPaths }),
36
- })
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
+ }))
37
54
 
38
55
  export type Schema = ClientSchema<typeof schema>
39
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
+ })
@@ -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/auth/user-admin'
@@ -0,0 +1,13 @@
1
+ import { defineFunction } from '@aws-amplify/backend'
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.
8
+ export const userAdmin = defineFunction({
9
+ name: 'user-admin',
10
+ entry: './handler.ts',
11
+ runtime: 22,
12
+ resourceGroupName: 'data',
13
+ })
@@ -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 = ''
@@ -0,0 +1,5 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createMcpTokensPage } from '@ampless/admin/pages'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+ export default createMcpTokensPage(admin)
@@ -0,0 +1,5 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createPluginsPage } from '@ampless/admin/pages'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+ export default createPluginsPage(admin)
@@ -0,0 +1,5 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createUsersListPage } from '@ampless/admin/pages'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+ export default createUsersListPage(admin)
@@ -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 { DEFAULT_SITE_ID } from 'ampless'
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
- const h = await headers()
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
- const siteId = h.get('x-site-id') ?? DEFAULT_SITE_ID
24
- const theme = await loadThemeConfig(siteId)
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 = getLocale()
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 lang={locale}>
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
  )
@@ -2,8 +2,6 @@
2
2
 
3
3
  import { useEffect } from 'react'
4
4
  import '@/lib/amplify'
5
- import '@/lib/posts-provider'
6
- import '@/lib/kv-provider'
7
5
 
8
6
  export function Providers({ children }: { children: React.ReactNode }) {
9
7
  useEffect(() => {
@@ -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)