create-rush-cms 0.0.1 → 0.1.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.
@@ -0,0 +1,36 @@
1
+ ---
2
+ import type { Alternate, LocaleCode, NavigationLink } from '@rushcms/client'
3
+ import LanguageSwitcher from './language-switcher.astro'
4
+ import NavLinks from './nav-links.astro'
5
+
6
+ type Props = {
7
+ name: string
8
+ home: string
9
+ links: NavigationLink[]
10
+ locale: LocaleCode
11
+ translations: Alternate[]
12
+ }
13
+
14
+ const { name, home, links, locale, translations } = Astro.props
15
+ ---
16
+
17
+ <header class="relative z-30 border-b border-line bg-surface/80 backdrop-blur">
18
+ <div
19
+ class="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-x-8 gap-y-3 px-6 py-4"
20
+ >
21
+ <a href={home} class="flex items-center gap-2 text-lg font-semibold tracking-tight">
22
+ <img src="/favicon.svg" alt="" width="28" height="28" />
23
+ <span>{name}</span>
24
+ </a>
25
+ <div class="flex flex-wrap items-center gap-x-6 gap-y-2">
26
+ {
27
+ links.length > 0 && (
28
+ <nav aria-label="Main" class="site-nav">
29
+ <NavLinks links={links} />
30
+ </nav>
31
+ )
32
+ }
33
+ <LanguageSwitcher locale={locale} translations={translations} />
34
+ </div>
35
+ </div>
36
+ </header>
@@ -0,0 +1,49 @@
1
+ ---
2
+ import { getNavigation, getRush, getUrls } from '@rushcms/astro'
3
+ import PreviewBanner from '@rushcms/astro/components/preview-banner.astro'
4
+ import { optional, toBcp47 } from '@rushcms/client'
5
+ import type { Alternate, LocaleCode } from '@rushcms/client'
6
+ import SiteFooter from '../components/site-footer.astro'
7
+ import SiteHeader from '../components/site-header.astro'
8
+ import '../styles/global.css'
9
+
10
+ type Props = {
11
+ locale: LocaleCode
12
+ title?: string | undefined
13
+ translations?: Alternate[]
14
+ }
15
+
16
+ const { locale, title, translations = [] } = Astro.props
17
+ const site = await getRush(Astro).site.get()
18
+ const home = getUrls(Astro).homepage(locale)
19
+ // TODO: create a navigation with the key "main" in Rush CMS, or change the key here.
20
+ const main = (await optional(getNavigation(Astro, 'main'))) ?? []
21
+ // TODO: create a navigation with the key "footer" in Rush CMS, or change the key here.
22
+ const footer = (await optional(getNavigation(Astro, 'footer'))) ?? []
23
+ ---
24
+
25
+ <!doctype html>
26
+ <html lang={toBcp47(locale)}>
27
+ <head>
28
+ <meta charset="utf-8" />
29
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
30
+ <meta name="color-scheme" content="light dark" />
31
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
32
+ {title !== undefined && <title>{title}</title>}
33
+ <slot name="head" />
34
+ </head>
35
+ <body class="flex min-h-screen flex-col bg-canvas font-sans text-ink antialiased">
36
+ <a
37
+ href="#content"
38
+ class="sr-only rounded-md bg-accent px-4 py-2 font-medium text-accent-ink focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50"
39
+ >
40
+ Skip to content
41
+ </a>
42
+ <PreviewBanner />
43
+ <SiteHeader name={site.name} home={home} links={main} locale={locale} translations={translations} />
44
+ <main id="content" tabindex="-1" class="flex-1 focus:outline-none">
45
+ <slot />
46
+ </main>
47
+ <SiteFooter name={site.name} links={footer} />
48
+ </body>
49
+ </html>
@@ -0,0 +1,12 @@
1
+ import { toBcp47 } from '@rushcms/client'
2
+ import type { LocaleCode } from '@rushcms/client'
3
+
4
+ export function languageName(locale: LocaleCode): string {
5
+ const tag = toBcp47(locale)
6
+ const name = new Intl.DisplayNames([tag], { type: 'language' }).of(tag) ?? tag
7
+ return name.charAt(0).toLocaleUpperCase(tag) + name.slice(1)
8
+ }
9
+
10
+ export function formatDate(value: string, locale: LocaleCode): string {
11
+ return new Intl.DateTimeFormat(toBcp47(locale), { dateStyle: 'long' }).format(new Date(value))
12
+ }
@@ -0,0 +1,15 @@
1
+ import { resolvePage } from '@rushcms/astro'
2
+ import type { EntryMissing, SitePage, SitePageContext } from '@rushcms/astro'
3
+ import type { APIContext } from 'astro'
4
+
5
+ export type RouteContext = SitePageContext & Pick<APIContext, 'redirect'>
6
+
7
+ export type RoutedPage = SitePage | EntryMissing
8
+
9
+ export async function routePage(context: RouteContext): Promise<RoutedPage | Response> {
10
+ const page = await resolvePage(context)
11
+ if (page.kind === 'moved') {
12
+ return context.redirect(page.location, page.permanent ? 301 : 302)
13
+ }
14
+ return page
15
+ }
@@ -0,0 +1,17 @@
1
+ ---
2
+ import type { SitePageProps } from '@rushcms/astro'
3
+ import { routePage } from '../lib/route-page'
4
+ import NotFound from '../views/not-found.astro'
5
+
6
+ export const prerender = false
7
+
8
+ type Props = SitePageProps
9
+
10
+ const page = await routePage(Astro)
11
+ if (page instanceof Response) {
12
+ return page
13
+ }
14
+ Astro.response.status = 404
15
+ ---
16
+
17
+ <NotFound />
@@ -0,0 +1,59 @@
1
+ ---
2
+ import { getRush, getUrls } from '@rushcms/astro'
3
+ import Form from '@rushcms/astro/components/form.astro'
4
+ import { optional } from '@rushcms/client'
5
+ import Base from '../../layouts/base.astro'
6
+ import NotFound from '../../views/not-found.astro'
7
+
8
+ export const prerender = false
9
+
10
+ const locale = getUrls(Astro).localeOf(Astro.params.locale)
11
+ // TODO: create a form with the key "contact" in Rush CMS, or change the key here.
12
+ const form = locale === undefined ? undefined : await optional(getRush(Astro).forms.get('contact'))
13
+ const notice = locale !== undefined && form === undefined && import.meta.env.DEV
14
+ if (form === undefined && !notice) {
15
+ Astro.response.status = 404
16
+ }
17
+ ---
18
+
19
+ {
20
+ locale !== undefined && (form !== undefined || notice) ? (
21
+ <Base locale={locale} title={form?.name ?? 'Contact'}>
22
+ <div class="mx-auto max-w-2xl px-6 py-16">
23
+ <h1 class="text-4xl font-semibold tracking-tight">{form?.name ?? 'Contact'}</h1>
24
+ {form ? (
25
+ <div class="contact-form mt-10">
26
+ <Form
27
+ form={form}
28
+ submitLabel="Send"
29
+ messages={{
30
+ sent: 'Thank you, your message was sent.',
31
+ error: 'Your message could not be sent. Please try again.',
32
+ required: 'This field is required.',
33
+ invalid_option: 'Choose one of the options.',
34
+ invalid_email: 'Enter a valid email address.',
35
+ invalid_url: 'Enter a valid URL.',
36
+ invalid_number: 'Enter a number.',
37
+ invalid_format: 'Check the format of this field.',
38
+ too_small: 'Enter {min} or more.',
39
+ too_large: 'Enter {max} or less.',
40
+ too_short: 'Use at least {min} characters.',
41
+ too_long: 'Use at most {max} characters.',
42
+ rate_limited: 'Too many attempts. Try again in a minute.',
43
+ captcha_failed: 'The captcha check failed. Please try again.'
44
+ }}
45
+ />
46
+ </div>
47
+ ) : (
48
+ <p lang="en" class="mt-6 rounded-lg border border-dashed border-line px-5 py-4 text-muted">
49
+ This page shows the Rush CMS form with the key <code class="font-mono">contact</code>.
50
+ Create that form in Rush CMS and reload. This notice appears only in{' '}
51
+ <code class="font-mono">astro dev</code>; production builds answer 404 here.
52
+ </p>
53
+ )}
54
+ </div>
55
+ </Base>
56
+ ) : (
57
+ <NotFound />
58
+ )
59
+ }
@@ -0,0 +1,32 @@
1
+ ---
2
+ import { getUrls } from '@rushcms/astro'
3
+ import type { SitePageProps } from '@rushcms/astro'
4
+ import { routePage } from '../lib/route-page'
5
+ import Home from '../views/home.astro'
6
+ import { viewsFor } from '../views/index'
7
+ import NotFound from '../views/not-found.astro'
8
+
9
+ export * from '@rushcms/astro/site-paths'
10
+
11
+ type Props = SitePageProps
12
+
13
+ const page = await routePage(Astro)
14
+ if (page instanceof Response) {
15
+ return page
16
+ }
17
+ if (page.kind === 'missing') {
18
+ Astro.response.status = 404
19
+ }
20
+ const collection =
21
+ page.kind === 'collection'
22
+ ? page.collection
23
+ : page.kind === 'found'
24
+ ? page.entry.collection
25
+ : undefined
26
+ const { list: List, entry: Entry } = viewsFor(collection, getUrls(Astro))
27
+ ---
28
+
29
+ {page.kind === 'homepage' && <Home page={page} />}
30
+ {page.kind === 'collection' && <List page={page} />}
31
+ {page.kind === 'found' && <Entry entry={page.entry} locale={page.locale} />}
32
+ {page.kind === 'missing' && <NotFound />}
@@ -0,0 +1,10 @@
1
+ import type { APIRoute } from 'astro'
2
+ import { RUSH_PREVIEW } from 'astro:env/server'
3
+
4
+ export const GET: APIRoute = ({ site }) => {
5
+ const rules = RUSH_PREVIEW ? 'Disallow: /' : 'Allow: /'
6
+ const sitemap = site === undefined ? '' : `\nSitemap: ${new URL('/sitemap.xml', site).href}\n`
7
+ return new Response(`User-agent: *\n${rules}\n${sitemap}`, {
8
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' }
9
+ })
10
+ }
@@ -0,0 +1,4 @@
1
+ import { sitemapResponse } from '@rushcms/astro'
2
+ import type { APIRoute } from 'astro'
3
+
4
+ export const GET: APIRoute = (context) => sitemapResponse({ context })
@@ -0,0 +1,278 @@
1
+ @import 'tailwindcss';
2
+ @plugin '@tailwindcss/typography';
3
+
4
+ @theme {
5
+ --font-sans:
6
+ ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
7
+ sans-serif;
8
+ --font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
9
+ --color-canvas: oklch(0.985 0.004 265);
10
+ --color-surface: oklch(1 0 0);
11
+ --color-ink: oklch(0.23 0.025 265);
12
+ --color-muted: oklch(0.5 0.025 265);
13
+ --color-line: oklch(0.91 0.012 265);
14
+ --color-accent: oklch(0.52 0.21 277);
15
+ --color-accent-ink: oklch(0.99 0.005 277);
16
+ --color-accent-soft: oklch(0.93 0.045 277);
17
+ --color-danger: oklch(0.55 0.2 25);
18
+ --color-success: oklch(0.55 0.14 155);
19
+ }
20
+
21
+ @media (prefers-color-scheme: dark) {
22
+ :root {
23
+ --color-canvas: oklch(0.17 0.018 265);
24
+ --color-surface: oklch(0.21 0.02 265);
25
+ --color-ink: oklch(0.95 0.008 265);
26
+ --color-muted: oklch(0.74 0.02 265);
27
+ --color-line: oklch(0.31 0.022 265);
28
+ --color-accent: oklch(0.76 0.14 277);
29
+ --color-accent-ink: oklch(0.18 0.03 277);
30
+ --color-accent-soft: oklch(0.3 0.07 277);
31
+ --color-danger: oklch(0.72 0.17 25);
32
+ --color-success: oklch(0.75 0.14 155);
33
+ }
34
+ }
35
+
36
+ @layer base {
37
+ :root {
38
+ color-scheme: light dark;
39
+ }
40
+
41
+ :focus-visible {
42
+ outline: 2px solid var(--color-accent);
43
+ outline-offset: 3px;
44
+ border-radius: 4px;
45
+ }
46
+
47
+ ::selection {
48
+ background-color: var(--color-accent-soft);
49
+ }
50
+ }
51
+
52
+ @layer components {
53
+ .button,
54
+ .contact-form button[type='submit'] {
55
+ display: inline-flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ gap: 0.5rem;
59
+ border-radius: 9999px;
60
+ padding: 0.625rem 1.25rem;
61
+ font-weight: 500;
62
+ transition:
63
+ background-color 150ms,
64
+ border-color 150ms,
65
+ color 150ms;
66
+ }
67
+
68
+ .button-primary,
69
+ .contact-form button[type='submit'] {
70
+ background-color: var(--color-accent);
71
+ color: var(--color-accent-ink);
72
+ }
73
+
74
+ .button-primary:hover,
75
+ .contact-form button[type='submit']:hover {
76
+ background-color: color-mix(in oklch, var(--color-accent) 88%, var(--color-ink));
77
+ }
78
+
79
+ .button-secondary {
80
+ border: 1px solid var(--color-line);
81
+ background-color: var(--color-surface);
82
+ }
83
+
84
+ .button-secondary:hover {
85
+ border-color: var(--color-accent);
86
+ }
87
+
88
+ .card {
89
+ border: 1px solid var(--color-line);
90
+ border-radius: 1rem;
91
+ background-color: var(--color-surface);
92
+ padding: 1.5rem;
93
+ transition:
94
+ border-color 150ms,
95
+ box-shadow 150ms;
96
+ }
97
+
98
+ .card:hover,
99
+ .card:focus-within {
100
+ border-color: var(--color-accent);
101
+ box-shadow: 0 8px 30px -12px color-mix(in oklch, var(--color-accent) 40%, transparent);
102
+ }
103
+
104
+ .nav-links-top {
105
+ display: flex;
106
+ flex-wrap: wrap;
107
+ align-items: center;
108
+ gap: 0.25rem 1.25rem;
109
+ }
110
+
111
+ .nav-item {
112
+ position: relative;
113
+ }
114
+
115
+ .nav-link,
116
+ .nav-label {
117
+ display: inline-block;
118
+ padding-block: 0.25rem;
119
+ color: var(--color-muted);
120
+ }
121
+
122
+ .nav-link:hover,
123
+ .nav-link[aria-current] {
124
+ color: var(--color-ink);
125
+ }
126
+
127
+ .site-nav .nav-links-nested {
128
+ position: absolute;
129
+ top: 100%;
130
+ left: -1rem;
131
+ z-index: 20;
132
+ display: none;
133
+ min-width: 12rem;
134
+ border: 1px solid var(--color-line);
135
+ border-radius: 0.75rem;
136
+ background-color: var(--color-surface);
137
+ padding: 0.5rem 1rem;
138
+ box-shadow: 0 12px 30px -12px rgb(0 0 0 / 0.25);
139
+ }
140
+
141
+ .site-nav .nav-item:hover > .nav-links-nested,
142
+ .site-nav .nav-item:focus-within > .nav-links-nested {
143
+ display: block;
144
+ }
145
+
146
+ .footer-nav .nav-links-top {
147
+ align-items: flex-start;
148
+ gap: 1rem 2.5rem;
149
+ }
150
+
151
+ .footer-nav .nav-links-nested {
152
+ margin-top: 0.25rem;
153
+ }
154
+
155
+ [data-rush-preview] {
156
+ position: sticky;
157
+ top: 0;
158
+ z-index: 40;
159
+ display: flex;
160
+ flex-wrap: wrap;
161
+ align-items: center;
162
+ justify-content: center;
163
+ gap: 0.5rem 1rem;
164
+ background-color: var(--color-ink);
165
+ padding: 0.5rem 1rem;
166
+ color: var(--color-canvas);
167
+ font-size: 0.875rem;
168
+ }
169
+
170
+ [data-rush-preview] a {
171
+ font-weight: 600;
172
+ text-decoration: underline;
173
+ text-underline-offset: 3px;
174
+ }
175
+
176
+ .contact-form form {
177
+ display: grid;
178
+ gap: 1.5rem;
179
+ }
180
+
181
+ .contact-form label,
182
+ .contact-form legend {
183
+ display: block;
184
+ margin-bottom: 0.375rem;
185
+ font-weight: 500;
186
+ }
187
+
188
+ .contact-form fieldset label {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 0.5rem;
192
+ font-weight: 400;
193
+ }
194
+
195
+ .contact-form input:not([type='checkbox'], [type='radio']),
196
+ .contact-form textarea,
197
+ .contact-form select {
198
+ width: 100%;
199
+ border: 1px solid var(--color-line);
200
+ border-radius: 0.5rem;
201
+ background-color: var(--color-surface);
202
+ padding: 0.625rem 0.75rem;
203
+ }
204
+
205
+ .contact-form textarea {
206
+ min-height: 9rem;
207
+ }
208
+
209
+ .contact-form input:focus-visible,
210
+ .contact-form textarea:focus-visible,
211
+ .contact-form select:focus-visible {
212
+ outline-offset: 0;
213
+ border-color: var(--color-accent);
214
+ }
215
+
216
+ .contact-form input[type='checkbox'],
217
+ .contact-form input[type='radio'] {
218
+ accent-color: var(--color-accent);
219
+ }
220
+
221
+ .contact-form [id$='-help'] {
222
+ margin-top: 0.375rem;
223
+ color: var(--color-muted);
224
+ font-size: 0.875rem;
225
+ }
226
+
227
+ .contact-form [data-rush-error-for] {
228
+ margin-top: 0.375rem;
229
+ color: var(--color-danger);
230
+ font-size: 0.875rem;
231
+ }
232
+
233
+ .contact-form [data-rush-status] {
234
+ border: 1px solid var(--color-line);
235
+ border-radius: 0.5rem;
236
+ padding: 0.75rem 1rem;
237
+ }
238
+
239
+ .contact-form [data-rush-status][data-state='sent'] {
240
+ border-color: var(--color-success);
241
+ color: var(--color-success);
242
+ }
243
+
244
+ .contact-form [data-rush-status][data-state='error'] {
245
+ border-color: var(--color-danger);
246
+ color: var(--color-danger);
247
+ }
248
+
249
+ .contact-form button[type='submit'] {
250
+ justify-self: start;
251
+ cursor: pointer;
252
+ }
253
+
254
+ .contact-form button[type='submit']:disabled {
255
+ cursor: progress;
256
+ opacity: 0.7;
257
+ }
258
+ }
259
+
260
+ .prose {
261
+ --tw-prose-body: var(--color-ink);
262
+ --tw-prose-headings: var(--color-ink);
263
+ --tw-prose-lead: var(--color-muted);
264
+ --tw-prose-links: var(--color-accent);
265
+ --tw-prose-bold: var(--color-ink);
266
+ --tw-prose-counters: var(--color-muted);
267
+ --tw-prose-bullets: var(--color-line);
268
+ --tw-prose-hr: var(--color-line);
269
+ --tw-prose-quotes: var(--color-ink);
270
+ --tw-prose-quote-borders: var(--color-accent);
271
+ --tw-prose-captions: var(--color-muted);
272
+ --tw-prose-kbd: var(--color-ink);
273
+ --tw-prose-code: var(--color-ink);
274
+ --tw-prose-pre-code: var(--color-ink);
275
+ --tw-prose-pre-bg: var(--color-surface);
276
+ --tw-prose-th-borders: var(--color-line);
277
+ --tw-prose-td-borders: var(--color-line);
278
+ }
@@ -0,0 +1,84 @@
1
+ ---
2
+ import { collectionAlternates, getRush, getUrls } from '@rushcms/astro'
3
+ import RushImage from '@rushcms/astro/components/rush-image.astro'
4
+ import Base from '../layouts/base.astro'
5
+ import { formatDate } from '../lib/locale'
6
+ import type { ListViewProps } from './index'
7
+
8
+ type Props = ListViewProps
9
+
10
+ const { page } = Astro.props
11
+ const { collection, locale, entries, lastPage } = page
12
+ const urls = getUrls(Astro)
13
+ const site = await getRush(Astro).site.get()
14
+ const previous = page.page > 1 ? urls.collection(collection, locale, page.page - 1) : undefined
15
+ const next = page.page < lastPage ? urls.collection(collection, locale, page.page + 1) : undefined
16
+ const canonical = Astro.site && new URL(urls.collection(collection, locale, page.page), Astro.site)
17
+ const heading = page.page > 1 ? `${collection.name} (${page.page}/${lastPage})` : collection.name
18
+ const translations = collectionAlternates(collection.id, urls)
19
+ ---
20
+
21
+ <Base locale={locale} title={`${heading} | ${site.name}`} translations={translations}>
22
+ {canonical && <link slot="head" rel="canonical" href={canonical.href} />}
23
+ <div class="mx-auto max-w-6xl px-6 py-16">
24
+ <header class="max-w-3xl">
25
+ <h1 class="text-4xl font-semibold tracking-tight">{collection.name}</h1>
26
+ {
27
+ lastPage > 1 && (
28
+ <p class="mt-2 text-muted">
29
+ Page {page.page} of {lastPage}
30
+ </p>
31
+ )
32
+ }
33
+ </header>
34
+ {
35
+ entries.items.length > 0 ? (
36
+ <ul class="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
37
+ {entries.items.map((entry) => (
38
+ <li>
39
+ <article class="card group relative flex h-full flex-col">
40
+ {entry.featured_image?.url && (
41
+ <RushImage
42
+ media={entry.featured_image}
43
+ class="mb-4 aspect-video w-full rounded-lg object-cover"
44
+ />
45
+ )}
46
+ <h2 class="text-lg font-medium group-hover:text-accent">
47
+ <a href={urls.servedPath(entry, locale)} class="after:absolute after:inset-0">
48
+ {entry.title}
49
+ </a>
50
+ </h2>
51
+ {entry.excerpt && <p class="mt-2 line-clamp-3 text-muted">{entry.excerpt}</p>}
52
+ <p class="mt-auto pt-4 text-sm text-muted">
53
+ <time datetime={entry.published_at}>
54
+ {formatDate(entry.published_at, locale)}
55
+ </time>
56
+ </p>
57
+ </article>
58
+ </li>
59
+ ))}
60
+ </ul>
61
+ ) : (
62
+ <p class="mt-12 text-muted">Nothing has been published here yet.</p>
63
+ )
64
+ }
65
+ {
66
+ lastPage > 1 && (
67
+ <nav aria-label="Pagination" class="mt-12 flex items-center justify-between gap-4">
68
+ {previous ? (
69
+ <a href={previous} rel="prev" class="button button-secondary">
70
+ Previous page
71
+ </a>
72
+ ) : (
73
+ <span />
74
+ )}
75
+ {next && (
76
+ <a href={next} rel="next" class="button button-secondary">
77
+ Next page
78
+ </a>
79
+ )}
80
+ </nav>
81
+ )
82
+ }
83
+ </div>
84
+ </Base>
@@ -0,0 +1,65 @@
1
+ ---
2
+ import { getUrls } from '@rushcms/astro'
3
+ import RichText from '@rushcms/astro/components/rich-text.astro'
4
+ import RushImage from '@rushcms/astro/components/rush-image.astro'
5
+ import Seo from '@rushcms/astro/components/seo.astro'
6
+ import { alternates, isRichText, toBcp47 } from '@rushcms/client'
7
+ import Base from '../layouts/base.astro'
8
+ import { formatDate, languageName } from '../lib/locale'
9
+ import type { EntryViewProps } from './index'
10
+
11
+ type Props = EntryViewProps
12
+
13
+ const { entry, locale } = Astro.props
14
+ const urls = getUrls(Astro)
15
+ const list = urls.collectionRef(entry.collection.id, locale)
16
+ const documents = Object.values(entry.data).filter(isRichText)
17
+ ---
18
+
19
+ <Base locale={locale} translations={alternates(entry, urls)}>
20
+ <Seo slot="head" entry={entry} />
21
+ <article lang={toBcp47(entry.locale)} class="mx-auto max-w-3xl px-6 py-16">
22
+ <header>
23
+ {
24
+ list && (
25
+ <p class="text-sm">
26
+ <a href={urls.collection(list, locale)} class="text-muted hover:text-accent">
27
+ &larr; Back to the list
28
+ </a>
29
+ </p>
30
+ )
31
+ }
32
+ <h1 class="mt-6 text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
33
+ {entry.title}
34
+ </h1>
35
+ <p class="mt-4 text-sm text-muted">
36
+ <time datetime={entry.published_at}>{formatDate(entry.published_at, entry.locale)}</time>
37
+ {entry.author && <span> &middot; {entry.author.name}</span>}
38
+ </p>
39
+ {
40
+ entry.fallback && (
41
+ <p lang="en" class="mt-6 rounded-lg border border-line bg-surface px-4 py-3 text-sm">
42
+ Not translated yet: shown in {languageName(entry.locale)}.
43
+ </p>
44
+ )
45
+ }
46
+ {entry.excerpt && <p class="mt-8 text-xl text-pretty text-muted">{entry.excerpt}</p>}
47
+ </header>
48
+ {
49
+ entry.featured_image?.url && (
50
+ <figure class="mt-10">
51
+ <RushImage media={entry.featured_image} class="w-full rounded-xl" loading="eager" />
52
+ </figure>
53
+ )
54
+ }
55
+ {
56
+ documents.length > 0 && (
57
+ <div class="prose prose-lg mt-10 max-w-none">
58
+ {documents.map((content) => (
59
+ <RichText content={content} media={entry.media} />
60
+ ))}
61
+ </div>
62
+ )
63
+ }
64
+ </article>
65
+ </Base>