@earlyseo/blog 1.0.1
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/README.md +304 -0
- package/dist/client.d.ts +22 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +65 -0
- package/dist/client.js.map +1 -0
- package/dist/css.d.ts +16 -0
- package/dist/css.d.ts.map +1 -0
- package/dist/css.js +186 -0
- package/dist/css.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/next/back-link.d.ts +12 -0
- package/dist/next/back-link.d.ts.map +1 -0
- package/dist/next/back-link.js +15 -0
- package/dist/next/back-link.js.map +1 -0
- package/dist/next/blog-list-page.d.ts +22 -0
- package/dist/next/blog-list-page.d.ts.map +1 -0
- package/dist/next/blog-list-page.js +49 -0
- package/dist/next/blog-list-page.js.map +1 -0
- package/dist/next/blog-post-page.d.ts +65 -0
- package/dist/next/blog-post-page.d.ts.map +1 -0
- package/dist/next/blog-post-page.js +103 -0
- package/dist/next/blog-post-page.js.map +1 -0
- package/dist/next/index.d.ts +5 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +11 -0
- package/dist/next/index.js.map +1 -0
- package/dist/react/blog-list.d.ts +27 -0
- package/dist/react/blog-list.d.ts.map +1 -0
- package/dist/react/blog-list.js +49 -0
- package/dist/react/blog-list.js.map +1 -0
- package/dist/react/blog-post.d.ts +35 -0
- package/dist/react/blog-post.d.ts.map +1 -0
- package/dist/react/blog-post.js +41 -0
- package/dist/react/blog-post.js.map +1 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +15 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/provider.d.ts +30 -0
- package/dist/react/provider.d.ts.map +1 -0
- package/dist/react/provider.js +34 -0
- package/dist/react/provider.js.map +1 -0
- package/dist/react/styles.d.ts +11 -0
- package/dist/react/styles.d.ts.map +1 -0
- package/dist/react/styles.js +16 -0
- package/dist/react/styles.js.map +1 -0
- package/dist/react/use-article.d.ts +17 -0
- package/dist/react/use-article.d.ts.map +1 -0
- package/dist/react/use-article.js +53 -0
- package/dist/react/use-article.js.map +1 -0
- package/dist/react/use-articles.d.ts +22 -0
- package/dist/react/use-articles.d.ts.map +1 -0
- package/dist/react/use-articles.js +59 -0
- package/dist/react/use-articles.js.map +1 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +68 -0
- package/src/cli/init.mjs +188 -0
package/README.md
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# @earlyseo/blog
|
|
2
|
+
|
|
3
|
+
Drop-in blog integration for **React** & **Next.js** — powered by [EarlySEO](https://earlyseo.com).
|
|
4
|
+
|
|
5
|
+
Articles are served from the EarlySEO CDN. No database, no webhooks, no storage config — just add your **site ID** and go.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **CDN-first** — Articles served from EarlySEO's global CDN. Zero infrastructure to manage.
|
|
10
|
+
- **Paginated** — Pre-built paginated JSON. No DB queries per visitor.
|
|
11
|
+
- **Next.js App Router** — Drop-in server components for `/blog` and `/blog/[slug]`
|
|
12
|
+
- **React hooks** — `useArticles()` and `useArticle(slug)` for custom UIs
|
|
13
|
+
- **Full SEO** — Open Graph, Twitter cards, meta descriptions, canonical URLs
|
|
14
|
+
- **Auto-styling** — CSS custom properties adapt to your site's theme
|
|
15
|
+
- **100% customizable** — Render props, custom components, or headless hooks
|
|
16
|
+
|
|
17
|
+
## Quick Start (Next.js)
|
|
18
|
+
|
|
19
|
+
### Option A: Auto-generate pages (recommended)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @earlyseo/blog
|
|
23
|
+
npx earlyseo-blog
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The CLI will detect your Next.js project, ask for your site ID, and generate:
|
|
27
|
+
- `app/blog/page.tsx` — Paginated blog listing
|
|
28
|
+
- `app/blog/[slug]/page.tsx` — Article detail with full SEO metadata
|
|
29
|
+
- `.env.local` — `EARLYSEO_SITE_ID` added automatically
|
|
30
|
+
|
|
31
|
+
That's it — run `npm run dev` and visit `/blog`.
|
|
32
|
+
|
|
33
|
+
### Option B: Manual setup
|
|
34
|
+
|
|
35
|
+
### 1. Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @earlyseo/blog
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Set your site ID
|
|
42
|
+
|
|
43
|
+
```env
|
|
44
|
+
# .env.local
|
|
45
|
+
EARLYSEO_SITE_ID=your-site-id
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 3. Add the blog listing page
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// app/blog/page.tsx
|
|
52
|
+
import { createBlogListPage, createBlogListMetadata } from "@earlyseo/blog/next";
|
|
53
|
+
|
|
54
|
+
export default createBlogListPage({
|
|
55
|
+
siteId: process.env.EARLYSEO_SITE_ID!,
|
|
56
|
+
title: "Our Blog",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const generateMetadata = createBlogListMetadata({
|
|
60
|
+
title: "Our Blog",
|
|
61
|
+
description: "Read our latest articles about...",
|
|
62
|
+
siteName: "My Site",
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 4. Add the article detail page
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
// app/blog/[slug]/page.tsx
|
|
70
|
+
import { createBlogPostPage, createBlogPostMetadata } from "@earlyseo/blog/next";
|
|
71
|
+
|
|
72
|
+
const siteId = process.env.EARLYSEO_SITE_ID!;
|
|
73
|
+
|
|
74
|
+
export default createBlogPostPage({ siteId });
|
|
75
|
+
export const generateMetadata = createBlogPostMetadata({ siteId, siteName: "My Site" });
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
That's it! Articles published from EarlySEO automatically appear on your blog via CDN.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## How It Works
|
|
83
|
+
|
|
84
|
+
When you publish an article in [EarlySEO](https://earlyseo.com), we write pre-built JSON files to our CDN:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
media.earlyseo.com/site/{siteId}/manifest.json → Total pages, version
|
|
88
|
+
media.earlyseo.com/site/{siteId}/articles/page-1.json → Paginated article list
|
|
89
|
+
media.earlyseo.com/site/{siteId}/article/my-slug.json → Full article content
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Your site reads from these JSON files — **no database, no API calls, no webhook handlers**.
|
|
93
|
+
|
|
94
|
+
Only the affected files are updated on each publish:
|
|
95
|
+
- The individual article JSON
|
|
96
|
+
- The paginated list pages
|
|
97
|
+
- The manifest
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## React Components
|
|
102
|
+
|
|
103
|
+
### Using with any React framework (Vite, Remix, etc.)
|
|
104
|
+
|
|
105
|
+
Wrap your app in `EarlySeoProvider` and use the components:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import { EarlySeoProvider, BlogList, BlogPost, ArticleStyles } from "@earlyseo/blog/react";
|
|
109
|
+
|
|
110
|
+
function App() {
|
|
111
|
+
return (
|
|
112
|
+
<EarlySeoProvider siteId="your-site-id" basePath="/blog">
|
|
113
|
+
<ArticleStyles />
|
|
114
|
+
<Routes>
|
|
115
|
+
<Route path="/blog" element={<BlogList />} />
|
|
116
|
+
<Route path="/blog/:slug" element={<BlogPostRoute />} />
|
|
117
|
+
</Routes>
|
|
118
|
+
</EarlySeoProvider>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function BlogPostRoute() {
|
|
123
|
+
const { slug } = useParams();
|
|
124
|
+
return <BlogPost slug={slug!} />;
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Headless Hooks
|
|
129
|
+
|
|
130
|
+
For full control over the UI, use the hooks directly:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { useArticles, useArticle } from "@earlyseo/blog/react";
|
|
134
|
+
|
|
135
|
+
function CustomBlogList() {
|
|
136
|
+
const { articles, loading, page, totalPages, goToPage } = useArticles();
|
|
137
|
+
|
|
138
|
+
if (loading) return <Skeleton />;
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div>
|
|
142
|
+
{articles.map((a) => (
|
|
143
|
+
<Link key={a.slug} to={`/blog/${a.slug}`}>
|
|
144
|
+
<h2>{a.title}</h2>
|
|
145
|
+
<p>{a.metaDescription}</p>
|
|
146
|
+
</Link>
|
|
147
|
+
))}
|
|
148
|
+
<div>Page {page} of {totalPages}</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function CustomArticle({ slug }: { slug: string }) {
|
|
154
|
+
const { article, loading } = useArticle(slug);
|
|
155
|
+
|
|
156
|
+
if (loading) return <Skeleton />;
|
|
157
|
+
if (!article) return <NotFound />;
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<article>
|
|
161
|
+
<h1>{article.title}</h1>
|
|
162
|
+
<div dangerouslySetInnerHTML={{ __html: article.contentRawHtml }} />
|
|
163
|
+
</article>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Custom Card Rendering
|
|
169
|
+
|
|
170
|
+
Override the default card in the blog list:
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
<BlogList
|
|
174
|
+
renderCard={(article, basePath) => (
|
|
175
|
+
<a href={`${basePath}/${article.slug}`} className="my-custom-card">
|
|
176
|
+
<img src={article.imageUrl} alt={article.title} />
|
|
177
|
+
<h3>{article.title}</h3>
|
|
178
|
+
</a>
|
|
179
|
+
)}
|
|
180
|
+
/>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## CDN Client
|
|
186
|
+
|
|
187
|
+
Use `EarlySeoClient` directly for full control:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { EarlySeoClient } from "@earlyseo/blog";
|
|
191
|
+
|
|
192
|
+
const client = new EarlySeoClient(
|
|
193
|
+
{ siteId: "your-site-id" },
|
|
194
|
+
// Optional: Next.js ISR caching
|
|
195
|
+
{ next: { revalidate: 60 } }
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const manifest = await client.getManifest(); // Manifest | null
|
|
199
|
+
const page1 = await client.getListPage(1); // ArticleListPage | null
|
|
200
|
+
const article = await client.getArticle("slug"); // Article | null
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Styling & Theming
|
|
206
|
+
|
|
207
|
+
Articles come with a `.earlyseo-article` CSS wrapper that uses CSS custom properties for full theming control:
|
|
208
|
+
|
|
209
|
+
```css
|
|
210
|
+
:root {
|
|
211
|
+
--earlyseo-font-family: "Inter", sans-serif;
|
|
212
|
+
--earlyseo-text-color: #1a1a1a;
|
|
213
|
+
--earlyseo-heading-color: #111;
|
|
214
|
+
--earlyseo-link-color: #2563eb;
|
|
215
|
+
--earlyseo-border-color: #e5e7eb;
|
|
216
|
+
--earlyseo-code-bg: #f3f4f6;
|
|
217
|
+
--earlyseo-pre-bg: #f9fafb;
|
|
218
|
+
--earlyseo-th-bg: #f3f4f6;
|
|
219
|
+
--earlyseo-muted-color: #6b7280;
|
|
220
|
+
--earlyseo-tag-bg: #f3f4f6;
|
|
221
|
+
--earlyseo-blog-max-width: 72rem;
|
|
222
|
+
--earlyseo-post-max-width: 48rem;
|
|
223
|
+
--earlyseo-blog-padding: 2rem 1rem;
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Or use `htmlMode="raw"` to strip all EarlySEO styles and use your own:
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
// Next.js
|
|
231
|
+
export default createBlogPostPage({ siteId, htmlMode: "raw" });
|
|
232
|
+
|
|
233
|
+
// React
|
|
234
|
+
<BlogPost slug={slug} htmlMode="raw" />
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## API Reference
|
|
240
|
+
|
|
241
|
+
### Types
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
interface Manifest {
|
|
245
|
+
schemaVersion: 1;
|
|
246
|
+
version: number;
|
|
247
|
+
totalArticles: number;
|
|
248
|
+
totalPages: number;
|
|
249
|
+
pageSize: number;
|
|
250
|
+
updatedAt: string;
|
|
251
|
+
featuredSlugs: string[];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
interface ArticleListItem {
|
|
255
|
+
title: string;
|
|
256
|
+
slug: string;
|
|
257
|
+
imageUrl?: string;
|
|
258
|
+
imageAlt?: string;
|
|
259
|
+
metaDescription: string;
|
|
260
|
+
createdAt: string;
|
|
261
|
+
tags: string[];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
interface ArticleListPage {
|
|
265
|
+
page: number;
|
|
266
|
+
totalPages: number;
|
|
267
|
+
totalArticles: number;
|
|
268
|
+
version: number;
|
|
269
|
+
articles: ArticleListItem[];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
interface Article {
|
|
273
|
+
id: string;
|
|
274
|
+
title: string;
|
|
275
|
+
slug: string;
|
|
276
|
+
contentHtml: string; // Styled HTML (with .earlyseo-article wrapper)
|
|
277
|
+
contentRawHtml: string; // Raw HTML (inherits your site theme)
|
|
278
|
+
contentCss: string; // Standalone CSS for .earlyseo-article
|
|
279
|
+
metaDescription: string;
|
|
280
|
+
createdAt: string;
|
|
281
|
+
updatedAt: string;
|
|
282
|
+
imageUrl?: string;
|
|
283
|
+
imageAlt?: string;
|
|
284
|
+
tags: string[];
|
|
285
|
+
summary?: string;
|
|
286
|
+
canonicalUrl?: string;
|
|
287
|
+
version: number;
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Imports
|
|
292
|
+
|
|
293
|
+
| Import Path | Contents |
|
|
294
|
+
|-------------|----------|
|
|
295
|
+
| `@earlyseo/blog` | `EarlySeoClient`, core types, CSS constants |
|
|
296
|
+
| `@earlyseo/blog/react` | Provider, hooks, components |
|
|
297
|
+
| `@earlyseo/blog/next` | Next.js page creators, metadata helpers |
|
|
298
|
+
| `@earlyseo/blog/css` | `ARTICLE_CSS`, `BLOG_CSS` |
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## License
|
|
303
|
+
|
|
304
|
+
MIT
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type EarlySeoConfig, type Manifest, type ArticleListPage, type Article } from "./types";
|
|
2
|
+
export declare class EarlySeoClient {
|
|
3
|
+
private baseUrl;
|
|
4
|
+
private siteId;
|
|
5
|
+
/** Additional fetch options (e.g. Next.js `{ next: { revalidate: 60 } }`) */
|
|
6
|
+
private fetchInit;
|
|
7
|
+
constructor(config: EarlySeoConfig, fetchInit?: RequestInit);
|
|
8
|
+
/** URL for the site manifest */
|
|
9
|
+
manifestUrl(): string;
|
|
10
|
+
/** URL for a paginated article list page (1-indexed) */
|
|
11
|
+
listPageUrl(page: number): string;
|
|
12
|
+
/** URL for a single article's full JSON */
|
|
13
|
+
articleUrl(slug: string): string;
|
|
14
|
+
/** Fetch the site manifest. Returns null on 404. */
|
|
15
|
+
getManifest(): Promise<Manifest | null>;
|
|
16
|
+
/** Fetch a paginated article list page (1-indexed). Returns null on 404. */
|
|
17
|
+
getListPage(page: number): Promise<ArticleListPage | null>;
|
|
18
|
+
/** Fetch a single article by slug. Returns null on 404. */
|
|
19
|
+
getArticle(slug: string): Promise<Article | null>;
|
|
20
|
+
private fetchJson;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAYA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,OAAO,EACb,MAAM,SAAS,CAAC;AAEjB,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAS;IACvB,6EAA6E;IAC7E,OAAO,CAAC,SAAS,CAAc;gBAEnB,MAAM,EAAE,cAAc,EAAE,SAAS,CAAC,EAAE,WAAW;IAQ3D,gCAAgC;IAChC,WAAW,IAAI,MAAM;IAIrB,wDAAwD;IACxD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAIjC,2CAA2C;IAC3C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAOhC,oDAAoD;IAC9C,WAAW,IAAI,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAI7C,4EAA4E;IACtE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAKhE,2DAA2D;IACrD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;YAMzC,SAAS;CAcxB"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// @earlyseo/blog — CDN Client
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// Thin read-only client that fetches pre-built JSON from the EarlySEO CDN.
|
|
5
|
+
// All writes happen server-side in our worker — users never write.
|
|
6
|
+
//
|
|
7
|
+
// CDN layout:
|
|
8
|
+
// /site/{siteId}/manifest.json → Manifest
|
|
9
|
+
// /site/{siteId}/articles/page-{n}.json → ArticleListPage
|
|
10
|
+
// /site/{siteId}/article/{slug}.json → Article
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
import { DEFAULT_CDN_BASE_URL, } from "./types";
|
|
13
|
+
export class EarlySeoClient {
|
|
14
|
+
constructor(config, fetchInit) {
|
|
15
|
+
this.siteId = config.siteId;
|
|
16
|
+
this.baseUrl = (config.cdnBaseUrl ?? DEFAULT_CDN_BASE_URL).replace(/\/+$/, "");
|
|
17
|
+
this.fetchInit = fetchInit ?? {};
|
|
18
|
+
}
|
|
19
|
+
// ─── URL builders (public so users can prefetch / build links) ─────────
|
|
20
|
+
/** URL for the site manifest */
|
|
21
|
+
manifestUrl() {
|
|
22
|
+
return `${this.baseUrl}/site/${this.siteId}/manifest.json`;
|
|
23
|
+
}
|
|
24
|
+
/** URL for a paginated article list page (1-indexed) */
|
|
25
|
+
listPageUrl(page) {
|
|
26
|
+
return `${this.baseUrl}/site/${this.siteId}/articles/page-${page}.json`;
|
|
27
|
+
}
|
|
28
|
+
/** URL for a single article's full JSON */
|
|
29
|
+
articleUrl(slug) {
|
|
30
|
+
const safe = slug.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
31
|
+
return `${this.baseUrl}/site/${this.siteId}/article/${safe}.json`;
|
|
32
|
+
}
|
|
33
|
+
// ─── Data fetchers ─────────────────────────────────────────────────────
|
|
34
|
+
/** Fetch the site manifest. Returns null on 404. */
|
|
35
|
+
async getManifest() {
|
|
36
|
+
return this.fetchJson(this.manifestUrl());
|
|
37
|
+
}
|
|
38
|
+
/** Fetch a paginated article list page (1-indexed). Returns null on 404. */
|
|
39
|
+
async getListPage(page) {
|
|
40
|
+
if (page < 1)
|
|
41
|
+
return null;
|
|
42
|
+
return this.fetchJson(this.listPageUrl(page));
|
|
43
|
+
}
|
|
44
|
+
/** Fetch a single article by slug. Returns null on 404. */
|
|
45
|
+
async getArticle(slug) {
|
|
46
|
+
return this.fetchJson(this.articleUrl(slug));
|
|
47
|
+
}
|
|
48
|
+
// ─── Internal ──────────────────────────────────────────────────────────
|
|
49
|
+
async fetchJson(url) {
|
|
50
|
+
const res = await fetch(url, {
|
|
51
|
+
...this.fetchInit,
|
|
52
|
+
headers: {
|
|
53
|
+
Accept: "application/json",
|
|
54
|
+
...(this.fetchInit.headers ?? {}),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
if (res.status === 404)
|
|
58
|
+
return null;
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
throw new Error(`EarlySEO CDN error: ${res.status} ${res.statusText} for ${url}`);
|
|
61
|
+
}
|
|
62
|
+
return res.json();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,8BAA8B;AAC9B,gFAAgF;AAChF,2EAA2E;AAC3E,mEAAmE;AACnE,EAAE;AACF,cAAc;AACd,sDAAsD;AACtD,6DAA6D;AAC7D,qDAAqD;AACrD,gFAAgF;AAEhF,OAAO,EACL,oBAAoB,GAKrB,MAAM,SAAS,CAAC;AAEjB,MAAM,OAAO,cAAc;IAMzB,YAAY,MAAsB,EAAE,SAAuB;QACzD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,OAAO,GAAG,CAAC,MAAM,CAAC,UAAU,IAAI,oBAAoB,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC/E,IAAI,CAAC,SAAS,GAAG,SAAS,IAAI,EAAE,CAAC;IACnC,CAAC;IAED,0EAA0E;IAE1E,gCAAgC;IAChC,WAAW;QACT,OAAO,GAAG,IAAI,CAAC,OAAO,SAAS,IAAI,CAAC,MAAM,gBAAgB,CAAC;IAC7D,CAAC;IAED,wDAAwD;IACxD,WAAW,CAAC,IAAY;QACtB,OAAO,GAAG,IAAI,CAAC,OAAO,SAAS,IAAI,CAAC,MAAM,kBAAkB,IAAI,OAAO,CAAC;IAC1E,CAAC;IAED,2CAA2C;IAC3C,UAAU,CAAC,IAAY;QACrB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC;QAClD,OAAO,GAAG,IAAI,CAAC,OAAO,SAAS,IAAI,CAAC,MAAM,YAAY,IAAI,OAAO,CAAC;IACpE,CAAC;IAED,0EAA0E;IAE1E,oDAAoD;IACpD,KAAK,CAAC,WAAW;QACf,OAAO,IAAI,CAAC,SAAS,CAAW,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,4EAA4E;IAC5E,KAAK,CAAC,WAAW,CAAC,IAAY;QAC5B,IAAI,IAAI,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QAC1B,OAAO,IAAI,CAAC,SAAS,CAAkB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;IACjE,CAAC;IAED,2DAA2D;IAC3D,KAAK,CAAC,UAAU,CAAC,IAAY;QAC3B,OAAO,IAAI,CAAC,SAAS,CAAU,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,0EAA0E;IAElE,KAAK,CAAC,SAAS,CAAI,GAAW;QACpC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC3B,GAAG,IAAI,CAAC,SAAS;YACjB,OAAO,EAAE;gBACP,MAAM,EAAE,kBAAkB;gBAC1B,GAAG,CAAE,IAAI,CAAC,SAAS,CAAC,OAAkC,IAAI,EAAE,CAAC;aAC9D;SACF,CAAC,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QACpC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,uBAAuB,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,QAAQ,GAAG,EAAE,CAAC,CAAC;QACpF,CAAC;QACD,OAAO,GAAG,CAAC,IAAI,EAAgB,CAAC;IAClC,CAAC;CACF"}
|
package/dist/css.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Companion CSS for the `.earlyseo-article` wrapper. All color defaults
|
|
3
|
+
* are semi-transparent so they adapt to both light and dark backgrounds.
|
|
4
|
+
* Every color is overridable via CSS custom properties.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import { ARTICLE_CSS } from '@earlyseo/blog/css';
|
|
8
|
+
* // Inject via <style> tag or your CSS-in-JS solution
|
|
9
|
+
*/
|
|
10
|
+
export declare const ARTICLE_CSS: string;
|
|
11
|
+
/**
|
|
12
|
+
* Minimal blog layout CSS — provides sensible defaults for the blog listing
|
|
13
|
+
* and article detail pages. All values are overridable via CSS custom properties.
|
|
14
|
+
*/
|
|
15
|
+
export declare const BLOG_CSS: string;
|
|
16
|
+
//# sourceMappingURL=css.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"css.d.ts","sourceRoot":"","sources":["../src/css.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,QAsChB,CAAC;AAET;;;GAGG;AACH,eAAO,MAAM,QAAQ,QAiIb,CAAC"}
|
package/dist/css.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// @earlyseo/blog — Article CSS
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
/**
|
|
5
|
+
* Companion CSS for the `.earlyseo-article` wrapper. All color defaults
|
|
6
|
+
* are semi-transparent so they adapt to both light and dark backgrounds.
|
|
7
|
+
* Every color is overridable via CSS custom properties.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { ARTICLE_CSS } from '@earlyseo/blog/css';
|
|
11
|
+
* // Inject via <style> tag or your CSS-in-JS solution
|
|
12
|
+
*/
|
|
13
|
+
export const ARTICLE_CSS = `
|
|
14
|
+
.earlyseo-article {
|
|
15
|
+
color: var(--earlyseo-text-color, inherit);
|
|
16
|
+
font-size: 16px;
|
|
17
|
+
line-height: 1.75;
|
|
18
|
+
word-wrap: break-word;
|
|
19
|
+
font-family: var(--earlyseo-font-family, inherit);
|
|
20
|
+
}
|
|
21
|
+
.earlyseo-article > :first-child { margin-top: 0; }
|
|
22
|
+
.earlyseo-article > :last-child { margin-bottom: 0; }
|
|
23
|
+
.earlyseo-article h1,.earlyseo-article h2,.earlyseo-article h3,.earlyseo-article h4,.earlyseo-article h5,.earlyseo-article h6 {
|
|
24
|
+
margin-top: 1.6em; margin-bottom: 0.6em; font-weight: 700; line-height: 1.3;
|
|
25
|
+
color: var(--earlyseo-heading-color, inherit);
|
|
26
|
+
}
|
|
27
|
+
.earlyseo-article h1 { font-size: 2rem; }
|
|
28
|
+
.earlyseo-article h2 { font-size: 1.5rem; padding-bottom: 0.25rem; border-bottom: 1px solid var(--earlyseo-border-color, rgba(128,128,128,0.2)); }
|
|
29
|
+
.earlyseo-article h3 { font-size: 1.25rem; }
|
|
30
|
+
.earlyseo-article h4 { font-size: 1.05rem; }
|
|
31
|
+
.earlyseo-article p,.earlyseo-article ul,.earlyseo-article ol,.earlyseo-article dl,.earlyseo-article table,.earlyseo-article blockquote,.earlyseo-article pre,.earlyseo-article figure {
|
|
32
|
+
margin-top: 0; margin-bottom: 1rem;
|
|
33
|
+
}
|
|
34
|
+
.earlyseo-article a { color: var(--earlyseo-link-color, inherit); text-decoration: underline; text-underline-offset: 2px; }
|
|
35
|
+
.earlyseo-article a:hover { text-decoration-thickness: 2px; }
|
|
36
|
+
.earlyseo-article ul,.earlyseo-article ol { padding-left: 1.5rem; }
|
|
37
|
+
.earlyseo-article li + li { margin-top: 0.3rem; }
|
|
38
|
+
.earlyseo-article li > p { margin-bottom: 0.4rem; }
|
|
39
|
+
.earlyseo-article blockquote { margin-left: 0; padding: 0.25rem 1rem; color: var(--earlyseo-muted-color, inherit); opacity: 0.75; border-left: 4px solid var(--earlyseo-border-color, rgba(128,128,128,0.2)); }
|
|
40
|
+
.earlyseo-article blockquote > * { opacity: 1; }
|
|
41
|
+
.earlyseo-article code { padding: 0.12rem 0.35rem; border-radius: 0.375rem; background: var(--earlyseo-code-bg, rgba(128,128,128,0.1)); font-size: 0.88em; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
|
42
|
+
.earlyseo-article pre { overflow: auto; padding: 1rem; border-radius: 0.75rem; background: var(--earlyseo-pre-bg, rgba(128,128,128,0.06)); border: 1px solid var(--earlyseo-border-color, rgba(128,128,128,0.2)); }
|
|
43
|
+
.earlyseo-article pre code { background: transparent; padding: 0; border-radius: 0; font-size: 0.9em; line-height: 1.55; }
|
|
44
|
+
.earlyseo-article table { display: table; width: 100%; overflow-x: auto; border-collapse: collapse; }
|
|
45
|
+
.earlyseo-article th,.earlyseo-article td { padding: 0.55rem 0.75rem; border: 1px solid var(--earlyseo-border-color, rgba(128,128,128,0.2)); text-align: left; }
|
|
46
|
+
.earlyseo-article th { font-weight: 600; background: var(--earlyseo-th-bg, rgba(128,128,128,0.1)); }
|
|
47
|
+
.earlyseo-article img { max-width: 100%; height: auto; border-radius: 0.75rem; }
|
|
48
|
+
.earlyseo-article hr { margin: 1.5rem 0; border: 0; border-top: 1px solid var(--earlyseo-border-color, rgba(128,128,128,0.2)); }
|
|
49
|
+
.earlyseo-article .task-list-item { list-style: none; }
|
|
50
|
+
.earlyseo-article input[type="checkbox"] { margin-right: 0.45rem; }
|
|
51
|
+
`.trim();
|
|
52
|
+
/**
|
|
53
|
+
* Minimal blog layout CSS — provides sensible defaults for the blog listing
|
|
54
|
+
* and article detail pages. All values are overridable via CSS custom properties.
|
|
55
|
+
*/
|
|
56
|
+
export const BLOG_CSS = `
|
|
57
|
+
.earlyseo-blog {
|
|
58
|
+
max-width: var(--earlyseo-blog-max-width, 72rem);
|
|
59
|
+
margin: 0 auto;
|
|
60
|
+
padding: var(--earlyseo-blog-padding, 2rem 1rem);
|
|
61
|
+
font-family: var(--earlyseo-font-family, inherit);
|
|
62
|
+
color: var(--earlyseo-text-color, inherit);
|
|
63
|
+
}
|
|
64
|
+
.earlyseo-blog-header {
|
|
65
|
+
margin-bottom: 2rem;
|
|
66
|
+
}
|
|
67
|
+
.earlyseo-blog-header h1 {
|
|
68
|
+
font-size: 2rem;
|
|
69
|
+
font-weight: 700;
|
|
70
|
+
margin: 0 0 0.5rem;
|
|
71
|
+
}
|
|
72
|
+
.earlyseo-blog-grid {
|
|
73
|
+
display: grid;
|
|
74
|
+
grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
|
|
75
|
+
gap: 2rem;
|
|
76
|
+
}
|
|
77
|
+
.earlyseo-blog-card {
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
border-radius: 0.75rem;
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
border: 1px solid var(--earlyseo-border-color, rgba(128,128,128,0.2));
|
|
83
|
+
transition: box-shadow 0.15s;
|
|
84
|
+
}
|
|
85
|
+
.earlyseo-blog-card:hover {
|
|
86
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
|
87
|
+
}
|
|
88
|
+
.earlyseo-blog-card a {
|
|
89
|
+
text-decoration: none;
|
|
90
|
+
color: inherit;
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-direction: column;
|
|
93
|
+
height: 100%;
|
|
94
|
+
}
|
|
95
|
+
.earlyseo-blog-card-image {
|
|
96
|
+
width: 100%;
|
|
97
|
+
aspect-ratio: 16/9;
|
|
98
|
+
object-fit: cover;
|
|
99
|
+
}
|
|
100
|
+
.earlyseo-blog-card-body {
|
|
101
|
+
padding: 1rem 1.25rem;
|
|
102
|
+
flex: 1;
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
}
|
|
106
|
+
.earlyseo-blog-card-title {
|
|
107
|
+
font-size: 1.15rem;
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
margin: 0 0 0.5rem;
|
|
110
|
+
line-height: 1.3;
|
|
111
|
+
}
|
|
112
|
+
.earlyseo-blog-card-desc {
|
|
113
|
+
font-size: 0.9rem;
|
|
114
|
+
opacity: 0.7;
|
|
115
|
+
margin: 0 0 0.75rem;
|
|
116
|
+
flex: 1;
|
|
117
|
+
display: -webkit-box;
|
|
118
|
+
-webkit-line-clamp: 3;
|
|
119
|
+
-webkit-box-orient: vertical;
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
}
|
|
122
|
+
.earlyseo-blog-card-meta {
|
|
123
|
+
display: flex;
|
|
124
|
+
gap: 0.5rem;
|
|
125
|
+
flex-wrap: wrap;
|
|
126
|
+
font-size: 0.8rem;
|
|
127
|
+
opacity: 0.5;
|
|
128
|
+
}
|
|
129
|
+
.earlyseo-blog-tag {
|
|
130
|
+
display: inline-block;
|
|
131
|
+
padding: 0.15rem 0.5rem;
|
|
132
|
+
border-radius: 9999px;
|
|
133
|
+
background: var(--earlyseo-tag-bg, rgba(128,128,128,0.1));
|
|
134
|
+
font-size: 0.75rem;
|
|
135
|
+
}
|
|
136
|
+
.earlyseo-blog-post {
|
|
137
|
+
max-width: var(--earlyseo-post-max-width, 48rem);
|
|
138
|
+
margin: 0 auto;
|
|
139
|
+
padding: var(--earlyseo-blog-padding, 2rem 1rem);
|
|
140
|
+
}
|
|
141
|
+
.earlyseo-blog-post-header {
|
|
142
|
+
margin-bottom: 2rem;
|
|
143
|
+
}
|
|
144
|
+
.earlyseo-blog-post-header h1 {
|
|
145
|
+
font-size: 2.25rem;
|
|
146
|
+
font-weight: 700;
|
|
147
|
+
margin: 0 0 0.75rem;
|
|
148
|
+
line-height: 1.2;
|
|
149
|
+
}
|
|
150
|
+
.earlyseo-blog-post-meta {
|
|
151
|
+
display: flex;
|
|
152
|
+
gap: 1rem;
|
|
153
|
+
flex-wrap: wrap;
|
|
154
|
+
font-size: 0.85rem;
|
|
155
|
+
opacity: 0.6;
|
|
156
|
+
}
|
|
157
|
+
.earlyseo-blog-post-image {
|
|
158
|
+
width: 100%;
|
|
159
|
+
border-radius: 0.75rem;
|
|
160
|
+
margin-bottom: 2rem;
|
|
161
|
+
}
|
|
162
|
+
.earlyseo-blog-empty {
|
|
163
|
+
text-align: center;
|
|
164
|
+
padding: 4rem 1rem;
|
|
165
|
+
opacity: 0.5;
|
|
166
|
+
}
|
|
167
|
+
.earlyseo-blog-pagination {
|
|
168
|
+
display: flex;
|
|
169
|
+
justify-content: center;
|
|
170
|
+
gap: 0.5rem;
|
|
171
|
+
margin-top: 2rem;
|
|
172
|
+
}
|
|
173
|
+
.earlyseo-blog-pagination button {
|
|
174
|
+
padding: 0.5rem 1rem;
|
|
175
|
+
border: 1px solid var(--earlyseo-border-color, rgba(128,128,128,0.2));
|
|
176
|
+
border-radius: 0.375rem;
|
|
177
|
+
background: transparent;
|
|
178
|
+
cursor: pointer;
|
|
179
|
+
font-size: 0.85rem;
|
|
180
|
+
}
|
|
181
|
+
.earlyseo-blog-pagination button:disabled {
|
|
182
|
+
opacity: 0.3;
|
|
183
|
+
cursor: not-allowed;
|
|
184
|
+
}
|
|
185
|
+
`.trim();
|
|
186
|
+
//# sourceMappingURL=css.js.map
|
package/dist/css.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"css.js","sourceRoot":"","sources":["../src/css.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,+BAA+B;AAC/B,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsC1B,CAAC,IAAI,EAAE,CAAC;AAET;;;GAGG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiIvB,CAAC,IAAI,EAAE,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { EarlySeoClient } from "./client";
|
|
2
|
+
export type { Manifest, ArticleListItem, ArticleListPage, Article, EarlySeoConfig, } from "./types";
|
|
3
|
+
export { DEFAULT_CDN_BASE_URL } from "./types";
|
|
4
|
+
export { ARTICLE_CSS, BLOG_CSS } from "./css";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAG1C,YAAY,EACV,QAAQ,EACR,eAAe,EACf,eAAe,EACf,OAAO,EACP,cAAc,GACf,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// @earlyseo/blog — Main Entry Point
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// Core client, types, and CSS. For framework-specific imports use:
|
|
5
|
+
// @earlyseo/blog/react — React hooks & components
|
|
6
|
+
// @earlyseo/blog/next — Next.js pages & metadata helpers
|
|
7
|
+
// @earlyseo/blog/css — Article & blog CSS constants
|
|
8
|
+
// Client
|
|
9
|
+
export { EarlySeoClient } from "./client";
|
|
10
|
+
export { DEFAULT_CDN_BASE_URL } from "./types";
|
|
11
|
+
// CSS
|
|
12
|
+
export { ARTICLE_CSS, BLOG_CSS } from "./css";
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,oCAAoC;AACpC,gFAAgF;AAChF,mEAAmE;AACnE,qDAAqD;AACrD,6DAA6D;AAC7D,yDAAyD;AAEzD,SAAS;AACT,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAW1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAE/C,MAAM;AACN,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A "← Go back" link that navigates to the previous page via browser history,
|
|
4
|
+
* or falls back to `fallbackHref` (default: "/") if there is no history.
|
|
5
|
+
*/
|
|
6
|
+
export declare function BackLink({ fallbackHref, children, style, className, }: {
|
|
7
|
+
fallbackHref?: string;
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
style?: React.CSSProperties;
|
|
10
|
+
className?: string;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=back-link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"back-link.d.ts","sourceRoot":"","sources":["../../src/next/back-link.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,EACvB,YAAkB,EAClB,QAAsB,EACtB,KAAK,EACL,SAAS,GACV,EAAE;IACD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,2CAgBA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* A "← Go back" link that navigates to the previous page via browser history,
|
|
5
|
+
* or falls back to `fallbackHref` (default: "/") if there is no history.
|
|
6
|
+
*/
|
|
7
|
+
export function BackLink({ fallbackHref = "/", children = "← Go back", style, className, }) {
|
|
8
|
+
return (_jsx("a", { href: fallbackHref, className: className, style: style, onClick: (e) => {
|
|
9
|
+
if (window.history.length > 1) {
|
|
10
|
+
e.preventDefault();
|
|
11
|
+
window.history.back();
|
|
12
|
+
}
|
|
13
|
+
}, children: children }));
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=back-link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"back-link.js","sourceRoot":"","sources":["../../src/next/back-link.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAIb;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,EACvB,YAAY,GAAG,GAAG,EAClB,QAAQ,GAAG,WAAW,EACtB,KAAK,EACL,SAAS,GAMV;IACC,OAAO,CACL,YACE,IAAI,EAAE,YAAY,EAClB,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;YACb,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC9B,CAAC,CAAC,cAAc,EAAE,CAAC;gBACnB,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YACxB,CAAC;QACH,CAAC,YAEA,QAAQ,GACP,CACL,CAAC;AACJ,CAAC"}
|