提交 cb22d367 编写于 作者: J Juan Olvera 提交者: Luis Fernando Alvarez D

Added new blog example (#7511)

上级 fbc20a54
{
"presets": ["next/babel"],
"plugins": ["preval", "macros"]
}
\ No newline at end of file
# Blog starter example
## How to use
### Using `create-next-app`
Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example:
```
npm i -g create-next-app
create-next-app --example blog-starter
```
### Download manually
Download the example [or clone the repo](https://github.com/zeit/next.js):
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/blog-starter
cd blog-starter
```
### Run locally
Install and run the development server:
```bash
yarn install
now dev
```
### Deploy
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
```bash
now
```
## The idea behind the example
This is an example of a blog built with Next.js. [Read more about the motivation and how it is built](https://jolvera.dev/posts/rebuilding-my-blog-with-nextjs).
The blog is still barebones and need more improvements and styling, but this should be enough to get you started.
[Demo deployed in Now](https://nextjs-blog-starter.now.sh/)
module.exports = {
siteMeta: {
title: 'Next.js Starter Blog',
author: 'Juan Olvera',
image: '/static/site-feature.png',
description: 'Next.js starter blog',
siteUrl: 'https://nextjs-blog-starter.now.sh',
social: {
twitter: '_jolvera'
},
postsPerPage: 5
}
}
import Link from 'next/link'
import PublishedAt from './utils/published-at'
const Post = ({ title, summary, date, path }) => (
<article>
<header>
<h2>
<Link href={path}>
<a>{title}</a>
</Link>
</h2>
<PublishedAt link={path} date={date} />
</header>
<div className='post-summary'>{summary}</div>
<style jsx>{`
article {
margin-bottom: 2em;
}
a {
text-decoration: none;
}
.post-summary {
margin-top: 1em;
}
`}</style>
</article>
)
export default Post
const Container = ({ children }) => (
<>
<div>{children}</div>
<style jsx>{`
max-width: 45rem;
margin: 0 auto;
padding: 0 1em;
`}</style>
</>
)
export default Container
import Profile from './profile'
function Footer () {
return (
<footer>
<Profile className='profile-footer' />
<p>
Proudly built with <a href='https://nextjs.org'>Next.js</a> -{' '}
<a href='/feed.json'>RSS Feed</a>
</p>
<style jsx>{`
footer {
padding: 1em 0;
}
p {
margin-top: 2em;
}
`}</style>
</footer>
)
}
export default Footer
import React from 'react'
import NextHead from 'next/head'
import { string } from 'prop-types'
import { siteMeta } from '../blog.config'
const defaultDescription = siteMeta.description
const defaultOGURL = siteMeta.siteUrl
const defaultOGImage = siteMeta.image
const Head = props => (
<NextHead>
<meta charSet='UTF-8' />
<title>
{props.title ? `${props.title} - ${siteMeta.title}` : siteMeta.title}
</title>
<meta
name='description'
content={props.description || defaultDescription}
/>
<meta name='viewport' content='width=device-width, initial-scale=1' />
<link rel='icon' href='/static/favicon.ico' />
<link
rel='alternate'
title='RSS Feed'
type='application/json'
href={`${siteMeta.siteUrl}/feed.json`}
/>
<meta property='og:url' content={props.url || defaultOGURL} />
<meta property='og:title' content={props.title || ''} />
<meta
property='og:description'
content={props.description || defaultDescription}
/>
<meta name='twitter:site' content={props.url || defaultOGURL} />
<meta name='twitter:card' content='summary_large_image' />
<meta
name='twitter:image'
content={`${siteMeta.siteUrl}${props.ogImage || defaultOGImage}`}
/>
<meta
property='og:image'
content={`${siteMeta.siteUrl}${props.ogImage || defaultOGImage}`}
/>
<meta property='og:image:width' content='1200' />
<meta property='og:image:height' content='630' />
</NextHead>
)
Head.propTypes = {
title: string,
description: string,
url: string,
ogImage: string
}
export default Head
import React from 'react'
import PropTypes from 'prop-types'
import Head from './head'
import Nav from './nav'
import Title from './title'
function Header ({ path, pageTitle, ogImage }) {
return (
<>
<Head title={pageTitle} ogImage={ogImage} />
<header>
<Title path={path} />
<Nav />
</header>
<style jsx>
{`
header {
padding: 1em 0;
display: flex;
align-items: center;
justify-content: space-between;
}
`}
</style>
<style jsx global>
{`
html {
margin: 0;
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
font-size: 18px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
color: #333;
line-height: 1.5;
background-color: #fff;
}
h1,
h2,
h3,
h4 {
margin-bottom: 0.5rem;
font-weight: bold;
color: inherit;
line-height: 1.25;
}
h1 {
font-size: 2rem;
}
h2 {
margin-top: 1rem;
font-size: 1.8rem;
}
h3 {
margin-top: 1.5rem;
font-size: 1.5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
ul,
ol,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
a {
color: #33e;
cursor: pointer;
}
a:hover,
a:focus {
text-decoration: underline;
}
hr {
position: relative;
margin: 1.5em 0;
border: 0;
border-top: 1px solid #eee;
border-bottom: 1px solid #fff;
}
blockquote {
padding: 0.5em 1em;
margin: 0.8em 0;
color: #555;
border-left: 0.25em solid #ccc;
}
blockquote p:last-child {
margin-bottom: 0;
}
pre code {
font-size: 18px;
}
code {
color: #fff;
}
.wrap {
max-width: 38rem;
margin-left: auto;
margin-right: auto;
}
article img {
max-width: 100%;
height: auto;
}
`}
</style>
</>
)
}
Header.propTypes = {
path: PropTypes.string,
pageTitle: PropTypes.string,
ogImage: PropTypes.string
}
export default Header
import Link from 'next/link'
import { siteMeta } from '../../blog.config'
import Layout from './default'
import SyntaxHighlight from '../syntax-highlight'
import PublishedAt from '../utils/published-at'
import blogposts from '../../posts/index'
import NextPrevPost from '../next-prev-post'
function BlogPost ({ path, meta, children }) {
const currentPostIndex = blogposts
.map(({ title }) => title)
.indexOf(meta.title)
const previousPost = blogposts[currentPostIndex + 1]
const nextPost = blogposts[currentPostIndex - 1]
return (
<Layout pageTitle={meta.title} ogImage={meta.image}>
<SyntaxHighlight />
<article className='h-entry'>
<header>
<h1 className='p-name'>{meta.title}</h1>
<div>
<PublishedAt date={meta.publishedAt} link={path} />
<Link href='/about'>
<a
color='#aaa'
rel='author'
className='p-author h-card'
href='/about'
>
{siteMeta.author}
</a>
</Link>
</div>
</header>
<div className='e-content'>{children}</div>
<footer>
{(previousPost || nextPost) && (
<div className='post-pagination'>
{previousPost && (
<NextPrevPost
title={previousPost.title}
path={previousPost.path}
position='previous'
/>
)}
{nextPost && (
<NextPrevPost
title={nextPost.title}
path={nextPost.path}
position='next'
/>
)}
</div>
)}
</footer>
</article>
<style jsx>{`
header {
margin-bottom: 2em;
}
[rel='author'] {
margin-left: 1em;
}
article {
margin-bottom: 2em;
}
footer {
margin-top: 2em;
}
.post-pagination {
display: grid;
grid-template-columns: 1fr 1fr;
}
`}</style>
</Layout>
)
}
export default BlogPost
import React from 'react'
import Header from '../header'
import Footer from '../footer'
import Container from '../container'
function Layout ({ path, children, pageTitle, ogImage }) {
return (
<Container>
<Header path={path} pageTitle={pageTitle} ogImage={ogImage} />
<main>{children}</main>
<Footer />
</Container>
)
}
export default Layout
import Layout from './default'
function Page ({ meta, children }) {
return (
<Layout pageTitle={meta.title}>
<article>
<header>
<h1>{meta.title}</h1>
</header>
<div>{children}</div>
</article>
<style jsx>{`
header {
margin-bottom: 2em;
}
`}</style>
</Layout>
)
}
export default Page
import Link from 'next/link'
const Nav = () => (
<nav>
<Link href='/about'>
<a>About</a>
</Link>
<style jsx>{`
nav {
display: flex;
}
a:not(:last-child) {
margin-right: 1em;
}
`}</style>
</nav>
)
export default Nav
import PropTypes from 'prop-types'
import Link from 'next/link'
const NextPrevPost = ({ title, path, position }) => {
const isNext = position === 'next'
return (
<>
<Link href={path}>
<a>
<small>Read {position} post </small>
{title}
</a>
</Link>
<style jsx>{`
a {
display: flex;
flex-direction: column;
${isNext ? 'text-align: right;' : ''}
${isNext ? 'grid-column: 2 / 2;' : ''}
}
`}</style>
</>
)
}
NextPrevPost.propTypes = {
title: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
position: PropTypes.oneOf(['next', 'previous'])
}
export default NextPrevPost
import { siteMeta } from '../blog.config'
const Profile = () => (
<div className='h-card profile'>
<img className='u-photo' src='/static/_jolvera.png' alt={siteMeta.author} />
<div>
<p>
Hi, I'm{' '}
<a className='u-url p-name' href={siteMeta.siteUrl} rel='me'>
{siteMeta.author}
</a>
</p>
<p className='p-note'>
I'm a frontend developer &amp; web standards enthusiastic.
</p>
</div>
<style jsx>{`
.profile {
display: flex;
align-items: center;
padding: 1em;
background-color: #eee;
}
img {
width: 5em;
height: 5em;
margin-right: 0.5em;
}
p:last-child {
margin-bottom: 0;
}
`}</style>
</div>
)
export default Profile
code[class*='language-'],
pre[class*='language-'] {
color: #f8f8f2;
background: none;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
background: #2b2b2b;
}
:not(pre) > code[class*='language-'] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #d4d0ab;
}
.token.punctuation {
color: #fefefe;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
color: #ffa07a;
}
.token.boolean,
.token.number {
color: #00e0e0;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #abe338;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
color: #00e0e0;
}
.token.atrule,
.token.attr-value,
.token.function {
color: #ffd700;
}
.token.keyword {
color: #00e0e0;
}
.token.regex,
.token.important {
color: #ffd700;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
@media screen and (-ms-high-contrast: active) {
code[class*='language-'],
pre[class*='language-'] {
color: windowText;
background: window;
}
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
background: window;
}
.token.important {
background: highlight;
color: window;
font-weight: normal;
}
.token.atrule,
.token.attr-value,
.token.function,
.token.keyword,
.token.operator,
.token.selector {
font-weight: bold;
}
.token.attr-value,
.token.comment,
.token.doctype,
.token.function,
.token.keyword,
.token.operator,
.token.property,
.token.string {
color: highlight;
}
.token.attr-value,
.token.url {
font-weight: normal;
}
}
import React from 'react'
export default () => (
<style jsx global>
{`
code[class*='language-'],
pre[class*='language-'] {
color: #f8f8f2;
background: none;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
background: #2b2b2b;
}
:not(pre) > code[class*='language-'] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #d4d0ab;
}
.token.punctuation {
color: #fefefe;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
color: #ffa07a;
}
.token.boolean,
.token.number {
color: #00e0e0;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #abe338;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
color: #00e0e0;
}
.token.atrule,
.token.attr-value,
.token.function {
color: #ffd700;
}
.token.keyword {
color: #00e0e0;
}
.token.regex,
.token.important {
color: #ffd700;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
@media screen and (-ms-high-contrast: active) {
code[class*='language-'],
pre[class*='language-'] {
color: windowText;
background: window;
}
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
background: window;
}
.token.important {
background: highlight;
color: window;
font-weight: normal;
}
.token.atrule,
.token.attr-value,
.token.function,
.token.keyword,
.token.operator,
.token.selector {
font-weight: bold;
}
.token.attr-value,
.token.comment,
.token.doctype,
.token.function,
.token.keyword,
.token.operator,
.token.property,
.token.string {
color: highlight;
}
.token.attr-value,
.token.url {
font-weight: normal;
}
}
// TODO: add this in the prismjs plugin
code {
padding: 0.1em 0.2em;
background-color: #2b2b2b;
border-radius: 0.3em;
}
code[class*='language-'],
pre[class*='language-'] {
font-size: 16px;
line-height: 1.3;
}
pre[class*='language-'] {
margin-bottom: 1em;
}
pre[class='language-jsx'] {
padding-bottom: 0;
}
.mdx-marker {
background-color: rgba(255, 255, 255, 0.1);
display: block;
margin-right: -1em;
margin-left: -1em;
padding-right: 1em;
padding-left: 0.75em;
border-left: 0.25em solid #ffdc00;
}
`}
</style>
)
import Link from 'next/link'
import { siteMeta } from '../blog.config'
const Title = ({ path }) => (
<>
{path === '/' ? (
<h1>
<a href={siteMeta.siteUrl}>{siteMeta.title}</a>
</h1>
) : (
<p>
<Link href='/'>
<a rel='me'>{siteMeta.title}</a>
</Link>
</p>
)}
<style jsx>{`
h1 {
margin-top: 0;
}
a {
color: #333;
text-decoration: none;
}
p {
font-size: 1.3em;
font-weight: bold;
}
`}</style>
</>
)
export default Title
import React from 'react'
import Link from 'next/link'
import { parse, format } from 'date-fns'
function PublishedAt (props) {
const { link, date } = props
return (
<>
<Link href={link}>
<a href={link} className='u-url' mcolor='#aaa' {...props}>
<time className='dt-published'>
{format(parse(date), 'MMMM DD, YYYY')}
</time>
</a>
</Link>
<style jsx>{`
a {
color: #555;
text-decoration: none;
}
`}</style>
</>
)
}
export default PublishedAt
const withMDX = require('@zeit/next-mdx')({
extension: /.mdx?$/,
options: {
hastPlugins: [require('mdx-prism')]
}
})
module.exports = withMDX({
target: 'serverless',
pageExtensions: ['js', 'jsx', 'mdx', 'md'],
webpack: (config, { defaultLoaders, isServer, dev }) => {
// Fixes npm packages that depend on `fs` module
config.node = {
fs: 'empty',
module: 'empty'
}
config.module.rules.push(
{
test: /\.css$/,
use: [
defaultLoaders.babel,
{
loader: require('styled-jsx/webpack').loader,
options: {
type: 'global'
}
}
]
},
{
test: /\.svg$/,
use: [
{
loader: '@svgr/webpack'
}
]
}
)
if (isServer && !dev) {
const originalEntry = config.entry
config.entry = async () => {
const entries = { ...(await originalEntry()) }
// This script imports components from the Next app, so it's transpiled to `.next/server/scripts/build-rss.js`
entries['./posts/rss-feed.js'] = './posts/rss-feed.js'
return entries
}
}
return config
}
})
{
"version": 2,
"name": "nextjs-blog-starter",
"alias": ["nextjs-blog-starter.now.sh"],
"builds": [{ "src": "package.json", "use": "@now/next" }],
"routes": [
{ "src": "/feed.json", "dest": "/_next/static/feed.json" },
{ "src": "/blog/(?<page>[^/]+)$", "dest": "/blog?page=$page" }
]
}
{
"name": "nextjs-blog-starter",
"scripts": {
"dev": "next",
"build": "next build",
"build:rss": "node ./.next/serverless/posts/rss-feed.js",
"now-build": "next build && yarn build:rss"
},
"dependencies": {
"@zeit/next-css": "^1.0.1",
"@zeit/next-mdx": "^1.2.0",
"babel-plugin-macros": "^2.5.0",
"babel-plugin-preval": "^3.0.1",
"date-fns": "^1.30.1",
"human-date": "^1.4.0",
"isomorphic-unfetch": "^3.0.0",
"lodash.range": "^3.2.0",
"mdx-prism": "^0.3.1",
"micro": "^9.3.3",
"next": "^8.1.1-canary.37",
"pagination": "^0.4.6",
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
"devDependencies": {
"@svgr/webpack": "^4.2.0",
"prettier": "^1.16.4",
"rehype": "^8.0.0"
}
}
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
static async getInitialProps (ctx) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
}
render () {
return (
<Html lang='en'>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
import { withRouter } from 'next/router'
import BlogPost from '../components/layouts/blog-post'
import Page from '../components/layouts/page'
export const meta = {
title: 'About',
summary: 'Hi! This is a placeholder page. I thought that the header was too empty so I wanted to add an element to show how to add additional pages and put a small navigation here.',
}
export default withRouter(({ children, router }) => (
<Page path={router.pathname} meta={meta}>
{children}
</Page>
))
Hi! This is a placeholder page.
I thought that the header was too empty, so I added an element to show how to add additional pages and put a small navigation there.
You can create pages using React components or MDX files. [Read more about MDX](https://mdxjs.com/).
import { withRouter } from 'next/router'
import _range from 'lodash.range'
import Link from 'next/link'
import pagination from 'pagination'
import Layout from '../components/layouts/default'
import Post from '../components/blog-index-item'
import blogposts from '../posts/index'
import { siteMeta } from '../blog.config'
const Blog = ({ router, page = 1 }) => {
const paginator = new pagination.SearchPaginator({
prelink: '/',
current: page,
rowsPerPage: siteMeta.postsPerPage,
totalResult: blogposts.length
})
const {
previous,
range,
next,
fromResult,
toResult
} = paginator.getPaginationData()
const results = _range(fromResult - 1, toResult)
return (
<Layout pageTitle='Blog' path={router.pathname}>
<header>
<h1>Blog</h1>
</header>
{blogposts
.filter((_post, index) => results.indexOf(index) > -1)
.map((post, index) => (
<Post
key={index}
title={post.title}
summary={post.summary}
date={post.publishedAt}
path={post.path}
/>
))}
<ul>
{previous && (
<li>
<Link href={`/blog?page=${previous}`} as={`/blog/${previous}`}>
<a>Previous</a>
</Link>
</li>
)}
{range.map((page, index) => (
<li key={index}>
<Link key={index} href={`/blog?page=${page}`} as={`/blog/${page}`}>
<a>{page}</a>
</Link>
</li>
))}
{next && (
<li>
<Link href={`/blog?page=${next}`} as={`/blog/${next}`}>
<a>Next</a>
</Link>
</li>
)}
</ul>
<style jsx>{`
header {
margin-bottom: 3em;
}
`}</style>
</Layout>
)
}
Blog.getInitialProps = async ({ query }) => {
return query ? { page: query.page } : {}
}
export default withRouter(Blog)
import { withRouter } from 'next/router'
import BlogPost from '../../components/layouts/blog-post'
export const meta = {
published: true,
publishedAt: '2018-05-18',
title: 'Deploy to Now',
summary:
'One of the most amazing things about this project is that you can easily deploy it to Now',
image: '/static/site-feature.png',
}
export default withRouter(({ children, router }) => (
<BlogPost path={router.pathname} meta={meta}>
{children}
</BlogPost>
))
One of the amazing things about this project is that you can easily deploy it to Now.
The time from cloning the repository to deploy and preview your blog online can be of a couple minutes.
Also, you can see exactly how it will look in production using [now dev](https://zeit.co/blog/now-dev)
import { withRouter } from 'next/router'
import BlogPost from '../../components/layouts/blog-post'
export const meta = {
published: true,
publishedAt: '2018-05-19',
title: 'Sintax highlighting',
summary:
'Here are some code snippets to show you how syntax highlighting works',
image: '/static/site-feature.png',
}
export default withRouter(({ children, router }) => (
<BlogPost path={router.pathname} meta={meta}>
{children}
</BlogPost>
))
Here are some code snippets to show you how syntax highlighting looks.
```js{4,5}
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
)
ReactDOM.render(element, document.getElementById('root'))
}
setInterval(tick, 1000)
```
```bash
>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
>>> r.status_code
200
>>> r.headers['content-type']
'application/json; charset=utf8'
>>> r.encoding
'utf-8'
>>> r.text
u'{"type":"User"...'
>>> r.json()
{u'private_gists': 419, u'total_private_repos': 77, ...}
```
```scss
$font-stack: Helvetica, sans-serif;
$primary-color: #333;
body {
font: 100% $font-stack;
color: $primary-color;
}
nav {
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
display: inline-block;
}
a {
display: block;
padding: 6px 12px;
text-decoration: none;
}
}
```
import { withRouter } from 'next/router'
import BlogPost from '../../components/layouts/blog-post'
export const meta = {
published: true,
publishedAt: '2018-05-20',
title: 'Welcome',
summary: 'Welcome to your new blog built with Next.js',
image: '/static/site-feature.png',
}
export default withRouter(({ children, router }) => (
<BlogPost path={router.pathname} meta={meta}>
{children}
</BlogPost>
))
Welcome to your new blog built with Next.js.
You&rsquo;ll find this post in your `page/posts` directory. To add new posts, add a MDX file in the `page/posts` directory, press `ctrl + c` to stop the development server and run `now dev` again.
After you personalize your blog you can deploy it to Now.
```bash
$ now
```
Then, you will see your blog online in a URL like https://nextjs-blog-starter.now.sh
const fs = require('fs')
const path = require('path')
const DIR = path.join(process.cwd(), '/pages/posts/')
const META = /export\s+const\s+meta\s+=\s+({[\s\S]*?\n})/
const files = fs
.readdirSync(DIR)
.filter(file => file.endsWith('.md') || file.endsWith('.mdx'))
module.exports = files
.map((file, index) => {
const name = path.join(DIR, file)
const contents = fs.readFileSync(name, 'utf-8')
const match = META.exec(contents)
if (!match || typeof match[1] !== 'string') {
throw new Error(`${name} needs to export const meta = {}`)
}
// eslint-disable-next-line no-eval
const meta = eval('(' + match[1] + ')')
return {
...meta,
path: '/posts/' + file.replace(/\.mdx?$/, ''),
index
}
})
.filter(meta => meta.published)
.sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt))
import preval from 'babel-plugin-preval/macro'
/**
* The preval plugin pre-evaluates code at build time. We use this to get the
* meta from the MDX files (blog posts) and use it for displaying the list of
* posts in the `index.js` page
*
* This code is not used in the browser or the app at all, but only in build
* time when Node is available.
*/
const posts = preval`
module.exports = require('./get-blog-posts.js');
`
module.exports = posts
const fs = require('fs')
const path = require('path')
const posts = require('./get-blog-posts')
const { siteMeta } = require('../blog.config')
// https://jsonfeed.org/version/1
const feed = {
version: 'https://jsonfeed.org/version/1',
title: siteMeta.title,
home_page_url: siteMeta.siteUrl,
feed_url: `${siteMeta.siteUrl}/feed.json`,
description: siteMeta.description,
icon: `${siteMeta.siteUrl}/static/apple-touch-icon-152x152.png`,
favicon: `${siteMeta.siteUrl}/static/favicon.ico`,
author: {
name: siteMeta.author,
url: siteMeta.siteUrl,
avatar: `${siteMeta.siteUrl}/static/_jolvera-avatar.jpg`
},
items: posts.map(post => ({
id: `${siteMeta.siteUrl}${post.path}`,
url: `${siteMeta.siteUrl}${post.path}`,
title: post.title,
content_text: `${post.summary} - ${siteMeta.siteUrl}${post.path}`,
summary: post.summary,
image: `${siteMeta.siteUrl}${post.image}`,
date_published: post.publishedAt,
author: siteMeta.author
}))
}
fs.writeFileSync(path.join('./.next/static', 'feed.json'), JSON.stringify(feed))
<svg class="svg-icon" width="22" height="22" aria-hidden="true" role="img" focusable="false"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" fill="#ddd"></path>
<path d="M0 0h24v24H0z" fill="none"></path>
</svg>
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册