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,25 @@
|
|
|
1
|
+
import { defineThemeModule } from 'ampless'
|
|
2
|
+
import './tokens.css'
|
|
3
|
+
import manifest from './manifest'
|
|
4
|
+
import DocsHome from './pages/home'
|
|
5
|
+
import DocsPost, { generatePostMetadata } from './pages/post'
|
|
6
|
+
import DocsTag from './pages/tag'
|
|
7
|
+
import { docsFeedHandler } from './pages/feed'
|
|
8
|
+
import { docsSitemapHandler } from './pages/sitemap'
|
|
9
|
+
|
|
10
|
+
export default defineThemeModule({
|
|
11
|
+
name: 'docs',
|
|
12
|
+
manifest,
|
|
13
|
+
components: {
|
|
14
|
+
Home: DocsHome,
|
|
15
|
+
Post: DocsPost,
|
|
16
|
+
Tag: DocsTag,
|
|
17
|
+
},
|
|
18
|
+
metadata: {
|
|
19
|
+
Post: generatePostMetadata,
|
|
20
|
+
},
|
|
21
|
+
routes: {
|
|
22
|
+
feed: docsFeedHandler,
|
|
23
|
+
sitemap: docsSitemapHandler,
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { defineTheme } from 'ampless'
|
|
2
|
+
|
|
3
|
+
// Docs theme manifest. Sidebar-led layout where the sidebar can mix
|
|
4
|
+
// plain links with tag-driven sections — `tag:guide` expands to a
|
|
5
|
+
// list of every published post tagged "guide". Lets users organize
|
|
6
|
+
// content by tag and have it appear automatically in the nav.
|
|
7
|
+
export default defineTheme({
|
|
8
|
+
name: 'docs',
|
|
9
|
+
label: { en: 'Docs', ja: 'ドキュメント' },
|
|
10
|
+
description: {
|
|
11
|
+
en: 'Sidebar-led documentation layout. Sidebar entries can be plain links or `tag:<name>` to auto-expand into a post list.',
|
|
12
|
+
ja: 'サイドバー型のドキュメントレイアウト。サイドバー項目は通常のリンクか、`tag:<name>` 指定でそのタグの記事一覧に自動展開可能。',
|
|
13
|
+
},
|
|
14
|
+
fields: [
|
|
15
|
+
{
|
|
16
|
+
key: 'primary',
|
|
17
|
+
label: { en: 'Primary color', ja: 'プライマリカラー' },
|
|
18
|
+
group: { en: 'Colors', ja: 'カラー' },
|
|
19
|
+
type: 'color',
|
|
20
|
+
default: 'oklch(0.5 0.18 280)',
|
|
21
|
+
cssVar: '--primary',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
key: 'radius',
|
|
25
|
+
label: { en: 'Corner radius', ja: '角丸' },
|
|
26
|
+
group: { en: 'Shape', ja: '形状' },
|
|
27
|
+
type: 'length',
|
|
28
|
+
default: '0.25rem',
|
|
29
|
+
cssVar: '--radius',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: 'codeFont',
|
|
33
|
+
label: { en: 'Code font', ja: 'コードフォント' },
|
|
34
|
+
group: { en: 'Typography', ja: 'タイポグラフィ' },
|
|
35
|
+
type: 'fontFamily',
|
|
36
|
+
default: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
37
|
+
cssVar: '--ampless-code-font',
|
|
38
|
+
options: [
|
|
39
|
+
{
|
|
40
|
+
value: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
41
|
+
label: { en: 'System monospace', ja: 'システム等幅' },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
value: '"JetBrains Mono", ui-monospace, monospace',
|
|
45
|
+
label: { en: 'JetBrains Mono', ja: 'JetBrains Mono' },
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: 'sidebarNav',
|
|
51
|
+
label: { en: 'Sidebar navigation', ja: 'サイドバーナビ' },
|
|
52
|
+
group: { en: 'Navigation', ja: 'ナビゲーション' },
|
|
53
|
+
type: 'linkList',
|
|
54
|
+
default: [],
|
|
55
|
+
maxItems: 30,
|
|
56
|
+
description: {
|
|
57
|
+
en: 'Each entry is a plain link or `tag:<name>` (auto-lists posts with that tag).',
|
|
58
|
+
ja: '通常のリンクか、`tag:<name>` 指定でそのタグの記事一覧を自動展開。',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: 'logoUrl',
|
|
63
|
+
label: { en: 'Logo image URL', ja: 'ロゴ画像 URL' },
|
|
64
|
+
group: { en: 'Branding', ja: 'ブランディング' },
|
|
65
|
+
type: 'image',
|
|
66
|
+
default: '',
|
|
67
|
+
description: {
|
|
68
|
+
en: 'URL or media path. Empty falls back to the site name as text.',
|
|
69
|
+
ja: '画像 URL またはメディアパス。空欄ならサイト名がテキスト表示されます。',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: 'headerNav',
|
|
74
|
+
label: { en: 'Header navigation', ja: 'ヘッダーナビ' },
|
|
75
|
+
group: { en: 'Navigation', ja: 'ナビゲーション' },
|
|
76
|
+
type: 'linkList',
|
|
77
|
+
default: [],
|
|
78
|
+
maxItems: 6,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: 'footerLinks',
|
|
82
|
+
label: { en: 'Footer links', ja: 'フッターリンク' },
|
|
83
|
+
group: { en: 'Navigation', ja: 'ナビゲーション' },
|
|
84
|
+
type: 'linkList',
|
|
85
|
+
default: [],
|
|
86
|
+
maxItems: 12,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
})
|
|
@@ -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 docsFeedHandler({ 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,88 @@
|
|
|
1
|
+
import Link from 'next/link'
|
|
2
|
+
import { formatDate, type ThemeRouteContext } from 'ampless'
|
|
3
|
+
import { listPublishedPosts } from '@/lib/posts-public'
|
|
4
|
+
import { loadSiteSettings } from '@/lib/site-settings'
|
|
5
|
+
import { loadThemeConfig } from '@/lib/theme-config'
|
|
6
|
+
import { SiteHeader } from '@/components/site-chrome/site-header'
|
|
7
|
+
import { SiteSidebar } from '@/components/site-chrome/site-sidebar'
|
|
8
|
+
import { SiteFooter } from '@/components/site-chrome/site-footer'
|
|
9
|
+
import { CollapsibleSidebar } from '@/components/site-chrome/collapsible-sidebar'
|
|
10
|
+
|
|
11
|
+
// Docs home: sidebar nav on the left (with optional tag-driven
|
|
12
|
+
// sections), latest posts list on the right. Acts as the docs landing
|
|
13
|
+
// page until the user arranges static pages.
|
|
14
|
+
export default async function DocsHome({ 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: 12 }),
|
|
20
|
+
])
|
|
21
|
+
const posts = postsResult.items
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<SiteHeader
|
|
26
|
+
links={theme.values.headerNav}
|
|
27
|
+
logoUrl={theme.values.logoUrl}
|
|
28
|
+
siteName={settings.site.name}
|
|
29
|
+
brandClassName="font-mono text-sm font-semibold tracking-tight"
|
|
30
|
+
/>
|
|
31
|
+
|
|
32
|
+
<div className="mx-auto grid max-w-6xl gap-6 px-6 py-10 lg:grid-cols-[15rem_1fr] lg:gap-10">
|
|
33
|
+
<CollapsibleSidebar className="lg:sticky lg:top-6 lg:self-start">
|
|
34
|
+
<SiteSidebar links={theme.values.sidebarNav} siteId={siteId} />
|
|
35
|
+
</CollapsibleSidebar>
|
|
36
|
+
|
|
37
|
+
<main className="min-w-0">
|
|
38
|
+
<header className="mb-10">
|
|
39
|
+
<h1 className="text-3xl font-bold tracking-tight">{settings.site.name}</h1>
|
|
40
|
+
{settings.site.description && (
|
|
41
|
+
<p className="mt-2 text-[var(--muted-foreground)]">{settings.site.description}</p>
|
|
42
|
+
)}
|
|
43
|
+
</header>
|
|
44
|
+
|
|
45
|
+
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wide text-[var(--muted-foreground)]">
|
|
46
|
+
Recently updated
|
|
47
|
+
</h2>
|
|
48
|
+
{posts.length === 0 ? (
|
|
49
|
+
<p className="text-sm text-[var(--muted-foreground)]">No posts published yet.</p>
|
|
50
|
+
) : (
|
|
51
|
+
<ul className="space-y-3">
|
|
52
|
+
{posts.map((post) => (
|
|
53
|
+
<li key={post.postId}>
|
|
54
|
+
<Link
|
|
55
|
+
href={`/${post.slug}`}
|
|
56
|
+
className="block rounded-[var(--radius)] border bg-[var(--card)] p-4 transition hover:border-[var(--primary)]"
|
|
57
|
+
>
|
|
58
|
+
<div className="font-medium">{post.title}</div>
|
|
59
|
+
{post.publishedAt && (
|
|
60
|
+
<time
|
|
61
|
+
dateTime={post.publishedAt}
|
|
62
|
+
className="mt-1 block font-mono text-xs text-[var(--muted-foreground)]"
|
|
63
|
+
>
|
|
64
|
+
{formatDate(post.publishedAt, settings.dateFormat, settings.timezone)}
|
|
65
|
+
</time>
|
|
66
|
+
)}
|
|
67
|
+
{post.excerpt && (
|
|
68
|
+
<p className="mt-2 text-sm text-[var(--muted-foreground)]">{post.excerpt}</p>
|
|
69
|
+
)}
|
|
70
|
+
</Link>
|
|
71
|
+
</li>
|
|
72
|
+
))}
|
|
73
|
+
</ul>
|
|
74
|
+
)}
|
|
75
|
+
</main>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<SiteFooter
|
|
79
|
+
links={theme.values.footerLinks}
|
|
80
|
+
legend={
|
|
81
|
+
<span>
|
|
82
|
+
© {new Date().getFullYear()} {settings.site.name}
|
|
83
|
+
</span>
|
|
84
|
+
}
|
|
85
|
+
/>
|
|
86
|
+
</>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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 { SiteSidebar } from '@/components/site-chrome/site-sidebar'
|
|
14
|
+
import { SiteFooter } from '@/components/site-chrome/site-footer'
|
|
15
|
+
import { CollapsibleSidebar } from '@/components/site-chrome/collapsible-sidebar'
|
|
16
|
+
|
|
17
|
+
type PostCtx = ThemeRouteContext<{ slug: string }>
|
|
18
|
+
|
|
19
|
+
export async function generatePostMetadata({ params }: PostCtx): Promise<Metadata> {
|
|
20
|
+
const { siteId, slug } = await params
|
|
21
|
+
const post = await getPublishedPost(slug, { siteId })
|
|
22
|
+
if (!post) return {}
|
|
23
|
+
return postMetadata(post, siteId)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Docs post page: sidebar always visible while reading. The sidebar
|
|
27
|
+
// re-uses theme.sidebarNav, so navigation context stays consistent
|
|
28
|
+
// across the home page and individual articles.
|
|
29
|
+
export default async function DocsPost({ params }: PostCtx) {
|
|
30
|
+
const { siteId, slug } = await params
|
|
31
|
+
const [post, settings, theme] = await Promise.all([
|
|
32
|
+
getPublishedPost(slug, { siteId }),
|
|
33
|
+
loadSiteSettings(siteId),
|
|
34
|
+
loadThemeConfig(siteId),
|
|
35
|
+
])
|
|
36
|
+
if (!post) notFound()
|
|
37
|
+
|
|
38
|
+
const defaultLightbox = settings.media.imageDisplay === 'lightbox'
|
|
39
|
+
const maxWidth = settings.media.imageMaxWidth ?? '100%'
|
|
40
|
+
const proseStyle: React.CSSProperties = {
|
|
41
|
+
['--ampless-img-max-width' as string]: maxWidth,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<SiteHeader
|
|
47
|
+
links={theme.values.headerNav}
|
|
48
|
+
logoUrl={theme.values.logoUrl}
|
|
49
|
+
siteName={settings.site.name}
|
|
50
|
+
brandClassName="font-mono text-sm font-semibold tracking-tight"
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
<div className="mx-auto grid max-w-6xl gap-6 px-6 py-10 lg:grid-cols-[15rem_1fr] lg:gap-10">
|
|
54
|
+
<CollapsibleSidebar className="lg:sticky lg:top-6 lg:self-start">
|
|
55
|
+
<SiteSidebar links={theme.values.sidebarNav} siteId={siteId} />
|
|
56
|
+
</CollapsibleSidebar>
|
|
57
|
+
|
|
58
|
+
<main className="min-w-0">
|
|
59
|
+
<article>
|
|
60
|
+
<header className="mb-8 border-b pb-6">
|
|
61
|
+
<h1 className="text-3xl font-bold tracking-tight">{post.title}</h1>
|
|
62
|
+
{post.publishedAt && (
|
|
63
|
+
<time
|
|
64
|
+
dateTime={post.publishedAt}
|
|
65
|
+
className="mt-2 block font-mono text-xs text-[var(--muted-foreground)]"
|
|
66
|
+
>
|
|
67
|
+
{formatDate(post.publishedAt, settings.dateFormat, settings.timezone)}
|
|
68
|
+
</time>
|
|
69
|
+
)}
|
|
70
|
+
</header>
|
|
71
|
+
|
|
72
|
+
<div
|
|
73
|
+
id="post-body"
|
|
74
|
+
className="prose prose-neutral dark:prose-invert max-w-none [&_img]:max-w-[var(--ampless-img-max-width)] [&_img]:mx-auto"
|
|
75
|
+
style={proseStyle}
|
|
76
|
+
dangerouslySetInnerHTML={{ __html: renderBody(post) }}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
<TagList tags={post.tags} className="mt-10 border-t pt-6" />
|
|
80
|
+
</article>
|
|
81
|
+
</main>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<SiteFooter
|
|
85
|
+
links={theme.values.footerLinks}
|
|
86
|
+
legend={
|
|
87
|
+
<span>
|
|
88
|
+
© {new Date().getFullYear()} {settings.site.name}
|
|
89
|
+
</span>
|
|
90
|
+
}
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
<LightboxBinder scopeSelector="#post-body" defaultLightbox={defaultLightbox} />
|
|
94
|
+
</>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -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 docsSitemapHandler({ 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,79 @@
|
|
|
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 { SiteSidebar } from '@/components/site-chrome/site-sidebar'
|
|
9
|
+
import { SiteFooter } from '@/components/site-chrome/site-footer'
|
|
10
|
+
import { CollapsibleSidebar } from '@/components/site-chrome/collapsible-sidebar'
|
|
11
|
+
import { t } from '@/lib/i18n'
|
|
12
|
+
|
|
13
|
+
export default async function DocsTag({ params }: ThemeRouteContext<{ tag: string }>) {
|
|
14
|
+
const { siteId, tag } = await params
|
|
15
|
+
const decodedTag = decodeURIComponent(tag)
|
|
16
|
+
const [{ items: posts }, settings, theme] = await Promise.all([
|
|
17
|
+
listPostsByTag(decodedTag, { siteId, limit: 50 }),
|
|
18
|
+
loadSiteSettings(siteId),
|
|
19
|
+
loadThemeConfig(siteId),
|
|
20
|
+
])
|
|
21
|
+
if (posts.length === 0) notFound()
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<SiteHeader
|
|
26
|
+
links={theme.values.headerNav}
|
|
27
|
+
logoUrl={theme.values.logoUrl}
|
|
28
|
+
siteName={settings.site.name}
|
|
29
|
+
brandClassName="font-mono text-sm font-semibold tracking-tight"
|
|
30
|
+
/>
|
|
31
|
+
|
|
32
|
+
<div className="mx-auto grid max-w-6xl gap-6 px-6 py-10 lg:grid-cols-[15rem_1fr] lg:gap-10">
|
|
33
|
+
<CollapsibleSidebar className="lg:sticky lg:top-6 lg:self-start">
|
|
34
|
+
<SiteSidebar links={theme.values.sidebarNav} siteId={siteId} />
|
|
35
|
+
</CollapsibleSidebar>
|
|
36
|
+
|
|
37
|
+
<main className="min-w-0">
|
|
38
|
+
<header className="mb-10">
|
|
39
|
+
<p className="text-sm text-[var(--muted-foreground)]">{t('public.tagLabel')}</p>
|
|
40
|
+
<h1 className="text-3xl font-bold tracking-tight">#{decodedTag}</h1>
|
|
41
|
+
</header>
|
|
42
|
+
|
|
43
|
+
<ul className="space-y-3">
|
|
44
|
+
{posts.map((post) => (
|
|
45
|
+
<li key={post.postId}>
|
|
46
|
+
<Link
|
|
47
|
+
href={`/${post.slug}`}
|
|
48
|
+
className="block rounded-[var(--radius)] border bg-[var(--card)] p-4 transition hover:border-[var(--primary)]"
|
|
49
|
+
>
|
|
50
|
+
<div className="font-medium">{post.title}</div>
|
|
51
|
+
{post.publishedAt && (
|
|
52
|
+
<time
|
|
53
|
+
dateTime={post.publishedAt}
|
|
54
|
+
className="mt-1 block font-mono text-xs text-[var(--muted-foreground)]"
|
|
55
|
+
>
|
|
56
|
+
{formatDate(post.publishedAt, settings.dateFormat, settings.timezone)}
|
|
57
|
+
</time>
|
|
58
|
+
)}
|
|
59
|
+
{post.excerpt && (
|
|
60
|
+
<p className="mt-2 text-sm text-[var(--muted-foreground)]">{post.excerpt}</p>
|
|
61
|
+
)}
|
|
62
|
+
</Link>
|
|
63
|
+
</li>
|
|
64
|
+
))}
|
|
65
|
+
</ul>
|
|
66
|
+
</main>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<SiteFooter
|
|
70
|
+
links={theme.values.footerLinks}
|
|
71
|
+
legend={
|
|
72
|
+
<span>
|
|
73
|
+
© {new Date().getFullYear()} {settings.site.name}
|
|
74
|
+
</span>
|
|
75
|
+
}
|
|
76
|
+
/>
|
|
77
|
+
</>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* Docs theme tokens — purple accent on near-white. Crisp contrast,
|
|
2
|
+
* tighter radius, monospace-leaning code styling. */
|
|
3
|
+
|
|
4
|
+
[data-theme='docs'] {
|
|
5
|
+
--background: oklch(1 0 0);
|
|
6
|
+
--foreground: oklch(0.18 0.02 280);
|
|
7
|
+
--card: oklch(0.99 0 0);
|
|
8
|
+
--card-foreground: oklch(0.18 0.02 280);
|
|
9
|
+
--primary: oklch(0.5 0.18 280);
|
|
10
|
+
--primary-foreground: oklch(0.99 0 0);
|
|
11
|
+
--secondary: oklch(0.96 0.01 280);
|
|
12
|
+
--secondary-foreground: oklch(0.25 0.05 280);
|
|
13
|
+
--muted: oklch(0.97 0.005 280);
|
|
14
|
+
--muted-foreground: oklch(0.5 0.02 280);
|
|
15
|
+
--accent: oklch(0.95 0.04 280);
|
|
16
|
+
--accent-foreground: oklch(0.3 0.1 280);
|
|
17
|
+
--destructive: oklch(0.55 0.22 25);
|
|
18
|
+
--destructive-foreground: oklch(0.99 0 0);
|
|
19
|
+
--border: oklch(0.9 0.005 280);
|
|
20
|
+
--input: oklch(0.9 0.005 280);
|
|
21
|
+
--ring: oklch(0.5 0.18 280);
|
|
22
|
+
--radius: 0.25rem;
|
|
23
|
+
--ampless-body-font: system-ui, -apple-system, sans-serif;
|
|
24
|
+
--ampless-code-font: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@media (prefers-color-scheme: dark) {
|
|
28
|
+
[data-theme='docs'] {
|
|
29
|
+
--background: oklch(0.16 0.02 280);
|
|
30
|
+
--foreground: oklch(0.96 0.005 280);
|
|
31
|
+
--card: oklch(0.2 0.02 280);
|
|
32
|
+
--card-foreground: oklch(0.96 0.005 280);
|
|
33
|
+
--primary: oklch(0.72 0.16 280);
|
|
34
|
+
--primary-foreground: oklch(0.16 0.02 280);
|
|
35
|
+
--secondary: oklch(0.28 0.03 280);
|
|
36
|
+
--secondary-foreground: oklch(0.96 0.005 280);
|
|
37
|
+
--muted: oklch(0.24 0.02 280);
|
|
38
|
+
--muted-foreground: oklch(0.7 0.02 280);
|
|
39
|
+
--accent: oklch(0.32 0.06 280);
|
|
40
|
+
--accent-foreground: oklch(0.96 0.005 280);
|
|
41
|
+
--destructive: oklch(0.55 0.22 25);
|
|
42
|
+
--destructive-foreground: oklch(0.99 0 0);
|
|
43
|
+
--border: oklch(0.32 0.02 280);
|
|
44
|
+
--input: oklch(0.32 0.02 280);
|
|
45
|
+
--ring: oklch(0.72 0.16 280);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Code font scope: prose code blocks under the docs theme use the
|
|
50
|
+
* configurable --ampless-code-font variable. The body font remains the
|
|
51
|
+
* theme-configured --ampless-body-font (or system fallback). */
|
|
52
|
+
[data-theme='docs'] .prose code,
|
|
53
|
+
[data-theme='docs'] .prose pre {
|
|
54
|
+
font-family: var(--ampless-code-font);
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# {{siteName}}
|
|
2
|
+
|
|
3
|
+
A site powered by [ampless](https://github.com/heavymoons/ampless), using the **Landing** theme — hero-led single-page layout with optional "Latest" post grid, configurable header / footer nav, and a warm-coral accent palette.
|
|
4
|
+
|
|
5
|
+
## Customizing
|
|
6
|
+
|
|
7
|
+
In `/admin/sites/<siteId>/theme`:
|
|
8
|
+
|
|
9
|
+
- Hero headline / subheadline / CTA button text + URL
|
|
10
|
+
- Header navigation (label + URL pairs)
|
|
11
|
+
- Footer links
|
|
12
|
+
- Primary color
|
|
13
|
+
- Corner radius
|
|
14
|
+
|
|
15
|
+
Empty hero fields fall back to the site name / description from `/admin/sites/<siteId>`.
|
|
16
|
+
|
|
17
|
+
## Getting started
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install
|
|
21
|
+
npx ampx sandbox # provision the AWS backend
|
|
22
|
+
npm run dev # start Next.js
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
See the project README for full setup.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineThemeModule } from 'ampless'
|
|
2
|
+
import './tokens.css'
|
|
3
|
+
import manifest from './manifest'
|
|
4
|
+
import LandingHome from './pages/home'
|
|
5
|
+
import LandingPost, { generatePostMetadata } from './pages/post'
|
|
6
|
+
import LandingTag from './pages/tag'
|
|
7
|
+
import { landingFeedHandler } from './pages/feed'
|
|
8
|
+
import { landingSitemapHandler } from './pages/sitemap'
|
|
9
|
+
|
|
10
|
+
export default defineThemeModule({
|
|
11
|
+
name: 'landing',
|
|
12
|
+
manifest,
|
|
13
|
+
components: {
|
|
14
|
+
Home: LandingHome,
|
|
15
|
+
Post: LandingPost,
|
|
16
|
+
Tag: LandingTag,
|
|
17
|
+
},
|
|
18
|
+
metadata: {
|
|
19
|
+
Post: generatePostMetadata,
|
|
20
|
+
},
|
|
21
|
+
routes: {
|
|
22
|
+
feed: landingFeedHandler,
|
|
23
|
+
sitemap: landingSitemapHandler,
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { defineTheme } from 'ampless'
|
|
2
|
+
|
|
3
|
+
// Landing theme manifest. The home page is hero-led; posts (if any)
|
|
4
|
+
// show up as a "latest" section beneath. Most fields are content
|
|
5
|
+
// rather than chrome — admins fill in the marketing copy.
|
|
6
|
+
export default defineTheme({
|
|
7
|
+
name: 'landing',
|
|
8
|
+
label: { en: 'Landing', ja: 'ランディング' },
|
|
9
|
+
description: {
|
|
10
|
+
en: 'Single-page hero focus, optional features and post list.',
|
|
11
|
+
ja: '1 ページ完結型のヒーロー中心レイアウト。お知らせ一覧も併設可能。',
|
|
12
|
+
},
|
|
13
|
+
fields: [
|
|
14
|
+
{
|
|
15
|
+
key: 'heroHeadline',
|
|
16
|
+
label: { en: 'Hero headline', ja: 'ヒーロー見出し' },
|
|
17
|
+
group: { en: 'Hero', ja: 'ヒーロー' },
|
|
18
|
+
type: 'text',
|
|
19
|
+
default: '',
|
|
20
|
+
maxLength: 120,
|
|
21
|
+
description: {
|
|
22
|
+
en: 'Empty falls back to the site name.',
|
|
23
|
+
ja: '空欄の場合はサイト名を使用。',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: 'heroSubheadline',
|
|
28
|
+
label: { en: 'Hero subheadline', ja: 'ヒーローサブ見出し' },
|
|
29
|
+
group: { en: 'Hero', ja: 'ヒーロー' },
|
|
30
|
+
type: 'text',
|
|
31
|
+
default: '',
|
|
32
|
+
maxLength: 200,
|
|
33
|
+
description: {
|
|
34
|
+
en: 'Empty falls back to the site description.',
|
|
35
|
+
ja: '空欄の場合はサイトの説明を使用。',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: 'ctaText',
|
|
40
|
+
label: { en: 'CTA button text', ja: 'CTA ボタンのテキスト' },
|
|
41
|
+
group: { en: 'Hero', ja: 'ヒーロー' },
|
|
42
|
+
type: 'text',
|
|
43
|
+
default: '',
|
|
44
|
+
maxLength: 40,
|
|
45
|
+
description: {
|
|
46
|
+
en: 'Leave empty to hide the call-to-action button.',
|
|
47
|
+
ja: '空欄にするとボタンを非表示。',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: 'ctaUrl',
|
|
52
|
+
label: { en: 'CTA URL', ja: 'CTA リンク先' },
|
|
53
|
+
group: { en: 'Hero', ja: 'ヒーロー' },
|
|
54
|
+
type: 'text',
|
|
55
|
+
default: '#',
|
|
56
|
+
maxLength: 200,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: 'primary',
|
|
60
|
+
label: { en: 'Primary color', ja: 'プライマリカラー' },
|
|
61
|
+
group: { en: 'Colors', ja: 'カラー' },
|
|
62
|
+
type: 'color',
|
|
63
|
+
default: 'oklch(0.6 0.18 35)',
|
|
64
|
+
cssVar: '--primary',
|
|
65
|
+
description: {
|
|
66
|
+
en: 'Hero accent and CTA button background.',
|
|
67
|
+
ja: 'ヒーロー強調色と CTA ボタンの背景色。',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: 'radius',
|
|
72
|
+
label: { en: 'Corner radius', ja: '角丸' },
|
|
73
|
+
group: { en: 'Shape', ja: '形状' },
|
|
74
|
+
type: 'length',
|
|
75
|
+
default: '0.75rem',
|
|
76
|
+
cssVar: '--radius',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: 'featuredSlug',
|
|
80
|
+
label: { en: 'Featured post slug', ja: 'フィーチャー記事のスラッグ' },
|
|
81
|
+
group: { en: 'Hero', ja: 'ヒーロー' },
|
|
82
|
+
type: 'text',
|
|
83
|
+
default: '',
|
|
84
|
+
maxLength: 200,
|
|
85
|
+
description: {
|
|
86
|
+
en: 'Slug of a published post to embed below the hero. Useful for "About" or "Welcome" content. Empty disables the section.',
|
|
87
|
+
ja: 'ヒーロー下に本文を埋め込みたい公開記事のスラッグ。「About」「Welcome」的なコンテンツ向け。空なら非表示。',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: 'logoUrl',
|
|
92
|
+
label: { en: 'Logo image URL', ja: 'ロゴ画像 URL' },
|
|
93
|
+
group: { en: 'Branding', ja: 'ブランディング' },
|
|
94
|
+
type: 'image',
|
|
95
|
+
default: '',
|
|
96
|
+
description: {
|
|
97
|
+
en: 'URL or media path. Empty falls back to the site name as text.',
|
|
98
|
+
ja: '画像 URL またはメディアパス。空欄ならサイト名がテキスト表示されます。',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
key: 'headerNav',
|
|
103
|
+
label: { en: 'Header navigation', ja: 'ヘッダーナビ' },
|
|
104
|
+
group: { en: 'Navigation', ja: 'ナビゲーション' },
|
|
105
|
+
type: 'linkList',
|
|
106
|
+
default: [],
|
|
107
|
+
maxItems: 8,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
key: 'footerLinks',
|
|
111
|
+
label: { en: 'Footer links', ja: 'フッターリンク' },
|
|
112
|
+
group: { en: 'Navigation', ja: 'ナビゲーション' },
|
|
113
|
+
type: 'linkList',
|
|
114
|
+
default: [],
|
|
115
|
+
maxItems: 12,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
})
|