1657812900
WordPress уже давно является основной CMS для управления контентом вашего веб-сайта, но быстрое развитие интерфейсных технологий открывает новые возможности. Давайте окунемся в шумиху и увидим смесь старого и нового: CMS WordPress с внешним интерфейсом, обслуживаемым приложением Next.js!
Последние годы показали, что популярность архитектуры Jamstack растет, и можно с уверенностью предположить, что она не изменится в ближайшее время. Каждый год рождаются новые JavaScript-фреймворки, а существующие становятся все популярнее и приобретают новых пользователей.
В то же время новая CMS (система управления контентом) разрабатывается с двумя целями. Во-первых, предоставить удобную среду, позволяющую редакторам создавать контент и управлять им. Вторая цель — предоставить данные для внешнего слоя (часто написанного исключительно на JavaScript) таким образом, чтобы было легко использовать данные и красиво отображать тщательно созданный контент.
Между тем, в нашей повседневной работе часто необходимо работать с уже существующими веб-сайтами и поддерживать их без необходимости переписывать их с нуля. Система управления контентом WordPress остается самой популярной CMS в мире, и на это есть много веских причин.
Это бесплатно (как в пиве, так и в свободе) и легко настраивается даже нетехническим пользователем. Более того, многие пользователи уже знакомы с панелью администратора WordPress. Слой WordPress CMS быстро развивается (просто взгляните на редактор Gutenberg — его любят или ненавидят, но он становится все более важным инструментом в вашей настройке WordPress), и у него есть большое сообщество, которое может предоставить доступ к бесчисленным темам и плагинам, позволяющим вам изменить свой сайт в соответствии с вашими прихотями.
С другой стороны, дизайн темы WordPress требует знания PHP , которого может не быть у среднего разработчика интерфейса, и не позволяет использовать современные, готовые к использованию интерфейсные фреймворки. Даже когда это возможно, реализовать тему WordPress с помощью React непросто и неудобно, а простая реализация может привести к плохой оценке SEO.
Можем ли мы получить лучшее из обоих миров? Как насчет того, чтобы обеспечить свободу выбора современных инструментов для внешнего интерфейса наряду с лучшим опытом разработки для внутреннего интерфейса и при этом предоставить знакомую среду WordPress для редакторов контента?
Ниже вы увидите доказательство концепции такого решения с Next.js в качестве фреймворка, с помощью которого мы создадим внешний слой. Таким образом, мы будем стремиться к достижению высоких показателей SEO благодаря рендерингу на стороне сервера.
Приложение будет работать в паре с WordPress как безголовая CMS. Если вы хотите сразу перейти к коду, перейдите в репозиторий GitHub с приложением Next.js и соответствующей безголовой темой WordPress , содержащей упомянутое доказательство концепции.
Получение данных WordPress для приложения Next.js
Для удобства мы будем использовать WordPress API, но не встроенный WordPress REST API. Вместо этого мы будем использовать GraphQL API для получения контента WordPress. Для этого нам понадобится несколько плагинов.
Давайте установим плагин WPGraphQL (и, кроме того, плагин WPGraphQL Offset Pagination ), чтобы расширить нашу WordPress CMS функциональностью GraphQL API и заменить разбиение на страницы на основе курсора на разбиение на страницы на основе страниц. Это понадобится вам для создания приложения Next.js с теми же маршрутами, которые используются на сайте WordPress.
Мы будем использовать Axios
для получения данных из API. Давайте сначала напишем клиент API.
const client = axios.create({ baseURL: `${process.env.WP_PROTOCOL}://${process.env.WP_DOMAIN}/graphql` });
client.interceptors.response.use(
({ data }) => data,
(error) => Promise.reject(error)
);
function graphQL(query: string, variables: Record<string, any>, options?: AxiosRequestConfig) {
return client.post("/", { query, variables }, options);
}
Мы начнем с реализации представления, отображающего страницу WordPress, так как это немного проще, чем реализация записи WordPress. Мы будем извлекать содержимое WordPress каждой страницы по ее слагу в формате getStaticProps
. Нам также нужно получить список страниц, которые будут использоваться в файлах getStaticPaths
. Давайте сначала создадим наши запросы.
export async function getPageBySlug(slug: string) {
return graphQL(
`query PageBySlug($slug: String!) {
pageBy(uri: $slug) {
title
content(format: RENDERED)
slug
}
}`,
{ slug }
);
}
export async function getPages(page: number, perPage: number) {
return graphQL(
`
query AllPages($size: Int!, $offset: Int!) {
pages(where: {offsetPagination: { size: $size, offset: $offset }}) {
edges {
node {
title
slug
}
}
pageInfo {
offsetPagination {
total
}
}
}
}
`,
{ offset: (page - 1) * perPage, size: perPage }
);
}
Теперь пришло время использовать эти запросы. Мы будем использовать маршрут по умолчанию src/pages/[...slug]/index.tsx
для отображения наших страниц там. Сначала мы реализуем наши функции выборки данных.
export const getStaticPaths: GetStaticPaths = async () => {
// Returns a list of { slug: string } objects, uses getPages underneath
const pages = await getAllPages();
return {
paths: pages.map(({ slug }) => ({ params: { slug: slug.split("/") } })),
fallback: false,
};
};
export const getStaticProps: GetStaticProps = async (ctx) => {
if (!ctx.params || !ctx.params.slug) {
return { notFound: true };
}
const {
data: { pageBy: props },
} = await getPageBySlug(ctx.params.slug.join("/"));
return { props };
};
Теперь мы можем использовать данные, возвращаемые getStaticProps
компонентом React, отображаемым на странице.
import parseHTML from "html-react-parser";
const Page = (page: PageProps) => (
<>
<Head>
<title>{page.title}</title>
</Head>
<article>
<header className={styles.header}>
<h1 className={styles.title}>{page.title}</h1>
</header>
<div className={styles.content}>{parseHTML(page.content)}</div>
</article>
</>
);
export default Page;
Аналогичным образом мы реализуем представление, отображающее сообщения WordPress. Опять же, сначала мы реализуем запросы данных.
export type Post = {
id: string;
author: { node: Author };
date: string;
content: string;
slug: string;
title: string;
categories: { edges: { node: Term }[] };
tags: { edges: { node: Term }[] };
};
export async function getPostBySlug(slug: string): Promise<{data: {post: Post}}> {
return graphQL(
`query PostBySlug($slug: ID!) {
post(id: $slug, idType: SLUG) {
author {
node {
name
slug
avatar {
url
}
}
}
id
title
content(format: RENDERED)
slug
date
tags {
edges {
node {
name
slug
}
}
}
categories {
edges {
node {
name
slug
}
}
}
}
}`,
{ slug }
);
}
export async function getPosts(page: number, perPage: number) {
return graphQL(
`
query AllPosts($size: Int!, $offset: Int!) {
posts(where: {offsetPagination: { size: $size, offset: $offset }, orderby: { field: DATE, order: DESC }}) {
edges {
node {
id
author {
node {
name
slug
avatar {
url
}
}
}
title
content(format: RENDERED)
slug
date
tags {
edges {
node {
name
slug
}
}
}
categories {
edges {
node {
name
slug
}
}
}
}
}
pageInfo {
offsetPagination {
total
}
}
}
}
`,
{ offset: (page - 1) * perPage, size: perPage }
);
}
Давайте перейдем к функциям выборки данных WordPress далее в src/pages/[year]/[month]/[day]/[slug]/index.tsx
. На этот раз мы создадим только несколько первых сообщений (обратите внимание fallback: "blocking"
на getStaticPaths
). Остальные будут сгенерированы в первый раз, а затем запрошены и кэшированы на будущее. Это сократит продолжительность сборки. Вполне вероятно, что нам никогда не понадобится отображать большинство старых сообщений, так зачем нам их предварительно генерировать?
export async function getStaticPaths() {
const {
data: {
posts: { edges },
},
} = await getPosts(1, POSTS_PER_PAGE);
return {
paths: edges.map(({ node: { date, slug } }) => ({
params: {
year: format(parseISO(date), "yyyy"),
month: format(parseISO(date), "MM"),
day: format(parseISO(date), "dd"),
slug,
},
})),
fallback: "blocking",
};
}
export const getStaticProps: GetStaticProps = async (ctx) => {
if (!ctx.params || !ctx.params.slug) {
return { notFound: true };
}
const {
data: { post: props },
} = await getPostBySlug(ctx.params.slug);
return { props };
};
Теперь мы можем написать компонент, отображающий один пост WordPress.
import { Post } from "components/post/Post";
const PostPage = (post: PostPageProps) => (
<>
<Head>
<title>{post.title}</title>
</Head>
<Post post={post} />
</>
);
export default PostPage;
Итак, мы реализовали просмотры записей и страниц. Теперь пришло время реализовать страницу архивов. В /page/<pageNumber>/
URL-адресе мы хотели бы отобразить список сообщений. Кроме того, мы хотели бы отображать первую страницу в корне приложения и перенаправлять /page/1/` to `/
. Во-первых, давайте добавим перенаправление в next.config.js
файл.
module.exports = {
...,
async redirects() {
return [
{
source: "/page/1/",
destination: "/",
permanent: true,
}
];
},
trailingSlash: true,
...
};
Теперь давайте реализуем страницу архива в формате src/pages/page/[page]/index.tsx
. Как и раньше, мы начнем с функций, извлекающих данные (то есть список сообщений) для страницы архивов. Мы будем использовать генерацию статического сайта (функция инкрементной статической генерации Next.js), чтобы сначала отображать их, когда будет запрошена страница разбиения на страницы, и в следующий раз обслуживать кешированную версию.
export const getStaticPaths: GetStaticPaths = async () => ({
paths: [],
fallback: "blocking"
});
Метод getStaticProps
для страницы архивов будет выглядеть следующим образом:
export const getStaticProps = async ({ params }: GetStaticPropsContext<{ page: string }>) => {
if (!params || !params.page) {
return { notFound: true };
}
const page = parseInt(params.page);
const {
data: {
posts: {
edges,
pageInfo: {
offsetPagination: { total },
},
},
},
} = await getPosts(page, POSTS_PER_PAGE);
const totalPages = Math.ceil(total / POSTS_PER_PAGE);
return edges.length > 0
? {
props: {
posts: edges.map(({ node }) => node),
pagination: { currentPage: page, totalPages, href: "/" },
},
}
: { notFound: true };
};
Осталось отобразить данные, возвращенные getStaticProps
в представлении.
import { Pagination } from "components/pagination/Pagination";
import { Post } from "components/post/Post";
export const ArchivesPage = ({ posts, pagination }: ArchivesPageProps) => (
<div>
<div>
{posts.map((post) => (
<Post key={post.id} post={post} />
))}
</div>
{pagination.totalPages > 1 && <Pagination {...pagination} className={styles.pagination} />}
</div>
);
export const ArchivesPage;
Это касается всех страниц архива, кроме первой. Этот случай будет рассмотрен на индексной странице в src/pages/index.tsx
файле.
К счастью, с этим можно справиться с помощью нескольких строк кода. Мы будем использовать тот же компонент React. Более того, то, что getStaticProps
мы написали ранее, можно использовать повторно!
import { GetStaticPropsContext } from "next";
import ArchivesPage, { getStaticProps as getPostsArchiveStaticProps } from "pages/page/[page]";
export default ArchivesPage;
export const getStaticProps = async (ctx: GetStaticPropsContext<{}>) =>
getPostsArchiveStaticProps({ ...ctx, params: { page: "1", ...ctx.params } });
Аналогичным образом могут быть реализованы категории, теги и авторские страницы. Для краткости эти конкретные случаи не будут здесь описываться, но вы можете заглянуть в репозиторий с доказательством концепции.
Запуск сборки Next.js при обновлении поста/страницы
Хорошо, у нас работает приложение. Приложение Next.js заполняется данными с сайта WordPress. Тем не менее, было бы неплохо обновлять данные всякий раз, когда запись или страница изменяется в панели администратора WordPress.
Предположим, что приложение Next.js размещается на Vercel . На самом деле подойдет любая платформа, если она позволяет инициировать новое развертывание с помощью запроса POST по указанному URL-адресу. В случае с Vercel мы будем использовать Deploy Hooks .
Вот момент, когда окупится создание всего нескольких начальных постов. Даже небольшое сокращение времени сборки приведет к большой экономии при частых обновлениях в панели администратора WordPress.
Мы будем использовать плагин Vercel Deploy Hooks для WordPress, чтобы инициировать новое развертывание при каждом изменении поста/страницы. Давайте создадим URL-адрес для Deploy Hook, как описано в документации Vercel. Скопируйте его в конфигурацию плагина, установите флажок «Активировать развертывание после обновления» и вуаля!
Каждый раз, когда страница/сообщение обновляется или создается новое сообщение, будет запускаться новое развертывание, чтобы гарантировать, что содержимое нашего приложения Next.js всегда будет актуальным.
Предварительный просмотр постов и страниц
Во-первых, мы сделаем еще несколько предположений о наших приложениях, чтобы предварительный просмотр работал:
admin.domain.com
и next.domain.com
) .admin.domain.com
) домена приложения Next.js (то есть domain.com
),wp-config.php
.define('COOKIE_DOMAIN', '.domain.com');
Тема Headnext изменяет URL-адрес предварительного просмотра WordPress по умолчанию. Остальные предположения гарантируют, что приложение Next.js сможет получить доступ к файлам cookie WordPress для аутентификации и получения данных предварительного просмотра.
Поскольку мы хотим, чтобы наши редакторы могли просматривать контент, который они создают, до того, как он станет общедоступным, мы не можем создать для них статическую страницу, чтобы они могли видеть, как их контент будет выглядеть после публикации. Мы также не хотим использовать getServerSideProps
, так как не хотим еще раз реализовывать просмотры записей/страниц.
Мы будем использовать режим предварительного просмотра Next.js, чтобы переопределить getStaticProps
данные для этих страниц. Таким образом, нам не нужно отказываться от скорости статической генерации, но при этом иметь возможность повторно использовать существующий код.
Но сначала мы реализуем конечную точку API для предварительного просмотра поста/страницы в src/pages/api/[type]/[id]/preview/index.ts
файле. И тип предварительного просмотра (запись/страница), и идентификатор записи/страницы будут переданы в URL-адресе. Мы также передадим nonce
строку запроса для доступа к защищенным данным из GraphQL API.
import Cookies from "cookies";
import format from "date-fns/format";
import parseISO from "date-fns/parseISO";
import type { NextApiRequest, NextApiResponse } from "next";
import { getPageRevisions } from "api/pages";
import { getPostById, getPostRevisions } from "api/posts";
import { authorizationCookieName } from "utils/wordpress";
enum PreviewType {
Post = "post",
Page = "page",
}
export default async (req: NextApiRequest, res: NextApiResponse) => {
const cookies = new Cookies(req, res);
if (!req.query.nonce) {
return res.status(401).json({ message: "Invalid nonce token!" });
}
const cookieName = authorizationCookieName();
const id = parseInt(req.query.id as string, 10);
const headers = { "X-WP-Nonce": req.query.nonce as string, Cookie: `${cookieName}=${cookies.get(cookieName)}` };
try {
let revisions;
if (req.query.type === PreviewType.Page) {
const { data } = await getPageRevisions(id, { headers });
revisions = data.page.revisions.edges;
} else {
const { data } = await getPostRevisions(id, { headers });
revisions = data.post.revisions.edges;
}
const databaseId = revisions.find(({ node: { isPreview } }) => isPreview)?.node.databaseId || null;
const {
data: { post },
} = databaseId ? await getPostById(databaseId, { headers }) : { data: { post: null } };
if (!databaseId || !post) {
res.status(200).json({ message: "No preview." });
return;
}
const url =
req.query.type === PreviewType.Page
? `/${post.slug}`
: `/${format(parseISO(post.date), "yyyy/MM/dd")}/${post.slug}/`;
res.setPreviewData({ id: databaseId, headers });
res.redirect(url);
} catch (error) {
console.log(error);
res.status(200).json({ error });
}
};
Во-первых, мы должны убедиться, что мы получили необходимые данные в строке запроса. Далее мы извлечем файлы cookie и создадим заголовки для доступа к данным, требующим аутентификации.
Теперь, отправляя заголовки, чтобы действовать как аутентифицированный пользователь, нам нужно получить версии страницы/сообщения WordPress по идентификатору страницы/сообщения из API и получить идентификатор предварительного просмотра. Предварительный просмотр — это запись WordPress, поскольку все версии являются экземплярами сообщений, поэтому нам нужно получить данные по идентификатору предварительного просмотра.
И теперь мы можем использовать режим предварительного просмотра. Давайте установим идентификатор предварительного просмотра и заголовки в данных предварительного просмотра, используя setPreviewData
. К сожалению, его размер ограничен 2 КБ (это в основном файл cookie, но его размер ограничен самим Next.js), поэтому мы не сможем передать туда все данные предварительного просмотра.
Наконец, давайте перенаправим на страницу/публикацию, где мы будем использовать ранее установленные данные.
Теперь, в представлении публикации, мы должны настроить getStaticProps
функцию. Когда данные предварительного просмотра установлены, они доступны в контексте. Давайте посмотрим на обновленную функцию.
export const getStaticProps: GetStaticProps<PostPageProps, Params, PreviewData> = async (ctx) => {
if (ctx.preview && ctx.previewData) {
const { id, headers } = ctx.previewData;
const {
data: { post: post },
} = await getPostById(id, { headers });
return { props: post };
}
if (!ctx.params || !ctx.params.slug) {
return { notFound: true };
}
const {
data: { post: props },
} = await getPostBySlug(ctx.params.slug);
return { props };
};
Что изменилось? Первый if
был добавлен в функцию. Здесь происходит вся магия. Если данные предварительного просмотра доступны, мы хотим переопределить отображение кэшированного представления и снова получить данные предварительного просмотра. o вы помните об ограничении размера данных предварительного просмотра? Затем мы можем вернуть данные предварительного просмотра в качестве данных публикации, которые будут использоваться в компоненте React.
Что с просмотром страницы? Аналогичным образом if
следует добавить аналогичный пункт. Чтобы быть кратким, мы не будем подробно останавливаться на этом. Взгляните на POC-код сами 😉
Что еще предстоит сделать? Мы должны убедиться, что WordPress укажет нашим редакторам на конечную точку предварительного просмотра API, а не на страницу предварительного просмотра WordPress по умолчанию.
Тема Headnext — это базовая тема WordPress без заголовка, позволяющая пользователям изменять URL-адрес по умолчанию для предварительного просмотра WordPress. Кроме того, он не содержит ненужного кода и отображает только ссылку для входа на индексную страницу темы — больше ничего не нужно, так как приложение Next.js будет обрабатывать внешний интерфейс.
Честно говоря, возможность изменить URL-адрес страницы предварительного просмотра WordPress может (и, вероятно, должна быть) извлечена из плагина, чтобы разрешить настройку URL-адреса предварительного просмотра WordPress в общем виде, а не жестко закодированном (сейчас он позволяет настраивать только Next.js). домен приложения). Для проверки концепции этого вполне достаточно.
Из кода видно, что пользователь, нажимающий Preview
кнопку во время публикации примера публикации, будет перенаправлен на https://domain.com/api/post/1/preview?nonce=b192fc4204
URL-адрес предварительного просмотра страницы примера:https://domain.com/api/page/2/preview?nonce=796c7766b1
Честно говоря, мы еще не закончили. Данные предварительного просмотра сохранятся, так как это файл cookie. Мы должны очистить его сами. Давайте создадим новый маршрут API в src/pages/api/exit-preview/index.ts
файле.
export default (_: NextApiRequest, res: NextApiResponse) => {
try {
res.clearPreviewData();
return res.status(200).json({ message: "Everything clear!" });
} catch (error) {
return res.status(500).json({ message: error });
}
};
Мы должны вызывать эту конечную точку API каждый раз, когда отображаем предварительный просмотр. Давайте создадим для этого хук React.
export const usePreviewModeExit = () => {
useEffect(() => {
axios.post("/api/exit-preview")
}, [])
}
Мы должны использовать хук в каждом компоненте, который может позволить нам отображать данные предварительного просмотра. Давайте посмотрим, как он используется в компоненте просмотра публикации.
const PostPage = (post: PostPageProps) => {
usePreviewModeExit();
return (
<>
<Head>
<title>{post.title}</title>
</Head>
<Post post={post} />
</>
);
};
Мы обработали отображение превью как для внешнего интерфейса, так и для уровня CMS. Это должно работать как шарм!
Так вы говорите, что это просто доказательство концепции?
Но да, это все еще доказательство концепции. Есть несколько вещей, которые необходимо сделать, чтобы считать это готовым к производству. Приведенный ниже список, вероятно, может быть еще более исчерпывающим. Итак, что нужно сделать?
theme.json
может быть использована новая функция или вы можете использовать традиционные стили CSS),html-react-parser
,Ссылка: https://tsh.io/blog/headless-wordpress-as-an-api-for-a-next-js-application/
#nextjs #javascript #wordpress
1632537859
Not babashka. Node.js babashka!?
Ad-hoc CLJS scripting on Node.js.
Experimental. Please report issues here.
Nbb's main goal is to make it easy to get started with ad hoc CLJS scripting on Node.js.
Additional goals and features are:
Nbb requires Node.js v12 or newer.
CLJS code is evaluated through SCI, the same interpreter that powers babashka. Because SCI works with advanced compilation, the bundle size, especially when combined with other dependencies, is smaller than what you get with self-hosted CLJS. That makes startup faster. The trade-off is that execution is less performant and that only a subset of CLJS is available (e.g. no deftype, yet).
Install nbb
from NPM:
$ npm install nbb -g
Omit -g
for a local install.
Try out an expression:
$ nbb -e '(+ 1 2 3)'
6
And then install some other NPM libraries to use in the script. E.g.:
$ npm install csv-parse shelljs zx
Create a script which uses the NPM libraries:
(ns script
(:require ["csv-parse/lib/sync$default" :as csv-parse]
["fs" :as fs]
["path" :as path]
["shelljs$default" :as sh]
["term-size$default" :as term-size]
["zx$default" :as zx]
["zx$fs" :as zxfs]
[nbb.core :refer [*file*]]))
(prn (path/resolve "."))
(prn (term-size))
(println (count (str (fs/readFileSync *file*))))
(prn (sh/ls "."))
(prn (csv-parse "foo,bar"))
(prn (zxfs/existsSync *file*))
(zx/$ #js ["ls"])
Call the script:
$ nbb script.cljs
"/private/tmp/test-script"
#js {:columns 216, :rows 47}
510
#js ["node_modules" "package-lock.json" "package.json" "script.cljs"]
#js [#js ["foo" "bar"]]
true
$ ls
node_modules
package-lock.json
package.json
script.cljs
Nbb has first class support for macros: you can define them right inside your .cljs
file, like you are used to from JVM Clojure. Consider the plet
macro to make working with promises more palatable:
(defmacro plet
[bindings & body]
(let [binding-pairs (reverse (partition 2 bindings))
body (cons 'do body)]
(reduce (fn [body [sym expr]]
(let [expr (list '.resolve 'js/Promise expr)]
(list '.then expr (list 'clojure.core/fn (vector sym)
body))))
body
binding-pairs)))
Using this macro we can look async code more like sync code. Consider this puppeteer example:
(-> (.launch puppeteer)
(.then (fn [browser]
(-> (.newPage browser)
(.then (fn [page]
(-> (.goto page "https://clojure.org")
(.then #(.screenshot page #js{:path "screenshot.png"}))
(.catch #(js/console.log %))
(.then #(.close browser)))))))))
Using plet
this becomes:
(plet [browser (.launch puppeteer)
page (.newPage browser)
_ (.goto page "https://clojure.org")
_ (-> (.screenshot page #js{:path "screenshot.png"})
(.catch #(js/console.log %)))]
(.close browser))
See the puppeteer example for the full code.
Since v0.0.36, nbb includes promesa which is a library to deal with promises. The above plet
macro is similar to promesa.core/let
.
$ time nbb -e '(+ 1 2 3)'
6
nbb -e '(+ 1 2 3)' 0.17s user 0.02s system 109% cpu 0.168 total
The baseline startup time for a script is about 170ms seconds on my laptop. When invoked via npx
this adds another 300ms or so, so for faster startup, either use a globally installed nbb
or use $(npm bin)/nbb script.cljs
to bypass npx
.
Nbb does not depend on any NPM dependencies. All NPM libraries loaded by a script are resolved relative to that script. When using the Reagent module, React is resolved in the same way as any other NPM library.
To load .cljs
files from local paths or dependencies, you can use the --classpath
argument. The current dir is added to the classpath automatically. So if there is a file foo/bar.cljs
relative to your current dir, then you can load it via (:require [foo.bar :as fb])
. Note that nbb
uses the same naming conventions for namespaces and directories as other Clojure tools: foo-bar
in the namespace name becomes foo_bar
in the directory name.
To load dependencies from the Clojure ecosystem, you can use the Clojure CLI or babashka to download them and produce a classpath:
$ classpath="$(clojure -A:nbb -Spath -Sdeps '{:aliases {:nbb {:replace-deps {com.github.seancorfield/honeysql {:git/tag "v2.0.0-rc5" :git/sha "01c3a55"}}}}}')"
and then feed it to the --classpath
argument:
$ nbb --classpath "$classpath" -e "(require '[honey.sql :as sql]) (sql/format {:select :foo :from :bar :where [:= :baz 2]})"
["SELECT foo FROM bar WHERE baz = ?" 2]
Currently nbb
only reads from directories, not jar files, so you are encouraged to use git libs. Support for .jar
files will be added later.
The name of the file that is currently being executed is available via nbb.core/*file*
or on the metadata of vars:
(ns foo
(:require [nbb.core :refer [*file*]]))
(prn *file*) ;; "/private/tmp/foo.cljs"
(defn f [])
(prn (:file (meta #'f))) ;; "/private/tmp/foo.cljs"
Nbb includes reagent.core
which will be lazily loaded when required. You can use this together with ink to create a TUI application:
$ npm install ink
ink-demo.cljs
:
(ns ink-demo
(:require ["ink" :refer [render Text]]
[reagent.core :as r]))
(defonce state (r/atom 0))
(doseq [n (range 1 11)]
(js/setTimeout #(swap! state inc) (* n 500)))
(defn hello []
[:> Text {:color "green"} "Hello, world! " @state])
(render (r/as-element [hello]))
Working with callbacks and promises can become tedious. Since nbb v0.0.36 the promesa.core
namespace is included with the let
and do!
macros. An example:
(ns prom
(:require [promesa.core :as p]))
(defn sleep [ms]
(js/Promise.
(fn [resolve _]
(js/setTimeout resolve ms))))
(defn do-stuff
[]
(p/do!
(println "Doing stuff which takes a while")
(sleep 1000)
1))
(p/let [a (do-stuff)
b (inc a)
c (do-stuff)
d (+ b c)]
(prn d))
$ nbb prom.cljs
Doing stuff which takes a while
Doing stuff which takes a while
3
Also see API docs.
Since nbb v0.0.75 applied-science/js-interop is available:
(ns example
(:require [applied-science.js-interop :as j]))
(def o (j/lit {:a 1 :b 2 :c {:d 1}}))
(prn (j/select-keys o [:a :b])) ;; #js {:a 1, :b 2}
(prn (j/get-in o [:c :d])) ;; 1
Most of this library is supported in nbb, except the following:
:syms
.-x
notation. In nbb, you must use keywords.See the example of what is currently supported.
See the examples directory for small examples.
Also check out these projects built with nbb:
See API documentation.
See this gist on how to convert an nbb script or project to shadow-cljs.
Prequisites:
To build:
bb release
Run bb tasks
for more project-related tasks.
Download Details:
Author: borkdude
Download Link: Download The Source Code
Official Website: https://github.com/borkdude/nbb
License: EPL-1.0
#node #javascript
1595396220
As more and more data is exposed via APIs either as API-first companies or for the explosion of single page apps/JAMStack, API security can no longer be an afterthought. The hard part about APIs is that it provides direct access to large amounts of data while bypassing browser precautions. Instead of worrying about SQL injection and XSS issues, you should be concerned about the bad actor who was able to paginate through all your customer records and their data.
Typical prevention mechanisms like Captchas and browser fingerprinting won’t work since APIs by design need to handle a very large number of API accesses even by a single customer. So where do you start? The first thing is to put yourself in the shoes of a hacker and then instrument your APIs to detect and block common attacks along with unknown unknowns for zero-day exploits. Some of these are on the OWASP Security API list, but not all.
Most APIs provide access to resources that are lists of entities such as /users
or /widgets
. A client such as a browser would typically filter and paginate through this list to limit the number items returned to a client like so:
First Call: GET /items?skip=0&take=10
Second Call: GET /items?skip=10&take=10
However, if that entity has any PII or other information, then a hacker could scrape that endpoint to get a dump of all entities in your database. This could be most dangerous if those entities accidently exposed PII or other sensitive information, but could also be dangerous in providing competitors or others with adoption and usage stats for your business or provide scammers with a way to get large email lists. See how Venmo data was scraped
A naive protection mechanism would be to check the take count and throw an error if greater than 100 or 1000. The problem with this is two-fold:
skip = 0
while True: response = requests.post('https://api.acmeinc.com/widgets?take=10&skip=' + skip), headers={'Authorization': 'Bearer' + ' ' + sys.argv[1]}) print("Fetched 10 items") sleep(randint(100,1000)) skip += 10
To secure against pagination attacks, you should track how many items of a single resource are accessed within a certain time period for each user or API key rather than just at the request level. By tracking API resource access at the user level, you can block a user or API key once they hit a threshold such as “touched 1,000,000 items in a one hour period”. This is dependent on your API use case and can even be dependent on their subscription with you. Like a Captcha, this can slow down the speed that a hacker can exploit your API, like a Captcha if they have to create a new user account manually to create a new API key.
Most APIs are protected by some sort of API key or JWT (JSON Web Token). This provides a natural way to track and protect your API as API security tools can detect abnormal API behavior and block access to an API key automatically. However, hackers will want to outsmart these mechanisms by generating and using a large pool of API keys from a large number of users just like a web hacker would use a large pool of IP addresses to circumvent DDoS protection.
The easiest way to secure against these types of attacks is by requiring a human to sign up for your service and generate API keys. Bot traffic can be prevented with things like Captcha and 2-Factor Authentication. Unless there is a legitimate business case, new users who sign up for your service should not have the ability to generate API keys programmatically. Instead, only trusted customers should have the ability to generate API keys programmatically. Go one step further and ensure any anomaly detection for abnormal behavior is done at the user and account level, not just for each API key.
APIs are used in a way that increases the probability credentials are leaked:
If a key is exposed due to user error, one may think you as the API provider has any blame. However, security is all about reducing surface area and risk. Treat your customer data as if it’s your own and help them by adding guards that prevent accidental key exposure.
The easiest way to prevent key exposure is by leveraging two tokens rather than one. A refresh token is stored as an environment variable and can only be used to generate short lived access tokens. Unlike the refresh token, these short lived tokens can access the resources, but are time limited such as in hours or days.
The customer will store the refresh token with other API keys. Then your SDK will generate access tokens on SDK init or when the last access token expires. If a CURL command gets pasted into a GitHub issue, then a hacker would need to use it within hours reducing the attack vector (unless it was the actual refresh token which is low probability)
APIs open up entirely new business models where customers can access your API platform programmatically. However, this can make DDoS protection tricky. Most DDoS protection is designed to absorb and reject a large number of requests from bad actors during DDoS attacks but still need to let the good ones through. This requires fingerprinting the HTTP requests to check against what looks like bot traffic. This is much harder for API products as all traffic looks like bot traffic and is not coming from a browser where things like cookies are present.
The magical part about APIs is almost every access requires an API Key. If a request doesn’t have an API key, you can automatically reject it which is lightweight on your servers (Ensure authentication is short circuited very early before later middleware like request JSON parsing). So then how do you handle authenticated requests? The easiest is to leverage rate limit counters for each API key such as to handle X requests per minute and reject those above the threshold with a 429 HTTP response.
There are a variety of algorithms to do this such as leaky bucket and fixed window counters.
APIs are no different than web servers when it comes to good server hygiene. Data can be leaked due to misconfigured SSL certificate or allowing non-HTTPS traffic. For modern applications, there is very little reason to accept non-HTTPS requests, but a customer could mistakenly issue a non HTTP request from their application or CURL exposing the API key. APIs do not have the protection of a browser so things like HSTS or redirect to HTTPS offer no protection.
Test your SSL implementation over at Qualys SSL Test or similar tool. You should also block all non-HTTP requests which can be done within your load balancer. You should also remove any HTTP headers scrub any error messages that leak implementation details. If your API is used only by your own apps or can only be accessed server-side, then review Authoritative guide to Cross-Origin Resource Sharing for REST APIs
APIs provide access to dynamic data that’s scoped to each API key. Any caching implementation should have the ability to scope to an API key to prevent cross-pollution. Even if you don’t cache anything in your infrastructure, you could expose your customers to security holes. If a customer with a proxy server was using multiple API keys such as one for development and one for production, then they could see cross-pollinated data.
#api management #api security #api best practices #api providers #security analytics #api management policies #api access tokens #api access #api security risks #api access keys
1601381326
We’ve conducted some initial research into the public APIs of the ASX100 because we regularly have conversations about what others are doing with their APIs and what best practices look like. Being able to point to good local examples and explain what is happening in Australia is a key part of this conversation.
The method used for this initial research was to obtain a list of the ASX100 (as of 18 September 2020). Then work through each company looking at the following:
With regards to how the APIs are shared:
#api #api-development #api-analytics #apis #api-integration #api-testing #api-security #api-gateway
1625751960
In this video, I wanted to touch upon the functionality of adding Chapters inside a Course. The idea was to not think much and start the development and pick up things as they come.
There are places where I get stuck and trying to find answers to it up doing what every developer does - Google and get help. I hope this will help you understand the flow and also how developers debug while doing development.
App url: https://video-reviews.vercel.app
Github code links below:
Next JS App: https://github.com/amitavroy/video-reviews
Laravel API: https://github.com/amitavdevzone/video-review-api
You can find me on:
Twitter: https://twitter.com/amitavroy7
Discord: https://discord.gg/Em4nuvQk
#next js #api #react next js #next #frontend #development
1604399880
I’ve been working with Restful APIs for some time now and one thing that I love to do is to talk about APIs.
So, today I will show you how to build an API using the API-First approach and Design First with OpenAPI Specification.
First thing first, if you don’t know what’s an API-First approach means, it would be nice you stop reading this and check the blog post that I wrote to the Farfetchs blog where I explain everything that you need to know to start an API using API-First.
Before you get your hands dirty, let’s prepare the ground and understand the use case that will be developed.
If you desire to reproduce the examples that will be shown here, you will need some of those items below.
To keep easy to understand, let’s use the Todo List App, it is a very common concept beyond the software development community.
#api #rest-api #openai #api-first-development #api-design #apis #restful-apis #restful-api