make sure date later than now will now be publish
This commit is contained in:
1 parent
9ea60c68c7
commit
d8161be401
47 files changed
+25
-11589
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
|
You will need these files and folders, either at `./content/` or `../../`
|
||||||
npm run dev
|
|
||||||
# or
|
```sh
|
||||||
yarn dev
|
_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:
|
- Search engine with `lunr.js`
|
||||||
|
- Commenting engine with `remark42`
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
- Analytics with `plausible analytics`
|
||||||
- [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.
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@zhdiary/next",
|
"name": "@patarapolw/next-awsm",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -58,4 +58,4 @@
|
|||||||
"style": "module"
|
"style": "module"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||||||
paths: Array(
|
paths: Array(
|
||||||
Math.ceil(
|
Math.ceil(
|
||||||
dbPost.find({
|
dbPost.find({
|
||||||
date: { $exists: true } as any
|
date: { $lt: new Date().getTime() }
|
||||||
}).length / 5
|
}).length / 5
|
||||||
) - 1
|
) - 1
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||||||
await initDatabase()
|
await initDatabase()
|
||||||
const tMap = new Map<string, number>()
|
const tMap = new Map<string, number>()
|
||||||
|
|
||||||
dbPost.find().map(({ tag = [] }) => {
|
dbPost.find({
|
||||||
|
date: { $lt: new Date().getTime() }
|
||||||
|
}).map(({ tag = [] }) => {
|
||||||
return tag.map((t) => {
|
return tag.map((t) => {
|
||||||
return tMap.set(t, (tMap.get(t) || 0) + 1)
|
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