make sure date later than now will now be publish
This commit is contained in:
1 parent
9ea60c68c7
commit
d8161be401
47 files changed
+24
-11588
No files matched your search
Whitespace-only changes.
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint",
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
+15
-22
@@ -1,30 +1,23 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
Almost classless Next.js template (with TypeScript, ESLint, Prettier) powered by awsm.css
|
||||
|
||||
## Getting Started
|
||||
Tested with Yarn and Node.js 12.
|
||||
|
||||
First, run the development server:
|
||||
## Needed files
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
You will need these files and folders, either at `./content/` or `../../`
|
||||
|
||||
```sh
|
||||
_post/ # => will be parsed to /build/db.loki and /build/idx.json
|
||||
_media/ # => will be copied to /public/media/
|
||||
theme.yml # => will be parsed to /build/theme.json
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
The structure of `theme.yml` can be seen at [/server/theme.ts](/server/theme.ts).
|
||||
|
||||
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
|
||||
Markdown files should be put `_post/`, with YAML frontmatter required keys - `title`. `date` < NOW is required to be "visible" in the listing (at `${baseUrl}/blog`, `${baseUrl}/tag`). For more information, please see [/server/db.ts](/server/db.ts).
|
||||
|
||||
## Learn More
|
||||
## Features
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
- Search engine with `lunr.js`
|
||||
- Commenting engine with `remark42`
|
||||
- Analytics with `plausible analytics`
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@zhdiary/next",
|
||||
"name": "@patarapolw/next-awsm",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -29,7 +29,7 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||
paths: Array(
|
||||
Math.ceil(
|
||||
dbPost.find({
|
||||
date: { $exists: true } as any
|
||||
date: { $lt: new Date().getTime() }
|
||||
}).length / 5
|
||||
) - 1
|
||||
)
|
||||
|
||||
@@ -35,7 +35,9 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||
await initDatabase()
|
||||
const tMap = new Map<string, number>()
|
||||
|
||||
dbPost.find().map(({ tag = [] }) => {
|
||||
dbPost.find({
|
||||
date: { $lt: new Date().getTime() }
|
||||
}).map(({ tag = [] }) => {
|
||||
return tag.map((t) => {
|
||||
return tMap.set(t, (tMap.get(t) || 0) + 1)
|
||||
})
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"presets": ["@babel/preset-typescript"],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-object-assign",
|
||||
"@babel/plugin-proposal-object-rest-spread"
|
||||
]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -1,21 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'@nuxtjs/eslint-config-typescript',
|
||||
'prettier',
|
||||
'prettier/vue',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:nuxt/recommended',
|
||||
],
|
||||
plugins: ['prettier'],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
'require-await': 0,
|
||||
'vue/no-v-html': 0,
|
||||
'import/order': 0,
|
||||
},
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
# Logs
|
||||
/logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# IDE / Editor
|
||||
.idea
|
||||
|
||||
# Service worker
|
||||
sw.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Vim swap files
|
||||
*.swp
|
||||
|
||||
/build/
|
||||
/static/media/
|
||||
/*-dist/
|
||||
@@ -1,4 +0,0 @@
|
||||
server/
|
||||
scripts/
|
||||
lambda/
|
||||
lambda-dist/
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
# zhdiary
|
||||
|
||||
## Build Setup
|
||||
|
||||
```bash
|
||||
# install dependencies
|
||||
$ yarn install
|
||||
|
||||
# serve with hot reload at localhost:3000
|
||||
$ yarn dev
|
||||
|
||||
# build for production and launch server
|
||||
$ yarn build
|
||||
$ yarn start
|
||||
|
||||
# generate static project
|
||||
$ yarn generate
|
||||
```
|
||||
|
||||
For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org).
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function () {
|
||||
return { x: 0, y: 0 }
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
interface IPostTeaser {
|
||||
title: string
|
||||
path: string
|
||||
date?: string
|
||||
excerptHtml: string
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { ITheme } from '~/server/theme'
|
||||
|
||||
export const THEME: ITheme = JSON.parse(process.env.THEME!)
|
||||
@@ -1,48 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import { THEME } from './global'
|
||||
|
||||
export type Theme = 'light' | 'dark'
|
||||
|
||||
export interface CommentsConfig {
|
||||
host: string
|
||||
site_id: string
|
||||
url?: string
|
||||
max_shown_comments?: number
|
||||
theme?: Theme
|
||||
page_title?: string
|
||||
node?: string | HTMLElement
|
||||
locale?: string
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
__colors__?: any
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
remark_config: CommentsConfig
|
||||
REMARK42: {
|
||||
changeTheme(theme: Theme): void
|
||||
destroy?: () => void
|
||||
createInstance: (
|
||||
remark_config: CommentsConfig
|
||||
) =>
|
||||
| {
|
||||
changeTheme(theme: Theme): void
|
||||
destroy(): void
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.browser && THEME?.comment?.remark42) {
|
||||
;(function (c) {
|
||||
for (let i = 0; i < c.length; i++) {
|
||||
const d = document
|
||||
const s = d.createElement('script')
|
||||
s.src = THEME.comment.remark42.host + '/web/' + c[i] + '.js'
|
||||
s.defer = true
|
||||
;(d.head || d.body).appendChild(s)
|
||||
}
|
||||
})(['embed'])
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
<template>
|
||||
<article>
|
||||
<post-teaser v-for="p in posts" :key="p.path" :post="p"></post-teaser>
|
||||
|
||||
<nav v-if="count > 5">
|
||||
<ul>
|
||||
<li v-if="page > 1">
|
||||
<nuxt-link :to="makePageUrl(1)" aria-label="page 1">1</nuxt-link>
|
||||
</li>
|
||||
|
||||
<li v-if="page > 3">
|
||||
<span> … </span>
|
||||
</li>
|
||||
|
||||
<li v-if="page > 2">
|
||||
<nuxt-link
|
||||
:to="makePageUrl(page - 1)"
|
||||
:aria-label="`page ${page - 1}`"
|
||||
>
|
||||
{{ page - 1 }}
|
||||
</nuxt-link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<span>
|
||||
{{ page }}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li v-if="page < total - 1">
|
||||
<nuxt-link
|
||||
:to="makePageUrl(page + 1)"
|
||||
:aria-label="`page ${page + 1}`"
|
||||
>
|
||||
{{ page + 1 }}
|
||||
</nuxt-link>
|
||||
</li>
|
||||
|
||||
<li v-if="page < total - 2">
|
||||
<span> … </span>
|
||||
</li>
|
||||
|
||||
<li v-if="page < total">
|
||||
<nuxt-link :to="makePageUrl(total)" :aria-label="`page ${total}`">
|
||||
{{ total }}
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop, Watch } from 'nuxt-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class PostQuery extends Vue {
|
||||
@Prop({ required: true }) posts!: IPostTeaser[]
|
||||
@Prop({ required: true }) count!: number
|
||||
|
||||
readonly defaults = {
|
||||
posts: this.posts,
|
||||
count: this.count,
|
||||
}
|
||||
|
||||
get page() {
|
||||
return parseInt((this.$route.query.page as string) || '1')
|
||||
}
|
||||
|
||||
get total() {
|
||||
return Math.ceil(this.count / 5)
|
||||
}
|
||||
|
||||
makePageUrl(p: number) {
|
||||
let path = this.$route.path
|
||||
if (!(path.startsWith('/tag') || path.startsWith('/blog'))) {
|
||||
path = '/blog'
|
||||
}
|
||||
|
||||
path = path.replace(/\d+$/, '').replace(/\/$/, '')
|
||||
if (p > 1) {
|
||||
path = path + '/' + p
|
||||
}
|
||||
|
||||
return this.$router.resolve({
|
||||
path,
|
||||
query: this.$route.query,
|
||||
}).href
|
||||
}
|
||||
|
||||
@Watch('$route.params.page')
|
||||
@Watch('$route.params.tag')
|
||||
async updatePosts() {
|
||||
if (this.$route.query.q) {
|
||||
const ps = await this.$axios.$post(
|
||||
'/.netlify/functions/search',
|
||||
undefined,
|
||||
{
|
||||
params: {
|
||||
q: this.$route.query.q,
|
||||
page: this.$route.params.page,
|
||||
tag: this.$route.params.tag,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
this.count = ps.count
|
||||
this.$set(this, 'posts', ps.result)
|
||||
} else {
|
||||
this.count = this.defaults.count
|
||||
this.$set(this, 'posts', this.defaults.posts)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<section>
|
||||
<h2>
|
||||
<nuxt-link :to="'/post/' + post.path">
|
||||
{{ post.title }}
|
||||
</nuxt-link>
|
||||
</h2>
|
||||
<small>
|
||||
<span>
|
||||
by
|
||||
<a
|
||||
v-if="theme.author.url"
|
||||
:href="theme.author.url"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{{ theme.author.name }}
|
||||
</a>
|
||||
<span v-else>
|
||||
{{ theme.author.name }}
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="post.date" style="float: right">
|
||||
{{ post.date | formatDate }}
|
||||
</span>
|
||||
</small>
|
||||
|
||||
<article v-html="post.excerptHtml"></article>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop } from 'nuxt-property-decorator'
|
||||
import { THEME } from '~/assets/global'
|
||||
|
||||
@Component
|
||||
export default class PostTeaser extends Vue {
|
||||
@Prop() post!: IPostTeaser
|
||||
|
||||
theme = THEME
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
article {
|
||||
margin-top: 2em;
|
||||
}
|
||||
</style>
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Handler } from 'aws-lambda'
|
||||
import lunr, { Index } from 'lunr'
|
||||
|
||||
import idxJson from '../build/idx.json'
|
||||
import { dbPost, initDatabase } from '../server/db'
|
||||
|
||||
let idx: Index
|
||||
|
||||
export const handler: Handler = async (evt) => {
|
||||
const { q = '', tag, page: _page = '1' } = evt.queryStringParameters || {}
|
||||
const perPage = 5
|
||||
const offset = (parseInt(_page) - 1) * perPage
|
||||
|
||||
idx = idx || lunr.Index.load(idxJson)
|
||||
await initDatabase()
|
||||
|
||||
let cond = dbPost.chain()
|
||||
|
||||
if (q) {
|
||||
cond = cond.find({
|
||||
path: { $in: idx.search(q).map(({ ref }) => ref) },
|
||||
})
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
cond = cond.find({
|
||||
tag: { $contains: tag },
|
||||
})
|
||||
}
|
||||
|
||||
const count = cond.branch().count()
|
||||
const result = cond
|
||||
.offset(offset)
|
||||
.limit(5)
|
||||
.data()
|
||||
.map(({ title, path, date, excerptHtml }) => ({
|
||||
title,
|
||||
path,
|
||||
date,
|
||||
excerptHtml,
|
||||
}))
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({
|
||||
count,
|
||||
result,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<main>
|
||||
<header>
|
||||
<h1>{{ theme.title }}</h1>
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<nuxt-link to="/">Home</nuxt-link>
|
||||
</li>
|
||||
<li v-for="(n, i) in theme.nav || []" :key="i">
|
||||
<nuxt-link v-if="n.to" :to="n.to">{{ n.name }}</nuxt-link>
|
||||
<a v-else :href="n.href" target="_blank" rel="noopener">
|
||||
{{ n.name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<form @submit.prevent="doSearch">
|
||||
<input v-model="q" type="search" placeholder="Search..." />
|
||||
<div class="icon">🔎</div>
|
||||
</form>
|
||||
</nav>
|
||||
</header>
|
||||
<Nuxt />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from 'nuxt-property-decorator'
|
||||
import { THEME } from '~/assets/global'
|
||||
|
||||
@Component
|
||||
export default class DefaultLayout extends Vue {
|
||||
theme = THEME
|
||||
q = this.$route.query.q || ''
|
||||
|
||||
doSearch() {
|
||||
let path = this.$route.path
|
||||
if (['/blog', '/tag'].includes(path)) {
|
||||
path = '/blog'
|
||||
}
|
||||
|
||||
this.$router.push({
|
||||
path,
|
||||
query: {
|
||||
q: this.q,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
nav ul {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
form {
|
||||
float: right;
|
||||
max-width: 300px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
form .icon {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
input {
|
||||
padding-right: 1.5em;
|
||||
}
|
||||
</style>
|
||||
@@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<section>
|
||||
<h2 v-if="error.statusCode === 404">Page not found</h2>
|
||||
<pre v-else style="width: 100%">{{ error }}</pre>
|
||||
</section>
|
||||
<section>
|
||||
<NuxtLink to="/">Home page</NuxtLink>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['error'],
|
||||
layout: 'error', // you can set a custom layout for the error page
|
||||
}
|
||||
</script>
|
||||
@@ -1,8 +0,0 @@
|
||||
# MIDDLEWARE
|
||||
|
||||
**This directory is not required, you can delete it if you don't want to use it.**
|
||||
|
||||
This directory contains your application middleware.
|
||||
Middleware let you define custom functions that can be run before rendering either a page or a group of pages.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware).
|
||||
@@ -1,3 +0,0 @@
|
||||
[build]
|
||||
functions = "lambda-dist"
|
||||
publish = "dist"
|
||||
@@ -1,107 +0,0 @@
|
||||
import { NuxtConfig } from '@nuxt/types'
|
||||
|
||||
import { SERVER_PORT, runServer } from './server/cli'
|
||||
import { getTheme } from './server/theme'
|
||||
|
||||
const config = async (): Promise<NuxtConfig> => {
|
||||
await runServer()
|
||||
const theme = getTheme()
|
||||
|
||||
return {
|
||||
target: 'static',
|
||||
|
||||
// Global page headers (https://go.nuxtjs.dev/config-head)
|
||||
head: {
|
||||
title: theme.title,
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: theme.description || '',
|
||||
},
|
||||
],
|
||||
link: [
|
||||
{ rel: 'icon', type: 'image/x-icon', href: '/media/favicon.ico' },
|
||||
{
|
||||
rel: 'stylesheet',
|
||||
href: theme.awsm
|
||||
? `https://unpkg.com/awsm.css/dist/awsm_theme_${theme.awsm}.min.css`
|
||||
: 'https://unpkg.com/awsm.css/dist/awsm.min.css',
|
||||
},
|
||||
],
|
||||
script: [
|
||||
...(theme.analytics?.plausible
|
||||
? [
|
||||
{
|
||||
src: 'https://plausible.io/js/plausible.js',
|
||||
async: true,
|
||||
defer: true,
|
||||
'data-domain': theme.analytics?.plausible,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
src:
|
||||
'https://unpkg.com/@webcomponents/webcomponentsjs/webcomponents-loader.js',
|
||||
defer: true,
|
||||
},
|
||||
{
|
||||
type: 'module',
|
||||
src: '/webcomponents.js',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Global CSS (https://go.nuxtjs.dev/config-css)
|
||||
css: [],
|
||||
|
||||
// Plugins to run before rendering page (https://go.nuxtjs.dev/config-plugins)
|
||||
plugins: ['~/plugins/axios-fix.ts', '~/plugins/format.ts'],
|
||||
|
||||
// Auto import components (https://go.nuxtjs.dev/config-components)
|
||||
components: true,
|
||||
|
||||
// Modules for dev and build (recommended) (https://go.nuxtjs.dev/config-modules)
|
||||
buildModules: [
|
||||
// https://go.nuxtjs.dev/typescript
|
||||
'@nuxt/typescript-build',
|
||||
],
|
||||
|
||||
// Modules (https://go.nuxtjs.dev/config-modules)
|
||||
modules: ['@nuxtjs/axios'],
|
||||
|
||||
axios: {
|
||||
proxy: true,
|
||||
},
|
||||
proxy: {
|
||||
'/.netlify/functions': 'http://localhost:9000',
|
||||
'/api': `http://localhost:${SERVER_PORT}`,
|
||||
},
|
||||
|
||||
// Build Configuration (https://go.nuxtjs.dev/config-build)
|
||||
build: {},
|
||||
env: {
|
||||
THEME: JSON.stringify(theme),
|
||||
SERVER_PORT: SERVER_PORT.toString(),
|
||||
},
|
||||
hooks: {
|
||||
generate: {
|
||||
done() {
|
||||
/**
|
||||
* Do not kill
|
||||
*/
|
||||
// try {
|
||||
// execSync(
|
||||
// `lsof -i tcp:${SERVER_PORT} | grep LISTEN | awk '{print $2}' | xargs kill -9`
|
||||
// )
|
||||
// } catch (_) {}
|
||||
process.exit(0)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -1,85 +0,0 @@
|
||||
{
|
||||
"name": "@patarapolw/nuxt-awsm",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "nuxt-ts start",
|
||||
"generate": "nuxt-ts generate",
|
||||
"lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore .",
|
||||
"ts": "ts-node -O '{\"module\":\"commonjs\"}'",
|
||||
"db:make": "yarn ts scripts/make.ts",
|
||||
"dev": "run-p 'dev:**'",
|
||||
"dev:nuxt": "nuxt-ts",
|
||||
"dev:lambda": "netlify-lambda serve lambda",
|
||||
"prebuild": "yarn db:make",
|
||||
"build": "run-p 'build:**'",
|
||||
"build:nuxt": "nuxt-ts generate",
|
||||
"build:lambda": "netlify-lambda build lambda",
|
||||
"postinstall": "netlify-lambda install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/typescript-runtime": "^2.0.0",
|
||||
"@nuxtjs/axios": "^5.12.4",
|
||||
"@patarapolw/scope-css": "^1.3.0",
|
||||
"@types/aws-lambda": "^8.10.66",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/js-yaml": "^3.12.5",
|
||||
"@types/lokijs": "^1.5.3",
|
||||
"@types/lunr": "^2.3.3",
|
||||
"@types/morgan": "^1.9.2",
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"core-js": "^3.6.5",
|
||||
"express": "^4.17.1",
|
||||
"js-yaml": "^3.14.1",
|
||||
"lokijs": "^1.5.11",
|
||||
"lunr": "^2.3.9",
|
||||
"morgan": "^1.10.0",
|
||||
"netlify-lambda": "^2.0.2",
|
||||
"nuxt": "^2.14.6",
|
||||
"nuxt-property-decorator": "^2.8.8",
|
||||
"showdown": "^1.9.1",
|
||||
"zod": "^1.11.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-transform-object-assign": "^7.12.1",
|
||||
"@babel/preset-typescript": "^7.12.1",
|
||||
"@nuxt/types": "^2.14.6",
|
||||
"@nuxt/typescript-build": "^2.0.3",
|
||||
"@nuxtjs/eslint-config": "^3.1.0",
|
||||
"@nuxtjs/eslint-config-typescript": "^3.0.0",
|
||||
"@nuxtjs/eslint-module": "^2.0.0",
|
||||
"@types/cheerio": "^0.22.23",
|
||||
"@types/ejs": "^3.0.5",
|
||||
"@types/fs-extra": "^9.0.5",
|
||||
"@types/rimraf": "^3.0.0",
|
||||
"@types/showdown": "^1.9.3",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"ejs": "^3.1.5",
|
||||
"eslint": "^7.10.0",
|
||||
"eslint-config-prettier": "^6.12.0",
|
||||
"eslint-plugin-nuxt": "^1.0.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"import-sort-parser-typescript": "^6.0.0",
|
||||
"nodejieba": "^2.5.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.1.2",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "4.0.5"
|
||||
},
|
||||
"importSort": {
|
||||
".ts, .tsx, .config.ts": {
|
||||
"parser": "typescript",
|
||||
"style": "module"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": "12",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": "1",
|
||||
"pnpm": "please-use-yarn"
|
||||
},
|
||||
"engineStrict": true
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<post-query :posts="posts" :count="count" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from 'nuxt-property-decorator'
|
||||
|
||||
@Component<BlogPagePaged>({
|
||||
async asyncData({ $axios, params }) {
|
||||
const { result, count } = await $axios.$get('/api/q', {
|
||||
params: {
|
||||
page: params.page,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
posts: result,
|
||||
count,
|
||||
}
|
||||
},
|
||||
})
|
||||
export default class BlogPagePaged extends Vue {
|
||||
posts: IPostTeaser[] = []
|
||||
count = 0
|
||||
}
|
||||
</script>
|
||||
@@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<post-query :posts="posts" :count="count" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from 'nuxt-property-decorator'
|
||||
|
||||
@Component<BlogPage>({
|
||||
async asyncData({ $axios }) {
|
||||
const { result, count } = await $axios.$get('/api/q')
|
||||
|
||||
return {
|
||||
posts: result,
|
||||
count,
|
||||
}
|
||||
},
|
||||
})
|
||||
export default class BlogPage extends Vue {
|
||||
posts: IPostTeaser[] = []
|
||||
count = 0
|
||||
}
|
||||
</script>
|
||||
@@ -1,9 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
middleware: (ctx) => {
|
||||
ctx.redirect('/blog')
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,173 +0,0 @@
|
||||
<template>
|
||||
<section>
|
||||
<section>
|
||||
<h2>
|
||||
{{ post.title }}
|
||||
</h2>
|
||||
<small>
|
||||
<span>
|
||||
by
|
||||
<a
|
||||
v-if="theme.author.url"
|
||||
:href="theme.author.url"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{{ theme.author.name }}
|
||||
</a>
|
||||
<span v-else>
|
||||
{{ theme.author.name }}
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="post.date" style="float: right">
|
||||
{{ post.date | formatDate }}
|
||||
</span>
|
||||
</small>
|
||||
</section>
|
||||
|
||||
<section v-html="post.contentHtml"></section>
|
||||
|
||||
<section class="tags-container">
|
||||
<span>Tags:</span>
|
||||
<span v-for="t in post.tag || []" :key="t">
|
||||
<nuxt-link :to="`/tag/${t}`">
|
||||
{{ t }}
|
||||
</nuxt-link>
|
||||
</span>
|
||||
</section>
|
||||
|
||||
<footer v-if="remark42settings">
|
||||
<div ref="remark42" />
|
||||
</footer>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Watch } from 'nuxt-property-decorator'
|
||||
import { THEME } from '~/assets/global'
|
||||
import '~/assets/remark42'
|
||||
|
||||
@Component<PostPage>({
|
||||
async asyncData({ $axios, params }) {
|
||||
const r = await $axios.$get('/api', {
|
||||
params: {
|
||||
path: params.pathMatch,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
post: r,
|
||||
}
|
||||
},
|
||||
head() {
|
||||
const { title: _title, content, tag, image } = this.post
|
||||
const title = `${_title} - ${THEME.title}`
|
||||
const description = content.substr(0, 140)
|
||||
|
||||
return {
|
||||
title,
|
||||
meta: [
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: description,
|
||||
},
|
||||
...(tag
|
||||
? [
|
||||
{
|
||||
hid: 'keywords',
|
||||
name: 'keywords',
|
||||
content: tag.join(','),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
hid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: title,
|
||||
},
|
||||
{
|
||||
hid: 'og:description',
|
||||
property: 'og:description',
|
||||
content: description,
|
||||
},
|
||||
{
|
||||
hid: 'twitter:title',
|
||||
property: 'twitter:title',
|
||||
content: title,
|
||||
},
|
||||
{
|
||||
hid: 'twitter:description',
|
||||
property: 'twitter:description',
|
||||
content: description,
|
||||
},
|
||||
...(image
|
||||
? [
|
||||
{
|
||||
hid: 'og:image',
|
||||
property: 'og:image',
|
||||
content: image,
|
||||
},
|
||||
{
|
||||
hid: 'twitter:image',
|
||||
property: 'twitter:image',
|
||||
content: image,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initRemark42()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.remark42Instance) {
|
||||
this.remark42Instance.destroy()
|
||||
}
|
||||
},
|
||||
})
|
||||
export default class PostPage extends Vue {
|
||||
post!: {
|
||||
date?: string
|
||||
title: string
|
||||
image?: string
|
||||
tag?: string[]
|
||||
category?: string[]
|
||||
content: string
|
||||
}
|
||||
|
||||
theme = THEME
|
||||
|
||||
remark42settings = THEME.comment?.remark42 || null
|
||||
remark42Instance: any = null
|
||||
|
||||
@Watch('$route.params.pathMatch')
|
||||
initRemark42() {
|
||||
this.$nextTick(() => {
|
||||
if (
|
||||
this.$refs.remark42 &&
|
||||
process.browser &&
|
||||
this.remark42settings &&
|
||||
window.REMARK42
|
||||
) {
|
||||
if (this.remark42Instance) {
|
||||
this.remark42Instance.destroy()
|
||||
}
|
||||
|
||||
this.remark42Instance = window.REMARK42.createInstance({
|
||||
node: this.$refs.remark42 as HTMLElement,
|
||||
host: this.remark42settings.host,
|
||||
site_id: this.remark42settings.siteId,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tags-container > span + span {
|
||||
margin-left: 0.6em;
|
||||
}
|
||||
</style>
|
||||
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<post-query :posts="posts" :count="count" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from 'nuxt-property-decorator'
|
||||
|
||||
@Component<TagPagePaged>({
|
||||
async asyncData({ $axios, params }) {
|
||||
const { result, count } = await $axios.$get('/api/q', {
|
||||
params: {
|
||||
page: params.page,
|
||||
tag: params.tag,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
posts: result,
|
||||
count,
|
||||
}
|
||||
},
|
||||
})
|
||||
export default class TagPagePaged extends Vue {
|
||||
posts: IPostTeaser[] = []
|
||||
count = 0
|
||||
}
|
||||
</script>
|
||||
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<post-query :posts="posts" :count="count" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from 'nuxt-property-decorator'
|
||||
|
||||
@Component<TagPage>({
|
||||
async asyncData({ $axios, params }) {
|
||||
const { result, count } = await $axios.$get('/api/q', {
|
||||
params: {
|
||||
tag: params.tag,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
posts: result,
|
||||
count,
|
||||
}
|
||||
},
|
||||
})
|
||||
export default class TagPage extends Vue {
|
||||
posts: IPostTeaser[] = []
|
||||
count = 0
|
||||
}
|
||||
</script>
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Plugin } from '@nuxt/types'
|
||||
|
||||
const plugin: Plugin = ({ $axios, error }) => {
|
||||
$axios.defaults.baseURL =
|
||||
typeof location !== 'undefined'
|
||||
? location.origin
|
||||
: `http://localhost:${process.env.SERVER_PORT}`
|
||||
|
||||
$axios.interceptors.response.use(
|
||||
(r) => {
|
||||
if (r.data?.error) {
|
||||
error({
|
||||
statusCode: r.status,
|
||||
message: r.data.error,
|
||||
})
|
||||
throw new Error(r.data.error)
|
||||
}
|
||||
|
||||
return r
|
||||
},
|
||||
(err) => {
|
||||
error({
|
||||
statusCode: err.response.status,
|
||||
message: err.toJSON(),
|
||||
})
|
||||
|
||||
return Promise.reject(err)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export default plugin
|
||||
@@ -1,12 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
Vue.filter('formatDate', (s?: string | number) => {
|
||||
return s
|
||||
? new Date(s).toLocaleDateString([], {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'short',
|
||||
})
|
||||
: ''
|
||||
})
|
||||
@@ -1,19 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
import { THEME_FILENAME } from '../server/theme'
|
||||
|
||||
dotenv.config({ path: '../../.env' })
|
||||
dotenv.config()
|
||||
|
||||
export const CONTENT_PATH = path.resolve(
|
||||
process.env.CONTENT_PATH ||
|
||||
(fs.existsSync('../../_post') ? '../..' : 'content')
|
||||
)
|
||||
|
||||
export const srcPath = (
|
||||
root: '_post' | '_media' | typeof THEME_FILENAME,
|
||||
...ps: string[]
|
||||
): string => path.join(CONTENT_PATH, root, ...ps)
|
||||
@@ -1,154 +0,0 @@
|
||||
import crypto from 'crypto'
|
||||
|
||||
import scopeCss from '@patarapolw/scope-css'
|
||||
import cheerio from 'cheerio'
|
||||
import ejs from 'ejs'
|
||||
import showdown from 'showdown'
|
||||
|
||||
export class MakeHtml {
|
||||
$ = cheerio.load('')
|
||||
|
||||
mdConverter = new showdown.Converter({
|
||||
noHeaderId: true,
|
||||
parseImgDimensions: true,
|
||||
simplifiedAutoLink: true,
|
||||
// literalMidWordUnderscores: true,
|
||||
strikethrough: true,
|
||||
tables: true,
|
||||
tasklists: true,
|
||||
// simpleLineBreaks: true,
|
||||
// openLinksInNewWindow: true,
|
||||
backslashEscapesHTMLTags: true,
|
||||
emoji: true,
|
||||
underline: true,
|
||||
disableForced4SpacesIndentedSublists: true,
|
||||
metadata: true,
|
||||
})
|
||||
|
||||
constructor() {
|
||||
this.mdConverter.addExtension(
|
||||
{
|
||||
type: 'lang',
|
||||
regex: /\\\n/g,
|
||||
replace: '<br />',
|
||||
},
|
||||
'commonmark-simple-linebreak'
|
||||
)
|
||||
|
||||
/**
|
||||
* ! For some reasons default underscore italic fails to render...
|
||||
*/
|
||||
this.mdConverter.addExtension(
|
||||
{
|
||||
type: 'lang',
|
||||
regex: /([^\\])_{1}(.*?[^\\])_{1}/g,
|
||||
replace: '$1<i>$2</i>',
|
||||
},
|
||||
'underscore-italic'
|
||||
)
|
||||
|
||||
/**
|
||||
* ! This is the only way to preserve syntax highlighting, while output multiline strings correctly
|
||||
*/
|
||||
this.mdConverter.addExtension(
|
||||
{
|
||||
type: 'lang',
|
||||
regex: /(^|\n)```html parsed([^]+)\n```\n/g,
|
||||
replace: (_: string, p1: string, p2: string) => {
|
||||
return (
|
||||
p1 +
|
||||
ejs.render(p2, {
|
||||
rmWhitespace: true,
|
||||
beautify: false,
|
||||
})
|
||||
)
|
||||
},
|
||||
},
|
||||
'html-parsed'
|
||||
)
|
||||
}
|
||||
|
||||
async parse(
|
||||
filename: string
|
||||
): Promise<{
|
||||
html: string
|
||||
text: string
|
||||
}> {
|
||||
const id =
|
||||
'md-' +
|
||||
crypto
|
||||
.createHash('sha256')
|
||||
.update(filename)
|
||||
.digest()
|
||||
.toString('hex')
|
||||
.substr(0, 8)
|
||||
|
||||
const $div = this.$(`<div id="${id}">`)
|
||||
$div.html(await this.getHTML(filename))
|
||||
|
||||
$div.find('style').each((_, el) => {
|
||||
const $el = this.$(el)
|
||||
const html = $el.html() || ''
|
||||
if (html.trim()) {
|
||||
$el.html(scopeCss(html, `#${id}`))
|
||||
}
|
||||
})
|
||||
|
||||
$div.find('a').each((_, el) => {
|
||||
const $el = this.$(el)
|
||||
const href = $el.attr('href') || ''
|
||||
|
||||
if (/^https?:\/\//.test(href)) {
|
||||
$el.attr({
|
||||
rel: 'noopener',
|
||||
target: '_blank',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
html: this.$('<div>').append($div).html() || '',
|
||||
text: $div.text().trim(),
|
||||
}
|
||||
}
|
||||
|
||||
private async getHTML(filename: string) {
|
||||
return this.mdConverter.makeHtml(
|
||||
await ejs.renderFile(filename, {
|
||||
youtube: (id: string) =>
|
||||
ejs.render(
|
||||
/* html */ `
|
||||
<div style="position:relative;padding-top:56.25%;">
|
||||
<iframe src="https://www.youtube.com/embed/${id}"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
style="position:absolute;top:0;left:0;width:100%;height:100%"></iframe>
|
||||
</div>`,
|
||||
{ rmWhitespace: true, beautify: false }
|
||||
),
|
||||
xCard: (opts: {
|
||||
href: string
|
||||
image?: string
|
||||
title: string
|
||||
description?: string
|
||||
}) =>
|
||||
this.$('<div>')
|
||||
.append(
|
||||
this.$('<x-card>')
|
||||
.attr(opts)
|
||||
.append(
|
||||
this.$('<a>')
|
||||
.attr({
|
||||
href: opts.href,
|
||||
rel: 'noopener',
|
||||
target: '_blank',
|
||||
})
|
||||
.text(opts.title)
|
||||
)
|
||||
)
|
||||
.html(),
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import fg from 'fast-glob'
|
||||
import fs from 'fs-extra'
|
||||
import yaml from 'js-yaml'
|
||||
import lunr from 'lunr'
|
||||
import jieba from 'nodejieba'
|
||||
import rimraf from 'rimraf'
|
||||
|
||||
import { IPost, db, dbPost, initDatabase, zMatter, zPost } from '../server/db'
|
||||
import { buildPath, dstMediaPath } from '../server/dir'
|
||||
import { THEME_FILENAME } from '../server/theme'
|
||||
import { srcPath } from './dir'
|
||||
import { MakeHtml } from './make-html'
|
||||
|
||||
async function main() {
|
||||
rimraf.sync(dstMediaPath())
|
||||
rimraf.sync(buildPath())
|
||||
|
||||
await fs.copy(srcPath('_media'), dstMediaPath()).catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e)
|
||||
})
|
||||
await fs.ensureDir(buildPath())
|
||||
|
||||
await fs.copyFile(srcPath(THEME_FILENAME), buildPath(THEME_FILENAME))
|
||||
|
||||
const files = await fg('**/*.md', {
|
||||
cwd: srcPath('_post'),
|
||||
})
|
||||
|
||||
const makeHtml = new MakeHtml()
|
||||
const rawJson: IPost[] = []
|
||||
|
||||
await initDatabase()
|
||||
|
||||
await Promise.all(
|
||||
files.map(async (f) => {
|
||||
const header = zMatter.parse(
|
||||
yaml.safeLoad(
|
||||
fs.readFileSync(srcPath('_post', f), 'utf-8').split(/\n---\n/)[0]
|
||||
)
|
||||
)
|
||||
|
||||
const { html, text } = await makeHtml.parse(srcPath('_post', f))
|
||||
|
||||
const p: IPost = {
|
||||
...header,
|
||||
date: header.date ? header.date.getTime() : undefined,
|
||||
createdAt: header.createdAt ? header.createdAt.getTime() : undefined,
|
||||
updatedAt: header.updatedAt ? header.updatedAt.getTime() : undefined,
|
||||
path: f.replace(/\.mdx?$/, ''),
|
||||
excerptHtml: html.split(/<!-- excerpt(?:_separator)? -->/)[0],
|
||||
contentHtml: html,
|
||||
content: text,
|
||||
}
|
||||
|
||||
dbPost.insert(zPost.parse(p))
|
||||
rawJson.push({
|
||||
...p,
|
||||
content: jieba.cutForSearch(p.content).join(' '),
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
db.save()
|
||||
|
||||
fs.writeFileSync(
|
||||
buildPath('idx.json'),
|
||||
JSON.stringify(
|
||||
lunr(function () {
|
||||
this.ref('path')
|
||||
this.field('title', { boost: 5 })
|
||||
this.field('tag', { boost: 5 })
|
||||
this.field('category', { boost: 5 })
|
||||
this.field('content')
|
||||
|
||||
rawJson.map((doc) => {
|
||||
return this.add(doc)
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main()
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import http from 'http'
|
||||
|
||||
import express from 'express'
|
||||
import lunr from 'lunr'
|
||||
import morgan from 'morgan'
|
||||
import * as z from 'zod'
|
||||
|
||||
import idxJson from '../build/idx.json'
|
||||
import { dbPost, initDatabase } from './db'
|
||||
|
||||
export const SERVER_PORT = 5000
|
||||
|
||||
export async function runServer() {
|
||||
await initDatabase()
|
||||
|
||||
const app = express()
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
app.use(morgan('common'))
|
||||
}
|
||||
|
||||
const router = express.Router()
|
||||
app.use('/api', router)
|
||||
|
||||
const idx = lunr.Index.load(idxJson)
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
const { path } = z
|
||||
.object({
|
||||
path: z.string(),
|
||||
})
|
||||
.parse(req.query)
|
||||
|
||||
const r = dbPost.findOne({ path })
|
||||
if (!r) {
|
||||
res.sendStatus(404)
|
||||
return
|
||||
}
|
||||
|
||||
res.json(r)
|
||||
})
|
||||
|
||||
router.get('/q', (req, res) => {
|
||||
const { q, tag, page: _page = '1' } = z
|
||||
.object({
|
||||
q: z.string().optional(),
|
||||
tag: z.string().optional(),
|
||||
page: z
|
||||
.string()
|
||||
.refine((s) => /^\d+$/.test(s))
|
||||
.optional(),
|
||||
})
|
||||
.parse(req.query)
|
||||
|
||||
const perPage = 5
|
||||
const offset = (parseInt(_page) - 1) * perPage
|
||||
|
||||
let cond = dbPost.chain().find({
|
||||
date: { $exists: true } as any,
|
||||
})
|
||||
|
||||
if (q) {
|
||||
cond = cond.find({
|
||||
path: { $in: idx.search(q).map(({ ref }) => ref) },
|
||||
})
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
cond = cond.find({
|
||||
tag: { $contains: tag },
|
||||
})
|
||||
}
|
||||
|
||||
cond = cond.simplesort('date', { desc: true })
|
||||
|
||||
const count = cond.branch().count()
|
||||
const result = cond.offset(offset).limit(5).data()
|
||||
|
||||
res.json({
|
||||
count,
|
||||
result,
|
||||
})
|
||||
})
|
||||
|
||||
app.use((err: Error, _: express.Request, res: express.Response) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err.stack)
|
||||
res.status(500).send(err)
|
||||
})
|
||||
|
||||
await new Promise<http.Server | Error>((resolve) => {
|
||||
const srv = app
|
||||
.listen(SERVER_PORT, () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(
|
||||
`serverMiddleware is running at http://localhost:${SERVER_PORT}`
|
||||
)
|
||||
resolve(srv)
|
||||
})
|
||||
.once('error', (e) => {
|
||||
/**
|
||||
* Nuxt generate will try to spawn two process on the same SERVER_PORT.
|
||||
*
|
||||
* One of which will inevitably fail.
|
||||
*
|
||||
* ! DO NOT reject()
|
||||
*/
|
||||
resolve(e)
|
||||
})
|
||||
.once('close', () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('serverMiddleware is closing')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
// eslint-disable-next-line no-console
|
||||
runServer().catch(console.error)
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/* eslint-disable import/no-mutable-exports */
|
||||
import Loki, { Collection } from 'lokijs'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { buildPath } from './dir'
|
||||
|
||||
const _zMatter = {
|
||||
priority: z.number().optional(),
|
||||
title: z.string(),
|
||||
image: z.string().optional(),
|
||||
tag: z.array(z.string()).optional(),
|
||||
category: z.array(z.string()).optional(),
|
||||
}
|
||||
|
||||
export const zMatter = z.object({
|
||||
..._zMatter,
|
||||
date: z.date().optional(),
|
||||
createdAt: z.date().optional(),
|
||||
updatedAt: z.date().optional(),
|
||||
})
|
||||
|
||||
export type IMatter = z.infer<typeof zMatter>
|
||||
|
||||
const zMatterSerialized = z.object({
|
||||
..._zMatter,
|
||||
date: z.number().optional(),
|
||||
createdAt: z.number().optional(),
|
||||
updatedAt: z.number().optional(),
|
||||
})
|
||||
|
||||
export type IMatterSerialized = z.infer<typeof zMatterSerialized>
|
||||
|
||||
export const zPost = z
|
||||
.object({
|
||||
path: z.string(),
|
||||
content: z.string(),
|
||||
excerptHtml: z.string(),
|
||||
contentHtml: z.string(),
|
||||
})
|
||||
.merge(zMatterSerialized)
|
||||
|
||||
export type IPost = z.infer<typeof zPost>
|
||||
|
||||
export interface IPostSearch {
|
||||
path: string
|
||||
title: string
|
||||
tag?: string[]
|
||||
category?: string[]
|
||||
/**
|
||||
* Cleaned and jieba-separated markdown
|
||||
*/
|
||||
content: string
|
||||
}
|
||||
|
||||
export let db: Loki
|
||||
|
||||
export let dbPost: Collection<IPost>
|
||||
|
||||
export async function initDatabase() {
|
||||
if (db) {
|
||||
return
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
db = new Loki(buildPath('data.loki'), {
|
||||
autoload: true,
|
||||
autoloadCallback: () => {
|
||||
dbPost = db.getCollection('post')
|
||||
if (!dbPost) {
|
||||
dbPost = db.addCollection('post', {
|
||||
unique: ['path'],
|
||||
indices: [
|
||||
'date',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'priority',
|
||||
'tag',
|
||||
'category',
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
resolve()
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import path from 'path'
|
||||
|
||||
export const buildPath = (...ps: string[]): string =>
|
||||
path.join(__dirname, '../build', ...ps)
|
||||
|
||||
export const dstMediaPath = (...ps: string[]): string =>
|
||||
path.join(__dirname, '../static/media', ...ps)
|
||||
@@ -1,62 +0,0 @@
|
||||
import fs from 'fs'
|
||||
|
||||
import yaml from 'js-yaml'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { buildPath } from './dir'
|
||||
|
||||
export const THEME_FILENAME = 'theme.yml'
|
||||
|
||||
export const zTheme = z
|
||||
.object({
|
||||
title: z.string(),
|
||||
awsm: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
keywords: z.array(z.string()).optional(),
|
||||
nav: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
name: z.string(),
|
||||
to: z.string(),
|
||||
})
|
||||
.or(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
href: z.string(),
|
||||
})
|
||||
)
|
||||
)
|
||||
.optional(),
|
||||
author: z
|
||||
.object({
|
||||
url: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
name: z.string(),
|
||||
})
|
||||
.nonstrict(),
|
||||
analytics: z
|
||||
.object({
|
||||
plausible: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
comment: z
|
||||
.object({
|
||||
remark42: z
|
||||
.object({
|
||||
host: z.string(),
|
||||
siteId: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.nonstrict()
|
||||
|
||||
export type ITheme = z.infer<typeof zTheme>
|
||||
|
||||
export function getTheme() {
|
||||
return zTheme.parse(
|
||||
yaml.safeLoad(fs.readFileSync(buildPath(THEME_FILENAME), 'utf-8'))
|
||||
)
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,11 +0,0 @@
|
||||
/* global WebComponents */
|
||||
WebComponents.waitFor(() => {
|
||||
// At this point we are guaranteed that all required polyfills have
|
||||
// loaded, and can use web components API's.
|
||||
// The standard pattern is to load element definitions that call
|
||||
// `customElements.define` here.
|
||||
// Note: returning the import's promise causes the custom elements
|
||||
// polyfill to wait until all definitions are loaded and then upgrade
|
||||
// the document in one batch, for better performance.
|
||||
return import('https://unpkg.com/@patarapolw/make-html-x-card')
|
||||
})
|
||||
@@ -1,10 +0,0 @@
|
||||
# STORE
|
||||
|
||||
**This directory is not required, you can delete it if you don't want to use it.**
|
||||
|
||||
This directory contains your Vuex Store files.
|
||||
Vuex Store option is implemented in the Nuxt.js framework.
|
||||
|
||||
Creating a file in this directory automatically activates the option in the framework.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2018",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"lib": ["ESNext", "ESNext.AsyncIterable", "DOM"],
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"experimentalDecorators": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./*"],
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"types": ["@types/node", "@nuxt/types", "@nuxtjs/axios"],
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": ["node_modules", ".nuxt", "dist"]
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user