create-ampless 0.2.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +229 -0
- package/dist/templates/_shared/RUNBOOK.md +178 -0
- package/dist/templates/_shared/amplify/auth/post-confirmation/handler.ts +4 -0
- package/dist/templates/_shared/amplify/auth/post-confirmation/resource.ts +6 -0
- package/dist/templates/_shared/amplify/auth/resource.ts +8 -0
- package/dist/templates/_shared/amplify/backend.ts +29 -0
- package/dist/templates/_shared/amplify/data/get-published-post.js +33 -0
- package/dist/templates/_shared/amplify/data/list-posts-by-tag.js +52 -0
- package/dist/templates/_shared/amplify/data/list-published-posts.js +57 -0
- package/dist/templates/_shared/amplify/data/resource.ts +30 -0
- package/dist/templates/_shared/amplify/events/dispatcher/handler.ts +4 -0
- package/dist/templates/_shared/amplify/events/dispatcher/resource.ts +12 -0
- package/dist/templates/_shared/amplify/events/processor-trusted/handler.ts +12 -0
- package/dist/templates/_shared/amplify/events/processor-trusted/resource.ts +14 -0
- package/dist/templates/_shared/amplify/events/processor-untrusted/handler.ts +10 -0
- package/dist/templates/_shared/amplify/events/processor-untrusted/resource.ts +9 -0
- package/dist/templates/_shared/amplify/functions/api-key-renewer/handler.ts +4 -0
- package/dist/templates/_shared/amplify/functions/api-key-renewer/resource.ts +12 -0
- package/dist/templates/_shared/amplify/storage/resource.ts +7 -0
- package/dist/templates/_shared/app/(admin)/admin/layout.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/media/page.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/page.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/posts/[postId]/page.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/posts/new/page.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/posts/page.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/sites/[siteId]/page.tsx +5 -0
- package/dist/templates/_shared/app/(admin)/admin/sites/[siteId]/theme/page.tsx +6 -0
- package/dist/templates/_shared/app/(admin)/admin/sites/page.tsx +5 -0
- package/dist/templates/_shared/app/api/media/[...path]/route.ts +5 -0
- package/dist/templates/_shared/app/globals.css +114 -0
- package/dist/templates/_shared/app/layout.tsx +48 -0
- package/dist/templates/_shared/app/login/page.tsx +4 -0
- package/dist/templates/_shared/app/providers.tsx +13 -0
- package/dist/templates/_shared/app/site/[siteId]/[slug]/page.tsx +10 -0
- package/dist/templates/_shared/app/site/[siteId]/feed.xml/route.ts +5 -0
- package/dist/templates/_shared/app/site/[siteId]/og/[slug]/route.ts +6 -0
- package/dist/templates/_shared/app/site/[siteId]/page.tsx +10 -0
- package/dist/templates/_shared/app/site/[siteId]/raw/[slug]/route.ts +5 -0
- package/dist/templates/_shared/app/site/[siteId]/sitemap.xml/route.ts +5 -0
- package/dist/templates/_shared/app/site/[siteId]/tag/[tag]/page.tsx +10 -0
- package/dist/templates/_shared/cms.config.ts +110 -0
- package/dist/templates/_shared/components/i18n-provider.tsx +7 -0
- package/dist/templates/_shared/components/lightbox-content.tsx +69 -0
- package/dist/templates/_shared/components/site-chrome/collapsible-sidebar.tsx +54 -0
- package/dist/templates/_shared/components/site-chrome/mobile-menu.tsx +68 -0
- package/dist/templates/_shared/components/site-chrome/site-footer.tsx +43 -0
- package/dist/templates/_shared/components/site-chrome/site-header.tsx +94 -0
- package/dist/templates/_shared/components/site-chrome/site-sidebar.tsx +81 -0
- package/dist/templates/_shared/components/tag-list.tsx +25 -0
- package/dist/templates/_shared/components.json +21 -0
- package/dist/templates/_shared/lib/admin-site-client.ts +10 -0
- package/dist/templates/_shared/lib/admin-site.ts +8 -0
- package/dist/templates/_shared/lib/admin.ts +24 -0
- package/dist/templates/_shared/lib/ampless.ts +23 -0
- package/dist/templates/_shared/lib/amplify-server.ts +7 -0
- package/dist/templates/_shared/lib/amplify.ts +9 -0
- package/dist/templates/_shared/lib/auth-server.ts +11 -0
- package/dist/templates/_shared/lib/cn.ts +5 -0
- package/dist/templates/_shared/lib/i18n.ts +31 -0
- package/dist/templates/_shared/lib/kv-provider.ts +7 -0
- package/dist/templates/_shared/lib/media.ts +6 -0
- package/dist/templates/_shared/lib/posts-provider.ts +7 -0
- package/dist/templates/_shared/lib/posts-public.ts +19 -0
- package/dist/templates/_shared/lib/posts.ts +12 -0
- package/dist/templates/_shared/lib/seo.ts +8 -0
- package/dist/templates/_shared/lib/site-settings.ts +8 -0
- package/dist/templates/_shared/lib/storage.ts +7 -0
- package/dist/templates/_shared/lib/theme-actions.ts +5 -0
- package/dist/templates/_shared/lib/theme-active.ts +8 -0
- package/dist/templates/_shared/lib/theme-config.ts +10 -0
- package/dist/templates/_shared/lib/upload.ts +6 -0
- package/dist/templates/_shared/middleware.ts +13 -0
- package/dist/templates/_shared/next.config.mjs +11 -0
- package/dist/templates/_shared/package.json +63 -0
- package/dist/templates/_shared/postcss.config.mjs +5 -0
- package/dist/templates/_shared/themes-registry.ts +38 -0
- package/dist/templates/_shared/tsconfig.json +23 -0
- package/dist/templates/blog/README.md +52 -0
- package/dist/templates/blog/index.ts +29 -0
- package/dist/templates/blog/manifest.ts +144 -0
- package/dist/templates/blog/pages/feed.ts +31 -0
- package/dist/templates/blog/pages/home.tsx +108 -0
- package/dist/templates/blog/pages/post.tsx +94 -0
- package/dist/templates/blog/pages/sitemap.ts +30 -0
- package/dist/templates/blog/pages/tag.tsx +76 -0
- package/dist/templates/blog/tokens.css +54 -0
- package/dist/templates/corporate/README.md +20 -0
- package/dist/templates/corporate/index.ts +25 -0
- package/dist/templates/corporate/manifest.ts +94 -0
- package/dist/templates/corporate/pages/feed.ts +29 -0
- package/dist/templates/corporate/pages/home.tsx +130 -0
- package/dist/templates/corporate/pages/post.tsx +96 -0
- package/dist/templates/corporate/pages/sitemap.ts +28 -0
- package/dist/templates/corporate/pages/tag.tsx +81 -0
- package/dist/templates/corporate/tokens.css +47 -0
- package/dist/templates/dads/README.md +35 -0
- package/dist/templates/dads/index.ts +25 -0
- package/dist/templates/dads/manifest.ts +84 -0
- package/dist/templates/dads/pages/feed.ts +29 -0
- package/dist/templates/dads/pages/home.tsx +126 -0
- package/dist/templates/dads/pages/post.tsx +102 -0
- package/dist/templates/dads/pages/sitemap.ts +28 -0
- package/dist/templates/dads/pages/tag.tsx +86 -0
- package/dist/templates/dads/tokens.css +67 -0
- package/dist/templates/docs/README.md +27 -0
- package/dist/templates/docs/index.ts +25 -0
- package/dist/templates/docs/manifest.ts +89 -0
- package/dist/templates/docs/pages/feed.ts +29 -0
- package/dist/templates/docs/pages/home.tsx +88 -0
- package/dist/templates/docs/pages/post.tsx +96 -0
- package/dist/templates/docs/pages/sitemap.ts +28 -0
- package/dist/templates/docs/pages/tag.tsx +79 -0
- package/dist/templates/docs/tokens.css +55 -0
- package/dist/templates/landing/README.md +25 -0
- package/dist/templates/landing/index.ts +25 -0
- package/dist/templates/landing/manifest.ts +118 -0
- package/dist/templates/landing/pages/feed.ts +31 -0
- package/dist/templates/landing/pages/home.tsx +123 -0
- package/dist/templates/landing/pages/post.tsx +95 -0
- package/dist/templates/landing/pages/sitemap.ts +28 -0
- package/dist/templates/landing/pages/tag.tsx +85 -0
- package/dist/templates/landing/tokens.css +47 -0
- package/dist/templates/minimal/README.md +52 -0
- package/dist/templates/minimal/index.ts +25 -0
- package/dist/templates/minimal/manifest.ts +35 -0
- package/dist/templates/minimal/pages/feed.ts +31 -0
- package/dist/templates/minimal/pages/home.tsx +44 -0
- package/dist/templates/minimal/pages/post.tsx +65 -0
- package/dist/templates/minimal/pages/sitemap.ts +30 -0
- package/dist/templates/minimal/pages/tag.tsx +46 -0
- package/dist/templates/minimal/tokens.css +46 -0
- package/package.json +41 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/* Corporate theme tokens — slate / navy on near-white. Conservative
|
|
2
|
+
* palette intended for company / institutional sites. Same scoped-
|
|
3
|
+
* selector pattern as the other themes. */
|
|
4
|
+
|
|
5
|
+
[data-theme='corporate'] {
|
|
6
|
+
--background: oklch(0.99 0 0);
|
|
7
|
+
--foreground: oklch(0.18 0.02 250);
|
|
8
|
+
--card: oklch(1 0 0);
|
|
9
|
+
--card-foreground: oklch(0.18 0.02 250);
|
|
10
|
+
--primary: oklch(0.4 0.13 250);
|
|
11
|
+
--primary-foreground: oklch(0.99 0 0);
|
|
12
|
+
--secondary: oklch(0.95 0.01 250);
|
|
13
|
+
--secondary-foreground: oklch(0.25 0.05 250);
|
|
14
|
+
--muted: oklch(0.96 0.005 250);
|
|
15
|
+
--muted-foreground: oklch(0.5 0.02 250);
|
|
16
|
+
--accent: oklch(0.95 0.02 250);
|
|
17
|
+
--accent-foreground: oklch(0.3 0.08 250);
|
|
18
|
+
--destructive: oklch(0.55 0.22 25);
|
|
19
|
+
--destructive-foreground: oklch(0.99 0 0);
|
|
20
|
+
--border: oklch(0.9 0.005 250);
|
|
21
|
+
--input: oklch(0.9 0.005 250);
|
|
22
|
+
--ring: oklch(0.4 0.13 250);
|
|
23
|
+
--radius: 0.25rem;
|
|
24
|
+
--ampless-body-font: system-ui, -apple-system, sans-serif;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@media (prefers-color-scheme: dark) {
|
|
28
|
+
[data-theme='corporate'] {
|
|
29
|
+
--background: oklch(0.16 0.02 250);
|
|
30
|
+
--foreground: oklch(0.96 0.005 250);
|
|
31
|
+
--card: oklch(0.22 0.02 250);
|
|
32
|
+
--card-foreground: oklch(0.96 0.005 250);
|
|
33
|
+
--primary: oklch(0.7 0.14 250);
|
|
34
|
+
--primary-foreground: oklch(0.16 0.02 250);
|
|
35
|
+
--secondary: oklch(0.28 0.03 250);
|
|
36
|
+
--secondary-foreground: oklch(0.96 0.005 250);
|
|
37
|
+
--muted: oklch(0.26 0.02 250);
|
|
38
|
+
--muted-foreground: oklch(0.7 0.02 250);
|
|
39
|
+
--accent: oklch(0.32 0.05 250);
|
|
40
|
+
--accent-foreground: oklch(0.96 0.005 250);
|
|
41
|
+
--destructive: oklch(0.55 0.22 25);
|
|
42
|
+
--destructive-foreground: oklch(0.99 0 0);
|
|
43
|
+
--border: oklch(0.32 0.02 250);
|
|
44
|
+
--input: oklch(0.32 0.02 250);
|
|
45
|
+
--ring: oklch(0.7 0.14 250);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# {{siteName}}
|
|
2
|
+
|
|
3
|
+
DADS theme — built on the official **[Digital Agency Design System Tailwind plugin](https://github.com/digital-go-jp/tailwind-theme-plugin)** (`@digital-go-jp/tailwind-theme-plugin`, MIT). Color palette and typography conform to DADS specifications, suitable for Japanese government / public-sector / institutional sites.
|
|
4
|
+
|
|
5
|
+
## What's plugin-backed
|
|
6
|
+
|
|
7
|
+
- **Color palette** — `solidBlue` (`#0017c1`) as primary, plus the full DADS scale (light-blue, cyan, green, lime, yellow, orange, red, magenta) accessible as Tailwind classes (`bg-blue-900`, `text-blue-50`, etc.)
|
|
8
|
+
- **Typography** — `fontFamily.sans` set to Noto Sans JP via the plugin; surfaced through `--ampless-body-font`
|
|
9
|
+
- **Border radii** — `rounded-4` / `rounded-6` available
|
|
10
|
+
|
|
11
|
+
`templates/dads/tokens.css` binds the plugin's CSS variables (`--color-blue-900` etc.) to ampless's standard theme variables (`--primary`, `--background`, ...) so all shared chrome (SiteHeader, SiteFooter, shadcn buttons, etc.) automatically renders in DADS colors.
|
|
12
|
+
|
|
13
|
+
When DADS publishes a new palette version, bumping `@digital-go-jp/tailwind-theme-plugin` in `package.json` is enough — the theme picks it up.
|
|
14
|
+
|
|
15
|
+
## Customizing
|
|
16
|
+
|
|
17
|
+
In `/admin/sites/<siteId>/theme`:
|
|
18
|
+
|
|
19
|
+
- **Logo image URL** — branding (organization mark)
|
|
20
|
+
- **Primary color** — defaults to DADS solidBlue. Changing to a non-DADS color makes the site no longer DADS-conformant.
|
|
21
|
+
- **Top story slug** — feature one published post between hero and news
|
|
22
|
+
- **Header navigation / Footer links / Footer legend**
|
|
23
|
+
|
|
24
|
+
## Notes
|
|
25
|
+
|
|
26
|
+
- Dark mode uses an inverted approximation. DADS doesn't ship an official dark palette as of plugin 0.3.4; when one lands, update `templates/dads/tokens.css` to bind the dark variables.
|
|
27
|
+
- The plugin only provides design tokens. For full DADS components (buttons, alerts, tabs, etc.), see [design-system-example-components](https://github.com/digital-go-jp/design-system-example-components) and adapt as needed.
|
|
28
|
+
|
|
29
|
+
## Getting started
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install
|
|
33
|
+
npx ampx sandbox
|
|
34
|
+
npm run dev
|
|
35
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineThemeModule } from 'ampless'
|
|
2
|
+
import './tokens.css'
|
|
3
|
+
import manifest from './manifest'
|
|
4
|
+
import DadsHome from './pages/home'
|
|
5
|
+
import DadsPost, { generatePostMetadata } from './pages/post'
|
|
6
|
+
import DadsTag from './pages/tag'
|
|
7
|
+
import { dadsFeedHandler } from './pages/feed'
|
|
8
|
+
import { dadsSitemapHandler } from './pages/sitemap'
|
|
9
|
+
|
|
10
|
+
export default defineThemeModule({
|
|
11
|
+
name: 'dads',
|
|
12
|
+
manifest,
|
|
13
|
+
components: {
|
|
14
|
+
Home: DadsHome,
|
|
15
|
+
Post: DadsPost,
|
|
16
|
+
Tag: DadsTag,
|
|
17
|
+
},
|
|
18
|
+
metadata: {
|
|
19
|
+
Post: generatePostMetadata,
|
|
20
|
+
},
|
|
21
|
+
routes: {
|
|
22
|
+
feed: dadsFeedHandler,
|
|
23
|
+
sitemap: dadsSitemapHandler,
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { defineTheme } from 'ampless'
|
|
2
|
+
|
|
3
|
+
// DADS (Digital Agency Design System) — デジタル庁デザインシステム準拠
|
|
4
|
+
// テーマ。政府 / 自治体 / 公共系サイト向け。視覚的特徴: 高コントラス
|
|
5
|
+
// ト、明朝寄りの本文、控えめな装飾、日付や出典の明示、アクセシビリティ
|
|
6
|
+
// 重視。
|
|
7
|
+
//
|
|
8
|
+
// カスタマイズ surface はあえて狭め。DADS は「色を自由に変えると準拠
|
|
9
|
+
// しなくなる」性質のシステムなので、ユーザーがロゴ / ナビ / 簡素な
|
|
10
|
+
// アクセント色程度を触れる範囲に限定。primary は solidBlue 系を
|
|
11
|
+
// 強く推奨 (description に注意書き)。
|
|
12
|
+
export default defineTheme({
|
|
13
|
+
name: 'dads',
|
|
14
|
+
label: { en: 'DADS', ja: 'デジタル庁デザインシステム' },
|
|
15
|
+
description: {
|
|
16
|
+
en: 'Government / public-sector layout following the Digital Agency Design System aesthetic. High contrast, accessibility-first, minimal decoration.',
|
|
17
|
+
ja: 'デジタル庁デザインシステム準拠のレイアウト。政府・自治体・公共系サイト向け。高コントラスト、アクセシビリティ重視、装飾控えめ。',
|
|
18
|
+
},
|
|
19
|
+
fields: [
|
|
20
|
+
{
|
|
21
|
+
key: 'logoUrl',
|
|
22
|
+
label: { en: 'Logo image URL', ja: 'ロゴ画像 URL' },
|
|
23
|
+
group: { en: 'Branding', ja: 'ブランディング' },
|
|
24
|
+
type: 'image',
|
|
25
|
+
default: '',
|
|
26
|
+
description: {
|
|
27
|
+
en: 'URL or media path. Empty falls back to the site name as text.',
|
|
28
|
+
ja: '画像 URL またはメディアパス。空欄ならサイト名がテキスト表示されます。',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: 'primary',
|
|
33
|
+
label: { en: 'Primary color', ja: 'プライマリカラー' },
|
|
34
|
+
group: { en: 'Colors', ja: 'カラー' },
|
|
35
|
+
type: 'color',
|
|
36
|
+
default: 'oklch(0.34 0.18 264)',
|
|
37
|
+
cssVar: '--primary',
|
|
38
|
+
description: {
|
|
39
|
+
en: 'Defaults to DADS solidBlue (#0017c1 equivalent). Changing to a non-DADS color makes the site no longer DADS-compliant.',
|
|
40
|
+
ja: 'デフォルトは DADS solidBlue (#0017c1 相当)。DADS 仕様外の色に変更すると DADS 準拠ではなくなります。',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: 'featuredSlug',
|
|
45
|
+
label: { en: 'Top story slug', ja: 'トップストーリーのスラッグ' },
|
|
46
|
+
group: { en: 'Hero', ja: 'ヒーロー' },
|
|
47
|
+
type: 'text',
|
|
48
|
+
default: '',
|
|
49
|
+
maxLength: 200,
|
|
50
|
+
description: {
|
|
51
|
+
en: 'Slug of a published post to feature between the hero and the news list. Empty disables.',
|
|
52
|
+
ja: 'ヒーローとニュース一覧の間に載せたい公開記事のスラッグ。空なら非表示。',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: 'headerNav',
|
|
57
|
+
label: { en: 'Header navigation', ja: 'ヘッダーナビ' },
|
|
58
|
+
group: { en: 'Navigation', ja: 'ナビゲーション' },
|
|
59
|
+
type: 'linkList',
|
|
60
|
+
default: [],
|
|
61
|
+
maxItems: 8,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
key: 'footerLinks',
|
|
65
|
+
label: { en: 'Footer links', ja: 'フッターリンク' },
|
|
66
|
+
group: { en: 'Navigation', ja: 'ナビゲーション' },
|
|
67
|
+
type: 'linkList',
|
|
68
|
+
default: [],
|
|
69
|
+
maxItems: 16,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: 'footerLegend',
|
|
73
|
+
label: { en: 'Footer legend', ja: 'フッター注記' },
|
|
74
|
+
group: { en: 'Navigation', ja: 'ナビゲーション' },
|
|
75
|
+
type: 'text',
|
|
76
|
+
default: '',
|
|
77
|
+
maxLength: 300,
|
|
78
|
+
description: {
|
|
79
|
+
en: 'Organization name / address / responsibility statement shown below the footer links.',
|
|
80
|
+
ja: '組織名・所在地・責任者名など、フッター下部に表示する細字。',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { publicAssetUrl } from '@/lib/storage'
|
|
2
|
+
|
|
3
|
+
interface Ctx {
|
|
4
|
+
siteId: string
|
|
5
|
+
request: Request
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function dadsFeedHandler({ siteId }: Ctx): Promise<Response> {
|
|
9
|
+
const url = publicAssetUrl(`public/plugins/rss/${siteId}/feed.xml`)
|
|
10
|
+
const upstream = await fetch(url, { cache: 'no-store' })
|
|
11
|
+
if (!upstream.ok) {
|
|
12
|
+
return new Response(
|
|
13
|
+
`<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0"><channel></channel></rss>\n`,
|
|
14
|
+
{
|
|
15
|
+
status: 200,
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/rss+xml; charset=utf-8',
|
|
18
|
+
'Cache-Control': 'public, max-age=60',
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
return new Response(upstream.body, {
|
|
24
|
+
headers: {
|
|
25
|
+
'Content-Type': 'application/rss+xml; charset=utf-8',
|
|
26
|
+
'Cache-Control': 'public, max-age=300',
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import Link from 'next/link'
|
|
2
|
+
import { formatDate, type ThemeRouteContext } from 'ampless'
|
|
3
|
+
import { listPublishedPosts, getPublishedPost } from '@/lib/posts-public'
|
|
4
|
+
import { loadSiteSettings } from '@/lib/site-settings'
|
|
5
|
+
import { loadThemeConfig } from '@/lib/theme-config'
|
|
6
|
+
import { renderBody } from '@/lib/posts'
|
|
7
|
+
import { SiteHeader } from '@/components/site-chrome/site-header'
|
|
8
|
+
import { SiteFooter } from '@/components/site-chrome/site-footer'
|
|
9
|
+
|
|
10
|
+
// DADS home: hero band with strong title bar, optional featured post,
|
|
11
|
+
// formal news list with prominent dates. Layout deliberately
|
|
12
|
+
// understated — DADS leans on hierarchy and whitespace, not
|
|
13
|
+
// decoration.
|
|
14
|
+
export default async function DadsHome({ params }: ThemeRouteContext) {
|
|
15
|
+
const { siteId } = await params
|
|
16
|
+
const [settings, theme, postsResult] = await Promise.all([
|
|
17
|
+
loadSiteSettings(siteId),
|
|
18
|
+
loadThemeConfig(siteId),
|
|
19
|
+
listPublishedPosts({ siteId, limit: 10 }),
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
const featuredSlug = theme.values.featuredSlug?.trim()
|
|
23
|
+
const featured = featuredSlug
|
|
24
|
+
? await getPublishedPost(featuredSlug, { siteId })
|
|
25
|
+
: null
|
|
26
|
+
const posts = featured
|
|
27
|
+
? postsResult.items.filter((p) => p.slug !== featured.slug)
|
|
28
|
+
: postsResult.items
|
|
29
|
+
|
|
30
|
+
const footerLegend = theme.values.footerLegend?.trim()
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
<SiteHeader
|
|
35
|
+
links={theme.values.headerNav}
|
|
36
|
+
logoUrl={theme.values.logoUrl}
|
|
37
|
+
siteName={settings.site.name}
|
|
38
|
+
brandClassName="text-base font-bold tracking-tight"
|
|
39
|
+
className="border-b-2 border-[var(--primary)]"
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
<main>
|
|
43
|
+
<section className="border-b bg-[var(--secondary)] px-6 py-12">
|
|
44
|
+
<div className="mx-auto max-w-4xl">
|
|
45
|
+
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">
|
|
46
|
+
{settings.site.name}
|
|
47
|
+
</h1>
|
|
48
|
+
{settings.site.description && (
|
|
49
|
+
<p className="mt-3 max-w-2xl leading-relaxed">{settings.site.description}</p>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
53
|
+
|
|
54
|
+
{featured && (
|
|
55
|
+
<section className="border-b px-6 py-12">
|
|
56
|
+
<div className="mx-auto max-w-4xl">
|
|
57
|
+
<p className="mb-2 text-xs font-bold uppercase tracking-widest text-[var(--primary)]">
|
|
58
|
+
{featured.publishedAt ? (
|
|
59
|
+
<time dateTime={featured.publishedAt}>
|
|
60
|
+
{formatDate(featured.publishedAt, settings.dateFormat, settings.timezone)}
|
|
61
|
+
</time>
|
|
62
|
+
) : (
|
|
63
|
+
'—'
|
|
64
|
+
)}
|
|
65
|
+
</p>
|
|
66
|
+
<h2 className="text-2xl font-bold tracking-tight">
|
|
67
|
+
<Link href={`/${featured.slug}`} className="underline-offset-4 hover:underline">
|
|
68
|
+
{featured.title}
|
|
69
|
+
</Link>
|
|
70
|
+
</h2>
|
|
71
|
+
<div
|
|
72
|
+
className="prose prose-neutral dark:prose-invert mt-6 max-w-none"
|
|
73
|
+
dangerouslySetInnerHTML={{ __html: renderBody(featured) }}
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
</section>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{posts.length > 0 && (
|
|
80
|
+
<section className="px-6 py-12">
|
|
81
|
+
<div className="mx-auto max-w-4xl">
|
|
82
|
+
<h2 className="mb-6 border-b-2 border-[var(--primary)] pb-2 text-lg font-bold">
|
|
83
|
+
お知らせ / News
|
|
84
|
+
</h2>
|
|
85
|
+
<ul className="divide-y">
|
|
86
|
+
{posts.map((post) => (
|
|
87
|
+
<li key={post.postId} className="py-4">
|
|
88
|
+
<Link
|
|
89
|
+
href={`/${post.slug}`}
|
|
90
|
+
className="group flex flex-col gap-1 sm:flex-row sm:items-baseline sm:gap-6"
|
|
91
|
+
>
|
|
92
|
+
{post.publishedAt && (
|
|
93
|
+
<time
|
|
94
|
+
dateTime={post.publishedAt}
|
|
95
|
+
className="font-mono text-xs tracking-wide text-[var(--muted-foreground)] sm:w-32 sm:shrink-0"
|
|
96
|
+
>
|
|
97
|
+
{formatDate(post.publishedAt, settings.dateFormat, settings.timezone)}
|
|
98
|
+
</time>
|
|
99
|
+
)}
|
|
100
|
+
<span className="flex-1 underline-offset-4 group-hover:underline">
|
|
101
|
+
{post.title}
|
|
102
|
+
</span>
|
|
103
|
+
</Link>
|
|
104
|
+
</li>
|
|
105
|
+
))}
|
|
106
|
+
</ul>
|
|
107
|
+
</div>
|
|
108
|
+
</section>
|
|
109
|
+
)}
|
|
110
|
+
</main>
|
|
111
|
+
|
|
112
|
+
<SiteFooter
|
|
113
|
+
links={theme.values.footerLinks}
|
|
114
|
+
className="bg-[var(--secondary)]"
|
|
115
|
+
legend={
|
|
116
|
+
<div className="space-y-1">
|
|
117
|
+
{footerLegend && <p className="whitespace-pre-line">{footerLegend}</p>}
|
|
118
|
+
<p>
|
|
119
|
+
© {new Date().getFullYear()} {settings.site.name}
|
|
120
|
+
</p>
|
|
121
|
+
</div>
|
|
122
|
+
}
|
|
123
|
+
/>
|
|
124
|
+
</>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import Link from 'next/link'
|
|
3
|
+
import { notFound } from 'next/navigation'
|
|
4
|
+
import { formatDate, type ThemeRouteContext } from 'ampless'
|
|
5
|
+
import { renderBody } from '@/lib/posts'
|
|
6
|
+
import { LightboxBinder } from '@/components/lightbox-content'
|
|
7
|
+
import { TagList } from '@/components/tag-list'
|
|
8
|
+
import { postMetadata } from '@/lib/seo'
|
|
9
|
+
import { loadSiteSettings } from '@/lib/site-settings'
|
|
10
|
+
import { loadThemeConfig } from '@/lib/theme-config'
|
|
11
|
+
import { getPublishedPost } from '@/lib/posts-public'
|
|
12
|
+
import { SiteHeader } from '@/components/site-chrome/site-header'
|
|
13
|
+
import { SiteFooter } from '@/components/site-chrome/site-footer'
|
|
14
|
+
import { t } from '@/lib/i18n'
|
|
15
|
+
|
|
16
|
+
type PostCtx = ThemeRouteContext<{ slug: string }>
|
|
17
|
+
|
|
18
|
+
export async function generatePostMetadata({ params }: PostCtx): Promise<Metadata> {
|
|
19
|
+
const { siteId, slug } = await params
|
|
20
|
+
const post = await getPublishedPost(slug, { siteId })
|
|
21
|
+
if (!post) return {}
|
|
22
|
+
return postMetadata(post, siteId)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default async function DadsPost({ params }: PostCtx) {
|
|
26
|
+
const { siteId, slug } = await params
|
|
27
|
+
const [post, settings, theme] = await Promise.all([
|
|
28
|
+
getPublishedPost(slug, { siteId }),
|
|
29
|
+
loadSiteSettings(siteId),
|
|
30
|
+
loadThemeConfig(siteId),
|
|
31
|
+
])
|
|
32
|
+
if (!post) notFound()
|
|
33
|
+
|
|
34
|
+
const defaultLightbox = settings.media.imageDisplay === 'lightbox'
|
|
35
|
+
const maxWidth = settings.media.imageMaxWidth ?? '100%'
|
|
36
|
+
const proseStyle: React.CSSProperties = {
|
|
37
|
+
['--ampless-img-max-width' as string]: maxWidth,
|
|
38
|
+
}
|
|
39
|
+
const footerLegend = theme.values.footerLegend?.trim()
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<SiteHeader
|
|
44
|
+
links={theme.values.headerNav}
|
|
45
|
+
logoUrl={theme.values.logoUrl}
|
|
46
|
+
siteName={settings.site.name}
|
|
47
|
+
brandClassName="text-base font-bold tracking-tight"
|
|
48
|
+
className="border-b-2 border-[var(--primary)]"
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
<main className="mx-auto max-w-3xl px-6 py-10">
|
|
52
|
+
<nav className="mb-8 text-sm">
|
|
53
|
+
<Link
|
|
54
|
+
href="/"
|
|
55
|
+
className="text-[var(--primary)] underline-offset-4 hover:underline"
|
|
56
|
+
>
|
|
57
|
+
{t('public.back')}
|
|
58
|
+
</Link>
|
|
59
|
+
</nav>
|
|
60
|
+
|
|
61
|
+
<article>
|
|
62
|
+
<header className="mb-10 border-b pb-6">
|
|
63
|
+
<h1 className="text-3xl font-bold leading-tight tracking-tight sm:text-4xl">
|
|
64
|
+
{post.title}
|
|
65
|
+
</h1>
|
|
66
|
+
{post.publishedAt && (
|
|
67
|
+
<p className="mt-3 text-sm text-[var(--muted-foreground)]">
|
|
68
|
+
<time dateTime={post.publishedAt}>
|
|
69
|
+
{formatDate(post.publishedAt, settings.dateFormat, settings.timezone)}
|
|
70
|
+
</time>
|
|
71
|
+
</p>
|
|
72
|
+
)}
|
|
73
|
+
</header>
|
|
74
|
+
|
|
75
|
+
<div
|
|
76
|
+
id="post-body"
|
|
77
|
+
className="prose prose-neutral dark:prose-invert max-w-none [&_a]:text-[var(--primary)] [&_a]:underline-offset-4 [&_img]:max-w-[var(--ampless-img-max-width)] [&_img]:mx-auto"
|
|
78
|
+
style={proseStyle}
|
|
79
|
+
dangerouslySetInnerHTML={{ __html: renderBody(post) }}
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
<TagList tags={post.tags} className="mt-10 border-t pt-6" />
|
|
83
|
+
</article>
|
|
84
|
+
|
|
85
|
+
<LightboxBinder scopeSelector="#post-body" defaultLightbox={defaultLightbox} />
|
|
86
|
+
</main>
|
|
87
|
+
|
|
88
|
+
<SiteFooter
|
|
89
|
+
links={theme.values.footerLinks}
|
|
90
|
+
className="bg-[var(--secondary)]"
|
|
91
|
+
legend={
|
|
92
|
+
<div className="space-y-1">
|
|
93
|
+
{footerLegend && <p className="whitespace-pre-line">{footerLegend}</p>}
|
|
94
|
+
<p>
|
|
95
|
+
© {new Date().getFullYear()} {settings.site.name}
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
}
|
|
99
|
+
/>
|
|
100
|
+
</>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { publicAssetUrl } from '@/lib/storage'
|
|
2
|
+
|
|
3
|
+
interface Ctx {
|
|
4
|
+
siteId: string
|
|
5
|
+
request: Request
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function dadsSitemapHandler({ siteId }: Ctx): Promise<Response> {
|
|
9
|
+
const url = publicAssetUrl(`public/plugins/seo/${siteId}/sitemap.xml`)
|
|
10
|
+
const upstream = await fetch(url, { cache: 'no-store' })
|
|
11
|
+
if (!upstream.ok) {
|
|
12
|
+
return new Response(
|
|
13
|
+
`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>\n`,
|
|
14
|
+
{
|
|
15
|
+
headers: {
|
|
16
|
+
'Content-Type': 'application/xml; charset=utf-8',
|
|
17
|
+
'Cache-Control': 'public, max-age=60',
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
return new Response(upstream.body, {
|
|
23
|
+
headers: {
|
|
24
|
+
'Content-Type': 'application/xml; charset=utf-8',
|
|
25
|
+
'Cache-Control': 'public, max-age=300',
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import Link from 'next/link'
|
|
2
|
+
import { notFound } from 'next/navigation'
|
|
3
|
+
import { formatDate, type ThemeRouteContext } from 'ampless'
|
|
4
|
+
import { listPostsByTag } from '@/lib/posts-public'
|
|
5
|
+
import { loadSiteSettings } from '@/lib/site-settings'
|
|
6
|
+
import { loadThemeConfig } from '@/lib/theme-config'
|
|
7
|
+
import { SiteHeader } from '@/components/site-chrome/site-header'
|
|
8
|
+
import { SiteFooter } from '@/components/site-chrome/site-footer'
|
|
9
|
+
import { t } from '@/lib/i18n'
|
|
10
|
+
|
|
11
|
+
export default async function DadsTag({ params }: ThemeRouteContext<{ tag: string }>) {
|
|
12
|
+
const { siteId, tag } = await params
|
|
13
|
+
const decodedTag = decodeURIComponent(tag)
|
|
14
|
+
const [{ items: posts }, settings, theme] = await Promise.all([
|
|
15
|
+
listPostsByTag(decodedTag, { siteId, limit: 50 }),
|
|
16
|
+
loadSiteSettings(siteId),
|
|
17
|
+
loadThemeConfig(siteId),
|
|
18
|
+
])
|
|
19
|
+
if (posts.length === 0) notFound()
|
|
20
|
+
|
|
21
|
+
const footerLegend = theme.values.footerLegend?.trim()
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<SiteHeader
|
|
26
|
+
links={theme.values.headerNav}
|
|
27
|
+
logoUrl={theme.values.logoUrl}
|
|
28
|
+
siteName={settings.site.name}
|
|
29
|
+
brandClassName="text-base font-bold tracking-tight"
|
|
30
|
+
className="border-b-2 border-[var(--primary)]"
|
|
31
|
+
/>
|
|
32
|
+
|
|
33
|
+
<main className="mx-auto max-w-4xl px-6 py-10">
|
|
34
|
+
<nav className="mb-8 text-sm">
|
|
35
|
+
<Link
|
|
36
|
+
href="/"
|
|
37
|
+
className="text-[var(--primary)] underline-offset-4 hover:underline"
|
|
38
|
+
>
|
|
39
|
+
{t('public.home')}
|
|
40
|
+
</Link>
|
|
41
|
+
</nav>
|
|
42
|
+
|
|
43
|
+
<header className="mb-10">
|
|
44
|
+
<p className="text-sm text-[var(--muted-foreground)]">{t('public.tagLabel')}</p>
|
|
45
|
+
<h1 className="text-3xl font-bold tracking-tight">#{decodedTag}</h1>
|
|
46
|
+
</header>
|
|
47
|
+
|
|
48
|
+
<ul className="divide-y border-y">
|
|
49
|
+
{posts.map((post) => (
|
|
50
|
+
<li key={post.postId} className="py-4">
|
|
51
|
+
<Link
|
|
52
|
+
href={`/${post.slug}`}
|
|
53
|
+
className="group flex flex-col gap-1 sm:flex-row sm:items-baseline sm:gap-6"
|
|
54
|
+
>
|
|
55
|
+
{post.publishedAt && (
|
|
56
|
+
<time
|
|
57
|
+
dateTime={post.publishedAt}
|
|
58
|
+
className="font-mono text-xs tracking-wide text-[var(--muted-foreground)] sm:w-32 sm:shrink-0"
|
|
59
|
+
>
|
|
60
|
+
{formatDate(post.publishedAt, settings.dateFormat, settings.timezone)}
|
|
61
|
+
</time>
|
|
62
|
+
)}
|
|
63
|
+
<span className="flex-1 underline-offset-4 group-hover:underline">
|
|
64
|
+
{post.title}
|
|
65
|
+
</span>
|
|
66
|
+
</Link>
|
|
67
|
+
</li>
|
|
68
|
+
))}
|
|
69
|
+
</ul>
|
|
70
|
+
</main>
|
|
71
|
+
|
|
72
|
+
<SiteFooter
|
|
73
|
+
links={theme.values.footerLinks}
|
|
74
|
+
className="bg-[var(--secondary)]"
|
|
75
|
+
legend={
|
|
76
|
+
<div className="space-y-1">
|
|
77
|
+
{footerLegend && <p className="whitespace-pre-line">{footerLegend}</p>}
|
|
78
|
+
<p>
|
|
79
|
+
© {new Date().getFullYear()} {settings.site.name}
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
}
|
|
83
|
+
/>
|
|
84
|
+
</>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* DADS theme tokens — Digital Agency Design System準拠.
|
|
2
|
+
*
|
|
3
|
+
* Pulls colors from the official `@digital-go-jp/tailwind-theme-plugin`
|
|
4
|
+
* (loaded in `_shared/app/globals.css`). The plugin extends Tailwind
|
|
5
|
+
* v4's theme with the DADS palette, which Tailwind exposes as
|
|
6
|
+
* `--color-<name>-<step>` CSS variables on `:root`. We bind them to
|
|
7
|
+
* the same `--primary` / `--background` / etc. variable names every
|
|
8
|
+
* other ampless theme uses, so SiteHeader / SiteFooter / shadcn
|
|
9
|
+
* components all light up with DADS colors automatically.
|
|
10
|
+
*
|
|
11
|
+
* This is the canonical plugin-backed integration — when DADS
|
|
12
|
+
* publishes a new palette version, bumping the plugin version is
|
|
13
|
+
* enough; we don't hardcode hex values here. The fallbacks after
|
|
14
|
+
* each `var(...)` are the DADS values from version 0.3.4 in case
|
|
15
|
+
* the plugin isn't loaded for some reason.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
[data-theme='dads'] {
|
|
19
|
+
--background: #ffffff;
|
|
20
|
+
--foreground: #1a1a1c;
|
|
21
|
+
--card: #ffffff;
|
|
22
|
+
--card-foreground: #1a1a1c;
|
|
23
|
+
--primary: var(--color-blue-900, #0017c1);
|
|
24
|
+
--primary-foreground: #ffffff;
|
|
25
|
+
--secondary: var(--color-blue-50, #e8f1fe);
|
|
26
|
+
--secondary-foreground: var(--color-blue-900, #0017c1);
|
|
27
|
+
--muted: #f5f5f5;
|
|
28
|
+
--muted-foreground: #595959;
|
|
29
|
+
--accent: var(--color-blue-50, #e8f1fe);
|
|
30
|
+
--accent-foreground: var(--color-blue-900, #0017c1);
|
|
31
|
+
--destructive: var(--color-red-900, #ce0000);
|
|
32
|
+
--destructive-foreground: #ffffff;
|
|
33
|
+
--border: #d9d9d9;
|
|
34
|
+
--input: #d9d9d9;
|
|
35
|
+
--ring: var(--color-blue-900, #0017c1);
|
|
36
|
+
--radius: 0.25rem;
|
|
37
|
+
/* DADS recommends Noto Sans JP for body text. The plugin sets
|
|
38
|
+
* fontFamily.sans to it; we surface that through our usual
|
|
39
|
+
* --ampless-body-font hook so SiteHeader / footer / prose pick it
|
|
40
|
+
* up consistently. */
|
|
41
|
+
--ampless-body-font: 'Noto Sans JP', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@media (prefers-color-scheme: dark) {
|
|
45
|
+
[data-theme='dads'] {
|
|
46
|
+
/* DADS doesn't ship an official dark palette as of v0.3.4; this
|
|
47
|
+
* is an inverted approximation. When DADS adds a dark variant,
|
|
48
|
+
* bind these to the corresponding plugin variables. */
|
|
49
|
+
--background: #1a1a1c;
|
|
50
|
+
--foreground: #f0f0f0;
|
|
51
|
+
--card: #232327;
|
|
52
|
+
--card-foreground: #f0f0f0;
|
|
53
|
+
--primary: var(--color-blue-300, #9db7f9);
|
|
54
|
+
--primary-foreground: #1a1a1c;
|
|
55
|
+
--secondary: #2c2f3a;
|
|
56
|
+
--secondary-foreground: #f0f0f0;
|
|
57
|
+
--muted: #28282c;
|
|
58
|
+
--muted-foreground: #b3b3b3;
|
|
59
|
+
--accent: #2c2f3a;
|
|
60
|
+
--accent-foreground: #f0f0f0;
|
|
61
|
+
--destructive: var(--color-red-700, #fa0000);
|
|
62
|
+
--destructive-foreground: #ffffff;
|
|
63
|
+
--border: #3a3a3f;
|
|
64
|
+
--input: #3a3a3f;
|
|
65
|
+
--ring: var(--color-blue-300, #9db7f9);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# {{siteName}}
|
|
2
|
+
|
|
3
|
+
Docs theme: sidebar-led documentation layout. The sidebar combines plain links with **tag-driven sections** — entering a URL like `tag:guide` in the sidebar nav field auto-expands to a list of every published post tagged "guide". Lets writers organize content by tag and have it appear in nav automatically.
|
|
4
|
+
|
|
5
|
+
## Customizing
|
|
6
|
+
|
|
7
|
+
In `/admin/sites/<siteId>/theme`:
|
|
8
|
+
|
|
9
|
+
- **Sidebar navigation** — each row is `Label` + `URL`. The URL can be:
|
|
10
|
+
- a path (`/getting-started`)
|
|
11
|
+
- an external URL (`https://...`)
|
|
12
|
+
- a tag reference (`tag:tutorials`) → renders as a heading + list of tagged posts
|
|
13
|
+
- Header navigation (top-level links)
|
|
14
|
+
- Code font (system mono / JetBrains Mono / etc.)
|
|
15
|
+
- Primary color, corner radius
|
|
16
|
+
|
|
17
|
+
## Authoring tip
|
|
18
|
+
|
|
19
|
+
Tag a post `guide` (in the post editor) and add a sidebar row with URL `tag:guide`. The sidebar will list that post automatically — no manual link editing every time you publish.
|
|
20
|
+
|
|
21
|
+
## Getting started
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install
|
|
25
|
+
npx ampx sandbox
|
|
26
|
+
npm run dev
|
|
27
|
+
```
|