adding export checklists and script to do the export
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,3 +22,4 @@ exports/
|
|||||||
|
|
||||||
# Claude Code
|
# Claude Code
|
||||||
.claude/
|
.claude/
|
||||||
|
.sneakycode/
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
## 1. PROJECT OVERVIEW
|
## 1. PROJECT OVERVIEW
|
||||||
|
|
||||||
**Series Title (working):** The Shade Series
|
**Series Title (working):** The Shade Series
|
||||||
**Author:** Phillip (writing as [pen name TBD])
|
**Author:** Phillip Tarrant
|
||||||
**Publishing Target:** Amazon Kindle Direct Publishing (KDP) — eBook + Print on Demand
|
**Publishing Target:** Amazon Kindle Direct Publishing (KDP) — eBook + Print on Demand
|
||||||
**Enrollment:** Kindle Unlimited (KU) eligible — exclusive to Amazon
|
**Enrollment:** Kindle Unlimited (KU) eligible — exclusive to Amazon
|
||||||
**Format:** Episodic fantasy series, 3+ books planned
|
**Format:** Episodic fantasy series, 3+ books planned
|
||||||
|
|||||||
446
export_kindle.py
Normal file
446
export_kindle.py
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Export Book chapters to a Kindle-ready EPUB file.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
venv/bin/python export_kindle.py --book 1
|
||||||
|
# or: source venv/bin/activate && python export_kindle.py --book 1
|
||||||
|
|
||||||
|
Requires: pip install -r requirements.txt (ebooklib, markdown)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import markdown
|
||||||
|
from ebooklib import epub
|
||||||
|
|
||||||
|
ROOT = Path(__file__).parent
|
||||||
|
KINDLE_CSS = """\
|
||||||
|
body {
|
||||||
|
font-family: Georgia, "Times New Roman", serif;
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.8em;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 2em;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
page-break-before: always;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0 0 0.8em 0;
|
||||||
|
text-indent: 1.5em;
|
||||||
|
}
|
||||||
|
p.first-para {
|
||||||
|
text-indent: 0;
|
||||||
|
}
|
||||||
|
p.scene-break {
|
||||||
|
text-align: center;
|
||||||
|
text-indent: 0;
|
||||||
|
margin: 1.5em 0;
|
||||||
|
letter-spacing: 0.5em;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
.title-page {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 4em;
|
||||||
|
margin-bottom: 4em;
|
||||||
|
}
|
||||||
|
.title-page h1 {
|
||||||
|
font-size: 2.2em;
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
page-break-before: avoid;
|
||||||
|
}
|
||||||
|
.title-page p {
|
||||||
|
text-indent: 0;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
em { font-style: italic; }
|
||||||
|
strong { font-weight: bold; }
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Markdown → HTML conversion
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def convert_md_to_html(text: str) -> str:
|
||||||
|
"""Convert markdown text to HTML with smart quotes and scene breaks."""
|
||||||
|
md = markdown.Markdown(extensions=["smarty"])
|
||||||
|
html = md.convert(text)
|
||||||
|
html = convert_scene_breaks(html)
|
||||||
|
html = inject_first_para_class(html)
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
|
def convert_scene_breaks(html: str) -> str:
|
||||||
|
"""Replace bare '* * *' paragraphs with styled scene breaks."""
|
||||||
|
return html.replace("<p>* * *</p>", '<p class="scene-break">* * *</p>')
|
||||||
|
|
||||||
|
|
||||||
|
def inject_first_para_class(html: str) -> str:
|
||||||
|
"""Add class='first-para' to paragraphs following h1 and scene breaks."""
|
||||||
|
# After </h1>
|
||||||
|
html = re.sub(
|
||||||
|
r"(</h1>\s*)<p>",
|
||||||
|
r'\1<p class="first-para">',
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
# After scene break
|
||||||
|
html = re.sub(
|
||||||
|
r'(<p class="scene-break">.*?</p>\s*)<p>',
|
||||||
|
r'\1<p class="first-para">',
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
|
def extract_title(text: str) -> str:
|
||||||
|
"""Extract the chapter title from the first # heading."""
|
||||||
|
match = re.match(r"^#\s+(.+)$", text.strip(), re.MULTILINE)
|
||||||
|
return match.group(1) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Chapter discovery
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def find_chapter_file(chapter_dir: Path, num: int) -> Path | None:
|
||||||
|
"""Find the best file for a chapter number (prefer final, fall back to draft)."""
|
||||||
|
for suffix in ("final", "draft"):
|
||||||
|
path = chapter_dir / f"ch{num:02d}-{suffix}.md"
|
||||||
|
if path.exists():
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_special_chapter(chapter_dir: Path, name: str) -> Path | None:
|
||||||
|
"""Find prologue or epilogue (prefer final, fall back to draft)."""
|
||||||
|
for suffix in ("final", "draft"):
|
||||||
|
path = chapter_dir / f"{name}-{suffix}.md"
|
||||||
|
if path.exists():
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def discover_chapters(chapter_dir: Path) -> list[tuple[str, Path]]:
|
||||||
|
"""Discover all chapter files in order: prologue, ch01..chNN, epilogue."""
|
||||||
|
chapters = []
|
||||||
|
|
||||||
|
# Prologue
|
||||||
|
prologue = find_special_chapter(chapter_dir, "prologue")
|
||||||
|
if prologue:
|
||||||
|
chapters.append(("prologue", prologue))
|
||||||
|
|
||||||
|
# Numbered chapters
|
||||||
|
nums = set()
|
||||||
|
for p in chapter_dir.glob("ch*-final.md"):
|
||||||
|
m = re.match(r"ch(\d+)", p.name)
|
||||||
|
if m:
|
||||||
|
nums.add(int(m.group(1)))
|
||||||
|
for p in chapter_dir.glob("ch*-draft.md"):
|
||||||
|
m = re.match(r"ch(\d+)", p.name)
|
||||||
|
if m:
|
||||||
|
nums.add(int(m.group(1)))
|
||||||
|
|
||||||
|
for num in sorted(nums):
|
||||||
|
path = find_chapter_file(chapter_dir, num)
|
||||||
|
if path:
|
||||||
|
chapters.append((f"ch{num:02d}", path))
|
||||||
|
|
||||||
|
# Epilogue
|
||||||
|
epilogue = find_special_chapter(chapter_dir, "epilogue")
|
||||||
|
if epilogue:
|
||||||
|
chapters.append(("epilogue", epilogue))
|
||||||
|
|
||||||
|
return chapters
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Template handling
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def is_stub(text: str) -> bool:
|
||||||
|
"""Check if a template file is a stub (placeholder with no real content)."""
|
||||||
|
# Strip headings, HTML comments, and whitespace
|
||||||
|
content = re.sub(r"^#+\s+.*$", "", text, flags=re.MULTILINE)
|
||||||
|
content = re.sub(r"<!--.*?-->", "", content, flags=re.DOTALL)
|
||||||
|
content = content.strip()
|
||||||
|
return len(content.split()) < 50
|
||||||
|
|
||||||
|
|
||||||
|
def load_template(path: Path) -> str | None:
|
||||||
|
"""Load a template file, returning None if it doesn't exist or is a stub."""
|
||||||
|
if not path.exists():
|
||||||
|
return None
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
if is_stub(text):
|
||||||
|
print(f" Skipping stub template: {path.name}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# EPUB construction
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def make_epub_chapter(filename: str, title: str, body_html: str) -> epub.EpubHtml:
|
||||||
|
"""Create an EPUB chapter item from HTML body content."""
|
||||||
|
chapter = epub.EpubHtml(
|
||||||
|
title=title,
|
||||||
|
file_name=filename,
|
||||||
|
lang="en",
|
||||||
|
)
|
||||||
|
chapter.set_content(body_html)
|
||||||
|
chapter.add_link(href="style/kindle.css", rel="stylesheet", type="text/css")
|
||||||
|
return chapter
|
||||||
|
|
||||||
|
|
||||||
|
def build_title_page(title: str, subtitle: str, author: str) -> epub.EpubHtml:
|
||||||
|
"""Generate a title page chapter."""
|
||||||
|
body = f"""\
|
||||||
|
<div class="title-page">
|
||||||
|
<h1>{title}</h1>
|
||||||
|
<p>{subtitle}</p>
|
||||||
|
<p>by {author}</p>
|
||||||
|
</div>"""
|
||||||
|
return make_epub_chapter("title.xhtml", "Title Page", body)
|
||||||
|
|
||||||
|
|
||||||
|
def build_epub(args) -> Path:
|
||||||
|
"""Build the EPUB file and return its path."""
|
||||||
|
book_num = args.book
|
||||||
|
title = args.title or "The Unbreakable Curse"
|
||||||
|
author = args.author or "Phillip Tarrant"
|
||||||
|
subtitle = f"Book {book_num} of The Shade Series"
|
||||||
|
|
||||||
|
chapter_dir = ROOT / "chapters" / f"book{book_num}"
|
||||||
|
if not chapter_dir.exists():
|
||||||
|
print(f"Error: Chapter directory not found: {chapter_dir}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
template_dir = ROOT / "kindle" / "templates"
|
||||||
|
export_dir = ROOT / "kindle" / "exports"
|
||||||
|
export_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# --- Create the EPUB book ---
|
||||||
|
book = epub.EpubBook()
|
||||||
|
book.set_identifier(f"shade-series-book{book_num}")
|
||||||
|
book.set_title(f"{title}: {subtitle}")
|
||||||
|
book.set_language("en")
|
||||||
|
book.add_author(author)
|
||||||
|
|
||||||
|
# Cover image
|
||||||
|
if args.cover:
|
||||||
|
cover_path = Path(args.cover)
|
||||||
|
if cover_path.exists():
|
||||||
|
cover_data = cover_path.read_bytes()
|
||||||
|
ext = cover_path.suffix.lower()
|
||||||
|
media_type = {
|
||||||
|
".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
||||||
|
".png": "image/png",
|
||||||
|
}.get(ext, "image/jpeg")
|
||||||
|
book.set_cover("cover" + ext, cover_data)
|
||||||
|
print(f" Cover: {cover_path.name}")
|
||||||
|
else:
|
||||||
|
print(f" Warning: Cover image not found: {args.cover}", file=sys.stderr)
|
||||||
|
|
||||||
|
# CSS
|
||||||
|
css = epub.EpubItem(
|
||||||
|
uid="kindle_css",
|
||||||
|
file_name="style/kindle.css",
|
||||||
|
media_type="text/css",
|
||||||
|
content=KINDLE_CSS,
|
||||||
|
)
|
||||||
|
book.add_item(css)
|
||||||
|
|
||||||
|
spine = ["nav"]
|
||||||
|
toc = []
|
||||||
|
|
||||||
|
# --- Title page ---
|
||||||
|
title_page = build_title_page(title, subtitle, author)
|
||||||
|
book.add_item(title_page)
|
||||||
|
spine.append(title_page)
|
||||||
|
|
||||||
|
# --- Copyright page ---
|
||||||
|
copyright_md = load_template(template_dir / "copyright-page.md")
|
||||||
|
if copyright_md:
|
||||||
|
html = convert_md_to_html(copyright_md)
|
||||||
|
ch = make_epub_chapter("copyright.xhtml", "Copyright", html)
|
||||||
|
book.add_item(ch)
|
||||||
|
spine.append(ch)
|
||||||
|
|
||||||
|
# --- Front matter ---
|
||||||
|
front_md = load_template(template_dir / "front-matter.md")
|
||||||
|
if front_md:
|
||||||
|
html = convert_md_to_html(front_md)
|
||||||
|
ch = make_epub_chapter("front-matter.xhtml", "Front Matter", html)
|
||||||
|
book.add_item(ch)
|
||||||
|
spine.append(ch)
|
||||||
|
|
||||||
|
# --- Chapters ---
|
||||||
|
chapters = discover_chapters(chapter_dir)
|
||||||
|
if not chapters:
|
||||||
|
print("Error: No chapters found.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f" Found {len(chapters)} chapter(s)")
|
||||||
|
|
||||||
|
for slug, path in chapters:
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
ch_title = extract_title(text) or slug.title()
|
||||||
|
html = convert_md_to_html(text)
|
||||||
|
|
||||||
|
filename = f"{slug}.xhtml"
|
||||||
|
ch = make_epub_chapter(filename, ch_title, html)
|
||||||
|
book.add_item(ch)
|
||||||
|
spine.append(ch)
|
||||||
|
toc.append(ch)
|
||||||
|
print(f" Added: {path.name} → {ch_title}")
|
||||||
|
|
||||||
|
# --- Back matter templates ---
|
||||||
|
for template_name, epub_title in [
|
||||||
|
("back-matter.md", "Back Matter"),
|
||||||
|
("also-by.md", "Also by the Author"),
|
||||||
|
]:
|
||||||
|
back_md = load_template(template_dir / template_name)
|
||||||
|
if back_md:
|
||||||
|
html = convert_md_to_html(back_md)
|
||||||
|
fname = template_name.replace(".md", ".xhtml")
|
||||||
|
ch = make_epub_chapter(fname, epub_title, html)
|
||||||
|
book.add_item(ch)
|
||||||
|
spine.append(ch)
|
||||||
|
|
||||||
|
# --- Finalize ---
|
||||||
|
book.toc = toc
|
||||||
|
book.spine = spine
|
||||||
|
book.add_item(epub.EpubNcx())
|
||||||
|
book.add_item(epub.EpubNav())
|
||||||
|
|
||||||
|
# Output
|
||||||
|
if args.output:
|
||||||
|
out_path = Path(args.output)
|
||||||
|
else:
|
||||||
|
out_path = export_dir / f"book{book_num}-kindle.epub"
|
||||||
|
|
||||||
|
epub.write_epub(str(out_path), book, {})
|
||||||
|
return out_path
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Checklist generation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def generate_checklist(book_num: int, epub_path: Path) -> Path:
|
||||||
|
"""Write a publishing checklist to notes/kindle-publish-checklist.md."""
|
||||||
|
notes_dir = ROOT / "notes"
|
||||||
|
notes_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
checklist_path = notes_dir / "kindle-publish-checklist.md"
|
||||||
|
|
||||||
|
checklist = f"""\
|
||||||
|
# Kindle Publishing Checklist — Book {book_num}
|
||||||
|
|
||||||
|
Generated from `export_kindle.py`. Review and complete before uploading.
|
||||||
|
|
||||||
|
EPUB: `{epub_path.relative_to(ROOT)}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Front & Back Matter
|
||||||
|
|
||||||
|
- [ ] Copyright page — fill in year, pen name, legal text (`kindle/templates/copyright-page.md`)
|
||||||
|
- [ ] Dedication (optional) — add to front matter if desired
|
||||||
|
- [ ] Author\u2019s Note (optional) — add to back matter if desired
|
||||||
|
- [ ] \u201cAlso by the Author\u201d page — create `kindle/templates/also-by.md` when applicable
|
||||||
|
- [ ] Next book preview — append preview chapter to back matter when available
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
- [ ] Confirm pen name / author name
|
||||||
|
- [ ] Confirm book title and subtitle
|
||||||
|
- [ ] Write book description / blurb (for KDP listing)
|
||||||
|
- [ ] Choose KDP categories (2 allowed)
|
||||||
|
- [ ] Select 7 keywords for KDP search
|
||||||
|
- [ ] Set price (consider KU 70% royalty tier: $2.99\u2013$9.99)
|
||||||
|
|
||||||
|
## Cover Image
|
||||||
|
|
||||||
|
- [ ] Cover image ready (2560\u00d71600px recommended for Kindle)
|
||||||
|
- [ ] Cover passes KDP image quality checks
|
||||||
|
- [ ] Run `export_kindle.py --cover path/to/cover.jpg` to embed cover in EPUB
|
||||||
|
|
||||||
|
## Quality Checks
|
||||||
|
|
||||||
|
- [ ] Open EPUB in Kindle Previewer \u2014 check rendering on multiple devices
|
||||||
|
- [ ] Verify Table of Contents links work
|
||||||
|
- [ ] Verify chapter breaks (each chapter starts on new page)
|
||||||
|
- [ ] Verify scene breaks (`* * *`) are centered and styled
|
||||||
|
- [ ] Check smart quotes, em dashes, ellipses throughout
|
||||||
|
- [ ] Check first paragraphs are not indented after headings/scene breaks
|
||||||
|
- [ ] Proofread front and back matter
|
||||||
|
- [ ] Confirm no `[CONTINUITY FLAG]` markers remain in text
|
||||||
|
|
||||||
|
## Upload
|
||||||
|
|
||||||
|
- [ ] Log in to KDP (kdp.amazon.com)
|
||||||
|
- [ ] Create new Kindle eBook
|
||||||
|
- [ ] Upload EPUB manuscript
|
||||||
|
- [ ] Upload cover image
|
||||||
|
- [ ] Fill in metadata (title, description, categories, keywords)
|
||||||
|
- [ ] Set pricing and royalty
|
||||||
|
- [ ] Enroll in Kindle Unlimited (KDP Select)
|
||||||
|
- [ ] Preview and publish
|
||||||
|
"""
|
||||||
|
|
||||||
|
checklist_path.write_text(checklist, encoding="utf-8")
|
||||||
|
return checklist_path
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Export chapter markdown to a Kindle-ready EPUB file.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--book", type=int, default=1,
|
||||||
|
help="Book number (default: 1)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output", "-o",
|
||||||
|
help="Output EPUB path (default: kindle/exports/bookN-kindle.epub)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--title",
|
||||||
|
help="Book title (default: The Unbreakable Curse)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--author",
|
||||||
|
help="Author name (default: [Author Name])",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--cover",
|
||||||
|
help="Path to cover image (JPG or PNG)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print(f"Building EPUB for Book {args.book}...")
|
||||||
|
|
||||||
|
epub_path = build_epub(args)
|
||||||
|
print(f"\nEPUB written: {epub_path}")
|
||||||
|
|
||||||
|
checklist_path = generate_checklist(args.book, epub_path)
|
||||||
|
print(f"Checklist written: {checklist_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,3 +1,11 @@
|
|||||||
# Copyright Page Template
|
# Copyright
|
||||||
|
|
||||||
Standard copyright page for KDP publication.
|
Copyright © 2026 by Phillip Tarrant. All rights reserved.
|
||||||
|
|
||||||
|
No part of this publication may be reproduced, distributed, or transmitted in any form or by any means, including photocopying, recording, or other electronic or mechanical methods, without the prior written permission of the author, except in the case of brief quotations embodied in critical reviews and certain other noncommercial uses permitted by copyright law.
|
||||||
|
|
||||||
|
This is a work of fiction. Names, characters, places, and incidents either are the product of the author's imagination or are used fictitiously. Any resemblance to actual persons, living or dead, events, or locales is entirely coincidental.
|
||||||
|
|
||||||
|
<!-- TODO: Add ISBN if applicable -->
|
||||||
|
<!-- TODO: Add edition info (First Edition, [Month] [Year]) -->
|
||||||
|
<!-- TODO: Add cover art credit if applicable -->
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# Front Matter Template
|
# Dedication
|
||||||
|
|
||||||
Title page and front matter for KDP publication.
|
*For my family — who navigated my lack of emotion, my autistic brain, and my ADD with more grace and patience than I deserved. You believed in me when I couldn't. This one's yours.*
|
||||||
|
|||||||
76
notes/book1-kindle-publish-checklist.md
Normal file
76
notes/book1-kindle-publish-checklist.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Kindle Publishing Checklist — Book 1
|
||||||
|
|
||||||
|
Generated from `export_kindle.py`. Review and complete before uploading.
|
||||||
|
|
||||||
|
EPUB: `kindle/exports/book1-kindle.epub`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Front & Back Matter
|
||||||
|
|
||||||
|
- [X] Copyright page — fill in year, pen name, legal text (`kindle/templates/copyright-page.md`)
|
||||||
|
- [X] Dedication (optional) — add to front matter if desired
|
||||||
|
- [-] Author’s Note (optional) — add to back matter if desired
|
||||||
|
- [-] “Also by the Author” page — create `kindle/templates/also-by.md` when applicable
|
||||||
|
- [-] Next book preview — append preview chapter to back matter when available
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
- [X] Confirm pen name / author name — **Phillip Tarrant**
|
||||||
|
- [X] Confirm book title and subtitle — **The Unbreakable Curse: The Shade Series, Book One**
|
||||||
|
- [X] Write book description / blurb (for KDP listing) — see below
|
||||||
|
- [X] Choose KDP categories (2 allowed) — Fantasy > Humorous; Fantasy > Mystery & Detective
|
||||||
|
- [X] Select 7 keywords for KDP search — see below
|
||||||
|
- [X] Set price (consider KU 70% royalty tier: $2.99–$9.99) — **$2.99** (launch price, raise later)
|
||||||
|
|
||||||
|
### Book Description
|
||||||
|
|
||||||
|
Phelan Varrant sees what nobody else can — the flaws in magic that everyone else calls perfect.
|
||||||
|
|
||||||
|
Broke, antisocial, and operating out of a rented shack on Drenwick's dockside, Phelan makes his living through the Guild of Necessary Services, solving problems other guilds won't touch. When a desperate client arrives with a dying family member cursed by a working that two registered curse-breakers have already declared unbreakable, Phelan takes the case. The fee is significant. The problem is interesting. In that order.
|
||||||
|
|
||||||
|
The curse is named unbreakable by the Arcane Compact, but the curse isn't what it seems.
|
||||||
|
|
||||||
|
To save a man he barely knows, Phelan will have to do the one thing he's worst at: trust other people.
|
||||||
|
|
||||||
|
Phelan has a gift for hacking magic and seeing flaws. Can he find a way to crack this curse? Can he learn to work well with others? Will the effort be worth the cost?
|
||||||
|
|
||||||
|
*The Unbreakable Curse* is the first book in The Shade Series — a fantasy adventure for readers who love Jim Butcher's Dresden Files, Terry Pratchett's Discworld, and protagonists who are more clever than they are kind.
|
||||||
|
|
||||||
|
### KDP Keywords
|
||||||
|
|
||||||
|
1. fantasy series first person
|
||||||
|
2. sarcastic protagonist fantasy
|
||||||
|
3. magic system fantasy
|
||||||
|
4. guild fantasy adventure
|
||||||
|
5. fantasy mystery curse
|
||||||
|
6. Dresden Files similar
|
||||||
|
7. funny fantasy novel
|
||||||
|
|
||||||
|
## Cover Image
|
||||||
|
|
||||||
|
- [ ] Cover image ready (2560×1600px recommended for Kindle)
|
||||||
|
- [ ] Cover passes KDP image quality checks
|
||||||
|
- [ ] Run `export_kindle.py --cover path/to/cover.jpg` to embed cover in EPUB
|
||||||
|
|
||||||
|
## Quality Checks
|
||||||
|
|
||||||
|
- [ ] Open EPUB in Kindle Previewer — check rendering on multiple devices
|
||||||
|
- [ ] Verify Table of Contents links work
|
||||||
|
- [ ] Verify chapter breaks (each chapter starts on new page)
|
||||||
|
- [ ] Verify scene breaks (`* * *`) are centered and styled
|
||||||
|
- [ ] Check smart quotes, em dashes, ellipses throughout
|
||||||
|
- [ ] Check first paragraphs are not indented after headings/scene breaks
|
||||||
|
- [ ] Proofread front and back matter
|
||||||
|
- [ ] Confirm no `[CONTINUITY FLAG]` markers remain in text
|
||||||
|
|
||||||
|
## Upload
|
||||||
|
|
||||||
|
- [ ] Log in to KDP (kdp.amazon.com)
|
||||||
|
- [ ] Create new Kindle eBook
|
||||||
|
- [ ] Upload EPUB manuscript
|
||||||
|
- [ ] Upload cover image
|
||||||
|
- [ ] Fill in metadata (title, description, categories, keywords)
|
||||||
|
- [ ] Set pricing and royalty
|
||||||
|
- [ ] Enroll in Kindle Unlimited (KDP Select)
|
||||||
|
- [ ] Preview and publish
|
||||||
52
notes/kindle-publish-checklist.md
Normal file
52
notes/kindle-publish-checklist.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Kindle Publishing Checklist — Book 1
|
||||||
|
|
||||||
|
Generated from `export_kindle.py`. Review and complete before uploading.
|
||||||
|
|
||||||
|
EPUB: `kindle/exports/book1-kindle.epub`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Front & Back Matter
|
||||||
|
|
||||||
|
- [ ] Copyright page — fill in year, pen name, legal text (`kindle/templates/copyright-page.md`)
|
||||||
|
- [ ] Dedication (optional) — add to front matter if desired
|
||||||
|
- [ ] Author’s Note (optional) — add to back matter if desired
|
||||||
|
- [ ] “Also by the Author” page — create `kindle/templates/also-by.md` when applicable
|
||||||
|
- [ ] Next book preview — append preview chapter to back matter when available
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
- [ ] Confirm pen name / author name
|
||||||
|
- [ ] Confirm book title and subtitle
|
||||||
|
- [ ] Write book description / blurb (for KDP listing)
|
||||||
|
- [ ] Choose KDP categories (2 allowed)
|
||||||
|
- [ ] Select 7 keywords for KDP search
|
||||||
|
- [ ] Set price (consider KU 70% royalty tier: $2.99–$9.99)
|
||||||
|
|
||||||
|
## Cover Image
|
||||||
|
|
||||||
|
- [ ] Cover image ready (2560×1600px recommended for Kindle)
|
||||||
|
- [ ] Cover passes KDP image quality checks
|
||||||
|
- [ ] Run `export_kindle.py --cover path/to/cover.jpg` to embed cover in EPUB
|
||||||
|
|
||||||
|
## Quality Checks
|
||||||
|
|
||||||
|
- [ ] Open EPUB in Kindle Previewer — check rendering on multiple devices
|
||||||
|
- [ ] Verify Table of Contents links work
|
||||||
|
- [ ] Verify chapter breaks (each chapter starts on new page)
|
||||||
|
- [ ] Verify scene breaks (`* * *`) are centered and styled
|
||||||
|
- [ ] Check smart quotes, em dashes, ellipses throughout
|
||||||
|
- [ ] Check first paragraphs are not indented after headings/scene breaks
|
||||||
|
- [ ] Proofread front and back matter
|
||||||
|
- [ ] Confirm no `[CONTINUITY FLAG]` markers remain in text
|
||||||
|
|
||||||
|
## Upload
|
||||||
|
|
||||||
|
- [ ] Log in to KDP (kdp.amazon.com)
|
||||||
|
- [ ] Create new Kindle eBook
|
||||||
|
- [ ] Upload EPUB manuscript
|
||||||
|
- [ ] Upload cover image
|
||||||
|
- [ ] Fill in metadata (title, description, categories, keywords)
|
||||||
|
- [ ] Set pricing and royalty
|
||||||
|
- [ ] Enroll in Kindle Unlimited (KDP Select)
|
||||||
|
- [ ] Preview and publish
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ebooklib
|
||||||
|
markdown
|
||||||
Reference in New Issue
Block a user