master dfb28087ad3c cached
5 files
11.0 KB
3.1k tokens
15 symbols
1 requests
Download .txt
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
================================================
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width,initial-scale=1">
	<title>Google Sheets CMS</title>
	<link rel="stylesheet" href="css/normalize.min.css">
	<link rel="stylesheet" href="css/main.css">
</head>
<body>
	<header>
		<img src="img/google-sheets-logo.png" alt="logo for google sheets">
		<h1>Generic Blog</h1>
	</header>
	<main>
		<div class="information">
			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 <a href="https://medium.com/p/c2eab3fb0b2b">Medium article</a>. The blog is designed as a single page application with pagination and category filtering.
		</div>
		<nav id="filter"></nav>
		<div id="container"></div>
		<div id="notice"></div>
	</main>
	<footer>
		<p>These blog posts are from a Google Sheets backend</p>
		<a href="https://medium.com/p/c2eab3fb0b2b">Read the article on Medium</a>
	</footer>
	<script src="js/main.js"></script>
	<script>
		document.addEventListener('DOMContentLoaded', app.init);
	</script>
</body>
</html>


================================================
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 = `
				<h2>${post.title}</h2>
				<div class="article-details">
					<div>By ${post.author} on ${_formatDate(post.timestamp)}</div>
					<div>Posted in ${post.category}</div>
				</div>
				${_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) => `<p>${str}</p>`)
			.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
 	};
}();
Download .txt
gitextract_u0d0v3i2/

├── LICENSE
├── README.md
└── src/
    ├── css/
    │   └── main.css
    ├── index.html
    └── js/
        └── main.js
Download .txt
SYMBOL INDEX (15 symbols across 1 files)

FILE: src/js/main.js
  function init (line 9) | function init () {
  function _getNewPosts (line 18) | function _getNewPosts () {
  function _getPosts (line 23) | function _getPosts () {
  function _buildFilter (line 41) | function _buildFilter () {
  function _buildFilterLink (line 49) | function _buildFilterLink (label, isSelected) {
  function _buildApiUrl (line 64) | function _buildApiUrl (page, category) {
  function _setNotice (line 73) | function _setNotice (label) {
  function _renderPosts (line 77) | function _renderPosts (posts) {
  function _renderPostsPagination (line 92) | function _renderPostsPagination (pages) {
  function _formatDate (line 108) | function _formatDate (string) {
  function _formatContent (line 112) | function _formatContent (string) {
  function _capitalize (line 119) | function _capitalize (label) {
  function _resetActivePage (line 123) | function _resetActivePage () {
  function _incrementActivePage (line 127) | function _incrementActivePage () {
  function _setActiveCategory (line 131) | function _setActiveCategory (category) {
Condensed preview — 5 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (12K chars).
[
  {
    "path": "LICENSE",
    "chars": 1070,
    "preview": "MIT License\n\nCopyright (c) 2017 Daniel Ireson\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
  },
  {
    "path": "README.md",
    "chars": 4012,
    "preview": "# Google Sheets CMS\nExample of a blog powered by Google Sheets, Google Forms and Google Apps Scripts. New posts are adde"
  },
  {
    "path": "src/css/main.css",
    "chars": 1586,
    "preview": "html, body {\n\tbackground: #f8f8f8;\n\tcolor: #2d3436;\n\tfont-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n\tfont-"
  },
  {
    "path": "src/index.html",
    "chars": 1113,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"width=device-width,initi"
  },
  {
    "path": "src/js/main.js",
    "chars": 3522,
    "preview": "const app = function () {\n\tconst API_BASE = 'https://script.google.com/macros/s/AKfycbyP5Rifn7Q05Qcd7CTfm-AOouFHHvUAvCVV"
  }
]

About this extraction

This page contains the full source code of the danielireson/google-sheets-blog-cms GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 5 files (11.0 KB), approximately 3.1k tokens, and a symbol index with 15 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!