Repository: danielireson/google-sheets-blog-cms Branch: master Commit: dfb28087ad3c Files: 5 Total size: 11.0 KB Directory structure: gitextract_u0d0v3i2/ ├── LICENSE ├── README.md └── src/ ├── css/ │ └── main.css ├── index.html └── js/ └── main.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Daniel Ireson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Google Sheets CMS Example of a blog powered by Google Sheets, Google Forms and Google Apps Scripts. New posts are added via a Google Forms interface and stored in a Google Sheets spreadsheet. Google Apps Script is used to create an API to make the content accessible in an easy to use format. The blog is designed as a single page application with pagination and category filtering. ![Blog screenshot](readme-screenshot.png) ## Google Apps Script API Google Apps Script is a platform to extend Google’s G Suite of online products through a scripting language derived from JavaScript. It’s analogous to VBA, which is built into the majority of Microsoft Office products. To get started with Google Apps Script you can access the online editor by going to *Tools > Script Editor* in the menu bar from a Google Sheets spreadsheet. A script can then be made publicly available by going to *Publish > Deploy as webapp* from the script editor menu bar. Ensure the app is being executed as *me* and that *anyone, even anonymous* has access. ``` var API_KEY = ''; var SPREADSHEET_ID = ''; var RESULTS_PER_PAGE = 5; function doGet(e) { if (!isAuthorized(e)) { return buildErrorResponse('not authorized'); } var options = { page: getPageParam(e), category: getCategoryParam(e) } var spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID); var worksheet = spreadsheet.getSheets()[0]; var rows = worksheet.getDataRange().sort({column: 2, ascending: false}).getValues(); var headings = rows[0].map(String.toLowerCase); var posts = rows.slice(1); var postsWithHeadings = addHeadings(posts, headings); var postsPublic = removeDrafts(postsWithHeadings); var postsFiltered = filter(postsPublic, options.category); var paginated = paginate(postsFiltered, options.page); return buildSuccessResponse(paginated.posts, paginated.pages); } function addHeadings(posts, headings) { return posts.map(function(postAsArray) { var postAsObj = {}; headings.forEach(function(heading, i) { postAsObj[heading] = postAsArray[i]; }); return postAsObj; }); } function removeDrafts(posts, category) { return posts.filter(function(post) { return post['published'] === true; }); } function filter(posts, category) { return posts.filter(function(post) { if (category !== null) { return post['category'].toLowerCase() === category.toLowerCase(); } else { return true; } }); } function paginate(posts, page) { var postsCopy = posts.slice(); var postsChunked = []; var postsPaginated = { posts: [], pages: { previous: null, next: null } }; while (postsCopy.length > 0) { postsChunked.push(postsCopy.splice(0, RESULTS_PER_PAGE)); } if (page - 1 in postsChunked) { postsPaginated.posts = postsChunked[page - 1]; } else { postsPaginated.posts = []; } if (page > 1 && page <= postsChunked.length) { postsPaginated.pages.previous = page - 1; } if (page >= 1 && page < postsChunked.length) { postsPaginated.pages.next = page + 1; } return postsPaginated; } function isAuthorized(e) { return 'key' in e.parameters && e.parameters.key[0] === API_KEY; } function getPageParam(e) { if ('page' in e.parameters) { var page = parseInt(e.parameters['page'][0]); if (!isNaN(page) && page > 0) { return page; } } return 1 } function getCategoryParam(e) { if ('category' in e.parameters) { return e.parameters['category'][0]; } return null } function buildSuccessResponse(posts, pages) { var output = JSON.stringify({ status: 'success', data: posts, pages: pages }); return ContentService.createTextOutput(output).setMimeType(ContentService.MimeType.JSON); } function buildErrorResponse(message) { var output = JSON.stringify({ status: 'error', message: message }); return ContentService.createTextOutput(output).setMimeType(ContentService.MimeType.JSON); } ``` ================================================ FILE: src/css/main.css ================================================ html, body { background: #f8f8f8; color: #2d3436; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: normal; line-height: 1.5; } a { color: #1a1a1a; border-bottom: solid 1px #b2bec3; text-decoration: none; } a:hover { border-color: #1a1a1a; } img { max-width: 100%; } button { border-radius: 5px; border: solid 1px #b2bec3; cursor: pointer; display: inline-block; padding: 0.2em 0.5em; } button:hover { background: #dfe6e9; border-color: #b2bec3; } button.selected { border-color: #2d3436; } header, main, footer { padding: 4em 5%; } header { background: #2d3436; color: white; text-align: center; } header h1 { margin-bottom: 0; } .information { background: #dfe6e9; margin-bottom: 4em; padding: 2em; } nav { margin-bottom: 4em; } nav span { display: block; font-size: 0.9em; margin-bottom: 1em; } nav button { margin: 0 0.5em 1em 0; } article { margin-bottom: 6em; } .article-details { border-bottom: solid 2px #dfe6e9; font-size: 0.9em; padding-bottom: 1em; } .article-details div:first-child { margin-bottom: 0.5em; } #notice { font-weight: bold; text-align: center; } footer { background: #dfe6e9; text-align: center; } footer p { margin-top: 0; } @media only screen and (min-width : 992px) { .article-details { align-items: center; display: flex; justify-content: space-between; } .article-details div:first-child { margin-bottom: 0; } header, main, footer { padding: 4em 25%; } } @media only screen and (min-width : 1200px) { header, main, footer { padding: 4em 30%; } } ================================================ FILE: src/index.html ================================================ Google Sheets CMS
logo for google sheets

Generic Blog

This is an example of a frontend for a blog CMS built using Google Sheets, Google Forms and Google Apps Script which was published as a Medium article. The blog is designed as a single page application with pagination and category filtering.
================================================ FILE: src/js/main.js ================================================ const app = function () { const API_BASE = 'https://script.google.com/macros/s/AKfycbyP5Rifn7Q05Qcd7CTfm-AOouFHHvUAvCVVuKSfQu-LCqJocP8/exec'; const API_KEY = 'abcdef'; const CATEGORIES = ['general', 'financial', 'technology', 'marketing']; const state = {activePage: 1, activeCategory: null}; const page = {}; function init () { page.notice = document.getElementById('notice'); page.filter = document.getElementById('filter'); page.container = document.getElementById('container'); _buildFilter(); _getNewPosts(); } function _getNewPosts () { page.container.innerHTML = ''; _getPosts(); } function _getPosts () { _setNotice('Loading posts'); fetch(_buildApiUrl(state.activePage, state.activeCategory)) .then((response) => response.json()) .then((json) => { if (json.status !== 'success') { _setNotice(json.message); } _renderPosts(json.data); _renderPostsPagination(json.pages); }) .catch((error) => { _setNotice('Unexpected error loading posts'); }) } function _buildFilter () { page.filter.appendChild(_buildFilterLink('no filter', true)); CATEGORIES.forEach(function (category) { page.filter.appendChild(_buildFilterLink(category, false)); }); } function _buildFilterLink (label, isSelected) { const link = document.createElement('button'); link.innerHTML = _capitalize(label); link.classList = isSelected ? 'selected' : ''; link.onclick = function (event) { let category = label === 'no filter' ? null : label.toLowerCase(); _resetActivePage(); _setActiveCategory(category); _getNewPosts(); }; return link; } function _buildApiUrl (page, category) { let url = API_BASE; url += '?key=' + API_KEY; url += '&page=' + page; url += category !== null ? '&category=' + category : ''; return url; } function _setNotice (label) { page.notice.innerHTML = label; } function _renderPosts (posts) { posts.forEach(function (post) { const article = document.createElement('article'); article.innerHTML = `

${post.title}

By ${post.author} on ${_formatDate(post.timestamp)}
Posted in ${post.category}
${_formatContent(post.content)} `; page.container.appendChild(article); }); } function _renderPostsPagination (pages) { if (pages.next) { const link = document.createElement('button'); link.innerHTML = 'Load more posts'; link.onclick = function (event) { _incrementActivePage(); _getPosts(); }; page.notice.innerHTML = ''; page.notice.appendChild(link); } else { _setNotice('No more posts to display'); } } function _formatDate (string) { return new Date(string).toLocaleDateString('en-GB'); } function _formatContent (string) { return string.split('\n') .filter((str) => str !== '') .map((str) => `

${str}

`) .join(''); } function _capitalize (label) { return label.slice(0, 1).toUpperCase() + label.slice(1).toLowerCase(); } function _resetActivePage () { state.activePage = 1; } function _incrementActivePage () { state.activePage += 1; } function _setActiveCategory (category) { state.activeCategory = category; const label = category === null ? 'no filter' : category; Array.from(page.filter.children).forEach(function (element) { element.classList = label === element.innerHTML.toLowerCase() ? 'selected' : ''; }); } return { init: init }; }();