Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,48 @@ npm run dev

---

## ✅ 项目可用性自检(2026-04-04)

本地已验证:

- TypeScript 类型检查可通过:`npm run lint`
- 生产构建可通过:`npm run build`
- `lint:eslint` 当前会失败(原因:仓库还未提供 ESLint v9 的 `eslint.config.*`)

> 建议下一步:补齐 ESLint v9 flat config,保证 CI 一次性通过。

---

## 🚀 爆火增长玩法(开源项目实战版)

想让项目“爆火”,核心不是只做功能,而是**降低首次成功成本 + 提高传播素材密度**:

1. **首屏 10 秒打动人**:保留 GIF,但再加 3 张“前后对比图”(原始 Markdown vs 发布效果)。
2. **给出可复制模板**:增加「公众号开场白模板」「技术长文模板」「小红书卡片模板」三个示例。
3. **做分发钩子**:每次导出后提示“本排版由 PageSkill 生成(可关闭)”,形成自然扩散。
4. **做 OpenClaw 场景案例**:放 1 个自动化流程图,告诉用户“输入主题 -> AI 生成 -> PageSkill 排版 -> 导出发布”。
5. **发布节奏固定化**:每周发布 1 个模板 + 1 条使用案例,持续占领搜索。

---

## 🧪 常用命令

```bash
# 启动开发环境
npm run dev

# 类型检查
npm run lint

# 构建产物
npm run build

# 预览构建结果
npm run preview
```

---

## 🎯 适用场景

- 公众号文章排版与发布
Expand Down Expand Up @@ -102,6 +144,32 @@ PageSkill/

---


## 🤖 OpenClaw 直接调度

已支持通过 URL 参数和 `postMessage` 直接调度 PageSkill:

- URL 参数:`?template=tech&view=mobile&dark=1&markdown=%23%20标题`
- 消息协议:

```js
window.postMessage({
type: 'pageskill.command',
payload: { action: 'setMarkdown', markdown: '# OpenClaw 调度成功' }
}, '*')
```

可用 action:`setMarkdown` / `setTemplate` / `setViewMode` / `toggleDark` / `copyRich` / `copyHTML` / `exportHTML` / `openPoster`。

OpenClaw 直接调用(宿主页面)示例:

```js
await window.PageSkillOpenClaw.dispatch({
action: 'setTemplate',
templateId: 'tech'
})
```

## 🗺️ Roadmap

- [x] Markdown 实时渲染
Expand Down
35 changes: 35 additions & 0 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,41 @@ Open: `http://localhost:5173`

---

## ✅ Project health check (April 4, 2026)

Validated locally:

- TypeScript check passes: `npm run lint`
- Production build passes: `npm run build`
- `lint:eslint` currently fails because the repo does not include ESLint v9 `eslint.config.*`

> Recommended next step: add ESLint v9 flat config to make CI checks fully green.

---

## 🚀 Growth playbook (to make this project spread faster)

If you want this project to take off, focus on **time-to-first-success** and **shareable output**:

1. Keep GIF demo, and add 3 before/after screenshots.
2. Add copy-ready template packs (WeChat intro, tech tutorial, social card).
3. Add export watermark toggle ("Made with PageSkill") for organic distribution.
4. Publish one OpenClaw automation flow example.
5. Ship one template + one real user case every week.

---

## 🧪 Common commands

```bash
npm run dev
npm run lint
npm run build
npm run preview
```

---

## 🎯 Use cases

- WeChat article layout and publishing
Expand Down
130 changes: 128 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,31 @@ import {
copyRichHTML,
copyHTMLSource,
} from './utils/renderer'
import {
parseOpenClawURLParams,
resolveTemplateById,
isOpenClawMessage,
postOpenClawAck,
type OpenClawCommand,
type ViewMode,
} from './utils/openclaw'

type ViewMode = 'mobile' | 'tablet' | 'desktop'
type ToastType = 'success' | 'error'

declare global {
interface Window {
PageSkillOpenClaw?: {
dispatch: (command: OpenClawCommand) => Promise<boolean>
getState: () => {
markdown: string
templateId: string
viewMode: ViewMode
darkMode: boolean
}
}
}
}

function Toast({
message,
type,
Expand Down Expand Up @@ -127,8 +148,113 @@ export default function App() {
showToast('HTML 文件已下载', 'success')
}, [markdown, previewHTML, template.styles, showToast])

const dispatchOpenClawCommand = useCallback(async (command: OpenClawCommand): Promise<boolean> => {
switch (command.action) {
case 'setMarkdown':
if (typeof command.markdown === 'string') {
setMarkdown(command.markdown)
showToast('已接收 OpenClaw 内容调度', 'success')
return true
}
return false
case 'setTemplate': {
const resolved = resolveTemplateById(allTemplates, command.templateId)
if (resolved) {
setTemplate(resolved)
return true
}
return false
}
case 'setViewMode':
if (command.viewMode) {
setViewMode(command.viewMode)
return true
}
return false
case 'toggleDark':
if (typeof command.darkMode === 'boolean') {
setDarkMode(command.darkMode)
} else {
setDarkMode((prev) => !prev)
}
return true
case 'copyRich':
return handleCopyRich()
case 'copyHTML':
return handleCopyHTML()
case 'exportHTML':
handleExportHTML()
return true
case 'openPoster':
setPosterOpen(true)
return true
default:
return false
}
}, [handleCopyHTML, handleCopyRich, handleExportHTML, showToast])

useEffect(() => {
const { templateId, markdown: markdownByURL, viewMode: viewModeByURL, darkMode: darkModeByURL } =
parseOpenClawURLParams(window.location.href)

if (templateId) {
const resolved = resolveTemplateById(allTemplates, templateId)
if (resolved) {
setTemplate(resolved)
}
}

if (markdownByURL) {
setMarkdown(markdownByURL)
showToast('已从 OpenClaw URL 参数导入内容', 'success')
}

if (viewModeByURL && ['mobile', 'tablet', 'desktop'].includes(viewModeByURL)) {
setViewMode(viewModeByURL)
}

if (typeof darkModeByURL === 'boolean') {
setDarkMode(darkModeByURL)
}
}, [showToast])

useEffect(() => {
const onMessage = (event: MessageEvent) => {
if (!isOpenClawMessage(event.data)) {
return
}

const command = event.data.payload
if (!command) {
postOpenClawAck({ ok: false, action: 'setMarkdown', message: 'missing payload' })
return
}

void dispatchOpenClawCommand(command).then((ok) => {
postOpenClawAck({ ok, action: command.action, message: ok ? 'ok' : 'invalid command' })
})
}

window.addEventListener('message', onMessage)

window.PageSkillOpenClaw = {
dispatch: dispatchOpenClawCommand,
getState: () => ({
markdown,
templateId: template.id,
viewMode,
darkMode,
}),
}

return () => {
window.removeEventListener('message', onMessage)
delete window.PageSkillOpenClaw
}
}, [darkMode, dispatchOpenClawCommand, markdown, template.id, viewMode])

return (
<div className="h-screen flex flex-col overflow-hidden">
<div className="h-screen flex flex-col overflow-hidden app-shell">
<Header
template={template}
markdown={markdown}
Expand Down
3 changes: 2 additions & 1 deletion src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ export default function Header({
<div className="w-7 h-7 bg-gradient-to-br from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
<FileText size={14} className="text-white" />
</div>
<span className="font-semibold text-gray-800 text-sm hidden sm:inline">ArticleLayout</span>
<span className="font-semibold text-gray-800 text-sm hidden sm:inline">PageSkill</span>
<span className="hidden xl:inline text-[10px] text-emerald-600 bg-emerald-50 px-1.5 py-0.5 rounded-full">OpenClaw Ready</span>
</div>

<div className="hidden md:flex items-center gap-1 ml-3 px-2 py-1 bg-gray-50 rounded-md">
Expand Down
79 changes: 49 additions & 30 deletions src/components/Preview/Preview.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react'
import { Smartphone, Tablet, Monitor } from 'lucide-react'

interface PreviewProps {
html: string
Expand All @@ -8,45 +9,63 @@ interface PreviewProps {
}

const viewModeWidths = {
mobile: '375px',
tablet: '768px',
mobile: '390px',
tablet: '820px',
desktop: '100%',
}

const viewModeMeta = {
mobile: { label: '移动端', icon: Smartphone },
tablet: { label: '平板端', icon: Tablet },
desktop: { label: '桌面端', icon: Monitor },
}

export default function Preview({ html, css, templateId, viewMode }: PreviewProps) {
const previewWidth = viewModeWidths[viewMode]
const isMobileOrTablet = viewMode !== 'desktop'
const { label, icon: Icon } = viewModeMeta[viewMode]

return (
<div className="h-full bg-gray-100 overflow-y-auto flex justify-center">
<div
className={`
transition-all duration-300 ease-out
${isMobileOrTablet ? 'my-6 shadow-2xl rounded-2xl overflow-hidden border border-gray-200' : 'w-full'}
`}
style={{
width: isMobileOrTablet ? previewWidth : '100%',
maxWidth: isMobileOrTablet ? previewWidth : '100%',
minHeight: isMobileOrTablet ? '600px' : 'auto',
}}
>
{isMobileOrTablet && (
<div className="bg-gray-800 px-4 py-2 flex items-center justify-center">
<div className="w-20 h-1 bg-gray-600 rounded-full" />
</div>
)}

<style dangerouslySetInnerHTML={{ __html: css }} />
<div className="h-full preview-surface overflow-y-auto flex justify-center">
<div className="pt-4 pb-8 px-4 w-full">
<div className="mb-3 text-xs text-gray-500 flex items-center gap-2">
<Icon size={14} />
<span>{label}预览</span>
<span className="text-gray-300">•</span>
<span className="uppercase tracking-wide">template {templateId}</span>
</div>

<div
className={`article-preview template-${templateId} animate-fade-in`}
dangerouslySetInnerHTML={{ __html: html }}
/>

{isMobileOrTablet && (
<div className="bg-gray-800 px-4 py-3 flex items-center justify-center">
<div className="w-10 h-10 rounded-full border-2 border-gray-600" />
</div>
)}
className={[
'transition-all duration-300 ease-out',
isMobileOrTablet
? 'mx-auto shadow-2xl rounded-2xl overflow-hidden border border-gray-200 bg-white'
: 'w-full rounded-xl border border-gray-200 overflow-hidden bg-white shadow-sm',
].join(' ')}
style={{
width: isMobileOrTablet ? previewWidth : '100%',
maxWidth: isMobileOrTablet ? previewWidth : '100%',
minHeight: isMobileOrTablet ? '640px' : 'auto',
}}
>
{isMobileOrTablet && (
<div className="bg-gray-900/95 px-4 py-2 flex items-center justify-center">
<div className="w-24 h-1 bg-gray-600 rounded-full" />
</div>
)}

<style dangerouslySetInnerHTML={{ __html: css }} />
<div
className={`article-preview template-${templateId} animate-fade-in`}
dangerouslySetInnerHTML={{ __html: html }}
/>

{isMobileOrTablet && (
<div className="bg-gray-900/95 px-4 py-3 flex items-center justify-center">
<div className="w-10 h-10 rounded-full border-2 border-gray-600" />
</div>
)}
</div>
</div>
</div>
)
Expand Down
11 changes: 11 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,14 @@ body {
.toast-enter {
animation: slideIn 0.3s ease-out;
}

.app-shell {
background: radial-gradient(circle at 0% 0%, #f8fafc 0%, #eef2ff 35%, #f8fafc 100%);
}

.preview-surface {
background-image:
linear-gradient(to right, rgba(148, 163, 184, 0.08) 1px, transparent 1px),
linear-gradient(to bottom, rgba(148, 163, 184, 0.08) 1px, transparent 1px);
background-size: 24px 24px;
}
Loading