diff --git a/posty/cli.py b/posty/cli.py
index 5285708..a8e0ee0 100644
--- a/posty/cli.py
+++ b/posty/cli.py
@@ -57,6 +57,42 @@ def build(output, config):
     shutil.copytree('media', os.path.join(output, 'media'))
 
 
+@cli.group(name='new')
+def _new():
+    """
+    Create a new post or page
+    """
+    pass
+
+
+@_new.command()
+@click.option(
+    '--name',
+    help='Name of the new page',
+    default='New Page',
+)
+def page(name):
+    """
+    Create a new page from the template
+    """
+    site = Site()
+    site.new_page(name=name)
+
+
+@_new.command()
+@click.option(
+    '--name',
+    help='Name of the new post',
+    default='New Post',
+)
+def post(name):
+    """
+    Create a new page from the template
+    """
+    site = Site()
+    site.new_post(name=name)
+
+
 @cli.group(name='import')
 def _import():
     """
diff --git a/posty/page.py b/posty/page.py
index 873f356..c5874af 100644
--- a/posty/page.py
+++ b/posty/page.py
@@ -26,6 +26,20 @@ class Page(Model):
 
         return cls(payload, config=config)
 
+    def to_yaml(self):
+        """
+        Returns a string of the YAML and text representation of this Post.
+        This is the reverse of from_yaml
+        """
+        metadata = {'title': self['title']}
+        if self['parent']:
+            metadata['parent'] = self['parent']
+        output = yaml.dump(metadata, default_flow_style=False)
+        output += "---\n"
+        output += self['body']
+
+        return output
+
     def validate(self):
         required_fields = ('title', 'body')
         for field in required_fields:
diff --git a/posty/post.py b/posty/post.py
index 060c2c1..fbd2b0d 100644
--- a/posty/post.py
+++ b/posty/post.py
@@ -39,6 +39,32 @@ class Post(Model):
 
         return cls(post, config=config)
 
+    def to_yaml(self):
+        """
+        Returns the YAML and text representation of this Post. This is the
+        reverse of ``from_yaml()``
+        """
+        metadata = {
+            'title': self['title'],
+            'date': self['date'],
+            'tags': self['tags'],
+        }
+        body = self['body']
+
+        output = yaml.dump(metadata, default_flow_style=False)
+
+        if self['blurb'] != self['body']:
+            output += "---\n"
+            output += self['blurb'].strip()
+            output += "\n"
+
+            body = body.replace(self['blurb'], '')
+
+        output += "---\n"
+        output += body.strip()
+
+        return output
+
     def validate(self):
         required_fields = ('title', 'date', 'blurb', 'body')
         for field in required_fields:
diff --git a/posty/site.py b/posty/site.py
index 04bebff..0aeb50f 100644
--- a/posty/site.py
+++ b/posty/site.py
@@ -1,4 +1,5 @@
 from collections import Counter
+import datetime
 import os.path
 import shutil
 
@@ -175,3 +176,44 @@ class Site(object):
         )
 
         return copyright
+
+    def new_post(self, name="New Post"):
+        """
+        Create a new post in the site directory from the skeleton post
+        """
+        post_dir = os.path.join(self.site_path, 'posts')
+        if not os.path.exists(post_dir):
+            raise PostyError('You must initialize the site first')
+
+        date = datetime.date.today()
+        filename = '{}_{}.yaml'.format(date, slugify(name))
+        post_path = os.path.join(post_dir, filename)
+
+        skel_path = os.path.join(os.path.dirname(__file__),
+                                 'skel/posts/1970-01-01_new-post.yaml')
+        post = Post.from_yaml(open(skel_path).read(), config=self.config)
+        post['title'] = name
+        post['date'] = date
+
+        with open(post_path, 'w') as output_file:
+            output_file.write(post.to_yaml())
+
+    def new_page(self, name="New Page"):
+        """
+        Create a new page in the site directory from the skeleton page
+        """
+        page_dir = os.path.join(self.site_path, 'pages')
+        if not os.path.exists(page_dir):
+            raise PostyError('You must initialize the site first')
+
+        filename = '{}.yaml'.format(slugify(name))
+        page_path = os.path.join(page_dir, filename)
+
+        skel_path = os.path.join(os.path.dirname(__file__),
+                                 'skel/pages/new-page.yaml')
+
+        page = Page.from_yaml(open(skel_path).read(), config=self.config)
+        page['title'] = name
+
+        with open(page_path, 'w') as output_file:
+            output_file.write(page.to_yaml())
diff --git a/posty/skel/pages/new-page.yaml b/posty/skel/pages/new-page.yaml
new file mode 100644
index 0000000..73cc0f6
--- /dev/null
+++ b/posty/skel/pages/new-page.yaml
@@ -0,0 +1,4 @@
+title: New Page
+parent: None
+---
+This is your new page. Write what you want here!
diff --git a/posty/skel/posts/1970-01-01_new-post.yaml b/posty/skel/posts/1970-01-01_new-post.yaml
new file mode 100644
index 0000000..9057105
--- /dev/null
+++ b/posty/skel/posts/1970-01-01_new-post.yaml
@@ -0,0 +1,11 @@
+title: New Post
+date: 1970-01-01
+tags:
+    - tag1
+    - tag2
+---
+This the the summary/first paragraph of your new post
+---
+This is the rest of the post.
+
+Write what you want here!

`posty new` commands
Add a set of commands to create new posts or pages. Pull from an example file from the skeleton data.

Example CLI:
`posty new page "About me"` -> `pages/{{slug}}.yaml`
`posty new post "My new post"` -> `posts/{{date}}_{{slug}}.yaml`
`posty new post` -> `posts/{{date}}_new-post.yaml`
diff --git a/tests/fixtures/site/posts/multi-paragraph.yaml b/tests/fixtures/site/posts/multi-paragraph.yaml
index 1c55996..633e099 100644
--- a/tests/fixtures/site/posts/multi-paragraph.yaml
+++ b/tests/fixtures/site/posts/multi-paragraph.yaml
@@ -1,8 +1,8 @@
-title: Multi-paragraph Post
 date: 2017-01-14
 tags:
-    - blah
-    - test
+- blah
+- test
+title: Multi-paragraph Post
 ---
 This is a post that has multiple paragraphs, where the first paragraph should get converted into a blurb.
 ---
diff --git a/tests/test_page.py b/tests/test_page.py
index d06c567..62feb33 100644
--- a/tests/test_page.py
+++ b/tests/test_page.py
@@ -7,15 +7,19 @@ from posty.page import Page
 from .fixtures import config    # noqa
 
 
+@pytest.fixture
+def page_contents():
+    path = os.path.join(os.path.dirname(__file__), 'fixtures', 'site', 'pages',
+                        'test.yaml')
+    return open(path).read()
+
+
 @pytest.fixture     # noqa
-def page(config):
+def page(config, page_contents):
     """
     Basic top-level page (has no parent)
     """
-    path = os.path.join(os.path.dirname(__file__), 'fixtures', 'site', 'pages',
-                        'test.yaml')
-    contents = open(path).read()
-    return Page.from_yaml(contents, config=config)
+    return Page.from_yaml(page_contents, config=config)
 
 
 class TestValidation(object):
@@ -44,3 +48,7 @@ def test_url(page):
     expected_url = 'http://example.org/test/{}/'.format(page['slug'])
 
     assert page.url() == expected_url
+
+
+def test_to_yaml(page, page_contents):
+    assert page_contents.strip() == page.to_yaml()
diff --git a/tests/test_post.py b/tests/test_post.py
index d27217d..478f834 100644
--- a/tests/test_post.py
+++ b/tests/test_post.py
@@ -8,15 +8,19 @@ from posty.post import Post
 from .fixtures import config    # noqa
 
 
+@pytest.fixture
+def post_contents():
+    path = os.path.join(os.path.dirname(__file__), 'fixtures', 'site', 'posts',
+                        'multi-paragraph.yaml')
+    return open(path).read()
+
+
 @pytest.fixture     # noqa
-def post(config):
+def post(config, post_contents):
     """
     Basic post
     """
-    path = os.path.join(os.path.dirname(__file__), 'fixtures', 'site', 'posts',
-                        'multi-paragraph.yaml')
-    contents = open(path).read()
-    return Post.from_yaml(contents, config=config)
+    return Post.from_yaml(post_contents, config=config)
 
 
 class TestValidation(object):
@@ -46,3 +50,7 @@ def test_url(post):
                                                                   post['slug'])
 
     assert post.url() == expected_url
+
+
+def test_to_yaml(post, post_contents):
+    assert post_contents.strip() == post.to_yaml()
diff --git a/tests/test_site.py b/tests/test_site.py
index ecee2af..1468563 100644
--- a/tests/test_site.py
+++ b/tests/test_site.py
@@ -1,3 +1,6 @@
+import datetime
+import os
+
 from .fixtures import site  # noqa
 
 
@@ -32,3 +35,27 @@ def test_post_sorting(site):    # noqa
 def test_copyright(site):   # noqa
     site.load()
     assert site.copyright == 'Copyright 2010 - 2017, Jimbo Jawn'
+
+
+def test_new_page(site):    # noqa
+    site.new_page()
+    new_page_path = os.path.join(site.site_path, 'pages', 'new-page.yaml')
+    assert os.path.exists(new_page_path)
+
+    site.new_page('Neato page')
+    new_page_path = os.path.join(site.site_path, 'pages', 'neato-page.yaml')
+    assert os.path.exists(new_page_path)
+
+
+def test_new_post(site):    # noqa
+    date = datetime.date.today()
+
+    site.new_post()
+    filename = '{}_new-post.yaml'.format(date)
+    expected_path = os.path.join(site.site_path, 'posts', filename)
+    assert os.path.exists(expected_path)
+
+    site.new_post('Neato Post')
+    filename = '{}_neato-post.yaml'.format(date)
+    expected_path = os.path.join(site.site_path, 'posts', filename)
+    assert os.path.exists(expected_path)

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-xdist pytest-mock pytest-asyncio
3.6
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
awesome-slugify==1.6.5
certifi==2021.5.30
click==6.7
coverage==6.2
execnet==1.9.0
feedgen==0.6.1
future==0.16.0
importlib-metadata==4.8.3
iniconfig==1.1.1
Jinja2==2.11.3
lxml==5.3.1
Markdown==2.6.11
MarkupSafe==2.0.1
packaging==21.3
pluggy==1.0.0
-e git+https://github.com/nickpegg/posty.git@0187ca11ba573f0bb03b0fbcec666b30c3568927#egg=Posty
py==1.11.0
pyparsing==3.1.4
pytest==7.0.1
pytest-asyncio==0.16.0
pytest-cov==4.0.0
pytest-mock==3.6.1
pytest-xdist==3.0.2
python-dateutil==2.9.0.post0
pytz==2025.2
PyYAML==3.13
regex==2023.8.8
six==1.17.0
tomli==1.2.3
typing_extensions==4.1.1
Unidecode==0.4.21
zipp==3.6.0

name: posty
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - awesome-slugify==1.6.5
      - click==6.7
      - coverage==6.2
      - execnet==1.9.0
      - feedgen==0.6.1
      - future==0.16.0
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - jinja2==2.11.3
      - lxml==5.3.1
      - markdown==2.6.11
      - markupsafe==2.0.1
      - packaging==21.3
      - pluggy==1.0.0
      - py==1.11.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - pytest-asyncio==0.16.0
      - pytest-cov==4.0.0
      - pytest-mock==3.6.1
      - pytest-xdist==3.0.2
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyyaml==3.13
      - regex==2023.8.8
      - six==1.17.0
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - unidecode==0.04.21
      - zipp==3.6.0
prefix: /opt/conda/envs/posty

tests/test_page.py::test_to_yaml tests/test_post.py::test_to_yaml tests/test_site.py::test_new_page tests/test_site.py::test_new_post
tests/test_page.py::TestValidation::test_basic_case tests/test_page.py::TestValidation::test_no_title tests/test_page.py::TestValidation::test_no_parent tests/test_page.py::test_url tests/test_post.py::TestValidation::test_basic_case tests/test_post.py::TestValidation::test_no_title tests/test_post.py::TestValidation::test_no_tags tests/test_post.py::test_url tests/test_site.py::test_site_loads_config tests/test_site.py::test_page_sorting tests/test_site.py::test_post_sorting tests/test_site.py::test_copyright
posty/page.py posty/cli.py posty/skel/posts/1970-01-01_new-post.yaml posty/skel/pages/new-page.yaml posty/site.py posty/post.py
posty/page.py posty/cli.py posty/skel/posts/1970-01-01_new-post.yaml posty/skel/pages/new-page.yaml posty/site.py posty/post.py