设计实践

把 Markdown 写的门店动态批量导入 Framer CMS

把 Markdown 写的门店动态批量导入 Framer CMS:脚本读取 posts/.md 的标题、日期、分类、封面和正文,正文转成 HTML 供“格式化文本”字段使用,同时检查重复 slug、日期格式和非 https 图片,输出可导入的 CSV。

BINBHO · 图文教程

把 Markdown 写的门店动态批量导入 Framer CMS:脚本读取 posts/*.md 的标题、日期、分类、封面和正文,正文转成 HTML 供“格式化文本”字段使用,同时检查重复 slug、日期格式和非 https 图片,输出可导入的 CSV。

先看清成品

脚本 2 个测试通过;样例 3 篇文章输出 3 行 CSV,并报出 3 个问题(封面不是 https、slug 重复、日期格式错)。没有在 Framer 中实际导入,字段映射界面以 Framer 为准。

注意:Framer 的计划、价格和自定义域名、CMS 条目数等限额以官方价格页为准,本文不列具体数字。只使用你有权使用的图片、字体和文案;不要共享账号。

做完之后你会得到什么?

成果 说明
文章集合 Title、Slug、Date、Category、Cover、Content
转换脚本 Markdown → HTML,导入后保留标题、加粗、列表、链接
导入前检查 slug 重复、日期、图片地址

需要准备哪些环境?

项目 要求 说明
Framer 项目 有 CMS 的计划 条目上限以价格页为准
Python 3.9 以上 pip install markdown
图片 公开可访问的 https 地址 导入时 Framer 会读取图片

文件结构

26-framer-B-src/
├── md_to_framer_csv.py
├── test_convert.py
└── posts/
    ├── 01-opening.md
    ├── 02-beans.md       # 故意用 http 图片
    └── 03-dup.md         # 故意重复标题、错误日期

第 1 步:在 Framer 建集合

CMS → 新建集合 门店动态,字段:

字段 类型
Title 纯文本(集合自带标题字段)
Slug Slug(集合自带)
Date 日期
Category 选项或纯文本
Cover 图片
Content 格式化文本

第 2 步:写文章

posts/01-opening.md:

---
title: 新店开业,第一周全场八折
date: 2026-11-01
category: 门店动态
cover: https://images.example.com/opening.jpg
---
## 开业时间

**11 月 1 日** 起正式营业,营业时间 10:00–20:00。

- 前 50 位到店顾客送挂耳咖啡
- 会员注册送积分

第 3 步:转换脚本

md_to_framer_csv.py:

"""把 posts/*.md(带简单头信息)转成 Framer CMS 可导入的 CSV。
头信息格式(文件开头,--- 包围):title / date / cover / category / slug(可选)
正文 Markdown 转成 HTML,导入时映射到“格式化文本(Formatted Text)”字段。
"""
import csv, re, sys, unicodedata
from datetime import date
from pathlib import Path
import markdown

FIELDS = ["Title", "Slug", "Date", "Category", "Cover", "Content"]

def slugify(s):
    s = unicodedata.normalize("NFKC", s).lower()
    s = re.sub(r"[^\w\u4e00-\u9fff]+", "-", s).strip("-")
    return re.sub(r"-{2,}", "-", s)[:60]

def parse(text):
    m = re.match(r"^---\n(.*?)\n---\n(.*)$", text, re.S)
    if not m: raise ValueError("缺少 --- 头信息")
    meta = dict(line.split(":", 1) for line in m.group(1).splitlines() if ":" in line)
    return {k.strip(): v.strip() for k, v in meta.items()}, m.group(2)

def convert(paths):
    rows, problems, slugs = [], [], set()
    for p in sorted(paths):
        try:
            meta, body = parse(Path(p).read_text(encoding="utf-8"))
        except ValueError as e:
            problems.append(f"{p.name}: {e}"); continue
        title = meta.get("title", "")
        slug = meta.get("slug") or slugify(title)
        if not title: problems.append(f"{p.name}: 缺少 title")
        if slug in slugs: problems.append(f"{p.name}: slug “{slug}” 重复")
        slugs.add(slug)
        try: date.fromisoformat(meta.get("date", ""))
        except ValueError: problems.append(f"{p.name}: date 应为 YYYY-MM-DD")
        cover = meta.get("cover", "")
        if cover and not cover.startswith("https://"): problems.append(f"{p.name}: cover 需要是 https 公开图片地址")
        rows.append({"Title": title, "Slug": slug, "Date": meta.get("date", ""), "Category": meta.get("category", ""),
                     "Cover": cover, "Content": markdown.markdown(body.strip(), extensions=["tables"])})
    return rows, problems

if __name__ == "__main__":
    rows, problems = convert(Path(sys.argv[1]).glob("*.md"))
    with open(sys.argv[2], "w", encoding="utf-8", newline="") as f:
        w = csv.DictWriter(f, fieldnames=FIELDS); w.writeheader(); w.writerows(rows)
    print(f"写出 {sys.argv[2]}:{len(rows)} 条")
    for p in problems: print("⚠", p)
    sys.exit(1 if problems else 0)
pip install markdown pytest
python md_to_framer_csv.py posts framer_import.csv
python -m pytest -q

样例的本机实际输出:

写出 out.csv:3 条
⚠ 02-beans.md: cover 需要是 https 公开图片地址
⚠ 03-dup.md: slug “新店开业-第一周全场八折” 重复
⚠ 03-dup.md: date 应为 YYYY-MM-DD

(本机运行时输出文件名用的是 out.csv。)第一行的 Content 列为:

<h2>开业时间</h2>
<p><strong>11 月 1 日</strong> 起正式营业,营业时间 10:00–20:00。</p>
<ul>
<li>前 50 位到店顾客送挂耳咖啡</li>
<li>会员注册送积分</li>
</ul>

改完源文件,直到没有 ⚠ 再导入。

第 4 步:导入

  1. 打开集合,点工具栏的 Import,选择 CSV。
  2. 逐列确认映射:Content → 格式化文本,Cover → 图片,Date → 日期。
  3. 先用 2~3 篇文章试导,检查列表页和详情页,再导入全部。

检查结果:详情页里标题、加粗、列表、链接都正常显示;条目数与 CSV 行数一致。

调试示例

slug 默认由中文标题生成,例如 新店开业-第一周全场八折。如果 Framer 导入时提示 slug 不可用,或者你希望网址更短,在头信息中加 slug: opening-week 指定英文 slug,脚本会优先使用。

出错了怎么办?

现象 原因 解决
正文显示成 HTML 源码 Content 映射成了纯文本 改为格式化文本字段
图片没导进来 图片地址不是公开 https 或有防盗链 换成公开地址
日期错一天 时区换算 只写日期 YYYY-MM-DD
重复导入后条目翻倍 slug 不一致 保持 slug 稳定,用于更新已有条目
中文乱码 CSV 编码不是 UTF-8 使用脚本输出,不要用 Excel 另存

模板规则

  • 内容以 Markdown 为准,CSV 只是中间产物。
  • slug 一旦发布不改,避免旧链接失效。
  • 先试导少量,再全量。

项目资源

用途 地址
Framer CMS CSV 导入 https://www.framer.com/academy/lessons/csv-import
Python-Markdown https://python-markdown.github.io/
Framer 价格 https://www.framer.com/pricing

把内容放在 Markdown 里写、用脚本检查后再导入,CMS 里就不会出现重复页面和失效图片;以后换建站工具,内容也能原样带走。