From 2b9664280b0bd73f28048ba374299b4de978c2ea Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Mon, 26 Aug 2024 14:14:30 -0400 Subject: [PATCH 01/22] initial lift and shift of blog landing page and posts content and styles --- patches/@greenwood+cli+0.30.0-alpha.5.patch | 629 ++++++++++++++++++ src/assets/blog/evergreen.svg | 45 ++ src/assets/blog/greenwood-logo-1000w.webp | Bin 0 -> 20344 bytes src/assets/blog/greenwood-logo-1500w.webp | Bin 0 -> 31686 bytes src/assets/blog/greenwood-logo-300w.webp | Bin 0 -> 5432 bytes src/assets/blog/greenwood-logo-500w.webp | Bin 0 -> 9838 bytes src/assets/blog/greenwood-logo-750w.webp | Bin 0 -> 14998 bytes src/assets/greenwood-logo-full.svg | 2 +- src/assets/greenwood-logo-g.svg | 2 +- src/assets/greenwood-logo-leaf.svg | 2 +- .../blog-posts-list/blog-posts-list.js | 33 + .../blog-posts-list.module.css | 4 + src/layouts/blog.html | 17 + src/pages/blog/index.html | 34 + src/pages/blog/release/v0-15-0.md | 107 +++ src/pages/blog/release/v0-18-0.md | 37 ++ src/pages/blog/release/v0-19-0.md | 155 +++++ src/pages/blog/release/v0-20-0.md | 37 ++ src/pages/blog/release/v0-21-0.md | 87 +++ src/pages/blog/release/v0-23-0.md | 124 ++++ src/pages/blog/release/v0-24-0.md | 62 ++ src/pages/blog/release/v0-26-0.md | 219 ++++++ src/pages/blog/release/v0-27-0.md | 85 +++ src/pages/blog/release/v0-28-0.md | 141 ++++ src/pages/blog/release/v0-29-0.md | 114 ++++ src/pages/blog/state-of-greenwood-2022.md | 242 +++++++ src/pages/blog/state-of-greenwood-2023.md | 244 +++++++ src/styles/blog.css | 81 +++ 28 files changed, 2500 insertions(+), 3 deletions(-) create mode 100644 src/assets/blog/evergreen.svg create mode 100644 src/assets/blog/greenwood-logo-1000w.webp create mode 100644 src/assets/blog/greenwood-logo-1500w.webp create mode 100644 src/assets/blog/greenwood-logo-300w.webp create mode 100644 src/assets/blog/greenwood-logo-500w.webp create mode 100644 src/assets/blog/greenwood-logo-750w.webp create mode 100644 src/components/blog-posts-list/blog-posts-list.js create mode 100644 src/components/blog-posts-list/blog-posts-list.module.css create mode 100644 src/layouts/blog.html create mode 100644 src/pages/blog/index.html create mode 100644 src/pages/blog/release/v0-15-0.md create mode 100644 src/pages/blog/release/v0-18-0.md create mode 100644 src/pages/blog/release/v0-19-0.md create mode 100644 src/pages/blog/release/v0-20-0.md create mode 100644 src/pages/blog/release/v0-21-0.md create mode 100644 src/pages/blog/release/v0-23-0.md create mode 100644 src/pages/blog/release/v0-24-0.md create mode 100644 src/pages/blog/release/v0-26-0.md create mode 100644 src/pages/blog/release/v0-27-0.md create mode 100644 src/pages/blog/release/v0-28-0.md create mode 100644 src/pages/blog/release/v0-29-0.md create mode 100644 src/pages/blog/state-of-greenwood-2022.md create mode 100644 src/pages/blog/state-of-greenwood-2023.md create mode 100644 src/styles/blog.css diff --git a/patches/@greenwood+cli+0.30.0-alpha.5.patch b/patches/@greenwood+cli+0.30.0-alpha.5.patch index d2e86687..198b3fb2 100644 --- a/patches/@greenwood+cli+0.30.0-alpha.5.patch +++ b/patches/@greenwood+cli+0.30.0-alpha.5.patch @@ -13,6 +13,472 @@ index 1354e66..e50e2aa 100644 for (const plugin of resourcePlugins) { if (plugin.shouldPreIntercept && await plugin.shouldPreIntercept(assetUrl, request, response.clone())) { +diff --git a/node_modules/@greenwood/cli/src/data/queries.js b/node_modules/@greenwood/cli/src/data/queries.js +new file mode 100644 +index 0000000..f4b5f15 +--- /dev/null ++++ b/node_modules/@greenwood/cli/src/data/queries.js +@@ -0,0 +1,66 @@ ++// TODO how to sync host and port with greenwood config ++const host = 'localhost'; ++const port = 1985; ++ ++async function getContent() { ++ return await fetch(`http://${host}:${port}/graph.json`) ++ .then(resp => resp.json()); ++} ++ ++async function getContentByCollection(collection = '') { ++ return (await fetch(`http://${host}:${port}/graph.json`) ++ .then(resp => resp.json())) ++ .filter(page => page?.data?.collection === collection); ++} ++ ++async function getContentByRoute(route = '') { ++ return (await fetch(`http://${host}:${port}/graph.json`) ++ .then(resp => resp.json())) ++ .filter(page => page?.route.startsWith(route)); ++} ++ ++export { getContent, getContentByCollection, getContentByRoute }; ++// import { getQueryHash } from './common.js'; ++ ++// const client = { ++// query: (params) => { ++// const { query, variables = {} } = params; ++ ++// return fetch('http://localhost:4000/graphql', { ++// method: 'POST', ++// headers: { ++// 'Content-Type': 'application/json', ++// 'Accept': 'application/json' ++// }, ++// body: JSON.stringify({ ++// query, ++// variables ++// }) ++// }).then((response) => response.json()); ++// } ++// }; ++ ++// const APOLLO_STATE = globalThis.__APOLLO_STATE__; // eslint-disable-line no-underscore-dangle ++// const BASE_PATH = globalThis.__GWD_BASE_PATH__; // eslint-disable-line no-underscore-dangle ++// const backupQuery = client.query; ++ ++// client.query = (params) => { ++// if (APOLLO_STATE) { ++// // __APOLLO_STATE__ defined, in production mode ++// const queryHash = getQueryHash(params.query, params.variables); ++// const cachePath = `${BASE_PATH}/${queryHash}-cache.json`; ++ ++// return fetch(cachePath) ++// .then(response => response.json()) ++// .then((response) => { ++// return { ++// data: response ++// }; ++// }); ++// } else { ++// // __APOLLO_STATE__ NOT defined, in development mode ++// return backupQuery(params); ++// } ++// }; ++ ++// export default client; +\ No newline at end of file +diff --git a/node_modules/@greenwood/cli/src/lib/layout-utils.js b/node_modules/@greenwood/cli/src/lib/layout-utils.js +index 8dbf281..a3cb477 100644 +--- a/node_modules/@greenwood/cli/src/lib/layout-utils.js ++++ b/node_modules/@greenwood/cli/src/lib/layout-utils.js +@@ -108,7 +108,8 @@ async function getPageLayout(filePath, compilation, layout) { + } + + /* eslint-disable-next-line complexity */ +-async function getAppLayout(pageLayoutContents, compilation, customImports = [], frontmatterTitle) { ++async function getAppLayout(pageLayoutContents, compilation, customImports = [], matchingRoute) { ++ const activeFrontmatterTitleKey = '${globalThis.page.title}'; + const enableHud = compilation.config.devServer.hud; + const { layoutsDir, userLayoutsDir } = compilation.context; + const userStaticAppLayoutUrl = new URL('./app.html', userLayoutsDir); +@@ -193,20 +194,25 @@ async function getAppLayout(pageLayoutContents, compilation, customImports = [], + const appBody = appRoot.querySelector('body') ? appRoot.querySelector('body').innerHTML : ''; + const pageBody = pageRoot && pageRoot.querySelector('body') ? pageRoot.querySelector('body').innerHTML : ''; + const pageTitle = pageRoot && pageRoot.querySelector('head title'); +- const hasInterpolatedFrontmatter = pageTitle && pageTitle.rawText.indexOf('${globalThis.page.title}') >= 0 +- || appTitle && appTitle.rawText.indexOf('${globalThis.page.title}') >= 0; ++ const hasActiveFrontmatterTitle = pageTitle && pageTitle.rawText.indexOf(activeFrontmatterTitleKey) >= 0 ++ || appTitle && appTitle.rawText.indexOf(activeFrontmatterTitleKey) >= 0; ++ let title; + +- const title = hasInterpolatedFrontmatter // favor frontmatter interpolation first +- ? pageTitle && pageTitle.rawText ++ if (hasActiveFrontmatterTitle) { ++ const text = pageTitle && pageTitle.rawText.indexOf(activeFrontmatterTitleKey) >= 0 + ? pageTitle.rawText +- : appTitle.rawText +- : frontmatterTitle // otherwise, work in order of specificity from page -> page layout -> app layout +- ? frontmatterTitle ++ : appTitle.rawText; ++ ++ title = text.replace(activeFrontmatterTitleKey, matchingRoute.title || matchingRoute.label); ++ } else { ++ title = matchingRoute.title ++ ? matchingRoute.title + : pageTitle && pageTitle.rawText + ? pageTitle.rawText + : appTitle && appTitle.rawText + ? appTitle.rawText +- : 'My App'; ++ : matchingRoute.label; ++ } + + const mergedHtml = pageRoot && pageRoot.querySelector('html').rawAttrs !== '' + ? `` +diff --git a/node_modules/@greenwood/cli/src/lib/walker-package-ranger.js b/node_modules/@greenwood/cli/src/lib/walker-package-ranger.js +index 5ce30c1..fa1bed8 100644 +--- a/node_modules/@greenwood/cli/src/lib/walker-package-ranger.js ++++ b/node_modules/@greenwood/cli/src/lib/walker-package-ranger.js +@@ -217,16 +217,41 @@ async function walkPackageJson(packageJson = {}) { + + function mergeImportMap(html = '', map = {}) { + // es-modules-shims breaks on dangling commas in an importMap :/ +- const danglingComma = html.indexOf('"imports": {}') > 0 ? '' : ','; ++ const hasImportMap = html.indexOf('"importmap-shim": {') > 0; ++ const danglingComma = hasImportMap && html.indexOf('"imports": {}') > 0 ? '' : ','; + const importMap = JSON.stringify(map).replace('}', '').replace('{', ''); + +- const merged = html.replace('"imports": {', ` +- "imports": { +- ${importMap}${danglingComma} +- `); +- +- return merged; ++ // TODO looks like this was never working correctly!? :o ++ if (hasImportMap) { ++ return html.replace('"imports": {', ` ++ "imports": { ++ ${importMap}${danglingComma} ++ `); ++ } else { ++ return html.replace('', ` ++ ++ ++ `) ++ } + } ++// function mergeImportMap(html = '', map = {}) { ++// // es-modules-shims breaks on dangling commas in an importMap :/ ++// const danglingComma = html.indexOf('"imports": {}') > 0 ? '' : ','; ++// const importMap = JSON.stringify(map).replace('}', '').replace('{', ''); ++ ++// const merged = html.replace('"imports": {', ` ++// "imports": { ++// ${importMap}${danglingComma} ++// `); ++ ++// return merged; ++// } + + export { + mergeImportMap, +diff --git a/node_modules/@greenwood/cli/src/lifecycles/bundle.js b/node_modules/@greenwood/cli/src/lifecycles/bundle.js +index 9c963f7..d5f1a50 100644 +--- a/node_modules/@greenwood/cli/src/lifecycles/bundle.js ++++ b/node_modules/@greenwood/cli/src/lifecycles/bundle.js +@@ -243,7 +243,7 @@ async function bundleSsrPages(compilation, optimizePlugins) { + // and before we optimize so that all bundled assets can tracked up front + // would be nice to see if this can be done in a single pass though... + for (const page of ssrPages) { +- const { imports, route, layout, title, relativeWorkspacePagePath } = page; ++ const { imports, route, layout, relativeWorkspacePagePath } = page; + const moduleUrl = new URL(`.${relativeWorkspacePagePath}`, pagesDir); + const request = new Request(moduleUrl); + // TODO getLayout has to be static (for now?) +@@ -252,7 +252,7 @@ async function bundleSsrPages(compilation, optimizePlugins) { + let staticHtml = ''; + + staticHtml = data.layout ? data.layout : await getPageLayout(staticHtml, compilation, layout); +- staticHtml = await getAppLayout(staticHtml, compilation, imports, title); ++ staticHtml = await getAppLayout(staticHtml, compilation, imports, page); + staticHtml = await getUserScripts(staticHtml, compilation); + staticHtml = await (await interceptPage(new URL(`http://localhost:8080${route}`), new Request(new URL(`http://localhost:8080${route}`)), getPluginInstances(compilation), staticHtml)).text(); + +diff --git a/node_modules/@greenwood/cli/src/lifecycles/config.js b/node_modules/@greenwood/cli/src/lifecycles/config.js +index 7ee7fc2..9aa3592 100644 +--- a/node_modules/@greenwood/cli/src/lifecycles/config.js ++++ b/node_modules/@greenwood/cli/src/lifecycles/config.js +@@ -46,7 +46,7 @@ const defaultConfig = { + port: 8080, + basePath: '', + optimization: optimizations[0], +- interpolateFrontmatter: false, ++ activeFrontmatter: false, + plugins: greenwoodPlugins, + markdown: { plugins: [], settings: {} }, + prerender: false, +@@ -77,7 +77,7 @@ const readAndMergeConfig = async() => { + + if (hasConfigFile) { + const userCfgFile = (await import(configUrl)).default; +- const { workspace, devServer, markdown, optimization, plugins, port, prerender, basePath, staticRouter, pagesDirectory, layoutsDirectory, interpolateFrontmatter, isolation } = userCfgFile; ++ const { workspace, devServer, markdown, optimization, plugins, port, prerender, basePath, staticRouter, pagesDirectory, layoutsDirectory, activeFrontmatter, isolation } = userCfgFile; + + // workspace validation + if (workspace) { +@@ -98,11 +98,11 @@ const readAndMergeConfig = async() => { + reject(`Error: provided optimization "${optimization}" is not supported. Please use one of: ${optimizations.join(', ')}.`); + } + +- if (interpolateFrontmatter) { +- if (typeof interpolateFrontmatter !== 'boolean') { +- reject('Error: greenwood.config.js interpolateFrontmatter must be a boolean'); ++ if (activeFrontmatter) { ++ if (typeof activeFrontmatter !== 'boolean') { ++ reject('Error: greenwood.config.js activeFrontmatter must be a boolean'); + } +- customConfig.interpolateFrontmatter = interpolateFrontmatter; ++ customConfig.activeFrontmatter = activeFrontmatter; + } + + if (plugins && plugins.length > 0) { +diff --git a/node_modules/@greenwood/cli/src/lifecycles/graph.js b/node_modules/@greenwood/cli/src/lifecycles/graph.js +index a1d16e5..1a8c659 100644 +--- a/node_modules/@greenwood/cli/src/lifecycles/graph.js ++++ b/node_modules/@greenwood/cli/src/lifecycles/graph.js +@@ -5,6 +5,25 @@ import { checkResourceExists, requestAsObject } from '../lib/resource-utils.js'; + import toc from 'markdown-toc'; + import { Worker } from 'worker_threads'; + ++function getLabelFromRoute(_route) { ++ let route = _route; ++ ++ if (route === '/index/') { ++ return 'Home'; ++ } else if (route.endsWith('/index/')) { ++ route = route.replace('index/', ''); ++ } ++ ++ return route ++ .split('/') ++ .filter(part => part !== '') ++ .pop() ++ .split('-') ++ .map((routePart) => { ++ return `${routePart.charAt(0).toUpperCase()}${routePart.substring(1)}`; ++ }) ++ .join(' '); ++} + const generateGraph = async (compilation) => { + + return new Promise(async (resolve, reject) => { +@@ -12,6 +31,7 @@ const generateGraph = async (compilation) => { + const { context, config } = compilation; + const { basePath } = config; + const { pagesDir, projectDirectory, userWorkspace } = context; ++ const collections = {}; + const customPageFormatPlugins = config.plugins + .filter(plugin => plugin.type === 'resource' && !plugin.isGreenwoodDefaultPlugin) + .map(plugin => plugin.provider(compilation)); +@@ -22,8 +42,8 @@ const generateGraph = async (compilation) => { + filename: 'index.html', + path: '/', + route: `${basePath}/`, +- id: 'index', +- label: 'Index', ++ label: 'Home', ++ title: null, + data: {}, + imports: [], + resources: [], +@@ -91,9 +111,10 @@ const generateGraph = async (compilation) => { + }); + } else if (isPage) { + let route = relativePagePath.replace(extension, ''); +- let id = filename.split('/')[filename.split('/').length - 1].replace(extension, ''); ++ let root = filename.split('/')[filename.split('/').length - 1].replace(extension, ''); + let layout = extension === '.html' ? null : 'page'; + let title = null; ++ let label = getLabelFromRoute(`${route}/`); + let imports = []; + let customData = {}; + let filePath; +@@ -111,7 +132,7 @@ const generateGraph = async (compilation) => { + */ + if (relativePagePath.lastIndexOf('/') > 0) { + // https://github.com/ProjectEvergreen/greenwood/issues/455 +- route = id === 'index' || route.replace('/index', '') === `/${id}` ++ route = root === 'index' || route.replace('/index', '') === `/${root}` + ? route.replace('index', '') + : `${route}/`; + } else { +@@ -126,7 +147,7 @@ const generateGraph = async (compilation) => { + + layout = attributes.layout || layout; + title = attributes.title || title; +- id = attributes.label || id; ++ label = attributes.label || label; + imports = attributes.imports || []; + filePath = `${relativeWorkspacePath}${filename}`; + +@@ -207,11 +228,8 @@ const generateGraph = async (compilation) => { + page: JSON.stringify({ + servePage: isCustom, + route, +- id, +- label: id.split('-') +- .map((idPart) => { +- return `${idPart.charAt(0).toUpperCase()}${idPart.substring(1)}`; +- }).join(' ') ++ root, ++ label + }), + request + }); +@@ -222,6 +240,7 @@ const generateGraph = async (compilation) => { + title = ssrFrontmatter.title || title; + imports = ssrFrontmatter.imports || imports; + customData = ssrFrontmatter.data || customData; ++ label = ssrFrontmatter.label || label; + + /* Menu Query + * Custom front matter - Variable Definitions +@@ -241,31 +260,26 @@ const generateGraph = async (compilation) => { + *---------------------- + * data: custom page frontmatter + * filename: base filename of the page +- * id: filename without the extension + * relativeWorkspacePagePath: the file path relative to the user's workspace directory +- * label: "pretty" text representation of the filename ++ * label: by default is just a copy of title, otherwise can be overridden by the user + * imports: per page JS or CSS file imports to be included in HTML output from frontmatter + * resources: sum of all resources for the entire page + * outputPath: the filename to write to when generating static HTML + * path: path to the file relative to the workspace + * route: URL route for a given page on outputFilePath + * layout: page layout to use as a base for a generated component +- * title: a default value that can be used for ++ * title: A way to customize the tag of the page, otherwise defaults tot the value of label + * isSSR: if this is a server side route + * prerender: if this should be statically exported + * isolation: if this should be run in isolated mode + * hydration: if this page needs hydration support + * servePage: signal that this is a custom page file type (static | dynamic) + */ +- pages.push({ ++ const page = { + data: customData || {}, + filename, +- id, + relativeWorkspacePagePath: relativePagePath, +- label: id.split('-') +- .map((idPart) => { +- return `${idPart.charAt(0).toUpperCase()}${idPart.substring(1)}`; +- }).join(' '), ++ label, + imports, + resources: [], + outputPath: route === '/404/' +@@ -280,7 +294,21 @@ const generateGraph = async (compilation) => { + isolation, + hydration, + servePage: isCustom +- }); ++ }; ++ ++ pages.push(page); ++ ++ const pageCollection = customData.collection; ++ ++ if (pageCollection) { ++ if (!collections[pageCollection]) { ++ collections[pageCollection] = []; ++ } ++ ++ collections[pageCollection].push(page); ++ } ++ ++ compilation.collections = collections; + } else { + console.debug(`Unhandled extension (${extension}) for route => ${route}`); + } +@@ -323,8 +351,8 @@ const generateGraph = async (compilation) => { + filename: '404.html', + route: `${basePath}/404/`, + path: '404.html', +- id: '404', +- label: 'Not Found' ++ label: 'Not Found', ++ title: null + } + ]; + } +diff --git a/node_modules/@greenwood/cli/src/plugins/resource/plugin-content-as-data.js b/node_modules/@greenwood/cli/src/plugins/resource/plugin-content-as-data.js +new file mode 100644 +index 0000000..5d299de +--- /dev/null ++++ b/node_modules/@greenwood/cli/src/plugins/resource/plugin-content-as-data.js +@@ -0,0 +1,54 @@ ++import { mergeImportMap } from '../../lib/walker-package-ranger.js'; ++import { ResourceInterface } from '../../lib/resource-interface.js'; ++ ++const importMap = { ++ '@greenwood/cli/src/data/queries.js': '/node_modules/@greenwood/cli/src/data/queries.js' ++}; ++ ++class ContentAsDataResource extends ResourceInterface { ++ constructor(compilation, options = {}) { ++ super(compilation, options); ++ ++ this.contentType = ['text/html']; ++ } ++ ++ async shouldIntercept(url, request, response) { ++ return response.headers.get('Content-Type')?.indexOf(this.contentType[0]) >= 0; ++ } ++ ++ async intercept(url, request, response) { ++ const body = await response.text(); ++ const newBody = mergeImportMap(body, importMap); ++ ++ // TODO how come we need to forward headers, shouldn't mergeResponse do that for us? ++ return new Response(newBody, { ++ headers: response.headers ++ }); ++ } ++ ++ // TODO graphql based hydration? ++ // async shouldOptimize(url, response) { ++ // return response.headers.get('Content-Type').indexOf(this.contentType[1]) >= 0; ++ // } ++ ++ // async optimize(url, response) { ++ // let body = await response.text(); ++ ++ // body = body.replace('', ` ++ // ++ // ++ // `); ++ ++ // return new Response(body); ++ // } ++} ++ ++const greenwoodPluginContentAsData = { ++ type: 'resource', ++ name: 'plugin-content-as-data:resource', ++ provider: (compilation) => new ContentAsDataResource(compilation) ++}; ++ ++export { greenwoodPluginContentAsData }; +\ No newline at end of file diff --git a/node_modules/@greenwood/cli/src/plugins/resource/plugin-standard-css.js b/node_modules/@greenwood/cli/src/plugins/resource/plugin-standard-css.js index d8409ea..27bfbce 100644 --- a/node_modules/@greenwood/cli/src/plugins/resource/plugin-standard-css.js @@ -33,3 +499,166 @@ index d8409ea..27bfbce 100644 } async intercept(url, request, response) { +diff --git a/node_modules/@greenwood/cli/src/plugins/resource/plugin-standard-html.js b/node_modules/@greenwood/cli/src/plugins/resource/plugin-standard-html.js +index 06223cf..828e3a9 100644 +--- a/node_modules/@greenwood/cli/src/plugins/resource/plugin-standard-html.js ++++ b/node_modules/@greenwood/cli/src/plugins/resource/plugin-standard-html.js +@@ -5,7 +5,6 @@ + * This is a Greenwood default plugin. + * + */ +-import frontmatter from 'front-matter'; + import fs from 'fs/promises'; + import rehypeStringify from 'rehype-stringify'; + import rehypeRaw from 'rehype-raw'; +@@ -38,7 +37,7 @@ class StandardHtmlResource extends ResourceInterface { + async serve(url, request) { + const { config, context } = this.compilation; + const { pagesDir, userWorkspace } = context; +- const { interpolateFrontmatter } = config; ++ const { activeFrontmatter } = config; + const { pathname } = url; + const isSpaRoute = this.compilation.graph.find(node => node.isSPA); + const matchingRoute = this.compilation.graph.find((node) => node.route === pathname) || {}; +@@ -46,9 +45,7 @@ class StandardHtmlResource extends ResourceInterface { + const isMarkdownContent = (matchingRoute?.filename || '').split('.').pop() === 'md'; + + let body = ''; +- let title = matchingRoute.title || null; + let layout = matchingRoute.layout || null; +- let frontMatter = matchingRoute.data || {}; + let customImports = matchingRoute.imports || []; + let ssrBody; + let ssrLayout; +@@ -74,7 +71,6 @@ class StandardHtmlResource extends ResourceInterface { + } + + const settings = config.markdown.settings || {}; +- const fm = frontmatter(markdownContents); + + processedMarkdown = await unified() + .use(remarkParse, settings) // parse markdown into AST +@@ -85,23 +81,6 @@ class StandardHtmlResource extends ResourceInterface { + .use(rehypePlugins) // apply userland rehype plugins + .use(rehypeStringify) // convert AST to HTML string + .process(markdownContents); +- +- // configure via frontmatter +- if (fm.attributes) { +- frontMatter = fm.attributes; +- +- if (frontMatter.title) { +- title = frontMatter.title; +- } +- +- if (frontMatter.layout) { +- layout = frontMatter.layout; +- } +- +- if (frontMatter.imports) { +- customImports = frontMatter.imports; +- } +- } + } + + if (matchingRoute.isSSR) { +@@ -144,7 +123,7 @@ class StandardHtmlResource extends ResourceInterface { + body = ssrLayout ? ssrLayout : await getPageLayout(filePath, this.compilation, layout); + } + +- body = await getAppLayout(body, this.compilation, customImports, title); ++ body = await getAppLayout(body, this.compilation, customImports, matchingRoute); + body = await getUserScripts(body, this.compilation); + + if (processedMarkdown) { +@@ -171,15 +150,32 @@ class StandardHtmlResource extends ResourceInterface { + body = body.replace(/\(.*)<\/content-outlet>/s, `${ssrBody.replace(/\$/g, '$$$')}`); + } + +- if (interpolateFrontmatter) { +- for (const fm in frontMatter) { ++ if (activeFrontmatter) { ++ for (const fm in matchingRoute.data) { + const interpolatedFrontmatter = '\\$\\{globalThis.page.' + fm + '\\}'; ++ const needle = typeof matchingRoute.data[fm] === 'string' ? matchingRoute.data[fm] : JSON.stringify(matchingRoute.data[fm]).replace(/"/g, '"'); ++ ++ body = body.replace(new RegExp(interpolatedFrontmatter, 'g'), needle); ++ } ++ ++ // TODO ++ // const activeFrontmatterForwardKeys = ['route', 'label']; ++ ++ // for (const key of activeFrontmatterForwardKeys) { ++ // console.log({ key }) ++ // const interpolatedFrontmatter = '\\$\\{globalThis.page.' + key + '\\}'; ++ ++ // body = body.replace(new RegExp(interpolatedFrontmatter, 'g'), matchingRoute[key]); ++ // } ++ ++ for (const collection in this.compilation.collections) { ++ const interpolatedFrontmatter = '\\$\\{globalThis.collection.' + collection + '\\}'; + +- body = body.replace(new RegExp(interpolatedFrontmatter, 'g'), frontMatter[fm]); ++ body = body.replace(new RegExp(interpolatedFrontmatter, 'g'), JSON.stringify(this.compilation.collections[collection]).replace(/"/g, '"')); + } + } + +- // clean up placeholder content-outlet ++ // clean up any empty placeholder content-outlet + if (body.indexOf('') > 0) { + body = body.replace('', ''); + } +diff --git a/node_modules/@greenwood/cli/src/plugins/server/plugin-content.js b/node_modules/@greenwood/cli/src/plugins/server/plugin-content.js +new file mode 100644 +index 0000000..5023e6a +--- /dev/null ++++ b/node_modules/@greenwood/cli/src/plugins/server/plugin-content.js +@@ -0,0 +1,47 @@ ++import Koa from 'koa'; ++import { ServerInterface } from '../../lib/server-interface.js'; ++import { Readable } from 'stream'; ++ ++class ContentServer extends ServerInterface { ++ constructor(compilation, options = {}) { ++ super(compilation, options); ++ } ++ ++ async start() { ++ const app = new Koa(); ++ ++ app.use(async (ctx, next) => { ++ try { ++ if (ctx.request.path.startsWith('/graph.json')) { ++ const { graph } = this.compilation; ++ ++ ctx.body = Readable.from(JSON.stringify(graph)); ++ ctx.status = 200; ++ ctx.message = 'OK'; ++ ++ ctx.set('Content-Type', 'application/json'); ++ ctx.set('Access-Control-Allow-Origin', '*'); ++ } ++ } catch (e) { ++ ctx.status = 500; ++ console.error(e); ++ } ++ ++ await next(); ++ }); ++ ++ // TODO use dev server +1 ++ await app.listen('1985', () => { ++ console.log('Started content server at => http://localhost:1985'); ++ }); ++ } ++} ++ ++// TODO remove graph.json resolution from regular dev server? ++const greenwoodPluginContentServer = { ++ type: 'server', ++ name: 'plugin-content-server', ++ provider: (compilation) => new ContentServer(compilation) ++}; ++ ++export { greenwoodPluginContentServer }; +\ No newline at end of file diff --git a/src/assets/blog/evergreen.svg b/src/assets/blog/evergreen.svg new file mode 100644 index 00000000..75abb3ff --- /dev/null +++ b/src/assets/blog/evergreen.svg @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/src/assets/blog/greenwood-logo-1000w.webp b/src/assets/blog/greenwood-logo-1000w.webp new file mode 100644 index 0000000000000000000000000000000000000000..5a6aa049bbd17ea2137640405a280dece3d9c5e0 GIT binary patch literal 20344 zcmaI71B@>|6fQWnZQHhOn|Ey6`i<@1*fV!*>yB;PHt*cw{`b92_T}x%CT;SyIc-kb zp7c9CJ!-O2QqIaCAli~*Dq1Q$I&dH$ASnOUGjI?vP!LfC6`4A25D-uTNQW&M*r3gi zI}nLsh;K^+LPzgTPrHS>AYc5t!940Z&Gk!Q9ov@vkpeJ`dtSA5rYx1#gZ_a}LX%DUltf+N z`K~vrHx61aH$0Fj^uQ4*qzopK*DCh;GVcD~f7!GWPv?lV%~e{VNI-YBaGV>P+pfxC zm!JGBOfs(>O;OJ%m&F)yJRjUm%q-^q=SRH5IX9KIdR)K1}XwX|7STeII8-Jr&SN|~aT!cOK;=lEJV(r(GY zHoa2ONu+7TU~FoBRi3lsRyVzx_l_g0ilw*>3|&CJUe+@w1jYL3v3_gk~7_$Byw~NCg*h7%iA6yI)XP(^CIXL zMM7`iuVl1vM-CaB{5g%OJGvFQI(PG>CP5fsf9KQYo;RuBxcl}fZN32U?V+`4S-*cC zGQ4TmBmtiIS-6!9DH`-2(RoTR_~e+S=w(+|&8HmIquGWnE+Pv-ToAR~56Es2pab)q@Uz!@5uP#D>`58D7=1M+1C=M;tpGv@Kw1lL ztqYX;REx!C><&n|C2J+ul)5z2nvl;Jfa!JI%8W_l?ZFo5oRkGQwE;q+U)NOBG+hf< za!Z+mMSjZ>aZvV@o)v;+vAtVB#c6a9>D3U)CSflE5+o(7h$13zWuw|M@>Dg|*(i~L zXpR)+|Kv$As?i)x-(i)GK(9Ub1BcUjZGWWO09m628Tgn3Fs^Ss{M>%s)GO>7-f(oL zLy%yp$_<7rod6>G0{NMf2|nu#o3M7d7_g z!R-M*M&WIF)S%LqLVBi4tFV&)@q<>Zz)fe53qOT0F z$s)2nCR|+uHn_0I=LwOE)3KYWfp58tpz(gZ`Rji{QF|^v)z~^K>hH1#;C9ZJd}w3? zu2H5-uO43f&(BXx1kU0=@P7GC#x!)E1os9_*E>Q{BilyH!M`^9Uy3U7f)4)RzZ*L5 z+C;Wg+1b+mE@ewTdBkWH7jqtLG2%!RfL`smKJ@-WZmtuE5GqoM#^!+~LX19G#j9{T zHlI{Kq8la*Y?-vZY<(d~dW4+UzFCbd@Dc6r3;1}wa+_tnxr8iNFxgO9R3We0JLq7_ z#OZqB``X9CjDQ}0E2#O>&*$_WPSbqFGIKc@_JYpaHGs#A{Go2;Wt7IHA;Z=wdA~{E zJ=^=zoOrzpm~Fhl2d2?s@3o^huGU57E=-ZkTkI%r)cbg(imH{-UBa*A-1^D2%Oifx z3Lnj{w#s>UNmSGJq#Sicuni+TKLxEE5fQbwx^HRNWMiaq3+}(V28yQOk0R<2<8>nIp_Ug!{&SrMi}Hn{FsTgk*G$l#y$tPi9k% zrl-sFUsUM-eKh`u2YNlHkG8_#*#DddpJTlPwl|L@a32jx`8wyyKlEr2jwe3n zlXzWW;SM!jjx!G`W}`kt4cOKeXJGoP3fK->aVAl*fmHcArl!83L9Brt$)Bv8%QsR* z!j$|CyL=Jhw3eQdW;x&Vm)VYTNdx^M+!DLDIjw&8o!D_}Zl9g_>U)NfEOzjmuW>4| zTsfI*2C*a4ylBfxzcs1qQzmm-A#-}na7D&#l%CeB*CED4I%3Q2(i)Vj&yOy+8aZaY z|4O;#Ya&zzopJH+*(Fz`-EX`oe)OqU@Vk^YS(t9eDF;{WL!Ad^)*b+Vjql!BX*=m6 z=^WEZMh%nR&_Asn?=;k5!JxIzO&8wOQY@&Ox65L^cl^0M&8yc7%cJNlO=Sv8LDqk( zt9<(|jpVHKyjXvO=X+Uvm82z>&JR_@*7|IUQZ^STJ@y0J;>*jq+X$ZDxg=$=ci4tN z%U`O?e;=l&307F&Y!^VYJB(fQro|NL`&&0+y5-dHh*we(5Y-IMk>s^SPCW|QE8khnm5eV~4&iJKm zU+bA_Tm0nBliQ_rWXxTkd)_waojGMh>sO`RRcVL&T*AaQ4s6%W_D4 z|LF3v_8^aCouO~EOxoR!qk%FB+_aeo8j})e7Etw2VPx^^2aa;uZ>W$|VIvlCx`xv# zYiK}>Q=gpY-e38^v8=Mn_p7&Kie@7&?46&`6XrGSJ!U6-hnt`Lz%6Rt{7$)uYT41 zt;@NgCo6U%{sW~?&<>zgEY5`8$H2C;c8pemiuykWx{Ihtb$h1fva*vdmWSVq9!aM1 zYnnQX&R8F(S#(6JdD`V~YjPGr7vp&c*^Y9SnNLYf&27Bw2CASTzXu95SIcjML@8`1 zc50q_T(_4oz}kdY16F?jGLL<2*1F_A3HrO}0Tm*l`#!dD7@~kZZIbGal4_xRasz@O zFwWQ|Qy;!w#6cv*K*krw{4A-DXbR_c%7^lj0;Ulpj!TvK@=kDeTmPNGLr=W&n%RdS zTcB>4w#n`rlShgcP-PsFwKno@+5wARJ^d3NWjs)mfn1{-v&E$Tj9RyolfbcUlw0Pc zB_hffvr}&;;4(J6_cs*P{Ms+vSnU%@tW96@pIW;gBzOe5hO`p7=42b;>ejyEvlR(uC|X`4?+?~bA0(nqf|}8O2Z0;h-h1MI6j&F6Jy!3RP0Bx)15ualP`d=>-IJRSe<_Sl1B^m@Vv7@{k)WL;QsuQW|6n;&l!4}N5pZjd zR^yp<(5QGQ6-QZl^A`u-*YMO#DUGnI*k=_H3ak7V ze$6pd2!mprs`m(4UEP?4<(7s2=&?_Ey}+5erH@@y__=GDxcG<$;}qd#!GUQz=`OO4 zdt3j)#}wDmdQ`0`0ykZ^>fQdaeIq}0tXzyR+-L<(ep^-Ueu+Rle8U*p5OILdciRzQ z`ZwCXk#{~so^_3?1klFQt z^mL|cmi3N?${Pf8iq-A*iTTd}{tj3*-2#UPOwA@!p2aOpX#Kw*t7zY>RA1eg`pIok znD*)>4_3464C{%yMwz&hG()yrOtC*j_cJ|(IpunbBf<#XXYYa7&%NLI?Lw79shvAi z8S`1Ra$S}K*SyVVwYxTpEEcS8BTOi={yOQ(MvFHs9wP>2TWEr|sappI8Z`(Ow>XYo zjXx>Gkvo0?d+_f&&U^O-na2@4Js*xy8*TW}l?87gEb*9n=mU-ZsP3(FxraC~I#9yy z)XkeSIw9$2X>wgmV4m|4-x>J*O91o*H%r3@;^Z!}P|y|I3%Ne4+I_u#pBkhoY9~!Ic=Ynt8~XU?NUap}RH^l^90cTl^{SyqA0@L_^ZR8R zMT9*JCsV)QB?P{Og6l6c((KS>xA5OY{Eb7^MzEyZys{7)b9-j@cYD#tZmo7qZ}9yP za;oQWuc%Evsx-hf%X^Lz;E!Ju*Rtf{2Z+Ftvv?Th@<%9b5rWyB^e`6?a@-fYF%x)C zEs>vHvs69hdKFxMBDZAqX%TxGwmh~U{5A5?UAz6N^ExMr>Kl$SG7k2eE^FzmY3_B5(!*FI{#(Iqzf492U zu-mzPmYIpe{BS#c_G1xAE?-WUVdwir6MDIAYW@D4U+PR@P1!k#Jsi%tsEP`>r`UD( zM&*f0|4Mj2K~>vDk99xIg2&o3OyjAEQ$S>^B!FbF@j;wF-S3%zSX9EoQr|$aH)29e zw%Dq&&?*;Scp{~MYh>(Sl)3;*5UI7(Lce@2Xn3K-Z)V{aMIn%7HI#^Sye83U zSX7kaPa|Q_YSadwb+o|tKpO082fS9g*Nh53Tv`G0cv3*2C{hSh);2QF)|$Z9NI7$o zsi&*&$)`gRh%;0}7nMLlnGL#qY;dJ{Cj`c5h-`{jSV-ea5n8L{*A}vsPatZ)JG%se6BCZFUf3f@;Dxm|=l6t~Lc)%-{$^(;;a;uo^pNk`tc zx^v+tH>EsEq*e$Gn$NL)0ol#?oAm%G8}1~+Zwh$!5y%w+AnuSL#|OHBM|nQ^1zUF4 zfuFmk+OQvqr{{1Z!AHNIb5lPSt+$>H4TleD@UIBX;c4#%6cLy`(@Q7p^|%0gt2gKp zfLRB?lZlYsD&O23_P~l|bjSN|&>rin5g5x)Y6CizbAZ2Bp<^Wt?g=ca~NQ-e)?Vb-3<1$-&epR_Qfv zFly-Tn}PKp zg@vxR(z`K?@C(5v1EIj>uP=<|kv}}8Q>ZYn_EswH>m6gV4(a>@t{YrQ7GA_W)5vxV zDq7S5Q*o55>s$wzWk}U)t}QPS0&4snOHpO#5y4sU%CLYlFQfBiiZ^-0lr=582Eu=@ zMTIi&L|-!HrR#HQ=5f}#!@2)mUzWEJbRY)x*Hn=Ce)jSE9E&xTyZY`V&jBSKKvlk( zT4c!N4v0H5F<1dKvAU=LR?-<)FWFTbtHJATTy?qqTO}d%0bGO+<01!|Vw0e0p89>r z&2L`ET}ZLN#qp;qawn^UO`LOi(>@uRD;Q(qBG~p$vsNp+WYhE_VH3I3ogoJ5fO&6l z%bHVL3b94f=MR8HqL}%Sd;;#?niN0dk${%hjJ(ijD5lMJgo8_xCNs` zW#4XA@-pijBLmwX8NI|Y0b=WRy3e?$$IppW*jAI0u#nsg3lnB zC5d%KZnjk51wuy3PL%WO6*-iEo1=vkz?BYU_hdh}>G6ut-?TP`+tEDxbtx2**eZAg zAw90>M?Zg;j@|=f3Qc~Sob*~i1cS85I6V}XUxwj|A~NK04BYx7=-!(UtvQc;_h-0n z&tx_q9c4>%&Sk3(B_(D@@jW2s$_w308G+L>MU8qp&5QK9WSL5QA>EWcJq(7(WVmsI zwR##u-IBHV1g5MFM^#yK^dB`S5dtaeVuq0f8J1uKcStiQ>j>ahM6mzFn@576Z@DxA zP{Gq*Wf_ZMEVC!;Qk+WYSaK7@GaM=<0LeZ}LNkkb;e5bCm4chJiVj+-&)yA9@rE#5 z{8dXE2!UFulZpsTZ4nSl6J+Ezm5T1|Lxo_P!L`Lp)c9P*oQy*o4-1cVH^cC<4*R`U?fs>KG=xB7IZjDcBhHWfpK(rIabZ zR1W968Et|xKKvzR@(!&~9&>)K8z0b|Bwas^1>aEB*$5N-q!nx>v7!q9&`lStu^c1}9r4~{MQsS5 z0om2EV?A=sI}Es>(E*#9QCyyG5$lv6%OqVTa3pe>0coT+ru~XT1K+yPFp@3;mSkpD zU{PmgmO<*MJXo4A!e(+1&gb#8!h}fOS+^W&B*A!9c~Z%b;GMRae%?n2na-oXhVKO! zcOnc)QZ(;q9)-v6IikFo`El=%;hOqX99S(07>=_u8KVxwn!r9an${~&B$M%!cbaB{ zNNf_ONIeO0GY@_)MfStK&~P_Q&lNX+%#fphu@8z-iw`f~1u9%062d2&c^49!6!uju z{e}jS?nWLMzIY&uh-IP7`G_aQ;PQSErb$$2#FuDsMwCHTd@$ARho(7?RR1f2bK$j- z?*)!Age#zMn1#{bgd@0EZ!GINoYJX05qS$zI`!BOkQb7y|*@kRLbepg)t8yG` zTt*S#>v0&Cj)ru47$Wek7jDDBWQNX@3KtU55+-hvfy)kCkoY#+iri{ujyv~As6@LVQ7fR`1>(98^WK7|tDbsxYQsqi_E`sF?3`tZ z^OqS{d=c0F-W|{eV@#q(1?YApUEQo~xy0hXV7_U~nKXq73n~xr;uLf`ts)s5;!=8M z)8C%AgV|urQ2ux;Tu9ZcSD4T>Wi$5EM-1QOin;Yec=_2UD;I7bTWC*?AL}SR7(|`B zpSd4mKUAN3x{IlAk&KVzQB?4W(Im*jntNkmhb(X^oaT6Bk;?d{hByLKzSa}~^Qx%P zzCM?HkAnmiI#8TW#eSZ+xSls_1FjIRzWy4cKVV`NxaqLFx72Zx&J?E$e!sD8`_~QRT>6MA$-QHmeiC*B3z^H=2upIw z^M4m-$S5S6trCfC0rA#YrQ0UM2qaG7XC_QxI)$xn>! zgP35aNfA9?3BZyIoORtG1Bj(ybDXIoM%DKFSky_n0M(<9+Ms$4L%FV)kv87631MQ_ z2euX;9MzqyS+SWk*Y-z*utvrEDBKcwbF2ahuq)t0^oa!A&c>F|C{>nSc38EIK)B-* z6!}U?WCW8IEZ(b0AK!S1h7wMI#$T4=d%P@r)40+cM1GV9Shsswh@k+|h*=H7{?RSA zk0+KRYG1embcITvuKYSe+b=C%a6WA23}W}8$%}s({w0IP8|iCHSQj9a?Am7%)!VpA z7Mj3n*&v7iJQS#)q=UF|+esKBN|zEkG+}OBWH$jKGEZOs;bnmZhpf*5y&$zo%I$;| z7^A)c`1WmMO(5OA4P*qQ) z<$JCwm7M~WV;9Wl0-07ZJoAV~QU^Y*fwRQQs3I$6o^Ol?r#?hQcYuZ~&*@sUJ_uH1 zV*TWOH?9NI4m9*x4dV93YhEoj2Hb%I^VX--_qQ&~Us7H;TL>VjG?h&W-*j)?=TNXA zV=@L8$dO9~+Cwx4|G=_i{lmQM_lDQfO9=TlH+?pMx$epT)EMhtNZ+UVx5cFss&&j9 z#jP!n#dwKq*|kAtxXhGhsw{+RsB63qkNtSFz~_A@!u^0da+vZoH4X?N%m$&ALm!hc zyA(O}XYXugFb-Zx+(IQlhxQ=X>2g- zhJ;acj7Gy5{>cl2U=B+A^R#qSTSJLMWJf`%tPkK;G*H#Sm|tmZ+HvZL#MJ*)fu$Yy zajpx+IRY=KV+`Nawap^J@Y=dBY|tL$KHN;^b1Y^aM^o7|?Om*Og@dUm>gMRR5A~0e zMKp#oUC=N`Fld^{oQY`x&Jvi|YIEgK9S#`HGUySW!o&lxpE+0OTgKWQGK4#-*F^3fE=m_Q z>hi+#!gcatjt`|_bW}9}_BN+TzA*^52s%U#uB4X~(3@@rV;U*_M+0qEI{3i8rs0mb zWb$Q7nBvc`h9xjn&}F@7Ue59i_O#WDO5sqm=s5&TXB}f1gF8e>9I{wdqJDxn-d2b2+0LU~k~YqmZD9K#kXAvNr1X}UrEK@m zEq}SPhHsk|uzdJT)Gu&`E(KU3vdy(|rcHUG`+6h~U15lKkz(lo{H4C1jIg=Pyz29KTOEm0%@ zHEZq-C-GY9j)5hO@-}E=zJb(CMn?T`=9`hgysRsJpj<)kkU)N><*h?Rf9UyNxhg@!O%qKQpD=Owa{o+$&O z-fsRO_D%IhGo}BIrTxm-%Ea{Ak}}C3%`!mZnfUQ1j~Jt+h@CpnYw<5`U84=YoccZ1 zj0m5Z7UGDGwR_6@`q^imNYJSR$*H}^c_$K)DvSLuMZ{?dYMP->0tfET8fR7vi4Q-F z15AbLZ}*+yrYk%mBy=Nkwq}o*FaPi<7$nk3@*x*a<{8Tu#eb6^+-hJyD=@_k2z9}Y zcf_`PDB%qVOx-$y;pt)#d0ZsHzxXWUrt@>RF^+#>Jh}jq*Q2*uo#qNrWw(*|MM=?$ zIU|DoBSn$e@58lapBu>{a%3ta-o&v7m1qMm9fMe>5iLUUt60hwOF%_@TEx^pF~Mje zr|2R4!{vlT74AJ}BC#|j||iVWjf(}T%<^IA1@))&FVlp!vF$stvE5J+%G+^Ll&GQ`)zyd0?F`Z>4fjCc@^i|nCh7|_pAtm03o7;`qRk*4FQEs z-4}S=na`2F(HE(EQhK6YDb&=Yp}rpS;L#k`iYLwVDM>}>X8jYGsBhjr2$b2uI6bcj z?{?|l`cp4V7VMU&eVA!UH7Ke3zK##S1Vkq5#CvB^ThIPy=^;9`QranueEd_3~L!KZnHp@8JMvG)6w=brwZ~F~KA1KqG z;Po_Mc7v`Z4Xrwx)A=5Q1aUd~SA)GFAQ~+y_7<2DU8%d|=Bn^-mtdfBk`&hM1Gfun zG3VE0B}4Cj{8@tUyS0zt2U_6*0xNELOer?+ZFAQq0v(~OzYMq|iXAlx_NChiKXpeW zo^kPw^4&Wakf-;KK$u~hxf|C%|M-meXMmF*x52O;xG|Gg^~s;wjP;&Q~&wBFUl4n2>Gx_Z_ciQ zZ|2B&VPcV@G%Vq|t%1zaq6kv4xZ24Wt*C^BG>qZ0+VLnX!4#p-Qp9z-3MV$Y zY4nXQ7wJr5NL=8AaeA8OTHUWvMur%v*s*}G$}C(j9AZCLX9qH{$4X%P@B(Ci9E3)h z8&UXWLqT-%H*7uyG;(tdu!U5L3(xAK{1g{0w1RIH9Q-$kK;-w-kPA^lBTd^~D^$9m zQR$G5{8h}q?Y`P3b_(f3)F9r+>(5V(>Su(+e$8>qpb6&$RY%lZG5=ElN=K`Gx??2nRzvwjn{ALe*01=*S{(7g;SCTeYOCKq+*$i26j#o{P-l4Lbl% z%Y+838p=i<^*k?zFQaF%XG2k%Aw2V#&9YIZa?&SMb%Ff=?OI?u0wi81VO2ve#*1H$O*k1}J_G;{=(1 zRBl;3%iN9Wcu33peI^1D4hdB4w3PELsDxWtE^Sca^#Ehrm#y^ zY)^1)vmt4SSnQa`JbgLnPi2)0$9X zIYC1NL#a0=hCoN)`FRLdT2Z`kfpT>ZkLu!?I(#T!8mZv_esJiYd zU-E>MiXhmc04Z%Wc=}^Ft*v8isK0!T8k|E^l>7Y@yn4ZQE$h#P3g15RpD&Kw4y0{=AC&=FaS$2} zBr6)#(HBU6-%G%BuN&`EdShy=N0y&@C;74eVx}Z)Oi_!S@f79a4EYn_u1|StKF~*n zIS>W|#jXck$?Fy0ZXhYvbsK}F_Maj`Te!#Fh^ASFXeN#gN-=y3IuQnyq5eTY=XpibYw$FGdWM<&(ML zv~FY}TQYp6adVu&@j8F|>1T#TNPYxB9F*5Q{|nROT1xb0>FyROp!B>dP9Sd*7p@b;+6XhTn=*;EWA@xK^kTRISMK0TcXC~uM<2}O zovopzXX+u$R+3-(=dwx!`hLLRGilIVO<8;@51wp!`)SxmX9aK=Gi%;3DsNThYSF%t z5hy&^Rv1~4v7BimOS!k>GAUx{Zf79QyN$g?@0Wr!vbnf~ zS|N`(onu7^du?^fc}Jt8x5i+3?A0Y54>Pt$+f4Jn?(=fI^Kaa9>0NbyuI%9jW()oBc-XHB-nZ;R6jTB3Vsd^ zH89`Q#T7LT-rdCru{DoOo=ZYWHtP?(xN~B&e4@rW()W%!gPrn*2@|)^V|b9x8I`ci zS@-``7lAX19jTb-nqd&0`6lJ)G25Cg@%+cPV?FJY&vK)D_s!a~5L^!dvt?EfheUvj z-^^*=oszS{m!ii&zs@Lx(f#V1I5+f04S3GfDx)@)jjm43A5uUdIVUs~^?3jqv8%&+ zfJF_qhzZt>{$_-_vQ>s%VJ{KV2dHinQ>C*SN5qHE=Z3GHqNSoqQpLg%?va3U=dQlm zb?I(n6z>@3uC>|u;B#2vo!o3EZfqPl5d@5s`e0FSs;;p-2;)C>4x8ztQN_+4TAUz^ z^n5!;1oG5cPCtKXLfN_~GY~NBW!>_QX_w&VFLhH!>>_;v{5A)-F;fa4?v`B_E|`eZ z+wz5Z@!=DuAY&SiXg49Saklw&i8oyBavfMgDfkb4Rd$JOnlSPe0`v|ePOcZT-8#m0 z6tGNs_3G_S=vq}@+!-x(5K$4>MBCSscmNCV%{F|2njTh@wXym|$P^)1rR0=vF+>y0 z+&C^kb25tj(Q*K@+4D4y8FxgE_22-r9igaUIw`QW|4FUnIG)rz|4`*bX9F!WA!Gu) z>+lkB;hJZ1$I$NE5B+Cg#*?G^l6}(lYy4nEhP0Z%ea?NiQ}T&`*?EN>kp0AZ@L;vX zZ-}>~*4FhFOLo^2FBy_*0J!HH!$b%V>gr)+H2-3cs5>A^cE_|j*9mrJ@S|%)j;xo9 zLTkRHh-{A0QVO~{t&41PRO^mO zDO+Mfq^Gl8c4*o%>Am$1>N_~T`+@Ad4J#d?3lHhX1)uUTK_!K%YcJ7}_dJ{JdSzDxb%Kh@^fBrr2 zyI-7)|BFk^oxufG99w9~jL_B_1 z2JYa2LU-I~?#vPSiN9J3p#?N4%06ySceBzz+!F>d>h_q=AWg{@`V>f0EXSDyow@*@ zC^+MLT{aF!=(*1*7ve3V$doE63yonksaOn285ew}szzcn>y+gbr|4VAe*WNq)11zn z^+4M6-uo@p+olZ5=~GH(vl|Ws*q|Ia+rnzMbI7`OW7uS{Bkt{d{|uq9KAV?x!SuDm2agu^^)INyg& zt5cAcNdSOpm+a7*IBZ{Jm(++La0U)jc;S9qF*IbKF*Q1!!J>3uc@BCq6btVx(tT;L z(o>l?zGMw`Eb#dHC?ig_+tcJ>+VxhmQz!yc=t3W z1(7s=B0F%a1uAXm6c-!)s%cL9`UuJhP#2z=PRR{?TP$yc#T6j?Cp;$lHIR!%RzYWt zi)qQHrm}9|=Mbh_E;GC`v@(T>>LKVa5a`ig+`} zw%m0b@9vUs#oabReU-h2KRzM77BXU&+_`w#OcfKyxgJ|nPPadm2wrl<^W2f09lci^ z-F$zYR4>cOu2q{DFY1@!1v&>MpAfU*cn904@ndpBHb1|5X-&^R`8Po^y+LsT{U>Eg zYMQoN8Q5R`N)gG? zIM)ll+)ee*@N1X^l-Gs#ZoxB6ENsyOs9#;6d_b}Q>v1ax7sbJJu`mPZ272INLEEb4 z>sQehd{Hb@oXoh5&F`Jy_R|j|1-{^8_!t)7s<3`^{g+sopLP8rP*(m;Ls3AR+d~-j zIwywTMv#$i;$oJ9^@T8ZNumFJ47F@kdnr9TOIxx2uolvme zxJ%_c1Zr*SI_LYZr)&{<_#I^ho5wRqt_0*H31=p*Kv11WoOo%8OD&vRxeU*=P zIx(#~(#vb>-W^kIq~PUi z6OxpPU4Lsar!ky&|DKfh^u1sh-gyctFz=*N%3dFpz@4Ystk}DczS1pU7kHTo`52Qj z@hyu(&Aue$K5zcVuiI7oeiH4bFCrTl;p>8L@-tzkA4MKI>sFhhjMp#ne2tGv!0Q)k zc(bQ3TTLhyNsCUs2~ogv1OhV^%#SI(^LRlv`{dMhJS94|+Qd3me}zA69dvO|o0IHP zOZcSV6(*ScFE1#Y>+18(DTwT7lkcncJ5NL)Dogh>G&N0Sh)NirDXJ ziuK5HHe1kS+Urgz;L0bG3%rD3Dww^97rcA2Zp>KVJRCU?hu!71Cd*7Z;B{17J9np1 z$)=4!h;m=%(D>fGC%+(vN>j@obGtiF6;A`Tm8U26Uk-Ou8rqxV7(Fc#M#6 zVjiF0Ybonq-^a6};H%TGC{3*E#z|wRJ*G9`hu3y9s_A?|hwC|VBckoIsMC-Om_y&f z86F|LsENM5()I%2ypd_q$wDDTui`%dYd4>&HL}p?-F;r@pCAC-WW9m7oh+V@+~fru z?fd$3!coJvtzb^;+!;G+Zs7G8u^vt`Cl?P*)dQ- z=!#)%UEfr{)n7;OaGr~B^y(~Inm86G-`ybM6|-qhgR6SO{lep0Ibgn(-9M=NzeQtS z?nDI_R%}d>9@AD7G8ij=dHty4ss2^siq+m6x;y@ynHyY9YQ*r_5z2%2Th|$0O!!eb zc3FR}HA(D%(cSb$)$B$5Xee2V0G6GMsjetHT|UdvS{E<8;>VyP_kyo^)jegj0wSYL z_j=r8zhKE7ii1

x-v#e&TPw1AJ0Rh9i!8V&1O~H#1@6^`00D$YcB;dRq+`>pM7I zhQ)+)QFoSlv`>q>dL4wmE1}Wf+59k+A9EuW{uZjq{nWuw`pbq}`#H(U8u)tjXg;19 zT!9z!F!>C?oBl^q{5Efw^CoeU?$X(O4XmCClzfoGI76e`#8d|p4(32Af!b03lu z`!XQ*&&zYG$!X2|NZcw2;8^_KiCb1$h z&`R@77&9msgsfCZz9{s+3|01kIq%1-fCJEDMu}I3vA#vlS;aoslG>~0;rrWVxyd;#P9{2x=^^lG`hI`R6?SF&GCH-qd5k!q* z1&zS!AmMV^Rnd-fyQ&PwsTjryw+Jk20b!>PorW5@72z!j$3Zvk2jePhX{g32aY2=o zeDiS;J$*1`&{IgN4p43cpcj_eR3|k8FiINq(90x&Yoal*joPRVr`BB=rW|#}zfnrS zc!_$nqJUlSk1y;)@VKbG@hUNPO#kT1!aMNwVs0E1n9OrYMyzucSTR%Li%NkXF)Qx7 zTm~`m$nc0{zLE*#3k4}+l+cr`jO7@lMY4=2GOP}#*p*skZi)_|6>?#}T5#~upYTz? zn#*Z3O;z8>oLXDt7G6iG$Md-JC4WvIhEW#)@jDEomOQ9S98)v&@}}(QtBvR78ID-y z%?C14!n-xd%C}(p;z0D=`{(-R%sPD*uiHeSWFUXg`PxLo;y)ck?oq03S>*tENis-n%=_lZ;=ndq%=$rWmySAc1`*y5>`pN0u z_-}ygk5?e#e**nq6F&!cZa+Ui$Q{r>KPt#SU(R0!-$8GjZ=i2lwixjbJdTj4L3M5 zDF1&)f&Iv~Y=N^qw$Dt&1%co}^)lD`PPj`8F@K}V66S1uS=Eqy<+vc zeI0s4$>%xe03O6!n(Mvg%D?p|4bu?apO1CT8y^1)YLisoxJ0M$W=4#Y%1__#iQA|u->V-9Bn)p-2Qp@!}l?5*=YG$=(2W9#ZsIGZ6{0}kU!#p>n z)8u(%++HKYj)&PhAgGhev|DE5nn`&m)p=m4Y~<0i7JHK6@}X-qVI5BBEP?x zt2O(+yG5-rj{OyZjn9r7j>3Wq)_j7z7rCz?0XOU3A|j6mMmP&&$u0}S=#tB z4LHiV?5$L>;)IQ1i9FrRUyrN8-mb%=Al)bh4foPCbjKe8T_2(wt=5>h;6*c$A`g12 zg9OkQ-f+amn`T)5BIv0@0FMBV)<>^GMf)!!Z&JW|mW7lm__rs1!<=48`;{Ii6|a-& z=`N}s)s2eZJ^*e9M@(J*8pR7K3IZ<1(mj05wIw|3YCqisryfp@+{MS+wtfGntFyJ3 zgqzFUj1AT5E#0ru~ooBY{ss?V28fHUMs?lnkHnY}8c7pf? z1u<$USO|y!ladWSvfX8}#g0iA&6x9W<-_->lyCU>8OH>EZ*vU-e}FS!7OK=`bxPxi z?E^NVEJY5RYK$Vt9wA#|u^q|50jpOW`jA^IsX_)Vw&-P8ZvQjUmk2N?SyvwUtMIC8 zn?)S8zUd2t$k93MF%gfm6H&z@ZQqhg|4|0Th>tWccn*iY{vavozKHODHsPIk3KE$S^eX9@G2lu#aptH=9<5mfJzkF);8raUIA z=hfD9>J(fJHiKQ{SqNNa@+CoXo35=_{m|fLc_YgTKJkn4v^UUG4&kI$51%3K7wfg8 z@8{++?v(H%3P@hV5k<3|iWQ^nyaV+yqgN%#_QP@9e{*9MpORBgildgHUuvx z{=)lH%(Z1VQajD*{LlFyUW>cutZ6y2MmM?!9E;}vLm80B>Kyl@?g^}`4^!xp%yQX)kH&(hB{xKC@QHA* zEfDaf$xND;64WRux^wCI|1kvS!hUQMwyHxa`x?8p4~*2en_gJ=rCn9uw{b~fK&on| zN^?W_V8r)1VXy96ZJTAMHX6o%YFWqHpxF<6pSMDowkkoP4$usZm{vdfid;>S78!+4 zpZ;y){i~v9a2qUSYlDxo&OVAB#QY>xxHJ_!aPDrfP?UB##*XL!hbhjXx_YFME%F5Y zca?WErf4muL+BpY!%s>C!gcmh5P~u%-=F`XCwN#e{&MLy>WD`nE#^vw34(*^^#Jtq zUe(%YA);g-FGcPjT)*C|W>1SBLiL|AC|N!V%nc~aRGHSfBu@h?_;2Es?kw6e3{GXO z$OX@FS?|N|uLSSpI^f-WsnL%mi`Zi*CFBh-LnI$(uRup^R=Sl5YSGL=o>pLp#SJub z5pU>UZY*ZE2;QpGK(m#L2sz@Fd}|)g`_~xeVfdyDZ|q0ZJiL%#T}W^+uu#tz$8RS> z8BM__l5e3vruqJ*n!ma^oV4)Egc3dtLDNp#f9YY#~(y3?C(t(0Z}W99jI_CR(7-se{fL*5hdhtPqSM zmb&VLT-jz+myFuVogH!mZW@UsDqP_9xG*qLLv;3tE#VSkSA9VTm4;;}mvpT9 z$kk@3AV_cL&zu|~U1GB-5hKLf89K;>K89mFdD`)6=6xcTYO3o`Dh$2O)w?dwP}=79 zO)Q;cLLWmho;>Y%wPe4HVIn6jqZ}%HN$^#ls#$Zl1yn~1DVC_6X7nToxpN5m-6MM( z3_+F`z`KiWwqzby!^8)Sg-$An9PH+1A=e@Ap42Pq9b40FA!=HLGIg0?Uk3qqt@uzQ zC65i=a`zA3zObS+N22OSSf1V|&L7CI779^DMr&FW(q*&sFn{LZus+=awz;869%YEq zP2AXuH9W}pBCb~rg&aAoXeya73zjI~=-ti~IIl&O> zG1~ukK8NMKB3Rg|P2_{&>=}qe_6V^OC)^)Zv?mY86H1^Ye~!DuT=fl(AAhfZDH;p*QY(>1d`xKs6&Rfq2D3b*7#P;v(htrRt{-N3XmPJfjhN{3K zVsSRfZMo^}Pf4%0&lSkuW>m}v_?_vWS-jwu?Sq{Z4|km9;a&OMCwbT zo$igS|LIof*u9;~6W90#(eD%tAKdzqL*y11wW`Q?xcpX6ijaJIC;Y3Q7^~nL*lyQP zSeg^MQEQ7xzV1A z4@b@cx3hL&K5&yzu|fQ1@SK|5ubY_O%e;^DfD{vwPbi1V`%VI8%*_=v2v>752@R24 zd742frT>w)D)+-$FjUG=1<{jH|_KHyj@(b{1r9 z6cYW5k72*LX&-oDQsRrv7=+NYgZ6gGL9KUZ2};LMn!|-}y#O+2uM<>&&)$f3Cg?N{)%@mFo<|^GnC22nnJC%l=Sf2Mg#IQP#s zxOVd4bqLOo3`wAoI6X@wHQryng4zfB)ehq@+~ArK`l+qA*bKvKkMq?GNRmZP*Qn8~ zg~xkzez}nE`}qPzQ~s$GS_Q988aTO^_D3&1>3y0}@Wl&bLfaALYPacaa|Pm}#GfvG z{k{W6`=x%{ef_!VoQZVkVW7^#mjsJRt4 z;$IjTj<<+4PzP2P7nNl#pS7bmRC$4>9oWAATKvfe^QXl1!Gr>UK zLg$Qkj7t9*I^jr|NgUp=*xF-xLG?-~C{pqZPq~rSg`!39!Z`AZgQF1xyDrz)ae6;_ zu%6#aE@R$%WS;zlQt@{!lh;FhR%X^xoBlieG3r4}Zq<6f1si#@_pq5F(?BJJ6Hn-b z7C>GQ2qz<7&2MTdjH-Ubp!6_H!Hbadg0{EnNYQnX;b153o9k=$o9&!qO#x(+3gE7x z_S>y@a!55fl^jtH+`XUuaaZ`pfv$CCrTy!gz~YOhW@hS4gMU9XEDl37kG>jKR?|-5 zJi6{YG;tRbBeD|!4reSZj>i$sbpc44jt9)`diMlEtC!UN#{U_EaMXT99YP(Y5jM*8 z{%uSQ#$)kKTg<2CI6aCvynPPAlgpErk4gtGp8=QiDArTh*bgZ$`x+&r*+duOr?{`J z+?1Yq%QA)tGOzv@du~@E;P=hej2Y93x!ZZ1Yj>kbdBL`H>)Yf5KE8*5@ZV?`CtorW zqs^Kmp(T1V$7gk|S#Yg2eiVXgqX2{wm>|v`%y7P=cxuQP633M@VO?!`r!fM+M+kdz zqgt=k-J}q0Zk>HTo2J^m4gwV0F}rw^iVK*)_C58|h$FYl*a$I3Y6jCtz3i%M@L;Ur z9oB+V3Hslsdc~ud0HJ_!aW(=4tIQ+Y%_2gf_B=Q{oDp-)C@B|=z|1NeL4ET$CtEc# zxMbXJG<_%lCl}<2s9*##c)n%^ z!-5D@esNM3HQ#?_a|vK5&rwYG{;=AnMz1K(NGih$r?RlUB2^Hyl&QIXiv)t)DDJU5 zxUe5@vLo1p6foxs=4IUMqhrVVq6##Wg!yWJT6%z0qkiQS0Gd!$z#?f~Bz zrEmWp&b?%Q$;6a)R02TsFV<$;BrXCGeGvLOC+e<~P2egnXigxGrU${T5^8a|7bG!p zY5Hb;#9W`9?v8<{np9C6BV3;d7{V3XnSOII{TG--5il8i+e`7O-}9v8?Tc!Vn?(%} z6;2R8$iF<2!A_r6MEw4(Mu^r~*$DH^yA^D+Xli6l&(|4*7?Y79DYcAwCGY=;Fsgj~ zuxn(=HM9+Y=}oEMc0EukncB*N#Yoy)9nxTfHKI#l^e<)6Ijn{^DuEj!&8_p{hcIst z9?~f-i(Ukr0h_^QOeawHP_f0Jwci^TI%v{J4&B_~FF~ujiK{6t)0^%lIY$?B=VWltTqg7gkP*iFK zS^uj_ATz2m=VZz)4hA8&{%>Xmhv7w6f2jHV@?s~=G&%5l2=O6V_3#7Jtqs`P$k}pi zBZV<0MB;EJ;&PXe+dvu;Z&VAVn=bb>0OppB-|zn@Lcr1kIo|pOK=M!)KxAdOrEDZG^239^*iC@jUJ);XduYmw%e`u&D zRiDm0-WWRoSG7aJ`l2xFavcoR)c~fczbqC-l-~`-WQdkFz@G_E_Cjy%I@JyOU=3Hc zN?f{Gz{b7-$ygIcZ{t+AZ&w9~Ay8*l{H(x)5R|NJ zsK|}YZ*Dbxd0eFpiU8o#hv+{OSi-2DFE~Ytza)BubJlirbiqBzMPvSBiE}AV!&YnJ zmRa__9y2M-S48Y7Uw2Q+(-9@RN8_}$7^FEjIq0?tIA8_0QlJ5fp~=lnvi?|L7pYVM zkRb(*hX7a(Kmi#regH2hjeQ*`Ha{JF91Y=y+9gVBF?|t{Ar=oic#qL&ACH2D{YV(0 z?)9jqp&*3Ej~p!VuNgSy(6Gl4w!vGo%ofPi)R}|G4iGun lYQ}?+^^QNzn~s0&XzD*?(@Cm+c8n;9hX4Qo00000001IVVlMyy literal 0 HcmV?d00001 diff --git a/src/assets/blog/greenwood-logo-1500w.webp b/src/assets/blog/greenwood-logo-1500w.webp new file mode 100644 index 0000000000000000000000000000000000000000..2a06ae422fb840b5b12bb70a69a6f1d2b32826fc GIT binary patch literal 31686 zcmV)HK)t_GNk&G#djJ4eMM6+kP&il$0000G0002n1ptEq06|PpNPt!V00I9eBuMc8 zcxyzY?%@yrKh;+H|9j5N+H@mGNsCA$qJoNI2lmT;fem(G_sedv3%jro73?lrkOo0O zx>MrpwPv2zwPx*oo)bIX?`!=L(SKNwZAFc)IxaC8xNi{^0r>=rLjZ#}eE-VLXYN~{ zL1JJc5De1c1A}@CQ4c%TPW#~g^ZGT*2@V&6r@s-v&6t%@q%b*b{_E5G&+gk05<}X1 zR|fT$qr`YnN=9Lr+%;M=`SmLfYnO9rNC=kVo`;wmD*x)n86ONitaT!I@kcz3J+JW( z$|NfPYBD(MroKG5PlH4l1HoCU8UilC2+2dfT^E0Q@6l}%=ND<}j~IwKIdg+uqbN+= z-`zC+g;U$}H1$UToDt7$3^Ixm!P_zMh11&V&sdOx-oW737`6tL^HFx}uoIgmt|JAE zrJFxL1QqJR=0`G-bLX2o`_#}U9BJmC5C^fNECX*QpLY~ zdGd3|8)PEa{hD`9YnUp&=FfmnL8;-vM~SOKtKU7fPGUn!`17NwlgpdBONSp_RmX^@ zhkx0(4SzklPa60?aoD|rytnf#I}@&HlX#jc-Mjbw$+>?U;{CRKd{WI+Z+1)uwf@fg ziHHB`?*}`jcy||YYI5%JhUlx)YNd5E+jICfhR=i4ky>>B9(s3{zMZ4CI19*&3gpId1bpISuFJ=2A6@$8Ys^0c~cZIm}{v>j=0jv3FrD=n4R*UQ{Lhy1=07 zoOtiT8}!1Q3sbKP0*;BNf*fR&XCcjsxAooyz97=8>rK|{y$g9^_USq&W?FTAX=3lf zUYK+e0O#q{KR@gBf?xRlfK=-L;Lszie7!*P>U^+g3Uxuiy@~b0Ur<=_c;ocx&D_ji z`d5p>0^*u0c)Ij|2spxUTfvopFm-_bQi>EWSX?iVg5^JM4-lkBON?Jx$)93yGk4o< z)zYG03gFE6^WrkZ8|S1&zr^9AeCx&Jf4tioK%_v|n{JHPi_Rb=Zn`uDx-rQ29;?OY zOFwr{eQsp%c`l03m%`gvx^p9kHrwO%A~i&aQ~RVkHwJKRa$dAH;)6WBxsd_{Misi;@;=NLr>#9gXANoG313_wYy%_$pg?pzd|1F2_ za!@q)iiQA)^kf+}TXNQm=w2^p(vo`#xF}~{ME8<7em4kGlK4Ca#dWVWhk{^f$n|Dx z;`Jgt;>9s%smj+TE4DxJMEvvVN4%Ab;`CtfG}{=jmymHrs&EY8 zKYE^09|b%ae^Oe;>2*_tPsZ|DdHor7=pAN1RR?CN$LB+g>`_Z>=3|BiS^RB>Fv}V1bnJdDi_azAXNtjHD{qLm5{ip zHSjbYIP4MXJxk`M&w-`sAmGHrdgT$@Dc-RA@d(r|dARAX8dU@5p= zCTf)AP1iz@ZiAj0FOR=jnrEvU0Z+L>K(8?VW{I8+gCOOG08Wa-RGOi1dk5fYw?DU# zP(mx@7U?!{cqiVoWY6T@z*BC!OhH+yff0^QwIQAwEHB|u3J(O4VgrZn+v9JR@Y!d; zQ*01$Y&U%L!!54vWO*qmS$UdGX7@CpQYG#`7%bypQYH!D4UD2#Q(t_z*A~q z&~R0WM=XywL8R2cq34eHw@UnMM+e~PGz4(0o=W}fVF=P{xKM|v)X&y51eQvJfV)dS zFv80rNU3dp1EZ1;iBmbSblMO76o0b}7>@ywPQ$_&Wx(nuo_(2d5eUKKfyD#YgW%;A zyw`y^B%J-fI2x?iUn~7PSNQY8vBMHncjb)}V@aQ=hlH~%63&q^&9_%%c%%a+1Za-R zm0kTckaCR@r`U6QjB;RhCj@@@Fh~pyyB>D-HIF_w@~hFm&YAP)7hjAS{@Cq%0r$d1 z(oQ>gVSC5q?~%fzNpfkcKIuT?E{B{k^r7cReL4E~xpStD`QnS$p1kd%WAGRM4BMc}^Zq6d_);!KGf9E@cFB+jb_%(+S=T*>ao-1N@WrQ0Y` zy9=QFV)3`G^S*razkAnCa^sGgf}oW7A7SPhr97W1HfYs-pTlzvNmWphqE!Z1nXv*=}`!X&pH0<*AkG zfXvZ>C=7E*QVTvG+Pj)VY8ce%-nk2D2)y$6bqP`H+`X}1Q%eeWox3%5GBTb__te_+ zm}_77YU-l375NQ&>&m&~K74S{{w;IU8ikh12ohFOXU`+Ae*T+37Ou;xyp;UAV(!F` z9yq5@n}Vez5zl&^kG}TBZ>KC;UkN2!m(Lmh{(S@YZIw%jWn6*TvH%f&;L_(OyleLx zf8LUaK^QwP!WmeIH@gB$9fVeTLhj_jl9A^#gQZ)ZMprWnYYNv{v? zRzp)7IMhW)lr%f+meKQfm{GER@|zd$RZ}NR3XYN%N8a(p-xX$*teyDUh25(sl5)%< zaspHi?7XQZa93VTE5#MRsiY6fxvu67m{z%9-@AX^njj}iI!Tq^Hzw1-i9l@n?)L5p zCYbL?`0dt%MlMQPK!OdGmyI|K2{L3N5VK!CVV8nA)-ml1T3!8Xn4<$0v`r=hx9Ej~ zVparqF@B1YbnZFQA24XVmZrs>f%|jJI*D>ASmAU5%z?mpkDI<;m0uNU29gxh>^Dwt z<}|6{fD-6B^y}5OD4Fxt+06@!65iv+@79@9k_^hs*G}CvNr^SQ7^6&>UEyZuL|6}c zLw`TVhZ(`qQqKpb| zlxq@t+&vldsG=2J+&Jo-MhOe?9EtRJm)pl#q(ld7^O*A+7X&3jdzHHN7djOCS8?*sd7PLrT)4>E=l-G<%9@K*3B3!y`cGA#4MN-KzQb21 zsFt>Tc7lz$ceRbp$nOOmr_+APzs2ST9n#egx3w=W1`U^^A}QGrZ+t$3pGa;&;!JPm z99nH55f|+V+_G=1cfpS;N8(m>arLYH60X~Gw_Rt@w@xTo_h$b>prrnVKcj6*q?Xnz z`zI)|f^jIz11OBT0e7JnXHG*@38k9QbXe zx!e9)Z=E9d*G74~G%2qRIM{%=b9fu;;u7!zhQ#!VC;W2g9-*Qc$)Ua+v;w#sBMNQb z0Ap5duhl~<5*lyY&=cVtH>kRH-5$H`Huo)cN6CZ>YI;sdheuYrqvW@N)ifm*$|##x zE&xNFwcfthKlHa8t?&+-fhPB^%cpYo9r)7g>&DvVrQbels%FuJ-*~Z7;;SmQdFo$Bl;k8$7GGVKDTNm_P? zAM7&7P*Z#gSJngOO$YHRLZsI20?eMgn?GNXNMkq}lB%-s;wr$okE+=8$!*$n-BPmn zn(DxWcZlt`)e9a-YyKsfSXzc?x2bG^LbVqNyNYq$)`Xw539HU~&_B64=W=1vAu&CE z8EJ=+`pT5!n<=>MrVI$?p^@J7Z;lT5urPMtm}Hw#z5^-ol{lSwBI*I;lX;2%g}@Ji zJ%S;{L|z49?Se*?TQ?=`!?lIXJkh)JNG3qsu)z;XW}oIAC7eUmJGXj3Nv!OD9v_>W z8G3Cm8xYIoc8lN)y8f<%iXK7-uhVx)opY~sFV^$Nf3pC@ojXu^ks_aY2-rGy+th&` zmFa+cKXPAS(P4Agvw~u{N||O%w-~8+$#h0I+KWF7YWz((Xem704zy^FovrP|4^e-y zhhNGV030>VA4K!E~&hdwmPuc?r6C56u50E$+m_@gLRiyR8O9TMdW_7aMgCSD5 zW0>6xOsMZ0ma%$n9y?`@&e)%iXEW~Gg0E1?m$xcDxwzk0C zL%<1?N#d*wFgq#o#&4l7^43;Xaidltf6_m7U-5?$DKUGy@Q4xtjo$T2THCG$sA_=I zP!DCpmwVcB1Oe0=mGC`2f(6j#mt^AC7ytd352l(3`EAM{za1%@-YwqCpQ+yHd^+Hd zxUxe4xV3nnLtI^22bc$xUS>dOgf~Fo13sWc%vPp)0dV{(r>BpU*y6ZOzWk&BICe2A zzbKj1!y`&gU5Qe9Mag$<0q!H?0zEGuRjD5Qe zaf_!AL*EYMI|c&?FEkMz^N^D0a$hLPJntbTTL!{iWZZyJ`G5$21a3z&4&9>5{_zny zWRG~LbZh#%PMk9V{Z?YuFTbt#ijCBd>A-D%mct}N9Bpmn@EV3BdAy2x*a)#i_kme)Z~6z9ReSw1Gal>H(4CRfLrp+ zN0R<1+}zUn7{H?#8L86~?&Zed7e|@uSxogNA5YTsk>HQEuE}H^4qJ^`4=GU*PV3+S zCC6{{GW{bFcaVehbUE=SmcW@7^d^A4v>Wpi8E84pOH++7jx%#Na)7(gN8MCe9M=c} z_tXA6JT{vUM}w`SLS{BfQm`*|?t`GFSuL9q7u9jS%3-gFw7$LxF!Myll`*fr%6ff2 z5N@f2!QN7m#m$F8z}!-Dji;0t-O+fX%)qPWUL=4H8|_IdQC?L%=whF&Ab?JbJuT;1 zBHrO_)M9uErS#ND=Qf4F^|Z%E3|;7evghDlw!|OKI8FcD?KdU=bxg@HOz4rH3uL@qRzTuk#%~|bp}|}`ZcNEA zRF!Gujm8LIuT2CGX;9>OP6_RiRLW9{;tAl)l_?CzNVy;R1U)P?<4D*~ktt zA>Iwbj%>PQDQ2xO>JHp-a(<+tONj6z$6!$PFO(G1I~s(sCcchR`JAl7OB0^knUVNT z$(2r%W+Sxy7TWL=%JSkz_cpYsSfRePk))S7`^taUty?&8(v($eHdi3t3&MvfK2%Y> zS7CZR^%9m^kUr)oV8I%q7MGp9;@_ zzzzdHle~YZYj1QrwfYy$m{GX71#s^kwv{+9W3GwsS-W1TWE5s2t+g_WvSDP)+eVEBN=jcGQu15`i;IqZj3NI3Obg^<= z2w*UV{uKZmz)+0r)pX#>1;0&UE5v(D6IIrnqYNdHs!-V)1%N9f@tqQd*Sn#Fdmt@W z__@45gdYPlv-4BX@c5m0j8#p%|G369r8jhtO%`Bi!#~b{_oe|IY6N+ysMGbtJ4XGJ z6K9hlX6qCP9JuX!OvzA*xNiC3{&Rab%;dG9sx1$_{KctRO-5uoj*jKw%;%no5-sS! zga?jpK#N$FZkK=;q)ptcwx|#*8UWQhqyf(Vv{Z4cVNtA#2Qnc2T&NAwSiwp z^A%&~m`B&t`tk2SamV1nCmwm^*@LgR`-Ly&?ns{9N(-aOX__G=FB(tz%wXNb4~N}; z(U6mmJo2o;SKjl|XS26kq-2nzq}jXnq=m7x9>4X%OHMiBsB;Eib?@*od8Fk8f1@fV zpm13Y6Cc8%<$5zemQd-L|LdFxxNcSvXN)D(J-2xTCNT=eVQ8#y^4FVtR?FA2f&49P zsM+W4$;J*+Vz#$!b4a{_RiunF1}u2`#74Pf@B|sT1dFy8yte^OA50AKNp~myyKS_g z19$XD1~OBr4g?8-)jjEhe-RC>HH%MyutLIH7?C)A&LueXwc-$*zHoB@7hz<-VjK<+ zYQJqE?IWi>e^K|km6Beq$q_fczbp|nW$w8Tcar|O6V2n3pls?37xk)FDJ4}KAA0?W zrM4&;;D8bik6AjiRg<1QzXySmCP&=#;mRZ>_OZC!fHHa#g~1pz-Ow`s?6@FVTR*d& z)_DuFrW&{4-cAYq3fq6MtVL3j*`1yThM`{GdVh7QmcKN+ZM;dQfB+~w{Tz}zha`ogANb)7PhG?GjZ2oWE{gG^3zY9IR8U#LZG zO6Zn7L0Fg5xv`R%m+7T`yQyn}qaS0?6?&u%ScDvAgmN8!4rz>S1`L>yUTx4rDa zu6a@!l~Ku9@}#-OQ6KI!UI=k+UF+$egA=Xl29cXyZ{pB+t)UM5-p`f}Xkpd5 zZ3{+5V!z*pumn8hTX_cp&cev=rqKqpTR0KE1;Q+mVfsi(sO!+E7b}~Qi7=ss*x}jj zXdH!6w$ne&R-04u*A=_uTCZ8cto@^#%_;dDz%C{2x0oI6l=dq_EJ?Zre?P_RY5H5^4B!Y(Qo`A|Wg~orle6#xJ{S z1GPei7RKp|O-elp^`&$NF83ufQg-Ii0b9aq_Z6B59(U{x+yeGis3<|+(j1txG8-_+Et8nEtbrR>xRy48Q^UCnjMu^v1PXGLDOv(D|tJ|Zb z<**PVX-3Iyb}6ayhZ!Xi;;Jh$mMGy`TBxN}t_=!zw5NIy0lg5Ff#aXvBi0LdX~TC6 zv@TZd?>fXWVm2LQzgY%q7vUd)H76&`b_#LRc^0eFP6V*li)g^D!owi2E~$OZNSVzmC<=jFLkjuqfbXj464#p0Px~WBN?0v5ZxUkGE zoMKQcL4@=2bnU9udN>TO=YCktet9E;LI2el8n}>sEqFHpSCPv*0Om*+Z^zKmrOOLJ zxWcQHh=peNgiYVaOGP;MkX#F~=N~Wo*NEH*XI8Z)vj7f>f5C{7#Yfwx!@NSA?TzR%-Ot-b%)YA;w)ka{jW3 zubtBl0tY^Wb2w}pIW8f7W(NZHK~!`by#kyhQZrC(3|(>wwUqp1Zc|=<>IH9FQ z?3i3$mJq~JXChY}k}Kd)RJJZ%R&;Sga{%XIL@ZU!(Vdk@Kf>yOU={#-F`>wGaqkDh z5{XY3QgV$0N)iEm+K3W`2V0^-=uQtwq5I?mGZ6WPH^hXn)%`ho9=%BrxW>UIvpXa21;j?7ki& z@>duz3tD@w!Q`?NR)tAv9tw{DVH0nXgCF8YdfA<<_Lr#KRd~&y_TMOdDer(m<26K| zp=bv!;o_jhVb3fnQX37*JYyiHU}WGNmvwQbi%h`TChjrBAr|7JNgr=S$%Y;-DLFwJ zMd%7Ulms)4LcV-=XOwUbXP0f`01(v6xHLpKvpR66B~hJfFwN4FLkHP+2TCeY=bA@{ zLG=aHKr4l3L&oLv`|w%fXkfXJb&?KjrR%`-YL0c_QUe{h351m^5)p}tms~)=`7{E; z^~CWo7ZD3|xd0T3Mu0GB(w>=fZ0}GPSpa7lxJQZ0>I1V;K%7ysenU6NxYWaAqzNT! z8UeQu@ar%%kk*R!5V(}qY311%;ZTaK1A0#hE6jF>!0B>k8r}7;Ywfml!3-Q5vqH3n z=^0?o>h+KCMz>@Ls4|giWfG4CVgKP_S^-;ul>=qrw|9c7~etQ#1 z8$x{90T^Uvprk}>038f=G3aZC2v@dq>9+wAqoOP`0b`EI zXucYyqdA62nWRwgs4atM(Pu*9$D$&pe5iC z+VocqU?z<=3+V~x?fbYlkjv?wSK+$G7ScbrnNSk#@1Bw&^#7x9TRY=f0FRnbvaPdw zO0Gv)j{W%#fKh6?S01)tA`#-rbpLprYn{`#GC#zSh_NwOZsrmG831?8Mh1>Vq6igQ ztDsZB{)lM8r+Xg~(GcP=5H`;Ii5O*HR095oAw@2mTEQp8hz+!V&2u z2kwKY&LSU9t+Yah!)F)~`zS%!g&=0wQ+O<}+Od<+4@MgE*&k_Shj_Cct~qp8 z)TDsc0UwTd!3^>d>sAY_2z@}JwzI$kZIL)PptklGL;J1Pr11mPB)-rzS%Pq0bEU{MB>UO!2OWcMHrcv8()5` zo?WRj3K&fGvfta#4JWm}MmQ0G`2Y?rH&fmBH6V=cAS#2r?|~<6F?^LKMTyfZaRJ7; z0EzFI;fD*kiXJUnWxXqkxX|+3Dl=%dffD))P01Co0Mrs!=-Q4+FP4 z&=XaRV?4^>uys5L^H)+JD#``EP$FOiof6aL2=ZwL()y1-()!wTTv&b}(h1i}@g5bf z8(+?#=3yMz7$-KEpSBJPa}Ua;EHx=zd6q+jAQd@@?m zb7C-WC~3Kw9=A`-C^_6fTBE#?mVi!K!=uJ;D9a6m_zYz7gF)3fG~p;*y^CAL6>uC; z<`I5vRp~6`UQi^$Dh%G8kwMj&C~Y;a*tdoeg!qVtrN>Tb`Pb_K^TdUGj1kg?{=lu2 zTcJpl%&B6R5^4kTzSOY};1c3N>&ZNDccVn&i6H10PE9Z?%y#lc6*Z&xV?>SI=H|c* z@C2O_)x#%BZly==-!{Mv@H&PjrI-6d$)gySH*M#zkaXhEwSsn7j+}SwIES}ljd1^h zTsCqYQ!88$CVo*`X17ejit4cWOrElGqPZj%!Z zd&Lfsg0pncnSKgKQS@|))%sus9E6HO&7zvX$Yj=Cfr^F@AJ?a7{-l0ZqcXUtV{*bx zr{o#0D5*tmGhm1pn^4kdJ)M&KJ)(pso@Fz^_vHwFigltAD?FL#W~6SrnqysENACA<>C+^vH?CU#6DO4$Q^@iF9TuaYX}Q zb{m#TeEW}FU6sVv{BVn@jC#bUEqc>K&h@&}gs(+Dc1dJaq}fPQV1|cB=U2z9Tz_0f zrsUt&29&%_r{o^bq}6OADg(S;en8>shLz$Y8YE7u2FxuUrn<+KbuC24eL1`#o(jU` zIeQw0cxDQ}5?$lh76=#cAq^2u^+#45-lC=;mejHlHKwaqCEi~a_VnZ=lqLn&vqW2k zNxp42d$bEU9V61C#3oGE_=oOL_1oZhGz9vZy+I9ZCJ&d9dtt{nO76puIvWIm^~2ES z7JgA844;Zkv>~?a*wm~Nu``%LLx>}sCrT!vq($c?&(b<Y+Z{%mxA&gwkz377p)IVlDN_T292sZ56&60(%kr z%zCE!1CWR?(G~-rz@dkXBWTqSIrLPh{CIrM#%W+kjUc_f(VSk)eu%@plU6S@8SX4c zfZg(gLH)&4=YJ1m+aOfPnH(IOpfZ2-J?*CFV)ib&nZpM&Y z)W2O=YpE*#6f(x)ZACvR_vz2U zs1!|9Ct8V`Ahy#n#H&cO*mIK#*Ea`l{G3d0;I>%3p2ACr9~gw1!5Ffq|0`qDK5TJ* z3Dpl{NWJJpFN9Cx;%fd;azO(2+W*ZMSvw@o^zfB#hnip@Dl9ubsH}T_1oVz*PoCm_rh2Q<1fpnJi;16?cP z_3*GIa8sOsl9(Z*8*q=Xg^Hn-ac{RA-Vh(KgMgNqP~xoo*SQ0Dfp|C`0K!_6NQf=L zaBvl5MNMGla8h1A2@$pZoB~cKPb(XENJ+wYE#}A-K)u2)@$VWwQt}9fWVGH?ejw6i z4d%y*UV=lrEC~{q8-3!goO8gTT4I^ogx@kq;CBUYVBB*qwqo%loimB6?zli|YWR zK4L+nWK67s{^8JAKr~8L#fMhv(<}yU3-k?v(d$@MUsaCaw#sn43EaAR`op-)JMZ0U z2MsG|RlqU$6C<>Fi_j_U?~@?$Vsi{GTx50eMwaCKBSzF*SB~<$E8JO0x$q3wBwFY; zHnYLJp!pw(6U|s0MO4I4=l1jt99omh2Z&k%r4+Waboo0>qzGd8LcOSug3 zVaS+bNTGmwNjKxL)!HGxVgnpTQxM^&z-?5(8{uFG92LM8;!p!h7*wH_Sf?D{G&hJP zy*S=}z^RW|6i&bIG8h|2(pp9i2DN69DLKH;ZlH-(g9=>(M43~#uz+j5LZCd6{Y);0uYA)~t!MriF)B!dQPsFZB(2;4_XUL;Zy(b`ADp#?EJ zkHpn2eWZlx&OjjJ_3{HDK2jO3F@b-DPf8qd&26`MHr#q6^_6-DLO`u}pM?20-{RYF z+qG|?Bo_}?)Zns~tw>j&VWU9zh$8dFmEZUg)Z&koh1=LcY(bIb($6_`im2XkzMmak zMs6!Xgm=;$cPoX|`=o)7hr0l-#w^iCJp>t2_T5-mzLdyeE7w4ljQcSxKkzbSyaxmF zr9kEDlL8db-t;Yi(=aS(G9=EZQ4(+rN+%sH!+?_tbdPWt1W+kXAt6dMMZkENNQ>C1 zAWS$s!s~9Mp{V_ThzM~w&2jIqDIxb!nU6RLBO+b}LDxLm;ahl#b1@?0(sBf^6a=b} zuW8E2!$DCqE7OeO3M$3QM>1%nD_pTl2>^p?q-NzP+(9hw!2J0Qg}s2&DxRih(R5H= zox~-zff*oCfhzT;2lw*NL8$E2kR;$Fav)#zl#)IXDtbdG2sodNCvxH4Gb*U4I1y#} zfkz=I0MuGo;PQ^}HJY8?gHeG$iJWZ-yyyc2JV;X1sL~hEpf+`rR_!9eJ0% zCms7nsLGFB3IYu_p(=2U0YxU7VCT6GfcBI9IDiI1err(#iU5}qLAP)|{F6&);2MU>@eQc>WL z_!|vl3Le_(N&p00h9N274?)eM@ih?SmJU-3;WRFi-ugwIKsO_go~_8Q!IO#~nR@`-zEEtZGEUOH@j5KR;oR^bVWYL`4@* z0h7o`JPlBOlr3Bk=w0|&Sd<{~^iqKnNC@!_B1|e^jdc6{4C-&U{OSn|;CC9%aD{DM ziW&O&2@YRSDal$zwyUrB#Q?{lEHj|+xN;un7dbf2S*viZ!A({HN6-Sg-Jlr^Rc4_i z6)I{(=iW$u;{-?OT7vN<8M_t(j5S#4qWk`)%ozFtFNIiKl%Vj4(f|nHNs<5~onw)X zx5>p27+pG|)&gp^y*m*;7ZYKK_o&dVc`Q( z<`5@mUY|?Ly#4^uMnXIh0`PK}N2O$8EfSt1Q_|m4X=RpEu>`p8AF46~OX?K_8to`} zd{v~LhwT8osKIu%_tZCmGI+|0R<@zA-9@#LKEt451V$9x1Od1~R(Bt^m)LbV6;rBO zeIE8gPaSuIf{1Zpj&BFcu@f*5w-E5J#0K2o8= zY=I2Xzb%R<>=e;X!Gu`lh%FaVs0d`ro+e~6sJ{Xgu>ujBy+XUaJ#a@Yvk37@FTtTB zxr*H1ISH4cB;r<^u&YPp2DFS9B-~CfWZqLsI&Gs;@>5Qo>!lpDJ7)|E099!(hEZ4w z@B|2ATfPdMj7{}{6=m4F9j}8>@(J<5}QdV($tnCEo%tT8)!LJm9Ic4n-YLl#L~lPO|Lfg z+mmS#8&7THBQZx~17H@4

~=yXrD>RK`$fecAMh>$o3z)M_4zTxpWjgnrdh?wZ< z;qF#)RaF1eqI?{qaso$XGF=d+rD+pXmO`6Yu!Wn32wh|?|J)z~+pTzAm zMe;#!5x`)#EvOu{MwCN4(rw}pA5BEe#>)gL>~96t7ElEp^#S1{#$!Z(K%Ii>(}|QM z-Zj@hFmibU3eTWXQvYwNw6M3I_B(~F8b>B-)h{dBG$!${>IDG-Cy*E8v;+B+WhJ=1i1>&1zJM8AK2w(S569x27 zwA|QQR~ZBbmuEiol9IlX>bN%%DKU6f_f-!e;7FumQ6>?r5g3&dxCw$n0E6l)sIKcl z5Cx3~Xi&Jc9Au)~<1rb-H^ss5zw)<})!p|Xr3o$Gwc033~VSxIc@e(H& zxX(U`>_QF)QFZ|sj3JhHuqW_JKzj+92uIowbfB9TN^E>u9?zA>tq>aqU`Nmg$5Ek4 zTJ{F+8@Mi-o!oSez-l9X(V#KOPQApZWJ+EKPVQ7Sa__TvXuk&6ogPXcQE71bu;;2e zzzP8n=&q$8s~s;~aOjq$X<(#ZbFutFkf?c;SbJXO?LJi$oT%X@XJmwzRHh@0_6Ucx z)mh05>QP_PE7X;udFQvAZXiZ6!pmr2P0%hLL!}dquBgx+JpPInvFN$q*)zi0&UR9e+$;0G; zMm?QL%fY+yEXw5Iw6XyTRctC&;dY>QQb{6Ta6vn5A|f17p5ZW5)Rv@~CM^^yOFVLD zvKE#30Z9$=kqlLberE>4iy;cohf)`9(fMe7lgrp=<2Wi1|g@11|&Mrl(L$U)myon=m5v za08u2E4eBq&aMK?u(KK@&Jy;xt4DvA;FY%XY`N<&wkTQFCGmr$WJ=aF_KlJ&FfzKh zK>T16krIjj)bWjyTj`XXpF;_U@5_Z*sEcJR91I{^MA#8{0e`>~ofJrI)l1n05X5|O zQa|2VNsm2Ha1IE2nW`lzVn=|;hgUEpb`^>>IV9fL*-6hDI5_^!LP<5l&cNwU`I8Sr zqz+>C^vpim%Yc$k=+e5#GilYBPo`vRZW=IvTgn6|g@^eF>#rUc;nRiez5{ri2!*Sg z057}1p=*eWc$%Jv$bOW7NnBy2V?pxez94p(gju&ADa>{T?!24kB+d+g`K0gvrFuS@ zXycz*fBDc`8RnC+A4iYgObb0~NNWmgfk^9TV0m8c7*)AY&_PvTe!;x*dTM2YT?)c| z+wT7j4D}C}U5Hiq2jY9G=BX?)puH6t0OQX4osV-vYNtU5gD=pA`A_ zva@3`GVXW*+G`!qDY?ccN|qO+T0Ur@lv& zSsT27U8r9p!Jn2D%%(;z!r#m&Io^PhhdiQ0Ort{LWW$4`0VTJ1L`jVWbZLDDJb&Bi zR2hIM@koO&aN7am66aL|R;b|6IigGDbO_1{Sj<3U(nIE4_qDR)JCFekq(v+nN6aAa zd8muZvg^72M(II^D>>^q&QL!LYjh}@N2es}vh&6<9D0@Dy{Ye|;u20%lN7t()W)Ml zZaLw^OnTif!aKYo19&L}2=Y71^#Nhf*UHD)#sMUnlrCUJ<3-1r>LE{2C?S$J^>VC< zmE`<((wu4mFBnjw`uIReCfU1?ZVG$g@PPp(JG=TJtvd}Usi@@190oA9^v;O6jSCA5 zbVJ9%}H^bOK~Nfq=$MftN<~l&Gkk$SdG*+BB~!Fw43-k)C~% zLH*UJJZS1Dhwib?kNao`8J~!mNLg zyQk!cgtY8DQ6IoHB_Cm;tnR`B1p4jL=`4K8Jn@qOoT;C|^W(OE9GZ%KJQ)M1Hf3zg zYY7A}TAQpTF0Kho&}vVP>}Prj;ChUxEdx$;|B~JI)|lsUq>1CpI9#q(u!T`KEgt~F z*||&{obNoKq|pkxUr115tEf*-~`#c+v6A9|6^-CS0$pPO;+9DQiN>tL`b8U_!}g;5NH@j&{Px9aBYGdOG@Aq6G~KXE0nxqK#6oNE%9A0CDam=ZZVLS z!n>SO5`1k$$>Si1CBT*R`8U8=0YX662$g|)jN8?%{|*2i!-Otn@mFq@B@Uk$9qGce zW4>HEZ3YLfjboJ{kuSBo1TZvK zL<9Huq%%bfpr*kxO8wX_x0B_-gpw%M7FWr#4AlI>BtE#Qg(XUEGzq%I2b@VO1NDD4 zlGe%w6iNc!KV~KU+XuuFEo_%Jv; z)CCaBF^B00w|0b}2^a09wy5F}lbs>p|r?3r)sPt3k#MB`v3#`Gd=CQ6i?BN$XwVijqC%nn`OA1hg{K zO6cngllx?}Ai<#eJOjPc5x(7(|JI6i>wmmzx zZQK0az306Dh;zT(^FALsx+^lPvNF1}A~G_cjufFhxLn?w$L-l~1!N(e% z$DRIqdF{7VUXU$hf&QtUd7+*!6#Oke=&pPchatel64js%%tVEJ9#j*vr+M77b2;wd4!&h*J9CK9|!XrWHXUV795wY`Qgq4VT@CN1Cz< z<`)sHdeLueSa?UFo?p|e=&XtPy6GYXMq_#FjYihFF+ZGQ$D^qmZjNt;c#H$l)2(V; zXT=njY%916OnzW5D$}S>r}9u<(LCcQB)u%zqL4uB1lz*z%6I=;a(qS=KnJFjA3!u; z1}E{OE3hSGlY32$)ee?YEw+89lvNaPBf%J5s|ty?)Kh%>kDbv4$p$x4O&BR%E6o zyP15&iLH!dHTKLwUBNRgrjuFik@ZW)R_A7bRlbBhT@HG+r%*kU7!`W~1_Lf+rq{*- zRhF@?0-vkt7nj_Z>!L5SFj?YCo1b!U)H4J>XQ?ESC;^=AL(W8AEseE6Zin|>ybdz40hYD$xB zd**4#<#Sm|k7&D+uTt2hv#e;h9Ld|H$L)t=so(pDeP1Ni3k4O~G8U2f)|Z3@H!WI` zG4^X=GqR7M#e<~pdOI>_Xo5C6guj{UB#*r}o&Yb{t2{s~A_o`FpY}OA4 zie_Dx(@pQT2<08)$VO@lbPPRiz$)W3hBeKcU_8a2CrwFs{K98by%>Rq2E$HUsD-1> z@HIxqO+Ks2dUn__k{h3(NTu!oBUQEfem5-8k3zTlcBo<|_g`ht4>@n$NItQ$o$8={ zYdk+17Od$#p<;m#zm$+&s>J9J4V}7=BGQq7Tt=! zm7;W5$&U5~#CWYNt>lh(Ne?*`+?sJG{X80BWmfYxL8)_19(KkVJ$?ifQ&?pdH&4K% z$)J!Bd`O0ap_AjGKWcw?i!e-%qE2?Wb z7yOmkDa-Y!b)dAl>Tgc1n#$6$03-GB|nMJT?sU<~x{{ea31VMk+n7 z#=*?E+C^%`)Hr~-dwMp>_m^qRh)&3zuXPHs7`blQkhV=S+5phvg0hL&!-F=4TLV%; z?38+C^FM*J)ACqxED4`mo&B;nSW6{teGx4p{&vtu3UD=YkNB&|69xNxaCXJM7JObJ zq!LZBmH3Q)4*Vw{5~P>ykRiS@*78lF<6D%9wu4s5W*ho%FBdW~EBR9H_J))=jP;`9 zQ)3Wp7Gg3l?QcD9nJDIR0eJzklrmn_Z72G8aJ<2W!iU8wF(1w;lOp z0ScE>)CtfnTVJ25nt00<_7sTZ@-O_)GsrI0(wF2$QzDp2X$P%yV0&-^v5^Phba2Wy z#HM2>;ER4gu2X08hH@Dl7P`qkbIKnKcEHO0(o>8Q6+{+e>1q$eg*mWQSE zTb-ncx&vq4R~CFHimO*!okkmo4R*gdoIWn> zlm)t&UpEPd-OoZ2e|_7OmSY=Dt}3Q@ae#tZ-^zs9*}%7p**$sa=cA@$=c zFIZ-=%g`X^G>S`O2!dKB;}Mvc$?B-d(8+HFIr{||TYZlVe?oMGctlSeXvZb8lwm@d z<3ZWr9Wt-r7X`q&dE(a-(>E4~{MPR3T!4Vuvv4^dH*1sY=uWAJE8%8XzPuUhS=n@v z(}AtY3WK?Sto)d}u->|2C&wC15|u&N+;3|x95x#&?KD$PSu%`qRs9tO;(E0!PW>&w zx^Z3zKm*|tc;ZbcAmMkSa@(u-IliT~+=&h$K)Y7Bg0z4Th}m8>TCt@64e5OD@wl(j zNBy48U1C<(@}p9ivz(P`%SqMX3xQ1DsKOoU5RFJG@k0^~_!p2O*{|9rOl^cJ?zrs` z$PxQvy5z+r-3(W`faF!vhG~OU_HHK-RG9~_G4&`j9$P$5I{j#1y z8S`G%H&_b8^<-K39$iG;M7nndx=ByW%zKv|f4-rv0PsunsyIV4**D#Xl(=}=PZxsd z8{GOu^Q-e=?+;rbaW#eQsT(4?5l=Lss%NGQ3&LyQ)`odDbm(X`AhJ#mfiEAp8A^C= zNs6lx+jF6Sx%dk|`zcbdvjB0qFxF7=mzZ4>V^ABz`BA474a4Mwydcb=*9}M^etVFR z)TsydmlPN%?H_WtMmgU5ZA2!E1q7r`ElKZ(S=3e>&Q&I69!6{n9WJU*F1C4>MIax{ z))3zXC8~0KNJb7i*`bj4cM9&nZ=Do1_d37?F1NqNq;#W+kQ<`Z{uO$3yvMlZs6=Wr zgi_##Ih;Gj+(mGU)cU<3#G8;9DLQ4#J=nJOsak;hVe^}CpbB-%{M`^*T+dp{=TXxd#9Z=Flz9tUYX`k6@Sg1J;GrcjJ6X0?h-ihl`nCz#Y zz2Vg0PC||fWa7eAJm!OT_Q+~Lgv>r0P3an_XXpLb@mcI%3&?FdM_dddWn<1$fr5AP+}FM)6uT%b&_NAMmu;Hl?eJL13@t*?eDcbohY{pFiaOt zrlGvAcY$08+p$wupoBGI!&Z5aD9ga@FR_b?E2aB}jny-tS-$yZH`oL!C|R2fL#-X9 zWD5&C1L)*%Z)PzvTg^SVU`pNbj_tkz!fgn8RwX%p2*Z8nDcnmvei^MC$16A-{7ZPQ z5A91N0gvYqjxm_s%ocMxXIeUxfp=`zizgL!XEzfq3;X(>z>pMQ#l^>uyxYo&aJSvW z>OCdJ+cpU{R?}RX?JiUxoi}3Y)m}%%_5d_fRTd%Uk+Y_ID6oG#)~wat#k5Jd}fi=soG-c zK0SGtVCMii`6A;>dLkD`PTtNzV&eE|0$$Yh2e|BpD+HL*5fIVAuaGVEmW(u}_euob~**I(o3Nx}bDKO)Y$MEHe28gtF zs3tP}c-~I0PSZpTc>J&^6e0My_nE1{l0FkF2)w82wpahmH;cYkD|T z<4l%nLp-hAts)>Q?#FL&=q~nRvegzvpQC=7jdm^Lw`7Z83Hl~6X^~?8tzF$WVtU~1 zwjw%hR7(ygRBn)O7!aMdmJ9Vux8pGv7lqwHyfn%r(Ky2i?ytI>yDRD%l-6O`euDgI zXvG-AtfaY#=OJ#hzhKzLapkAJj#htY;ul+S+uFDCNl5n*c@l~^w)jn~Z;B+W=$K9- zeJ|zRck%!l(+uSt^G)oV(Y){xIB9Kjd*?fRR5V_5j+{IjG)0 z7puI@{;^|7ND8pb#5D`_Hdje5!0bJ(0jCj2g|3bzsq95R(e7fs&i1)7*JXVlo0h$lr_7lS@ZDwL zhtH|)1wG=t6@kKCAauv0wLCIhgKmY#4P!YI(97`q@@$^FP)|!SY?UC{hEU>>U9AAZ zkX1F}>qs07-iDfjUuyL`Fff3~rHAbN?pki;v^9iF%qD{ja|P=>_{-UjRk;>;o!YPom_7OBHE`WG5svB0NHIIIpQ3yUC%Ly|H%_CfLK)TB~ZXuxTg;)a2diN+?Uw zzEQUe2TE=r7P8Z>sRj%upoh|oXoz%_MECJfFtoop+6kQ5I2dsOl!TrH{A8OF!dVf( z8`l%0KWWqZ~hVBGN!UwPWQWa=HjnWoY|IX9Sdg zWJa6IlX4eA4-=HFu+$?^aW1LtxCqoyBq|)uBowKilo#Fq^pe9{Juwf%=m+o!!T72X zGu-6-Rmju9BM%$--8PF$=11DY^`alXUf6@X1MDrs9=EM&q*d&p>nC==sS;=UxQ>N! zygkY$t+tiujc4QcCxNC@_gJY%7seS0sE%lYr0aRsAX37mglsGesmP=g{TA_{#%es! zh06~%Y{ccVTC8lWm=Y30Xw2$f5pA+2VD<%WBlV!TmzJFU@9V=2zro~iS&?dZ|M9ewn`WmI;RGFZpGNgHLud(F_et_4r3c+3!fKsbJ`eY|q{LbReh`hsq| zd#960Ij0zbZ?UsCo`&|OQ)z^UToD4_JH^H7cQZOFA@wOCH9EPV?)P?RU4C=RVP|W< zX|WpT*|2xlPE#fSgqme2!iWTSd@Th@(0U9>gYSb`*&>Y1$E(B&!@i$>a?s8t=)?kycOO2H{;n4$ z@<`V!NjWgD+G(|_)#A-S0Ji54yhq>N)+6_jOWQWvQw-|o&3wCk%I~zq17B2Zs>cKW zR-*en@9{jD`I`yx_y(tUeKU}R)2@_*-?~ceC8m6``v`Odu6+_qxgC#jNpR{z%n3g94+pj(LOPN5L=+S8$mkRRcV=HrM?fvV@*W zZF2rZ;P_HLb14!5~fl!ZLc$ z+V@F%@-ye3Kc^ghUZxWtS(+v*2b3_MrIo~2eyRs}-_n`hX$e^Cdj4#=WtHJh8nj>o zzN#yGuyiDYw*h6%`yr zwE^CB?qGXilXQvip_Q`_;56E(E%<|m*capdo#Io=f4P?`d|<7-;Rp@%kC~PTiyR1u zi2^tWm}Uz+1Xv(JDp#hYxRbcFQJPl>1=`ep-vL4EhyD%y5bKBCrJt};aJpyej1b76 zcs<|;_zn0||66v6aM?fJ56CaTjoKgR2lxy04)+e!0bmJWGdv*d7X%XY2Lu@EmilJ{ zct4Fl0cY>ipJPXwFO1uSBJ^ml{@wm-07vhMg}N$%`=8lQ|98XfKbEuheItFd{`p^k z51=31ugM?(H^Il9rQY_w9>6}p;|lco@I`U^_6zd+_SWD4(CF{`UHS&})9^zJu>8KY zLo`O*7Bpf!H9YTo^*;J}^nZQl_!$DAzI*~adS(HD&)z*kz_*a#_w(-a;aA$e;vVD< z?R;C_U{C%;IdM#;z>|g91O6wWv#6qn6a;=Mt^Lh^i=kdbV}q4x4mY#|66(wpkZ{WA z;y)OUp)Iac|1Hpom@frz?iIopNaW0dpS`SFs?as(t)kmJ{!4(2#z13O0$=6GJVL8N_?_a6Ik)$FC9SO?EEH?pKsbDn*K#!VyCJf5(`VA;B(f zCp*i!W9Q59F|JV7XN#62E({%kVrSAytb#>bQOjxb=jw2NFLkj>ItgD-T+Vo_=jGdb zpfQfM$+B^Zn^;)$%kJk7Y_LD7xp~|Uqr!R0A`1r>Lo{M`qYo4$aXQNLA^XzC$rf(& z_5gOzhc7a+3`|kbEi!F}>f)(3x+5A%tna2OqTL{H0d4r+K*`|ie0HHqQ~Rfayg_=_p_AWwIp((aBMeLE@EAgz3*V zslp(ZO*8oSiF8M6>@{efY+!VyJ{!^JHOFhBF>I(kosEFt?P^6*3`giM1aCogqXYvm z@lT^!O}dZDu{{Aox1-toigl(Cj8>&||D*8Sw`WB?sz4rZ)5e%nJD6m!=SX0!SRocN z7P3JII^G{528fJ>K6)U^#okDlx*9MeEdofUJrtGIe2K13>LzDR)4Ju50w0ge4EFIs zn+|CkHx_q}%9q5m*}q{--QF3Jf_qwtB~f|0lLXplg@TZXdqIDb zI_A2dDW0S3wAkm#p!A1%OJ{qQ-34N*tuuf2=RMd_#<_wzn0N0o>>4zFHwfxEykM)F zflBJ^tpVOzR+Az_D>iM@8e_&>Mi*)vEv}r79=RO#GUp{+S<`P1Pk91H6O&`VXr5A5 zN<$LVl@{eavrV5ZrR_ufiHq1=O=gf%Cza3Q{FdOt~nnslZqn9u{ILblP1-N2>`~@CiFRuYp+z%4j$2bD@)R0XI7u7`H z0~7XUqiKbFYIw2pWrY&)VMF1o5v5*wa1Y7g5_Pw+#f$d*=%)NjtIUt*3lFU3WqA}W znl#tP!-@@_sI?4nrnEzcbeyeHJC|8+eF#o`8BEUYHQ$^l%bD25=H8%SZD|UHZK;cK zgkI!V1`i1eA+lt7XCSu?laZ@GFWQ9Hhe#RjVp~4Luc4r(_vgfzqK$nWYx_a~ox0Bp z`z^Z!A4VvyQ3j&Alzcixrd;jf7#o_pH-|SNFEaaYSY;hcI$%Un4eh?u^+t?Tmf|Zy zDq0s1QcIM_7P8&s>qk5K|Ba<&5lbfh|ETK!H-Mi{{*SQlzj?N5XuUD_T~$ruOr(QT zh6q&~_{@y_UjykKv}*ecLQMxH;lDo2Jly$^JBGG?b%cq#5&z$V_5Vnuv26*lt= ze9buh`Vd^V+Z&xX%C*Cf!%y&YAm0Xd)IT(U1j>G=5M62%Q z{wLr+zDISmVf3O!+SIEcJLL_~8Z8LLs(i{k-voZfM4r)vU zGdRbwHU*6-kvlng#(6;01cipZs8$k4C~Kg6#r79&;|=+;A+xP{Ac_D|)tjpoib%)-Wye=^z0<|Q-$NGa!3R&Tf{2Q+)^T4_aqlc!$LMr%6W*YW>OPFk?HqL( zn7TR~XF*&7mspk(@cv99Zb=cHAOyH6Rx>2FIOiRM()VdRjr?t>Jm+u% zTI+hx2mi5_%XFN!aAax}EgaEp*d8dDn9N^A(8>8MMNeE1VH@=| zdk=X_#k#in@6sc7&JPk_C{4}~jo(+Fm2dDBL292{n*4u*{`D}-Md^)q))MwRkrqZ; z=1>FWg-L0Ib{{OKtAm2Ssf-spLZ;3*RhYN!I{Dv`Q~)?iCt*K%1qJ0k>qaUrpTLP` zMek;+tD|P=ccmHIRE6UmoIVJWm75qUvOvl}y)*66zHOCs3I_LW|FZ?3Ou+&7yPGbl zyek2YuAo4O#daKFmVuo_ZOIJxQgxZ{FGPpTW8|%GhOsNQvc~)&rI3l7`H*c2eKS8_ zwqqL4Rq$JtIDnS@=#065keDxADr};Zj`!jTEwCP zElf5k2O>#kx9=2E*aKZlhlG1y8_3R4H=trVmQ!V=q46iv za%##Byh@zEj0O!9k|FtouP-qheD?__jO4J5<mi_k&n6BlmSY5jy22q%>)m)x3D z#6GYGu9T(ol3Y0lis(&ksn&s{g5rIW7sGkzIU5eaN3dp>6n)qeCqt>mX14+_2b ziMMt5*f(f8y*9+FI3P4GLli4sh;I!_twhs97i zizLgRREm5>au%b+hKvunz|c7b?tl?gYqhf0jAxvOT~cacr+s$7A~wq$*cYq~e{t=& z(e8-6MbDN9tS!NefH02>g(Mx@PJeLjKT7r~e_nrAUFswm%8}2K%gF{ETk+D8Bs453 z7EXcq*nvA~Koaxdcwb);IB3quv%|bY5B(8pHP74HAxXth?eBX;E2vNEv_4voLRxGb zHHcLwVWc$d!v<>kq~+609g^RO_>f`5wHT^-z*H8<{mcKQ1NV4q3^>=j0o5rbRK5IxYS12`+nKPkUPUK8%)i)TR~(q#4Yz$4^FjjdC({ zp|pOI14wHcJXDfv3Vtak>7==OOh-ZixczInD8Cmvf5}O(*F|G$U$EmlDM{Qx5?UjR z`U#2144Y=NyHEQztXj6pvIBhIszo2Wl7NrkKEe)pwEFfK3hulEZ z&mGJ$S}Y(njiFXr{mcUBDoE;*7$f)YbOjCTTsSf_d*3GC7*`&|xsINgL|l65dRM{d3Jgk+`Kg7R5~T{<_`T+({b(G% zxo^(~S!rVH0lUp^moIhDPq__0=2O9g6TQA5zB~H3y%>MKD#2@_Y*wUCLhPz1QdW)@ zhwf8lIX?Y>Ry8A5Bl9*w8DT|nplhczoqLEbw(;TTw)maXz$UuRygPGz)y0TJwqdgG zLYp@}7Pz_$S!O#CF5$RB)p}$oGkeShEZ8;I^`@(x9U?1E2Mu$QX0#AcKJbs1SHkH< z7)92%*T;x7yngUt6e95?+6twOVb~0rX*n!nYW%r_S~3%-#|Q z2Vj;NV~ua-CHmVbydNYdC}Qe?fXYFMUh^Bf!QH3_|Cg9v3MbaNrf1kzg8ZmIbOEir zf=z;0E|o7=$<;UoWumPu(!@jpnCzhR3N{Wj*?~)U2MGl{1eAt)iA((0C=%u#B7{@$ zRuJ%Boc3#;$G3H+pKH@ys+<~5kZXbjoYRuJ@ab29KuzneX<;R-c8hAf8V*Na^K6zV4r{;XbR8!2!a{yw z1x|8J9^J+!a4X^Mmqxfs6DOTGKV=eDCPhhqvap!Q%t} z)ariK*EH1iuTxQSHC102)(t$iYd63gOwS*B%J|K%-1Co!stA^6$2ttjZS-p7!@5q* z+b^J5d6gd&*4aEX9$cH0ine=@1‚mJ-5NrHCVT!28PpCv*Ucv>-;8$6V1RA4EQ zM}6^ZKQ9Wa}n+#?gCRM|&2K&(b$nX2mJa{3D z8RDXZ#N9@ckVTP*GUet^RWOMRvz9@$WGEE0L67W*#l3m0afAz6ElU5=fv{h$T61Y@7O2~3)DliWZO6-|Ob2g@&S)1#!2qndeJ zhA~NHga4hW%aH`@8Nh~ms;JI1rzGsT+K>xD!g(d792%7-k zjB=Vl??EL3s*Lhn{mH&OUkJJsgq!5MD9gmexz^N|HNEbd$F(1s@NC>(UBbd-)RCB_ zhdL<qDAT6c@MzZ+FUxlz@oD?-T!Ij-B2J981_MSKGPy8d{ zOP~%<6}}>4n5APRkp7Vgez4Yd`RkSt2+CNDZfvUj>sL11i~z5~z#4KDQ$|Rky~=SY zHKHS-@6KfY9Nms`=BVE>WK<|1#1T<>%T(Pb-+zP_O4a3t@H_GDI6WTFQHk|9i>
    t6di7<;PrPjyli9RiQTSlZ2K` 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 R4a~%P?Y{Ou%l(u8e*hdzfRF$H literal 0 HcmV?d00001 diff --git a/src/assets/blog/greenwood-logo-300w.webp b/src/assets/blog/greenwood-logo-300w.webp new file mode 100644 index 0000000000000000000000000000000000000000..ee2070813d87fde1992150bb84df6b0af4930922 GIT binary patch literal 5432 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 literal 0 HcmV?d00001 diff --git a/src/assets/blog/greenwood-logo-500w.webp b/src/assets/blog/greenwood-logo-500w.webp new file mode 100644 index 0000000000000000000000000000000000000000..e0347a74791d5a7f079d680e982ffabf95ea6d9c GIT binary patch literal 9838 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 Ujaet*+Mu@@YPbLZ000000D61X2><{9 literal 0 HcmV?d00001 diff --git a/src/assets/blog/greenwood-logo-750w.webp b/src/assets/blog/greenwood-logo-750w.webp new file mode 100644 index 0000000000000000000000000000000000000000..222df252fc9936a441513162e82e4e1b16c2c62e GIT binary patch literal 14998 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 literal 0 HcmV?d00001 diff --git a/src/assets/greenwood-logo-full.svg b/src/assets/greenwood-logo-full.svg index 093692c3..bc65d5c4 100644 --- a/src/assets/greenwood-logo-full.svg +++ b/src/assets/greenwood-logo-full.svg @@ -1,4 +1,4 @@ - +
        + ${posts + .map((post) => { + const { title, route } = post; + // const { coverImage } = post.data; + // const previewImage = !coverImage ? '' : ` + // ${title} + // `; + // ${previewImage} + // console.log({ coverImage, previewImage }) + return ` +
      • + ${title} +
      • + `; + }) + .join("")} +
      + `; + } +} + +customElements.define("app-blog-posts-list", BlogPostsList); diff --git a/src/components/blog-posts-list/blog-posts-list.module.css b/src/components/blog-posts-list/blog-posts-list.module.css new file mode 100644 index 00000000..7bf58c88 --- /dev/null +++ b/src/components/blog-posts-list/blog-posts-list.module.css @@ -0,0 +1,4 @@ +.postsListItem { + list-style-type: none; + margin: var(--size-2); +} diff --git a/src/layouts/blog.html b/src/layouts/blog.html new file mode 100644 index 00000000..74f3b8bd --- /dev/null +++ b/src/layouts/blog.html @@ -0,0 +1,17 @@ + + + + + + + + + + +
      + +
      + + + + \ No newline at end of file diff --git a/src/pages/blog/index.html b/src/pages/blog/index.html new file mode 100644 index 00000000..cea34f51 --- /dev/null +++ b/src/pages/blog/index.html @@ -0,0 +1,34 @@ + + + + + + + +
      +

      News and Announcements

      +

      + Get the latest updates and information about Greenwood activities and releases. +

      + + +
      + + diff --git a/src/pages/blog/release/v0-15-0.md b/src/pages/blog/release/v0-15-0.md new file mode 100644 index 00000000..38e98970 --- /dev/null +++ b/src/pages/blog/release/v0-15-0.md @@ -0,0 +1,107 @@ +--- +title: v0.15.0 Release +layout: blog +--- + +# Greenwood v0.15.0 + +**Published: Aug 6, 2021 (postdated)** + +## What's New + +We are super excited for this release as it introduces a new feature to Greenwood we are calling **_"Theme Packs"_**, courtesy of the new [`Context` Plugin type](https://www.greenwoodjs.io/plugins/context/) that was also made available. This release also addresses some bug fixes and some refactoring, all of which you can check out in our [GitHub release notes](https://github.com/ProjectEvergreen/greenwood/releases/edit/v0.15.0). + +### Theme Packs + +Being a developer is a lot of work. Being a designer is also lot of work. Being both is even more work! But if you're a developer without an eye for a design, it can often feel like an insurmountable task to get that article you're writing, or that landing page for a small business, or that little splash of color to your weekend side project to look juuuuusst right. Struggling with aesthetics is the last thing you want in your way when trying to get a good idea out the door. + +With Greenwood [_**Theme Packs**_](https://www.greenwoodjs.io/guides/theme-packs/), now developers and designers can create and share reusable HTML / CSS / JS as npm packages that other Greenwood users can pull into their Greenwood projects as a plugin. Now anyone can get up and running with a fully designed and themed site and all they have to do is just add the content! 🥳 + +#### In Practice + +For those unfamiliar with [**CSS Zen Garden**](http://www.csszengarden.com/), it is a site aimed at showcasing the power of CSS through static HTML. + +> _The HTML remains the same, the only thing that has changed is the external CSS file. Yes, really._ + +That is really what is at the heart of a Theme Pack, wherein the user of a theme pack only has to provide content, effectively. + +For example, think of a template for a presentation / slide deck. There will generally be the following + +- theme (colors, fonts) +- background images and graphic +- slide layouts (title, two column, list) + +As HTML, that might look like + +```html + + + + + + + +
      + +
      +
      + + + + +``` + +For a user of a theme pack, they would just need to provide markdown that matches the template and presto! Instant theming an layout. 💯 + +```md +# My Slide Title + + + +With my own slide content. + +![my-image](/assets/my-image.png) +``` + +## Learn More + +We're excited to see how this feature allows for greater collaboration across the web and those with design skills can help those of us still working on ours look good. _**Theme Packs**_ are powerful, and can encompass a full application framework as demonstrated in this [presentation template repo](https://github.com/thescientist13/greenwood-starter-presentation), and you can see [this example](https://github.com/thescientist13/knowing-your-tco) of an end user experience of a theme pack used for a presentation I gave. + +![greenwood-starter-presentation](/assets/greenwood-starter-presentation.png) + +To learn about Theme Pack development, check out our [guide](https://www.greenwoodjs.io/guides/theme-packs/). + +Thanks and make sure to share what you've made and we can all learn and grow together! 👋 diff --git a/src/pages/blog/release/v0-18-0.md b/src/pages/blog/release/v0-18-0.md new file mode 100644 index 00000000..8107b7f3 --- /dev/null +++ b/src/pages/blog/release/v0-18-0.md @@ -0,0 +1,37 @@ +--- +title: v0.18.0 Release +layout: blog +--- + +# Greenwood v0.18.0 + +**Published: Oct 22, 2021** + +## What's New + +As part of [this latest release](https://github.com/ProjectEvergreen/greenwood/releases/tag/v0.18.0), there are a couple new features that we wanted to share and highlight for you: + +1. HUD (head-up display) UI for development +1. Default and Custom Not Found (404) Pages + +### HUD UI + +It can be annoying during development to refresh the page and not see anything change even though your code has been saved and updated in your editor. Grr.... there was an error in the terminal the whole time! 😤 + +Greenwood understands how important it is to get fast feedback during development and nothing is worse than an error in your terminal when your deep into your browser and editor workflow while building your website. So in this release, as Greenwood is processing your HTML file, if it detects invalid HTML that it can't parse, it will raise a message to you in the browser with a "heads up" to let you know about it. 📣 + +![HUD UI](/assets/blog-images/hud.png) + +Neat! + +### Not Found Page + +Admittedly we are probably a little late to the party on this one, but thanks to the enthusiastic voices pushing for this one to be completed, now it's here. With first class support for a traditional Not Found (404) Page, Greenwood will now automatically generate a _404.html_ page for you as part of the build. Or if you provide one in the root of your _pages/_ directory, Greenwood will use that instead. + +For example, here's the Greenwood website [404 page](https://www.greenwoodjs.io/404.html). You'll notice that we are not on an active page in the site, and so most hosts, like ours (Netlify), will automatically serve the _404.html_! 🔍 + +![Not Found Page](/assets/blog-images/not-found.png) + +## Learn More + +If you would like to learn more about these features, please join [our discussion around improving the HUD workflow and implementation](https://github.com/ProjectEvergreen/greenwood/discussions/631), or check out the docs on [creating your own Not Found Page](https://www.greenwoodjs.io/docs/layouts/#not-found-page). diff --git a/src/pages/blog/release/v0-19-0.md b/src/pages/blog/release/v0-19-0.md new file mode 100644 index 00000000..ab397c71 --- /dev/null +++ b/src/pages/blog/release/v0-19-0.md @@ -0,0 +1,155 @@ +--- +title: v0.19.0 Release +layout: blog +--- + +# Greenwood v0.19.0 + +**Published: Nov 11, 2021** + +## What's New + +[This latest release](https://github.com/ProjectEvergreen/greenwood/releases/tag/v0.19.0) is pretty exciting for us with the introduction of two new features! + +1. New Project Scaffolding +1. HTML Include Plugin + +### New Project Scaffolding + +With the Greenwood [CLI](https://www.greenwoodjs.io/docs/#cli), you can run and manage Greenwood projects right from the command line, using your favorite package manager, or on the fly with `npx`. But starting a new project was a different story. Although we had a few options for [getting started with an example repo](https://www.greenwoodjs.io/getting-started/quick-start/), starting a new "empty" project was not possible without manually creating it all yourself. But now, Greenwood has you covered! + +With the new package `@greenwood/init` (contributed by [**@hutchgrant**](https://github.com/hutchgrant)), getting a new bare project started is just one command away! + +```bash +# make your project directory +$ mkdir my-app && cd my-app + +# init! +$ npx @greenwood/init@latest + +# or, init and auto npm install +$ npx @greenwood/init@latest --install +``` + +![init](/assets/blog-images/init-scaffolding.png) + +We have a lot of ideas and plans to add more capabilities to this command like scaffolding from an existing repository and creating a prompt based interface to make adding and navigating options more ergonomic, so please feel free to [follow along or join the discussion](https://github.com/ProjectEvergreen/greenwood/discussions/770)! + +> _Reminder, if you've [built something with Greenwood](https://github.com/ProjectEvergreen/greenwood#built-with-greenwood), submit a PR to [our README](https://github.com/ProjectEvergreen/greenwood/blob/master/README.md) and add it to the list!_ ✌️ + +### HTML Include Plugin + +Greenwood loves the web. And we love JavaScript. What could be better than JavaScript? More JavaScript surely!? Actually, we believe it's [more HTML](https://projectevergreen.github.io/blog/always-bet-on-html/) (and don't call me Shirley). With our new plugin, we hope to blend the best of both worlds! 🤝 + +Our new plugin, [**plugin-include-html**](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-include-html) aims to follow in the spirit of the [abandoned HTML Imports spec](https://www.html5rocks.com/en/tutorials/webcomponents/imports/) that was originally part of the initial Web Components "feature suite", and gives developers two new ways to ship more _static_ HTML with NO client side JavaScript overhead incurred. + +#### `` Tag (HTML only) + +This is the simplest "flavor" and follows the spec more closely to address the use case where you have static HTML that you want to reuse across your pages, like a global header or footer. + +So given a snippet of HTML + +```html +
      +

      Welcome to my website!

      +

      +``` + +And a page template, you could then add this `` tag + +```html + + + + + + +

      Hello 👋

      + + + + +``` + +And Greenwood will statically generate this + +```html + + + +
      +

      Welcome to my website!

      +

      + +

      Hello 👋

      + + + + +``` + +#### Custom Element (JavaScript) + +For more advanced use cases where customization of the output may need to be done in a programmatic fashion and in supporting upcoming [SSR](https://github.com/ProjectEvergreen/greenwood/issues/708 based workflows, the custom element flavor supports declaring functions for providing markup and data that Greenwood will then build the HTML for on the fly. + +So using the [Greenwood footer as an example](https://github.com/ProjectEvergreen/greenwood/blob/master/www/includes/footer.js), have a JS file that exports two functions; `getTemplate` and `getData` + +```js +// src/includes/footer.js +const getTemplate = async (data) => { + return ` + + + + `; +}; + +const getData = async () => { + const version = require("../../package.json").version; + + return { version }; +}; + +module.exports = { + getTemplate, + getData, +}; +``` + +In a page template, you can now do this with a custom element tag + +```html + + +

      Hello 👋

      + + + + + + +``` + +And Greenwood would statically generate this + +```html + + +

      Hello 👋

      + + + + + + + + +``` + +## Learn More + +If you would like to learn more about these features, please join [our discussion around enhancing the `init` scaffolding workflow and implementation](https://github.com/ProjectEvergreen/greenwood/discussions/770) and check out [the `init` docs](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/init#api). Make sure to check out the docs on how to [get more HTML out of your JS](https://github.com/ProjectEvergreen/greenwood/blob/master/packages/plugin-include-html/README.md) with our new plugin. diff --git a/src/pages/blog/release/v0-20-0.md b/src/pages/blog/release/v0-20-0.md new file mode 100644 index 00000000..9a383a5e --- /dev/null +++ b/src/pages/blog/release/v0-20-0.md @@ -0,0 +1,37 @@ +--- +title: v0.20.0 Release +layout: blog +--- + +# Greenwood v0.20.0 + +**Published: Dec 18, 2021** + +## What's New + + + +So although there are no new features in [this release](https://github.com/ProjectEvergreen/greenwood/releases/tag/v0.20.0), there is quite a major change incoming... we are proud and excited to announce Greenwood has moved towards [**ECMAScript Modules (ESM)**](https://nodejs.org/api/esm.html)!!! 📦 🥳 + +## Why We Did It + +It was a lot of work, and although ESM is not quite in a perfect place yet within the ecosystem (CJS <> ESM interop and the "dual module hazard"), there were two key motivations for us that made us want to make the jump now, especially before hitting a 1.0 release. + +### Browser Parity ♻️ + +As Greenwood expands past just static sites with our upcoming plans to add support for [**Server Side Rendering**](https://github.com/ProjectEvergreen/greenwood/issues/708) and [**External Data Sources**](https://github.com/ProjectEvergreen/greenwood/issues/21), user's would be able to start writing server side code within their project's workspace. This meant there would be NodeJS and browser code right next to each other, and as part of Greenwood's [mission to make writing sites for the web easier](/about/), taking advantage of a consolidated module system makes perfect sense for developer experience. We want the NodeJS code you have to write to be as close to the code that you write for the browser, and so for Greenwood, this means supporting ESM in NodeJS. + +### Server Rendering 🚀 + +Additionally, libraries like [**Lit**](https://lit.dev/) that provide [support for SSR](https://github.com/lit/lit/tree/main/packages/labs/ssr) are themselves written in ESM and unfortunately because interop between CJS and ESM doesn't go both ways, we would not be able to support these projects if we stayed on CJS. For this reason, the first party code by users will need to be written in ESM. we also expect more packages to become ESM first / only, and so this helps us get ahead of an eventual migration anyway. + +## Upgrade Path + +As would be expected, there are some breaking changes and new conventions that come along with adopting ESM. If you are coming from an existing Greenwood application, you can follow the _Upgrade Notes_ in the release notes and check out some of our links in the _Learn More_ section below. 👇 + +## Learn More + +Below are some great resources to learn a bit more a CJS and ESM in NodeJS that are worth reading. In addition, the Greenwood website and all documentation examples have been written in ESM. + +- [Pure ESM Package](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c) +- [https://github.com/johnloy/esm-commonjs-interop-manual](https://github.com/johnloy/esm-commonjs-interop-manual) diff --git a/src/pages/blog/release/v0-21-0.md b/src/pages/blog/release/v0-21-0.md new file mode 100644 index 00000000..97f270c5 --- /dev/null +++ b/src/pages/blog/release/v0-21-0.md @@ -0,0 +1,87 @@ +--- +title: v0.21.0 Release +layout: blog +--- + +# Greenwood v0.21.0 + +**Published: Jan 8, 2022** + +## What's New + +As the Greenwood teams continues on its path towards a [1.0 release](https://docs.google.com/document/d/1MwDkszKvq81QgIYa8utJgyUgSpLZQx9eKCWjIikvfHU/edit#heading=h.belq6qnmcr0h), we are especially excited to share this new release which adds the capability to pull in content from external sources as part of generating a site. This is perfect for integrating with a Headless CMS, custom API, database, or even the filesystem. It's really up to you! ⚙️ + +We also improved the `@greenwood/init` command with the ability to scaffold from a template now! + +### External Data Sources + +#### How It Works + +With this new API added to Greenwood, pulling in external content into your site is super easy. At minimum, you will just need to define a `route` and a `body` for each page you want to add. For example, here is how you could pull from an "artists" API, returning an array of pages, that Greenwood will then use to statically generate a page for each artist with. + +```js +const customExternalSourcesPlugin = { + type: "source", + name: "source-plugin-artists", + provider: () => { + return async function () { + const artists = await fetch("http://.../api/artists").then((resp) => resp.json()); + + return artists.map((artist) => { + const { bio, imageUrl, name } = artist; + const route = `/artists/${name.toLowerCase().replace(/ /g, "-")}/`; + + // body and route are required fields + return { + route, + title: name, + body: ` +

      ${bio}

      + + `, + }; + }); + }; + }, +}; + +export { customExternalSourcesPlugin }; +``` + +And then when running the build, you would get the following output. ✨ + +```bash +. +└── public/ + ├── index.html + ├── ... + └── artists/ + ├── /index.html + ├── /index.html + └── /index.html +``` + +### Init Template + +To scaffold your new project based on one of [Greenwood's starter templates](https://github.com/orgs/ProjectEvergreen/repositories?q=greenwood-template-&type=all&language=&sort=), pass the `--template` flag and then follow the prompts to complete the scaffolding. + +```bash +# example +npx @greenwood/init@latest --template +------------------------------------------------------- +Initialize Greenwood Template ♻️ +------------------------------------------------------- +? Which template would you like to use? (Use arrow keys) +❯ blog +``` + +You can also pass the template you want from the CLI too. + +```bash +# example +npx @greenwood/init@latest --template=blog +``` + +## Learn More + +Read more in our docs on how to use this [new API](/plugins/source/), learn more about using [content as data](/docs/data/) in your project, and feel free to checkout and / or contribute to our [discussion around future thoughts and enhancements](https://github.com/ProjectEvergreen/greenwood/discussions/839) around the local development for this workflow. Also, find more information on the `init` package [here](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/init). All feedback appreciated! 🙌 diff --git a/src/pages/blog/release/v0-23-0.md b/src/pages/blog/release/v0-23-0.md new file mode 100644 index 00000000..d4848adb --- /dev/null +++ b/src/pages/blog/release/v0-23-0.md @@ -0,0 +1,124 @@ +--- +title: v0.23.0 Release +layout: blog +--- + +# Greenwood v0.23.0 + +**Published: Feb 11, 2022** + +## What's New + +With this new release, the Greenwood team is excited to (soft) launch the ability to add Server Side Rendering (SSR) to your Greenwood project as well as support for using a custom renderer like [**Lit** SSR](https://www.npmjs.com/package/@lit-labs/ssr). Additionally, to enhance the ability of purely static sites to benefit from some build time templating, a new feature called "interpolate frontmatter" was introduced to easily reuse frontmatter similar to how you would use JavaScript interpolation, but in your HTML and markdown. Let's highlight them both below! 👇 + +### Server Side Rendering (SSR) + +As mentioned above, we are soft launching the ability to incorporate server rendering into your Greenwood projects. By simply adding a JavaScript file to your project, you will be able to have server rendered content available when running `greenwood serve`. You can also combine static and server rendered content all in the same project for a hybrid application! Let's take a look at a quick example. + +#### How It Works + +You can add a file to your project in the _pages/_ directory and implement either of the three supported APIs, and you will have a server rendered route available! + +```shell +. +└── src + └── pages + ├── artists.js + ├── about.md +   └── index.html +``` + +```js +// artists.js +import fetch from "node-fetch"; // this needs to be installed from npm + +async function getBody() { + const artists = await fetch("http://www.example.com/api/artists").then((resp) => resp.json()); + + return ` + +

      Hello from the server rendered artists page! 👋

      + + + + + + ${artists.map((artist) => { + const { name, imageUrl } = artist; + return ` + + + + + `; + })} +
      NameImage
      ${name}
      + + `; +} + +export { getBody }; +``` + +You can then access `/artists/` and see the content! 💥 + +![Server Side Rendering example](/assets/blog-images/ssr.webp) + +> _In the above screenshot, we can also see a demonstration of our custom rendering using LitSSR and the `` component._ + +### Interpolate Frontmatter + +At the risk of (re) implementing a templating system (handlebars, nunjucks, etc) but still recognizing that having a JavaScript only solution though our [_graph.json_](/docs/data/) for static sites can be a bit cumbersome, the Greenwood team is introducing the `interpolateFrontmatter` feature. With this new feature, when setting the corresponding flag in your _greenwood.config.js_, frontmatter in your markdown will be available in your HTML or markdown similar to how variable interpolation works in JavaScript. Great for `` tags! + +#### How It Works + +So given the following frontmatter + +```md +--- +template: "post" +title: "Git Explorer" +emoji: "💡" +date: "04.07.2020" +description: "Local git repository viewer" +image: "/assets/blog-post-images/git.png" +--- +``` + +And enabling the feature in _greenwood.config.js_ + +```js +export default { + interpolateFrontmatter: true, +}; +``` + +You access the frontmatter data in the markdown or HTML on a _per page instance_ following the convention of JavaScript [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals), and Greenwood will interpolate those values at build time. + +```md +# My Blog Post + +Banner image for ${globalThis.page.description} + +Lorum Ipsum. +``` + +```html + + + My Blog - ${globalThis.page.title} + + + + + + + + + + +``` + +## Learn More + +To learn more about SSR and the full API please check out our docs on [SSR](/docs/server-rendering/) and [interpolateFrontmatter](/docs/config#interpolateFrontmatter). For custom SSR, we have [plugin docs](/plugins/renderer/) and a [Lit Renderer plugin](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-renderer-lit) you can start using. As referenced at the start of the blog post, the SSR feature is brand new and we have many plans to incorporate new features and enhancements related to [hydration, statically exporting content from server routes, and more](https://github.com/ProjectEvergreen/greenwood/issues?q=is%3Aissue+is%3Aopen+label%3Assr)! Feedback is appreciated and we cant wait to see what you end up building! 🙏 diff --git a/src/pages/blog/release/v0-24-0.md b/src/pages/blog/release/v0-24-0.md new file mode 100644 index 00000000..a5a61de7 --- /dev/null +++ b/src/pages/blog/release/v0-24-0.md @@ -0,0 +1,62 @@ +--- +title: v0.24.0 Release +layout: blog +--- + +# Greenwood v0.24.0 + +**Published: Mar 6, 2022** + +## What's New + +For this release, the Greenwood team would like to highlight how we were able to improve the speed of local development, and our decision to favor content over configuration. Let's dive in! 🤿 + +### Local Development Enhancements + +Greenwood understands the importance of a tight feedback loop and we are excited to share with you one of the key enhancements we made in this area. By using [`E-Tag`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) headers to send `304` HTTP status code, the browser can avoid unnecessarily needing to transfer and process unchanged files during development. This is very similar to how a CDN functions. + +Let's step through how it works. 👀 + +> ⚠️ Make sure you don't have _Disable Cache_ set in your dev tools! ⚠️ + +For this walk through, we're demonstrating how we tested it using the Greenwood website. + +1. Open a new tab with the network tab open +1. Load the website with the `develop` command and observe the status of all requests is `200` + ![dev-cache-step-1](/assets/blog-images/dev-cache-step1.png) +1. Refresh and now the status should change to `304` + ![dev-cache-step2.png](/assets/blog-images/dev-cache-step2.png) +1. Change a file (in this case a CSS file) and observe now that now there has been a change, the status is `200` and the page should have changed the color of the text. But! All the other files return a `304` + ![dev-cache-step3.png](/assets/blog-images/dev-cache-step3.png) +1. Keep refreshing, and the status should now go back to `304` for all requests + ![dev-cache-step4.png](/assets/blog-images/dev-cache-step4.png) + +Combined with another change to "cache" import maps, we are now seeing about [a 30-50% reduction in page load times for development](https://github.com/ProjectEvergreen/greenwood/pull/760#issuecomment-1046120992)! + +Neat! + +### Favor Content Over Configuration + +> 🛑 **This is a breaking change** 🛑 + +At the initial outset of Greenwood, one way to set metadata in your HTML was through Greenwood's configuration file. + +```js +export default { + title: "My Website", + meta: [ + { name: "description", content: "The website for my blog and portfolio." }, + { name: "twitter:site", content: "@Username" }, + { rel: "icon", href: "/favicon.ico" }, + // ... + ], +}; +``` + +However, as [our project and vision has matured](/blog/state-of-greenwood-2022/), we firmly believe content should not live in configuration and so we are favoring usage of HTML for this instead. For Greenwood, it is more important that you can own your code and your content, and so for us, this means removing these configuration options. + +## Learn More + +You can review the [notes for this release over in GitHub](https://github.com/ProjectEvergreen/greenwood/releases/tag/v0.24.0) to find out more information on upgrading. If your curious, you can learn more about [how Greenwood works](/about/how-it-works/#cli) and how we leverage techniques like ESM and import maps to keep your workflow and site efficient and aligned with web standards. + +Thanks for reading! diff --git a/src/pages/blog/release/v0-26-0.md b/src/pages/blog/release/v0-26-0.md new file mode 100644 index 00000000..5eab667d --- /dev/null +++ b/src/pages/blog/release/v0-26-0.md @@ -0,0 +1,219 @@ +--- +title: v0.26.0 Release +layout: blog +--- + +# Greenwood v0.26.0 + +**Published: July 26, 2022** + +## What's New + +After a lot of hard work, the Greenwood team is eager to share our first round of enhancements related to our [SSR work](/blog/release/v0-24-0/). By fully leaning into Web Components as a standard API for server rendered pages, we have finally realized something we've been chasing since the early days of the project; all made possible through a new library we've started developing called [**Web Components Compiler (WCC)**](https://github.com/ProjectEvergreen/wcc)! 📣 + + + +![WCC logo](/assets/blog-images/wcc-logo.png) + +### Custom Elements as Pages + +The most significant change in this release is how Greenwood handles server rendering by default. Instead of spinning up a (headless) browser with Puppeteer, WCC now provides the ability to deliver on what we think is a really nice and familiar developer experience for authoring server rendered content. We think custom elements fit right at home in providing a consistent and standards based solution for authoring pages, just as easily as they do for components. + +Here is an example of what authoring an SSR page in Greenwood looks like now if using the new `export default` API. + +```js +// src/pages/artists.js +import fetch from "node-fetch"; +import "../components/card.js"; + +export default class ArtistsPage extends HTMLElement { + async connectedCallback() { + const artists = await fetch("https://.../api/artists").then((resp) => resp.json()); + const html = artists + .map((artist) => { + const { name, imageUrl } = artist; + + return ` + +

      ${name}

      + Picture of ${name} +
      + `; + }) + .join(""); + + this.innerHTML = ` +

      List of Artists: ${artists.length}

      + ${html} + `; + } +} +``` + +> **Note**: In this example, Greenwood will _not_ ship any JS for this page. All the HTML is extracted at build / request time from the custom element. 💯 + +Since WCC is un-opinionated in how you author your custom elements, you will notice from the above snippet that there is no usage of Shadow DOM. This is intentional as this page content is _intended for the Light DOM_. The goal here is to allow users to opt-in as needed where it makes sense, because not everything needs the tight encapsulation of a Shadow Root. + +However, the `` totally _can_ opt-in to [(Declarative) Shadow DOM](https://web.dev/declarative-shadow-dom/) as you can see through its component definition, and its usage of ``s. It all works the same! + +```js +const template = document.createElement("template"); + +template.innerHTML = ` + + +
      + My default title + +
      + +
      +`; + +export default class Card extends HTMLElement { + connectedCallback() { + if (!this.shadowRoot) { + this.attachShadow({ mode: "open" }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } + } +} + +customElements.define("wc-card", Card); +``` + +### WCC + +In keeping with the spirit of Project Evergreen, the team wanted to keep things as close to the vest as possible. Although Greenwood does support [**Lit** as an SSR solution](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-renderer-lit), we wanted to make sure that it could be just as easy to author native `HTMLElement` custom elements, and refine that developer experience for the benefit of the community. + +For those curious, let's take a quick peek under the hood to see how it works. + +1. Write a Web Component + + ```js + const template = document.createElement("template"); + + template.innerHTML = ` + + +
      +

      My Blog © ${new Date().getFullYear()}

      +
      + `; + + class Footer extends HTMLElement { + connectedCallback() { + if (!this.shadowRoot) { + this.attachShadow({ mode: "open" }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } + } + } + + export default Footer; + + customElements.define("wcc-footer", Footer); + ``` + +1. Run it through the compiler + + ```js + import { renderToString } from "wc-compiler"; + + const { html } = await renderToString(new URL("./path/to/component.js", import.meta.url)); + + console.log(html); + ``` + +1. Get HTML! + + ```html + + + + ``` + +Web Components Compiler is designed to make the writing and rendering of native web components as easy as possible, and has [serverless and the edge in mind](https://github.com/thescientist13/web-components-at-the-edge) as first party runtimes, which we plan to support in Greenwood [very soon](https://github.com/ProjectEvergreen/greenwood/issues/953). It is an opportunity to explore the web and ideate on shared goals and objectives. It also has no opinions on Light vs. Shadow DOM, recognizing that not one size fits all. + +WCC also has no opinion on framework. We've even made [a plugin for **11ty**](https://github.com/ProjectEvergreen/eleventy-plugin-wcc) you can try! 🎈 + +And so now has come the time when Greenwood can transition off of Puppeteer and continue to live up to its ideal of becoming leaner over time and staying true to being _your workbench for the web_. + +### Puppeteer Plugin + +> 🛑 **This is a breaking change** 🛑 + +OK, so by now with all this talk of WCC and internalized SSR support for web components, we should talk about Puppeteer. One thing the Greenwood team recognizes is that there are some things having access to an entire (headless) browser can provide, and in some cases, [certain features](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-graphql#caveats) still depend on it [(for now)](https://github.com/ProjectEvergreen/greenwood/issues/952). + +So although WCC is now the default for SSR, Puppeteer is still available as a plugin that can be installed after you make the upgrade to `v0.26.0`. The upgrade should be quick and work the same as it did before. Just follow these steps. + +1. Install the Puppeteer renderer plugin + ```shell + $ npm install @greenwood/plugin-renderer-puppeteer --save-dev + ``` +1. Add the plugin to your _greenwood.config.js_. You can also remove `prerender: true`. + + ```js + import { greenwoodPluginRendererPuppeteer } from "@greenwood/plugin-renderer-puppeteer"; + + export default { + plugins: [greenwoodPluginRendererPuppeteer()], + }; + ``` + +1. You can also delete the **puppeteer** package from your _package.json_. + +That's it! + +## Learn More + +That was a lot of info and a lot of new things to look forward to when building your next Greenwood project. We're very eager to continue exploring where WCC can go to really continue to enhance the authoring experience for native custom elements, and what we can be accomplished with Greenwood + WCC running at the edge! + +For more information, check out these links: + +- [SSR docs](/docs/server-rendering/) including the new `export default` API +- Check out the [WCC repo](https://github.com/ProjectEvergreen/wcc) and [website](https://merry-caramel-524e61.netlify.app/) +- New package for the [Puppeteer renderer plugin](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-renderer-puppeteer) +- See the [release notes in GitHub](https://github.com/ProjectEvergreen/greenwood/releases/tag/v0.26.0) + +Thanks for reading! diff --git a/src/pages/blog/release/v0-27-0.md b/src/pages/blog/release/v0-27-0.md new file mode 100644 index 00000000..6c83b706 --- /dev/null +++ b/src/pages/blog/release/v0-27-0.md @@ -0,0 +1,85 @@ +--- +title: v0.27.0 Release +layout: blog +--- + +# Greenwood v0.27.0 + +**Published: November 23, 2022** + +## What's New + +Innovations in the industry like with [serverless and edge platforms](https://github.com/thescientist13/web-components-at-the-edge), combined with the emergence of [Web API based JavaScript runtimes](https://wintercg.org/), have been motivating the Greenwood team for a while now. In particular, how to make the experience of writing sites and applications more consistent across the entire stack, especially for web standards and Web Components. In our [last release](/blog/release/v0-26-0), we introduced [**Web Components Compiler (WCC)**](https://github.com/ProjectEvergreen/wcc), which made writing native Web Components for SSR even easier for developers, and enabled us to introduce our own innovation of [custom elements as pages](/blog/release/v0-26-0/#custom-elements-as-pages). + +![Full Stack Web Components](/assets/blog-images/full-stack-web-components.webp) Greenwood is ecstatic to embrace this future for the web, in which there is a world where dynamic can be just as practical as static, and the web can be all around you. With this release, Greenwood is able to deliver another step towards making sure it's just as easy to write a Web Component on the server, as it is in the browser; introducing _**Full Stack Web Components**_! ✨ + + + +Let's explore this concept through the first feature highlight of this release, _Custom Imports_. + +### Custom Imports + +While Greenwood has plugins to support using ESM for non standard module formats like CSS and JSON, these were only supported for client side (browser) based and bundling use cases. When we introduced SSR and custom elements as pages, trying to `import` a CSS file in a server route would break. But, no more! + +Starting with _.css_ and _.json_, you can now use native ESM to include these assets right into your SSR pages! + +```js +// src/pages/index.js +import packageJson from "../../package.json"; +import css from "../main.css"; + +export default class Home extends HTMLElement { + connectedCallback() { + this.innerHTML = ` + + ${packageJson.name} + + + + + + + + `; + } +} +``` + +What's really neat is that there is no bundling going on, just a real time transformation from source format to ESM, using the NodeJS runtime! + +This currently depends on an experimental feature in NodeJS `v16.17.0`, so checkout our [documentation](/docs/server-rendering/#custom-imports) for full details and usage instructions. + +> _Before the Greenwood `v1.0.0` release, do we aim to align this syntax on the [**Import Assertions** spec](https://github.com/ProjectEvergreen/greenwood/issues/923), while also looking to support [additional formats](https://github.com/ProjectEvergreen/greenwood/issues/1004) like TypeScript._ + +### CSS Bundling and Minification + +One goal Greenwood had from the outset was to minimize as much as possible the reliance on external dependencies and third party libraries, choosing to [eschew the common trend of building a "meta" framework](https://projectevergreen.github.io/blog/always-bet-on-html/). It's this perspective that we feel classifies Greenwood better as a "workbench", and not a framework per se. Although **PostCSS** is an invaluable tool in the ecosystem, we felt that for what we were using it for (minification and bundling relative `@import` rules), Greenwood should be able to support this basic functionality itself. + +So that is what we did! From this release forward, all CSS minification and bundling will be done by Greenwood. Along with that, we have been able to drop two dependencies from our _package.json_. No need to change anything, it will happen automatically when you upgrade. And you can still use this with our [PostCSS plugin](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-postcss). + +> _If you do find any issues or regressions in the CSS output, please file a bug report and we will make sure to fix it ASAP!_ + +### Build Capacity + +The last highlight we would like to feature from this release was the introduction of thread pooling for static builds that rely on SSR based page generation, like when using the [`prerender` configuration option](/docs/configuration/#prerender). In adopting this [SSG benchmark](https://github.com/thescientist13/bench-framework-markdown), it was clear that without some work, Greenwood would not be able to build thousands of pages in this way, let alone quickly. + +So under the hood, Greenwood now introduces thread pooling to avoid crashing NodeJS through the spawning of too many Worker threads, based on our [_Getting Started_ repo](https://github.com/ProjectEvergreen/greenwood-getting-started). While it might not be the fastest, at least Greenwood will now be able handle the [thousands of pages](https://github.com/thescientist13/bench-framework-markdown) you may throw at it! 😅 + +## What's Next + +With another release complete, the Greenwood team already has its sights set on the next one. In keeping with our goal to make _**Full Stack Web Components**_ the best experience possible, we are looking to explore these key features and enhancements next. + +- [_NodeJS v18_](https://github.com/ProjectEvergreen/greenwood/issues/957) - This will bring native support for `fetch`, JSON Modules, and import assertions! We plan to make this the new minimum version. +- _Standard and Conventions_ - Runtime wise, Greenwood would like to move in a direction less coupled to NodeJS ([agnostic runtime](https://github.com/ProjectEvergreen/greenwood/issues/1008)) by adopting a more web-centric based architecture and plugin model, leveraging standard [`Request` and `Response`](https://github.com/ProjectEvergreen/greenwood/issues/948) APIs. +- [_API Routes_](https://github.com/ProjectEvergreen/greenwood/issues/1007) - We want to make `/api/*` routes happen in Greenwood! +- As a project showcase, check out the recently launched **Tuesday's Tunes** [website](https://www.tuesdaystunes.tv/) and [repo](https://github.com/AnalogStudiosRI/www.tuesdaystunes.tv), built with Greenwood and WCC, leveraging Tailwind CSS, content and webhooks powered by Contentful, and built on Netlify! + +Thanks for reading! diff --git a/src/pages/blog/release/v0-28-0.md b/src/pages/blog/release/v0-28-0.md new file mode 100644 index 00000000..b484b7d7 --- /dev/null +++ b/src/pages/blog/release/v0-28-0.md @@ -0,0 +1,141 @@ +--- +title: v0.28.0 Release +layout: blog +--- + +# Greenwood v0.28.0 + +**Published: April 8, 2023** + +## What's New + + + +It's release time again and the **Greenwood** team is excited to share some of the exciting new features available in this version, as we continue to make the experience of writing for the _**Full-Stack Web**_ easier for everyone! 🙌 + +### Node 18 + +With this release, Greenwood has set the minimum version of NodeJS to 18. This NodeJS release is especially profound to us as it brings with it (amongst other things) native support for the [**Fetch API**](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). You can now use `fetch` directly in your server side code like your SSR pages! 💯 + +```js +// src/pages/artists.js +import "../components/card.js"; + +export default class ArtistsPage extends HTMLElement { + async connectedCallback() { + const artists = await fetch("https://.../api/artists").then((resp) => resp.json()); + const html = artists + .map((artist) => { + const { name, imageUrl } = artist; + + return ` + +

      ${name}

      + Picture of ${name} +
      + `; + }) + .join(""); + + this.innerHTML = html; + } +} +``` + +> _As a bonus worth mentioning, [JSON imports](https://simonplend.com/import-json-in-es-modules/) are now natively available using import assertions._ + +### Embracing Web APIs + +What's great about the addition of the Fetch API is that it brings along with it many Web API friends like [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL), [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request), and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) just to name a few. Greenwood has fully embraced this movement to adopting Web APIs not only throughout its code base, but basing entire user facing APIs around these standards as well. Why invent an API when we get everything we need from the web, in Node, and all documented by MDN! + +This was especially beneficial to our [Resource Plugin API](/plugins/resource/) as it was already modeling this request / response behavior anyway, and so it was a natural fit to adopt these APIs. To give an idea of this transformation, here is a before snippet of Greenwood's internal plugin for handling CSS. + + + +```js +class StandardCssResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + this.extensions = [".css"]; + this.contentType = "text/css"; + } + + async serve(url) { + const body = await fs.promises.readFile(url, "utf-8"); + + return { + body, + contentType: this.contentType, + }; + } +} +``` + +And here is what it looks like now, exclusively based on Web APIs. Nothing ad-hoc anymore! ✨ + + + +```js +class StandardCssResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + this.extensions = ["css"]; + this.contentType = "text/css"; + } + + // defining shouldServe is now required + async shouldServe(url) { + const { protocol, pathname } = url; + + return protocol === "file:" && this.extensions.includes(pathname.split(".").pop()); + } + + async serve(url) { + const body = await fs.promises.readFile(url, "utf-8"); + + return new Response(body, { + headers: new Headers({ + "Content-Type": this.contentType, + }), + }); + } +} +``` + +> _This refactor touched most of our plugin APIs, so you'll want to read our [release notes](https://github.com/ProjectEvergreen/greenwood/releases/tag/v0.28.0) to learn about how to migrate any of your own custom plugins._ + +### API Routes + +To fully round out Greenwood's server rendering capabilities, API Routes are now available. This API is based on a standard `Request` / `Response` model leveraging Web APIs directly as inputs and outputs. Let's take a look at an example. + +API routes follow a file based routing convention, just like [pages](/docs/layouts/#pages). So this structure will yield an endpoint available at `/api/greeting`. + +```shell +src/ + api/ + greeting.js +``` + +This example API reads a query parameter of `name` and returns a JSON response based on that. + +```js +export async function handler(request) { + const params = new URLSearchParams(request.url.slice(request.url.indexOf("?"))); + const name = params.has("name") ? params.get("name") : "World"; + const body = { message: `Hello ${name}! 👋` }; + + return new Response(JSON.stringify(body), { + headers: new Headers({ + "Content-Type": "application/json", + }), + }); +} +``` + +All web standards, all the time. You know we love to see it. 🤓 + +## What's Next + +In addition to the above mentioned features, this release also lays the ground work for our foray into [Serverless and Edge runtime support](https://github.com/ProjectEvergreen/greenwood/issues/1008). For out next release, Greenwood team has its sights set on being able to fully embrace running beyond the server with hosting providers like Netlify and Vercel and their serverless and edge offerings, allowing us to push web standards even further down the stack! We [see this trend becoming more ubiquitous](https://wintercg.org/) as more and more hosting providers coalesce around these standards too, so now is a great time to get started! + +Stay tuned, [join our Slack](https://join.slack.com/t/thegreenhouseio/shared_invite/enQtMzcyMzE2Mjk1MjgwLTU5YmM1MDJiMTg0ODk4MjA4NzUwNWFmZmMxNDY5MTcwM2I0MjYxN2VhOTEwNDU2YWQwOWQzZmY1YzY4MWRlOGI) to be part of the conversation, and we look forward to seeing you in the next release notes! ✌️ diff --git a/src/pages/blog/release/v0-29-0.md b/src/pages/blog/release/v0-29-0.md new file mode 100644 index 00000000..c4c764b3 --- /dev/null +++ b/src/pages/blog/release/v0-29-0.md @@ -0,0 +1,114 @@ +--- +title: v0.29.0 Release +layout: blog +--- + +# Greenwood v0.29.0 + +**Published: Nov 8, 2023** + +Serverless function cloud + +## What's New + +The Greenwood team is back with a new release and we're excited to share with you what we've been up to. From this latest release, here are three features we'd like to highlight: + +1. Serverless Adapters (Netlify, Vercel) +1. Web Server Components +1. Static Asset Bundling + +Let's check them out! 👇 + +### Serverless Adapters + +The simplicity of serverless hosting can be a great advantage in achieving dynamic with the ease of static. As part of this release, the Greenwood team has now made it so that you can easily adapt a Greenwood project's SSR pages or API endpoints to run on [**Netlify**](https://www.netlify.com/) and [**Vercel**](https://vercel.com/) serverless hosting. + +In the demo video below, you can see a mix of static (HTML) pages and templates rendering alongside purely SSR pages and API endpoints, all running on serverless hosting. SSR pages and API endpoints are capable of server rendering real custom elements, meaning you can get **_full-stack Web Components_** with Greenwood! 🚀 + + + +It's as easy as installing and adding the plugin to your _greenwood.config.js_. + +```js +// import { greenwoodPluginAdapterVercel } from '@greenwood/plugin-adapter-vercel'; +import { greenwoodPluginAdapterNetlify } from "@greenwood/plugin-adapter-netlify"; + +export default { + plugins: [greenwoodPluginAdapterNetlify()], +}; +``` + +Check out the README docs for our currently supported [**Netlify**](https://github.com/ProjectEvergreen/greenwood/tree/rmaster/packages/plugin-adapter-netlify) and [**Vercel**](https://github.com/ProjectEvergreen/greenwood/tree/rmaster/packages/plugin-adapter-vercel) plugins, and keep your eyes out for future plugins as we look to land support for [**AWS**](https://github.com/ProjectEvergreen/greenwood/issues/1142) and [**Cloudflare**](https://github.com/ProjectEvergreen/greenwood/issues/1143). 👀 + +> _You can check out our showcase repos for each platform [here](https://github.com/ProjectEvergreen/greenwood-demo-adapter-netlify) and [here](https://github.com/ProjectEvergreen/greenwood-demo-adapter-vercel)._ + +### Web Server Components + +Although [Custom Elements as pages](/blog/release/v0-26-0/#custom-elements-as-pages) are not a new feature, as Greenwood continues to enhance its capabilities on the backend, hooking these pages into the request / response lifecycle was an obvious need, and so we are now "promoting" these custom elements to a new name; _Web Server Components_. ✨ + +The API is still the same and continues to run only on the server, except now Greenwood will provide the `Request` object for the incoming request as a ["constructor prop"](/docs/server-rendering/#data-loading), allowing dynamic request time handling to occur within the custom element. + +```js +export default class PostPage extends HTMLElement { + constructor(request) { + super(); + + const params = new URLSearchParams(request.url.slice(request.url.indexOf("?"))); + this.postId = params.get("id"); + } + + async connectedCallback() { + const { postId } = this; + const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`).then((resp) => + resp.json(), + ); + const { title, body } = post; + + this.innerHTML = ` +

      ${title}

      +

      ${body}

      + `; + } +} +``` + +> _We plan to continue [building on this concept for response handling](https://github.com/ProjectEvergreen/greenwood/issues/1177) and fleshing out Greenwood's capabilities through features like [dynamic routing](https://github.com/ProjectEvergreen/greenwood/issues/882) and [hydration](https://github.com/ProjectEvergreen/greenwood/issues/880)._ + +### Static Asset Bundling + +As an alternative to the pre-defined [_assets/_ directory](/docs/css-and-images/), Greenwood now handles static asset "bundling" when referencing resources like images in your JavaScript. Through a combination of [`new URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) and [`import.meta.url`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta), your resource can now be located anywhere in your project's workspace. + +For production builds, Greenwood will generate a unique filename for the asset as well, e.g. _logo-83bc009f.svg_. 💯 + +```js +const logo = new URL("../path/to/images/logo.svg", import.meta.url); + +class Header extends HTMLElement { + connectedCallback() { + this.innerHTML = ` +
      +

      Welcome to My Site

      + + My logo +
      + `; + } +} + +customElements.define("app-header", Header); +``` + +> _We are looking to improve the developer experience around this pattern so please feel free to follow along or comment in this [GitHub issue](https://github.com/ProjectEvergreen/greenwood/issues/1163)._ + +## What's Next + +We're really excited to see the progress **Greenwood** has been able to make this year, and are looking forward to seeing where the community can take it. As we get closer to finalizing our [1.0 Roadmap](https://github.com/ProjectEvergreen/greenwood/milestone/3), we've been playing around with more ecosystem projects and making little demos to share with you all. We encourage you to check them out to see what Greenwood is capable of and help us push the boundaries of the _**full-stack web**_! 🙌 + +- [Server rendering custom elements with WCC on Vercel Serverless functions using htmx](https://github.com/thescientist13/greenwood-htmx) +- [Rendering Lit+SSR on Vercel Serverless functions](https://github.com/thescientist13/greenwood-demo-adapter-vercel-lit) + +We're also planning a significant [redesign of the Greenwood website](https://github.com/ProjectEvergreen/greenwood/issues/978) to help better showcase all of Greenwood's capabilities and to streamline and simplify the documentation. + +So stay tuned, join our [Slack](https://join.slack.com/t/thegreenhouseio/shared_invite/enQtMzcyMzE2Mjk1MjgwLTU5YmM1MDJiMTg0ODk4MjA4NzUwNWFmZmMxNDY5MTcwM2I0MjYxN2VhOTEwNDU2YWQwOWQzZmY1YzY4MWRlOGI) or [Discord](https://discord.gg/pFbynPar) communities to be part of the conversation, and we look forward to seeing you for the next release. ✌️ diff --git a/src/pages/blog/state-of-greenwood-2022.md b/src/pages/blog/state-of-greenwood-2022.md new file mode 100644 index 00000000..4fc99bb5 --- /dev/null +++ b/src/pages/blog/state-of-greenwood-2022.md @@ -0,0 +1,242 @@ +--- +title: State of Greenwood (2022) +layout: blog +--- + +# State of Greenwood (2022) + +**Published: March 2, 2022** + +Looking back on the past year of Greenwood's development was a pleasant retrospective for us, as it often takes time to see the span or breadth of the work and effort you've put into a goal. Going back even a little bit further than that when the team was thinking about [what sort of technical and mission focused approach to take with the project](https://projectevergreen.github.io/blog/always-bet-on-html/), the trail of PR breadcrumbs and releases since then has now helped us realize our vision of Greenwood as a _workbench for the web_. + +At the time of that blog post, we were thinking introspectively in regards to not only technical direction, but also how we could ensure Greenwood would be differentiated from other projects in this space. Going bundleless for development and adopting ESM were not new ideas, but we still found ourselves looking at the web dev landscape and thinking; what if we started from the "bottom" up with HTML, and then fanned out the workflow from there? We wanted to be able to layer greater and greater capabilities on top of each other, but with a critical eye on _core vs plugin, longevity vs convenience, pragmatic vs popular_. We knew though that we would never want to sacrifice that core workflow of the trusted _index.html_ file, or getting so clever that user's of Greenwood (or ourselves!) would end up back in the arms of some custom DSL over HTML, or even worse, being required to start with JavaScript just to author a basic site. + +Now we obviously don't mean this sentiment so literally or casually to the point that there would have been no reason in creating Greenwood, but the fact of the matter is, the web (probably) won't "build" things like local dev servers and minifiers, NodeJS and npm, or CDNs and serverless functions, but we do feel that the web provides quite a lot for actually creating a web project! Maybe more than you think, and with even more on the horizon. So why not at least see how far it can get us and optimize for that? Our bet is that by [leveraging the web platform as your framework](/about/how-it-works/), anyone can benefit from the richness and resiliency the web provides us, now and in the future, for users and developers. + +All in all, that refreshing of our mindset was just the motivation we needed to be able to now write this post, a year later, with some highlights of our work in 2021. + +## The Year In Review + +### Theme Packs + +With Greenwood [_**Theme Packs**_](https://www.greenwoodjs.io/guides/theme-packs/), now you can create and share reusable HTML / CSS / JS as npm packages that yourself or other Greenwood users can pull into their Greenwood project as easily as a plugin. It was inspired by [**CSS Zen Garden**](http://www.csszengarden.com/), which is a site aimed at showcasing the power of CSS through static HTML. + +> _The HTML remains the same, the only thing that has changed is the contents of the CSS._ + +For example, think of a template for a presentation / slide deck, there would generally be the following considerations: + +- Theme (colors, fonts) +- Background images and graphics +- Slide layouts (title, two column, list) + +As HTML, that might look like: + +```html + + + + + + + +
      + +
      + + + + +``` + +For a user of a theme pack, it would only require setting the `template` in a markdown file's frontmatter that matches the template name and presto! Instant theming. 💯 + +```md +--- +template: title-card +--- + +# My Slide Title + + + +This is my own slide content! + +![my-image](/assets/my-image.png) +``` + +![theme-pack](/assets/greenwood-starter-presentation.png) + +> _You can see [this example](https://github.com/thescientist13/knowing-your-tco) of the end user experience of a theme pack I used for a presentation I gave and our [guide on Theme Packs](/guides/theme-packs/) can help you learn more._ + +### HTML Includes + +We created this [**custom plugin**](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-include-html) in an effort to carry on the spirit of the [abandoned HTML Imports spec](https://www.html5rocks.com/en/tutorials/webcomponents/imports/) that was originally part of the initial Web Components "feature suite". We thought we could breath a little life back into it for the benefit of Greenwood users. Let's take a quick peak at what the HTML flavor of this API looks like, where you have static HTML that you want to reuse across your pages, like a global header or footer. + +So given a snippet of HTML + +```html + +
      +

      Welcome to my website!

      +

      +``` + +And a page template, you could then add this `` tag + +```html + + + + + + +

      Hello 👋

      + + + + +``` + +And Greenwood will statically generate this + +```html + + + +
      +

      Welcome to my website!

      +

      + +

      Hello 👋

      + + + + +``` + +> _Check out the docs on how to [use both options](https://github.com/ProjectEvergreen/greenwood/blob/master/packages/plugin-include-html/README.md) with this plugin._ + +### Interpolate Frontmatter + +When setting the [`interpolateFrontmatter`](/docs/configuration/#interpolate-frontmatter) flag in your _greenwood.config.js_, frontmatter in your markdown will be available in your HTML or markdown similar to how variable interpolation works in JavaScript. Great for `` tags! + +#### How It Works + +So given the following frontmatter + +```md +--- +template: "post" +title: "Git Explorer" +emoji: "💡" +date: "04.07.2020" +description: "Local git repository viewer" +image: "/assets/blog-post-images/git.png" +--- +``` + +And enabling the feature in _greenwood.config.js_ + +```js +export default { + interpolateFrontmatter: true, +}; +``` + +You access the frontmatter data in the markdown or HTML on a _per page instance_ following the convention of JavaScript [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals), and Greenwood will interpolate those values at build time. + +```md +# My Blog Post + +Banner image for ${globalThis.page.description} + +Lorum Ipsum. +``` + +```html + + + My Blog - ${globalThis.page.title} + + + + + + + + + + +``` + +--- + +This is just a sampling of the work that we wanted to shout-out over the course of 2021. You can read about all our releases over in the [blog section](/blog/) of our website. Some honorable mentions include: + +- [_Greenwood `init`_](/blog/release/v0-19-0/#new-project-scaffolding) - With the power of `npx`, quickly scaffold out a new Greenwood project right from the command line 📦 +- [_External Data Sources_](/blog/release/v0-21-0/#external-data-sources) - No good Jamstack framework would be complete without the ability to pull in content from a database, CMS, or API ✍️ +- [_HUD UI_](/blog/release/v0-18-0/#hud-ui) - An overlay UI that can surface build related terminal errors like invalid HTML, right in the browser! We want to keep you in that browser /editor flow. ⚡ + +## The Year In Front of Us + +To kick off 2022, we have just soft launched a significant release and stepping stone for Greenwood; [Server Side Rendering (SSR)](/blog/release/v0-23-0/#server-side-rendering-ssr)! We couldn't be more excited about the future of Web Components and with work like this, we are eager to help take Web Components even further than the CDN; all the way to the edge! In support of this feature, we also released a new API and plugin so you can try this feature out with [**Lit**](https://lit.dev/). It is all still early days, but this is what we plan to work on and refine in the year ahead and we are super excited for the potential and possibilities. + +> _Greenwood wants to help you get more HTML from your JS!_ + +For Greenwood's roadmap specifically, we want to focus on getting to a [1.0 milestone](https://github.com/ProjectEvergreen/greenwood/milestone/3), which for us means: + +- Continued enhancements and [improvements for SSR](https://github.com/ProjectEvergreen/greenwood/projects/9) +- [Native `HTMLElement` for SSR](https://github.com/ProjectEvergreen/greenwood/discussions/548) (drop hard dependency on puppeteer) +- [Incorporating support for CSS / JSON modules](https://github.com/ProjectEvergreen/greenwood/discussions/606) (import assertions) +- [Bypassing the server altogether and going straight to the edge](https://github.com/ProjectEvergreen/greenwood/discussions/626)! (serverless) +- Continued testing and integration of [community Web Component projects](https://github.com/ProjectEvergreen/greenwood/discussions/523) + +In addition to building up Greenwood, we also hope to keep contributing to great community efforts and conversations around the web platform like the [Web Components Community Group](https://github.com/w3c/webcomponents-cg) are doing, and supporting their initiatives towards pushing web standards forward. Their [report](https://w3c.github.io/webcomponents-cg/) and presentation at least years TPAC aimed to advance specs and standards that are meaningful to all developers and users of the web, and we're here for it! The **Lit** team is also working hard on advancing techniques for SSR that we are eager to see gain more traction. Topics that we'll have our eye on include: + +- [Declarative Shadow DOM](https://github.com/whatwg/dom/issues/831) (wider implementation) +- [HTML Modules](https://github.com/WICG/webcomponents/issues/645) +- [Declarative Custom Elements](https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Declarative-Custom-Elements-Strawman.md) +- Helping advance [community protocols](https://github.com/webcomponents-cg/community-protocols) like [HMR](https://github.com/webcomponents-cg/community-protocols/issues/6) and [hydration](https://github.com/webcomponents-cg/community-protocols/issues/16) + +## In Closing + +Our hope is by reviewing some of the key features the team was able to accomplish in 2021, and in sharing our outlook for 2022, that we have given a good overview of what Greenwood hopes to accomplish for itself and what we hope it can contribute to the web dev community. We love the web and we love open source, and our vision for removing the friction and tools between your code and the browser is even more entrenched in us now. + +For us, it's great to see support for Web Components rising and we hope to be a champion and companion for all those building for the web, new or seasoned, user or spec author. Naturally, the decisions we've made come with tradeoffs, as do any of the other options out there in the community, and that is important for us to highlight. It's not necessarily about right or wrong; it's just emphasizing differing opinions and values. But this is what is great about open source! + +> _We all think different, and so for us the more we thought about our approach and the implications this could have on long term maintainability, knowledge sharing, and just general practicality, has only cemented our motivations even further to optimize for a web first world._ + +We want to not only be _your workbench for the web_, but a way to build for the web that looks past the **#hypegeist** and instead emphasizes usage of web APIs in an effort to shy away, where possible, from the complexity and magic often found in today's modern (meta) frameworks. Owning your code and owning your content is important to us, and developing for the web isn't the burden it once was. We feel an honest discussion around the efforts to build around and on top of it are worth having. Looking inside your _node_modules_ or network tab should be encouraging of you to ask yourself; _**what can the web do for me now**_? Project Evergreen logo diff --git a/src/pages/blog/state-of-greenwood-2023.md b/src/pages/blog/state-of-greenwood-2023.md new file mode 100644 index 00000000..f128e30f --- /dev/null +++ b/src/pages/blog/state-of-greenwood-2023.md @@ -0,0 +1,244 @@ +--- +title: State of Greenwood (2023) +layout: blog +coverImage: "/assets/greenwood-logo-leaf.svg" +--- + +# State of Greenwood (2023) + +Published: May 3, 2023 + +Greenwood Logo + +## The Full Stack Web + +About a year has passed since our [first _State of Greenwood_ blog post](/blog/state-of-greenwood-2022/) and wow, what a year of progress it has been! In our continued effort to make web development easier to get started with, we have made great strides in our journey of promoting the best of web standards not only for the frontend, but also on the backend as well. The web is _full stack_, even Web Components! (and we even picked up a new logo along the way!) + +I think more than ever we continue to be proud of our efforts to embrace not only HTML as the baseline for shipping websites, but also being able to write [actual _.html_ files](/getting-started/). We feel that being able to start a project this intuitively from any skill level makes Greenwood the perfect on-ramp for any web development project, and it would not be incorrect to say that we are happy to offload some of our docs to MDN if we can! Why create a new API if a good one already exists? In this way Greenwood will always [stay true to web standards](/about/how-it-works/) and refrain from introducing any "magic" as much as possible. + +So let's take a look back at some key features we added over the past year that we feel best exhibits what makes us excited not only for how we can help developers achieve their goals today, but also what it means for the next year of Greenwood development. 🔍 + +## The Year In Review + +### Custom Elements as Pages (WCC) + +Project Evergreen released a new project last year called [**WCC (Web Components Compiler)**](https://github.com/ProjectEvergreen/wcc) that was designed specifically to make it easy to render native Web Components to HTML on the server. Its focus is on making SSR (Server Side Rendering) for Web Components as intuitive as possible and this has helped us to manifest features in Greenwood like _Custom Elements as Pages_. WCC is also key to our strategy to enable Greenwood projects to run in serverless and edge runtimes. + +Instead of having to spin up a (headless) browser with Puppeteer, WCC now provides the ability to deliver on what we think is a familiar developer experience for authoring server rendered content. We think custom elements fit right at home in providing a consistent and standards based solution for authoring page entry points, just as easily as they do for components. + +Here is an example of what authoring an SSR page in Greenwood looks like with WCC. + +```js +// src/pages/artists.js +import "../components/card.js"; + +export default class ArtistsPage extends HTMLElement { + async connectedCallback() { + const artists = await fetch("https://.../api/artists").then((resp) => resp.json()); + const html = artists + .map((artist) => { + const { name, imageUrl } = artist; + + return ` + +

      ${name}

      + Picture of ${name} +
      + `; + }) + .join(""); + + this.innerHTML = ` +

      List of Artists: ${artists.length}

      + ${html} + `; + } +} +``` + +Since WCC is un-opinionated in how you author your custom elements, you will notice from the above snippet that there is no usage of Shadow DOM. This is intentional as this page content is _intended for the Light DOM_. The goal here is to allow users to opt-in as needed where it makes sense, because not everything needs the tight encapsulation of a Shadow Root. + +However, the `` component totally _can_ opt-in to [(Declarative) Shadow DOM](https://web.dev/declarative-shadow-dom/) as you can see through its component definition, and its usage of ``s. It all works the same! + +```js +// src/components/card.js +const template = document.createElement("template"); + +template.innerHTML = ` + + +
      + My default title + +
      + +
      +`; + +export default class Card extends HTMLElement { + connectedCallback() { + if (!this.shadowRoot) { + this.attachShadow({ mode: "open" }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } + } +} + +customElements.define("wc-card", Card); +``` + +> **Note**: In this example, Greenwood will _not_ ship any JS for this page by default. All the HTML is extracted at build / request time from the custom element. 💯 + +### Web APIs Standardization + +In the [v0.28.0 release](/blog/release-0.28.0/), Greenwood made Node 18 the minimum version, in particular to leverage the native Fetch API and its many companion APIs like [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL), [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request), and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) just to name a few. Greenwood has fully embraced this movement to adopting Web APIs on the server side not only throughout its code base, but now basing user facing APIs around these standards as well. Why invent an API when we get everything we need from the web, in Node, and all documented by MDN? + +This was especially beneficial to our [Resource Plugin API](/plugins/resource/) as it was already modeling this request / response paradigm anyway albeit in a very ad-hoc fashion, and so it was a natural fit to adopt these APIs. To give an idea of this what this migration looked like, here is a before snippet of Greenwood's internal plugin for handling CSS. + + + +```js +class StandardCssResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + this.extensions = [".css"]; + this.contentType = "text/css"; + } + + async shouldServe(url, headers) { + return this.extensions.indexOf(path.extname(url)) >= 0; + } + + async serve(url, headers) { + const body = await fs.promises.readFile(url, "utf-8"); + + return { + body, + contentType: this.contentType, + }; + } +} +``` + + + +And here is what it looks like now, now based on Web APIs and standards. ✨ + + + +```js +class StandardCssResource extends ResourceInterface { + constructor(compilation, options) { + super(compilation, options); + this.extensions = ["css"]; + this.contentType = "text/css"; + } + + async shouldServe(url) { + const { protocol, pathname } = url; + + return protocol === "file:" && this.extensions.includes(pathname.split(".").pop()); + } + + async serve(url) { + const body = await fs.promises.readFile(url, "utf-8"); + + return new Response(body, { + headers: new Headers({ + "Content-Type": this.contentType, + }), + }); + } +} +``` + +### Custom Imports + +In continuing with the theme of Web APIs, Greenwood also introduced experimental support for custom loaders in NodeJS, allowing users to start tapping into the upcoming [_Import Assertions specification_](https://v8.dev/features/import-assertions). + +This feature compliments Greenwood's plugin support for using ESM for non standard module formats like CSS and JSON for client side (browser) contexts, by now making this experience consistent on the server side too! Starting with _.css_ and _.json_, you can now use native ESM to include these assets right into your SSR pages! + +```js +// src/pages/index.js +import packageJson from "../../package.json"; +import css from "../main.css"; + +export default class Home extends HTMLElement { + connectedCallback() { + this.innerHTML = ` + + ${packageJson.name} + + + + + + + + `; + } +} +``` + +What's really neat is that there is no bundling going on, just a real time transformation from source format to ESM, using the NodeJS runtime! This currently depends on an experimental feature in NodeJS so checkout our [documentation](/docs/server-rendering/#custom-imports) for full details and usage instructions. + +> _Before the Greenwood `v1.0.0` release, we do aim to align this syntax with the [**Import Assertions** spec](https://github.com/ProjectEvergreen/greenwood/issues/923) more closely for client and server, while also looking to support [additional formats](https://github.com/ProjectEvergreen/greenwood/issues/1004) like TypeScript._ + +--- + +This is just a sampling of the work that we wanted to share over the course of the last year, and you can read about all our releases over in the [blog section](/blog/) of our website. Some honorable mentions include: + +- [Node 18 support](/blog/release/v0-28-0/#node-18) - Upgrading to Node 18 really helped us drive forward on making Web APIs a consistent experience from the front to the back of the stack. +- [API Routes](/blog/release/v0-28-0/#api-routes) - File based routing convention to make API endpoints in your projects, based on a standard `Request` / `Response` model. +- [Build Capacity](/blog/release/v0-27-0/#build-capacity) - Introduction of thread pooling for static builds that rely on SSR based page generation. + +## The Year In Front of Us + +While we managed to check off a lot of items from last years list, we already have our sites set on the next horizon for Greenwood; running on serverless and at the edge! A lot of the work to make this possible was completed in the last year as part of the features listed above, and so Greenwood is poised to cross that bridge very soon now. It's this last stretch of development that will allow Greenwood to consider being ready for its [1.0 release](https://github.com/ProjectEvergreen/greenwood/milestone/3)! + +So what's in store next? Here are a few key items we're tracking: + +- [Serverless and Edge runtime support](https://github.com/ProjectEvergreen/greenwood/issues/1008) +- [Agnostic Runtime](https://github.com/w3c/webcomponents-cg/discussions/39#discussioncomment-3452237) +- [Data Loading Strategies](https://github.com/ProjectEvergreen/greenwood/issues/952) +- [Hydration Strategies](https://github.com/ProjectEvergreen/greenwood/issues/880) + +We still plan to keep contributing to great community efforts and conversations around the web platform like the [Web Components Community Group](https://github.com/w3c/webcomponents-cg) and supporting their initiatives towards pushing web standards forward for the web and Web Components. Here were a couple of our contributions to the conversation in the past year: + +- [Web Components in 5 Years](https://github.com/w3c/webcomponents-cg/discussions/39#discussioncomment-3452237) +- [Self hydrating custom elements](https://github.com/webcomponents-cg/community-protocols/issues/33) +- [Web Components Interop Specification](https://github.com/webcomponents-cg/community-protocols/issues/35) + +## In Closing + +We're really encouraged to see the progress of web development these days, especially in seeing the growing adoption of web standards on the backend in tools like [Deno](https://deno.land/api@v1.33.1?unstable=), and the creation of groups like the [WinterCG](https://wintercg.org/) to help steward it. With the proliferation of many great JavaScript runtimes, competing against a standard will ultimately benefit users and maintainers, allowing us to mix and match as needed to find the right runtime for our projects. + +Greenwood wants to be there every step of the way to help get your projects out there; from SPA to SSG to SSR to everything in between. We can't wait to see what you build next! Project Evergreen logo diff --git a/src/styles/blog.css b/src/styles/blog.css new file mode 100644 index 00000000..b4226bc2 --- /dev/null +++ b/src/styles/blog.css @@ -0,0 +1,81 @@ +.content-outlet-container { + max-width: 100ch; + padding: 0 var(--font-size-1); +} + +h1 { + font-size: var(--font-size-5); + margin: var(--size-4) 0 0; +} + +h2 { + font-size: var(--font-size-4); + font-weight: bold; + line-height: 1.8rem; + margin: var(--size-4) 0 var(--size-2); +} + +h3 { + font-size: var(--font-size-2); + margin: var(--size-3) 0; +} + +p, +em, +a, +strong, +code, +li { + font-size: var(--font-size-1); +} + +p { + margin: var(--size-4) 0 var(--size-3); + line-height: var(--font-lineheight-2); +} + +img { + width: 100%; + margin: var(--size-2) 0; +} + +ul { + padding-left: var(--size-4); +} + +li { + padding: var(--size-1); +} + +blockquote { + padding: var(--size-1); + background-color: var(--color-accent) !important; + border-left: var(--size-1) solid var(--color-secondary); + font-style: italic; +} + +.publishDate { + font-size: var(--font-size-1); + display: block; + font-style: italic; +} + +@media (max-width: 760px) { + code[class*="language-"], + pre[class*="language-"], + .token { + font-size: 0.9rem; + } +} + +@media (min-width: 768px) { + .content-outlet-container { + max-width: 60ch; + margin: var(--size-2) auto var(--size-4); + } + + blockquote { + margin: var(--size-1); + padding: var(--size-2); + } +} From ad9e8c90a0bf8f6a2717ea3f2d7fb85334bfc835 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Mon, 26 Aug 2024 16:25:00 -0400 Subject: [PATCH 02/22] link headings for blog posts --- greenwood.config.js | 3 +- package-lock.json | 276 +++++++++++++++++++++++++++++------------- package.json | 3 + src/assets/link.svg | 10 ++ src/layouts/blog.html | 12 +- src/styles/blog.css | 30 +++-- 6 files changed, 233 insertions(+), 101 deletions(-) create mode 100644 src/assets/link.svg diff --git a/greenwood.config.js b/greenwood.config.js index 4b8ee172..6b74ea7a 100644 --- a/greenwood.config.js +++ b/greenwood.config.js @@ -4,7 +4,8 @@ import { greenwoodPluginCssModules } from "./plugin-css-modules.js"; export default { prerender: true, plugins: [greenwoodPluginCssModules(), greenwoodPluginImportRaw()], + // activeFrontmatter: true, markdown: { - plugins: ["@mapbox/rehype-prism"], + plugins: ["@mapbox/rehype-prism", "rehype-slug", "rehype-autolink-headings", "remark-github"], }, }; diff --git a/package-lock.json b/package-lock.json index 994f43a7..70c14a8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,9 @@ "lit": "^3.1.2", "patch-package": "^8.0.0", "prettier": "^3.2.5", + "rehype-autolink-headings": "^4.0.0", + "rehype-slug": "^3.0.0", + "remark-github": "^10.0.1", "rimraf": "^5.0.5", "storybook": "^8.0.6", "stylelint": "^16.4.0", @@ -2390,16 +2393,6 @@ "node": ">=10" } }, - "node_modules/@mapbox/rehype-prism/node_modules/hast-util-to-string": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-1.0.4.tgz", - "integrity": "sha512-eK0MxRX47AV2eZ+Lyr18DCpQgodvaS3fAQO2+b9Two9F5HEoRPhiUMNzoXArMJfZi2yieFzUBMRl3HNJ3Jus3w==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/@mdx-js/react": { "version": "3.0.1", "dev": true, @@ -3053,6 +3046,99 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/@storybook/addon-docs/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@storybook/addon-docs/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true + }, + "node_modules/@storybook/addon-docs/node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "dev": true + }, + "node_modules/@storybook/addon-docs/node_modules/hast-util-to-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.0.tgz", + "integrity": "sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-docs/node_modules/rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-docs/node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-docs/node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-docs/node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/@storybook/addon-essentials": { "version": "8.0.6", "dev": true, @@ -8694,9 +8780,10 @@ } }, "node_modules/github-slugger": { - "version": "2.0.0", - "dev": true, - "license": "ISC" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", + "dev": true }, "node_modules/glob": { "version": "7.2.3", @@ -9002,10 +9089,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-has-property": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-1.0.4.tgz", + "integrity": "sha512-ghHup2voGfgFoHMGnaLHOjbYFACKrRh9KFttdCzMCbFoBMJXiNi2+XTrPP8+q6cDJM/RSqlCfVWrjp1H201rZg==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-heading-rank": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", "dev": true, - "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" }, @@ -9016,8 +9114,9 @@ }, "node_modules/hast-util-heading-rank/node_modules/@types/hast": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/unist": "*" } @@ -9100,25 +9199,15 @@ } }, "node_modules/hast-util-to-string": { - "version": "3.0.0", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-1.0.4.tgz", + "integrity": "sha512-eK0MxRX47AV2eZ+Lyr18DCpQgodvaS3fAQO2+b9Two9F5HEoRPhiUMNzoXArMJfZi2yieFzUBMRl3HNJ3Jus3w==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-to-string/node_modules/@types/hast": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, "node_modules/hast-util-whitespace": { "version": "1.0.4", "dev": true, @@ -11404,6 +11493,33 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-find-and-replace": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-1.1.1.tgz", + "integrity": "sha512-9cKl33Y21lyckGzpSmEQnIDjEfeeWelN5s1kUW1LwdB0Fkuq2u+4GdqcGEygYxJE8GVqCl0741bYXHgamfWAZA==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^4.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mdast-util-to-hast": { "version": "9.1.2", "dev": true, @@ -11423,6 +11539,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-to-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz", + "integrity": "sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "dev": true, @@ -13428,6 +13554,22 @@ "jsesc": "bin/jsesc" } }, + "node_modules/rehype-autolink-headings": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-4.0.0.tgz", + "integrity": "sha512-2lglJ+4S3A4RCz+zlKVWj1wHvwO4bjunAoEOgMfjphT59EVXwdMiJzrL/A2fuAX/33k/LhkGW6BEK1Cl1I5WQw==", + "dev": true, + "dependencies": { + "extend": "^3.0.1", + "hast-util-has-property": "^1.0.0", + "hast-util-is-element": "^1.0.0", + "unist-util-visit": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-external-links": { "version": "3.0.0", "dev": true, @@ -13531,91 +13673,55 @@ } }, "node_modules/rehype-slug": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "github-slugger": "^2.0.0", - "hast-util-heading-rank": "^3.0.0", - "hast-util-to-string": "^3.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-slug/node_modules/@types/hast": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/rehype-slug/node_modules/@types/unist": { - "version": "3.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/rehype-slug/node_modules/unist-util-is": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-slug/node_modules/unist-util-visit": { - "version": "5.0.0", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-3.0.0.tgz", + "integrity": "sha512-zFnj5BCEJXV6+URwaz8yW+9BdjDwO5iVzlQui3+7cCJ9MXlIEL0IY8VefcT/03Gw+2Hutdrx+zXnS7bnOrepZg==", "dev": true, - "license": "MIT", "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" + "github-slugger": "^1.1.1", + "hast-util-has-property": "^1.0.0", + "hast-util-is-element": "^1.0.0", + "hast-util-to-string": "^1.0.0", + "unist-util-visit": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/rehype-slug/node_modules/unist-util-visit-parents": { - "version": "6.0.1", + "node_modules/rehype-stringify": { + "version": "8.0.0", "dev": true, "license": "MIT", "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" + "hast-util-to-html": "^7.1.1" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/rehype-stringify": { - "version": "8.0.0", + "node_modules/remark-frontmatter": { + "version": "2.0.0", "dev": true, "license": "MIT", "dependencies": { - "hast-util-to-html": "^7.1.1" + "fault": "^1.0.1" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/remark-frontmatter": { - "version": "2.0.0", + "node_modules/remark-github": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/remark-github/-/remark-github-10.1.0.tgz", + "integrity": "sha512-q0BTFb41N6/uXQVkxRwLRTFRfLFPYP+8li26Js5XC0GKritCSaxrftd+t+8sfN+1i9BtmJPUKoS7CZwtccj0Fg==", "dev": true, - "license": "MIT", "dependencies": { - "fault": "^1.0.1" + "mdast-util-find-and-replace": "^1.0.0", + "mdast-util-to-string": "^1.0.0", + "unist-util-visit": "^2.0.0" }, "funding": { "type": "opencollective", diff --git a/package.json b/package.json index a856151c..ab0377a6 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,9 @@ "lit": "^3.1.2", "patch-package": "^8.0.0", "prettier": "^3.2.5", + "rehype-autolink-headings": "^4.0.0", + "rehype-slug": "^3.0.0", + "remark-github": "^10.0.1", "rimraf": "^5.0.5", "storybook": "^8.0.6", "stylelint": "^16.4.0", diff --git a/src/assets/link.svg b/src/assets/link.svg new file mode 100644 index 00000000..85d02e36 --- /dev/null +++ b/src/assets/link.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/layouts/blog.html b/src/layouts/blog.html index 74f3b8bd..61089c31 100644 --- a/src/layouts/blog.html +++ b/src/layouts/blog.html @@ -1,17 +1,13 @@ - + - - - + + -
      - - - \ No newline at end of file + diff --git a/src/styles/blog.css b/src/styles/blog.css index b4226bc2..6f5d3215 100644 --- a/src/styles/blog.css +++ b/src/styles/blog.css @@ -60,20 +60,36 @@ blockquote { font-style: italic; } -@media (max-width: 760px) { - code[class*="language-"], - pre[class*="language-"], - .token { - font-size: 0.9rem; - } +h2 > a > span.icon { + width: var(--size-4); + height: var(--size-4); + padding: var(--size-2); + display: inline-block; + background-image: url(/assets/link.svg); + background-size: var(--size-3) var(--size-3); + background-repeat: no-repeat; + background-position-y: center; + vertical-align: text-bottom; +} + +code[class*="language-"], +pre[class*="language-"], +.token { + font-size: 0.9rem; } @media (min-width: 768px) { .content-outlet-container { - max-width: 60ch; + max-width: 65ch; margin: var(--size-2) auto var(--size-4); } + code[class*="language-"], + pre[class*="language-"], + .token { + font-size: 1rem; + } + blockquote { margin: var(--size-1); padding: var(--size-2); From ac38db1d9aa74a813de8833ba177b03b772313de Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Mon, 26 Aug 2024 16:26:27 -0400 Subject: [PATCH 03/22] active frontmatter title content --- src/layouts/app.html | 1 + src/layouts/blog.html | 1 + src/pages/blog/index.html | 2 ++ 3 files changed, 4 insertions(+) diff --git a/src/layouts/app.html b/src/layouts/app.html index ac2f860a..5528b35d 100644 --- a/src/layouts/app.html +++ b/src/layouts/app.html @@ -2,6 +2,7 @@ Greenwood + + Greenwood - ${globalThis.page.title} diff --git a/src/pages/blog/index.html b/src/pages/blog/index.html index cea34f51..34570de1 100644 --- a/src/pages/blog/index.html +++ b/src/pages/blog/index.html @@ -1,5 +1,7 @@ + Greenwood - ${globalThis.page.title} +
      -

      News and Announcements

      +

      News and
      Announcements

      Get the latest updates and information about Greenwood releases and activities.

      diff --git a/src/pages/blog/release/v0-15-0.md b/src/pages/blog/release/v0-15-0.md index 686505f3..ae5c5882 100644 --- a/src/pages/blog/release/v0-15-0.md +++ b/src/pages/blog/release/v0-15-0.md @@ -1,5 +1,5 @@ --- -title: v0.15.0 Release +title: Release - v0.15.0 abstract: Introducing our newest feature, Theme Packs. It's like CSS Zen Garden but as a plugin. published: 2021-08-06 layout: blog diff --git a/src/pages/blog/release/v0-18-0.md b/src/pages/blog/release/v0-18-0.md index 1f8ffca5..3d5155a3 100644 --- a/src/pages/blog/release/v0-18-0.md +++ b/src/pages/blog/release/v0-18-0.md @@ -1,5 +1,5 @@ --- -title: v0.18.0 Release +title: Release - v0.18.0 abstract: Let's review the latest enhancements now available in Greenwood. published: 2021-10-22 layout: blog diff --git a/src/pages/blog/release/v0-19-0.md b/src/pages/blog/release/v0-19-0.md index 65eed7da..07f3b1a5 100644 --- a/src/pages/blog/release/v0-19-0.md +++ b/src/pages/blog/release/v0-19-0.md @@ -1,5 +1,5 @@ --- -title: v0.19.0 Release +title: Release - v0.19.0 abstract: This release includes a new scaffolding tool for starting your next Greenwood project. published: 2021-11-11 layout: blog diff --git a/src/pages/blog/release/v0-20-0.md b/src/pages/blog/release/v0-20-0.md index d66c4f4e..781b5262 100644 --- a/src/pages/blog/release/v0-20-0.md +++ b/src/pages/blog/release/v0-20-0.md @@ -1,5 +1,5 @@ --- -title: v0.20.0 Release +title: Release - v0.20.0 abstract: We've migrated to ESM! Let's discuss our journey there. published: 2021-12-18 coverImage: /assets/blog/nodejs.png diff --git a/src/pages/blog/release/v0-21-0.md b/src/pages/blog/release/v0-21-0.md index bce6897a..b6e63432 100644 --- a/src/pages/blog/release/v0-21-0.md +++ b/src/pages/blog/release/v0-21-0.md @@ -1,5 +1,5 @@ --- -title: v0.21.0 Release +title: Release - v0.21.0 abstract: External data sources are now supported in Greenwood and template support to our new project scaffolding CLI. published: 2022-01-08 layout: blog diff --git a/src/pages/blog/release/v0-23-0.md b/src/pages/blog/release/v0-23-0.md index 044af193..edc677c0 100644 --- a/src/pages/blog/release/v0-23-0.md +++ b/src/pages/blog/release/v0-23-0.md @@ -1,5 +1,5 @@ --- -title: v0.23.0 Release +title: Release - v0.23.0 abstract: Server-Side Rendering is now available in Greenwood, so let's talk all about it. published: 2022-02-11 layout: blog diff --git a/src/pages/blog/release/v0-24-0.md b/src/pages/blog/release/v0-24-0.md index 20a17e5f..d315e509 100644 --- a/src/pages/blog/release/v0-24-0.md +++ b/src/pages/blog/release/v0-24-0.md @@ -1,5 +1,5 @@ --- -title: v0.24.0 Release +title: Release - v0.24.0 abstract: This release brings some exciting new features for optimizing the local development experience with Greenwood. published: 2022-03-06 layout: blog diff --git a/src/pages/blog/release/v0-26-0.md b/src/pages/blog/release/v0-26-0.md index 77c984b9..bcb24c06 100644 --- a/src/pages/blog/release/v0-26-0.md +++ b/src/pages/blog/release/v0-26-0.md @@ -1,5 +1,5 @@ --- -title: v0.26.0 Release +title: Release - v0.26.0 abstract: As a follow up to our SSR release, in this release we are excited to share our progress and introduce to you WCC! published: 2022-07-26 coverImage: /assets/blog/wcc-logo.png diff --git a/src/pages/blog/release/v0-27-0.md b/src/pages/blog/release/v0-27-0.md index 90b8b55b..39e07d27 100644 --- a/src/pages/blog/release/v0-27-0.md +++ b/src/pages/blog/release/v0-27-0.md @@ -1,5 +1,5 @@ --- -title: v0.27.0 Release +title: Release - v0.27.0 abstract: This release introduces the concept of full-stack Web Components to Greenwood. Let us share with you what we mean. published: 2022-11-23 coverImage: /assets/blog/full-stack-web-components.webp diff --git a/src/pages/blog/release/v0-28-0.md b/src/pages/blog/release/v0-28-0.md index 0f993473..3efe7d3e 100644 --- a/src/pages/blog/release/v0-28-0.md +++ b/src/pages/blog/release/v0-28-0.md @@ -1,5 +1,5 @@ --- -title: v0.28.0 Release +title: Release - v0.28.0 abstract: This is a big one, as we introduce API Routes to Greenwood and update to Node 18. published: 2023-04-08 coverImage: /assets/blog/nodejs.png diff --git a/src/pages/blog/release/v0-29-0.md b/src/pages/blog/release/v0-29-0.md index 5e662dc3..98c34508 100644 --- a/src/pages/blog/release/v0-29-0.md +++ b/src/pages/blog/release/v0-29-0.md @@ -1,5 +1,5 @@ --- -title: v0.29.0 Release +title: Release - v0.29.0 abstract: Go beyond the server and go serverless with this latest Greenwood release. published: 2023-11-08 coverImage: /assets/blog/serverless.webp diff --git a/src/stories/Styleguide.mdx b/src/stories/Styleguide.mdx index 5bc3a716..7189a130 100644 --- a/src/stories/Styleguide.mdx +++ b/src/stories/Styleguide.mdx @@ -68,6 +68,11 @@ import { Meta } from "@storybook/addon-docs"; color: var(--color-primary); } + .typography-primary { + font-family: var(--font-primary-bold); + color: var(--color-primary); + } + .typography-secondary { font-family: var(--font-secondary); color: var(--color-primary); @@ -139,6 +144,12 @@ This is the primary font for the website. The quick brown fox jumped over the lazy dog. +### `--font-primary-bold` + +This is the primary font for the website for bolded text. + +The quick brown fox jumped over the lazy dog. + ### `--font-secondary` This is the font intended to be used for code samples. diff --git a/src/styles/blog.css b/src/styles/blog.css index b2e781a4..30c78bae 100644 --- a/src/styles/blog.css +++ b/src/styles/blog.css @@ -10,8 +10,8 @@ } .page-content h2 { + font-family: var(--font-family-bold); font-size: var(--font-size-4); - font-weight: bold; line-height: 1.8rem; margin: var(--size-4) 0 var(--size-2); } diff --git a/src/styles/theme.css b/src/styles/theme.css index 2b44c998..ecaf575a 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -15,6 +15,11 @@ format("truetype"); } +@font-face { + font-family: "Geist-Sans Bold"; + src: url("../../node_modules/geist/dist/fonts/geist-sans/Geist-Bold.woff2") format("truetype"); +} + :root, :host { --color-primary: #016341; @@ -31,6 +36,7 @@ --color-border: #bababa; --font-primary: "Geist-Sans", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + --font-primary-bold: "Geist-Sans Bold"; --font-secondary: "Geist-Mono", monospace; } From 6d446455a62a2cf677528a3baab230c4d96ca9af Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Mon, 2 Sep 2024 13:39:13 -0400 Subject: [PATCH 20/22] formatting --- src/pages/blog/index.html | 6 ++++-- src/stories/Styleguide.mdx | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pages/blog/index.html b/src/pages/blog/index.html index 0cc2bc39..48e2cecc 100644 --- a/src/pages/blog/index.html +++ b/src/pages/blog/index.html @@ -28,13 +28,15 @@ font-size: var(--font-size-8); } } -
      -

      News and
      Announcements

      +

      + News and
      + Announcements +

      Get the latest updates and information about Greenwood releases and activities.

      diff --git a/src/stories/Styleguide.mdx b/src/stories/Styleguide.mdx index 7189a130..2aa95165 100644 --- a/src/stories/Styleguide.mdx +++ b/src/stories/Styleguide.mdx @@ -148,7 +148,9 @@ This is the primary font for the website. This is the primary font for the website for bolded text. -The quick brown fox jumped over the lazy dog. + + The quick brown fox jumped over the lazy dog. + ### `--font-secondary` From 8e31d3a11b8c074ce4c12a149b92129f0496ba9a Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Mon, 2 Sep 2024 14:20:40 -0400 Subject: [PATCH 21/22] auto link headings styles --- greenwood.config.js | 2 ++ src/assets/link.svg | 9 +++------ src/styles/theme.css | 14 ++++++++++++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/greenwood.config.js b/greenwood.config.js index 1396d0a8..1265b1fb 100644 --- a/greenwood.config.js +++ b/greenwood.config.js @@ -5,6 +5,8 @@ export default { prerender: true, plugins: [greenwoodPluginCssModules(), greenwoodPluginImportRaw()], activeFrontmatter: true, + // would be nice if we could customize these plugins, like appending the autolink headings + // https://github.com/ProjectEvergreen/greenwood/issues/1247 markdown: { plugins: ["@mapbox/rehype-prism", "rehype-slug", "rehype-autolink-headings", "remark-github"], }, diff --git a/src/assets/link.svg b/src/assets/link.svg index 85d02e36..4864650a 100644 --- a/src/assets/link.svg +++ b/src/assets/link.svg @@ -1,10 +1,7 @@ - + - - - - - + + \ No newline at end of file diff --git a/src/styles/theme.css b/src/styles/theme.css index ecaf575a..ce662b22 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -77,14 +77,24 @@ app-header { margin: 10px 0 0; } -h2 > a > span.icon { +/* tweaks for rehype-autolink-headings markdown plugin */ +h2 > a > span.icon, +h3 > a > span.icon, +h4 > a > span.icon { + display: inline-block; width: var(--size-4); height: var(--size-4); padding: var(--size-2); - display: inline-block; background-image: url("../assets/link.svg"); background-size: var(--size-3) var(--size-3); background-repeat: no-repeat; background-position-y: center; vertical-align: text-bottom; + opacity: 0.5; +} + +h2:hover > a > span.icon, +h3:hover > a > span.icon, +h4:hover > a > span.icon { + opacity: 1; } From 0e8c2d8ef446a4f8cacb348a1b0d8df0cd6c8004 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Mon, 2 Sep 2024 17:03:43 -0400 Subject: [PATCH 22/22] patching full compilation support for custom loaders --- patches/@greenwood+cli+0.30.0-alpha.5.patch | 57 +++++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/patches/@greenwood+cli+0.30.0-alpha.5.patch b/patches/@greenwood+cli+0.30.0-alpha.5.patch index d5fe58c4..a6f47285 100644 --- a/patches/@greenwood+cli+0.30.0-alpha.5.patch +++ b/patches/@greenwood+cli+0.30.0-alpha.5.patch @@ -209,6 +209,29 @@ index 9c963f7..d5f1a50 100644 staticHtml = await getUserScripts(staticHtml, compilation); staticHtml = await (await interceptPage(new URL(`http://localhost:8080${route}`), new Request(new URL(`http://localhost:8080${route}`)), getPluginInstances(compilation), staticHtml)).text(); +diff --git a/node_modules/@greenwood/cli/src/lifecycles/compile.js b/node_modules/@greenwood/cli/src/lifecycles/compile.js +index fb48336..68ce2d0 100644 +--- a/node_modules/@greenwood/cli/src/lifecycles/compile.js ++++ b/node_modules/@greenwood/cli/src/lifecycles/compile.js +@@ -1,8 +1,8 @@ + import { checkResourceExists } from '../lib/resource-utils.js'; + import { generateGraph } from './graph.js'; + import { initContext } from './context.js'; ++import { readAndMergeConfig } from './config.js'; + import fs from 'fs/promises'; +-import { readAndMergeConfig as initConfig } from './config.js'; + + const generateCompilation = () => { + return new Promise(async (resolve, reject) => { +@@ -20,7 +20,7 @@ const generateCompilation = () => { + }; + + console.info('Initializing project config'); +- compilation.config = await initConfig(); ++ compilation.config = await readAndMergeConfig(); + + // determine whether to use default layout or user detected workspace + console.info('Initializing project workspace contexts'); diff --git a/node_modules/@greenwood/cli/src/lifecycles/config.js b/node_modules/@greenwood/cli/src/lifecycles/config.js index 7ee7fc2..9aa3592 100644 --- a/node_modules/@greenwood/cli/src/lifecycles/config.js @@ -419,22 +442,36 @@ index a1d16e5..1a8c659 100644 ]; } diff --git a/node_modules/@greenwood/cli/src/loader.js b/node_modules/@greenwood/cli/src/loader.js -index 792f43c..ca3c8b7 100644 +index 792f43c..fd80928 100644 --- a/node_modules/@greenwood/cli/src/loader.js +++ b/node_modules/@greenwood/cli/src/loader.js -@@ -7,9 +7,10 @@ const resourcePlugins = config.plugins +@@ -1,19 +1,16 @@ +-import { readAndMergeConfig as initConfig } from './lifecycles/config.js'; ++import { readAndMergeConfig } from './lifecycles/config.js'; ++import { initContext } from './lifecycles/context.js'; + import { mergeResponse } from './lib/resource-utils.js'; + +-const config = await initConfig(); ++const config = await readAndMergeConfig(); ++const context = await initContext({ config }); ++ + const resourcePlugins = config.plugins + .filter(plugin => plugin.type === 'resource') .filter(plugin => plugin.name !== 'plugin-node-modules:resource' && plugin.name !== 'plugin-user-workspace') .map(plugin => plugin.provider({ - context: { +- context: { - outputDir: new URL(`file://${process.cwd()}/public`), -+ outputDir: new URL(`file://${process.cwd()}/public/`), - projectDirectory: new URL(`file://${process.cwd()}/`), +- projectDirectory: new URL(`file://${process.cwd()}/`), - scratchDir: new URL(`file://${process.cwd()}/.greenwood/`) -+ scratchDir: new URL(`file://${process.cwd()}/.greenwood/`), -+ userWorkspace: new URL(`file://${process.cwd()}/src/`) // TODO hmm, we can't hardcode this... - }, - config: { - devServer: {} +- }, +- config: { +- devServer: {} +- }, ++ context, ++ config, + graph: [] + })); + diff --git a/node_modules/@greenwood/cli/src/plugins/resource/plugin-content-as-data.js b/node_modules/@greenwood/cli/src/plugins/resource/plugin-content-as-data.js new file mode 100644 index 0000000..5d299de