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,76 @@
|
|
|
1
|
+
import Link from 'next/link'
|
|
2
|
+
import { notFound } from 'next/navigation'
|
|
3
|
+
import { formatDate, parseLinkList, 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 BlogTag({ 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
|
+
|
|
20
|
+
if (posts.length === 0) notFound()
|
|
21
|
+
|
|
22
|
+
const showHeader =
|
|
23
|
+
parseLinkList(theme.values.headerNav).length > 0 || !!theme.values.logoUrl?.trim()
|
|
24
|
+
const showFooter = parseLinkList(theme.values.footerLinks).length > 0
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
{showHeader && (
|
|
29
|
+
<SiteHeader
|
|
30
|
+
links={theme.values.headerNav}
|
|
31
|
+
logoUrl={theme.values.logoUrl}
|
|
32
|
+
siteName={settings.site.name}
|
|
33
|
+
brandClassName="font-semibold hover:underline"
|
|
34
|
+
/>
|
|
35
|
+
)}
|
|
36
|
+
|
|
37
|
+
<main className="mx-auto max-w-2xl px-6 py-12">
|
|
38
|
+
<nav className="mb-8">
|
|
39
|
+
<Link href="/" className="text-sm text-gray-500 hover:underline">{t('public.home')}</Link>
|
|
40
|
+
</nav>
|
|
41
|
+
|
|
42
|
+
<header className="mb-12 border-b pb-6">
|
|
43
|
+
<p className="text-sm text-gray-500">{t('public.tagLabel')}</p>
|
|
44
|
+
<h1 className="text-4xl font-bold tracking-tight">#{decodedTag}</h1>
|
|
45
|
+
</header>
|
|
46
|
+
|
|
47
|
+
<ul className="space-y-8">
|
|
48
|
+
{posts.map((post) => (
|
|
49
|
+
<li key={post.postId}>
|
|
50
|
+
<Link href={`/${post.slug}`} className="block group">
|
|
51
|
+
<h2 className="text-2xl font-semibold group-hover:underline">{post.title}</h2>
|
|
52
|
+
{post.publishedAt && (
|
|
53
|
+
<time dateTime={post.publishedAt} className="text-sm text-gray-500">
|
|
54
|
+
{formatDate(post.publishedAt, settings.dateFormat, settings.timezone)}
|
|
55
|
+
</time>
|
|
56
|
+
)}
|
|
57
|
+
{post.excerpt && <p className="mt-2 text-gray-700">{post.excerpt}</p>}
|
|
58
|
+
</Link>
|
|
59
|
+
</li>
|
|
60
|
+
))}
|
|
61
|
+
</ul>
|
|
62
|
+
</main>
|
|
63
|
+
|
|
64
|
+
{showFooter && (
|
|
65
|
+
<SiteFooter
|
|
66
|
+
links={theme.values.footerLinks}
|
|
67
|
+
legend={
|
|
68
|
+
<span>
|
|
69
|
+
© {new Date().getFullYear()} {settings.site.name}
|
|
70
|
+
</span>
|
|
71
|
+
}
|
|
72
|
+
/>
|
|
73
|
+
)}
|
|
74
|
+
</>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* Blog theme tokens.
|
|
2
|
+
*
|
|
3
|
+
* Each variable is namespaced under [data-theme="blog"] so that all
|
|
4
|
+
* installed themes can coexist in the bundle and `<body data-theme=...>`
|
|
5
|
+
* picks which set actually wins. The active theme's `theme.manifest.ts`
|
|
6
|
+
* runtime overrides come in as inline `:root { ... }` after this file
|
|
7
|
+
* loads, so `:root` selectors trump scoped attribute selectors of
|
|
8
|
+
* equal specificity. The dispatcher emits `--primary` etc. only for
|
|
9
|
+
* fields that have an override, leaving everything else to fall back
|
|
10
|
+
* to these defaults. */
|
|
11
|
+
|
|
12
|
+
[data-theme='blog'] {
|
|
13
|
+
--background: oklch(1 0 0);
|
|
14
|
+
--foreground: oklch(0.145 0 0);
|
|
15
|
+
--card: oklch(1 0 0);
|
|
16
|
+
--card-foreground: oklch(0.145 0 0);
|
|
17
|
+
--primary: oklch(0.205 0 0);
|
|
18
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
19
|
+
--secondary: oklch(0.97 0 0);
|
|
20
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
21
|
+
--muted: oklch(0.97 0 0);
|
|
22
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
23
|
+
--accent: oklch(0.97 0 0);
|
|
24
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
25
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
26
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
27
|
+
--border: oklch(0.922 0 0);
|
|
28
|
+
--input: oklch(0.922 0 0);
|
|
29
|
+
--ring: oklch(0.708 0 0);
|
|
30
|
+
--radius: 0.5rem;
|
|
31
|
+
--ampless-body-font: system-ui, -apple-system, sans-serif;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@media (prefers-color-scheme: dark) {
|
|
35
|
+
[data-theme='blog'] {
|
|
36
|
+
--background: oklch(0.145 0 0);
|
|
37
|
+
--foreground: oklch(0.985 0 0);
|
|
38
|
+
--card: oklch(0.145 0 0);
|
|
39
|
+
--card-foreground: oklch(0.985 0 0);
|
|
40
|
+
--primary: oklch(0.985 0 0);
|
|
41
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
42
|
+
--secondary: oklch(0.269 0 0);
|
|
43
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
44
|
+
--muted: oklch(0.269 0 0);
|
|
45
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
46
|
+
--accent: oklch(0.269 0 0);
|
|
47
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
48
|
+
--destructive: oklch(0.396 0.141 25.723);
|
|
49
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
50
|
+
--border: oklch(0.269 0 0);
|
|
51
|
+
--input: oklch(0.269 0 0);
|
|
52
|
+
--ring: oklch(0.439 0 0);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# {{siteName}}
|
|
2
|
+
|
|
3
|
+
Corporate / company-site theme: navy on near-white, hero + news layout, header + footer navigation.
|
|
4
|
+
|
|
5
|
+
## Customizing
|
|
6
|
+
|
|
7
|
+
In `/admin/sites/<siteId>/theme`:
|
|
8
|
+
|
|
9
|
+
- Tagline (small line above the site name in the hero)
|
|
10
|
+
- Header navigation (label + URL pairs)
|
|
11
|
+
- Footer links + legend (address / company info / extra small print)
|
|
12
|
+
- Primary color, corner radius
|
|
13
|
+
|
|
14
|
+
## Getting started
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install
|
|
18
|
+
npx ampx sandbox
|
|
19
|
+
npm run dev
|
|
20
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineThemeModule } from 'ampless'
|
|
2
|
+
import './tokens.css'
|
|
3
|
+
import manifest from './manifest'
|
|
4
|
+
import CorporateHome from './pages/home'
|
|
5
|
+
import CorporatePost, { generatePostMetadata } from './pages/post'
|
|
6
|
+
import CorporateTag from './pages/tag'
|
|
7
|
+
import { corporateFeedHandler } from './pages/feed'
|
|
8
|
+
import { corporateSitemapHandler } from './pages/sitemap'
|
|
9
|
+
|
|
10
|
+
export default defineThemeModule({
|
|
11
|
+
name: 'corporate',
|
|
12
|
+
manifest,
|
|
13
|
+
components: {
|
|
14
|
+
Home: CorporateHome,
|
|
15
|
+
Post: CorporatePost,
|
|
16
|
+
Tag: CorporateTag,
|
|
17
|
+
},
|
|
18
|
+
metadata: {
|
|
19
|
+
Post: generatePostMetadata,
|
|
20
|
+
},
|
|
21
|
+
routes: {
|
|
22
|
+
feed: corporateFeedHandler,
|
|
23
|
+
sitemap: corporateSitemapHandler,
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { defineTheme } from 'ampless'
|
|
2
|
+
|
|
3
|
+
// Corporate theme manifest. Conservative blue/slate palette with
|
|
4
|
+
// header + footer nav, an optional tagline, and a "news" section
|
|
5
|
+
// driven by published posts.
|
|
6
|
+
export default defineTheme({
|
|
7
|
+
name: 'corporate',
|
|
8
|
+
label: { en: 'Corporate', ja: 'コーポレート' },
|
|
9
|
+
description: {
|
|
10
|
+
en: 'Conservative business / company-site layout with hero and news section.',
|
|
11
|
+
ja: '企業サイト向けの落ち着いたレイアウト。ヒーローとお知らせ一覧を併設。',
|
|
12
|
+
},
|
|
13
|
+
fields: [
|
|
14
|
+
{
|
|
15
|
+
key: 'tagline',
|
|
16
|
+
label: { en: 'Tagline', ja: 'タグライン' },
|
|
17
|
+
group: { en: 'Hero', ja: 'ヒーロー' },
|
|
18
|
+
type: 'text',
|
|
19
|
+
default: '',
|
|
20
|
+
maxLength: 120,
|
|
21
|
+
description: {
|
|
22
|
+
en: 'Short phrase shown above the site name in the hero.',
|
|
23
|
+
ja: 'ヒーロー内、サイト名の上に表示される短いフレーズ。',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: 'primary',
|
|
28
|
+
label: { en: 'Primary color', ja: 'プライマリカラー' },
|
|
29
|
+
group: { en: 'Colors', ja: 'カラー' },
|
|
30
|
+
type: 'color',
|
|
31
|
+
default: 'oklch(0.4 0.13 250)',
|
|
32
|
+
cssVar: '--primary',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'radius',
|
|
36
|
+
label: { en: 'Corner radius', ja: '角丸' },
|
|
37
|
+
group: { en: 'Shape', ja: '形状' },
|
|
38
|
+
type: 'length',
|
|
39
|
+
default: '0.25rem',
|
|
40
|
+
cssVar: '--radius',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: 'featuredSlug',
|
|
44
|
+
label: { en: 'Top story slug', ja: 'トップストーリーのスラッグ' },
|
|
45
|
+
group: { en: 'Hero', ja: 'ヒーロー' },
|
|
46
|
+
type: 'text',
|
|
47
|
+
default: '',
|
|
48
|
+
maxLength: 200,
|
|
49
|
+
description: {
|
|
50
|
+
en: 'Slug of a published post to feature between the hero and the news list. Empty disables.',
|
|
51
|
+
ja: 'ヒーローとニュース一覧の間に載せたい公開記事のスラッグ。空なら非表示。',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: 'logoUrl',
|
|
56
|
+
label: { en: 'Logo image URL', ja: 'ロゴ画像 URL' },
|
|
57
|
+
group: { en: 'Branding', ja: 'ブランディング' },
|
|
58
|
+
type: 'image',
|
|
59
|
+
default: '',
|
|
60
|
+
description: {
|
|
61
|
+
en: 'URL or media path. Empty falls back to the site name as text.',
|
|
62
|
+
ja: '画像 URL またはメディアパス。空欄ならサイト名がテキスト表示されます。',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: 'headerNav',
|
|
67
|
+
label: { en: 'Header navigation', ja: 'ヘッダーナビ' },
|
|
68
|
+
group: { en: 'Navigation', ja: 'ナビゲーション' },
|
|
69
|
+
type: 'linkList',
|
|
70
|
+
default: [],
|
|
71
|
+
maxItems: 8,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
key: 'footerLinks',
|
|
75
|
+
label: { en: 'Footer links', ja: 'フッターリンク' },
|
|
76
|
+
group: { en: 'Navigation', ja: 'ナビゲーション' },
|
|
77
|
+
type: 'linkList',
|
|
78
|
+
default: [],
|
|
79
|
+
maxItems: 16,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: 'footerLegend',
|
|
83
|
+
label: { en: 'Footer legend', ja: 'フッター注記' },
|
|
84
|
+
group: { en: 'Navigation', ja: 'ナビゲーション' },
|
|
85
|
+
type: 'text',
|
|
86
|
+
default: '',
|
|
87
|
+
maxLength: 200,
|
|
88
|
+
description: {
|
|
89
|
+
en: 'Address / company info / extra small print below footer links.',
|
|
90
|
+
ja: '住所や会社情報、フッター下部の細字。',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
})
|
|
@@ -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 corporateFeedHandler({ 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,130 @@
|
|
|
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
|
+
export default async function CorporateHome({ params }: ThemeRouteContext) {
|
|
11
|
+
const { siteId } = await params
|
|
12
|
+
const [settings, theme, postsResult] = await Promise.all([
|
|
13
|
+
loadSiteSettings(siteId),
|
|
14
|
+
loadThemeConfig(siteId),
|
|
15
|
+
listPublishedPosts({ siteId, limit: 8 }),
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
// Top-story embed between hero and news. Filtered out of news to
|
|
19
|
+
// avoid duplication. Skipped silently if missing / unpublished.
|
|
20
|
+
const featuredSlug = theme.values.featuredSlug?.trim()
|
|
21
|
+
const featured = featuredSlug
|
|
22
|
+
? await getPublishedPost(featuredSlug, { siteId })
|
|
23
|
+
: null
|
|
24
|
+
const posts = featured
|
|
25
|
+
? postsResult.items.filter((p) => p.slug !== featured.slug)
|
|
26
|
+
: postsResult.items
|
|
27
|
+
|
|
28
|
+
const tagline = theme.values.tagline?.trim()
|
|
29
|
+
const footerLegend = theme.values.footerLegend?.trim()
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<SiteHeader
|
|
34
|
+
links={theme.values.headerNav}
|
|
35
|
+
logoUrl={theme.values.logoUrl}
|
|
36
|
+
siteName={settings.site.name}
|
|
37
|
+
brandClassName="text-lg font-semibold tracking-tight"
|
|
38
|
+
/>
|
|
39
|
+
|
|
40
|
+
<main>
|
|
41
|
+
<section className="border-b bg-[var(--secondary)] px-6 py-16">
|
|
42
|
+
<div className="mx-auto max-w-4xl">
|
|
43
|
+
{tagline && (
|
|
44
|
+
<p className="text-sm font-medium uppercase tracking-wider text-[var(--primary)]">
|
|
45
|
+
{tagline}
|
|
46
|
+
</p>
|
|
47
|
+
)}
|
|
48
|
+
<h1 className="mt-2 text-4xl font-bold tracking-tight sm:text-5xl">
|
|
49
|
+
{settings.site.name}
|
|
50
|
+
</h1>
|
|
51
|
+
{settings.site.description && (
|
|
52
|
+
<p className="mt-4 max-w-2xl text-lg text-[var(--muted-foreground)]">
|
|
53
|
+
{settings.site.description}
|
|
54
|
+
</p>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
</section>
|
|
58
|
+
|
|
59
|
+
{featured && (
|
|
60
|
+
<section className="mx-auto max-w-4xl px-6 py-16">
|
|
61
|
+
<article>
|
|
62
|
+
<p className="mb-2 text-xs font-medium uppercase tracking-wider text-[var(--primary)]">
|
|
63
|
+
Top story
|
|
64
|
+
</p>
|
|
65
|
+
<h2 className="text-3xl font-bold tracking-tight">
|
|
66
|
+
<Link href={`/${featured.slug}`} className="hover:text-[var(--primary)]">
|
|
67
|
+
{featured.title}
|
|
68
|
+
</Link>
|
|
69
|
+
</h2>
|
|
70
|
+
{featured.publishedAt && (
|
|
71
|
+
<time
|
|
72
|
+
dateTime={featured.publishedAt}
|
|
73
|
+
className="mt-2 block font-mono text-xs text-[var(--muted-foreground)]"
|
|
74
|
+
>
|
|
75
|
+
{formatDate(featured.publishedAt, settings.dateFormat, settings.timezone)}
|
|
76
|
+
</time>
|
|
77
|
+
)}
|
|
78
|
+
<div
|
|
79
|
+
className="prose prose-neutral dark:prose-invert mt-6 max-w-none"
|
|
80
|
+
dangerouslySetInnerHTML={{ __html: renderBody(featured) }}
|
|
81
|
+
/>
|
|
82
|
+
</article>
|
|
83
|
+
</section>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{posts.length > 0 && (
|
|
87
|
+
<section className="mx-auto max-w-4xl px-6 py-16">
|
|
88
|
+
<h2 className="mb-8 border-l-4 border-[var(--primary)] pl-4 text-2xl font-bold">
|
|
89
|
+
News
|
|
90
|
+
</h2>
|
|
91
|
+
<ul className="divide-y border-t border-b">
|
|
92
|
+
{posts.map((post) => (
|
|
93
|
+
<li key={post.postId} className="py-5">
|
|
94
|
+
<Link
|
|
95
|
+
href={`/${post.slug}`}
|
|
96
|
+
className="group flex flex-col gap-1 sm:flex-row sm:items-baseline sm:gap-6"
|
|
97
|
+
>
|
|
98
|
+
{post.publishedAt && (
|
|
99
|
+
<time
|
|
100
|
+
dateTime={post.publishedAt}
|
|
101
|
+
className="font-mono text-xs text-[var(--muted-foreground)] sm:w-28 sm:shrink-0"
|
|
102
|
+
>
|
|
103
|
+
{formatDate(post.publishedAt, settings.dateFormat, settings.timezone)}
|
|
104
|
+
</time>
|
|
105
|
+
)}
|
|
106
|
+
<span className="flex-1 text-base font-medium group-hover:text-[var(--primary)]">
|
|
107
|
+
{post.title}
|
|
108
|
+
</span>
|
|
109
|
+
</Link>
|
|
110
|
+
</li>
|
|
111
|
+
))}
|
|
112
|
+
</ul>
|
|
113
|
+
</section>
|
|
114
|
+
)}
|
|
115
|
+
</main>
|
|
116
|
+
|
|
117
|
+
<SiteFooter
|
|
118
|
+
links={theme.values.footerLinks}
|
|
119
|
+
legend={
|
|
120
|
+
<div className="space-y-1">
|
|
121
|
+
{footerLegend && <p>{footerLegend}</p>}
|
|
122
|
+
<p>
|
|
123
|
+
© {new Date().getFullYear()} {settings.site.name}
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
}
|
|
127
|
+
/>
|
|
128
|
+
</>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
@@ -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 { 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 CorporatePost({ 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-lg font-semibold tracking-tight"
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
<main className="mx-auto max-w-3xl px-6 py-12">
|
|
51
|
+
<nav className="mb-6">
|
|
52
|
+
<Link href="/" className="text-sm text-[var(--muted-foreground)] hover:text-[var(--primary)]">
|
|
53
|
+
{t('public.back')}
|
|
54
|
+
</Link>
|
|
55
|
+
</nav>
|
|
56
|
+
|
|
57
|
+
<article>
|
|
58
|
+
<header className="mb-8 border-b pb-6">
|
|
59
|
+
<h1 className="text-3xl font-bold tracking-tight">{post.title}</h1>
|
|
60
|
+
{post.publishedAt && (
|
|
61
|
+
<time
|
|
62
|
+
dateTime={post.publishedAt}
|
|
63
|
+
className="mt-2 block font-mono text-xs text-[var(--muted-foreground)]"
|
|
64
|
+
>
|
|
65
|
+
{formatDate(post.publishedAt, settings.dateFormat, settings.timezone)}
|
|
66
|
+
</time>
|
|
67
|
+
)}
|
|
68
|
+
</header>
|
|
69
|
+
|
|
70
|
+
<div
|
|
71
|
+
id="post-body"
|
|
72
|
+
className="prose prose-neutral dark:prose-invert max-w-none [&_img]:max-w-[var(--ampless-img-max-width)] [&_img]:mx-auto"
|
|
73
|
+
style={proseStyle}
|
|
74
|
+
dangerouslySetInnerHTML={{ __html: renderBody(post) }}
|
|
75
|
+
/>
|
|
76
|
+
|
|
77
|
+
<TagList tags={post.tags} className="mt-8 border-t pt-6" />
|
|
78
|
+
</article>
|
|
79
|
+
|
|
80
|
+
<LightboxBinder scopeSelector="#post-body" defaultLightbox={defaultLightbox} />
|
|
81
|
+
</main>
|
|
82
|
+
|
|
83
|
+
<SiteFooter
|
|
84
|
+
links={theme.values.footerLinks}
|
|
85
|
+
legend={
|
|
86
|
+
<div className="space-y-1">
|
|
87
|
+
{footerLegend && <p>{footerLegend}</p>}
|
|
88
|
+
<p>
|
|
89
|
+
© {new Date().getFullYear()} {settings.site.name}
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
}
|
|
93
|
+
/>
|
|
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 corporateSitemapHandler({ 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,81 @@
|
|
|
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 CorporateTag({ 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-lg font-semibold tracking-tight"
|
|
30
|
+
/>
|
|
31
|
+
|
|
32
|
+
<main className="mx-auto max-w-4xl px-6 py-12">
|
|
33
|
+
<nav className="mb-6">
|
|
34
|
+
<Link href="/" className="text-sm text-[var(--muted-foreground)] hover:text-[var(--primary)]">
|
|
35
|
+
{t('public.home')}
|
|
36
|
+
</Link>
|
|
37
|
+
</nav>
|
|
38
|
+
|
|
39
|
+
<header className="mb-10">
|
|
40
|
+
<p className="text-sm text-[var(--muted-foreground)]">{t('public.tagLabel')}</p>
|
|
41
|
+
<h1 className="text-3xl font-bold tracking-tight">#{decodedTag}</h1>
|
|
42
|
+
</header>
|
|
43
|
+
|
|
44
|
+
<ul className="divide-y border-t border-b">
|
|
45
|
+
{posts.map((post) => (
|
|
46
|
+
<li key={post.postId} className="py-5">
|
|
47
|
+
<Link
|
|
48
|
+
href={`/${post.slug}`}
|
|
49
|
+
className="group flex flex-col gap-1 sm:flex-row sm:items-baseline sm:gap-6"
|
|
50
|
+
>
|
|
51
|
+
{post.publishedAt && (
|
|
52
|
+
<time
|
|
53
|
+
dateTime={post.publishedAt}
|
|
54
|
+
className="font-mono text-xs text-[var(--muted-foreground)] sm:w-28 sm:shrink-0"
|
|
55
|
+
>
|
|
56
|
+
{formatDate(post.publishedAt, settings.dateFormat, settings.timezone)}
|
|
57
|
+
</time>
|
|
58
|
+
)}
|
|
59
|
+
<span className="flex-1 text-base font-medium group-hover:text-[var(--primary)]">
|
|
60
|
+
{post.title}
|
|
61
|
+
</span>
|
|
62
|
+
</Link>
|
|
63
|
+
</li>
|
|
64
|
+
))}
|
|
65
|
+
</ul>
|
|
66
|
+
</main>
|
|
67
|
+
|
|
68
|
+
<SiteFooter
|
|
69
|
+
links={theme.values.footerLinks}
|
|
70
|
+
legend={
|
|
71
|
+
<div className="space-y-1">
|
|
72
|
+
{footerLegend && <p>{footerLegend}</p>}
|
|
73
|
+
<p>
|
|
74
|
+
© {new Date().getFullYear()} {settings.site.name}
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
}
|
|
78
|
+
/>
|
|
79
|
+
</>
|
|
80
|
+
)
|
|
81
|
+
}
|