create-ampless 0.2.0-alpha.9 → 1.0.0-alpha.41

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 (90) hide show
  1. package/README.ja.md +77 -0
  2. package/README.md +4 -1
  3. package/dist/index.js +99 -47
  4. package/dist/templates/_shared/AGENTS.ja.md +104 -0
  5. package/dist/templates/_shared/AGENTS.md +104 -0
  6. package/dist/templates/_shared/README.ja.md +176 -0
  7. package/dist/templates/_shared/README.md +176 -0
  8. package/dist/templates/_shared/RUNBOOK.ja.md +120 -0
  9. package/dist/templates/_shared/RUNBOOK.md +24 -51
  10. package/dist/templates/_shared/amplify/auth/post-confirmation/resource.ts +6 -0
  11. package/dist/templates/_shared/amplify/backend.ts +2 -0
  12. package/dist/templates/_shared/amplify/data/get-published-post.js +9 -12
  13. package/dist/templates/_shared/amplify/data/list-posts-by-tag.js +6 -9
  14. package/dist/templates/_shared/amplify/data/list-published-posts.js +10 -11
  15. package/dist/templates/_shared/amplify/data/resource.custom.ts +1 -2
  16. package/dist/templates/_shared/amplify/data/resource.ts +22 -5
  17. package/dist/templates/_shared/amplify/events/processor-untrusted/resource.ts +4 -0
  18. package/dist/templates/_shared/amplify/functions/mcp-handler/handler.ts +1 -0
  19. package/dist/templates/_shared/amplify/functions/mcp-handler/resource.ts +11 -0
  20. package/dist/templates/_shared/amplify/functions/user-admin/resource.ts +6 -0
  21. package/dist/templates/_shared/app/(admin)/admin/mcp-tokens/page.tsx +5 -0
  22. package/dist/templates/_shared/app/globals.css +55 -39
  23. package/dist/templates/_shared/app/layout.tsx +24 -11
  24. package/dist/templates/_shared/app/r/[slug]/[[...path]]/route.ts +18 -0
  25. package/dist/templates/_shared/cms.config.ts +1 -23
  26. package/dist/templates/_shared/components/site-chrome/site-sidebar.tsx +2 -3
  27. package/dist/templates/_shared/lib/admin.ts +9 -7
  28. package/dist/templates/_shared/lib/posts-public.ts +1 -1
  29. package/dist/templates/_shared/package.json +11 -12
  30. package/dist/templates/_shared/proxy.ts +22 -8
  31. package/dist/templates/blog/README.ja.md +22 -0
  32. package/dist/templates/blog/README.md +17 -47
  33. package/dist/templates/blog/manifest.ts +1 -1
  34. package/dist/templates/blog/pages/feed.ts +3 -4
  35. package/dist/templates/blog/pages/home.tsx +5 -8
  36. package/dist/templates/blog/pages/post.tsx +7 -7
  37. package/dist/templates/blog/pages/sitemap.ts +3 -4
  38. package/dist/templates/blog/pages/tag.tsx +4 -4
  39. package/dist/templates/blog/tokens.css +26 -40
  40. package/dist/templates/corporate/README.ja.md +18 -0
  41. package/dist/templates/corporate/README.md +12 -14
  42. package/dist/templates/corporate/pages/feed.ts +2 -3
  43. package/dist/templates/corporate/pages/home.tsx +5 -8
  44. package/dist/templates/corporate/pages/post.tsx +7 -7
  45. package/dist/templates/corporate/pages/sitemap.ts +2 -3
  46. package/dist/templates/corporate/pages/tag.tsx +4 -4
  47. package/dist/templates/corporate/tokens.css +17 -39
  48. package/dist/templates/dads/README.ja.md +31 -0
  49. package/dist/templates/dads/README.md +13 -17
  50. package/dist/templates/dads/pages/feed.ts +2 -3
  51. package/dist/templates/dads/pages/home.tsx +5 -8
  52. package/dist/templates/dads/pages/post.tsx +7 -7
  53. package/dist/templates/dads/pages/sitemap.ts +2 -3
  54. package/dist/templates/dads/pages/tag.tsx +4 -4
  55. package/dist/templates/dads/tokens.css +22 -42
  56. package/dist/templates/docs/README.ja.md +24 -0
  57. package/dist/templates/docs/README.md +10 -13
  58. package/dist/templates/docs/pages/feed.ts +2 -3
  59. package/dist/templates/docs/pages/home.tsx +5 -6
  60. package/dist/templates/docs/pages/post.tsx +8 -8
  61. package/dist/templates/docs/pages/sitemap.ts +2 -3
  62. package/dist/templates/docs/pages/tag.tsx +5 -5
  63. package/dist/templates/docs/tokens.css +17 -39
  64. package/dist/templates/landing/README.ja.md +20 -0
  65. package/dist/templates/landing/README.md +14 -19
  66. package/dist/templates/landing/pages/feed.ts +3 -4
  67. package/dist/templates/landing/pages/home.tsx +5 -8
  68. package/dist/templates/landing/pages/post.tsx +7 -7
  69. package/dist/templates/landing/pages/sitemap.ts +2 -3
  70. package/dist/templates/landing/pages/tag.tsx +4 -4
  71. package/dist/templates/landing/tokens.css +17 -39
  72. package/dist/templates/minimal/README.ja.md +14 -0
  73. package/dist/templates/minimal/README.md +9 -47
  74. package/dist/templates/minimal/pages/feed.ts +3 -4
  75. package/dist/templates/minimal/pages/home.tsx +3 -4
  76. package/dist/templates/minimal/pages/post.tsx +6 -6
  77. package/dist/templates/minimal/pages/sitemap.ts +3 -4
  78. package/dist/templates/minimal/pages/tag.tsx +3 -3
  79. package/dist/templates/minimal/tokens.css +17 -39
  80. package/package.json +1 -1
  81. package/dist/templates/_shared/app/(admin)/admin/sites/page.tsx +0 -5
  82. package/dist/templates/_shared/app/site/[siteId]/raw/[slug]/route.ts +0 -5
  83. package/dist/templates/_shared/lib/admin-site-client.ts +0 -10
  84. package/dist/templates/_shared/lib/admin-site.ts +0 -12
  85. /package/dist/templates/_shared/app/{site/[siteId]/[slug] → [slug]}/page.tsx +0 -0
  86. /package/dist/templates/_shared/app/{site/[siteId]/feed.xml → feed.xml}/route.ts +0 -0
  87. /package/dist/templates/_shared/app/{site/[siteId]/og → og}/[slug]/route.ts +0 -0
  88. /package/dist/templates/_shared/app/{site/[siteId]/page.tsx → page.tsx} +0 -0
  89. /package/dist/templates/_shared/app/{site/[siteId]/sitemap.xml → sitemap.xml}/route.ts +0 -0
  90. /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,
@@ -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(['siteId', 'bookmarkId'])
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 { amplessSchemaModels, defaultAuthorizationModes } from '@ampless/backend'
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.schema({
30
- ...amplessSchemaModels(a, { resolverPaths, userAdminFunction: userAdmin }),
31
- ...customSchemaModels(a),
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
  })
@@ -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)
@@ -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 { DEFAULT_SITE_ID } from 'ampless'
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
- const h = await headers()
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 siteId = h.get('x-site-id') ?? DEFAULT_SITE_ID
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 lang={locale}>
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,18 @@
1
+ import { ampless } from '@/lib/ampless'
2
+ import { createUnderscoreRouteHandler } from '@ampless/runtime/routes'
3
+
4
+ // Internal handler for no_layout HTML and static-bundle posts. Reached
5
+ // via middleware rewrite from `/<slug>(/<path>)` — never directly.
6
+ // Covers two cases that both bypass the themed post page:
7
+ //
8
+ // - `format: 'html'` posts with `metadata.no_layout === true`
9
+ // → bare HTML response, no Next.js root layout, no theme chrome
10
+ // - `format: 'static'` posts
11
+ // → S3 presigned URL redirect for the entrypoint and every bundle file
12
+ //
13
+ // The literal folder is `r/` (not `_/` etc.) because Next.js's App
14
+ // Router excludes any path part starting with `_` from route
15
+ // discovery. The browser URL stays `/<slug>(/<path>)` throughout —
16
+ // middleware does the public→internal translation.
17
+ export const dynamic = 'force-dynamic'
18
+ export const GET = createUnderscoreRouteHandler(ampless)
@@ -27,29 +27,7 @@ export default defineConfig({
27
27
  losslessForPng: true,
28
28
  },
29
29
  },
30
- // Multi-site config. Leave undefined for single-site operation; the
31
- // runtime then serves every request as siteId='default'. To run
32
- // multiple sites in one Amplify environment, declare two or more
33
- // entries here:
34
- //
35
- // sites: {
36
- // blog: {
37
- // domains: ['blog.example.com', 'www.example.com'],
38
- // name: 'My Blog',
39
- // url: 'https://blog.example.com',
40
- // },
41
- // docs: {
42
- // domains: ['docs.example.com'],
43
- // name: 'Docs',
44
- // url: 'https://docs.example.com',
45
- // },
46
- // },
47
- //
48
- // In multi-site mode the middleware forces `Cache-Control: private,
49
- // no-store` on public responses (Amplify Hosting's CloudFront cache
50
- // key cannot disambiguate by Host). Single-site mode leaves caching
51
- // to the default page directives.
52
-
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, siteId, className }: Props) {
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, { siteId, limit: 50 })
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: We intentionally do NOT pass an `ampless` instance to
11
- // createAdmin. That would import `lib/ampless.ts`, which imports
12
- // `themes-registry`, which (via theme pages `lib/i18n.ts`) imports
13
- // back to this file a circular chain that crashes with a TDZ
14
- // ReferenceError on `ampless` at module init. `createAdmin` builds
15
- // its own internal Ampless when omitted, which is functionally
16
- // equivalent for admin's needs.
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({ siteId })
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
- "sandbox:dev": "ampx sandbox --once && next dev",
13
- "update-ampless": "npx create-ampless@alpha upgrade",
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.2",
27
- "@ampless/plugin-rss": "^0.2.0-alpha.2",
28
- "@ampless/plugin-seo": "^0.2.0-alpha.2",
29
- "@ampless/plugin-webhook": "^0.2.0-alpha.2",
30
- "@ampless/admin": "^0.2.0-alpha.7",
31
- "@ampless/backend": "^0.2.0-alpha.3",
32
- "@ampless/runtime": "^0.2.0-alpha.4",
25
+ "@ampless/plugin-og-image": "^0.2.0-alpha.10",
26
+ "@ampless/plugin-rss": "^0.2.0-alpha.10",
27
+ "@ampless/plugin-seo": "^0.2.0-alpha.10",
28
+ "@ampless/plugin-webhook": "^0.2.0-alpha.10",
29
+ "@ampless/admin": "^1.0.0-alpha.27",
30
+ "@ampless/backend": "^1.0.0-alpha.20",
31
+ "@ampless/runtime": "^1.0.0-alpha.14",
33
32
  "@digital-go-jp/tailwind-theme-plugin": "^0.3.4",
34
- "ampless": "^0.2.0-alpha.2",
33
+ "ampless": "^1.0.0-alpha.10",
35
34
  "aws-amplify": "^6.10.0",
36
35
  "class-variance-authority": "^0.7.1",
37
36
  "clsx": "^2.1.1",
@@ -1,18 +1,32 @@
1
1
  // Public-site proxy (Next.js 16 rename of "middleware"). Implementation
2
2
  // moved to `@ampless/runtime` (L1 extraction); this file wires the
3
- // project's `cms.config` into the factory and re-exports the default
4
- // matcher. The runtime export name is still `createAmplessMiddleware`
5
- // for API stability; only the user-side file convention (proxy.ts +
6
- // `export const proxy`) follows Next 16's rename.
3
+ // project's `cms.config` + AppSync endpoint into the factory and
4
+ // re-exports the default matcher. The runtime export name is still
5
+ // `createAmplessMiddleware` for API stability; only the user-side
6
+ // file convention (proxy.ts + `export const proxy`) follows Next 16's
7
+ // rename.
7
8
  //
8
- // See `@ampless/runtime/middleware` for behaviour details: multi-site
9
- // host rewrite, `<slug>.html` raw route, `?previewTheme=` header
10
- // forwarding, multi-site Cache-Control override.
9
+ // See `@ampless/runtime/middleware` for behaviour details: AppSync
10
+ // flag fetch + Lambda-memory LRU + `/r/<slug>(/<path>)` rewrite for
11
+ // no_layout HTML / static bundles + per-post Cache-Control.
11
12
 
12
13
  import cmsConfig from './cms.config'
14
+ import outputs from './amplify_outputs.json'
13
15
  import { createAmplessMiddleware } from '@ampless/runtime/middleware'
14
16
 
15
- export const proxy = createAmplessMiddleware({ cmsConfig })
17
+ // `amplify_outputs.json` is generated by `npx ampx sandbox` /
18
+ // `npx ampx pipeline-deploy` — the public API key has a 365-day TTL
19
+ // and is rotated monthly by the api-key-renewer Lambda. See
20
+ // RUNBOOK.md §"API key rotation".
21
+ export const proxy = createAmplessMiddleware({
22
+ cmsConfig,
23
+ appsyncUrl: outputs.data.url,
24
+ // The public API key is always present in production deploys (the
25
+ // backend declares `apiKeyAuthorizationMode`). The non-null assertion
26
+ // is honest about the assumption; `ampx sandbox` will surface a clear
27
+ // error long before middleware ever runs if it ever isn't.
28
+ apiKey: outputs.data.api_key!,
29
+ })
16
30
 
17
31
  // Next.js 16's Turbopack requires `config` to be a statically
18
32
  // analysable object literal — referencing an imported variable
@@ -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 の標準テーマトークン経由で反映されます。